| 1 | use markdown_it::parser::block::{BlockRule, BlockState}; |
| 2 | use markdown_it::Node; |
| 3 | |
| 4 | use crate::adjust_err; |
| 5 | use crate::plugin::{ErrorBlock, StatementBlock}; |
| 6 | use crate::typescript::scan_first_statement_forbid_trailing; |
| 7 | |
| 8 | /// Parse JavaScript statements (import, export, static X, server X, client X) |
| 9 | pub struct Rule; |
| 10 | |
| 11 | const KEYWORDS: &[&str] = &["import ", "export ", "static ", "server ", "client "]; |
| 12 | const MARKO_PREFIXES: &[&str] = &["static ", "server ", "client "]; |
| 13 | |
| 14 | impl 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 | } |