diff options
author | Titus Wormer <tituswormer@gmail.com> | 2022-10-11 09:54:56 +0200 |
---|---|---|
committer | Titus Wormer <tituswormer@gmail.com> | 2022-10-11 09:55:16 +0200 |
commit | a4b56e7b971fa81c56a59b465f90c8016f01320d (patch) | |
tree | 7002a44087e57c8158a51dd30b6eb89eb260af2b | |
parent | 1fd94f512834aa7bd70f22a60229ce01edfc754e (diff) | |
download | markdown-rs-a4b56e7b971fa81c56a59b465f90c8016f01320d.tar.gz markdown-rs-a4b56e7b971fa81c56a59b465f90c8016f01320d.tar.bz2 markdown-rs-a4b56e7b971fa81c56a59b465f90c8016f01320d.zip |
Add support for proper positional info in swc tree
* Fix some positional info in SWC error messages
* Add positional info in `to_document` on duplicate layouts
* Add support for `path` on `Program` (`to_swc`, `to_document`, `jsx_rewrite`),
for the path of a file on disk
* Add support for `development` to `jsx-rewrite`, which when defined will embed
info on where tags were written into the runtime code when they are not passed
* Refactor to move some utilities to `micromark_swc_utils.rs`, `swc_utils.rs`
-rw-r--r-- | src/construct/mdx_esm.rs | 19 | ||||
-rw-r--r-- | src/construct/partial_mdx_expression.rs | 24 | ||||
-rw-r--r-- | src/lib.rs | 11 | ||||
-rw-r--r-- | src/mdast.rs | 17 | ||||
-rw-r--r-- | src/parser.rs | 16 | ||||
-rw-r--r-- | src/to_mdast.rs | 153 | ||||
-rw-r--r-- | src/util/location.rs | 111 | ||||
-rw-r--r-- | src/util/mdx_collect.rs | 81 | ||||
-rw-r--r-- | src/util/mod.rs | 1 | ||||
-rw-r--r-- | tests/mdx_esm.rs | 5 | ||||
-rw-r--r-- | tests/mdx_expression_flow.rs | 3 | ||||
-rw-r--r-- | tests/mdx_expression_text.rs | 5 | ||||
-rw-r--r-- | tests/mdx_jsx_text.rs | 10 | ||||
-rw-r--r-- | tests/test_utils/hast.rs | 10 | ||||
-rw-r--r-- | tests/test_utils/jsx_rewrite.rs | 261 | ||||
-rw-r--r-- | tests/test_utils/micromark_swc_utils.rs | 134 | ||||
-rw-r--r-- | tests/test_utils/mod.rs | 2 | ||||
-rw-r--r-- | tests/test_utils/swc.rs | 145 | ||||
-rw-r--r-- | tests/test_utils/swc_utils.rs | 96 | ||||
-rw-r--r-- | tests/test_utils/to_document.rs | 65 | ||||
-rw-r--r-- | tests/test_utils/to_hast.rs | 22 | ||||
-rw-r--r-- | tests/test_utils/to_swc.rs | 78 | ||||
-rw-r--r-- | tests/xxx_document.rs | 16 | ||||
-rw-r--r-- | tests/xxx_hast.rs | 12 | ||||
-rw-r--r-- | tests/xxx_jsx_rewrite.rs | 32 | ||||
-rw-r--r-- | tests/xxx_swc.rs | 434 |
26 files changed, 1182 insertions, 581 deletions
diff --git a/src/construct/mdx_esm.rs b/src/construct/mdx_esm.rs index 53f8beb..4fb6b50 100644 --- a/src/construct/mdx_esm.rs +++ b/src/construct/mdx_esm.rs @@ -31,10 +31,7 @@ use crate::event::Name; use crate::state::{Name as StateName, State}; use crate::tokenizer::Tokenizer; -use crate::util::{ - mdx_collect::{collect, place_to_point}, - slice::Slice, -}; +use crate::util::{mdx_collect::collect, slice::Slice}; use crate::MdxSignal; use alloc::format; @@ -197,16 +194,24 @@ fn parse_esm(tokenizer: &mut Tokenizer) -> State { // Collect the body of the ESM and positional info for each run of it. let result = collect( - tokenizer, + &tokenizer.events, + tokenizer.parse_state.bytes, tokenizer.tokenize_state.start, &[Name::MdxEsmData, Name::LineEnding], + &[], ); // Parse and handle what was signaled back. match parse(&result.value) { MdxSignal::Ok => State::Ok, - MdxSignal::Error(message, place) => { - let point = place_to_point(&result, place); + MdxSignal::Error(message, relative) => { + let point = tokenizer + .parse_state + .location + .as_ref() + .expect("expected location index if aware mdx is on") + .relative_to_point(&result.stops, relative) + .expect("expected non-empty string"); State::Error(format!("{}:{}: {}", point.line, point.column, message)) } MdxSignal::Eof(message) => { diff --git a/src/construct/partial_mdx_expression.rs b/src/construct/partial_mdx_expression.rs index 789443e..fbb13e0 100644 --- a/src/construct/partial_mdx_expression.rs +++ b/src/construct/partial_mdx_expression.rs @@ -60,10 +60,7 @@ use crate::construct::partial_space_or_tab::space_or_tab_min_max; use crate::event::Name; use crate::state::{Name as StateName, State}; use crate::tokenizer::Tokenizer; -use crate::util::{ - constant::TAB_SIZE, - mdx_collect::{collect, place_to_point}, -}; +use crate::util::{constant::TAB_SIZE, mdx_collect::collect}; use crate::{MdxExpressionKind, MdxExpressionParse, MdxSignal}; use alloc::{format, string::ToString}; @@ -205,9 +202,11 @@ pub fn eol_after(tokenizer: &mut Tokenizer) -> State { fn parse_expression(tokenizer: &mut Tokenizer, parse: &MdxExpressionParse) -> State { // Collect the body of the expression and positional info for each run of it. let result = collect( - tokenizer, + &tokenizer.events, + tokenizer.parse_state.bytes, tokenizer.tokenize_state.start, &[Name::MdxExpressionData, Name::LineEnding], + &[], ); // Turn the name of the expression into a kind. @@ -221,9 +220,18 @@ fn parse_expression(tokenizer: &mut Tokenizer, parse: &MdxExpressionParse) -> St // Parse and handle what was signaled back. match parse(&result.value, &kind) { MdxSignal::Ok => State::Ok, - MdxSignal::Error(message, place) => { - let point = place_to_point(&result, place); - State::Error(format!("{}:{}: {}", point.line, point.column, message)) + MdxSignal::Error(message, relative) => { + let point = tokenizer + .parse_state + .location + .as_ref() + .expect("expected location index if aware mdx is on") + .relative_to_point(&result.stops, relative) + .map_or((tokenizer.point.line, tokenizer.point.column), |d| { + (d.line, d.column) + }); + + State::Error(format!("{}:{}: {}", point.0, point.1, message)) } MdxSignal::Eof(message) => { tokenizer.tokenize_state.mdx_last_parse_error = Some(message); @@ -41,6 +41,9 @@ use util::{ sanitize_uri::sanitize, }; +#[doc(hidden)] +pub use util::location::Location; + /// Type of line endings in markdown. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub enum LineEnding { @@ -1252,8 +1255,8 @@ pub fn micromark(value: &str) -> String { /// # } /// ``` pub fn micromark_with_options(value: &str, options: &Options) -> Result<String, String> { - let (events, bytes) = parse(value, &options.parse)?; - Ok(to_html(&events, bytes, &options.compile)) + let (events, parse_state) = parse(value, &options.parse)?; + Ok(to_html(&events, parse_state.bytes, &options.compile)) } /// Turn markdown into a syntax tree. @@ -1279,8 +1282,8 @@ pub fn micromark_with_options(value: &str, options: &Options) -> Result<String, /// # } /// ``` pub fn micromark_to_mdast(value: &str, options: &ParseOptions) -> Result<Node, String> { - let (events, bytes) = parse(value, options)?; - let node = to_mdast(&events, bytes)?; + let (events, parse_state) = parse(value, options)?; + let node = to_mdast(&events, parse_state.bytes)?; Ok(node) } diff --git a/src/mdast.rs b/src/mdast.rs index 8b5b74d..de53532 100644 --- a/src/mdast.rs +++ b/src/mdast.rs @@ -9,6 +9,10 @@ use alloc::{ vec::Vec, }; +/// Relative byte index into a string, to an absolute byte index into the +/// whole document. +pub type Stop = (usize, usize); + /// Explicitness of a reference. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ReferenceKind { @@ -429,7 +433,7 @@ pub enum AttributeContent { /// > | <a {...b} /> /// ^^^^^^ /// ``` - Expression(String), + Expression(String, Vec<Stop>), /// JSX property. /// /// ```markdown @@ -448,7 +452,7 @@ pub enum AttributeValue { /// > | <a b={c} /> /// ^^^ /// ``` - Expression(String), + Expression(String, Vec<Stop>), /// Static value. /// /// ```markdown @@ -1040,6 +1044,9 @@ pub struct MdxjsEsm { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } /// MDX: expression (flow). @@ -1055,6 +1062,9 @@ pub struct MdxFlowExpression { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } /// MDX: expression (text). @@ -1070,6 +1080,9 @@ pub struct MdxTextExpression { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } /// MDX: JSX element (container). diff --git a/src/parser.rs b/src/parser.rs index b694bc5..a7962d0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,6 +4,7 @@ use crate::event::{Event, Point}; use crate::state::{Name as StateName, State}; use crate::subtokenize::subtokenize; use crate::tokenizer::Tokenizer; +use crate::util::location::Location; use crate::ParseOptions; use alloc::{string::String, vec, vec::Vec}; @@ -14,6 +15,8 @@ use alloc::{string::String, vec, vec::Vec}; #[derive(Debug)] pub struct ParseState<'a> { /// Configuration. + pub location: Option<Location>, + /// Configuration. pub options: &'a ParseOptions, /// List of chars. pub bytes: &'a [u8], @@ -29,10 +32,17 @@ pub struct ParseState<'a> { pub fn parse<'a>( value: &'a str, options: &'a ParseOptions, -) -> Result<(Vec<Event>, &'a [u8]), String> { +) -> Result<(Vec<Event>, ParseState<'a>), String> { + let bytes = value.as_bytes(); + let mut parse_state = ParseState { options, - bytes: value.as_bytes(), + bytes, + location: if options.mdx_esm_parse.is_some() || options.mdx_expression_parse.is_some() { + Some(Location::new(bytes)) + } else { + None + }, definitions: vec![], gfm_footnote_definitions: vec![], }; @@ -72,5 +82,5 @@ pub fn parse<'a>( } } - Ok((events, parse_state.bytes)) + Ok((events, parse_state)) } diff --git a/src/to_mdast.rs b/src/to_mdast.rs index 4db76e6..f2b3c30 100644 --- a/src/to_mdast.rs +++ b/src/to_mdast.rs @@ -1,6 +1,6 @@ //! Turn events into a syntax tree. -use crate::event::{Event, Kind, Name}; +use crate::event::{Event, Kind, Name, Point as EventPoint}; use crate::mdast::{ AttributeContent, AttributeValue, BlockQuote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, @@ -14,6 +14,7 @@ use crate::util::{ decode as decode_character_reference, parse as parse_character_reference, }, infer::{gfm_table_align, list_item_loose, list_loose}, + mdx_collect::collect, normalize_identifier::normalize_identifier, slice::{Position as SlicePosition, Slice}, }; @@ -255,8 +256,6 @@ fn enter(context: &mut CompileContext) -> Result<(), String> { | Name::HtmlTextData | Name::MathFlowChunk | Name::MathTextData - | Name::MdxExpressionData - | Name::MdxEsmData | Name::MdxJsxTagAttributeValueLiteralValue => on_enter_data(context), Name::CodeFencedFenceInfo | Name::CodeFencedFenceMeta @@ -267,7 +266,6 @@ fn enter(context: &mut CompileContext) -> Result<(), String> { | Name::LabelText | Name::MathFlowFenceMeta | Name::MdxJsxTagAttributeValueLiteral - | Name::MdxJsxTagAttributeValueExpression | Name::ReferenceString | Name::ResourceDestinationString | Name::ResourceTitleString => on_enter_buffer(context), @@ -306,6 +304,9 @@ fn enter(context: &mut CompileContext) -> Result<(), String> { Name::MdxJsxTagClosingMarker => on_enter_mdx_jsx_tag_closing_marker(context)?, Name::MdxJsxTagAttribute => on_enter_mdx_jsx_tag_attribute(context)?, Name::MdxJsxTagAttributeExpression => on_enter_mdx_jsx_tag_attribute_expression(context)?, + Name::MdxJsxTagAttributeValueExpression => { + on_enter_mdx_jsx_tag_attribute_value_expression(context); + } Name::MdxJsxTagSelfClosingMarker => on_enter_mdx_jsx_tag_self_closing_marker(context)?, Name::Paragraph => on_enter_paragraph(context), Name::Reference => on_enter_reference(context), @@ -347,11 +348,12 @@ fn exit(context: &mut CompileContext) -> Result<(), String> { | Name::HtmlTextData | Name::MathFlowChunk | Name::MathTextData - | Name::MdxExpressionData - | Name::MdxEsmData | Name::MdxJsxTagAttributeValueLiteralValue => { on_exit_data(context)?; } + Name::MdxJsxTagAttributeExpression | Name::MdxJsxTagAttributeValueExpression => { + on_exit_drop(context); + } Name::AutolinkProtocol => on_exit_autolink_protocol(context)?, Name::AutolinkEmail => on_exit_autolink_email(context)?, Name::CharacterReferenceMarker => on_exit_character_reference_marker(context), @@ -391,28 +393,23 @@ fn exit(context: &mut CompileContext) -> Result<(), String> { Name::HeadingSetext => on_exit_heading_setext(context)?, Name::HeadingSetextUnderlineSequence => on_exit_heading_setext_underline_sequence(context), Name::HeadingSetextText => on_exit_heading_setext_text(context), - Name::HtmlFlow - | Name::HtmlText - | Name::MdxEsm - | Name::MdxFlowExpression - | Name::MdxTextExpression => on_exit_literal(context)?, + Name::HtmlFlow | Name::HtmlText => on_exit_html(context)?, Name::LabelText => on_exit_label_text(context), Name::LineEnding => on_exit_line_ending(context)?, Name::ListItemValue => on_exit_list_item_value(context), + Name::MdxEsm | Name::MdxFlowExpression | Name::MdxTextExpression => { + on_exit_mdx_esm_or_expression(context)?; + } Name::MdxJsxFlowTag | Name::MdxJsxTextTag => on_exit_mdx_jsx_tag(context)?, Name::MdxJsxTagClosingMarker => on_exit_mdx_jsx_tag_closing_marker(context), Name::MdxJsxTagNamePrimary => on_exit_mdx_jsx_tag_name_primary(context), Name::MdxJsxTagNameMember => on_exit_mdx_jsx_tag_name_member(context), Name::MdxJsxTagNameLocal => on_exit_mdx_jsx_tag_name_local(context), - Name::MdxJsxTagAttributeExpression => on_exit_mdx_jsx_tag_attribute_expression(context), Name::MdxJsxTagAttributePrimaryName => on_exit_mdx_jsx_tag_attribute_primary_name(context), Name::MdxJsxTagAttributeNameLocal => on_exit_mdx_jsx_tag_attribute_name_local(context), Name::MdxJsxTagAttributeValueLiteral => { on_exit_mdx_jsx_tag_attribute_value_literal(context); } - Name::MdxJsxTagAttributeValueExpression => { - on_exit_mdx_jsx_tag_attribute_value_expression(context); - } Name::MdxJsxTagSelfClosingMarker => on_exit_mdx_jsx_tag_self_closing_marker(context), Name::ReferenceString => on_exit_reference_string(context), @@ -499,27 +496,51 @@ fn on_enter_math_text(context: &mut CompileContext) { /// Handle [`Enter`][Kind::Enter]:[`MdxEsm`][Name::MdxEsm]. fn on_enter_mdx_esm(context: &mut CompileContext) { + let result = collect( + context.events, + context.bytes, + context.index, + &[Name::MdxEsmData, Name::LineEnding], + &[Name::MdxEsm], + ); context.tail_push(Node::MdxjsEsm(MdxjsEsm { - value: String::new(), + value: result.value, position: None, + stops: result.stops, })); context.buffer(); } /// Handle [`Enter`][Kind::Enter]:[`MdxFlowExpression`][Name::MdxFlowExpression]. fn on_enter_mdx_flow_expression(context: &mut CompileContext) { + let result = collect( + context.events, + context.bytes, + context.index, + &[Name::MdxExpressionData, Name::LineEnding], + &[Name::MdxFlowExpression], + ); context.tail_push(Node::MdxFlowExpression(MdxFlowExpression { - value: String::new(), + value: result.value, position: None, + stops: result.stops, })); context.buffer(); } /// Handle [`Enter`][Kind::Enter]:[`MdxTextExpression`][Name::MdxTextExpression]. fn on_enter_mdx_text_expression(context: &mut CompileContext) { + let result = collect( + context.events, + context.bytes, + context.index, + &[Name::MdxExpressionData, Name::LineEnding], + &[Name::MdxTextExpression], + ); context.tail_push(Node::MdxTextExpression(MdxTextExpression { - value: String::new(), + value: result.value, position: None, + stops: result.stops, })); context.buffer(); } @@ -801,18 +822,50 @@ fn on_enter_mdx_jsx_tag_attribute(context: &mut CompileContext) -> Result<(), St fn on_enter_mdx_jsx_tag_attribute_expression(context: &mut CompileContext) -> Result<(), String> { on_enter_mdx_jsx_tag_any_attribute(context)?; + let result = collect( + context.events, + context.bytes, + context.index, + &[Name::MdxExpressionData, Name::LineEnding], + &[Name::MdxJsxTagAttributeExpression], + ); context .jsx_tag .as_mut() .expect("expected tag") .attributes - .push(AttributeContent::Expression(String::new())); + .push(AttributeContent::Expression(result.value, result.stops)); context.buffer(); Ok(()) } +/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagAttributeValueExpression`][Name::MdxJsxTagAttributeValueExpression]. +fn on_enter_mdx_jsx_tag_attribute_value_expression(context: &mut CompileContext) { + let result = collect( + context.events, + context.bytes, + context.index, + &[Name::MdxExpressionData, Name::LineEnding], + &[Name::MdxJsxTagAttributeValueExpression], + ); + + if let Some(AttributeContent::Property(node)) = context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .last_mut() + { + node.value = Some(AttributeValue::Expression(result.value, result.stops)); + } else { + unreachable!("expected property") + } + + context.buffer(); +} + /// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagSelfClosingMarker`][Name::MdxJsxTagSelfClosingMarker]. fn on_enter_mdx_jsx_tag_self_closing_marker(context: &mut CompileContext) -> Result<(), String> { let tag = context.jsx_tag.as_ref().expect("expected tag"); @@ -1086,6 +1139,11 @@ fn on_exit_definition_title_string(context: &mut CompileContext) { } } +/// Handle [`Exit`][Kind::Exit]:*, by dropping the current buffer. +fn on_exit_drop(context: &mut CompileContext) { + context.resume(); +} + /// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter]. fn on_exit_frontmatter(context: &mut CompileContext) -> Result<(), String> { let value = trim_eol(context.resume().to_string(), true, true); @@ -1280,20 +1338,16 @@ fn on_exit_line_ending(context: &mut CompileContext) -> Result<(), String> { Ok(()) } -/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`MdxFlowExpression`][Name::MdxFlowExpression],etc}. -fn on_exit_literal(context: &mut CompileContext) -> Result<(), String> { +/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`HtmlText`][Name::HtmlText]}. +fn on_exit_html(context: &mut CompileContext) -> Result<(), String> { let value = context.resume().to_string(); match context.tail_mut() { Node::Html(node) => node.value = value, - Node::MdxFlowExpression(node) => node.value = value, - Node::MdxTextExpression(node) => node.value = value, - Node::MdxjsEsm(node) => node.value = value, - _ => unreachable!("expected html, mdx expression, etc on stack for value"), + _ => unreachable!("expected html on stack for value"), } on_exit(context)?; - Ok(()) } @@ -1483,26 +1537,13 @@ fn on_exit_mdx_jsx_tag_name_local(context: &mut CompileContext) { name.push_str(slice.as_str()); } -/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression]. -fn on_exit_mdx_jsx_tag_attribute_expression(context: &mut CompileContext) { - let value = context.resume(); - - if let Some(AttributeContent::Expression(expression)) = context - .jsx_tag - .as_mut() - .expect("expected tag") - .attributes - .last_mut() - { - expression.push_str(value.to_string().as_str()); - } else { - unreachable!("expected expression") - } +/// Handle [`Exit`][Kind::Exit]:{[`MdxEsm`][Name::MdxEsm],[`MdxFlowExpression`][Name::MdxFlowExpression],[`MdxTextExpression`][Name::MdxTextExpression]}. +fn on_exit_mdx_esm_or_expression(context: &mut CompileContext) -> Result<(), String> { + on_exit_drop(context); + context.tail_pop()?; + Ok(()) } -// Name:: => (context), -// Name:: => (context), - /// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributePrimaryName`][Name::MdxJsxTagAttributePrimaryName]. fn on_exit_mdx_jsx_tag_attribute_primary_name(context: &mut CompileContext) { let slice = Slice::from_position( @@ -1563,23 +1604,6 @@ fn on_exit_mdx_jsx_tag_attribute_value_literal(context: &mut CompileContext) { } } -/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeValueExpression`][Name::MdxJsxTagAttributeValueExpression]. -fn on_exit_mdx_jsx_tag_attribute_value_expression(context: &mut CompileContext) { - let value = context.resume(); - - if let Some(AttributeContent::Property(node)) = context - .jsx_tag - .as_mut() - .expect("expected tag") - .attributes - .last_mut() - { - node.value = Some(AttributeValue::Expression(value.to_string())); - } else { - unreachable!("expected property") - } -} - /// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagSelfClosingMarker`][Name::MdxJsxTagSelfClosingMarker]. fn on_exit_mdx_jsx_tag_self_closing_marker(context: &mut CompileContext) { context.jsx_tag.as_mut().expect("expected tag").self_closing = true; @@ -1625,8 +1649,13 @@ fn on_exit_resource_title_string(context: &mut CompileContext) { } // Create a point from an event. +fn point_from_event_point(point: &EventPoint) -> Point { + Point::new(point.line, point.column, point.index) +} + +// Create a point from an event. fn point_from_event(event: &Event) -> Point { - Point::new(event.point.line, event.point.column, event.point.index) + point_from_event_point(&event.point) } // Create a position from an event. diff --git a/src/util/location.rs b/src/util/location.rs new file mode 100644 index 0000000..0c9c426 --- /dev/null +++ b/src/util/location.rs @@ -0,0 +1,111 @@ +//! Deal with positions in a file. +//! +//! * Convert between byte indices and unist points. +//! * Convert between byte indices into a string which is built up of several +//! slices in a whole document, and byte indices into that whole document. + +use crate::unist::Point; +use alloc::{vec, vec::Vec}; + +/// Each stop represents a new slice, which contains the byte index into the +/// corresponding string where the slice starts (`0`), and the byte index into +/// the whole document where that slice starts (`1`). +pub type Stop = (usize, usize); + +#[derive(Debug)] +pub struct Location { + /// List, where each index is a line number (0-based), and each value is + /// the byte index *after* where the line ends. + indices: Vec<usize>, +} + +impl Location { + /// Get an index for the given `bytes`. + /// + /// Port of <https://github.com/vfile/vfile-location/blob/main/index.js> + #[must_use] + pub fn new(bytes: &[u8]) -> Self { + let mut index = 0; + let mut location_index = Self { indices: vec![] }; + + while index < bytes.len() { + if bytes[index] == b'\r' { + if index + 1 < bytes.len() && bytes[index + 1] == b'\n' { + location_index.indices.push(index + 2); + } else { + location_index.indices.push(index + 1); + } + } else if bytes[index] == b'\n' { + location_index.indices.push(index + 1); + } + + index += 1; + } + + location_index.indices.push(index + 1); + location_index + } + + /// Get the line and column-based `point` for `offset` in the bound indices. + /// + /// Returns `None` when given out of bounds input. + /// + /// Port of <https://github.com/vfile/vfile-location/blob/main/index.js> + #[must_use] + pub fn to_point(&self, offset: usize) -> Option<Point> { + let mut index = 0; + + if let Some(end) = self.indices.last() { + if offset < *end { + while index < self.indices.len() { + if self.indices[index] > offset { + break; + } + + index += 1; + } + + let previous = if index > 0 { + self.indices[index - 1] + } else { + 0 + }; + return Some(Point { + line: index + 1, + column: offset + 1 - previous, + offset, + }); + } + } + + None + } + + /// Like `to_point`, but takes a relative offset from a certain string + /// instead of an absolute offset into the whole document. + /// + /// The relative offset is made absolute based on `stops`, which represent + /// where that certain string is in the whole document. + #[must_use] + pub fn relative_to_point(&self, stops: &[Stop], relative: usize) -> Option<Point> { + Location::relative_to_absolute(stops, relative).and_then(|absolute| self.to_point(absolute)) + } + + /// Turn a relative offset into an absolute offset. + #[must_use] + pub fn relative_to_absolute(stops: &[Stop], relative: usize) -> Option<usize> { + let mut index = 0; + + while index < stops.len() && stops[index].0 <= relative { + index += 1; + } + + // There are no points: that only occurs if there was an empty string. + if index == 0 { + None + } else { + let (stop_relative, stop_absolute) = &stops[index - 1]; + Some(stop_absolute + (relative - stop_relative)) + } + } +} diff --git a/src/util/mdx_collect.rs b/src/util/mdx_collect.rs index 73ead51..02921a4 100644 --- a/src/util/mdx_collect.rs +++ b/src/util/mdx_collect.rs @@ -1,70 +1,53 @@ //! Collect info for MDX. -use crate::event::{Kind, Name, Point}; -use crate::tokenizer::Tokenizer; +use crate::event::{Event, Kind, Name}; use crate::util::slice::{Position, Slice}; use alloc::{string::String, vec, vec::Vec}; -pub type Location<'a> = (usize, &'a Point); +pub type Stop = (usize, usize); -pub struct Result<'a> { - pub start: &'a Point, +#[derive(Debug)] +pub struct Result { pub value: String, - pub locations: Vec<Location<'a>>, + pub stops: Vec<Stop>, } -pub fn collect<'a>(tokenizer: &'a Tokenizer, from: usize, names: &[Name]) -> Result<'a> { +pub fn collect( + events: &[Event], + bytes: &[u8], + from: usize, + names: &[Name], + stop: &[Name], +) -> Result { let mut result = Result { - start: &tokenizer.events[from].point, value: String::new(), - locations: vec![], + stops: vec![], }; let mut index = from; - let mut acc = 0; - while index < tokenizer.events.len() { - if tokenizer.events[index].kind == Kind::Enter - && names.contains(&tokenizer.events[index].name) - { - // Include virtual spaces. - let value = Slice::from_position( - tokenizer.parse_state.bytes, - &Position { - start: &tokenizer.events[index].point, - end: &tokenizer.events[index + 1].point, - }, - ) - .serialize(); - acc += value.len(); - result.locations.push((acc, &tokenizer.events[index].point)); - result.value.push_str(&value); - } - - index += 1; - } - - result -} - -// Turn an index of `result.value` into a point in the whole document. -pub fn place_to_point(result: &Result, place: usize) -> Point { - let mut index = 0; - let mut point = result.start; - let mut rest = place; - - while index < result.locations.len() { - point = result.locations[index].1; - - if result.locations[index].0 > place { + while index < events.len() { + if events[index].kind == Kind::Enter { + if names.contains(&events[index].name) { + // Include virtual spaces, and assume void. + let value = Slice::from_position( + bytes, + &Position { + start: &events[index].point, + end: &events[index + 1].point, + }, + ) + .serialize(); + result + .stops + .push((result.value.len(), events[index].point.index)); + result.value.push_str(&value); + } + } else if stop.contains(&events[index].name) { break; } - rest = place - result.locations[index].0; index += 1; } - let mut point = point.clone(); - point.column += rest; - point.index += rest; - point + result } diff --git a/src/util/mod.rs b/src/util/mod.rs index ac93be0..f44e183 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -8,6 +8,7 @@ pub mod encode; pub mod gfm_tagfilter; pub mod identifier; pub mod infer; +pub mod location; pub mod mdx_collect; pub mod normalize_identifier; pub mod sanitize_uri; diff --git a/tests/mdx_esm.rs b/tests/mdx_esm.rs index d6021a1..31f493b 100644 --- a/tests/mdx_esm.rs +++ b/tests/mdx_esm.rs @@ -101,7 +101,7 @@ fn mdx_esm() -> Result<(), String> { assert_eq!( micromark_with_options("import 1/1", &swc).err().unwrap(), - "1:9: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)'", + "1:8: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)'", "should crash on invalid import/exports (2)" ); @@ -250,7 +250,8 @@ fn mdx_esm() -> Result<(), String> { Node::Root(Root { children: vec![Node::MdxjsEsm(MdxjsEsm { value: "import a from 'b'\nexport {a}".to_string(), - position: Some(Position::new(1, 1, 0, 2, 11, 28)) + position: Some(Position::new(1, 1, 0, 2, 11, 28)), + stops: vec![(0, 0), (17, 17), (18, 18)] })], position: Some(Position::new(1, 1, 0, 2, 11, 28)) }), diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs index 0b13149..c9ff560 100644 --- a/tests/mdx_expression_flow.rs +++ b/tests/mdx_expression_flow.rs @@ -94,7 +94,8 @@ fn mdx_expression_flow_agnostic() -> Result<(), String> { Node::Root(Root { children: vec![Node::MdxFlowExpression(MdxFlowExpression { value: "alpha +\nbravo".to_string(), - position: Some(Position::new(1, 1, 0, 2, 7, 15)) + position: Some(Position::new(1, 1, 0, 2, 7, 15)), + stops: vec![(0, 1), (7, 8), (8, 9)] })], position: Some(Position::new(1, 1, 0, 2, 7, 15)) }), diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs index d893a70..0aee081 100644 --- a/tests/mdx_expression_text.rs +++ b/tests/mdx_expression_text.rs @@ -119,7 +119,7 @@ fn mdx_expression_text_gnostic_core() -> Result<(), String> { assert_eq!( micromark_with_options("a {var b = \"c\"} d", &swc).err().unwrap(), - "1:7: Could not parse expression with swc: Unexpected token `var`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", + "1:4: Could not parse expression with swc: Unexpected token `var`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", "should crash on non-expressions" ); @@ -213,7 +213,8 @@ fn mdx_expression_text_agnostic() -> Result<(), String> { }), Node::MdxTextExpression(MdxTextExpression { value: "alpha".to_string(), - position: Some(Position::new(1, 3, 2, 1, 10, 9)) + position: Some(Position::new(1, 3, 2, 1, 10, 9)), + stops: vec![(0, 3)] }), Node::Text(Text { value: " b.".to_string(), diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs index 9064e83..22a701a 100644 --- a/tests/mdx_jsx_text.rs +++ b/tests/mdx_jsx_text.rs @@ -169,7 +169,10 @@ fn mdx_jsx_text_core() -> Result<(), String> { children: vec![ Node::MdxJsxTextElement(MdxJsxTextElement { name: Some("a".to_string()), - attributes: vec![AttributeContent::Expression("...b".to_string())], + attributes: vec![AttributeContent::Expression( + "...b".to_string(), + vec![(0, 4)] + )], children: vec![], position: Some(Position::new(1, 1, 0, 1, 13, 12)) }), @@ -235,7 +238,10 @@ fn mdx_jsx_text_core() -> Result<(), String> { }), AttributeContent::Property(MdxJsxAttribute { name: "f".to_string(), - value: Some(AttributeValue::Expression("g".to_string())), + value: Some(AttributeValue::Expression( + "g".to_string(), + vec![(0, 18)] + )), }), ], children: vec![], diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs index 1ad8789..48460ca 100644 --- a/tests/test_utils/hast.rs +++ b/tests/test_utils/hast.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -// ^-- fix later +// ^-- To do: fix later extern crate alloc; extern crate micromark; @@ -9,7 +9,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute}; +pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute, Stop}; use micromark::unist::Position; /// Nodes. @@ -254,6 +254,9 @@ pub struct MdxExpression { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } /// MDX: ESM. @@ -269,4 +272,7 @@ pub struct MdxjsEsm { pub value: String, /// Positional info. pub position: Option<Position>, + + // Custom data on where each slice of `value` came from. + pub stops: Vec<Stop>, } diff --git a/tests/test_utils/jsx_rewrite.rs b/tests/test_utils/jsx_rewrite.rs index b6ffad6..9dd2605 100644 --- a/tests/test_utils/jsx_rewrite.rs +++ b/tests/test_utils/jsx_rewrite.rs @@ -1,9 +1,13 @@ extern crate swc_common; extern crate swc_ecma_ast; -use crate::{ - micromark::{id_cont_ as id_cont, id_start_ as id_start}, - test_utils::to_swc::Program, +use crate::test_utils::{ + micromark_swc_utils::{position_to_string, span_to_position}, + swc_utils::{ + create_binary_expression, create_ident, create_ident_expression, create_member_expression, + }, + to_swc::Program, }; +use micromark::{id_cont_ as id_cont, id_start_ as id_start, unist::Position, Location}; use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; /// Configuration. @@ -21,10 +25,17 @@ pub struct Options { /// Rewrite JSX in an MDX file so that components can be passed in and provided. #[allow(dead_code)] -pub fn jsx_rewrite(mut program: Program, options: &Options) -> Program { +pub fn jsx_rewrite( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Program { let mut state = State { scopes: vec![], + location, provider: options.provider_import_source.is_some(), + path: program.path.clone(), + development: options.development, create_provider_import: false, create_error_helper: false, }; @@ -44,7 +55,10 @@ pub fn jsx_rewrite(mut program: Program, options: &Options) -> Program { // If potentially missing components are used, add the helper used for // errors. if state.create_error_helper { - program.module.body.push(create_error_helper()); + program + .module + .body + .push(create_error_helper(state.development, state.path)); } program @@ -88,8 +102,7 @@ struct Info { /// ``` /// vec![("a".into(), false), ("a.b".into(), true)] /// ``` - // To do: add positional info later. - references: Vec<(String, bool)>, + references: Vec<(String, bool, Option<Position>)>, } /// Scope (block or function/global). @@ -103,9 +116,14 @@ struct Scope { /// Context. #[derive(Debug, Default, Clone)] -struct State { +struct State<'a> { + location: Option<&'a Location>, + /// Path to file. + path: Option<String>, /// List of current scopes. scopes: Vec<Scope>, + /// Whether the user is in development mode. + development: bool, /// Whether the user uses a provider. provider: bool, /// Whether a provider is referenced. @@ -117,7 +135,7 @@ struct State { create_error_helper: bool, } -impl State { +impl<'a> State<'a> { /// Open a new scope. fn enter(&mut self, info: Option<Info>) { self.scopes.push(Scope { @@ -432,8 +450,47 @@ impl State { // if (!a) _missingMdxReference("a", false); // if (!a.b) _missingMdxReference("a.b", true); // ``` - for (id, component) in info.references { + for (id, component, position) in info.references { self.create_error_helper = true; + + let mut args = vec![ + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: id.clone().into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }, + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( + swc_ecma_ast::Bool { + value: component, + span: swc_common::DUMMY_SP, + }, + ))), + }, + ]; + + // Add the source location if it exists and if `development` is on. + if let Some(position) = position.as_ref() { + if self.development { + args.push(swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: position_to_string(position).into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }) + } + } + statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt { test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr { op: swc_ecma_ast::UnaryOp::Bang, @@ -446,27 +503,7 @@ impl State { callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( "_missingMdxReference", ))), - args: vec![ - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: id.into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }, - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( - swc_ecma_ast::Bool { - value: component, - span: swc_common::DUMMY_SP, - }, - ))), - }, - ], + args, type_args: None, span: swc_common::DUMMY_SP, })), @@ -545,6 +582,7 @@ impl State { None } } + /// Get the top-level scope’s info, mutably. fn current_top_level_info_mut(&mut self) -> Option<&mut Info> { if let Some(scope) = self.scopes.get_mut(1) { @@ -623,7 +661,7 @@ impl State { } } -impl VisitMut for State { +impl<'a> VisitMut for State<'a> { noop_visit_mut_type!(); /// Rewrite JSX identifiers. @@ -632,6 +670,7 @@ impl VisitMut for State { if let Some(info) = self.current_top_level_info() { // Rewrite only if we can rewrite. if is_props_receiving_fn(&info.name) || self.provider { + let position = span_to_position(&node.span, self.location); match &node.opening.name { // `<x.y>`, `<Foo.Bar>`, `<x.y.z>`. swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => { @@ -656,7 +695,6 @@ impl VisitMut for State { if !in_scope { let info_mut = self.current_top_level_info_mut().unwrap(); - // To do: add positional info. let mut index = 1; while index <= ids.len() { let full_id = ids[0..index].join("."); @@ -668,7 +706,9 @@ impl VisitMut for State { reference.1 = true; } } else { - info_mut.references.push((full_id, component)) + info_mut + .references + .push((full_id, component, position.clone())) } index += 1; } @@ -749,7 +789,7 @@ impl VisitMut for State { { reference.1 = true; } else { - info_mut.references.push((id.clone(), true)) + info_mut.references.push((id.clone(), true, position)) } } @@ -948,8 +988,8 @@ fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem { /// throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it."); /// } /// ``` -fn create_error_helper() -> swc_ecma_ast::ModuleItem { - let parameters = vec![ +fn create_error_helper(development: bool, path: Option<String>) -> swc_ecma_ast::ModuleItem { + let mut parameters = vec![ swc_ecma_ast::Param { pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { id: create_ident("id"), @@ -968,7 +1008,19 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { }, ]; - let message = vec![ + // Accept a source location (which might be undefiend). + if development { + parameters.push(swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("place"), + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }) + } + + let mut message = vec![ swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { value: "Expected ".into(), span: swc_common::DUMMY_SP, @@ -1009,8 +1061,43 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { })), ]; - // To do: in development, add `place` param, and use the positional info. - // Also, then, add file path. + // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""` + if development { + message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { + expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(create_ident_expression("place")), + cons: Box::new(create_binary_expression( + vec![ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: "\nIt’s referenced in your code at `".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + create_ident_expression("place"), + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: if let Some(path) = path { + format!("` in `{}`", path).into() + } else { + "`".into() + }, + span: swc_common::DUMMY_SP, + raw: None, + })), + ], + swc_ecma_ast::BinaryOp::Add, + )), + alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: "".into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + })) + } swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn( swc_ecma_ast::FnDecl { @@ -1047,100 +1134,6 @@ fn create_error_helper() -> swc_ecma_ast::ModuleItem { ))) } -/// Generate a binary expression. -/// -/// ```js -/// a + b + c -/// a || b -/// ``` -fn create_binary_expression( - mut exprs: Vec<swc_ecma_ast::Expr>, - op: swc_ecma_ast::BinaryOp, -) -> swc_ecma_ast::Expr { - exprs.reverse(); - - let mut left = None; - - while let Some(right_expr) = exprs.pop() { - left = Some(if let Some(left_expr) = left { - swc_ecma_ast::Expr::Bin(swc_ecma_ast::BinExpr { - left: Box::new(left_expr), - right: Box::new(right_expr), - op, - span: swc_common::DUMMY_SP, - }) - } else { - right_expr - }); - } - - left.expect("expected one or more expressions") -} - -/// Generate a member expression. -/// -/// ```js -/// a.b -/// a -/// ``` -fn create_member_expression(name: &str) -> swc_ecma_ast::Expr { - let bytes = name.as_bytes(); - let mut index = 0; - let mut start = 0; - let mut parts = vec![]; - - while index < bytes.len() { - if bytes[index] == b'.' { - parts.push(&name[start..index]); - start = index + 1; - } - - index += 1; - } - - if parts.len() > 1 { - let mut member = swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression(parts[0])), - prop: swc_ecma_ast::MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - let mut index = 2; - while index < parts.len() { - member = swc_ecma_ast::MemberExpr { - obj: Box::new(swc_ecma_ast::Expr::Member(member)), - prop: swc_ecma_ast::MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - index += 1; - } - swc_ecma_ast::Expr::Member(member) - } else { - create_ident_expression(name) - } -} - -/// Generate an ident expression. -/// -/// ```js -/// a -/// ``` -fn create_ident_expression(sym: &str) -> swc_ecma_ast::Expr { - swc_ecma_ast::Expr::Ident(create_ident(sym)) -} - -/// Generate an ident. -/// -/// ```js -/// a -/// ``` -fn create_ident(sym: &str) -> swc_ecma_ast::Ident { - swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - } -} - /// Check if this function is a props receiving component: it’s one of ours. fn is_props_receiving_fn(name: &Option<String>) -> bool { if let Some(name) = name { diff --git a/tests/test_utils/micromark_swc_utils.rs b/tests/test_utils/micromark_swc_utils.rs new file mode 100644 index 0000000..13678d5 --- /dev/null +++ b/tests/test_utils/micromark_swc_utils.rs @@ -0,0 +1,134 @@ +extern crate swc_common; +use micromark::{ + mdast::Stop, + unist::{Point, Position}, + Location, +}; +use swc_common::{BytePos, Span, SyntaxContext, DUMMY_SP}; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut}; + +/// Turn a unist position, into an SWC span, of two byte positions. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn position_to_span(position: Option<&Position>) -> Span { + position.map_or(DUMMY_SP, |d| Span { + lo: point_to_bytepos(&d.start), + hi: point_to_bytepos(&d.end), + ctxt: SyntaxContext::empty(), + }) +} + +/// Turn an SWC span, of two byte positions, into a unist position. +/// +/// This assumes the span comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn span_to_position(span: &Span, location: Option<&Location>) -> Option<Position> { + let lo = span.lo.0 as usize; + let hi = span.hi.0 as usize; + + if lo > 0 && hi > 0 { + if let Some(location) = location { + if let Some(start) = location.to_point(lo - 1) { + if let Some(end) = location.to_point(hi - 1) { + return Some(Position { start, end }); + } + } + } + } + + None +} + +/// Turn a unist point into an SWC byte position. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn point_to_bytepos(point: &Point) -> BytePos { + BytePos(point.offset as u32 + 1) +} + +/// Turn an SWC byte position into a unist point. +/// +/// This assumes the byte position comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn bytepos_to_point(bytepos: &BytePos, location: Option<&Location>) -> Option<Point> { + let pos = bytepos.0 as usize; + + if pos > 0 { + if let Some(location) = location { + return location.to_point(pos - 1); + } + } + + None +} + +/// Prefix an error message with an optional point. +pub fn prefix_error_with_point(reason: String, point: Option<&Point>) -> String { + if let Some(point) = point { + format!("{}: {}", point_to_string(point), reason) + } else { + reason + } +} + +/// Serialize a unist position for humans. +pub fn position_to_string(position: &Position) -> String { + format!( + "{}-{}", + point_to_string(&position.start), + point_to_string(&position.end) + ) +} + +/// Serialize a unist point for humans. +pub fn point_to_string(point: &Point) -> String { + format!("{}:{}", point.line, point.column) +} + +/// Visitor to fix SWC byte positions. +/// +/// This assumes the byte position comes from an **unfixed** tree. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +#[derive(Debug, Default, Clone)] +pub struct RewriteContext<'a> { + pub prefix_len: usize, + pub stops: &'a [Stop], + pub location: Option<&'a Location>, +} + +impl<'a> VisitMut for RewriteContext<'a> { + noop_visit_mut_type!(); + + // Rewrite spans. + fn visit_mut_span(&mut self, span: &mut Span) { + let mut result = DUMMY_SP; + let lo_rel = span.lo.0 as usize; + let hi_rel = span.hi.0 as usize; + + if lo_rel > self.prefix_len && hi_rel > self.prefix_len { + if let Some(lo_abs) = + Location::relative_to_absolute(self.stops, lo_rel - 1 - self.prefix_len) + { + if let Some(hi_abs) = + Location::relative_to_absolute(self.stops, hi_rel - 1 - self.prefix_len) + { + result = Span { + lo: BytePos(lo_abs as u32 + 1), + hi: BytePos(hi_abs as u32 + 1), + ctxt: SyntaxContext::empty(), + }; + } + } + } + + *span = result; + } +} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 339992c..99ded2f 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,6 +1,8 @@ pub mod hast; pub mod jsx_rewrite; +pub mod micromark_swc_utils; pub mod swc; +pub mod swc_utils; pub mod to_document; pub mod to_hast; pub mod to_swc; diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs index fb91a3b..78859b6 100644 --- a/tests/test_utils/swc.rs +++ b/tests/test_utils/swc.rs @@ -2,7 +2,10 @@ extern crate micromark; extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_parser; -use micromark::{MdxExpressionKind, MdxSignal}; +use crate::test_utils::micromark_swc_utils::{ + bytepos_to_point, prefix_error_with_point, RewriteContext, +}; +use micromark::{mdast::Stop, unist::Point, Location, MdxExpressionKind, MdxSignal}; use swc_common::{ source_map::Pos, sync::Lrc, BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Spanned, }; @@ -11,10 +14,7 @@ use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; use swc_ecma_parser::{ error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, }; - -// To do: -// Use lexer in the future: -// <https://docs.rs/swc_ecma_parser/0.99.1/swc_ecma_parser/lexer/index.html> +use swc_ecma_visit::VisitMutWith; /// Lex ESM in MDX with SWC. #[allow(dead_code)] @@ -24,40 +24,42 @@ pub fn parse_esm(value: &str) -> MdxSignal { let result = parse_file_as_module(&file, syntax, version, None, &mut errors); match result { - Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"), + Err(error) => swc_error_to_signal(&error, "esm", value.len(), 0), Ok(tree) => { if errors.is_empty() { check_esm_ast(&tree) } else { - if errors.len() > 1 { - println!("parse_esm: todo: multiple errors? {:?}", errors); - } - swc_error_to_signal(&errors[0], value.len(), 0, "esm") + swc_error_to_signal(&errors[0], "esm", value.len(), 0) } } } } /// Parse ESM in MDX with SWC. -/// To do: figure out how to fix positional info. /// See `drop_span` in `swc_ecma_utils` for inspiration? #[allow(dead_code)] -pub fn parse_esm_to_tree(value: &str) -> Result<swc_ecma_ast::Module, String> { +pub fn parse_esm_to_tree( + value: &str, + stops: &[Stop], + location: Option<&Location>, +) -> Result<swc_ecma_ast::Module, String> { let (file, syntax, version) = create_config(value.to_string()); let mut errors = vec![]; - let result = parse_file_as_module(&file, syntax, version, None, &mut errors); + let mut rewrite_context = RewriteContext { + stops, + location, + prefix_len: 0, + }; match result { - Err(error) => Err(swc_error_to_string(&error)), - Ok(module) => { + Err(error) => Err(swc_error_to_error(&error, "esm", &rewrite_context)), + Ok(mut module) => { if errors.is_empty() { + module.visit_mut_with(&mut rewrite_context); Ok(module) } else { - if errors.len() > 1 { - println!("parse_esm_to_tree: todo: multiple errors? {:?}", errors); - } - Err(swc_error_to_string(&errors[0])) + Err(swc_error_to_error(&errors[0], "esm", &rewrite_context)) } } } @@ -87,33 +89,31 @@ pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); match result { - Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"), + Err(error) => swc_error_to_signal(&error, "expression", value.len(), prefix.len()), Ok(tree) => { if errors.is_empty() { - let place = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); + let expression_end = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); let result = check_expression_ast(&tree, kind); if matches!(result, MdxSignal::Ok) { - whitespace_and_comments(place, value) + whitespace_and_comments(expression_end, value) } else { result } } else { - if errors.len() > 1 { - unreachable!("parse_expression: todo: multiple errors? {:?}", errors); - } - swc_error_to_signal(&errors[0], value.len(), prefix.len(), "expression") + swc_error_to_signal(&errors[0], "expression", value.len(), prefix.len()) } } } } /// Parse ESM in MDX with SWC. -/// To do: figure out how to fix positional info. /// See `drop_span` in `swc_ecma_utils` for inspiration? #[allow(dead_code)] pub fn parse_expression_to_tree( value: &str, kind: &MdxExpressionKind, + stops: &[Stop], + location: Option<&Location>, ) -> Result<Box<swc_ecma_ast::Expr>, String> { // For attribute expression, a spread is needed, for which we have to prefix // and suffix the input. @@ -127,11 +127,21 @@ pub fn parse_expression_to_tree( 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); + let mut rewrite_context = RewriteContext { + stops, + location, + prefix_len: prefix.len(), + }; match result { - Err(error) => Err(swc_error_to_string(&error)), - Ok(expr) => { + Err(error) => Err(swc_error_to_error(&error, "expression", &rewrite_context)), + Ok(mut expr) => { if errors.is_empty() { + // Fix positions. + expr.visit_mut_with(&mut rewrite_context); + + let expr_bytepos = expr.span().lo; + if matches!(kind, MdxExpressionKind::AttributeExpression) { let mut obj = None; @@ -143,27 +153,37 @@ pub fn parse_expression_to_tree( if let Some(mut obj) = obj { if obj.props.len() > 1 { - Err("Unexpected extra content in spread: only a single spread is supported".into()) + Err(create_error_message( + "Unexpected extra content in spread: only a single spread is supported", + "expression", + bytepos_to_point(&obj.span.lo, location).as_ref() + )) } else if let Some(swc_ecma_ast::PropOrSpread::Spread(d)) = obj.props.pop() { Ok(d.expr) } else { - Err("Unexpected prop in spread: only a spread is supported".into()) + Err(create_error_message( + "Unexpected prop in spread: only a spread is supported", + "expression", + bytepos_to_point(&obj.span.lo, location).as_ref(), + )) } } else { - Err("Expected an object spread (`{...spread}`)".into()) + Err(create_error_message( + "Expected an object spread (`{...spread}`)", + "expression", + bytepos_to_point(&expr_bytepos, location).as_ref(), + )) } } else { Ok(expr) } } else { - if errors.len() > 1 { - println!( - "parse_expression_to_tree: todo: multiple errors? {:?}", - errors - ); - } - Err(swc_error_to_string(&errors[0])) + Err(swc_error_to_error( + &errors[0], + "expression", + &rewrite_context, + )) } } } @@ -203,10 +223,10 @@ fn check_esm_ast(tree: &Module) -> MdxSignal { let node = &tree.body[index]; if !node.is_module_decl() { - let place = fix_swc_position(node.span().hi.to_usize(), 0); + let relative = fix_swc_position(node.span().lo.to_usize(), 0); return MdxSignal::Error( "Unexpected statement in code: only import/exports are supported".to_string(), - place, + relative, ); } @@ -248,24 +268,47 @@ fn check_expression_ast(tree: &Expr, kind: &MdxExpressionKind) -> MdxSignal { /// * Else, yields `MdxSignal::Error`. fn swc_error_to_signal( error: &SwcError, + name: &str, value_len: usize, prefix_len: usize, - name: &str, ) -> MdxSignal { - let place = fix_swc_position(error.span().hi.to_usize(), prefix_len); - let message = format!( - "Could not parse {} with swc: {}", - name, - swc_error_to_string(error) - ); + let reason = create_error_reason(&swc_error_to_string(error), name); + let error_end = fix_swc_position(error.span().hi.to_usize(), prefix_len); - if place >= value_len { - MdxSignal::Eof(message) + if error_end >= value_len { + MdxSignal::Eof(reason) } else { - MdxSignal::Error(message, place) + MdxSignal::Error( + reason, + fix_swc_position(error.span().lo.to_usize(), prefix_len), + ) } } +fn swc_error_to_error(error: &SwcError, name: &str, context: &RewriteContext) -> String { + create_error_message( + &swc_error_to_string(error), + name, + context + .location + .and_then(|location| { + location.relative_to_point( + context.stops, + fix_swc_position(error.span().lo.to_usize(), context.prefix_len), + ) + }) + .as_ref(), + ) +} + +fn create_error_message(reason: &str, name: &str, point: Option<&Point>) -> String { + prefix_error_with_point(create_error_reason(name, reason), point) +} + +fn create_error_reason(reason: &str, name: &str) -> String { + format!("Could not parse {} with swc: {}", name, reason) +} + /// Turn an SWC error into a string. fn swc_error_to_string(error: &SwcError) -> String { error.kind().msg().into() diff --git a/tests/test_utils/swc_utils.rs b/tests/test_utils/swc_utils.rs new file mode 100644 index 0000000..1e1a526 --- /dev/null +++ b/tests/test_utils/swc_utils.rs @@ -0,0 +1,96 @@ +extern crate swc_common; +extern crate swc_ecma_ast; + +use swc_common::DUMMY_SP; +use swc_ecma_ast::{BinExpr, BinaryOp, Expr, Ident, MemberExpr, MemberProp}; + +/// Generate an ident. +/// +/// ```js +/// a +/// ``` +pub fn create_ident(sym: &str) -> Ident { + Ident { + sym: sym.into(), + optional: false, + span: DUMMY_SP, + } +} + +/// Generate an ident expression. +/// +/// ```js +/// a +/// ``` +pub fn create_ident_expression(sym: &str) -> Expr { + Expr::Ident(create_ident(sym)) +} + +/// Generate a binary expression. +/// +/// ```js +/// a + b + c +/// a || b +/// ``` +pub fn create_binary_expression(mut exprs: Vec<Expr>, op: BinaryOp) -> Expr { + exprs.reverse(); + + let mut left = None; + + while let Some(right_expr) = exprs.pop() { + left = Some(if let Some(left_expr) = left { + Expr::Bin(BinExpr { + left: Box::new(left_expr), + right: Box::new(right_expr), + op, + span: swc_common::DUMMY_SP, + }) + } else { + right_expr + }); + } + + left.expect("expected one or more expressions") +} + +/// Generate a member expression. +/// +/// ```js +/// a.b +/// a +/// ``` +pub fn create_member_expression(name: &str) -> Expr { + let bytes = name.as_bytes(); + let mut index = 0; + let mut start = 0; + let mut parts = vec![]; + + while index < bytes.len() { + if bytes[index] == b'.' { + parts.push(&name[start..index]); + start = index + 1; + } + + index += 1; + } + + if parts.len() > 1 { + let mut member = MemberExpr { + obj: Box::new(create_ident_expression(parts[0])), + prop: MemberProp::Ident(create_ident(parts[1])), + span: swc_common::DUMMY_SP, + }; + let mut index = 2; + while index < parts.len() { + member = MemberExpr { + obj: Box::new(Expr::Member(member)), + prop: MemberProp::Ident(create_ident(parts[1])), + span: swc_common::DUMMY_SP, + }; + index += 1; + } + Expr::Member(member) + } else { + create_ident_expression(name) + } +} diff --git a/tests/test_utils/to_document.rs b/tests/test_utils/to_document.rs index ded028a..938df1b 100644 --- a/tests/test_utils/to_document.rs +++ b/tests/test_utils/to_document.rs @@ -1,6 +1,12 @@ -extern crate swc_common; extern crate swc_ecma_ast; -use crate::test_utils::to_swc::Program; +use crate::test_utils::{ + micromark_swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, + to_swc::Program, +}; +use micromark::{ + unist::{Point, Position}, + Location, +}; /// JSX runtimes. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -60,7 +66,11 @@ impl Default for Options { } #[allow(dead_code)] -pub fn to_document(mut program: Program, options: &Options) -> Result<Program, String> { +pub fn to_document( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Result<Program, String> { // New body children. let mut replacements = vec![]; @@ -158,8 +168,8 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S // is. let mut input = program.module.body.split_off(0); input.reverse(); - // To do: place position in this. let mut layout = false; + let mut layout_position = None; let content = true; while let Some(module_item) = input.pop() { @@ -174,13 +184,15 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( decl, )) => { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&decl.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&decl.span, location); match decl.decl { swc_ecma_ast::DefaultDecl::Class(cls) => { replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) @@ -190,22 +202,26 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S } swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { return Err( - "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component" - .into(), - ) + prefix_error_with_point( + "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), + bytepos_to_point(&decl.span.lo, location).as_ref() + ) + ); } } } swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( expr, )) => { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&expr.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&expr.span, location); replacements.push(create_layout_decl(*expr.expr)); } // ```js @@ -239,12 +255,14 @@ pub fn to_document(mut program: Program, options: &Options) -> Result<Program, S // Looks like some TC39 proposal. Ignore for now // and only do things if this is an ID. if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { - // To do: use positional info. if layout { - return Err("Cannot specify multiple layouts".into()); + return Err(create_double_layout_message( + bytepos_to_point(&ident.span.lo, location).as_ref(), + layout_position.as_ref(), + )); } - // To do: set positional info. layout = true; + layout_position = span_to_position(&ident.span, location); take = true; id = Some(ident.clone()); } @@ -623,3 +641,18 @@ fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { }, )))) } + +/// Create an error message about multiple layouts. +fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { + prefix_error_with_point( + format!( + "Cannot specify multiple layouts{}", + if let Some(previous) = previous { + format!(" (previous: {:?})", previous) + } else { + "".into() + } + ), + at, + ) +} diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs index 0716daa..4907e23 100644 --- a/tests/test_utils/to_hast.rs +++ b/tests/test_utils/to_hast.rs @@ -827,10 +827,23 @@ fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) - /// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.to_string(), - position: node.position().cloned(), - })) + match node { + mdast::Node::MdxFlowExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + mdast::Node::MdxTextExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + _ => unreachable!("expected expression"), + } } /// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. @@ -858,6 +871,7 @@ fn transform_mdxjs_esm( Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { value: mdxjs_esm.value.clone(), position: mdxjs_esm.position.clone(), + stops: mdxjs_esm.stops.clone(), })) } diff --git a/tests/test_utils/to_swc.rs b/tests/test_utils/to_swc.rs index 6c7312b..02de514 100644 --- a/tests/test_utils/to_swc.rs +++ b/tests/test_utils/to_swc.rs @@ -1,13 +1,18 @@ extern crate swc_common; extern crate swc_ecma_ast; -use crate::test_utils::hast; -use crate::test_utils::swc::{parse_esm_to_tree, parse_expression_to_tree}; +use crate::test_utils::{ + hast, + micromark_swc_utils::position_to_span, + swc::{parse_esm_to_tree, parse_expression_to_tree}, + swc_utils::create_ident, +}; use core::str; -use micromark::{unist::Position, MdxExpressionKind}; +use micromark::{Location, MdxExpressionKind}; /// Result. #[derive(Debug, PartialEq, Eq)] pub struct Program { + pub path: Option<String>, /// JS AST. pub module: swc_ecma_ast::Module, /// Comments relating to AST. @@ -22,7 +27,7 @@ enum Space { } #[derive(Debug)] -struct Context { +struct Context<'a> { /// Whether we’re in HTML or SVG. /// /// Not used yet, likely useful in the future. @@ -31,22 +36,22 @@ struct Context { comments: Vec<swc_common::comments::Comment>, /// Declarations and stuff. esm: Vec<swc_ecma_ast::ModuleItem>, -} - -impl Context { - /// Create a new context. - fn new() -> Context { - Context { - space: Space::Html, - comments: vec![], - esm: vec![], - } - } + /// Optional way to turn relative positions into points. + location: Option<&'a Location>, } #[allow(dead_code)] -pub fn to_swc(tree: &hast::Node) -> Result<Program, String> { - let mut context = Context::new(); +pub fn to_swc( + tree: &hast::Node, + path: Option<String>, + location: Option<&Location>, +) -> Result<Program, String> { + let mut context = Context { + space: Space::Html, + comments: vec![], + esm: vec![], + location, + }; let expr = match one(&mut context, tree)? { Some(swc_ecma_ast::JSXElementChild::JSXFragment(x)) => { Some(swc_ecma_ast::Expr::JSXFragment(x)) @@ -81,6 +86,7 @@ pub fn to_swc(tree: &hast::Node) -> Result<Program, String> { } Ok(Program { + path, module, comments: context.comments, }) @@ -260,12 +266,14 @@ fn transform_mdx_jsx_element( }, ))) } - Some(hast::AttributeValue::Expression(value)) => { + Some(hast::AttributeValue::Expression(value, stops)) => { Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer( swc_ecma_ast::JSXExprContainer { expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( value, &MdxExpressionKind::AttributeValueExpression, + stops, + context.location, )?), span: swc_common::DUMMY_SP, }, @@ -280,9 +288,13 @@ fn transform_mdx_jsx_element( value, }) } - hast::AttributeContent::Expression(value) => { - let expr = - parse_expression_to_tree(value, &MdxExpressionKind::AttributeExpression)?; + hast::AttributeContent::Expression(value, stops) => { + let expr = parse_expression_to_tree( + value, + &MdxExpressionKind::AttributeExpression, + stops, + context.location, + )?; swc_ecma_ast::JSXAttrOrSpread::SpreadElement(swc_ecma_ast::SpreadElement { dot3_token: swc_common::DUMMY_SP, expr, @@ -303,7 +315,7 @@ fn transform_mdx_jsx_element( /// [`MdxExpression`][hast::MdxExpression]. fn transform_mdx_expression( - _context: &mut Context, + context: &mut Context, node: &hast::Node, expression: &hast::MdxExpression, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { @@ -312,6 +324,8 @@ fn transform_mdx_expression( expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( &expression.value, &MdxExpressionKind::Expression, + &expression.stops, + context.location, )?), span: position_to_span(node.position()), }, @@ -324,7 +338,7 @@ fn transform_mdxjs_esm( _node: &hast::Node, esm: &hast::MdxjsEsm, ) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let mut module = parse_esm_to_tree(&esm.value)?; + let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?; let mut index = 0; // To do: check that identifiers are not duplicated across esm blocks. @@ -455,15 +469,6 @@ fn create_fragment( } } -/// Create an ident. -fn create_ident(sym: &str) -> swc_ecma_ast::Ident { - swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - } -} - /// Create a JSX element name. fn create_jsx_name(name: &str) -> swc_ecma_ast::JSXElementName { match parse_jsx_name(name) { @@ -515,15 +520,6 @@ fn create_jsx_attr_name(name: &str) -> swc_ecma_ast::JSXAttrName { } } -/// Turn a unist positions into an SWC span. -fn position_to_span(position: Option<&Position>) -> swc_common::Span { - position.map_or(swc_common::DUMMY_SP, |d| swc_common::Span { - lo: swc_common::BytePos(d.start.offset as u32), - hi: swc_common::BytePos(d.end.offset as u32), - ctxt: swc_common::SyntaxContext::empty(), - }) -} - fn inter_element_whitespace(value: &str) -> bool { let bytes = value.as_bytes(); let mut index = 0; diff --git a/tests/xxx_document.rs b/tests/xxx_document.rs index d5c8eef..7e43b1c 100644 --- a/tests/xxx_document.rs +++ b/tests/xxx_document.rs @@ -3,7 +3,7 @@ extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_codegen; mod test_utils; -use micromark::{micromark_to_mdast, Constructs, ParseOptions}; +use micromark::{micromark_to_mdast, Constructs, Location, ParseOptions}; use pretty_assertions::assert_eq; use test_utils::{ swc::{parse_esm, parse_expression, serialize}, @@ -13,6 +13,7 @@ use test_utils::{ }; fn from_markdown(value: &str) -> Result<String, String> { + let location = Location::new(value.as_bytes()); let mdast = micromark_to_mdast( value, &ParseOptions { @@ -23,7 +24,8 @@ fn from_markdown(value: &str) -> Result<String, String> { }, )?; let hast = to_hast(&mdast); - let program = to_document(to_swc(&hast)?, &DocumentOptions::default())?; + let swc_tree = to_swc(&hast, None, Some(&location))?; + let program = to_document(swc_tree, &DocumentOptions::default(), Some(&location))?; let value = serialize(&program.module); Ok(value) } @@ -156,8 +158,6 @@ export default MDXContent; "should support a named export w/o source, w/o a specifiers", ); - // ........... - assert_eq!( from_markdown("export {a, b as default} from 'c'")?, "export { a } from 'c'; @@ -201,5 +201,13 @@ export default MDXContent; "should support a named export w/ source, w/o a specifiers", ); + assert_eq!( + from_markdown("export default a = 1\n\nexport default b = 2") + .err() + .unwrap(), + "3:1: Cannot specify multiple layouts (previous: 1:1-1:21 (0-20))", + "should crash on a comment spread" + ); + Ok(()) } diff --git a/tests/xxx_hast.rs b/tests/xxx_hast.rs index 886bcee..b0856a2 100644 --- a/tests/xxx_hast.rs +++ b/tests/xxx_hast.rs @@ -1137,10 +1137,12 @@ fn hast() { to_hast(&mdast::Node::MdxFlowExpression(mdast::MdxFlowExpression { value: "a".into(), position: None, + stops: vec![] })), hast::Node::MdxExpression(hast::MdxExpression { value: "a".into(), - position: None + position: None, + stops: vec![] }), "should support an `MdxFlowExpression`", ); @@ -1149,10 +1151,12 @@ fn hast() { to_hast(&mdast::Node::MdxTextExpression(mdast::MdxTextExpression { value: "a".into(), position: None, + stops: vec![] })), hast::Node::MdxExpression(hast::MdxExpression { value: "a".into(), - position: None + position: None, + stops: vec![] }), "should support an `MdxTextExpression`", ); @@ -1193,10 +1197,12 @@ fn hast() { to_hast(&mdast::Node::MdxjsEsm(mdast::MdxjsEsm { value: "a".into(), position: None, + stops: vec![] })), hast::Node::MdxjsEsm(hast::MdxjsEsm { value: "a".into(), - position: None + position: None, + stops: vec![] }), "should support an `MdxjsEsm`", ); diff --git a/tests/xxx_jsx_rewrite.rs b/tests/xxx_jsx_rewrite.rs index 7a1c379..c383f13 100644 --- a/tests/xxx_jsx_rewrite.rs +++ b/tests/xxx_jsx_rewrite.rs @@ -3,7 +3,7 @@ extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_codegen; mod test_utils; -use micromark::{micromark_to_mdast, Constructs, ParseOptions}; +use micromark::{micromark_to_mdast, Constructs, Location, ParseOptions}; use pretty_assertions::assert_eq; use test_utils::{ jsx_rewrite::{jsx_rewrite, Options as RewriteOptions}, @@ -14,6 +14,7 @@ use test_utils::{ }; fn from_markdown(value: &str, options: &RewriteOptions) -> Result<String, String> { + let location = Location::new(value.as_bytes()); let mdast = micromark_to_mdast( value, &ParseOptions { @@ -24,8 +25,9 @@ fn from_markdown(value: &str, options: &RewriteOptions) -> Result<String, String }, )?; let hast = to_hast(&mdast); - let program = to_document(to_swc(&hast)?, &DocumentOptions::default())?; - let program = jsx_rewrite(program, options); + let swc_tree = to_swc(&hast, Some("example.mdx".into()), Some(&location))?; + let program = to_document(swc_tree, &DocumentOptions::default(), Some(&location))?; + let program = jsx_rewrite(program, options, Some(&location)); let value = serialize(&program.module); Ok(value) } @@ -351,5 +353,29 @@ function _missingMdxReference(id, component) { "should support providing components with JSX identifiers that are not JS identifiers in locally defined components", ); + assert_eq!( + from_markdown("# <Hi />", &RewriteOptions { + development: true, + ..Default::default() + })?, + "function _createMdxContent(props) { + const _components = Object.assign({ + h1: \"h1\" + }, props.components), { Hi } = _components; + if (!Hi) _missingMdxReference(\"Hi\", true, \"1:3-1:9\"); + return <_components.h1 ><Hi /></_components.h1>; +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); +} +export default MDXContent; +function _missingMdxReference(id, component, place) { + throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\" + (place ? \"\\nIt’s referenced in your code at `\" + place + \"` in `example.mdx`\" : \"\")); +} +", + "should create missing reference helpers w/o positional info in `development` mode", + ); + Ok(()) } diff --git a/tests/xxx_swc.rs b/tests/xxx_swc.rs index 26814cf..68a141d 100644 --- a/tests/xxx_swc.rs +++ b/tests/xxx_swc.rs @@ -12,14 +12,19 @@ use test_utils::{ #[test] fn swc() -> Result<(), String> { - let comment_ast = to_swc(&hast::Node::Comment(hast::Comment { - value: "a".into(), - position: None, - }))?; + let comment_ast = to_swc( + &hast::Node::Comment(hast::Comment { + value: "a".into(), + position: None, + }), + None, + None, + )?; assert_eq!( comment_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -66,19 +71,24 @@ fn swc() -> Result<(), String> { "should support a `Comment` (serialize)", ); - let element_ast = to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["b".into()]), - )], - children: vec![], - position: None, - }))?; + let element_ast = to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["b".into()]), + )], + children: vec![], + position: None, + }), + None, + None, + )?; assert_eq!( element_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -139,15 +149,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None, + })], position: None, - })], - position: None, - }))? + }), + None, + None + )? .module ), "<a >{\"a\"}</a>;\n", @@ -156,12 +170,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::String("c".into()),)], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("b".into(), hast::PropertyValue::String("c".into()),)], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b=\"c\"/>;\n", @@ -170,12 +188,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::Boolean(true),)], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("b".into(), hast::PropertyValue::Boolean(true),)], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b/>;\n", @@ -184,12 +206,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::Boolean(false),)], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("b".into(), hast::PropertyValue::Boolean(false),)], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a />;\n", @@ -198,15 +224,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![( - "b".into(), - hast::PropertyValue::CommaSeparated(vec!["c".into(), "d".into()]), - )], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![( + "b".into(), + hast::PropertyValue::CommaSeparated(vec!["c".into(), "d".into()]), + )], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b=\"c, d\"/>;\n", @@ -215,16 +245,20 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("data123".into(), hast::PropertyValue::Boolean(true),), - ("dataFoo".into(), hast::PropertyValue::Boolean(true),), - ("dataBAR".into(), hast::PropertyValue::Boolean(true),) - ], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("data123".into(), hast::PropertyValue::Boolean(true),), + ("dataFoo".into(), hast::PropertyValue::Boolean(true),), + ("dataBAR".into(), hast::PropertyValue::Boolean(true),) + ], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a data-123 data-foo data-b-a-r/>;\n", @@ -233,32 +267,41 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("role".into(), hast::PropertyValue::Boolean(true),), - ("ariaValueNow".into(), hast::PropertyValue::Boolean(true),), - ("ariaDescribedBy".into(), hast::PropertyValue::Boolean(true),) - ], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("role".into(), hast::PropertyValue::Boolean(true),), + ("ariaValueNow".into(), hast::PropertyValue::Boolean(true),), + ("ariaDescribedBy".into(), hast::PropertyValue::Boolean(true),) + ], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a role aria-valuenow aria-describedby/>;\n", "should support an `Element` w/ aria attributes", ); - let mdx_element_ast = to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - }))?; + let mdx_element_ast = to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: None, + attributes: vec![], + children: vec![], + position: None, + }), + None, + None, + )?; assert_eq!( mdx_element_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -293,12 +336,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a />;\n", @@ -307,12 +354,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a:b".into()), - attributes: vec![], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a:b".into()), + attributes: vec![], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a:b />;\n", @@ -321,12 +372,16 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a.b.c".into()), - attributes: vec![], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a.b.c".into()), + attributes: vec![], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a.b.c />;\n", @@ -335,15 +390,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "b".into(), + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "b".into(), + position: None, + })], position: None, - })], - position: None, - }))? + }), + None, + None + )? .module ), "<a >{\"b\"}</a>;\n", @@ -352,15 +411,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: None - })], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { + name: "b".into(), + value: None + })], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b/>;\n", @@ -369,15 +432,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: Some(hast::AttributeValue::Literal("c".into())) - })], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { + name: "b".into(), + value: Some(hast::AttributeValue::Literal("c".into())) + })], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b=\"c\"/>;\n", @@ -386,15 +453,19 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: Some(hast::AttributeValue::Expression("c".into())) - })], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { + name: "b".into(), + value: Some(hast::AttributeValue::Expression("c".into(), vec![])) + })], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a b={c}/>;\n", @@ -403,26 +474,36 @@ fn swc() -> Result<(), String> { assert_eq!( serialize( - &to_swc(&hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Expression("...c".into())], - children: vec![], - position: None, - }))? + &to_swc( + &hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: Some("a".into()), + attributes: vec![hast::AttributeContent::Expression("...c".into(), vec![])], + children: vec![], + position: None, + }), + None, + None + )? .module ), "<a {...c}/>;\n", "should support an `MdxElement` (element, expression attribute)", ); - let mdx_expression_ast = to_swc(&hast::Node::MdxExpression(hast::MdxExpression { - value: "a".into(), - position: None, - }))?; + let mdx_expression_ast = to_swc( + &hast::Node::MdxExpression(hast::MdxExpression { + value: "a".into(), + position: None, + stops: vec![], + }), + None, + None, + )?; assert_eq!( mdx_expression_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -439,13 +520,8 @@ fn swc() -> Result<(), String> { swc_ecma_ast::JSXExprContainer { expr: swc_ecma_ast::JSXExpr::Expr(Box::new( swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(1), - hi: swc_common::BytePos(2), - ctxt: swc_common::SyntaxContext::empty(), - }, sym: "a".into(), + span: swc_common::DUMMY_SP, optional: false, }) )), @@ -471,14 +547,20 @@ fn swc() -> Result<(), String> { "should support an `MdxExpression` (serialize)", ); - let mdxjs_esm_ast = to_swc(&hast::Node::MdxjsEsm(hast::MdxjsEsm { - value: "import a from 'b'".into(), - position: None, - }))?; + let mdxjs_esm_ast = to_swc( + &hast::Node::MdxjsEsm(hast::MdxjsEsm { + value: "import a from 'b'".into(), + position: None, + stops: vec![], + }), + None, + None, + )?; assert_eq!( mdxjs_esm_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::ModuleDecl( @@ -488,39 +570,19 @@ fn swc() -> Result<(), String> { local: swc_ecma_ast::Ident { sym: "a".into(), optional: false, - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(8), - hi: swc_common::BytePos(9), - ctxt: swc_common::SyntaxContext::empty(), - }, - }, - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(8), - hi: swc_common::BytePos(9), - ctxt: swc_common::SyntaxContext::empty(), + span: swc_common::DUMMY_SP, }, + span: swc_common::DUMMY_SP, } )], src: Box::new(swc_ecma_ast::Str { value: "b".into(), - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(15), - hi: swc_common::BytePos(18), - ctxt: swc_common::SyntaxContext::empty(), - }, + span: swc_common::DUMMY_SP, raw: Some("\'b\'".into()), }), type_only: false, asserts: None, - // To do: fix positions. - span: swc_common::Span { - lo: swc_common::BytePos(1), - hi: swc_common::BytePos(18), - ctxt: swc_common::SyntaxContext::empty(), - }, + span: swc_common::DUMMY_SP, }) )], span: swc_common::DUMMY_SP, @@ -536,17 +598,22 @@ fn swc() -> Result<(), String> { "should support an `MdxjsEsm` (serialize)", ); - let root_ast = to_swc(&hast::Node::Root(hast::Root { - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), + let root_ast = to_swc( + &hast::Node::Root(hast::Root { + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None, + })], position: None, - })], - position: None, - }))?; + }), + None, + None, + )?; assert_eq!( root_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( @@ -592,14 +659,19 @@ fn swc() -> Result<(), String> { "should support a `Root` (serialize)", ); - let text_ast = to_swc(&hast::Node::Text(hast::Text { - value: "a".into(), - position: None, - }))?; + let text_ast = to_swc( + &hast::Node::Text(hast::Text { + value: "a".into(), + position: None, + }), + None, + None, + )?; assert_eq!( text_ast, Program { + path: None, module: swc_ecma_ast::Module { shebang: None, body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( |