From c695b6d024f8c9b2eabface6703ca1910447eea5 Mon Sep 17 00:00:00 2001 From: Jack Hogan Date: Thu, 29 Oct 2026 15:06:40 +0000 Subject: [PATCH 1/3] Merge adjacent definition lists inside containers Apply the same definition list merging logic that occurs at the top level to blocks within containers. --- src/block.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/block.rs b/src/block.rs index 0864303..d117e74 100644 --- a/src/block.rs +++ b/src/block.rs @@ -1599,7 +1599,9 @@ impl<'a> ContainerBuilder<'a> { if let Some(block) = self.finish_node(child, close.unwrap_or(child_end), parser) { out.push(Draft { span, block }); } } Self::bind_ials(&mut out, parser); - out + let mut merged = Vec::with_capacity(out.len()); + append_blocks(&mut merged, out); // adjacent definition lists merge inside containers as they do at the top level + merged } fn bind_ials(blocks: &mut Vec, parser: &mut Parser<'_>) { From 6fccca8f3c545ccb746c8a3cce261d2e4b9d7a94 Mon Sep 17 00:00:00 2001 From: Jack Hogan Date: Thu, 29 Oct 2026 15:14:03 +0000 Subject: [PATCH 2/3] Unify cross-reference representation - `XrefSeg` was the scanner's record of a cross-reference item and `Reference` the semantic model's, both built by the same parser from the same grammar. - `ref_segs` now returns `Reference` items and `XrefSeg` is gone. - In the Python `inlines()` output a cross-reference item reports `tokens` (`["bare"]` for `-@`) in place of `bare`. --- README.md | 2 +- py/src/lib.rs | 4 ++-- src/ast.rs | 2 +- src/inline.rs | 23 +++++++---------------- src/lib.rs | 2 +- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f5f4fa5..ad9435f 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,7 @@ Types are `text`, `emph`, `strong`, `strike`, `highlight`, `superscript`, `subsc - Code spans include `text`. Raw inlines include `format` and `text`. - Math includes `delimiter`, `display`, and `tex`. - Attribute groups include `id`. -- Cross-references include `refs` and `tokens`. Footnote references include `label`. Autolinks include `url`. +- Cross-references include `refs`, one per item with `target`, `tokens` and `prefix`, and the group's `tokens`. Footnote references include `label`. Autolinks include `url`. - Template tokens include `form` (always `inline`), `source`, `syntax`, `body`, `kind`, `name`, and `inverted`. Template tokens inside raw HTML blocks are reported too, at depth 0. `inlines` accepts the same `math`, `bare_autolinks`, and `templates` options as `md2mdhtml`. diff --git a/py/src/lib.rs b/py/src/lib.rs index a52e6ff..9f883a2 100644 --- a/py/src/lib.rs +++ b/py/src/lib.rs @@ -291,11 +291,11 @@ fn block_span_node(py: Python<'_>, span: BlockSpan, start: usize, end: usize) -> /// `strip_trailing_attr` would consume, as the dialect's own grammar judges it. #[pyfunction] fn trailing_attr_span(line: &str) -> Option<(usize, usize)> { mdhtml::trailing_attr_span(line) } -fn xref_refs(py: Python<'_>, refs: Vec) -> PyResult>> { +fn xref_refs(py: Python<'_>, refs: Vec) -> PyResult>> { refs.into_iter().map(|r| { let rd = PyDict::new(py); rd.set_item("target", r.target)?; - rd.set_item("bare", r.bare)?; + rd.set_item("tokens", r.tokens)?; rd.set_item("prefix", r.prefix)?; Ok(rd.unbind()) }).collect() diff --git a/src/ast.rs b/src/ast.rs index e08b233..6994d2a 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -183,7 +183,7 @@ impl Panel { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Reference { pub target: String, pub tokens: Vec, pub prefix: String } #[derive(Clone, Debug, PartialEq)] diff --git a/src/inline.rs b/src/inline.rs index 1ffd761..cba3757 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -92,7 +92,7 @@ pub enum InlineData { Math { delimiter: &'static str, tex: String }, Raw { format: String, text: String }, Attrs { id: Option }, - Xref { refs: Vec, tokens: Option }, + Xref { refs: Vec, tokens: Option }, Template { syntax: String, body: String, kind: crate::template::TokenKind, name: String }, FootnoteRef { label: String }, Autolink { url: String }, @@ -219,9 +219,6 @@ fn inline_data(src: &str, ev: &InlineEvent, ctx: &InlineContext<'_>) -> InlineDa } } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct XrefSeg { pub target: String, pub bare: bool, pub prefix: Option } - fn ref_attr(src: &str, at: usize) -> Option { trailing_attr(&src[at..]).and_then(|(attr, _)| attr.pairs.iter().find(|(k, _)| k == "ref").map(|(_, v)| v.clone())) } @@ -846,23 +843,17 @@ fn following_link_label_blocks_shortcut(src: &str, after: usize) -> bool { } /// Cross-reference bracket group: `[@id]`, `[-@id]` (bare number), `[Prefix @id]` -/// (prefix override, single ref only), or `[@a; @b]` groups. Returns the dumb -/// carrier: a Link with a `data-ref` attr, or a `span[data-refs]` of them for groups. -fn ref_item(label: &str) -> Option { - let items = ref_segs(label)?.into_iter().map(|item| crate::ast::Reference { target: item.target, - tokens: if item.bare { vec!["bare".into()] } else { Vec::new() }, prefix: item.prefix.unwrap_or_default() }).collect(); - Some(Inline::Reference { attrs: Attr::default(), items }) -} +/// (prefix override, single ref only), or `[@a; @b]` groups. +fn ref_item(label: &str) -> Option { Some(Inline::Reference { attrs: Attr::default(), items: ref_segs(label)? }) } -/// The scanner-facing form of the same grammar: parsed segments for a whole -/// bracket group, or None when the group is not a cross-reference. -pub fn ref_segs(label: &str) -> Option> { +/// The items of a bracket group as references, or None when the group is not a cross-reference. +pub fn ref_segs(label: &str) -> Option> { let segs: Vec<&str> = label.split(';').collect(); let single = segs.len() == 1; segs.iter().map(|s| ref_seg_info(s, single)).collect() } -fn ref_seg_info(seg: &str, allow_prefix: bool) -> Option { +fn ref_seg_info(seg: &str, allow_prefix: bool) -> Option { let seg = seg.trim(); let at = seg.find('@')?; let before = &seg[..at]; @@ -875,7 +866,7 @@ fn ref_seg_info(seg: &str, allow_prefix: bool) -> Option { let mut chars = id.chars(); let valid_start = chars.next().is_some_and(|c| c.is_ascii_alphanumeric()); if !valid_start || !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { return None; } - Some(XrefSeg { target: id.to_string(), bare, prefix: (!prefix.is_empty()).then(|| prefix.to_string()) }) + Some(crate::ast::Reference { target: id.to_string(), tokens: if bare { vec!["bare".into()] } else { Vec::new() }, prefix: prefix.to_string() }) } fn code_span(src: &str, i: usize) -> Option<(Inline, usize)> { diff --git a/src/lib.rs b/src/lib.rs index c61ad79..cce7473 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ pub use diagnostic::{Diagnostic, Severity}; #[cfg(feature = "dom")] pub use fast5ever; pub use highlight::highlight_md; -pub use inline::{InlineData, InlineNode, XrefSeg}; +pub use inline::{InlineData, InlineNode}; pub use inline_spans::{SourceNode, source_nodes}; pub use line::{LineOffset, SourceLocation, SourceSpan}; #[cfg(feature = "dom")] From 53941d25f1c5a72bec680d367ae452e79dbd5059 Mon Sep 17 00:00:00 2001 From: Jack Hogan Date: Thu, 29 Oct 2026 15:21:24 +0000 Subject: [PATCH 3/3] Add citations and bibliographies --- DEV.md | 2 +- README.md | 10 +- docs/DIALECT.md | 56 ++++++++++- docs/sample.html | 21 ++++ examples/sample-clean.md | 16 +++ examples/sample-render.md | 28 ++++++ examples/sample.md | 18 ++++ py/src/lib.rs | 55 ++++++++--- python/mdhtml/export.py | 12 ++- python/mdhtml/md.py | 9 +- python/mdhtml/md2html.py | 3 +- python/mdhtml/typst.py | 19 +++- python/mdhtml/viewmd.py | 3 +- src/ast.rs | 38 ++++++- src/block.rs | 3 +- src/export_html.rs | 7 +- src/html.rs | 16 +-- src/import.rs | 6 +- src/inline.rs | 27 +++-- src/render.rs | 43 +++++++- src/resolve.rs | 202 +++++++++++++++++++++++++++++++++----- src/tmpl.css | 3 + src/write_md.rs | 31 +++++- tests/canonical.rs | 32 ++++++ tests/resolve.rs | 88 +++++++++++++++-- tests/test_export.py | 53 +++++++++- tests/test_focused.py | 4 +- 27 files changed, 692 insertions(+), 113 deletions(-) diff --git a/DEV.md b/DEV.md index 509c82f..7a4ffe8 100644 --- a/DEV.md +++ b/DEV.md @@ -53,7 +53,7 @@ The Python binding uses public semantic model, encoder, writer and resolver APIs ## Shared MDHTML core -`Document` is the shared semantic model. Panels, references and operations have explicit types; table cells, definitions, Figure bodies and operation operands contain blocks. MDHTML is its browser-readable import/encoding boundary. Source drafts, scanner tokens and rewrite traces remain separate from the semantic model. Importers and exporters share the Rust interpretation of raw payloads, metadata, plain text and references. +`Document` is the shared semantic model. Panels, references, bibliographies and operations have explicit types; table cells, definitions, Figure bodies and operation operands contain blocks. MDHTML is its browser-readable import/encoding boundary. Source drafts, scanner tokens and rewrite traces remain separate from the semantic model. Importers and exporters share the Rust interpretation of raw payloads, metadata, plain text and references. Public conversion names use `x2y`, with both representations explicit: `md2mdhtml`, `mdhtml2md`, `wiki2mdhtml`, `md2gfm`, `mdhtml2gfm`, `mdhtml2html`, `mdhtml2typst`, `mdhtml2pdf`, external `mdhtml2docx`, `md2dom`, and `mdhtml2dom`. Operations within a representation, including `bake_mdhtml`, `blocks`, `rewrite`, and `fill_md`, keep ordinary verbs. diff --git a/README.md b/README.md index ad9435f..71078a9 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ The dialect deviates from CommonMark for three reasons: - Panels: `::: callout-note` (also `tip`, `important`, `warning`, `caution`) creates a callout. Add `collapse="true"` or `collapse="false"` for initially closed/open disclosure, or use `::: details` without a callout kind. The first heading is the panel title, not a document heading. These normalize to `div[data-panel]` with an optional `header` title and independent `data-callout` / `data-disclosure` properties. Ordinary divs stay ordinary. - Raw passthrough: a Pandoc-style raw attribute names the payload's format. Use exactly `{=name}` as a fenced code block's info string or immediately after inline code. Both forms render as an inert `