diff --git a/src/component_transforms.rs b/src/component_transforms.rs index 3f7d3d6cb2021514825e38ffa1f6c171407f1e86..ff38cf959011e6da0ad24773695fda3efa95e1a5 100644 --- a/src/component_transforms.rs +++ b/src/component_transforms.rs @@ -82,10 +82,11 @@ pub fn generate_imports(imports: &ComponentImports) -> String { result } -/// Which non-heading markdown element types are present in the document. +/// Which markdown element types are present in the document. /// Used to emit only the necessary layout-component boilerplate. #[derive(Debug, Default, Clone, Copy)] pub struct UsedElements { + pub heading: bool, pub code_block: bool, pub link: bool, pub image: bool, @@ -100,6 +101,24 @@ pub fn detect_used_elements(node: &Node) -> UsedElements { } fn detect_recursive(node: &Node, used: &mut UsedElements) { + if node + .cast::() + .is_some() + || node + .cast::() + .is_some() + || node + .cast::() + .is_some_and(|n| parse_heading_level(n.open.as_ref().tag_name()).is_some()) + || node + .cast::() + .is_some_and(|n| parse_heading_level(n.open.as_ref().tag_name()).is_some()) + || node + .cast::() + .is_some_and(|n| parse_heading_level(n.open.as_ref().tag_name()).is_some()) + { + used.heading = true; + } if node .cast::() .is_some() @@ -147,13 +166,15 @@ fn detect_recursive(node: &Node, used: &mut UsedElements) { pub fn generate_layout_boilerplate(used: &UsedElements) -> String { let mut out = String::new(); - // Heading — always present when a layout is active - out.push_str(concat!( - "\n", - " <${'h' + level} ...attrs><${content} />\n", - "\n", - "\n", - )); + // Heading — only emitted when headings are present and not handled by componentImports + if used.heading { + out.push_str(concat!( + "\n", + " <${'h' + level} ...attrs><${content} />\n", + "\n", + "\n", + )); + } // Code block — fallback renders

     if used.code_block {
@@ -183,24 +204,16 @@ pub fn generate_layout_boilerplate(used: &UsedElements) -> String {
     out
 }
 
-/// Transform heading elements (h1-h6) to use `HeadingComponent__markodown__`.
-/// Called when a layout is active, so the layout can supply a heading component
-/// via its `components.heading` export (with a built-in fallback).
-/// Does not emit an import — the component is resolved at runtime via ``.
-pub fn transform_headings(node: &mut Node) {
-    for child in &mut node.children {
-        transform_headings(child);
-    }
-    transform_heading(node);
-}
-
-/// Transform non-heading elements present in `used` to use their layout-sourced
-/// components. Must be called after outline extraction and heading transforms,
-/// and only for element types not already handled by explicit `componentImports`.
+/// Transform elements present in `used` to use their layout-sourced components.
+/// Must be called after outline extraction so headings are still h1-h6 when collected.
+/// Only transforms element types not already handled by explicit `componentImports`.
 pub fn transform_layout_components(node: &mut Node, used: &UsedElements) {
     for child in &mut node.children {
         transform_layout_components(child, used);
     }
+    if used.heading {
+        transform_heading(node);
+    }
     if used.code_block {
         transform_code_block(node);
     }
diff --git a/src/lib.rs b/src/lib.rs
index 0cd75619ea5652973f8f01815bf137b964e8cf91..e5d880d363fba40c1f3ed35e74bfd02f071b2dde 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -160,6 +160,10 @@ pub fn transform(
         let detected = component_transforms::detect_used_elements(&ast);
         // Only take over types not already handled by an explicit componentImports entry
         let used = component_transforms::UsedElements {
+            heading: detected.heading
+                && component_imports
+                    .as_ref()
+                    .map_or(true, |i| i.heading.is_none()),
             code_block: detected.code_block
                 && component_imports
                     .as_ref()
@@ -177,7 +181,6 @@ pub fn transform(
                     .as_ref()
                     .map_or(true, |i| i.blockquote.is_none()),
         };
-        component_transforms::transform_headings(&mut ast);
         component_transforms::transform_layout_components(&mut ast, &used);
         Some(used)
     } else {
@@ -563,17 +566,18 @@ mod tests {
     }
 
     #[test]
-    fn layout_no_headings_still_emits_boilerplate() {
-        // Even with no headings, the module import and const are emitted so
-        // the layout can use components.heading if it wants to
+    fn layout_no_headings_omits_heading_boilerplate() {
+        // When a document has no headings, the heading boilerplate is omitted
+        // since there is nothing to render through HeadingComponent.
+        // The layout module import is still present for other component lookups.
         let out = run_with_layout("Just some prose.", "./layout.marko");
         assert!(
             out.contains("LayoutModule__markodown__"),
             "LayoutModule import must appear even without headings"
         );
         assert!(
-            out.contains("HeadingComponent__markodown__"),
-            "HeadingComponent const must appear even without headings"
+            !out.contains("HeadingComponent__markodown__"),
+            "HeadingComponent boilerplate must not appear when document has no headings"
         );
     }
 
@@ -840,6 +844,26 @@ mod tests {
         assert!(out.contains("level=2"), "setext h2 should get level=2");
     }
 
+    // -------------------------------------------------------------------------
+    // Marko  tags trigger heading boilerplate
+    // -------------------------------------------------------------------------
+
+    #[test]
+    fn marko_heading_tag_triggers_heading_boilerplate() {
+        // A document with only a raw Marko 

(no markdown headings) should + // still emit heading boilerplate when a layout is active, because the + // detection pass covers Marko nodes in addition to markdown nodes. + let out = run_with_layout("

Hello

", "./l.marko"); + assert!( + out.contains(" should trigger heading fallback define" + ); + assert!( + out.contains(" should trigger HeadingComponent const" + ); + } + // ------------------------------------------------------------------------- // Multi-line Marko heading (

\ncontent\n

) with layout // ------------------------------------------------------------------------- @@ -947,14 +971,14 @@ mod tests { } // ------------------------------------------------------------------------- - // layout + componentImports.heading conflict + // layout + componentImports.heading — componentImports wins // ------------------------------------------------------------------------- #[test] - fn layout_and_component_imports_heading_both_define_same_name() { - // When both layoutImport and componentImports.heading are set, they both - // try to define `HeadingComponent__markodown__` — one via and one - // via import. This pins the current behavior so any change is deliberate. + fn layout_and_component_imports_heading_no_conflict() { + // When both layoutImport and componentImports.heading are set, + // componentImports.heading takes precedence: only the explicit import + // is emitted; the layout boilerplate is suppressed. let out = transform( "# Hello", None, @@ -969,20 +993,13 @@ mod tests { ) .unwrap() .text; - // Both definitions appear — this is a known conflict. - // The from the layout boilerplate wins at runtime in Marko - // because it appears before the import in the rendered output. - assert!( - out.contains("HeadingComponent__markodown__"), - "HeadingComponent name must appear" - ); assert!( out.contains("import HeadingComponent__markodown__ from \"./h.marko\""), "explicit heading import should appear" ); assert!( - out.contains(" Result { } parse_fn_params_and_body(&mut l)?; } + + if is_attribute_tag { + return Err(err( + "Attribute tags do not support arguments", + offset, + (l.offset - offset) as usize, + )); + } + l.skip_whitespace(); has_js_arguments = true; } @@ -129,26 +138,18 @@ pub fn parse_open(src: &str) -> Result { l.offset += 1; l.skip_whitespace(); parse_var_binding(&mut l)?; + if is_attribute_tag { + return Err(err( + "Attribute tags do not support variables", + offset, + (l.offset - offset) as usize, + )); + } l.skip_whitespace(); has_js_variable = true; } _ => unreachable!(), } - - if is_attribute_tag && byte == b'/' { - return Err(err( - "Attribute tags do not support variables", - offset, - (l.offset - offset) as usize, - )); - } - if is_attribute_tag && byte == b'(' { - return Err(err( - "Attribute tags do not support arguments", - offset, - (l.offset - offset) as usize, - )); - } } let mut self_closing = false; diff --git a/src/marko_ast.rs b/src/marko_ast.rs index 7aafa5c7f24fa9ca92d2e72e2c5aee6a93a83728..ed272b4458b59abe2556ec7d363efcd1daeba464 100644 --- a/src/marko_ast.rs +++ b/src/marko_ast.rs @@ -113,9 +113,8 @@ impl OpenOwned { } } - /// Set the id attribute to a static string value. - /// If an id already exists (shorthand or attribute), it is replaced. - /// If no id exists, a shorthand #id is inserted after the tag name/classes. + /// Set the id attribute to a static string value. Asserts there is not + /// an ID attribute present; caller should prefer the existing one. pub fn insert_id_attr(&mut self, new_id: &str) { match self.id { AttributeValue::None => { diff --git a/src/outline.rs b/src/outline.rs index b3b61fef893cde04a2a56f1b27f9ded894e0ff03..4992afc7e8b45343414ae50953198ebb1cba645c 100644 --- a/src/outline.rs +++ b/src/outline.rs @@ -216,38 +216,6 @@ fn collect_recursive( component_name, }); } - // Check for setext headings (underline style) - else if let Some(heading) = - node.cast::() - { - let level = heading.level; - let text = node.collect_text(); - let id = generate_slug(&text, existing_ids); - - *heading_counter += 1; - let component_name = format!("Heading_{heading_counter}__markodown__"); - - let rendered_content = render_children(node); - - let trimmed_content = rendered_content.trim(); - defines.push_str(&format!( - "\n{trimmed_content}\n\n" - )); - - node.children.clear(); - node.children.push(Node::new(HeadingContentRef { - component_name: component_name.clone(), - })); - - node.attrs.push(("id", id.clone())); - - headings.push(HeadingEntry { - level, - id, - text, - component_name, - }); - } // Check for MarkoOpen tags that are h1-h6 else if node.cast::().is_some() { let heading_info = { diff --git a/src/typescript.rs b/src/typescript.rs index 2c32b4b6edf1768ae96d06557aecc72d2c1312bd..72ef4774c4eaaecd49e4afe96d95004ed3941e60 100644 --- a/src/typescript.rs +++ b/src/typescript.rs @@ -270,7 +270,6 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result { /// parse variable binding. identifier or destructuring pattern /// also parses optional `: Type` pub fn parse_var_binding(l: &mut LexState) -> Result { - // TODO: this approach has bugs let offset = l.offset(); let source = l.peek_rest(); if source.is_empty() { @@ -313,9 +312,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result { pub fn parse_type(l: &mut LexState) -> Result { let mut allocator = Allocator::default(); let source = format!("T as {}", l.peek_rest()); - println!("{{{source}}} HUH"); let expr = parse_expr_extra(source.as_str(), l.offset().cast_signed(), &mut allocator)?; - println!("{{{expr:#?}}}"); let span = find_leftmost(&expr, LeftmostSearch::As).ok_or_else(|| { err( diff --git a/wtf.mdo b/wtf.mdo deleted file mode 100644 index f414d22c61447b8b40c4500a44a21868d1935541..0000000000000000000000000000000000000000 --- a/wtf.mdo +++ /dev/null @@ -1,9 +0,0 @@ -xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx -xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. - -xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx, -"xxxxxx" xxxxxxxxxx xxx "xxxxxx" -xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`. - -xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx -xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx.