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 {...@@ -82,10 +82,11 @@ pub fn generate_imports(imports: &ComponentImports) -> String {
82 result82 result
83}83}
8484
85/// Which non-heading markdown element types are present in the document.85/// Which markdown element types are present in the document.
86/// Used to emit only the necessary layout-component boilerplate.86/// Used to emit only the necessary layout-component boilerplate.
87#[derive(Debug, Default, Clone, Copy)]87#[derive(Debug, Default, Clone, Copy)]
88pub struct UsedElements {88pub struct UsedElements {
89 pub heading: bool,
89 pub code_block: bool,90 pub code_block: bool,
90 pub link: bool,91 pub link: bool,
91 pub image: bool,92 pub image: bool,
...@@ -100,6 +101,24 @@ pub fn detect_used_elements(node: &Node) -> UsedElements {...@@ -100,6 +101,24 @@ pub fn detect_used_elements(node: &Node) -> UsedElements {
100}101}
101102
102fn detect_recursive(node: &Node, used: &mut UsedElements) {103fn 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 }
103 if node122 if node
104 .cast::<markdown_it::plugins::cmark::block::fence::CodeFence>()123 .cast::<markdown_it::plugins::cmark::block::fence::CodeFence>()
105 .is_some()124 .is_some()
...@@ -147,13 +166,15 @@ fn detect_recursive(node: &Node, used: &mut UsedElements) {...@@ -147,13 +166,15 @@ fn detect_recursive(node: &Node, used: &mut UsedElements) {
147pub fn generate_layout_boilerplate(used: &UsedElements) -> String {166pub fn generate_layout_boilerplate(used: &UsedElements) -> String {
148 let mut out = String::new();167 let mut out = String::new();
149168
150 // Heading — always present when a layout is active169 // Heading — only emitted when headings are present and not handled by componentImports
151 out.push_str(concat!(170 if used.heading {
152 "<define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|>\n",171 out.push_str(concat!(
153 " <${'h' + level} ...attrs><${content} /></>\n",172 "<define/HeadingComponentFallback__markodown__|{ level, content, ...attrs }|>\n",
154 "</>\n",173 " <${'h' + level} ...attrs><${content} /></>\n",
155 "<const/HeadingComponent__markodown__ = LayoutModule__markodown__.components?.heading ?? HeadingComponentFallback__markodown__ />\n",174 "</>\n",
156 ));175 "<const/HeadingComponent__markodown__ = LayoutModule__markodown__.components?.heading ?? HeadingComponentFallback__markodown__ />\n",
176 ));
177 }
157178
158 // Code block — fallback renders <pre><code class="language-...">179 // Code block — fallback renders <pre><code class="language-...">
159 if used.code_block {180 if used.code_block {
...@@ -183,24 +204,16 @@ pub fn generate_layout_boilerplate(used: &UsedElements) -> String {...@@ -183,24 +204,16 @@ pub fn generate_layout_boilerplate(used: &UsedElements) -> String {
183 out204 out
184}205}
185206
186/// Transform heading elements (h1-h6) to use `HeadingComponent__markodown__`.207/// Transform elements present in `used` to use their layout-sourced components.
187/// Called when a layout is active, so the layout can supply a heading component208/// Must be called after outline extraction so headings are still h1-h6 when collected.
188/// via its `components.heading` export (with a built-in fallback).209/// Only transforms element types not already handled by explicit `componentImports`.
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`.
200pub fn transform_layout_components(node: &mut Node, used: &UsedElements) {210pub fn transform_layout_components(node: &mut Node, used: &UsedElements) {
201 for child in &mut node.children {211 for child in &mut node.children {
202 transform_layout_components(child, used);212 transform_layout_components(child, used);
203 }213 }
214 if used.heading {
215 transform_heading(node);
216 }
204 if used.code_block {217 if used.code_block {
205 transform_code_block(node);218 transform_code_block(node);
206 }219 }
src/lib.rs+37-20
...@@ -160,6 +160,10 @@ pub fn transform(...@@ -160,6 +160,10 @@ pub fn transform(
160 let detected = component_transforms::detect_used_elements(&ast);160 let detected = component_transforms::detect_used_elements(&ast);
161 // Only take over types not already handled by an explicit componentImports entry161 // Only take over types not already handled by an explicit componentImports entry
162 let used = component_transforms::UsedElements {162 let used = component_transforms::UsedElements {
163 heading: detected.heading
164 && component_imports
165 .as_ref()
166 .map_or(true, |i| i.heading.is_none()),
163 code_block: detected.code_block167 code_block: detected.code_block
164 && component_imports168 && component_imports
165 .as_ref()169 .as_ref()
...@@ -177,7 +181,6 @@ pub fn transform(...@@ -177,7 +181,6 @@ pub fn transform(
177 .as_ref()181 .as_ref()
178 .map_or(true, |i| i.blockquote.is_none()),182 .map_or(true, |i| i.blockquote.is_none()),
179 };183 };
180 component_transforms::transform_headings(&mut ast);
181 component_transforms::transform_layout_components(&mut ast, &used);184 component_transforms::transform_layout_components(&mut ast, &used);
182 Some(used)185 Some(used)
183 } else {186 } else {
...@@ -563,17 +566,18 @@ mod tests {...@@ -563,17 +566,18 @@ mod tests {
563 }566 }
564567
565 #[test]568 #[test]
566 fn layout_no_headings_still_emits_boilerplate() {569 fn layout_no_headings_omits_heading_boilerplate() {
567 // Even with no headings, the module import and const are emitted so570 // When a document has no headings, the heading boilerplate is omitted
568 // the layout can use components.heading if it wants to571 // since there is nothing to render through HeadingComponent.
572 // The layout module import is still present for other component lookups.
569 let out = run_with_layout("Just some prose.", "./layout.marko");573 let out = run_with_layout("Just some prose.", "./layout.marko");
570 assert!(574 assert!(
571 out.contains("LayoutModule__markodown__"),575 out.contains("LayoutModule__markodown__"),
572 "LayoutModule import must appear even without headings"576 "LayoutModule import must appear even without headings"
573 );577 );
574 assert!(578 assert!(
575 out.contains("HeadingComponent__markodown__"),579 !out.contains("HeadingComponent__markodown__"),
576 "HeadingComponent const must appear even without headings"580 "HeadingComponent boilerplate must not appear when document has no headings"
577 );581 );
578 }582 }
579583
...@@ -840,6 +844,26 @@ mod tests {...@@ -840,6 +844,26 @@ mod tests {
840 assert!(out.contains("level=2"), "setext h2 should get level=2");844 assert!(out.contains("level=2"), "setext h2 should get level=2");
841 }845 }
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
843 // -------------------------------------------------------------------------867 // -------------------------------------------------------------------------
844 // Multi-line Marko heading (<h2>\ncontent\n</h2>) with layout868 // Multi-line Marko heading (<h2>\ncontent\n</h2>) with layout
845 // -------------------------------------------------------------------------869 // -------------------------------------------------------------------------
...@@ -947,14 +971,14 @@ mod tests {...@@ -947,14 +971,14 @@ mod tests {
947 }971 }
948972
949 // -------------------------------------------------------------------------973 // -------------------------------------------------------------------------
950 // layout + componentImports.heading conflict974 // layout + componentImports.heading — componentImports wins
951 // -------------------------------------------------------------------------975 // -------------------------------------------------------------------------
952976
953 #[test]977 #[test]
954 fn layout_and_component_imports_heading_both_define_same_name() {978 fn layout_and_component_imports_heading_no_conflict() {
955 // When both layoutImport and componentImports.heading are set, they both979 // When both layoutImport and componentImports.heading are set,
956 // try to define `HeadingComponent__markodown__` — one via <const> and one980 // componentImports.heading takes precedence: only the explicit import
957 // via import. This pins the current behavior so any change is deliberate.981 // is emitted; the layout boilerplate <const> is suppressed.
958 let out = transform(982 let out = transform(
959 "# Hello",983 "# Hello",
960 None,984 None,
...@@ -969,20 +993,13 @@ mod tests {...@@ -969,20 +993,13 @@ mod tests {
969 )993 )
970 .unwrap()994 .unwrap()
971 .text;995 .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 );
979 assert!(996 assert!(
980 out.contains("import HeadingComponent__markodown__ from \"./h.marko\""),997 out.contains("import HeadingComponent__markodown__ from \"./h.marko\""),
981 "explicit heading import should appear"998 "explicit heading import should appear"
982 );999 );
983 assert!(1000 assert!(
984 out.contains("<const/HeadingComponent__markodown__"),1001 !out.contains("<const/HeadingComponent__markodown__"),
985 "layout const should also appear"1002 "layout boilerplate const must be suppressed when componentImports.heading is set"
986 );1003 );
987 }1004 }
9881005
src/marko.rs+16-15
...@@ -116,6 +116,15 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {...@@ -116,6 +116,15 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
116 }116 }
117 parse_fn_params_and_body(&mut l)?;117 parse_fn_params_and_body(&mut l)?;
118 }118 }
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
119 l.skip_whitespace();128 l.skip_whitespace();
120 has_js_arguments = true;129 has_js_arguments = true;
121 }130 }
...@@ -129,26 +138,18 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {...@@ -129,26 +138,18 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> {
129 l.offset += 1;138 l.offset += 1;
130 l.skip_whitespace();139 l.skip_whitespace();
131 parse_var_binding(&mut l)?;140 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 }
132 l.skip_whitespace();148 l.skip_whitespace();
133 has_js_variable = true;149 has_js_variable = true;
134 }150 }
135 _ => unreachable!(),151 _ => unreachable!(),
136 }152 }
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 }
152 }153 }
153154
154 let mut self_closing = false;155 let mut self_closing = false;
src/marko_ast.rs+2-3
...@@ -113,9 +113,8 @@ impl OpenOwned {...@@ -113,9 +113,8 @@ impl OpenOwned {
113 }113 }
114 }114 }
115115
116 /// Set the id attribute to a static string value.116 /// Set the id attribute to a static string value. Asserts there is not
117 /// If an id already exists (shorthand or attribute), it is replaced.117 /// an ID attribute present; caller should prefer the existing one.
118 /// If no id exists, a shorthand #id is inserted after the tag name/classes.
119 pub fn insert_id_attr(&mut self, new_id: &str) {118 pub fn insert_id_attr(&mut self, new_id: &str) {
120 match self.id {119 match self.id {
121 AttributeValue::None => {120 AttributeValue::None => {
src/outline.rs-32
...@@ -216,38 +216,6 @@ fn collect_recursive(...@@ -216,38 +216,6 @@ fn collect_recursive(
216 component_name,216 component_name,
217 });217 });
218 }218 }
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 }
251 // Check for MarkoOpen tags that are h1-h6219 // Check for MarkoOpen tags that are h1-h6
252 else if node.cast::<MarkoOpen>().is_some() {220 else if node.cast::<MarkoOpen>().is_some() {
253 let heading_info = {221 let heading_info = {
src/typescript.rs-3
...@@ -270,7 +270,6 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result<u32, OxcDiagnostic> {...@@ -270,7 +270,6 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
270/// parse variable binding. identifier or destructuring pattern270/// parse variable binding. identifier or destructuring pattern
271/// also parses optional `: Type`271/// also parses optional `: Type`
272pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {272pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
273 // TODO: this approach has bugs
274 let offset = l.offset();273 let offset = l.offset();
275 let source = l.peek_rest();274 let source = l.peek_rest();
276 if source.is_empty() {275 if source.is_empty() {
...@@ -313,9 +312,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {...@@ -313,9 +312,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
313pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> {312pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> {
314 let mut allocator = Allocator::default();313 let mut allocator = Allocator::default();
315 let source = format!("T as {}", l.peek_rest());314 let source = format!("T as {}", l.peek_rest());
316 println!("{{{source}}} HUH");
317 let expr = parse_expr_extra(source.as_str(), l.offset().cast_signed(), &mut allocator)?;315 let expr = parse_expr_extra(source.as_str(), l.offset().cast_signed(), &mut allocator)?;
318 println!("{{{expr:#?}}}");
319316
320 let span = find_leftmost(&expr, LeftmostSearch::As).ok_or_else(|| {317 let span = find_leftmost(&expr, LeftmostSearch::As).ok_or_else(|| {
321 err(318 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.