diff --git a/README.md b/README.md
index 602961d..d99305b 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ md2html --theme=onedark --hl=api input.md --out -
`viewmd` is `md2html`'s page plus a small viewer UI, always opened in a browser: a theme picker and light/dark toggle, a collapsible table of contents with scrollspy (responsive below a breakpoint, ☰ to pin it either way), fold triangles on headings (shift-click folds a section's subsections too), copy buttons on code blocks, and mermaid diagrams drawn in place. Frontmatter handling is on by default (`--no-frontmatter` for raw), `DASHES` typography (en/em dashes and ellipses) is applied to plain text, references default to `--refs=lenient`, and `--head` inlines extra `.css`/`.js` files into the page — `examples/sample.css` and `examples/sample.js` show it styling the sample's custom attributes.
-`viewmd` also renders Jupyter notebooks: pass a `.ipynb` file and each code cell appears as a highlighted `python` block with its stored outputs beneath it in a bordered `output` section — streams, results, and tracebacks as text, HTML display objects (DataFrames and the like) rendered live, and images inlined. Markdown cells are ordinary Markdown, so cross-references, footnotes, math, and frontmatter all work; `examples/nbsample.ipynb` is a small demo. Solveit dialogs render too: each prompt appears in a bordered `prompt` section with the AI reply indented beneath it in a `reply` section, and tool calls in replies (fastllm's fenced JSON blocks) shown as folded details with a code-span label.
+`viewmd` also renders Jupyter notebooks: pass a `.ipynb` file and each code cell appears as a highlighted `python` block with its stored outputs beneath it in a bordered `output` section — streams, results, and tracebacks as text, HTML display objects (DataFrames and the like) rendered live, and images inlined. Markdown cells are ordinary Markdown, so cross-references, footnotes, math, and frontmatter all work, and a notebook's frontmatter message (a raw cell) supplies `number_headings` the same way a document's block does; `examples/nbsample.ipynb` is a small demo. Solveit dialogs render too: each prompt appears in a bordered `prompt` section with the AI reply indented beneath it in a `reply` section, and tool calls in replies (fastllm's fenced JSON blocks) shown as folded details with a code-span label.
```bash
viewmd README.md
@@ -143,7 +143,7 @@ The same paragraph reflowing is available as `wrap_md(markdown, width=None)`. It
The result is a `str` subclass whose `warnings` list names any construct whose closer never arrived — an unclosed `:::` div, code fence, math block, raw HTML container, or comment — each with its opening line number. The render itself closes them at end of input, so a viewer can show the page and append the warnings after it. Both CLIs print them to stderr.
-The result's `meta` dict holds the document's frontmatter: a leading block of `key: value` lines between `---` fences, recognized by default (`frontmatter=False` turns it off), stripped from the content, and never parsed as YAML — values are plain strings. A document that opens with `---` but doesn't fit that shape — a heading or prose inside, no closing fence, no keys at all — is left untouched, so a leading thematic break still parses as one. `md2html --frontmatter` (and `viewmd`, where it is on by default) uses `meta` to title the page and prepend a small metadata table (`meta_table(meta)` builds it).
+The result's `meta` dict holds the document's frontmatter: a leading block of `key: value` lines between `---` fences, recognized by default (`frontmatter=False` turns it off), stripped from the content, and never parsed as YAML — values are plain strings. A document that opens with `---` but doesn't fit that shape — a heading or prose inside, no closing fence, no keys at all — is left untouched, so a leading thematic break still parses as one. `md2html --frontmatter` (and `viewmd`, where it is on by default) uses `meta` to title the page and prepend a small metadata table (`meta_table(meta)` builds it). One key is read by the exporters themselves: `number_headings: legal` (or `decimal`) selects the heading numbering scheme whenever a call gives none, so a document carries its own numbering into `mdhtml2html`, `md2gfm`, and `mdhtml2typst`.
### Markdown chunks
@@ -363,8 +363,8 @@ html = mdhtml2html(md2mdhtml(markdown), number_headings='legal')
The result is still a body fragment (a str subclass carrying a `warnings` list; pass `dest=` to also write a file). `mdhtml2html` accepts an MDHTML string or a fast5ever node, never mutates its input, and applies:
-- Cross-references become real links with baked text: `[@sec-pay]` renders as `Section 1.`, groups join as "Sections 1. and 1.(a)", and figure and table targets get "Figure 1"-style text. `reftypes=dict(exh=('Exhibit', 'Exhibits'))` adds prefix words beyond the built-in `sec`, `fig`, and `tbl`. A missing target, an unknown token, or an unknown type needing a prefix raises. The Word-only `page` and `rel` variants render as the full number. `refs='ids'` is the second mode, for live-preview contexts where targets may sit outside the fragment: each reference bakes as a working link showing its target id (`sec-pay`, author text kept as a prefix, variants ignored), with no registry, numbering, or failure modes; captions render as authored, since without a registry the numbers would restart per fragment. `refs='lenient'` is the third, for drafts: everything resolves and numbers as in `resolve` mode, except that each reference which cannot resolve bakes as its `ids` link and is reported in `warnings` instead of raising. `id_prefix='md-'` namespaces the output against the ids of a host page: every element id is prefixed (the original kept in `data-id`, e.g. for CSS `attr()` markers), along with ref hrefs and any link to an in-fragment id; links to outside ids are untouched. `fn_salt` adds a further prefix to footnote ids only (`fn-*`/`fnref-*`), keeping footnote pairs distinct across fragments that share one `id_prefix`.
-- Headings are numbered when `number_headings` is given ('legal', 'decimal', or a `{lvlText: numFmt}` dict as in mdhtml2docx), or automatically with 'decimal' when some reference needs a heading number. Numbers bake in as ``, and full-context reference text ("3.(c)(iii)") is computed Word-style from the scheme. Scheme level 0 is the h1 document title: its empty lvlText shows no number, and bumping it restarts every level below (Word's own rule), so `%2` is the h2 counter and a file holding several documents, each opening with an h1, numbers each of them from 1. A custom dict has the same shape, title entry first. A title cannot be cited by number; cite it with `{ref=text}`.
+- Cross-references become real links with baked text: `[@sec-pay]` renders as `Section 1.`, groups join as "Sections 1. and 1.(a)", and figure and table targets get "Figure 1"-style text. `reftypes=dict(exh=('Exhibit', 'Exhibits'))` adds prefix words beyond the built-in `sec`, `fig`, and `tbl`. A missing target, an unknown token, or an unknown type needing a prefix raises. The Word-only `page` and `rel` variants render as the full number. `refs='ids'` is the second mode, for live-preview contexts where targets may sit outside the fragment: each reference bakes as a working link showing its target id (`sec-pay`, author text kept as a prefix, variants ignored), with no registry or failure modes; captions render as authored, since without a registry the numbers would restart per fragment, and headings number only when a scheme is requested (never automatically). `refs='lenient'` is the third, for drafts: everything resolves and numbers as in `resolve` mode, except that each reference which cannot resolve bakes as its `ids` link and is reported in `warnings` instead of raising. `id_prefix='md-'` namespaces the output against the ids of a host page: every element id is prefixed (the original kept in `data-id`, e.g. for CSS `attr()` markers), along with ref hrefs and any link to an in-fragment id; links to outside ids are untouched. `fn_salt` adds a further prefix to footnote ids only (`fn-*`/`fnref-*`), keeping footnote pairs distinct across fragments that share one `id_prefix`.
+- Headings are numbered when `number_headings` is given ('legal', 'decimal', or a `{lvlText: numFmt}` dict as in mdhtml2docx), when the source's frontmatter names a scheme (`number_headings: legal`; the call argument wins when both are present), or automatically with 'decimal' when some reference needs a heading number. Numbers bake in as ``, and full-context reference text ("3.(c)(iii)") is computed Word-style from the scheme. Scheme level 0 is the h1 document title: its empty lvlText shows no number, and bumping it restarts every level below (Word's own rule), so `%2` is the h2 counter and a file holding several documents, each opening with an h1, numbers each of them from 1. A custom dict has the same shape, title entry first. A title cannot be cited by number; cite it with `{ref=text}`.
- Figures and tables number independently whenever refs resolve: a caption or an id earns a `Figure 1: ` in the `figcaption` or `caption`.
- `{=html}` raw data is decoded and spliced in place; raw data for other formats is removed. Malformed payloads are dropped with a warning.
- A `colwidths` attribute lowers to a ``; `fr` values share the width remaining after fixed lengths.
diff --git a/docs/DIALECT.md b/docs/DIALECT.md
index e7714df..af71e49 100644
--- a/docs/DIALECT.md
+++ b/docs/DIALECT.md
@@ -101,7 +101,7 @@ Automatic heading ids are an export concern, not part of the parse: `md2mdhtml`
Parse options which infer document structure are off by default. Explicit Markdown syntax remains enabled: for example, an explicit heading id is emitted without any option, and bracket math is recognized because its delimiters state the author's intent. `implicit_figures` enables an inferred transformation.
-With `frontmatter=True` (the default), a document opening with a `---` line, closed by a `---` or `...` line, whose every non-blank, non-comment line between is `key: value` (at least one), is document metadata rather than content: the parse strips it and returns the pairs as `meta`, with values taken verbatim — no YAML types, one matching pair of surrounding quotes removed. A leading block that doesn't fit this shape is content as usual, so a document starting with a thematic break renders one. Frontmatter never reaches the fragment; consumers decide its rendering (page title, a metadata table) from `meta`.
+With `frontmatter=True` (the default), a document opening with a `---` line, closed by a `---` or `...` line, whose every non-blank, non-comment line between is `key: value` (at least one), is document metadata rather than content: the parse strips it and returns the pairs as `meta`, with values taken verbatim — no YAML types, one matching pair of surrounding quotes removed. A leading block that doesn't fit this shape is content as usual, so a document starting with a thematic break renders one. Frontmatter never reaches the fragment; consumers decide its rendering (page title, a metadata table) from `meta`. One key has dialect meaning: `number_headings` names the heading numbering scheme (see Cross-references and numbering), which the exporters apply when a call gives none.
## Links, images, and figures
@@ -341,7 +341,7 @@ The parser does not resolve numbers or require targets to exist. Converters repo
Reference targets are the id-bearing headings, paragraphs, figures, tables, spans, and definition terms. Headings resolve to a heading number ("Section 1"), figures and tables to a caption number ("Table 2"), and paragraphs only through `{ref=text}`. Spans and definition terms resolve to their own text with no prefix word, so `the [@def-term] period` reads as running prose citing the defined term, and a rendering variant that needs a number (`leaf`, `rel`) is an error for them. Converters with a link mechanism link the cited text back to its definition site; `md2gfm` renders the text alone.
-The shipped exporters lower references from one shared vocabulary (`mdhtml.export`) at three levels of liveness: `mdhtml2docx` bakes REF fields Word keeps live, `mdhtml2html` bakes links with computed text, and `md2gfm` bakes plain text. Prefix words come from `REFTYPES` (`sec`, `fig`, `tbl`; extended per call with `reftypes=`) and heading numbering from `SCHEMES` (`'legal'`, `'decimal'`, or a `{lvlText: numFmt}` dict). `number_headings=None` means automatic: headings are numbered exactly when some reference needs a heading number. Scheme level 0 is the h1 document title, with an empty lvlText: it shows no number, and bumping it restarts every level below, so `%2` is the h2 counter and each document in a file that opens with its own h1 numbers from 1; a title is cited with `{ref=text}`, never by number.
+The shipped exporters lower references from one shared vocabulary (`mdhtml.export`) at three levels of liveness: `mdhtml2docx` bakes REF fields Word keeps live, `mdhtml2html` bakes links with computed text, and `md2gfm` bakes plain text. Prefix words come from `REFTYPES` (`sec`, `fig`, `tbl`; extended per call with `reftypes=`) and heading numbering from `SCHEMES` (`'legal'`, `'decimal'`, or a `{lvlText: numFmt}` dict). `number_headings=None` defers to the frontmatter's `number_headings:` when the source has one, else means automatic: headings are numbered exactly when some reference needs a heading number. Scheme level 0 is the h1 document title, with an empty lvlText: it shows no number, and bumping it restarts every level below, so `%2` is the h2 counter and each document in a file that opens with its own h1 numbers from 1; a title is cited with `{ref=text}`, never by number.
`mdhtml2html` also offers `refs='ids'` for live-preview contexts where targets may sit outside the fragment: each reference bakes as a working link showing its target id (class `xref`), with no registry, numbering, or failure modes - and captions render as authored, since per-fragment numbers would lie. `id_prefix` namespaces the fragment's ids against a host page (the authored id kept in `data-id`), and `fn_salt` adds a further prefix to the `fn-*`/`fnref-*` footnote namespace only, keeping footnote pairs distinct across fragments that share one `id_prefix`.
diff --git a/python/mdhtml/export.py b/python/mdhtml/export.py
index f2b0b71..fda7e49 100644
--- a/python/mdhtml/export.py
+++ b/python/mdhtml/export.py
@@ -39,6 +39,11 @@ def math_js(fn=None, **opts):
return _math_js(fn, "".join(f", {k}: {json.dumps(v)}" for k, v in opts.items()))
+def _headnums(src, number_headings):
+ "The call's `number_headings`, else the source's frontmatter `number_headings:` (an `Mdhtml` carries its `meta`)"
+ return number_headings if number_headings is not None else getattr(src, "meta", {}).get("number_headings")
+
+
def meta_table(meta):
"Frontmatter metadata (`md2mdhtml`'s `meta` dict) as a small ``, for prepending to rendered output"
rows = "".join(f"| {escape(k)} | {escape(v)} |
" for k, v in meta.items())
@@ -93,8 +98,9 @@ def mdhtml2html(src, dest=None, reftypes: dict | None = None, number_headings=No
`gh_ids=True` derives them by GitHub's rules instead (github-slugger's), so anchors match a
GitHub-rendered page and links written against one keep working.
`refs='ids'` instead bakes each reference as a working link showing its
- target id (class `xref`), with no registry, numbering, or failure modes - for live-preview
- contexts where targets may sit outside the fragment. `refs='lenient'` sits between the two:
+ target id (class `xref`), with no registry or failure modes - for live-preview
+ contexts where targets may sit outside the fragment; a given scheme still numbers the
+ headings there (per fragment), but nothing numbers automatically. `refs='lenient'` sits between the two:
references resolve and number as usual, and any that cannot resolve bake as `ids` links and
are reported in `.warnings` rather than raising - for drafts, where some targets are still
to be written. `id_prefix` namespaces the output's ids:
@@ -106,8 +112,11 @@ def mdhtml2html(src, dest=None, reftypes: dict | None = None, number_headings=No
may return replacement markup for the highlighted block (None keeps it; `text` is unescaped).
Highlighting comes from the optional fastpylight package (`pip install 'mdhtml[hl]'`);
without it, code blocks render plain and a warning reports it.
+ `number_headings=None` takes the scheme from the source's frontmatter `number_headings:` when
+ `src` is `md2mdhtml`'s result (its `meta` carries the block), else numbers automatically.
Returns an `Html` str carrying `.warnings`; `dest` also writes it to a file."""
if refs not in ("resolve", "ids", "lenient"): raise ValueError(f"unknown refs mode {refs!r}")
+ number_headings = _headnums(src, number_headings)
if not isinstance(src, str): src = src.to_html()
hl_fn = None if hl is None else _hl_fn(hl)
out, warnings = _export_html(src, reftypes, number_headings, hl, toc, refs, id_prefix, fn_salt, hl_lang, code_wrap, hl_fn, auto_ids, gh_ids)
diff --git a/python/mdhtml/md.py b/python/mdhtml/md.py
index b2f1e5f..7464cce 100644
--- a/python/mdhtml/md.py
+++ b/python/mdhtml/md.py
@@ -10,7 +10,8 @@
from hashlib import sha256
from dataclasses import astuple, is_dataclass
-from ._native import blocks as _blocks, edit_nodes as _edit_nodes, anchors as _anchors, trailing_attr_span as _trailing_attr_span
+from ._native import (blocks as _blocks, edit_nodes as _edit_nodes, anchors as _anchors, trailing_attr_span as _trailing_attr_span,
+ frontmatter_meta as _frontmatter_meta)
from .export import HeadingNums, Resolver, group_plan, ref_tokens, ref_variant
__all__ = ["md2gfm"]
@@ -245,8 +246,10 @@ def md2gfm(src, dest=None, reftypes: dict | None = None, number_headings=None, m
each template token is rewritten to whatever the
`tmpl` callable `(node) -> str` returns: the node dict carries `body`, `syntax`, `form`,
scanner classification (`kind`, `name`, `inverted`), and spans (`mustache_code` is a ready-made recipe;
- without `tmpl`, tokens pass through). All other source text is preserved byte-for-byte.
+ without `tmpl`, tokens pass through). All other source text is preserved byte-for-byte,
+ the frontmatter included; `number_headings=None` takes the scheme from its `number_headings:`.
Returns an `Md` str carrying `.warnings`; `dest` also writes it to a file."""
+ if number_headings is None: number_headings = dict(_frontmatter_meta(src)).get("number_headings")
normalized, offsets = _normalize_offsets(src)
imgbase = Path(dest).parent if dest is not None else Path(".")
ex = _GfmExporter(reftypes, number_headings, math, implicit_figures, templates, tmpl,
diff --git a/python/mdhtml/md2html.py b/python/mdhtml/md2html.py
index 9dcd587..543d46a 100644
--- a/python/mdhtml/md2html.py
+++ b/python/mdhtml/md2html.py
@@ -85,7 +85,7 @@ def main(
out: str = None, # Where to write: a path, `-` for stdout; omitted opens a browser, or writes to stdout when piped
fragment: bool = False, # Emit the body fragment alone, with no page shell
refs: RefsMode = RefsMode.ids, # Bake references as target ids, with numbering ('resolve'), or numbering that degrades to ids ('lenient')
- number_headings: NumMode = None, # Heading numbering scheme
+ number_headings: NumMode = None, # Heading numbering scheme (default: the frontmatter's `number_headings:`, else automatic)
toc: bool = False, # Prepend a table of contents
hl: HlMode = HlMode.spans, # Code highlighting: classed spans, the Highlight API, or off
theme: str = "vscode_light", # Code colors in light mode: any name from `fastpylight.themes()`
diff --git a/python/mdhtml/typst.py b/python/mdhtml/typst.py
index 45113da..e35fbf3 100644
--- a/python/mdhtml/typst.py
+++ b/python/mdhtml/typst.py
@@ -7,7 +7,8 @@
from pathlib import Path
from fast5ever import Element, Text, parse_fragment as mdhtml2dom
-from .export import _HEADS, _RAW_TYPE, _els, _text, HeadingNums, Resolver, decode_raw, tmpl_node as _tmpl_node, group_plan, ref_tokens, ref_variant, target_kind
+from .export import (_HEADS, _RAW_TYPE, _els, _text, _headnums, HeadingNums, Resolver, decode_raw, tmpl_node as _tmpl_node, group_plan,
+ ref_tokens, ref_variant, target_kind)
__all__ = ["mdhtml2typst", "mdhtml2pdf"]
@@ -322,8 +323,10 @@ def mdhtml2typst(src, dest=None, reftypes: dict | None = None, number_headings=N
(a dict containing `op`, operand `value`, and DOM `form`;
`None` drops them). `table_styles` maps a table's `custom-style` name or class (matched in that
order, case-insensitively) to extra Typst table arguments, e.g. `{'borderless table': 'stroke: none'}`.
- `prelude` text is prepended before the generated setup. Returns a `Typst`
- str carrying `.warnings`; `dest` also writes it to a file."""
+ `prelude` text is prepended before the generated setup. `number_headings=None` takes the
+ scheme from the source's frontmatter `number_headings:` when `src` is `md2mdhtml`'s result.
+ Returns a `Typst` str carrying `.warnings`; `dest` also writes it to a file."""
+ number_headings = _headnums(src, number_headings)
if not isinstance(src, str): src = src.to_html()
ex = _TypstExporter(reftypes, number_headings, tmpl, table_styles)
body = ex.run(mdhtml2dom(src))
diff --git a/python/mdhtml/viewmd.py b/python/mdhtml/viewmd.py
index 19f74c8..06b4c18 100644
--- a/python/mdhtml/viewmd.py
+++ b/python/mdhtml/viewmd.py
@@ -9,6 +9,7 @@
from aidialog.dialog import dlg2md
from aidialog.ipynb import read_ipynb
+from fastcore.nbio import nb_frontmatter
from . import DASHES, replacements, mdhtml2html, md2mdhtml
from .export import _fastpylight
@@ -53,7 +54,7 @@ def _head_section(path):
def main(
file: str = None, # Markdown file (or .ipynb notebook) to view (default: stdin)
refs: RefsMode = RefsMode.lenient, # References: target ids ('ids'), numbered ('resolve'), or numbered with ids as fallback ('lenient')
- number_headings: NumMode = None, # Heading numbering scheme
+ number_headings: NumMode = None, # Heading numbering scheme (default: the frontmatter's `number_headings:`, else automatic)
hl: HlMode = HlMode.spans, # Code highlighting: classed spans, the Highlight API, or off
auto_ids: bool = True, # Derive ids for headings
implicit_figures: bool = True, # Promote image-only paragraphs to figures
@@ -61,7 +62,10 @@ def main(
head: Annotated[str, "File inlined into the page head: .css as