From 2791abe9937be320b37cbc9a4697b0139a1f9220 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Sat, 11 Jul 2026 13:48:08 -0400 Subject: [PATCH 1/4] impl --- CLAUDE.md | 11 +- src/benchmarks/benchmark_inputs.ts | 30 ++++++ src/lib/MdzNodeView.svelte | 44 +++++--- src/lib/MdzStreamNodeView.svelte | 44 +++++--- src/lib/mdz.ts | 14 ++- src/lib/mdz_contexts.ts | 6 +- src/lib/mdz_helpers.ts | 73 ++++++++++++- src/lib/mdz_lexer.ts | 100 +++++++++++++++++- src/lib/mdz_opcodes.ts | 4 +- src/lib/mdz_opcodes_to_nodes.ts | 5 + src/lib/mdz_stream_parser_link.ts | 84 +++++++++++++-- src/lib/mdz_stream_parser_table.ts | 9 +- src/lib/mdz_stream_state.svelte.ts | 5 +- src/lib/mdz_to_svelte.ts | 37 ++++++- src/lib/mdz_token_parser.ts | 18 +++- src/routes/+page.svelte | 11 +- src/routes/docs/usage/grammar/+page.svelte | 10 ++ src/routes/docs/usage/grammar/mdz_grammar.mdz | 26 ++++- src/test/EchoProp.svelte | 7 ++ src/test/MdzRenderHarness.svelte | 17 +++ .../mdz/component_adjacent_bold/expected.json | 1 + .../mdz/component_at_end/expected.json | 1 + .../mdz/component_at_start/expected.json | 1 + .../expected.json | 1 + .../mdz/component_breaks_inline/expected.json | 1 + .../mdz/component_empty/expected.json | 1 + .../mdz/component_inline/expected.json | 1 + .../mdz/component_multiline/expected.json | 1 + .../mdz/component_multiple/expected.json | 2 + .../mdz/component_nested/expected.json | 2 + .../mdz/component_self_closing/expected.json | 1 + .../mdz/component_simple/expected.json | 1 + .../expected.json | 1 + .../component_with_formatting/expected.json | 1 + .../expected.json | 1 + .../expected.json | 1 + .../fixtures/mdz/element_div/expected.json | 1 + .../fixtures/mdz/element_empty/expected.json | 1 + .../element_interleaved_overlap/expected.json | 1 + .../mdz/element_multiline/expected.json | 1 + .../element_nested_same_name/expected.json | 2 + .../expected.json | 1 + .../fixtures/mdz/element_span/expected.json | 1 + .../fixtures/mdz/element_void/expected.json | 2 + .../mdz/heading_with_tag/expected.json | 1 + .../tag_attribute_bare_boolean/expected.json | 59 +++++++++++ .../mdz/tag_attribute_bare_boolean/input.mdz | 1 + .../expected.json | 46 ++++++++ .../input.mdz | 3 + .../tag_attribute_empty_value/expected.json | 24 +++++ .../mdz/tag_attribute_empty_value/input.mdz | 1 + .../tag_attribute_gt_in_value/expected.json | 24 +++++ .../mdz/tag_attribute_gt_in_value/input.mdz | 1 + .../tag_attribute_invalid_brace/expected.json | 15 +++ .../mdz/tag_attribute_invalid_brace/input.mdz | 1 + .../expected.json | 15 +++ .../input.mdz | 1 + .../expected.json | 15 +++ .../input.mdz | 1 + .../expected.json | 15 +++ .../tag_attribute_invalid_duplicate/input.mdz | 1 + .../expected.json | 15 +++ .../input.mdz | 1 + .../expected.json | 15 +++ .../input.mdz | 2 + .../expected.json | 15 +++ .../input.mdz | 1 + .../expected.json | 15 +++ .../input.mdz | 1 + .../expected.json | 15 +++ .../tag_attribute_invalid_unquoted/input.mdz | 1 + .../expected.json | 15 +++ .../input.mdz | 1 + .../mdz/tag_attribute_multiple/expected.json | 36 +++++++ .../mdz/tag_attribute_multiple/input.mdz | 1 + .../expected.json | 17 +++ .../input.mdz | 1 + .../expected.json | 17 +++ .../input.mdz | 1 + .../tag_attribute_string_double/expected.json | 24 +++++ .../mdz/tag_attribute_string_double/input.mdz | 1 + .../tag_attribute_string_single/expected.json | 24 +++++ .../mdz/tag_attribute_string_single/input.mdz | 1 + .../expected.json | 3 + .../input.svelte | 5 + .../expected.json | 3 + .../input.svelte | 5 + .../expected.json | 3 + .../input.svelte | 5 + .../expected.json | 3 + .../input.svelte | 5 + src/test/mdz_helpers.test.ts | 86 ++++++++++++++- src/test/mdz_nesting_cap.test.ts | 33 ++++++ src/test/mdz_parser_parity.test.ts | 39 +++++++ src/test/mdz_render.test.ts | 88 ++++++++++++++- src/test/mdz_scaling.test.ts | 16 +++ src/test/mdz_scaling_ratio.test.ts | 27 +++++ src/test/mdz_to_svelte.test.ts | 37 +++++++ 98 files changed, 1321 insertions(+), 64 deletions(-) create mode 100644 src/test/EchoProp.svelte create mode 100644 src/test/MdzRenderHarness.svelte create mode 100644 src/test/fixtures/mdz/tag_attribute_bare_boolean/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_bare_boolean/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_element_and_component/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_element_and_component/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_empty_value/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_empty_value/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_gt_in_value/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_gt_in_value/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_brace/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_brace/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_digit_name/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_digit_name/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_duplicate/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_duplicate/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_unquoted/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_unquoted/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_multiple/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_multiple/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_self_close_no_space/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_self_close_no_space/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_self_close_with_space/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_self_close_with_space/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_string_double/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_string_double/input.mdz create mode 100644 src/test/fixtures/mdz/tag_attribute_string_single/expected.json create mode 100644 src/test/fixtures/mdz/tag_attribute_string_single/input.mdz create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/expected.json create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/input.svelte create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/expected.json create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/input.svelte create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/expected.json create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/input.svelte create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/expected.json create mode 100644 src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/input.svelte diff --git a/CLAUDE.md b/CLAUDE.md index 38aa40e..edff61e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,7 +120,8 @@ mdz is a **markdown dialect and renderer**: - CommonMark/GFM compatibility (it is a dialect, not a superset) - syntax highlighting (inject a highlighter — see the rendering seam) - a CSS framework or themed components (use fuz_css / fuz_ui) -- sanitization of arbitrary HTML (only registered components/elements render) +- sanitization of arbitrary HTML (only registered components/elements render, + and element attributes filter to a closed inert allowlist) ## Syntax @@ -135,7 +136,7 @@ mdz is a **markdown dialect and renderer**: | Code blocks | fenced with optional language hints; an unclosed fence consumes to EOF (or to the end of its enclosing blockquote) | | Horizontal rule | `---` on its own line | | Tables | `\| a \| b \|` rows + a `\| --- \| :-: \|` delimiter row (colons set per-column alignment); leading **and** trailing `\|` required; inline-only cells (`` `code` `` protects pipes, `\|` is a literal pipe); a header/delimiter column-count mismatch stays a paragraph; body rows pad/truncate at render | -| Components / elements | `` / `` (must be registered) | +| Components / elements | `` / `` (must be registered); attributes are quoted strings (`"` or `'`) or bare booleans (``); **elements** filter to a closed inert allowlist (`class`, `title`, `lang`, `dir`, `role`, `aria-label`/`-hidden`/`-describedby`/`-labelledby`), **components** pass all attributes through as props; malformed forms (unquoted, spaced `=`, `={…}`, duplicate name, newline in the open tag) stay literal text | | Paragraphs / breaks | blank line between paragraphs; single newlines are soft breaks; `
` (registered) for hard breaks | Whitespace follows standard markdown semantics: the parser preserves @@ -252,7 +253,11 @@ Svelte-shaped: the runtime views emit text through Svelte expressions `escape_svelte_text` — there is **no raw-HTML output path and no `{@html}` anywhere**. Unregistered components/elements render nothing, and link references pass the `mdz_is_safe_reference` protocol gate before becoming an -`href`. Any future raw-HTML emission (e.g. collapsing static subtrees to +`href`. Element attributes filter to a closed, hand-vetted allowlist of inert +names (nothing URL-, script-, or DOM-clobbering-bearing), so element safety is +independent of which element names a consumer registers; component attributes +pass through as props, since registering a component is itself the trust +decision. Any future raw-HTML emission (e.g. collapsing static subtrees to pre-rendered HTML strings rendered via `{@html}`) would move XSS-escaping out of Svelte and into mdz's own emitter — a trust-boundary change to design deliberately, not an incremental optimization. diff --git a/src/benchmarks/benchmark_inputs.ts b/src/benchmarks/benchmark_inputs.ts index bdb7a75..283f7d4 100644 --- a/src/benchmarks/benchmark_inputs.ts +++ b/src/benchmarks/benchmark_inputs.ts @@ -312,4 +312,34 @@ Functions with Array, Promise, and Map in prose. (_, i) => '['.repeat(200) + `text ${i} ` + ''.repeat(200) + `X${i}` + ''.repeat(200), ).join('\n\n'), }, + { + name: 'attribute heavy', + // The attribute-authoring success path: many correctly-closed tags each + // carrying several string and boolean attributes. Drives the attribute + // scan (name + quoted-value passes) and the `Set` dedup on the throughput + // path — the counterpart to the adversarial attribute inputs below. + content: Array.from( + {length: 300}, + (_, i) => + `Section ${i} with **${i}**.`, + ).join('\n\n'), + }, + { + name: 'many attributes single tag', + // One tag carrying a very large number of distinct attributes on a single + // line. Guards the `Set`-based duplicate-name check against an accidental + // O(n²) rescan of the collected list — an all-attributes line must stay + // linear (the same shape `mdz_scaling_ratio.test.ts` locks). + content: + ' `a${i}="${i}"`).join(' ') + '>x', + }, + { + name: 'hold line attribute value', + // One giant unterminated quoted attribute value (` import {resolve} from '$app/paths'; - import type {MdzNode, MdzNodeTableRow, MdzTableAlign} from './mdz.ts'; - import {mdz_classify_link, mdz_is_void_element} from './mdz_helpers.ts'; + import type {MdzAttribute, MdzNode, MdzNodeTableRow, MdzTableAlign} from './mdz.ts'; + import { + mdz_classify_link, + mdz_is_void_element, + mdz_filter_element_attributes, + mdz_attributes_to_props, + mdz_format_unregistered_attributes, + ALLOWED_ELEMENT_ATTRIBUTES, + } from './mdz_helpers.ts'; import { mdz_components_context, mdz_elements_context, @@ -37,30 +44,36 @@ tree instead of per node. --> {#snippet render_node(node: MdzNode)} {#if node.type === 'Element'} {@const element_config = get_elements?.()?.get(node.name)} - {#if element_config !== undefined} + {#if element_config === true} + {@const attrs = mdz_filter_element_attributes( + node.name, + node.attributes, + ALLOWED_ELEMENT_ATTRIBUTES, + )} {#if mdz_is_void_element(node.name)} - + {:else} - + {#if node.children.length > 0} {@render render_children(node.children)} {/if} {/if} {:else} - {@render render_unregistered_tag(node.name, node.children)} + {@render render_unregistered_tag(node.name, node.attributes, node.children)} {/if} {:else if node.type === 'Component'} {@const Component = get_components?.()?.get(node.name)} {#if Component} - + {@const props = mdz_attributes_to_props(node.attributes)} + {#if node.children.length > 0} {@render render_children(node.children)} {/if} {:else} - {@render render_unregistered_tag(node.name, node.children)} + {@render render_unregistered_tag(node.name, node.attributes, node.children)} {/if} {:else if node.type === 'Text'} {node.content} @@ -164,12 +177,17 @@ tree instead of per node. --> {/each} {/snippet} -{#snippet render_unregistered_tag(name: string, children: Array)} +{#snippet render_unregistered_tag( + name: string, + attributes: Array, + children: Array, +)} + {@const formatted = mdz_format_unregistered_attributes(attributes)} {#if children.length > 0} - <{name}>{@render render_children(children)}</{name}> + <{name}{formatted}>{@render render_children( + children, + )}</{name}> {:else} - <{name} /> + <{name}{formatted} /> {/if} {/snippet} diff --git a/src/lib/MdzStreamNodeView.svelte b/src/lib/MdzStreamNodeView.svelte index acc32f0..dd9efbd 100644 --- a/src/lib/MdzStreamNodeView.svelte +++ b/src/lib/MdzStreamNodeView.svelte @@ -1,7 +1,14 @@ + +{msg} diff --git a/src/test/MdzRenderHarness.svelte b/src/test/MdzRenderHarness.svelte new file mode 100644 index 0000000..04ae9aa --- /dev/null +++ b/src/test/MdzRenderHarness.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/test/fixtures/mdz/component_adjacent_bold/expected.json b/src/test/fixtures/mdz/component_adjacent_bold/expected.json index 54e2bcb..ace1c52 100644 --- a/src/test/fixtures/mdz/component_adjacent_bold/expected.json +++ b/src/test/fixtures/mdz/component_adjacent_bold/expected.json @@ -18,6 +18,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_at_end/expected.json b/src/test/fixtures/mdz/component_at_end/expected.json index 1946c2f..0cc2072 100644 --- a/src/test/fixtures/mdz/component_at_end/expected.json +++ b/src/test/fixtures/mdz/component_at_end/expected.json @@ -15,6 +15,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_at_start/expected.json b/src/test/fixtures/mdz/component_at_start/expected.json index 0d316ff..c6871f6 100644 --- a/src/test/fixtures/mdz/component_at_start/expected.json +++ b/src/test/fixtures/mdz/component_at_start/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_between_paragraphs/expected.json b/src/test/fixtures/mdz/component_between_paragraphs/expected.json index d300d64..02c8447 100644 --- a/src/test/fixtures/mdz/component_between_paragraphs/expected.json +++ b/src/test/fixtures/mdz/component_between_paragraphs/expected.json @@ -15,6 +15,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_breaks_inline/expected.json b/src/test/fixtures/mdz/component_breaks_inline/expected.json index 98c4506..9700984 100644 --- a/src/test/fixtures/mdz/component_breaks_inline/expected.json +++ b/src/test/fixtures/mdz/component_breaks_inline/expected.json @@ -20,6 +20,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_empty/expected.json b/src/test/fixtures/mdz/component_empty/expected.json index caa419d..3d0a8e1 100644 --- a/src/test/fixtures/mdz/component_empty/expected.json +++ b/src/test/fixtures/mdz/component_empty/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [], "start": 0, "end": 15 diff --git a/src/test/fixtures/mdz/component_inline/expected.json b/src/test/fixtures/mdz/component_inline/expected.json index fa4263d..766ee48 100644 --- a/src/test/fixtures/mdz/component_inline/expected.json +++ b/src/test/fixtures/mdz/component_inline/expected.json @@ -11,6 +11,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_multiline/expected.json b/src/test/fixtures/mdz/component_multiline/expected.json index 6cfdaa3..867d117 100644 --- a/src/test/fixtures/mdz/component_multiline/expected.json +++ b/src/test/fixtures/mdz/component_multiline/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_multiple/expected.json b/src/test/fixtures/mdz/component_multiple/expected.json index cf9a1c9..0128ab0 100644 --- a/src/test/fixtures/mdz/component_multiple/expected.json +++ b/src/test/fixtures/mdz/component_multiple/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", @@ -16,6 +17,7 @@ { "type": "Component", "name": "Card", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_nested/expected.json b/src/test/fixtures/mdz/component_nested/expected.json index 3fe26f6..15070d3 100644 --- a/src/test/fixtures/mdz/component_nested/expected.json +++ b/src/test/fixtures/mdz/component_nested/expected.json @@ -2,10 +2,12 @@ { "type": "Component", "name": "Card", + "attributes": [], "children": [ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_self_closing/expected.json b/src/test/fixtures/mdz/component_self_closing/expected.json index ece8e53..4c17157 100644 --- a/src/test/fixtures/mdz/component_self_closing/expected.json +++ b/src/test/fixtures/mdz/component_self_closing/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [], "start": 0, "end": 9 diff --git a/src/test/fixtures/mdz/component_simple/expected.json b/src/test/fixtures/mdz/component_simple/expected.json index d9e0da7..d776c57 100644 --- a/src/test/fixtures/mdz/component_simple/expected.json +++ b/src/test/fixtures/mdz/component_simple/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_space_before_closing/expected.json b/src/test/fixtures/mdz/component_space_before_closing/expected.json index 6d44191..a1e4c79 100644 --- a/src/test/fixtures/mdz/component_space_before_closing/expected.json +++ b/src/test/fixtures/mdz/component_space_before_closing/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/component_with_formatting/expected.json b/src/test/fixtures/mdz/component_with_formatting/expected.json index bc6b713..5bace1a 100644 --- a/src/test/fixtures/mdz/component_with_formatting/expected.json +++ b/src/test/fixtures/mdz/component_with_formatting/expected.json @@ -2,6 +2,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_bold_crossing_closer/expected.json b/src/test/fixtures/mdz/element_bold_crossing_closer/expected.json index fbdb714..4a60190 100644 --- a/src/test/fixtures/mdz/element_bold_crossing_closer/expected.json +++ b/src/test/fixtures/mdz/element_bold_crossing_closer/expected.json @@ -5,6 +5,7 @@ { "type": "Element", "name": "b", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_code_crossing_closer/expected.json b/src/test/fixtures/mdz/element_code_crossing_closer/expected.json index 25b563c..b1a4168 100644 --- a/src/test/fixtures/mdz/element_code_crossing_closer/expected.json +++ b/src/test/fixtures/mdz/element_code_crossing_closer/expected.json @@ -5,6 +5,7 @@ { "type": "Element", "name": "b", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_div/expected.json b/src/test/fixtures/mdz/element_div/expected.json index 3f760ba..f477935 100644 --- a/src/test/fixtures/mdz/element_div/expected.json +++ b/src/test/fixtures/mdz/element_div/expected.json @@ -2,6 +2,7 @@ { "type": "Element", "name": "div", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_empty/expected.json b/src/test/fixtures/mdz/element_empty/expected.json index 24614b4..2fa3751 100644 --- a/src/test/fixtures/mdz/element_empty/expected.json +++ b/src/test/fixtures/mdz/element_empty/expected.json @@ -2,6 +2,7 @@ { "type": "Element", "name": "div", + "attributes": [], "children": [], "start": 0, "end": 11 diff --git a/src/test/fixtures/mdz/element_interleaved_overlap/expected.json b/src/test/fixtures/mdz/element_interleaved_overlap/expected.json index fb741f3..ce0c921 100644 --- a/src/test/fixtures/mdz/element_interleaved_overlap/expected.json +++ b/src/test/fixtures/mdz/element_interleaved_overlap/expected.json @@ -5,6 +5,7 @@ { "type": "Element", "name": "b", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_multiline/expected.json b/src/test/fixtures/mdz/element_multiline/expected.json index c363a39..2d6eb5a 100644 --- a/src/test/fixtures/mdz/element_multiline/expected.json +++ b/src/test/fixtures/mdz/element_multiline/expected.json @@ -2,6 +2,7 @@ { "type": "Element", "name": "div", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_nested_same_name/expected.json b/src/test/fixtures/mdz/element_nested_same_name/expected.json index bd0b23e..cf717da 100644 --- a/src/test/fixtures/mdz/element_nested_same_name/expected.json +++ b/src/test/fixtures/mdz/element_nested_same_name/expected.json @@ -2,6 +2,7 @@ { "type": "Element", "name": "b", + "attributes": [], "children": [ { "type": "Text", @@ -12,6 +13,7 @@ { "type": "Element", "name": "b", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_nested_same_name_unclosed/expected.json b/src/test/fixtures/mdz/element_nested_same_name_unclosed/expected.json index a016af2..f2a5e6d 100644 --- a/src/test/fixtures/mdz/element_nested_same_name_unclosed/expected.json +++ b/src/test/fixtures/mdz/element_nested_same_name_unclosed/expected.json @@ -11,6 +11,7 @@ { "type": "Element", "name": "b", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_span/expected.json b/src/test/fixtures/mdz/element_span/expected.json index 0de0525..e126679 100644 --- a/src/test/fixtures/mdz/element_span/expected.json +++ b/src/test/fixtures/mdz/element_span/expected.json @@ -11,6 +11,7 @@ { "type": "Element", "name": "span", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/element_void/expected.json b/src/test/fixtures/mdz/element_void/expected.json index 7666904..4cf67a7 100644 --- a/src/test/fixtures/mdz/element_void/expected.json +++ b/src/test/fixtures/mdz/element_void/expected.json @@ -11,6 +11,7 @@ { "type": "Element", "name": "br", + "attributes": [], "children": [], "start": 4, "end": 10 @@ -28,6 +29,7 @@ { "type": "Element", "name": "br", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/heading_with_tag/expected.json b/src/test/fixtures/mdz/heading_with_tag/expected.json index 2c53de9..6279641 100644 --- a/src/test/fixtures/mdz/heading_with_tag/expected.json +++ b/src/test/fixtures/mdz/heading_with_tag/expected.json @@ -13,6 +13,7 @@ { "type": "Component", "name": "Alert", + "attributes": [], "children": [ { "type": "Text", diff --git a/src/test/fixtures/mdz/tag_attribute_bare_boolean/expected.json b/src/test/fixtures/mdz/tag_attribute_bare_boolean/expected.json new file mode 100644 index 0000000..69df203 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_bare_boolean/expected.json @@ -0,0 +1,59 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Element", + "name": "span", + "attributes": [ + { + "name": "hidden", + "value": true, + "start": 6, + "end": 12 + } + ], + "children": [ + { + "type": "Text", + "content": "x", + "start": 13, + "end": 14 + } + ], + "start": 0, + "end": 21 + }, + { + "type": "Text", + "content": " and ", + "start": 21, + "end": 26 + }, + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "dismissible", + "value": true, + "start": 33, + "end": 44 + } + ], + "children": [ + { + "type": "Text", + "content": "y", + "start": 45, + "end": 46 + } + ], + "start": 26, + "end": 54 + } + ], + "start": 0, + "end": 54 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_bare_boolean/input.mdz b/src/test/fixtures/mdz/tag_attribute_bare_boolean/input.mdz new file mode 100644 index 0000000..8ce77b9 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_bare_boolean/input.mdz @@ -0,0 +1 @@ + and y \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_element_and_component/expected.json b/src/test/fixtures/mdz/tag_attribute_element_and_component/expected.json new file mode 100644 index 0000000..4906bfc --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_element_and_component/expected.json @@ -0,0 +1,46 @@ +[ + { + "type": "Element", + "name": "div", + "attributes": [ + { + "name": "class", + "value": "box", + "start": 5, + "end": 16 + } + ], + "children": [ + { + "type": "Text", + "content": "a", + "start": 17, + "end": 18 + } + ], + "start": 0, + "end": 24 + }, + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "status", + "value": "error", + "start": 33, + "end": 47 + } + ], + "children": [ + { + "type": "Text", + "content": "b", + "start": 48, + "end": 49 + } + ], + "start": 26, + "end": 57 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_element_and_component/input.mdz b/src/test/fixtures/mdz/tag_attribute_element_and_component/input.mdz new file mode 100644 index 0000000..6811d9e --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_element_and_component/input.mdz @@ -0,0 +1,3 @@ +
a
+ +b \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_empty_value/expected.json b/src/test/fixtures/mdz/tag_attribute_empty_value/expected.json new file mode 100644 index 0000000..84ecba7 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_empty_value/expected.json @@ -0,0 +1,24 @@ +[ + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "title", + "value": "", + "start": 7, + "end": 15 + } + ], + "children": [ + { + "type": "Text", + "content": "x", + "start": 16, + "end": 17 + } + ], + "start": 0, + "end": 25 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_empty_value/input.mdz b/src/test/fixtures/mdz/tag_attribute_empty_value/input.mdz new file mode 100644 index 0000000..d704295 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_empty_value/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_gt_in_value/expected.json b/src/test/fixtures/mdz/tag_attribute_gt_in_value/expected.json new file mode 100644 index 0000000..0fa4bfb --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_gt_in_value/expected.json @@ -0,0 +1,24 @@ +[ + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "title", + "value": "a > b", + "start": 7, + "end": 20 + } + ], + "children": [ + { + "type": "Text", + "content": "x", + "start": 21, + "end": 22 + } + ], + "start": 0, + "end": 30 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_gt_in_value/input.mdz b/src/test/fixtures/mdz/tag_attribute_gt_in_value/input.mdz new file mode 100644 index 0000000..adf98f2 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_gt_in_value/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_brace/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_brace/expected.json new file mode 100644 index 0000000..32e228b --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_brace/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 26 + } + ], + "start": 0, + "end": 26 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_brace/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_brace/input.mdz new file mode 100644 index 0000000..3536444 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_brace/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/expected.json new file mode 100644 index 0000000..88add31 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 19 + } + ], + "start": 0, + "end": 19 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/input.mdz new file mode 100644 index 0000000..a05ca20 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_dangling_equals/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_digit_name/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_digit_name/expected.json new file mode 100644 index 0000000..acbfbfb --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_digit_name/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 23 + } + ], + "start": 0, + "end": 23 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_digit_name/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_digit_name/input.mdz new file mode 100644 index 0000000..96faa06 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_digit_name/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_duplicate/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_duplicate/expected.json new file mode 100644 index 0000000..075cc2a --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_duplicate/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 28 + } + ], + "start": 0, + "end": 28 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_duplicate/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_duplicate/input.mdz new file mode 100644 index 0000000..e83d755 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_duplicate/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/expected.json new file mode 100644 index 0000000..291e105 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "
x", + "start": 0, + "end": 19 + } + ], + "start": 0, + "end": 19 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/input.mdz new file mode 100644 index 0000000..8e9d6e0 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_missing_separator/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/expected.json new file mode 100644 index 0000000..d251bf7 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 22 + } + ], + "start": 0, + "end": 22 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/input.mdz new file mode 100644 index 0000000..066908a --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_newline_in_tag/input.mdz @@ -0,0 +1,2 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/expected.json new file mode 100644 index 0000000..d8466d5 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 24 + } + ], + "start": 0, + "end": 24 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/input.mdz new file mode 100644 index 0000000..b98579e --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_spaced_equals/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/expected.json new file mode 100644 index 0000000..6880717 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 22 + } + ], + "start": 0, + "end": 22 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/input.mdz new file mode 100644 index 0000000..05edae0 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_tab_separator/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_unquoted/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_unquoted/expected.json new file mode 100644 index 0000000..7757965 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_unquoted/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 20 + } + ], + "start": 0, + "end": 20 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_unquoted/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_unquoted/input.mdz new file mode 100644 index 0000000..e1ff2d1 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_unquoted/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/expected.json b/src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/expected.json new file mode 100644 index 0000000..ff6dcb1 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/expected.json @@ -0,0 +1,15 @@ +[ + { + "type": "Paragraph", + "children": [ + { + "type": "Text", + "content": "x", + "start": 0, + "end": 21 + } + ], + "start": 0, + "end": 21 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/input.mdz b/src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/input.mdz new file mode 100644 index 0000000..86d6a64 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_invalid_unterminated_quote/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_self_close_no_space/expected.json b/src/test/fixtures/mdz/tag_attribute_self_close_no_space/expected.json new file mode 100644 index 0000000..3f5f7a2 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_self_close_no_space/expected.json @@ -0,0 +1,17 @@ +[ + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "disabled", + "value": true, + "start": 7, + "end": 15 + } + ], + "children": [], + "start": 0, + "end": 17 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_self_close_no_space/input.mdz b/src/test/fixtures/mdz/tag_attribute_self_close_no_space/input.mdz new file mode 100644 index 0000000..ddb8c6c --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_self_close_no_space/input.mdz @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_self_close_with_space/expected.json b/src/test/fixtures/mdz/tag_attribute_self_close_with_space/expected.json new file mode 100644 index 0000000..ae5874b --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_self_close_with_space/expected.json @@ -0,0 +1,17 @@ +[ + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "disabled", + "value": true, + "start": 7, + "end": 15 + } + ], + "children": [], + "start": 0, + "end": 18 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_self_close_with_space/input.mdz b/src/test/fixtures/mdz/tag_attribute_self_close_with_space/input.mdz new file mode 100644 index 0000000..88ae026 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_self_close_with_space/input.mdz @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_string_double/expected.json b/src/test/fixtures/mdz/tag_attribute_string_double/expected.json new file mode 100644 index 0000000..ffb12c1 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_string_double/expected.json @@ -0,0 +1,24 @@ +[ + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "status", + "value": "error", + "start": 7, + "end": 21 + } + ], + "children": [ + { + "type": "Text", + "content": "x", + "start": 22, + "end": 23 + } + ], + "start": 0, + "end": 31 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_string_double/input.mdz b/src/test/fixtures/mdz/tag_attribute_string_double/input.mdz new file mode 100644 index 0000000..0c56abb --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_string_double/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/mdz/tag_attribute_string_single/expected.json b/src/test/fixtures/mdz/tag_attribute_string_single/expected.json new file mode 100644 index 0000000..ffb12c1 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_string_single/expected.json @@ -0,0 +1,24 @@ +[ + { + "type": "Component", + "name": "Alert", + "attributes": [ + { + "name": "status", + "value": "error", + "start": 7, + "end": 21 + } + ], + "children": [ + { + "type": "Text", + "content": "x", + "start": 22, + "end": 23 + } + ], + "start": 0, + "end": 31 + } +] diff --git a/src/test/fixtures/mdz/tag_attribute_string_single/input.mdz b/src/test/fixtures/mdz/tag_attribute_string_single/input.mdz new file mode 100644 index 0000000..62e6cd6 --- /dev/null +++ b/src/test/fixtures/mdz/tag_attribute_string_single/input.mdz @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/expected.json b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/expected.json new file mode 100644 index 0000000..69f6839 --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/expected.json @@ -0,0 +1,3 @@ +{ + "code": "\n\nhi\n" +} diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/input.svelte b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/input.svelte new file mode 100644 index 0000000..7700c1d --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_component_props/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/expected.json b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/expected.json new file mode 100644 index 0000000..3847a37 --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/expected.json @@ -0,0 +1,3 @@ +{ + "code": "\n\n\n" +} diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/input.svelte b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/input.svelte new file mode 100644 index 0000000..ac9fc61 --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_allowed/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/expected.json b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/expected.json new file mode 100644 index 0000000..d7558e6 --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/expected.json @@ -0,0 +1,3 @@ +{ + "code": "\n\n\n" +} diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/input.svelte b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/input.svelte new file mode 100644 index 0000000..8a54c0d --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_bare_boolean/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/expected.json b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/expected.json new file mode 100644 index 0000000..3847a37 --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/expected.json @@ -0,0 +1,3 @@ +{ + "code": "\n\n\n" +} diff --git a/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/input.svelte b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/input.svelte new file mode 100644 index 0000000..c462697 --- /dev/null +++ b/src/test/fixtures/svelte_preprocess_mdz/tag_attribute_element_dropped/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/test/mdz_helpers.test.ts b/src/test/mdz_helpers.test.ts index fc3d438..a7820a3 100644 --- a/src/test/mdz_helpers.test.ts +++ b/src/test/mdz_helpers.test.ts @@ -1,6 +1,6 @@ import {test, assert, describe} from 'vitest'; -import type {MdzNode, MdzNodeComponent, MdzNodeElement} from '$lib/mdz.ts'; +import type {MdzAttribute, MdzNode, MdzNodeComponent, MdzNodeElement} from '$lib/mdz.ts'; import { is_letter, is_tag_name_char, @@ -14,6 +14,9 @@ import { mdz_is_url, mdz_is_safe_reference, mdz_is_void_element, + ALLOWED_ELEMENT_ATTRIBUTES, + mdz_filter_element_attributes, + mdz_format_unregistered_attributes, match_url_prefix_case_insensitive, ascii_to_lower, mdz_resolve_relative_path, @@ -57,6 +60,7 @@ const link = (reference: string, children: Array): MdzNode => ({ const component = (name: string, children: Array): MdzNodeComponent => ({ type: 'Component', name, + attributes: [], children, start: 0, end: 0, @@ -64,6 +68,7 @@ const component = (name: string, children: Array): MdzNodeComponent => const element = (name: string, children: Array): MdzNodeElement => ({ type: 'Element', name, + attributes: [], children, start: 0, end: 0, @@ -766,3 +771,82 @@ describe('mdz_extract_single_tag', () => { assert.equal(mdz_extract_single_tag([text(' '), text('\n')]), null); }); }); + +// helper to create attributes with dummy positions +const attr = (name: string, value: string | true = true): MdzAttribute => ({ + name, + value, + start: 0, + end: 0, +}); + +describe('mdz_filter_element_attributes', () => { + test('passes through allowed attribute names', () => { + assert.deepEqual( + mdz_filter_element_attributes( + 'div', + [attr('class', 'box'), attr('title', 'hi')], + ALLOWED_ELEMENT_ATTRIBUTES, + ), + {class: 'box', title: 'hi'}, + ); + }); + + test('passes through an allowed aria attribute', () => { + assert.deepEqual( + mdz_filter_element_attributes( + 'span', + [attr('aria-label', 'close')], + ALLOWED_ELEMENT_ATTRIBUTES, + ), + {'aria-label': 'close'}, + ); + }); + + test('drops disallowed attribute names', () => { + assert.deepEqual( + mdz_filter_element_attributes( + 'div', + [attr('class', 'box'), attr('onclick', 'x'), attr('id', 'y')], + ALLOWED_ELEMENT_ATTRIBUTES, + ), + {class: 'box'}, + ); + }); + + test('preserves a boolean `true` value', () => { + assert.deepEqual( + mdz_filter_element_attributes( + 'span', + [attr('aria-hidden', true)], + ALLOWED_ELEMENT_ATTRIBUTES, + ), + {'aria-hidden': true}, + ); + }); + + test('returns an empty object for empty input', () => { + assert.deepEqual(mdz_filter_element_attributes('div', [], ALLOWED_ELEMENT_ATTRIBUTES), {}); + }); +}); + +describe('mdz_format_unregistered_attributes', () => { + test('formats a string-valued attribute', () => { + assert.equal(mdz_format_unregistered_attributes([attr('status', 'error')]), ' status="error"'); + }); + + test('formats a bare boolean attribute', () => { + assert.equal(mdz_format_unregistered_attributes([attr('disabled', true)]), ' disabled'); + }); + + test('formats a mix of string and boolean attributes', () => { + assert.equal( + mdz_format_unregistered_attributes([attr('status', 'error'), attr('disabled', true)]), + ' status="error" disabled', + ); + }); + + test('returns an empty string for empty input', () => { + assert.equal(mdz_format_unregistered_attributes([]), ''); + }); +}); diff --git a/src/test/mdz_nesting_cap.test.ts b/src/test/mdz_nesting_cap.test.ts index 4aaff57..a2dbd01 100644 --- a/src/test/mdz_nesting_cap.test.ts +++ b/src/test/mdz_nesting_cap.test.ts @@ -94,6 +94,12 @@ const BATTERY: Array<[string, string]> = [ ['[b](/u)', 'Paragraph("" Link:/u("b"))'], ['[b](/u)', 'Paragraph("[" Element:a("b](/u)"))'], ['``x', 'Paragraph(`` "x")'], + // attribute-bearing tags — attributes ride the open tag without perturbing + // nesting/scoping; a `>` inside a quoted value is content, and a failed tag's + // attributes survive in the literal-text fallback + ['xy', 'Element:a("x" Element:a("y"))'], + ['_x_', 'Element:a(Italic("x"))'], + ['_xy_z', 'Paragraph("_x" Element:a("y") "_z")'], // table cell delegates to the same sync lexer [ '| _xy_z | b |\n| --- | --- |\n| c | d |\n', @@ -148,6 +154,33 @@ describe('inline nesting-depth cap', () => { } }); + test('attributes do not perturb the nesting-depth cap', () => { + // attribute-bearing tags nest and cap exactly like bare tags — the cap + // counts containers, not attribute text (the closer stays attribute-free) + const open = ''; + assert.strictEqual( + max_container_depth(mdz_parse(open.repeat(CAP) + 'X' + ''.repeat(CAP))), + CAP, + ); + assert.strictEqual( + max_container_depth(mdz_parse(open.repeat(CAP + 1) + 'X' + ''.repeat(CAP + 1))), + CAP, + ); + // sync/stream parity at and beyond the cap, one-shot and chunked + for (const n of [CAP - 1, CAP, CAP + 1, CAP * 2]) { + const input = open.repeat(n) + 'X' + ''.repeat(n); + const expected = JSON.stringify(mdz_parse(input)); + assert.strictEqual(JSON.stringify(stream_parse(input)), expected, `parity at n=${n}`); + for (const chunk of [1, 3, 64]) { + assert.strictEqual( + JSON.stringify(stream_parse_chunked(input, chunk)), + expected, + `chunk=${chunk} n=${n}`, + ); + } + } + }); + test('the cap is per-run — a nest past it does not suppress a later run', () => { // depth resets when the paragraph closes: the second paragraph's link // still forms even though the first blew past the cap diff --git a/src/test/mdz_parser_parity.test.ts b/src/test/mdz_parser_parity.test.ts index b7c8a98..eb58d5e 100644 --- a/src/test/mdz_parser_parity.test.ts +++ b/src/test/mdz_parser_parity.test.ts @@ -767,6 +767,37 @@ describe('mdz parser parity', () => { } }); + describe('tag attribute chunking', () => { + // attributes ride the open tag, so the same open/close decisions must hold + // across every chunk boundary — a value streamed a char at a time resolves + // to the same tree as the one-shot parse, and every invalid form (brace + // value, duplicate name, unterminated quote) reverts to literal text + // identically. The ``-in-value case proves the quoted value is consumed + // atomically: the real closer is the final ``, not the one in the value. + for (const input of [ + 'x', + "x", + '', + 'x', + 'x', + 'x', + '', + '', + 'x', // invalid brace value → literal + 'z', // duplicate name → literal + 'y', // `` inside the value is content, not the closer + ]) { + test(`one-shot matches mdz_parse for ${JSON.stringify(input)}`, () => { + assert.deepEqual(stream_parse_text(input), mdz_parse(input)); + }); + for (const chunk_size of [1, 2, 3]) { + test(`chunk_size=${chunk_size} matches mdz_parse for ${JSON.stringify(input)}`, () => { + assert.deepEqual(stream_parse_chunked(input, chunk_size), mdz_parse(input)); + }); + } + } + }); + describe('mid-stack revert ordering', () => { // a failed-closer revert dissolves a mid-stack frame while optimistic // containers above it stay open — the replacement delimiter text must @@ -879,6 +910,14 @@ describe('mdz parser parity', () => { 'x', 'a a', + // tag attributes: string/bare values, `>`-in-value, duplicate revert, + // and a ``-shaped substring inside a value (atomic consumption — + // the real closer is the final ``, not the one in the value) + 'x', + '', + 'y', + 'z', + 'y', // doubled-delimiter and italic-closer holds 'a~x~~y~~z', '_a_x', diff --git a/src/test/mdz_render.test.ts b/src/test/mdz_render.test.ts index 5fd7590..7e078f3 100644 --- a/src/test/mdz_render.test.ts +++ b/src/test/mdz_render.test.ts @@ -6,17 +6,28 @@ * `svelte/server`'s `render` (no DOM environment needed). */ -import {describe, test, assert} from 'vitest'; +import {describe, test, assert, vi} from 'vitest'; import {render} from 'svelte/server'; import type {Component} from 'svelte'; +import {DEV} from 'esm-env'; import MdzComponent from '$lib/Mdz.svelte'; +import type {MdzComponents, MdzElements} from '$lib/mdz_contexts.ts'; +import MdzRenderHarnessComponent from './MdzRenderHarness.svelte'; +import EchoProp from './EchoProp.svelte'; import {load_fixtures} from './fixtures/mdz/mdz_test_helpers.ts'; // narrowed component type — the wrapper's `SvelteHTMLElements` rest-prop // union is too complex for `render()`'s generic inference const Mdz = MdzComponent as unknown as Component<{content: string}>; +// harness that provides the `elements`/`components` contexts around `Mdz` +const Harness = MdzRenderHarnessComponent as unknown as Component<{ + content: string; + elements?: MdzElements; + components?: MdzComponents; +}>; + describe('Mdz SSR rendering', () => { test('renders simple content', () => { const result = render(Mdz, {props: {content: 'hello **bold** and `code`'}}); @@ -34,3 +45,78 @@ describe('Mdz SSR rendering', () => { } }); }); + +describe('tag attributes', () => { + test('renders an allowed element attribute', () => { + const result = render(Harness, { + props: {content: '', elements: new Map([['aside', true]])}, + }); + assert.include(result.body, 'class="x"'); + assert.include(result.body, 'hi'); + }); + + test('drops a disallowed element attribute (DEV-warns)', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const result = render(Harness, { + props: { + content: '', + elements: new Map([['aside', true]]), + }, + }); + // `render()` is lazy — force the body (which runs the filter + its warn) + // while the spy is active, then snapshot the calls before `mockRestore` + const body = result.body; + const warned = warn.mock.calls.map((c) => String(c[0])); + warn.mockRestore(); + assert.include(body, 'class="x"'); + assert.notInclude(body, 'onclick'); + // the drop is a policy outcome surfaced as a DEV console.warn naming the + // element and the dropped attribute (silent in prod) + if (DEV) { + assert.ok( + warned.some((m) => m.includes('aside') && m.includes('onclick')), + 'expected a DEV drop-warning naming the element and attribute', + ); + } + }); + + test('renders a bare boolean allowed attribute', () => { + const result = render(Harness, { + props: {content: '', elements: new Map([['aside', true]])}, + }); + assert.include(result.body, 'aria-hidden'); + }); + + test('passes component attributes through as props', () => { + const result = render(Harness, { + props: {content: '', components: new Map([['Echo', EchoProp]])}, + }); + assert.include(result.body, 'data-echo'); + assert.include(result.body, 'hi'); + }); + + test('unregistered tag placeholder includes attribute text', () => { + // no registry entry → the source-faithful literal placeholder + const result = render(Harness, {props: {content: 'x'}}); + assert.include(result.body, 'bar="baz"'); + }); + + test('capstone: nested Mdz renders pass-through content and inline boolean', () => { + // mirrors the homepage demo — a `content` string prop and a bare `inline` + // boolean pass through to a recursively-rendered `Mdz`, which renders + // bold text inside an inline `` (not a block `
`) + const result = render(Harness, { + props: { + content: '', + components: new Map([['Mdz', MdzComponent]]), + }, + }); + // SSR interleaves `` markers, so assert structure not an exact + // `hi` slice: bold text is present, and a `` (the + // inline wrapper, not a block `
`) encloses the recursively-rendered + // bold + assert.include(result.body, ''); + assert.include(result.body, 'hi'); + assert.match(result.body, /[\s\S]*?[\s\S]*?hi/); + }); +}); diff --git a/src/test/mdz_scaling.test.ts b/src/test/mdz_scaling.test.ts index 9e876a7..65037ef 100644 --- a/src/test/mdz_scaling.test.ts +++ b/src/test/mdz_scaling.test.ts @@ -88,3 +88,19 @@ describe('streaming held-candidate scans stay linear', () => { assert.isAbove(feed_once('['.repeat(50_000) + 'x'), 0); }); }); + +// A single tag with a huge number of distinct attributes. The `Set`-based +// duplicate-name check is O(1) per attribute, so the sync parse stays linear in +// the attribute count; a reverted O(n) rescan of the collected list would be +// O(n²) and blow past the timeout. The guard uses the sync parser: there the +// dedup is the only super-linear risk, whereas the streaming-chunked path also +// re-scans the held open-tag prefix per feed (the acknowledged residual term for +// a long single line in small chunks, unrelated to the dedup). +describe('tag attributes stay linear', () => { + const many_attributes = (n: number): string => + ' `a${i}="${i}"`).join(' ') + '>x'; + + test('sync: many distinct attributes on one tag', {timeout: GUARD_TIMEOUT}, () => { + assert.isArray(mdz_parse(many_attributes(50_000))); + }); +}); diff --git a/src/test/mdz_scaling_ratio.test.ts b/src/test/mdz_scaling_ratio.test.ts index 145d1a4..aa45d88 100644 --- a/src/test/mdz_scaling_ratio.test.ts +++ b/src/test/mdz_scaling_ratio.test.ts @@ -123,4 +123,31 @@ describe('parser scan work is sub-quadratic (deterministic)', () => { (input) => stream_work(input, input.length + 1), ); }); + + test('sync: many distinct attributes on one tag (attribute scan is linear)', () => { + // the counted scan work on an attribute-dense tag grows with input length, + // not its square (the `Set` dedup is O(1)/attr — its wall-clock guard is in + // `mdz_scaling.test.ts`; this pins the scan-work side) + assert_subquadratic( + 'many attributes', + [2000, 4000, 8000].map( + (n) => + ' `a${i}="${i}"`).join(' ') + + '>x', + ), + sync_work, + ); + }); + + test('streaming: hold-line attribute value (buffer_index_of memo)', () => { + // the attribute analog of hold-line code/link: an unterminated quoted value + // grows across feeds and the tag holds; the closing-quote/newline search must + // be memoized or chunked feeds go quadratic in the value length + assert_subquadratic( + 'hold attribute value', + [4000, 8000, 16000].map((chars) => 'hi', {}, {aside: true}); + assert.equal(result.markup, ``); + }); + + test('drops a disallowed element attribute', () => { + const result = convert('', {}, {aside: true}); + assert.equal(result.markup, ``); + assert.notInclude(result.markup, 'onclick'); + }); + + test('emits a bare boolean element attribute', () => { + const result = convert('', {}, {aside: true}); + assert.equal(result.markup, ``); + }); + + test('passes component attributes through as props (string + bare boolean)', () => { + const result = convert('hi', { + Alert: '$lib/Alert.svelte', + }); + assert.equal(result.markup, `hi`); + assert_import(result, 'Alert', '$lib/Alert.svelte', 'default'); + }); + + test('escapes a string value through escape_js_string', () => { + // a value with a single-quote and a backslash must be escaped for the + // single-quoted JS-expression attribute (not emitted verbatim) + const value = "a'b\\c"; + const result = convert(`hi`, {Alert: '$lib/Alert.svelte'}); + assert.equal(result.markup, `hi`); + // the raw quote/backslash never appear unescaped in the output + assert.include(result.markup, escape_js_string(value)); + }); + }); }); From f5dbce0755f74961da0636079714e4e00bf667f0 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Sat, 11 Jul 2026 15:12:40 -0400 Subject: [PATCH 2/4] wip --- CLAUDE.md | 14 +++++++------- src/lib/mdz_contexts.ts | 7 ++++--- src/lib/mdz_helpers.ts | 13 ++++++++++--- src/test/mdz_helpers.test.ts | 10 ++++++++++ src/test/mdz_render.test.ts | 11 +++++++++++ 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index edff61e..a0bd6c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -265,13 +265,13 @@ deliberately, not an incremental optimization. The three renderers (`MdzNodeView`, `MdzStreamNodeView`, `mdz_to_svelte`) share one grammar and stay in sync by construction: rendering *decisions* (link safety + resolve-vs-raw classification via `mdz_classify_link`, void -elements, heading ids, table header/body normalization) live in -`mdz_helpers.ts`, and the parity tests bind their outputs. `mdz_to_svelte` -emits **Svelte markup, not HTML** — `{`/`}` escape to Svelte expressions and -attribute values are expressions — so its output cannot be repurposed as raw -HTML. A fourth render target (say an HTML-string emitter) should be added by -parameterizing the emission over a target interface, not as another -hand-synced switch. +elements, element attribute filtering, heading ids, table header/body +normalization) live in `mdz_helpers.ts`, and the parity tests bind their +outputs. `mdz_to_svelte` emits **Svelte markup, not HTML** — `{`/`}` escape +to Svelte expressions and attribute values are expressions — so its output +cannot be repurposed as raw HTML. A fourth render target (say an HTML-string +emitter) should be added by parameterizing the emission over a target +interface, not as another hand-synced switch. ### Pre-computation diff --git a/src/lib/mdz_contexts.ts b/src/lib/mdz_contexts.ts index a4db6a0..7b380aa 100644 --- a/src/lib/mdz_contexts.ts +++ b/src/lib/mdz_contexts.ts @@ -16,9 +16,10 @@ export type MdzComponents = Map>; /** * Element registry for HTML elements that can be used in mdz content. * - * For example, registering 'div' allows using `
...
` in mdz content. - * - * The Map values are boolean placeholders for future configuration options. + * Register an element by mapping its name to `true` (e.g. `['aside', true]`), + * which allows `` in mdz content. Only `true` enables an + * element — the renderers treat any other value (including `false`) as + * unregistered, so the tag falls back to literal text. */ export type MdzElements = Map; diff --git a/src/lib/mdz_helpers.ts b/src/lib/mdz_helpers.ts index 41d824e..b538df4 100644 --- a/src/lib/mdz_helpers.ts +++ b/src/lib/mdz_helpers.ts @@ -842,12 +842,19 @@ export const mdz_attributes_to_props = ( ): Record => Object.fromEntries(attributes.map((a) => [a.name, a.value])); // reconstructs authored attribute text (` name="value"` / ` name`) for the -// unregistered-tag literal placeholder. Values are emitted verbatim; the caller -// interpolates the result as Svelte text (auto-escaped). +// unregistered-tag literal placeholder. The quote char is chosen so a value +// containing one quote type stays balanced — a parsed value holds at most one +// (a `"`-delimited value can't contain `"`, a `'`-delimited one can't contain +// `'`). The caller interpolates the result as Svelte text (auto-escaped). export const mdz_format_unregistered_attributes = (attributes: Array): string => { let text = ''; for (const attr of attributes) { - text += attr.value === true ? ` ${attr.name}` : ` ${attr.name}="${attr.value}"`; + if (attr.value === true) { + text += ` ${attr.name}`; + } else { + const quote = attr.value.includes('"') ? "'" : '"'; + text += ` ${attr.name}=${quote}${attr.value}${quote}`; + } } return text; }; diff --git a/src/test/mdz_helpers.test.ts b/src/test/mdz_helpers.test.ts index a7820a3..27814a7 100644 --- a/src/test/mdz_helpers.test.ts +++ b/src/test/mdz_helpers.test.ts @@ -849,4 +849,14 @@ describe('mdz_format_unregistered_attributes', () => { test('returns an empty string for empty input', () => { assert.equal(mdz_format_unregistered_attributes([]), ''); }); + + test('uses single quotes for a value containing a double quote', () => { + // a `"`-containing value must have been single-quoted at the source + // (a `"`-delimited value can't hold `"`), so the placeholder stays balanced + assert.equal(mdz_format_unregistered_attributes([attr('title', 'a"b')]), " title='a\"b'"); + }); + + test('uses double quotes for a value containing a single quote', () => { + assert.equal(mdz_format_unregistered_attributes([attr('title', "a'b")]), ` title="a'b"`); + }); }); diff --git a/src/test/mdz_render.test.ts b/src/test/mdz_render.test.ts index 7e078f3..7ab4d7c 100644 --- a/src/test/mdz_render.test.ts +++ b/src/test/mdz_render.test.ts @@ -55,6 +55,17 @@ describe('tag attributes', () => { assert.include(result.body, 'hi'); }); + test('an element registered with `false` falls back to a literal placeholder', () => { + // only `true` enables an element — a `false` value is treated as + // unregistered, so the tag renders as source-faithful literal text + // (attributes included) rather than a real `