Skip to content
Closed
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
11 changes: 6 additions & 5 deletions docs/DIALECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ Markdown:
MDHTML {#def-mdhtml}
: HTML for Markdown-oriented documents.

::: {#tip-box .callout data-kind="tip"}
::: {#card-1 .card data-kind="sample"}
### A fenced div

Normal **Markdown** lives here.
Expand All @@ -232,12 +232,12 @@ Normal **Markdown** lives here.
MDHTML:

```html
<dl><dt id="def-mdhtml">MDHTML</dt><dd>HTML for Markdown-oriented documents.</dd></dl><div id="tip-box" class="callout" data-kind="tip"><h3>A fenced div</h3><p>Normal <strong>Markdown</strong> lives here.</p></div>
<dl><dt id="def-mdhtml">MDHTML</dt><dd>HTML for Markdown-oriented documents.</dd></dl><div id="card-1" class="card" data-kind="sample"><h3>A fenced div</h3><p>Normal <strong>Markdown</strong> lives here.</p></div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote this example to avoid confusion with the dialect's real callouts.

```

Definition lists use `dl`, `dt`, and `dd`. They are a leaf block: glued term lines followed by glued single-line `: definition` lines, with inline-only definitions and no loose form (a blank line ends the run, though adjacent lists merge into one `dl`). Only `:` marks a definition line. A term line may end with an attribute block, which binds to its `dt` exactly as a trailing attribute block binds to a heading. Block content in a definition is written as raw `<dl>` soup or a fenced div. Fenced divs follow Pandoc's opening syntax: an opening fence has at least three colons and attributes, a bare class word, or — deviating from Pandoc, which allows one or the other — both, merged: `::: details {#x open=''}` and `::: {.details #x open=''}` are the same opener. The bare word means that one class. A closing fence is a colon-only line of exactly the opening fence's length, so a longer outer fence can contain a shorter colon-only line as literal text.

A fenced div is an ordinary `div` in MDHTML: class words carry no parse-time behavior. A few class words carry *converter* behavior, assigned in the converter obligations section below — `details` (the collapsible block) and `math` (the display-math carrier) — so those names are reserved: a div classed `details` will fold in HTML output wherever it appears.
A fenced div is an ordinary `div` in MDHTML: class words carry no parse-time behavior. A few class words carry *converter* behavior, assigned in the converter obligations section below — `details` (the collapsible block), `callout-*` (the callout), and `math` (the display-math carrier) — so those names are reserved: a div classed `details` will fold in HTML output wherever it appears.

## Attributes and spans

Expand Down Expand Up @@ -542,12 +542,13 @@ Beyond the element mapping above, a few structural patterns carry a *meaning* ev
- **Cross-references.** An `a` with `data-ref` (or a `span` with `data-refs` grouping several) is a symbolic reference to be resolved and rendered per the captions and cross-references section; a converter never emits the empty carrier unresolved.
- **Raw data.** A raw-data `script` carrier addressed to the converter's own format is decoded and spliced; payloads for other formats are dropped (carried opaquely, never rendered as text), per the converter-specific raw data section.
- **Custom elements** without a native rendering are transparent wrappers: render the children, drop the tag.
- **The collapsible block.** A `div` whose class list contains `details` is a disclosure widget, its first child *heading* (any level) the label. HTML output lowers it to a `<details>` element with the heading as `<summary>` — the heading keeps its id but leaves the heading population: it joins neither tables of contents nor heading numbering. Formats without a folding affordance degrade with the label as a bold line and the body rendered normally; the body is always rendered, whatever the fold state. A missing heading means a format-default label.
- **The collapsible block.** A `div` whose class list contains `details`, or that carries a `collapse` attribute (the callout below), is a disclosure widget, its first child *heading* (any level) the label. HTML output lowers it to a `<details>` element with the heading as `<summary>` — the heading keeps its id but leaves the heading population: it joins neither tables of contents nor heading numbering. Formats without a folding affordance degrade with the label as a bold line and the body rendered normally; the body is always rendered, whatever the fold state. A missing heading means a format-default label.
- **The callout.** A `div` whose class list contains a `callout-<kind>` word is a callout, after Quarto; `kind` is one of `note`, `tip`, `warning`, `caution`, `important`. Its first child *heading* (any level) is its title and leaves the heading population as a summary does, whether or not the callout folds. A `collapse` attribute makes it a collapsible block exactly as if classed `details`: `"false"` means initially open, and the attribute is consumed. Formats with no callout affordance render it as a plain div.
- **Table widths.** `colwidths` and `width` on a table are layout requests the HTML exporter honors (`colgroup`, inline style width); formats that own their table layout (docx, typst) may ignore them.
- **Range markers.** Template `section`/`inverted` and `end` operations are paired siblings around the content they control. Converters render unfilled range instructions visibly when appropriate; the content between markers remains normal flow content.
- **Active-code carriers.** A `text/<lang>-block` script is template code, not content: dropped from final documents by default, echoable as code where an audit register wants it, and never executed by any converter (`instantiate` alone executes).

The class words with assigned behavior — `details` here, `math` for math carriers, `footnotes` on the footnote `section` — are reserved by this section; all other class words are inert data for styling.
The class words with assigned behavior — `details` and the `callout-` prefix here, `math` for math carriers, `footnotes` on the footnote `section` — are reserved by this section; all other class words are inert data for styling.

## Warnings

Expand Down
38 changes: 36 additions & 2 deletions docs/sample.html
Original file line number Diff line number Diff line change
Expand Up @@ -209,18 +209,52 @@ <h2>Abbreviations</h2>
</code></pre>
<p>The <abbr title="HyperText Markup Language, version 5">HTML5</abbr> standard changed the web.</p>
<h2>Fenced divs</h2>
<pre><code class="language-markdown">::: {#tip-box .callout .tip kind="tip"}
<pre><code class="language-markdown">::: {.card}
### A fenced div

Fenced divs are useful for notes, cards, columns, and other styled sections.
They can contain normal **Markdown**.
:::
</code></pre>
<div id="tip-box" class="callout tip" kind="tip">
<div class="card">
<h3>A fenced div</h3>
<p>Fenced divs are useful for notes, cards, columns, and other styled sections.
They can contain normal <strong>Markdown</strong>.</p>
</div>
<h2>Callouts</h2>
<pre><code class="language-markdown">::: {.callout-tip}
### Tip
Callouts follow Quarto: `note`, `tip`, `warning`, `caution`, `important`. The heading is the title and stays out of the TOC.
:::

A plain callout stays open; the heading is its title.

::: {.callout-note collapse="true"}
### A collapsed note
A `collapse` attribute folds the callout into a disclosure widget, like `.details`.
:::

Adding `collapse` folds it into a disclosure widget instead.

::: {.callout-warning collapse="false"}
### Starts open
`collapse="false"` folds too, but starts open.
:::
</code></pre>
<div class="callout-tip">
<h3>Tip</h3>
<p>Callouts follow Quarto: <code>note</code>, <code>tip</code>, <code>warning</code>, <code>caution</code>, <code>important</code>. The heading is the title and stays out of the TOC.</p>
</div>
<p>A plain callout stays open; the heading is its title.</p>
<div class="callout-note" collapse="true">
<h3>A collapsed note</h3>
<p>A <code>collapse</code> attribute folds the callout into a disclosure widget, like <code>.details</code>.</p>
</div>
<p>Adding <code>collapse</code> folds it into a disclosure widget instead.</p>
<div class="callout-warning" collapse="false">
<h3>Starts open</h3>
<p><code>collapse="false"</code> folds too, but starts open.</p>
</div>
<h2>Raw HTML</h2>
<pre><code class="language-markdown">&lt;section class="raw-panel"&gt;
&lt;h3&gt;Raw HTML section&lt;/h3&gt;
Expand Down
4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ space, and ordinary text keeps flowing after the table.
(a plain demo document, `mdhtml.tools.sample_clean()`), `puppy.jpg` the image they use, and
`docs/sample.html` the rendered page (regenerate all four with `mdhtml.tools.gen_docs()`).
- `sample.css` and `sample.js` - head sections for `viewmd`, styling the custom classes,
ids, and `data-` attributes the sample authors (badges from `data-kind`, callout borders,
ids, and `data-` attributes the sample authors (badges from `data-kind`, the `.card` box,
link markers) and flashing the target of any in-page link. Try them with
`viewmd sample.md --head sample.css --head sample.js` from this directory.
`viewmd sample-render.md --head sample.css --head sample.js` from this directory.
- `examples.ipynb` - a notebook rendering the feature examples from `sample.md` through
`md2mdhtml`, for eyeballing the raw MDHTML output.
- `demo.md` - a minimal dialect scrap (task list, fenced div, math) handy for quick CLI runs:
Expand Down
23 changes: 22 additions & 1 deletion examples/sample-clean.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,34 @@ The <abbr title="HyperText Markup Language, version 5">HTML5</abbr> standard cha

## Fenced divs

::: {#tip-box .callout .tip kind="tip"}
::: {.card}
### A fenced div

Fenced divs are useful for notes, cards, columns, and other styled sections.
They can contain normal **Markdown**.
:::

## Callouts

::: {.callout-tip}
### Tip
Callouts follow Quarto: `note`, `tip`, `warning`, `caution`, `important`. The heading is the title and stays out of the TOC.
:::

A plain callout stays open; the heading is its title.

::: {.callout-note collapse="true"}
### A collapsed note
A `collapse` attribute folds the callout into a disclosure widget, like `.details`.
:::

Adding `collapse` folds it into a disclosure widget instead.

::: {.callout-warning collapse="false"}
### Starts open
`collapse="false"` folds too, but starts open.
:::

## Raw HTML

<section class="raw-panel">
Expand Down
46 changes: 44 additions & 2 deletions examples/sample-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,21 +289,63 @@ The <abbr title="HyperText Markup Language, version 5">HTML5</abbr> standard cha
## Fenced divs

`````markdown
::: {#tip-box .callout .tip kind="tip"}
::: {.card}
### A fenced div

Fenced divs are useful for notes, cards, columns, and other styled sections.
They can contain normal **Markdown**.
:::
`````

::: {#tip-box .callout .tip kind="tip"}
::: {.card}
### A fenced div

Fenced divs are useful for notes, cards, columns, and other styled sections.
They can contain normal **Markdown**.
:::

## Callouts

`````markdown
::: {.callout-tip}
### Tip
Callouts follow Quarto: `note`, `tip`, `warning`, `caution`, `important`. The heading is the title and stays out of the TOC.
:::

A plain callout stays open; the heading is its title.

::: {.callout-note collapse="true"}
### A collapsed note
A `collapse` attribute folds the callout into a disclosure widget, like `.details`.
:::

Adding `collapse` folds it into a disclosure widget instead.

::: {.callout-warning collapse="false"}
### Starts open
`collapse="false"` folds too, but starts open.
:::
`````

::: {.callout-tip}
### Tip
Callouts follow Quarto: `note`, `tip`, `warning`, `caution`, `important`. The heading is the title and stays out of the TOC.
:::

A plain callout stays open; the heading is its title.

::: {.callout-note collapse="true"}
### A collapsed note
A `collapse` attribute folds the callout into a disclosure widget, like `.details`.
:::

Adding `collapse` folds it into a disclosure widget instead.

::: {.callout-warning collapse="false"}
### Starts open
`collapse="false"` folds too, but starts open.
:::

## Raw HTML

`````markdown
Expand Down
7 changes: 3 additions & 4 deletions examples/sample.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* Demo stylesheet for the attributes sample.md passes through: run
`viewmd sample.md --head sample.css --head sample.js` from examples/.
`viewmd sample-render.md --head sample.css --head sample.js` from examples/.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is the right command to see the pairs of raw codefences with their rendered examples.

Every rule targets a class, id, or data- attribute authored in sample.md. */

/* {#project-notes .section-title} on a heading */
Expand All @@ -25,9 +25,8 @@ img.thumbnail { border-radius: 6px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35); }
.important { font-weight: 700; color: rgba(220, 80, 80, 0.9); }
code.api-call { border: 1px solid rgba(140, 140, 255, 0.4); border-radius: 4px; padding: 0 0.3em; }

/* :::: {#tip-box .callout .tip kind="tip"} fenced div */
.callout { border: 1px solid rgba(120, 200, 140, 0.5); border-radius: 8px; padding: 0.2em 1em; margin: 1em 0; }
.callout.tip::before { content: "\1F4A1"; float: right; margin-top: 0.6em; }
/* ::: {.card} fenced div */
.card { border: 1px solid rgba(120, 200, 140, 0.5); border-radius: 8px; padding: 0.2em 1em; margin: 1em 0; }

/* <custom-card kind="note"> custom element in the HTML-subset section */
custom-card { background: rgba(140, 140, 255, 0.08); border-radius: 8px; padding: 0.1em 0.4em; }
Expand Down
25 changes: 24 additions & 1 deletion examples/sample.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,37 @@ The <abbr title="HyperText Markup Language, version 5">HTML5</abbr> standard cha
## Fenced divs

`````markdown
::: {#tip-box .callout .tip kind="tip"}
::: {.card}
### A fenced div

Fenced divs are useful for notes, cards, columns, and other styled sections.
They can contain normal **Markdown**.
:::
`````

## Callouts

`````markdown
::: {.callout-tip}
### Tip
Callouts follow Quarto: `note`, `tip`, `warning`, `caution`, `important`. The heading is the title and stays out of the TOC.
:::

A plain callout stays open; the heading is its title.

::: {.callout-note collapse="true"}
### A collapsed note
A `collapse` attribute folds the callout into a disclosure widget, like `.details`.
:::

Adding `collapse` folds it into a disclosure widget instead.

::: {.callout-warning collapse="false"}
### Starts open
`collapse="false"` folds too, but starts open.
:::
`````

## Raw HTML

`````markdown
Expand Down
6 changes: 4 additions & 2 deletions python/mdhtml/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ def mdhtml2html(src, dest=None, reftypes: dict | None = None, number_headings=No
toc: bool = False, refs: str = "resolve", id_prefix: str = "", fn_salt: str = "", hl_lang=None, code_wrap=None, gh_ids: bool = False) -> Html:
"""Lower MDHTML (a string or DocumentFragment; never mutated) to finished HTML: cross-references
baked as links, headings and captions numbered, `{=html}` raw data spliced, `colwidths` lowered,
and code highlighted. A `div` classed `details` lowers to a `<details>` element, its
first-child heading becoming the `<summary>` (id kept, excluded from TOC and numbering).
and code highlighted. A `div` classed `details`, or carrying a `collapse` attribute (a Quarto
callout), lowers to a `<details>` element, its first-child heading becoming the `<summary>`
(id kept, excluded from TOC and numbering); headings inside a `callout-*` div leave the TOC
likewise.
`auto_ids` derives Pandoc-style ids for headings without one (lowercased, punctuation dropped,
spaces to hyphens, `-1` suffixes on duplicates); pass `auto_ids=False` when rendering fragments
that share a page, where per-fragment derived ids would collide. Authored ids (never
Expand Down
30 changes: 24 additions & 6 deletions src/export_html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ fn ename(dom: &Dom, id: NodeId) -> Option<&str> { match &dom.get(id).data { Node

fn el_children(dom: &Dom, id: NodeId) -> Vec<NodeId> { dom.children(id).iter().copied().filter(|&c| ename(dom, c).is_some()).collect() }

fn has_class(dom: &Dom, id: NodeId, f: impl Fn(&str) -> bool) -> bool { dom.attr(id, "class").is_some_and(|c| c.split_whitespace().any(f)) }

/// Whether a heading at `id` stays out of the TOC: inside a `details` or
/// `summary`, or a Quarto `callout-*` div, whose headings title an aside
/// rather than a section.
fn hidden_from_toc(dom: &Dom, id: NodeId) -> bool {
let mut cur = id;
while let Some(p) = dom.parent(cur) {
if matches!(ename(dom, p), Some("details" | "summary")) || (ename(dom, p) == Some("div") && has_class(dom, p, |w| w.starts_with("callout-"))) { return true; }
cur = p;
}
false
}

fn walk(dom: &Dom, id: NodeId, out: &mut Vec<NodeId>) {
out.push(id);
for c in el_children(dom, id) { walk(dom, c, out); }
Expand Down Expand Up @@ -133,7 +147,7 @@ impl Exporter {
self.lower_details();
let mut els = Vec::new();
for c in el_children(&self.dom, DOCUMENT) { walk(&self.dom, c, &mut els); }
self.heads = els.iter().copied().filter(|&e| ename(&self.dom, e).is_some_and(|n| HEADS.contains(&n))).collect();
self.heads = els.iter().copied().filter(|&e| ename(&self.dom, e).is_some_and(|n| HEADS.contains(&n)) && !hidden_from_toc(&self.dom, e)).collect();
// Authored ids (present before `auto_ids` mints any) get a `data-id`
// marker, which anchor displays key on; auto ids stay unmarked.
for &e in &els { if let Some(i) = self.dom.attr(e, "id").map(str::to_string) { self.dom.set_attr(e, "data-id", &i).unwrap(); } }
Expand Down Expand Up @@ -194,15 +208,19 @@ impl Exporter {
Ok(())
}

/// `div.details` → `<details>`, its first-child heading → `<summary>`:
/// the dialect's collapsible block. Runs before heading collection, so a
/// summary joins neither the TOC, numbering, nor auto-id assignment.
/// `div.details`, or any div carrying `collapse=` (Quarto's collapsed
/// callout), → `<details>`, its first-child heading → `<summary>`;
/// `collapse="false"` opens it, and the attribute goes. Runs before heading
/// collection, so a summary joins neither the TOC, numbering, nor auto-id
/// assignment.
fn lower_details(&mut self) {
let mut els = Vec::new();
for c in el_children(&self.dom, DOCUMENT) { walk(&self.dom, c, &mut els); }
for &e in &els {
let classed = ename(&self.dom, e) == Some("div") && self.dom.attr(e, "class").is_some_and(|c| c.split_whitespace().any(|w| w == "details"));
if !classed { continue; }
if ename(&self.dom, e) != Some("div") { continue; }
let collapse = self.dom.remove_attr(e, "collapse").unwrap();
if collapse.is_none() && !has_class(&self.dom, e, |w| w == "details") { continue; }
if collapse.as_deref() == Some("false") { self.dom.set_attr(e, "open", "").unwrap(); }
self.dom.rename(e, "details").unwrap();
if let Some(&h) = el_children(&self.dom, e).first()
&& ename(&self.dom, h).is_some_and(|n| HEADS.contains(&n))
Expand Down
3 changes: 3 additions & 0 deletions src/tmpl.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ pre code .hl-markup-bold { font-weight: bold; }
pre code .hl-markup-strikethrough { text-decoration: line-through; }
pre code .hl-markup-highlight { background: light-dark(oklch(0.93 0.09 95 / 0.7), oklch(0.45 0.08 95 / 0.45)); }
details > summary { cursor: pointer; font-weight: 600; }
[class*="callout-"] { --h: 240; padding: 0.3em 1em; border-left: 4px solid oklch(0.65 0.13 var(--h)); background: light-dark(oklch(0.96 0.025 var(--h)), oklch(0.29 0.035 var(--h))); }
.callout-tip { --h: 155; } .callout-warning { --h: 85; } .callout-caution { --h: 50; } .callout-important { --h: 25; }
[class*="callout-"] > :is(h1, h2, h3, h4, h5, h6, summary):first-child { margin-block: 0.2em; font-size: 1em; }
Loading
Loading