| author | |
| committer | |
| log | ce070f60cf2f9d6c1966b3cf36c1efc1ff81e796 |
| tree | 79fcaacd261ea3f3edb1cd60f02af9da073c82af |
| parent | 4ab8d7af06ad7ed6563556540f679b77f9374120 |
| signature |
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 | result | 82 | result |
| 83 | } | 83 | } |
| 84 | 84 | ||
| 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)] |
| 88 | pub struct UsedElements { | 88 | pub 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 | } |
| 101 | 102 | ||
| 102 | fn detect_recursive(node: &Node, used: &mut UsedElements) { | 103 | fn 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 node | 122 | 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) { |
| 147 | pub fn generate_layout_boilerplate(used: &UsedElements) -> String { | 166 | pub fn generate_layout_boilerplate(used: &UsedElements) -> String { |
| 148 | let mut out = String::new(); | 167 | let mut out = String::new(); |
| 149 | 168 | ||
| 150 | // Heading — always present when a layout is active | 169 | // 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 | } | ||
| 157 | 178 | ||
| 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 | out | 204 | out |
| 184 | } | 205 | } |
| 185 | 206 | ||
| 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 component | 208 | /// 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>`. | ||
| 190 | pub 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`. | ||
| 200 | pub fn transform_layout_components(node: &mut Node, used: &UsedElements) { | 210 | pub 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 entry | 161 | // 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_block | 167 | code_block: detected.code_block |
| 164 | && component_imports | 168 | && 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 | } |
| 564 | 567 | ||
| 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 so | 570 | // When a document has no headings, the heading boilerplate is omitted |
| 568 | // the layout can use components.heading if it wants to | 571 | // 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 | } |
| 579 | 583 | ||
| ... | @@ -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 | } |
| 842 | 846 | ||
| 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 layout | 868 | // 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 | } |
| 948 | 972 | ||
| 949 | // ------------------------------------------------------------------------- | 973 | // ------------------------------------------------------------------------- |
| 950 | // layout + componentImports.heading conflict | 974 | // layout + componentImports.heading — componentImports wins |
| 951 | // ------------------------------------------------------------------------- | 975 | // ------------------------------------------------------------------------- |
| 952 | 976 | ||
| 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 both | 979 | // When both layoutImport and componentImports.heading are set, |
| 956 | // try to define `HeadingComponent__markodown__` — one via <const> and one | 980 | // 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 | } |
| 988 | 1005 |
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 | } |
| 153 | 154 | ||
| 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 | } |
| 115 | 115 | ||
| 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-h6 | 219 | // 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 pattern | 270 | /// parse variable binding. identifier or destructuring pattern |
| 271 | /// also parses optional `: Type` | 271 | /// also parses optional `: Type` |
| 272 | pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> { | 272 | pub 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> { |
| 313 | pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> { | 312 | pub 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:#?}}}"); | ||
| 319 | 316 | ||
| 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 @@ | ||
| 1 | xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 2 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. | ||
| 3 | |||
| 4 | xxx xxxxx xx xxx xx xxxx xxxxxxxxxx xxx xxx xxxx xxx xxxxxxxxxx, | ||
| 5 | <b>"xxxxxx"</b> xxxxxxxxxx xxx <b>"xxxxxx"</b> | ||
| 6 | xxxxxxxxxx. xxxxxx xxxxxxxxxx xxx'x xxxx `xxxxxxxx`, `xxxxxxxxx`. | ||
| 7 | |||
| 8 | xx xx xxxxxx xxx xxxx xxxxxxx xxxxxx xxxx xx xxxxx xxxxxx, xxx | ||
| 9 | xxx xxxx xxxxxxxxxx xxx xxx xx xxx xxxx xxxxxxxx. | ||