Something went wrong. Try again.
Markdown parser fork with extended syntax for personal use.
Something went wrong. Try again.
9.8 kB · 290 lines
Rust
at hack
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291//! Bridge between `markdown-rs` and SWC.
use crate::test_utils::swc_utils::{create_span, RewritePrefixContext};use markdown::{MdxExpressionKind, MdxSignal};use std::rc::Rc;use swc_core::common::{ comments::{Comment, SingleThreadedComments, SingleThreadedCommentsMap}, source_map::SmallPos, BytePos, FileName, SourceFile, Span, Spanned,};use swc_core::ecma::ast::{EsVersion, Expr, Module, PropOrSpread};use swc_core::ecma::parser::{ error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsSyntax, Syntax,};use swc_core::ecma::visit::VisitMutWith;
/// Lex ESM in MDX with SWC.pub fn parse_esm(value: &str) -> MdxSignal { let result = parse_esm_core(value);
match result { Err((span, message)) => swc_error_to_signal(span, &message, value.len()), Ok(_) => MdxSignal::Ok, }}
/// Core to parse ESM.fn parse_esm_core(value: &str) -> Result<Module, (Span, String)> { let (file, syntax, version) = create_config(value.into()); let mut errors = vec![]; let result = parse_file_as_module(&file, syntax, version, None, &mut errors);
match result { Err(error) => Err(( fix_span(error.span(), 1), format!( "Could not parse esm with swc: {}", swc_error_to_string(&error) ), )), Ok(module) => { if errors.is_empty() { let mut index = 0; while index < module.body.len() { let node = &module.body[index];
if !node.is_module_decl() { return Err(( fix_span(node.span(), 1), "Unexpected statement in code: only import/exports are supported" .into(), )); }
index += 1; }
Ok(module) } else { Err(( fix_span(errors[0].span(), 1), format!( "Could not parse esm with swc: {}", swc_error_to_string(&errors[0]) ), )) } } }}
fn parse_expression_core( value: &str, kind: &MdxExpressionKind,) -> Result<Option<Box<Expr>>, (Span, String)> { // Empty expressions are OK. if matches!(kind, MdxExpressionKind::Expression) && whitespace_and_comments(0, value).is_ok() { return Ok(None); }
// For attribute expression, a spread is needed, for which we have to prefix // and suffix the input. // See `check_expression_ast` for how the AST is verified. let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) { ("({", "})") } else { ("", "") };
let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); let mut errors = vec![]; let result = parse_file_as_expr(&file, syntax, version, None, &mut errors);
match result { Err(error) => Err(( fix_span(error.span(), prefix.len() + 1), format!( "Could not parse expression with swc: {}", swc_error_to_string(&error) ), )), Ok(mut expr) => { if errors.is_empty() { let expression_end = expr.span().hi.to_usize() - 1; if let Err((span, reason)) = whitespace_and_comments(expression_end, value) { return Err((span, reason)); }
expr.visit_mut_with(&mut RewritePrefixContext { prefix_len: prefix.len() as u32, });
if matches!(kind, MdxExpressionKind::AttributeExpression) { let expr_span = expr.span();
if let Expr::Paren(d) = *expr { if let Expr::Object(mut obj) = *d.expr { if obj.props.len() > 1 { return Err((obj.span, "Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`)".into())); }
if let Some(PropOrSpread::Spread(d)) = obj.props.pop() { return Ok(Some(d.expr)); } } };
return Err(( expr_span, "Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)".into(), )); }
Ok(Some(expr)) } else { Err(( fix_span(errors[0].span(), prefix.len() + 1), format!( "Could not parse expression with swc: {}", swc_error_to_string(&errors[0]) ), )) } } }}
/// Lex expressions in MDX with SWC.pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { let result = parse_expression_core(value, kind);
match result { Err((span, message)) => swc_error_to_signal(span, &message, value.len()), Ok(_) => MdxSignal::Ok, }}
// To do: remove this attribute, use it somewhere.#[allow(dead_code)]/// Turn SWC comments into a flat vec.pub fn flat_comments(single_threaded_comments: SingleThreadedComments) -> Vec<Comment> { let raw_comments = single_threaded_comments.take_all(); let take = |list: SingleThreadedCommentsMap| { Rc::try_unwrap(list) .unwrap() .into_inner() .into_values() .flatten() .collect::<Vec<_>>() }; let mut list = take(raw_comments.0); list.append(&mut take(raw_comments.1)); list}
/// Turn an SWC error into an `MdxSignal`.////// * If the error happens at `value_len`, yields `MdxSignal::Eof`/// * Else, yields `MdxSignal::Error`.fn swc_error_to_signal(span: Span, reason: &str, value_len: usize) -> MdxSignal { let source = Box::new("mdx".into()); let rule_id = Box::new("swc".into()); let error_end = span.hi.to_usize();
if error_end >= value_len { MdxSignal::Eof(reason.into(), source, rule_id) } else { MdxSignal::Error(reason.into(), span.lo.to_usize(), source, rule_id) }}
/// Turn an SWC error into a string.fn swc_error_to_string(error: &SwcError) -> String { error.kind().msg().into()}
/// Move past JavaScript whitespace (well, actually ASCII whitespace) and/// comments.////// This is needed because for expressions, we use an API that parses up to/// a valid expression, but there may be more expressions after it, which we/// don’t alow.fn whitespace_and_comments(mut index: usize, value: &str) -> Result<(), (Span, String)> { let bytes = value.as_bytes(); let len = bytes.len(); let mut in_multiline = false; let mut in_line = false;
while index < len { // In a multiline comment: `/* a */`. if in_multiline { if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' { index += 1; in_multiline = false; } } // In a line comment: `// a`. else if in_line { if bytes[index] == b'\r' || bytes[index] == b'\n' { in_line = false; } } // Not in a comment, opening a multiline comment: `/* a */`. else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' { index += 1; in_multiline = true; } // Not in a comment, opening a line comment: `// a`. else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' { index += 1; in_line = true; } // Outside comment, whitespace. else if bytes[index].is_ascii_whitespace() { // Fine! } // Outside comment, not whitespace. else { return Err(( create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected content after expression".into(), )); }
index += 1; }
if in_multiline { return Err(( create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".into())); }
if in_line { // EOF instead of EOL is specifically not allowed, because that would // mean the closing brace is on the commented-out line return Err((create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".into())); }
Ok(())}
/// Create configuration for SWC, shared between ESM and expressions.////// This enables modern JavaScript (ES2022) + JSX.fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) { ( // File. SourceFile::new( FileName::Anon.into(), false, FileName::Anon.into(), source.into(), BytePos::from_usize(1), ), // Syntax. Syntax::Es(EsSyntax { jsx: true, ..EsSyntax::default() }), // Version. // To do: update once in a while (last checked: 2024-04-18). EsVersion::Es2022, )}
fn fix_span(mut span: Span, offset: usize) -> Span { span.lo = BytePos::from_usize(span.lo.to_usize() - offset); span.hi = BytePos::from_usize(span.hi.to_usize() - offset); span}