Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
709b0f2
✨ feat(markdown): add native Markdown parser
harehare Oct 8, 2026
491290d
🐛 fix(typos): exclude the HTML entity table from the check
harehare Oct 8, 2026
9432ec5
✨ feat(markdown): switch to the native parser and remove markdown-rs
harehare Oct 8, 2026
3f656bd
✅ test(markdown): add parameterized tests for the HTML output
harehare Oct 8, 2026
699e6ff
✅ test(markdown): add parameterized tests for nodes and positions
harehare Oct 8, 2026
fc39f41
✅ test(markdown): add property-based tests for the parser and HTML
harehare Oct 8, 2026
7c73078
✨ feat(markdown): improve the native parser's conformance, HTML outpu…
harehare Oct 8, 2026
c53a1fb
🐛 fix(markdown): fix code language class, raw HTML end tags and task …
harehare Oct 8, 2026
6e5472d
📦 build(markdown): fix clippy errors and warnings without all features
harehare Oct 8, 2026
28d7348
🐛 fix(markdown): parse callout headers before inline parsing
harehare Oct 8, 2026
75bea90
✨ feat(markdown): render Obsidian syntax as HTML and harden the nativ…
harehare Oct 8, 2026
b0c4e77
🐛 fix(markdown): fix dash rules after wikilinks and embed labels in HTML
harehare Oct 8, 2026
24e2fd7
♻️ refactor(markdown): split block constructs into statically dispatc…
harehare Oct 8, 2026
b4c6d18
⚡ perf(markdown): remember missing terminators to keep inline scans l…
harehare Oct 8, 2026
9d585f0
🐛 fix(markdown): allow whitespace after = in MDX JSX attributes
harehare Oct 8, 2026
10d6812
🐛 fix(markdown): write MDX flow element children on their own lines
harehare Oct 8, 2026
87e8024
🐛 fix(markdown): serialize MDX JSX attribute values to JSON
harehare Oct 8, 2026
8ff1e6e
🐛 fix(markdown): keep the notation of link references when writing th…
harehare Oct 8, 2026
de9e168
🐛 fix(markdown): keep the meaning of text when writing it back
harehare Oct 8, 2026
3b1533e
⚡ perf(markdown): find code and math span closers from a table of run…
harehare Oct 8, 2026
6c1246f
🐛 fix(markdown): skip strings, template literals and comments when ma…
harehare Oct 8, 2026
056eb68
✅ test(markdown): compare MDX nodes with the official MDX packages
harehare Oct 8, 2026
b63986f
🐛 fix(markdown): strip a line ending at the ends of a code span
harehare Oct 8, 2026
4fdac53
✅ test(markdown): compare Markdown nodes with the official mdast
harehare Oct 8, 2026
41bfb7e
🐛 fix(markdown): match the official packages on titles, fences and JS…
harehare Oct 8, 2026
e3decbe
✅ test(markdown): add the tests of markdown-rs and micromark to the m…
harehare Oct 8, 2026
ccf4a0b
🐛 fix(markdown): write fragments, attributes, expressions, math and l…
harehare Oct 8, 2026
2fc14fe
✅ test(markdown): check that the corpus is written back as the same n…
harehare Oct 8, 2026
eb38ef8
🐛 fix(markdown): keep a blank line before a table and an asterisk nex…
harehare Oct 8, 2026
651e70f
✅ test(markdown): compare random documents with the official packages
harehare Oct 8, 2026
dcea13d
✨ feat(markdown): escape braces and ESM-like text when writing MDX
harehare Oct 8, 2026
faa9466
🐛 fix(markdown): match the official packages on tables, task items, E…
harehare Oct 8, 2026
c5d184a
🐛 fix(markdown): open emphasis next to strikethrough and keep the spa…
harehare Oct 8, 2026
b459a31
✅ test(markdown): note that markdown-rs reads null characters and ope…
harehare Oct 8, 2026
4b1cb35
✨ feat(markdown): export the types of JSX attributes
harehare Oct 8, 2026
a8282cf
✨ feat(markdown): read frontmatter at the start of MDX
harehare Oct 8, 2026
bb96d8f
✨ feat(run): add --no-frontmatter to read Markdown and MDX without fr…
harehare Oct 8, 2026
a704311
🔧 chore(typos): allow the character reference form of import in the M…
harehare Oct 8, 2026
8480c07
⚡ perf(markdown): look for an at sign once per text, not for each pai…
harehare Oct 8, 2026
81c2261
♻️ refactor(markdown): type the list that emphasis is matched in
harehare Oct 8, 2026
54d8899
♻️ refactor(markdown): share byte scanning helpers and fix stale pars…
harehare Oct 8, 2026
e53489a
♻️ refactor(markdown): read the dialect from a Flavor instead of mdx …
harehare Oct 8, 2026
3c9698d
♻️ refactor(markdown): name the parts of indentation and offset ranges
harehare Oct 8, 2026
7368ed1
♻️ refactor(markdown): decide what a line starts in one place
harehare Oct 8, 2026
04546d9
♻️ refactor(markdown): report invalid MDX as typed errors with a posi…
harehare Oct 8, 2026
bf7a90a
♻️ refactor(markdown): split node.rs and share its child accessors
harehare Oct 8, 2026
cde0de7
🐛 fix(markdown): keep heading depths from 1 to 6 with a HeadingDepth …
harehare Oct 8, 2026
d938af8
✅ test(markdown): drop a duplicate parser snapshot case and a repeate…
harehare Oct 8, 2026
9453f19
📝 docs(markdown): name the Unicode and HTML standard tables as the so…
harehare Oct 8, 2026
b22970f
♻️ refactor(markdown): restructure HTML block starts and autolink lit…
harehare Oct 8, 2026
12cadb8
✅ test(lang): drop the section test that built a heading of depth 7
harehare Oct 8, 2026
0865a7b
✅ test(markdown): replace the mdast comparison with rstest cases
harehare Oct 9, 2026
4838fdd
✅ test(markdown): replace the parser snapshot file with rstest cases
harehare Oct 9, 2026
ae71787
⚡ perf(markdown): compute tab columns from a per-line tab table
harehare Oct 9, 2026
8737bae
🐛 fix(markdown): end an empty list item at its last content line
harehare Oct 9, 2026
6f8ab76
🐛 fix(markdown): keep paragraphs apart in blocks without positions
harehare Oct 9, 2026
5cee621
🐛 fix(markdown): render the children of JSX elements as Markdown in HTML
harehare Oct 9, 2026
6591417
✅ test(markdown): run the spec tests with wikilinks and embeds, and w…
harehare Oct 9, 2026
ecc45e3
🐛 fix(markdown): keep the table run out of the recursive block renderer
harehare Oct 9, 2026
789da0f
♻️ refactor(markdown): share the lazy continuation checks with the bl…
harehare Oct 9, 2026
708c6c8
✅ test(markdown): check that the HTML shows what the callout and link…
harehare Oct 9, 2026
f699475
💥 feat(markdown)!: remove ListMarker::Continuation and make the optio…
harehare Oct 9, 2026
ee00d0f
📝 docs(markdown): describe the parser by the specs and mdast, and doc…
harehare Oct 9, 2026
4d34b3a
🐛 fix(markdown): track open containers and constructs when collecting…
harehare Oct 9, 2026
81d20c7
🐛 fix(web): pass a null list style to keep the markers of the document
harehare Oct 9, 2026
34628b4
🐛 fix(markdown): end raw HTML at any raw closing tag and keep tasks, …
harehare Oct 13, 2026
dfadd99
🐛 fix(markdown): keep the structure of emphasis, strong and reference…
harehare Oct 13, 2026
409f331
💥 feat(markdown)!: count a tab as one column and columns in UTF-16 co…
harehare Oct 13, 2026
c67e71c
💥 feat(markdown)!: include the backslash of an escape in the position…
harehare Oct 13, 2026
79790cc
⚡ perf(markdown): size the buffers of the inline parser and the resol…
harehare Oct 13, 2026
2be601d
⚡ perf(markdown): count the cells of a header only when a delimiter r…
harehare Oct 13, 2026
75cf5c9
⚡ perf(markdown): box the nodes among the inline items to keep the it…
harehare Oct 13, 2026
40562a4
♻️ refactor(markdown): share the collection of the lines of a contain…
harehare Oct 13, 2026
ad08112
♻️ refactor(markdown): move lists, footnotes and HTML blocks out of t…
harehare Oct 13, 2026
be51fa4
🐛 fix(markdown): separate the lines of pending MDX flow with the term…
harehare Oct 13, 2026
2ce9924
🐛 fix(markdown): keep a blank line before a dash rule that follows in…
harehare Oct 13, 2026
4e90471
✅ test(markdown): add fuzz targets for reading and rendering Markdown
harehare Oct 13, 2026
ee378db
⚡ perf(fuzz): cap the generated Markdown source at 64 KiB
harehare Oct 14, 2026
d500a70
✨ feat(markdown): compare HeadingDepth with u8 and convert it to usiz…
harehare Oct 14, 2026
acb5f7e
⚡ perf(markdown): write adjacent emphasis in linear time
harehare Oct 14, 2026
138ab5a
⚡ perf(fuzz): write only small sources as JSON in the markdown targets
harehare Oct 14, 2026
cae8f66
🐛 fix(markdown): keep an indented code block with trailing blank line…
harehare Oct 14, 2026
ff521d5
🐛 fix(markdown): keep the delimiter of emphasis that starts a new par…
harehare Oct 14, 2026
0bb49d7
🐛 fix(markdown): read an escaped pipe in a table cell as a pipe insid…
harehare Oct 14, 2026
abd80a1
🐛 fix(markdown): escape for a neighbor only when it touches the node
harehare Oct 14, 2026
01a15e0
🐛 fix(markdown): write one blank line after a table
harehare Oct 14, 2026
3000d06
🐛 fix(markdown): write lists in a block quote as at the top level
harehare Oct 14, 2026
cff56be
🐛 fix(markdown): keep a blank line after a quote or footnote inside a…
harehare Oct 14, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/gfm-spec.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: GFM spec round-trip fidelity
name: Markdown spec conformance

