1use markdown_it::parser::block::{BlockRule, BlockState};
2use markdown_it::Node;
3
4use crate::adjust_err;
5use crate::plugin::{ErrorBlock, StatementBlock};
6use crate::typescript::scan_first_statement_forbid_trailing;
7
8/// Parse JavaScript statements (import, export, static X, server X, client X)
9pub struct Rule;
10
11const KEYWORDS: &[&str] = &["import ", "export ", "static ", "server ", "client "];
12const MARKO_PREFIXES: &[&str] = &["static ", "server ", "client "];
13
14impl BlockRule for Rule {
15 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
16 if state.line >= state.line_max || state.blk_indent > 0 {
17 return None;
18 }
19
20 let first_line = state.get_line(state.line).trim_start();
21 let keyword = *KEYWORDS.iter().find(|k| first_line.starts_with(*k))?;
22
23 // Statements must start a new block - they can't be in the middle of a paragraph.
24 // Allow if: start of document, previous line is empty, or previous line is a valid statement.
25 if state.line > 0 {
26 let prev_line = state.get_line(state.line - 1).trim();
27 if !prev_line.is_empty()
28 && prev_line != "---"
29 && !KEYWORDS.iter().any(|k| prev_line.starts_with(*k))
30 {
31 return None;
32 }
33 }
34
35 let keyword_trim = MARKO_PREFIXES
36 .iter()
37 .find(|k| **k == keyword)
38 .map(|k| k.len())
39 .unwrap_or(0) as u32;
40
41 let unbounded_src = &state.src[state.line_offsets[state.line].first_nonspace..];
42
43 // Statements never span blank lines. Limit the source passed to the TypeScript
44 // parser to avoid it trying to parse subsequent Markdown/Marko content as TS.
45 let limited_end = unbounded_src
46 .find("\n\n")
47 .map(|p| p + 1) // include the newline that ends the statement
48 .unwrap_or(unbounded_src.len());
49 let limited_src = &unbounded_src[..limited_end];
50
51 let statement_end =
52 match scan_first_statement_forbid_trailing(&limited_src[keyword_trim as usize..]) {
53 Ok(ok) => ok,
54 Err(err) => {
55 return Some((
56 Node::new(ErrorBlock {
57 errors: vec![adjust_err(err, keyword_trim as isize)],
58 }),
59 1,
60 ));
61 }
62 };
63
64 let total_end = keyword_trim + statement_end;
65 let content = &unbounded_src[0..unbounded_src.len().min(total_end as usize + 1)];
66 let line_count = 1 + content[..total_end as usize]
67 .chars()
68 .filter(|&c| c == '\n')
69 .count();
70
71 let node = Node::new(StatementBlock {
72 content: content.into(),
73 });
74 Some((node, line_count))
75 }
76}