| 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 | 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 | 86 | /// Used to emit only the necessary layout-component boilerplate. |
| 87 | 87 | #[derive(Debug, Default, Clone, Copy)] |
| 88 | 88 | pub struct UsedElements { |
| 89 | pub heading: bool, | |
| 89 | 90 | pub code_block: bool, |
| 90 | 91 | pub link: bool, |
| 91 | 92 | pub image: bool, |
| ... | ... | @@ -100,6 +101,24 @@ pub fn detect_used_elements(node: &Node) -> UsedElements { |
| 100 | 101 | } |
| 101 | 102 | |
| 102 | 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 | 122 | if node |
| 104 | 123 | .cast::<markdown_it::plugins::cmark::block::fence::CodeFence>() |
| 105 | 124 | .is_some() |
| ... | ... | @@ -147,13 +166,15 @@ fn detect_recursive(node: &Node, used: &mut UsedElements) { |
| 147 | 166 | pub fn generate_layout_boilerplate(used: &UsedElements) -> String { |
| 148 | 167 | let mut out = String::new(); |
| 149 | 168 | |
| 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 | } | |
| 157 | 178 | |
| 158 | 179 | // Code block — fallback renders <pre><code class="language-..."> |
| 159 | 180 | if used.code_block { |
| ... | ... | @@ -183,24 +204,16 @@ pub fn generate_layout_boilerplate(used: &UsedElements) -> String { |
| 183 | 204 | out |
| 184 | 205 | } |
| 185 | 206 | |
| 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>`. | |
| 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`. | |
| 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`. | |
| 200 | 210 | pub fn transform_layout_components(node: &mut Node, used: &UsedElements) { |
| 201 | 211 | for child in &mut node.children { |
| 202 | 212 | transform_layout_components(child, used); |
| 203 | 213 | } |
| 214 | if used.heading { | |
| 215 | transform_heading(node); | |
| 216 | } | |
| 204 | 217 | if used.code_block { |
| 205 | 218 | transform_code_block(node); |
| 206 | 219 | } |
src/lib.rs+37-20| ... | ... | @@ -160,6 +160,10 @@ pub fn transform( |
| 160 | 160 | let detected = component_transforms::detect_used_elements(&ast); |
| 161 | 161 | // Only take over types not already handled by an explicit componentImports entry |
| 162 | 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 | 167 | code_block: detected.code_block |
| 164 | 168 | && component_imports |
| 165 | 169 | .as_ref() |
| ... | ... | @@ -177,7 +181,6 @@ pub fn transform( |
| 177 | 181 | .as_ref() |
| 178 | 182 | .map_or(true, |i| i.blockquote.is_none()), |
| 179 | 183 | }; |
| 180 | component_transforms::transform_headings(&mut ast); | |
| 181 | 184 | component_transforms::transform_layout_components(&mut ast, &used); |
| 182 | 185 | Some(used) |
| 183 | 186 | } else { |
| ... | ... | @@ -563,17 +566,18 @@ mod tests { |
| 563 | 566 | } |
| 564 | 567 | |
| 565 | 568 | #[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. | |
| 569 | 573 | let out = run_with_layout("Just some prose.", "./layout.marko"); |
| 570 | 574 | assert!( |
| 571 | 575 | out.contains("LayoutModule__markodown__"), |
| 572 | 576 | "LayoutModule import must appear even without headings" |
| 573 | 577 | ); |
| 574 | 578 | 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" | |
| 577 | 581 | ); |
| 578 | 582 | } |
| 579 | 583 | |
| ... | ... | @@ -840,6 +844,26 @@ mod tests { |
| 840 | 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 | 868 | // Multi-line Marko heading (<h2>\ncontent\n</h2>) with layout |
| 845 | 869 | // ------------------------------------------------------------------------- |
| ... | ... | @@ -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 | 977 | #[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. | |
| 958 | 982 | let out = transform( |
| 959 | 983 | "# Hello", |
| 960 | 984 | None, |
| ... | ... | @@ -969,20 +993,13 @@ mod tests { |
| 969 | 993 | ) |
| 970 | 994 | .unwrap() |
| 971 | 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 | 996 | assert!( |
| 980 | 997 | out.contains("import HeadingComponent__markodown__ from \"./h.marko\""), |
| 981 | 998 | "explicit heading import should appear" |
| 982 | 999 | ); |
| 983 | 1000 | 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" | |
| 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 | 116 | } |
| 117 | 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 | 128 | l.skip_whitespace(); |
| 120 | 129 | has_js_arguments = true; |
| 121 | 130 | } |
| ... | ... | @@ -129,26 +138,18 @@ pub fn parse_open(src: &str) -> Result<Open, OxcDiagnostic> { |
| 129 | 138 | l.offset += 1; |
| 130 | 139 | l.skip_whitespace(); |
| 131 | 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 | 148 | l.skip_whitespace(); |
| 133 | 149 | has_js_variable = true; |
| 134 | 150 | } |
| 135 | 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 | 155 | let mut self_closing = false; |
src/marko_ast.rs+2-3| ... | ... | @@ -113,9 +113,8 @@ impl OpenOwned { |
| 113 | 113 | } |
| 114 | 114 | } |
| 115 | 115 | |
| 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. | |
| 119 | 118 | pub fn insert_id_attr(&mut self, new_id: &str) { |
| 120 | 119 | match self.id { |
| 121 | 120 | AttributeValue::None => { |
src/outline.rs-32| ... | ... | @@ -216,38 +216,6 @@ fn collect_recursive( |
| 216 | 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 | 219 | // Check for MarkoOpen tags that are h1-h6 |
| 252 | 220 | else if node.cast::<MarkoOpen>().is_some() { |
| 253 | 221 | let heading_info = { |
src/typescript.rs-3| ... | ... | @@ -270,7 +270,6 @@ pub fn parse_call_arguments(l: &mut LexState) -> Result<u32, OxcDiagnostic> { |
| 270 | 270 | /// parse variable binding. identifier or destructuring pattern |
| 271 | 271 | /// also parses optional `: Type` |
| 272 | 272 | pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> { |
| 273 | // TODO: this approach has bugs | |
| 274 | 273 | let offset = l.offset(); |
| 275 | 274 | let source = l.peek_rest(); |
| 276 | 275 | if source.is_empty() { |
| ... | ... | @@ -313,9 +312,7 @@ pub fn parse_var_binding(l: &mut LexState) -> Result<u32, OxcDiagnostic> { |
| 313 | 312 | pub fn parse_type(l: &mut LexState) -> Result<u32, OxcDiagnostic> { |
| 314 | 313 | let mut allocator = Allocator::default(); |
| 315 | 314 | let source = format!("T as {}", l.peek_rest()); |
| 316 | println!("{{{source}}} HUH"); | |
| 317 | 315 | let expr = parse_expr_extra(source.as_str(), l.offset().cast_signed(), &mut allocator)?; |
| 318 | println!("{{{expr:#?}}}"); | |
| 319 | 316 | |
| 320 | 317 | let span = find_leftmost(&expr, LeftmostSearch::As).ok_or_else(|| { |
| 321 | 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. |