on:
schedule:
Expand All @@ -21,5 +21,7 @@ jobs:
with:
just-version: 1.58.0
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Render the spec examples
run: just test-spec
- name: Run GFM spec round-trip check
run: just test-gfm-spec
6 changes: 6 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[files]
# HTML entity names from the HTML spec, which include short non-words
extend-exclude = ["crates/mq-markdown/src/parser/inline/entities.rs"]

[default.extend-words]
# HashiCorp is a proper noun
Hashi = "Hashi"
Expand All @@ -6,6 +10,8 @@ ist = "ist"
# Intentional misspelling in the fuzzy-match-headings cookbook recipe
instalation = "instalation"
juxt = "juxt"
# `import` with its first letter as a character reference (`import`), as MDX text is written
mport = "mport"
reakpoint = "reakpoint"
sction = "sction"

Expand Down
12 changes: 2 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ itertools = "0.15.0"
js-sys = "0.3.77"
libc = "0.2"
libfuzzer-sys = "0.4"
markdown = "1.0.0"
memchr = "2"
nix = {version = "0.31", default-features = false, features = ["fs"]}
md5 = "0.8.0"
Expand Down Expand Up @@ -128,6 +127,7 @@ tower-lsp-server = "0.23.0"
tracing = "0.1.44"
tracing-opentelemetry = "0.34"
tracing-subscriber = "0.3.23"
unicode-id = "0.3.6"
unicode-normalization = "0.1.25"
unicode-segmentation = "1.13.3"
unicode-width = "0.2.2"
Expand Down
30 changes: 15 additions & 15 deletions crates/mq-check/src/constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ pub fn generate_constraints(hir: &Hir, ctx: &mut InferenceContext) -> ChildrenIn
// Pass 2.5: Process Assign symbols before other operators/calls.
// This ensures that variable types are updated by assignments before
// Refs and Calls in Pass 3 resolve against the (potentially stale) type.
// e.g., `var x = 10 | x = "hello" | upcase(x)` — the Assign updates
// e.g., `var x = 10 | x = "hello" | upcase(x)`: the Assign updates
// x's type to String before upcase(x) resolves its argument type.
for (symbol_id, kind) in &cats.assign_symbols {
generate_symbol_constraints(hir, *symbol_id, kind.clone(), ctx, &children_index);
Expand Down Expand Up @@ -675,7 +675,7 @@ pub(super) fn generate_symbol_constraints(
ctx.set_symbol_type(symbol_id, last_ty);
}
} else {
// No HIR resolution — try builtin registry by name as fallback
// No HIR resolution: try builtin registry by name as fallback
if let Some(symbol) = hir.symbol(symbol_id)
&& let Some(name) = &symbol.value
&& ctx.get_builtin_overloads(name.as_str()).is_some()
Expand Down Expand Up @@ -765,7 +765,7 @@ pub(super) fn generate_symbol_constraints(
},
));

