Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 `<a href="#sec-pay">Section 1.</a>`, 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 (`<a href="#sec-pay" class="xref">sec-pay</a>`, 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 `<span class="heading-number">`, 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 `<a href="#sec-pay">Section 1.</a>`, 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 (`<a href="#sec-pay" class="xref">sec-pay</a>`, 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 `<span class="heading-number">`, 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 `<span class="caption-label">Figure 1</span>: ` 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 `<colgroup>`; `fr` values share the width remaining after fixed lengths.
Expand Down
4 changes: 2 additions & 2 deletions docs/DIALECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.

Expand Down
13 changes: 11 additions & 2 deletions python/mdhtml/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<table class=\"frontmatter\">`, for prepending to rendered output"
rows = "".join(f"<tr><th>{escape(k)}</th><td>{escape(v)}</td></tr>" for k, v in meta.items())
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions python/mdhtml/md.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion python/mdhtml/md2html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down
Loading