authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-17 23:38:13-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-02-18 02:46:06-08:00
logce070f60cf2f9d6c1966b3cf36c1efc1ff81e796
tree79fcaacd261ea3f3edb1cd60f02af9da073c82af
parent4ab8d7af06ad7ed6563556540f679b77f9374120
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

fix: meow


7 files changed, 90 insertions(+), 104 deletions(-)

src/component_transforms.rs+35-22
......@@ -82,10 +82,11 @@ pub fn generate_imports(imports: &ComponentImports) -> String {
8282 result
8383}
8484
85/// Which non-heading markdown element types are present in the document.
85/// Which markdown element types are present in the document.
8686/// Used to emit only the necessary layout-component boilerplate.
8787#[derive(Debug, Default, Clone, Copy)]
8888pub struct UsedElements {
89 pub heading: bool,
8990 pub code_block: bool,
9091 pub link: bool,
9192 pub image: bool,
......@@ -100,6 +101,24 @@ pub fn detect_used_elements(node: &Node) -> UsedElements {
100101}
101102
102103fn detect_recursive(node: &Node, used: &mut UsedElements) {
104 if node
105 .cast::<markdown_it::plugins::cmark::block::heading::ATXHeading>()
106 .is_some()
107 || node
108 .cast::<markdown_it::plugins::cmark::block::lheading::SetextHeader>()
109 .is_some()
110 || node
111 .cast::<MarkoOpen>()
112 .is_some_and(|n| parse_heading_level(n.open.as_ref().tag_name()).is_some())
113 || node
114 .cast::<MarkoOpenWithText>()
115 .is_some_and(|n| parse_heading_level(n.open.as_ref().tag_name()).is_some())
116 || node
117 .cast::<MarkoBlockComplete>()
118 .is_some_and(|n| parse_heading_level(n.open.as_ref().tag_name()).is_some())
119 {
120 used.heading = true;
121 }
103122 if node
104123 .cast::<markdown_it::plugins::cmark::block::fence::CodeFence>()
105124 .is_some()
......@@ -147,13 +166,15 @@ fn detect_recursive(node: &Node, used: &mut UsedElements) {
147166pub fn generate_layout_boilerplate(used: &UsedElements) -> String {
148167 let mut out = String::new();
149168
150 // Heading — always present when a layout is active
151 out.push_str(concat!(
152 "<define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|>\n",
153 " <${'h' + level} ...attrs><${content} /></>\n",
154 "</>\n",
155 "<const/HeadingComponent__markodown__ = LayoutModule__markodown__.components?.heading ?? HeadingComponentFallback__markodown__ />\n",
156 ));
169 // Heading — only emitted when headings are present and not handled by componentImports
170 if used.heading {
171 out.push_str(concat!(
172 "<define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|>\n",
173 " <${'h' + level} ...attrs><${content} /></>\n",
174 "</>\n",
175 "<const/HeadingComponent__markodown__ = LayoutModule__markodown__.components?.heading ?? HeadingComponentFallback__markodown__ />\n",
176 ));
177 }
157178
158179 // Code block — fallback renders <pre><code class="language-...">
159180 if used.code_block {
......@@ -183,24 +204,16 @@ pub fn generate_layout_boilerplate(used: &UsedElements) -> String {
183204 out
184205}
185206
186/// Transform heading elements (h1-h6) to use `HeadingComponent__markodown__`.
187/// Called when a layout is active, so the layout can supply a heading component
188/// via its `components.heading` export (with a built-in fallback).
189/// Does not emit an import — the component is resolved at runtime via `<const>`.
190pub fn transform_headings(node: &mut Node) {
191 for child in &mut node.children {
192 transform_headings(child);
193 }
194 transform_heading(node);
195}
196
197/// Transform non-heading elements present in `used` to use their layout-sourced
198/// components. Must be called after outline extraction and heading transforms,
199/// and only for element types not already handled by explicit `componentImports`.
207/// Transform elements present in `used` to use their layout-sourced components.
208/// Must be called after outline extraction so headings are still h1-h6 when collected.
209/// Only transforms element types not already handled by explicit `componentImports`.
200210pub fn transform_layout_components(node: &mut Node, used: &UsedElements) {
201211 for child in &mut node.children {
202212 transform_layout_components(child, used);
203213 }
214 if used.heading {
215 transform_heading(node);
216 }
204217 if used.code_block {
205218 transform_code_block(node);
206219 }
src/lib.rs+37-20
......@@ -160,6 +160,10 @@ pub fn transform(
160160 let detected = component_transforms::detect_used_elements(&ast);
161161 // Only take over types not already handled by an explicit componentImports entry
162162 let used = component_transforms::UsedElements {
163 heading: detected.heading
164 && component_imports
165 .as_ref()
166 .map_or(true, |i| i.heading.is_none()),
163167 code_block: detected.code_block
164168 && component_imports
165169 .as_ref()
......@@ -177,7 +181,6 @@ pub fn transform(
177181 .as_ref()
178182 .map_or(true, |i| i.blockquote.is_none()),
179183 };
180 component_transforms::transform_headings(&mut ast);
181184 component_transforms::transform_layout_components(&mut ast, &used);
182185 Some(used)
183186 } else {
......@@ -563,17 +566,18 @@ mod tests {
563566 }
564567
565568 #[test]
566 fn layout_no_headings_still_emits_boilerplate() {
567 // Even with no headings, the module import and const are emitted so
568 // the layout can use components.heading if it wants to
569 fn layout_no_headings_omits_heading_boilerplate() {
570 // When a document has no headings, the heading boilerplate is omitted
571 // since there is nothing to render through HeadingComponent.
572 // The layout module import is still present for other component lookups.
569573 let out = run_with_layout("Just some prose.", "./layout.marko");
570574 assert!(
571575 out.contains("LayoutModule__markodown__"),
572576 "LayoutModule import must appear even without headings"
573577 );
574578 assert!(
575 out.contains("HeadingComponent__markodown__"),
576 "HeadingComponent const must appear even without headings"
579 !out.contains("HeadingComponent__markodown__"),
580 "HeadingComponent boilerplate must not appear when document has no headings"
577581 );
578582 }
579583
......@@ -840,6 +844,26 @@ mod tests {
840844 assert!(out.contains("level=2"), "setext h2 should get level=2");
841845 }
842846
847 // -------------------------------------------------------------------------
848 // Marko <hN> tags trigger heading boilerplate
849 // -------------------------------------------------------------------------
850
851 #[test]
852 fn marko_heading_tag_triggers_heading_boilerplate() {
853 // A document with only a raw Marko <h2> (no markdown headings) should
854 // still emit heading boilerplate when a layout is active, because the
855 // detection pass covers Marko <hN> nodes in addition to markdown nodes.
856 let out = run_with_layout("<h2>Hello</h2>", "./l.marko");
857 assert!(
858 out.contains("<define/HeadingComponentFallback__markodown__"),
859 "Marko <h2> should trigger heading fallback define"
860 );
861 assert!(
862 out.contains("<const/HeadingComponent__markodown__"),
863 "Marko <h2> should trigger HeadingComponent const"
864 );
865 }
866
843867 // -------------------------------------------------------------------------
844868 // Multi-line Marko heading (<h2>\ncontent\n</h2>) with layout
845869 // -------------------------------------------------------------------------
......@@ -947,14 +971,14 @@ mod tests {
947971 }
948972
949973 // -------------------------------------------------------------------------
950 // layout + componentImports.heading conflict
974 // layout + componentImports.heading — componentImports wins
951975 // -------------------------------------------------------------------------
952976
953977 #[test]
954 fn layout_and_component_imports_heading_both_define_same_name() {
955 // When both layoutImport and componentImports.heading are set, they both
956 // try to define `HeadingComponent__markodown__` — one via <const> and one
957 // via import. This pins the current behavior so any change is deliberate.
978 fn layout_and_component_imports_heading_no_conflict() {
979 // When both layoutImport and componentImports.heading are set,
980 // componentImports.heading takes precedence: only the explicit import
981 // is emitted; the layout boilerplate <const> is suppressed.
958982 let out = transform(
959983 "# Hello",
960984 None,
......@@ -969,20 +993,13 @@ mod tests {
969993 )
970994 .unwrap()
971995 .text;
972 // Both definitions appear — this is a known conflict.
973 // The <const> from the layout boilerplate wins at runtime in Marko
974 // because it appears before the import in the rendered output.
975 assert!(
976 out.contains("HeadingComponent__markodown__"),
977 "HeadingComponent name must appear"
978 );
979996 assert!(
980997 out.contains("import HeadingComponent__markodown__ from \"./h.marko\""),
981998 "explicit heading import should appear"
982999 );
9831000 assert!(
984 out.contains("<const/HeadingComponent__markodown__"),
985 "layout const should also appear"
1001 !out.contains("<const/HeadingComponent__markodown__"),
1002 "layout boilerplate const must be suppressed when componentImports.heading is set"
9861003 );
9871004 }
9881005
src/marko.rs+16-15
......@@ -116,6 +116,15 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
116116 }
117117 parse_fn_params_and_body(&mut l)?;
118118 }
119
120 if is_attribute_tag {
121 return Err(err(
122 "Attribute tags do not support arguments",
123 offset,
124 (l.offset - offset) as usize,
125 ));
126 }
127
119128 l.skip_whitespace();
120129 has_js_arguments = true;
121130 }
......@@ -129,26 +138,18 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
129138 l.offset += 1;
130139 l.skip_whitespace();
131140 parse_var_binding(&mut l)?;
141 if is_attribute_tag {
142 return Err(err(
143 "Attribute tags do not support variables",
144 offset,
145 (l.offset - offset) as usize,
146 ));
147 }
132148 l.skip_whitespace();
133149 has_js_variable = true;
134150 }
135151 _ => unreachable!(),
136152 }
137
138 if is_attribute_tag && byte == b'/' {
139 return Err(err(
140 "Attribute tags do not support variables",
141 offset,
142 (l.offset - offset) as usize,
143 ));
144 }
145 if is_attribute_tag && byte == b'(' {
146 return Err(err(
147 "Attribute tags do not support arguments",
148 offset,
149 (l.offset - offset) as usize,
150 ));
151 }
152153 }
153154
154155 let mut self_closing = false;
src/marko_ast.rs+2-3
......@@ -113,9 +113,8 @@ impl OpenOwned {
113113 }
114114 }
115115
116 /// Set the id attribute to a static string value.
117 /// If an id already exists (shorthand or attribute), it is replaced.
118 /// If no id exists, a shorthand #id is inserted after the tag name/classes.
116 /// Set the id attribute to a static string value. Asserts there is not
117 /// an ID attribute present; caller should prefer the existing one.
119118 pub fn insert_id_attr(&mut self, new_id: &str) {
120119 match self.id {
121120 AttributeValue::None => {
src/outline.rs-32
......@@ -216,38 +216,6 @@ fn collect_recursive(
216216 component_name,
217217 });
218218 }
219 // Check for setext headings (underline style)
220 else if let Some(heading) =
221 node.cast::<markdown_it::plugins::cmark::block::lheading::SetextHeader>()
222 {
223 let level = heading.level;
224 let text = node.collect_text();
225 let id = generate_slug(&text, existing_ids);
226
227 *heading_counter += 1;
228 let component_name = format!("Heading_{heading_counter}__markodown__");
229
230 let rendered_content = render_children(node);
231
232 let trimmed_content = rendered_content.trim();
233 defines.push_str(&format!(
234 "<define/{component_name}>\n{trimmed_content}\n</>\n"
235 ));
236
237 node.children.clear();
238 node.children.push(Node::new(HeadingContentRef {
239 component_name: component_name.clone(),
240 }));
241
242 node.attrs.push(("id", id.clone()));
243
244 headings.push(HeadingEntry {
245 level,
246 id,
247 text,
248 component_name,
249 });
250 }
251219 // Check for MarkoOpen tags that are h1-h6
252220 else if node.cast::<MarkoOpen>().is_some() {
253221 let heading_info = {
src/typescript.rs-3
......@@ -270,7 +270,6 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
270270/// parse variable binding. identifier or destructuring pattern
271271/// also parses optional `: Type`
272272pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
273 // TODO: this approach has bugs
274273 let offset = l.offset();
275274 let source = l.peek_rest();
276275 if source.is_empty() {
......@@ -313,9 +312,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
313312pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
314313 let mut allocator = Allocator::default();
315314 let source = format!("T as {}", l.peek_rest());
316 println!("{{{source}}} HUH");
317315 let expr = parse_expr_extra(source.as_str(), l.offset().cast_signed(), &mut allocator)?;
318 println!("{{{expr:#?}}}");
319316
320317 let span = find_leftmost(&expr, LeftmostSearch::As).ok_or_else(|| {
321318 err(
wtf.mdo deleted-9
......@@ -1,9 +0,0 @@
1xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx
2xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.
3
4xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx,
5<b>"xxxxxx"</b> xxxxxxxxxx xxx <b>"xxxxxx"</b>
6xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`.
7
8xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx
9xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.