// No need to re-bind subsequent Refs — Assigns are processed
// No need to re-bind subsequent Refs: Assigns are processed
// in Pass 2.5 (before Refs/Calls in Pass 3), so Refs will
// pick up the updated variable type when they are processed.
} else {
Expand Down Expand Up @@ -912,7 +912,7 @@ pub(super) fn generate_symbol_constraints(
return;
}

// Homogeneous or unresolved — unify all element types
// Homogeneous or unresolved: unify all element types
let elem_ty = elem_tys[0].clone();
let range = get_symbol_range(hir, symbol_id);
for ty in &elem_tys[1..] {
Expand Down Expand Up @@ -1037,7 +1037,7 @@ pub(super) fn generate_symbol_constraints(
if !children.is_empty() {
let range = get_symbol_range(hir, symbol_id);

// First child is the condition — mq is dynamically typed, so any
// First child is the condition: mq is dynamically typed, so any
// value can be used as a condition (truthy/falsy), not just Bool.
let _cond_ty = ctx.get_or_create_symbol_type(children[0]);

Expand Down Expand Up @@ -1080,7 +1080,7 @@ pub(super) fn generate_symbol_constraints(
true
};

// Check if any branch is None or a Union containing None — in mq,
// Check if any branch is None or a Union containing None: in mq,
// `if (...): value else: None` (or a while-loop branch that returns
// `Union(T, None)`) should not unify `value` with None; instead treat
// as different types so the overall if-expression preserves the None
Expand Down Expand Up @@ -1129,15 +1129,15 @@ pub(super) fn generate_symbol_constraints(
|| (has_none_branch && resolved.len() >= 2)
|| has_parameter_branch
{
// Different concrete types across branches — use Union type.
// Different concrete types across branches: use Union type.
// Include ALL resolved branch types (vars and concrete) so that
// branches whose types are not yet fully resolved can still be
// tracked and resolved later (e.g., `if (...): items else: None`
// where `items` is still a Var at constraint-generation time).
let union_ty = Type::union(resolved.clone());
ctx.set_symbol_type(symbol_id, union_ty);
} else {
// Homogeneous or unresolved — unify all branch types
// Homogeneous or unresolved: unify all branch types
ctx.set_symbol_type(symbol_id, then_ty.clone());
for ty in &branch_tys[1..] {
ctx.add_constraint(Constraint::Equal(
Expand Down Expand Up @@ -1446,12 +1446,12 @@ pub(super) fn generate_symbol_constraints(
};

if !all_same && concrete.len() >= 2 {
// Different concrete types in arms — use Union type
// Different concrete types in arms: use Union type
let unique_types: Vec<Type> = concrete.into_iter().cloned().collect();
let union_ty = Type::union(unique_types);
ctx.set_symbol_type(symbol_id, union_ty);
} else if concrete.len() == 1 && !var_tys.is_empty() {
// One concrete arm (e.g. `None`) plus still-unresolved arms — union
// One concrete arm (e.g. `None`) plus still-unresolved arms: union
// them instead of unifying, or the unresolved type collapses to the
// concrete one. Mirrors `merge_loop_types`.
let mut unified_var = var_tys[0].clone();
Expand Down Expand Up @@ -1535,7 +1535,7 @@ pub(super) fn generate_symbol_constraints(
));
ctx.set_symbol_type(symbol_id, try_ty);
} else {
// A branch type is still pending deferred resolution — decide later.
// A branch type is still pending deferred resolution: decide later.
let ty_var = ctx.fresh_var();
ctx.set_symbol_type(symbol_id, Type::Var(ty_var));
ctx.add_deferred_try_catch(infer::DeferredTryCatch {
Expand Down Expand Up @@ -1638,7 +1638,7 @@ pub(super) fn generate_symbol_constraints(
Type::Var(ty_var)
}
} else {
// Piped type not yet resolved — defer to post-unification.
// Piped type not yet resolved: defer to post-unification.
// Keep the selector so that if the piped type resolves to a Markdown node,
// the right type can be returned (e.g., `md.depth` → number).
let ty_var = ctx.fresh_var();
Expand Down Expand Up @@ -1741,7 +1741,7 @@ pub(super) fn generate_symbol_constraints(
}
}

// Block: pipe chain — compute type from children in pass 3.
// Block: pipe chain: compute type from children in pass 3.
// Block has higher insertion order than its parent (If/Else/etc.), so in
// the reverse-order pass 3 loop it is processed before its parent. This
// means the If handler can see the Block's real type (e.g. Union(T, None)
Expand Down Expand Up @@ -1926,7 +1926,7 @@ fn generate_call_constraints(

let real_children = &children[..children.len() - trailing_bracket_count];
let arg_symbol_ids = if prepend_piped {
// piped input was prepended — include a placeholder
// piped input was prepended: include a placeholder
let mut ids = vec![symbol_id]; // placeholder for piped arg
ids.extend_from_slice(real_children);
ids
Expand Down Expand Up @@ -2037,7 +2037,7 @@ fn generate_call_constraints(
// Only constrain the index to Number when it is a literal
// numeric index (e.g. v[0]). For variable indices
// (e.g. a String key used for Dict access), skip this
// constraint — the correct element type is resolved via
// constraint: the correct element type is resolved via
// resolve_deferred_tuple_accesses once the container's
// type is known.
if literal_index.is_some()
Expand Down
2 changes: 1 addition & 1 deletion crates/mq-help/src/docs/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3640,7 +3640,7 @@ x = 1
BuiltinDoc {
name: "to_h",
aliases: &[],
description: "Creates a markdown heading node with the given value and depth.",
description: "Creates a markdown heading node with the given value and depth. A depth outside of 1 to 6 becomes the nearest of them.",
params: &["value", "depth"],
param_types: &["dynamic", "number"],
returns: "markdown",
Expand Down
5 changes: 0 additions & 5 deletions crates/mq-lang/modules/section_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,3 @@ end
def test_section_toc_markdown_empty():
assert_eq(section::toc_markdown([]), [])
end

def test_section_sections_invalid_heading_depth():
let secs = section::sections([to_h("A", 7)])
| assert(is_none(secs))
end
14 changes: 12 additions & 2 deletions crates/mq-lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,12 @@ pub(crate) fn parse_in_module(

/// Parses an MDX string and returns an iterator over `Value` nodes.
pub fn parse_mdx_input(input: &str) -> miette::Result<Vec<RuntimeValue>> {
let mdx = mq_markdown::Markdown::from_mdx_str(input)?;
parse_mdx_input_with(input, mq_markdown::ParseOptions::default())
}

/// Parses an MDX string with the given options and returns an iterator over `Value` nodes.
pub fn parse_mdx_input_with(input: &str, options: mq_markdown::ParseOptions) -> miette::Result<Vec<RuntimeValue>> {
let mdx = mq_markdown::Markdown::from_mdx_str_with(input, options)?;
Ok(mdx.nodes.into_iter().map(RuntimeValue::from).collect())
}

Expand All @@ -233,7 +238,12 @@ pub fn parse_html_input_with_options(

/// Parses a Markdown string and returns an iterator over `Value` nodes.
pub fn parse_markdown_input(input: &str) -> miette::Result<Vec<RuntimeValue>> {
let md = mq_markdown::Markdown::from_markdown_str(input)?;
parse_markdown_input_with(input, mq_markdown::ParseOptions::default())
}

/// Parses a Markdown string with the given options and returns an iterator over `Value` nodes.
pub fn parse_markdown_input_with(input: &str, options: mq_markdown::ParseOptions) -> miette::Result<Vec<RuntimeValue>> {
let md = mq_markdown::Markdown::from_markdown_str_with(input, options)?;
Ok(md.nodes.into_iter().map(RuntimeValue::from).collect())
}

Expand Down
Loading
Loading