From aca1b27d0ed5c6a73be9851f71733839cb7615f3 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sun, 15 Feb 2026 21:47:05 -0800 Subject: [PATCH] fix: emit statements at top of file --- .../marko-run/src/tags/markdown-layout.marko | 81 ++++++++++++++++++- src/lib.rs | 61 ++++++++++---- src/plugin/frontmatter.rs | 4 +- src/plugin/mod.rs | 15 +++- src/plugin/statement.rs | 4 +- tests/fixtures.rs | 4 +- tests/fixtures/13-kitchen-sink.marko | 6 +- tests/fixtures/23-outline-extracting.marko | 24 ++++++ tests/fixtures/23-outline-extracting.mdo | 7 ++ 9 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/23-outline-extracting.marko create mode 100644 tests/fixtures/23-outline-extracting.mdo diff --git a/examples/marko-run/src/tags/markdown-layout.marko b/examples/marko-run/src/tags/markdown-layout.marko index 953c22b4f0c75909526fd8848b4cd28233955e82..1ff7b10a1d9311ae61cf310f8e752b3b1dbfe433 100644 --- a/examples/marko-run/src/tags/markdown-layout.marko +++ b/examples/marko-run/src/tags/markdown-layout.marko @@ -28,9 +28,86 @@ export interface Input { <${input.content} /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c48e01f14592245bdfab777c5da48a4691012203..0a8838413757c36cdf212fc1742b34630ec4bfdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,9 @@ pub fn transform( Vec::new() }; + // Extract statements before rendering - they need to be hoisted above Layout + let extracted_statements = extract_statements(&mut ast); + let mut text = ast.render(); // Inject heading IDs if we collected headings @@ -100,29 +103,37 @@ pub fn transform( text = plugin::toc::inject_heading_ids(&text, &headings); } - // Prepend preamble output if we extracted it + // Build hoisted statements: preamble (frontmatter exports) + extracted statements + let mut hoisted = String::new(); if let Some(preamble) = preamble_output { - text = preamble + &text; + hoisted.push_str(&preamble); } + hoisted.push_str(&extracted_statements); // Wrap with Layout component if layout_import is provided + // Statements must come before the Layout tag if let (Some(layout_path), Some(outline)) = (layout_import, outline_json) { if let Some(self_path) = self_path { text = format!( - "import Layout__markodown__ from \"{}\";import * as self__markodown__ from \"{}\";\n\n\n{}", - layout_path, - self_path, - outline, - text - ); + "import Layout__markodown__ from \"{}\";import * as self__markodown__ from \"{}\";\n{}\n\n{}", + layout_path, + self_path, + hoisted, + outline, + text + ); } else { text = format!( - "import Layout__markodown__ from \"{}\";\n\n\n{}", - layout_path, - outline, - text - ); + "import Layout__markodown__ from \"{}\";\n{}\n\n{}", + layout_path, + hoisted, + outline, + text + ); } + } else { + // No Layout wrapping, just prepend hoisted statements + text = hoisted + &text; } Ok(Output { text, format }) @@ -166,12 +177,34 @@ pub fn adjust_err(mut err: OxcDiagnostic, offset: isize) -> OxcDiagnostic { err } +/// Extract all StatementBlock content from the AST and clear them. +/// Returns the collected statement content as a single string. +fn extract_statements(node: &mut markdown_it::Node) -> String { + let mut statements = String::new(); + + fn walk(node: &mut markdown_it::Node, statements: &mut String) { + if let Some(stmt) = node.cast_mut::() { + statements.push_str(&stmt.content); + stmt.content.clear(); + } + for child in &mut node.children { + walk(child, statements); + } + } + + walk(node, &mut statements); + statements +} + /// Check if the AST contains any Marko-specific features fn has_marko_features(node: &markdown_it::Node) -> bool { - // Check if this node is a RawBlock + // Check if this node is a RawBlock or StatementBlock if node.cast::().is_some() { return true; } + if node.cast::().is_some() { + return true; + } // Check for Marko tags if node.cast::().is_some() diff --git a/src/plugin/frontmatter.rs b/src/plugin/frontmatter.rs index 72979a0766a565c4585225cceeff60c739796d1e..5e4788b5de0ee35ddf8dbd794e8440fe8b30f128 100644 --- a/src/plugin/frontmatter.rs +++ b/src/plugin/frontmatter.rs @@ -2,7 +2,7 @@ use markdown_it::parser::block::{BlockRule, BlockState}; use markdown_it::Node; use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; -use crate::plugin::{get_line_raw, ErrorBlock, RawBlock}; +use crate::plugin::{get_line_raw, ErrorBlock, StatementBlock}; /// Parse frontmatter (---) at document start pub(crate) struct Rule; @@ -243,6 +243,6 @@ impl BlockRule for Rule { lines_consumed += 1; } - Some((Node::new(RawBlock { content }), lines_consumed)) + Some((Node::new(StatementBlock { content }), lines_consumed)) } } diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index f6ffa59ad16b789aa51c7c367375bb6d2b7037d9..ba68e6a0ba648eab4c9bd4481a213fdc4483f200 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -40,7 +40,7 @@ impl NodeValue for ErrorBlock { } } -/// A raw node that passes Marko syntax verbaitim +/// A raw node that passes Marko syntax verbatim #[derive(Debug)] pub(crate) struct RawBlock { pub content: String, @@ -52,6 +52,19 @@ impl NodeValue for RawBlock { } } +/// A statement block (import, export, static, server, client) that needs hoisting. +/// These are kept separate from RawBlock so they can be extracted before Layout wrapping. +#[derive(Debug)] +pub struct StatementBlock { + pub content: String, +} + +impl NodeValue for StatementBlock { + fn render(&self, _node: &Node, fmt: &mut dyn Renderer) { + fmt.text_raw(&self.content); + } +} + /// Helper to get a line with original whitespace preserved pub(crate) fn get_line_raw<'a, 'b>(state: &'a BlockState<'a, 'b>, line: usize) -> &'b str { if line < state.line_max { diff --git a/src/plugin/statement.rs b/src/plugin/statement.rs index 55c59821e1205be090becda07237ad741c33d4db..eb0dbf282687bd4a1840c885625ece60f519f602 100644 --- a/src/plugin/statement.rs +++ b/src/plugin/statement.rs @@ -2,7 +2,7 @@ use markdown_it::parser::block::{BlockRule, BlockState}; use markdown_it::Node; use crate::adjust_err; -use crate::plugin::{ErrorBlock, RawBlock}; +use crate::plugin::{ErrorBlock, StatementBlock}; use crate::typescript::scan_first_statement_forbid_trailing; /// Parse JavaScript statements (import, export, static X, server X, client X) @@ -57,7 +57,7 @@ impl BlockRule for Rule { let content = &unbounded_src[0..unbounded_src.len().min(total_end + 1)]; let line_count = 1 + content[..total_end].chars().filter(|&c| c == '\n').count(); - let node = Node::new(RawBlock { + let node = Node::new(StatementBlock { content: content.into(), }); Some((node, line_count)) diff --git a/tests/fixtures.rs b/tests/fixtures.rs index 174b1644e12a43c9fc08d03a7c3d3d8b5911aebc..50fcc6a432c55795f90ce0237786948f30f60849 100644 --- a/tests/fixtures.rs +++ b/tests/fixtures.rs @@ -20,7 +20,7 @@ fn run_fixture(name: &str) { let expected = fs::read_to_string(&html_path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", html_path.display())); - let output = transform(&source, Some(OutputFormat::Html), None).unwrap(); + let output = transform(&source, Some(OutputFormat::Html), None, None).unwrap(); assert_eq!( output.format, @@ -40,7 +40,7 @@ fn run_fixture(name: &str) { let expected = fs::read_to_string(&marko_path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", marko_path.display())); - let output = transform(&source, Some(OutputFormat::Marko), None).unwrap(); + let output = transform(&source, Some(OutputFormat::Marko), None, None).unwrap(); assert_eq!( output.format, diff --git a/tests/fixtures/13-kitchen-sink.marko b/tests/fixtures/13-kitchen-sink.marko index 0be8f9a75eb5e952f1ea85171e869f579bf3c2c5..89f062a8f1e3949cc273d64bae2dbc078b38b78a 100644 --- a/tests/fixtures/13-kitchen-sink.marko +++ b/tests/fixtures/13-kitchen-sink.marko @@ -2,12 +2,12 @@ export const title = "Full Example"; import Chart from "./chart.marko"; static const year = 2026; export const slug = "full-example"; -// Page starts here -

${title}

-

This is a complete example of markodown featuring ${year}.

server { const data = await fetchData(); } +// Page starts here +

${title}

+

This is a complete example of markodown featuring ${year}.

Chart

diff --git a/tests/fixtures/23-outline-extracting.marko b/tests/fixtures/23-outline-extracting.marko new file mode 100644 index 0000000000000000000000000000000000000000..fbf38d068ae9b24a960651da95fcd01e644962e4 --- /dev/null +++ b/tests/fixtures/23-outline-extracting.marko @@ -0,0 +1,24 @@ + +good morning + + +good night + + +snow time + + + +

content 1

+ +

content 2

+ +

content 3

+ +

content 4

+ diff --git a/tests/fixtures/23-outline-extracting.mdo b/tests/fixtures/23-outline-extracting.mdo new file mode 100644 index 0000000000000000000000000000000000000000..d97eaaab8bcc6954ae574001acb34379981d53e8 --- /dev/null +++ b/tests/fixtures/23-outline-extracting.mdo @@ -0,0 +1,7 @@ +

good morning

+ +## good night + +### **snow** time + +rain **time** -- 2.54.0