extern crate markdown;
use crate::swc_utils::{create_span, DropContext, RewritePrefixContext, RewriteStopsContext};
use markdown::{mdast::Stop, Location, MdxExpressionKind, MdxSignal};
use std::rc::Rc;
use swc_core::common::{
comments::{Comment, Comments, SingleThreadedComments, SingleThreadedCommentsMap},
source_map::SmallPos,
sync::Lrc,
BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Span, Spanned,
};
use swc_core::ecma::ast::{EsVersion, Expr, Module, PropOrSpread};
use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter};
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;
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,
}
}
pub fn parse_esm_to_tree(
value: &str,
stops: &[Stop],
location: Option<&Location>,
) -> Result<Module, markdown::message::Message> {
let result = parse_esm_core(value);
let mut rewrite_context = RewriteStopsContext { stops, location };
match result {
Err((span, reason)) => Err(swc_error_to_error(span, &reason, &rewrite_context)),
Ok(mut module) => {
module.visit_mut_with(&mut rewrite_context);
Ok(module)
}
}
}
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)> {
if matches!(kind, MdxExpressionKind::Expression) && whitespace_and_comments(0, value).is_ok() {
return Ok(None);
}
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])
),
))
}
}
}
}
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,
}
}
pub fn parse_expression_to_tree(
value: &str,
kind: &MdxExpressionKind,
stops: &[Stop],
location: Option<&Location>,
) -> Result<Option<Box<Expr>>, markdown::message::Message> {
let result = parse_expression_core(value, kind);
let mut rewrite_context = RewriteStopsContext { stops, location };
match result {
Err((span, reason)) => Err(swc_error_to_error(span, &reason, &rewrite_context)),
Ok(expr_opt) => {
if let Some(mut expr) = expr_opt {
expr.visit_mut_with(&mut rewrite_context);
Ok(Some(expr))
} else {
Ok(None)
}
}
}
}
pub fn serialize(module: &mut Module, comments: Option<&Vec<Comment>>) -> String {
let single_threaded_comments = SingleThreadedComments::default();
if let Some(comments) = comments {
for c in comments {
single_threaded_comments.add_leading(c.span.lo, c.clone());
}
}
module.visit_mut_with(&mut DropContext {});
let mut buf = vec![];
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
{
let mut emitter = Emitter {
cfg: swc_core::ecma::codegen::Config::default(),
cm: cm.clone(),
comments: Some(&single_threaded_comments),
wr: JsWriter::new(cm, "\n", &mut buf, None),
};
emitter.emit_module(module).unwrap();
}
String::from_utf8_lossy(&buf).into()
}
#[allow(dead_code)]
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
}
fn swc_error_to_signal(span: Span, reason: &str, value_len: usize) -> MdxSignal {
let error_end = span.hi.to_usize();
let source = Box::new("mdxjs-rs".into());
let rule_id = Box::new("swc".into());
if error_end >= value_len {
MdxSignal::Eof(reason.into(), source, rule_id)
} else {
MdxSignal::Error(reason.into(), span.lo.to_usize(), source, rule_id)
}
}
fn swc_error_to_error(
span: Span,
reason: &str,
context: &RewriteStopsContext,
) -> markdown::message::Message {
let point = context
.location
.and_then(|location| location.relative_to_point(context.stops, span.lo.to_usize()));
markdown::message::Message {
reason: reason.into(),
place: point.map(|point| Box::new(markdown::message::Place::Point(point))),
source: Box::new("mdxjs-rs".into()),
rule_id: Box::new("swc".into()),
}
}
fn swc_error_to_string(error: &SwcError) -> String {
error.kind().msg().into()
}
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 {
if in_multiline {
if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' {
index += 1;
in_multiline = false;
}
}
else if in_line {
if bytes[index] == b'\r' || bytes[index] == b'\n' {
in_line = false;
}
}
else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' {
index += 1;
in_multiline = true;
}
else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' {
index += 1;
in_line = true;
}
else if bytes[index].is_ascii_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 {
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(())
}
fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) {
(
SourceFile::new(
FileName::Anon.into(),
false,
FileName::Anon.into(),
source.into(),
BytePos::from_usize(1),
),
Syntax::Es(EsSyntax {
jsx: true,
..EsSyntax::default()
}),
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
}