From b33a81e40620b8b3eaeeec9d0e0b34ca5958dead Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 28 Sep 2022 17:54:39 +0200 Subject: Add support for turning mdast to hast --- .github/workflows/main.yml | 2 +- readme.md | 3 +- src/construct/attention.rs | 5 +- src/construct/gfm_table.rs | 6 +- src/construct/heading_atx.rs | 6 +- src/construct/heading_setext.rs | 6 +- src/construct/label_end.rs | 4 +- src/construct/list_item.rs | 6 +- src/construct/partial_data.rs | 6 +- src/construct/partial_mdx_expression.rs | 2 +- src/construct/string.rs | 6 +- src/construct/text.rs | 5 +- src/lib.rs | 12 +- src/mdast.rs | 77 +- src/resolve.rs | 24 +- src/to_mdast.rs | 6 +- src/unist.rs | 75 ++ src/util/sanitize_uri.rs | 1 + tests/attention.rs | 6 +- tests/autolink.rs | 6 +- tests/block_quote.rs | 6 +- tests/character_escape.rs | 6 +- tests/character_reference.rs | 6 +- tests/code_fenced.rs | 6 +- tests/code_indented.rs | 6 +- tests/code_text.rs | 6 +- tests/definition.rs | 6 +- tests/frontmatter.rs | 6 +- tests/gfm_autolink_literal.rs | 6 +- tests/gfm_footnote.rs | 6 +- tests/gfm_strikethrough.rs | 6 +- tests/gfm_table.rs | 6 +- tests/gfm_task_list_item.rs | 6 +- tests/hard_break_escape.rs | 6 +- tests/hard_break_trailing.rs | 6 +- tests/heading_atx.rs | 6 +- tests/heading_setext.rs | 6 +- tests/html_flow.rs | 6 +- tests/html_text.rs | 6 +- tests/image.rs | 8 +- tests/link_reference.rs | 6 +- tests/link_resource.rs | 6 +- tests/list.rs | 6 +- tests/math_flow.rs | 6 +- tests/math_text.rs | 6 +- tests/mdx_esm.rs | 8 +- tests/mdx_expression_flow.rs | 8 +- tests/mdx_expression_text.rs | 8 +- tests/mdx_jsx_flow.rs | 6 +- tests/mdx_jsx_text.rs | 8 +- tests/mdx_swc.rs | 2 +- tests/misc_bom.rs | 4 +- tests/misc_dangerous_protocol.rs | 12 +- tests/misc_soft_break.rs | 4 +- tests/misc_tabs.rs | 8 +- tests/misc_url.rs | 4 +- tests/misc_zero.rs | 4 +- tests/test_utils/hast.rs | 279 ++++++ tests/test_utils/mod.rs | 248 +---- tests/test_utils/swc.rs | 247 +++++ tests/test_utils/to_hast.rs | 1457 ++++++++++++++++++++++++++++ tests/text.rs | 4 +- tests/thematic_break.rs | 6 +- tests/xxx_hast.rs | 1585 +++++++++++++++++++++++++++++++ 64 files changed, 3851 insertions(+), 467 deletions(-) create mode 100644 src/unist.rs create mode 100644 tests/test_utils/hast.rs create mode 100644 tests/test_utils/swc.rs create mode 100644 tests/test_utils/to_hast.rs create mode 100644 tests/xxx_hast.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d5d378b..9d45ca4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: with: toolchain: stable components: rustfmt, clippy - - run: cargo clippy -- -D clippy::pedantic -D clippy::cargo -A clippy::doc_link_with_quotes -A clippy::unnecessary_wraps + - run: cargo clippy --examples --tests --benches -- -D clippy::pedantic -A clippy::doc_link_with_quotes -A clippy::too_many_lines - run: cargo fmt --all -- --check - run: cargo test # - run: cargo install cargo-tarpaulin && cargo tarpaulin --out Xml diff --git a/readme.md b/readme.md index 3d18a00..24f2f02 100644 --- a/readme.md +++ b/readme.md @@ -363,7 +363,8 @@ The following scripts are useful when working on this project: ``` - lint: ```sh - cargo fmt --check && cargo clippy -- -D clippy::pedantic -D clippy::cargo -A clippy::doc_link_with_quotes -A clippy::unnecessary_wraps + cargo fmt --check &&\ + cargo clippy --examples --tests --benches -- -D clippy::pedantic -A clippy::doc_link_with_quotes -A clippy::too_many_lines ``` - test: ```sh diff --git a/src/construct/attention.rs b/src/construct/attention.rs index 4d58610..d99a52c 100644 --- a/src/construct/attention.rs +++ b/src/construct/attention.rs @@ -88,7 +88,6 @@ use crate::util::{ }, slice::Slice, }; -use alloc::string::String; use alloc::{vec, vec::Vec}; /// Attentention sequence that we can take markers from. @@ -152,7 +151,7 @@ pub fn inside(tokenizer: &mut Tokenizer) -> State { } /// Resolve sequences. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { // Find all sequences, gather info about them. let mut sequences = get_sequences(tokenizer); @@ -224,7 +223,7 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { tokenizer.map.consume(&mut tokenizer.events); - Ok(None) + None } /// Get sequences. diff --git a/src/construct/gfm_table.rs b/src/construct/gfm_table.rs index 63772c4..547358f 100644 --- a/src/construct/gfm_table.rs +++ b/src/construct/gfm_table.rs @@ -232,7 +232,7 @@ use crate::state::{Name as StateName, State}; use crate::subtokenize::Subresult; use crate::tokenizer::Tokenizer; use crate::util::{constant::TAB_SIZE, skip::opt_back as skip_opt_back}; -use alloc::{string::String, vec}; +use alloc::vec; /// Start of a GFM table. /// @@ -772,7 +772,7 @@ pub fn body_row_escape(tokenizer: &mut Tokenizer) -> State { } /// Resolve GFM table. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { let mut index = 0; let mut in_first_cell_awaiting_pipe = true; let mut in_row = false; @@ -887,7 +887,7 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { flush_table_end(tokenizer, last_table_end, last_table_has_body); } - Ok(None) + None } /// Generate a cell. diff --git a/src/construct/heading_atx.rs b/src/construct/heading_atx.rs index b76e455..c867117 100644 --- a/src/construct/heading_atx.rs +++ b/src/construct/heading_atx.rs @@ -69,7 +69,7 @@ use crate::state::{Name as StateName, State}; use crate::subtokenize::Subresult; use crate::tokenizer::Tokenizer; use crate::util::constant::{HEADING_ATX_OPENING_FENCE_SIZE_MAX, TAB_SIZE}; -use alloc::{string::String, vec}; +use alloc::vec; /// Start of a heading (atx). /// @@ -223,7 +223,7 @@ pub fn data(tokenizer: &mut Tokenizer) -> State { } /// Resolve heading (atx). -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { let mut index = 0; let mut heading_inside = false; let mut data_start: Option = None; @@ -283,5 +283,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { index += 1; } - Ok(None) + None } diff --git a/src/construct/heading_setext.rs b/src/construct/heading_setext.rs index 3a484e1..1e6fd00 100644 --- a/src/construct/heading_setext.rs +++ b/src/construct/heading_setext.rs @@ -77,7 +77,7 @@ use crate::state::{Name as StateName, State}; use crate::subtokenize::Subresult; use crate::tokenizer::Tokenizer; use crate::util::{constant::TAB_SIZE, skip}; -use alloc::{string::String, vec}; +use alloc::vec; /// At start of heading (setext) underline. /// @@ -184,7 +184,7 @@ pub fn after(tokenizer: &mut Tokenizer) -> State { } /// Resolve heading (setext). -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { tokenizer.map.consume(&mut tokenizer.events); let mut enter = skip::to(&tokenizer.events, 0, &[Name::HeadingSetextUnderline]); @@ -281,5 +281,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { tokenizer.map.consume(&mut tokenizer.events); - Ok(None) + None } diff --git a/src/construct/label_end.rs b/src/construct/label_end.rs index 95b9a27..ca71245 100644 --- a/src/construct/label_end.rs +++ b/src/construct/label_end.rs @@ -661,7 +661,7 @@ pub fn reference_collapsed_open(tokenizer: &mut Tokenizer) -> State { /// /// This turns matching label starts and label ends into links, images, and /// footnotes, and turns unmatched label starts back into data. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { // Inject labels. let labels = tokenizer.tokenize_state.labels.split_off(0); inject_labels(tokenizer, &labels); @@ -673,7 +673,7 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { tokenizer.map.consume(&mut tokenizer.events); - Ok(None) + None } /// Inject links/images/footnotes. diff --git a/src/construct/list_item.rs b/src/construct/list_item.rs index 13b740b..a4f166d 100644 --- a/src/construct/list_item.rs +++ b/src/construct/list_item.rs @@ -69,7 +69,7 @@ use crate::util::{ skip, slice::{Position, Slice}, }; -use alloc::{string::String, vec, vec::Vec}; +use alloc::{vec, vec::Vec}; /// Start of list item. /// @@ -371,7 +371,7 @@ pub fn cont_filled(tokenizer: &mut Tokenizer) -> State { } /// Find adjacent list items with the same marker. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { let mut lists_wip: Vec<(u8, usize, usize, usize)> = vec![]; let mut lists: Vec<(u8, usize, usize, usize)> = vec![]; let mut index = 0; @@ -474,5 +474,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { index += 1; } - Ok(None) + None } diff --git a/src/construct/partial_data.rs b/src/construct/partial_data.rs index b36d9f0..a27730c 100644 --- a/src/construct/partial_data.rs +++ b/src/construct/partial_data.rs @@ -10,7 +10,7 @@ use crate::event::{Kind, Name}; use crate::state::{Name as StateName, State}; use crate::subtokenize::Subresult; use crate::tokenizer::Tokenizer; -use alloc::{string::String, vec}; +use alloc::vec; /// At beginning of data. /// @@ -73,7 +73,7 @@ pub fn inside(tokenizer: &mut Tokenizer) -> State { } /// Merge adjacent data events. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { let mut index = 0; // Loop through events and merge adjacent data events. @@ -105,5 +105,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { index += 1; } - Ok(None) + None } diff --git a/src/construct/partial_mdx_expression.rs b/src/construct/partial_mdx_expression.rs index 3ebd0f0..789443e 100644 --- a/src/construct/partial_mdx_expression.rs +++ b/src/construct/partial_mdx_expression.rs @@ -219,7 +219,7 @@ fn parse_expression(tokenizer: &mut Tokenizer, parse: &MdxExpressionParse) -> St }; // Parse and handle what was signaled back. - match parse(&result.value, kind) { + match parse(&result.value, &kind) { MdxSignal::Ok => State::Ok, MdxSignal::Error(message, place) => { let point = place_to_point(&result, place); diff --git a/src/construct/string.rs b/src/construct/string.rs index cf2f222..cad570d 100644 --- a/src/construct/string.rs +++ b/src/construct/string.rs @@ -17,7 +17,6 @@ use crate::resolve::Name as ResolveName; use crate::state::{Name as StateName, State}; use crate::subtokenize::Subresult; use crate::tokenizer::Tokenizer; -use alloc::string::String; /// Characters that can start something in string. const MARKERS: [u8; 2] = [b'&', b'\\']; @@ -76,8 +75,7 @@ pub fn before_data(tokenizer: &mut Tokenizer) -> State { } /// Resolve whitespace in string. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { resolve_whitespace(tokenizer, false, false); - - Ok(None) + None } diff --git a/src/construct/text.rs b/src/construct/text.rs index 2648531..0ea0913 100644 --- a/src/construct/text.rs +++ b/src/construct/text.rs @@ -30,7 +30,6 @@ use crate::resolve::Name as ResolveName; use crate::state::{Name as StateName, State}; use crate::subtokenize::Subresult; use crate::tokenizer::Tokenizer; -use alloc::string::String; /// Characters that can start something in text. const MARKERS: [u8; 16] = [ @@ -244,7 +243,7 @@ pub fn before_data(tokenizer: &mut Tokenizer) -> State { } /// Resolve whitespace. -pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { +pub fn resolve(tokenizer: &mut Tokenizer) -> Option { resolve_whitespace( tokenizer, tokenizer.parse_state.options.constructs.hard_break_trailing, @@ -260,5 +259,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result, String> { resolve_gfm_autolink_literal(tokenizer); } - Ok(None) + None } diff --git a/src/lib.rs b/src/lib.rs index fcdab10..e552327 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ extern crate alloc; mod construct; mod event; -pub mod mdast; +pub mod mdast; // To do: externalize? mod parser; mod resolve; mod state; @@ -25,6 +25,7 @@ mod subtokenize; mod to_html; mod to_mdast; mod tokenizer; +pub mod unist; // To do: externalize. mod util; use alloc::{boxed::Box, fmt, string::String}; @@ -32,6 +33,7 @@ use mdast::Node; use parser::parse; use to_html::compile as to_html; use to_mdast::compile as to_mdast; +use util::sanitize_uri::sanitize; /// Type of line endings in markdown. #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -146,7 +148,7 @@ pub enum MdxExpressionKind { /// Can be passed as `mdx_expression_parse` in [`Options`][] to support /// expressions according to a certain grammar (typically, a programming /// language). -pub type MdxExpressionParse = dyn Fn(&str, MdxExpressionKind) -> MdxSignal; +pub type MdxExpressionParse = dyn Fn(&str, &MdxExpressionKind) -> MdxSignal; /// Signature of a function that parses ESM. /// @@ -1187,3 +1189,9 @@ pub fn micromark_to_mdast(value: &str, options: &Options) -> Result String { + sanitize(value) +} diff --git a/src/mdast.rs b/src/mdast.rs index 79a39dd..8b5b74d 100644 --- a/src/mdast.rs +++ b/src/mdast.rs @@ -1,83 +1,14 @@ -//! [mdast][] syntax tree. +//! markdown syntax tree: [mdast][]. //! //! [mdast]: https://github.com/syntax-tree/mdast +use crate::unist::Position; use alloc::{ fmt, string::{String, ToString}, vec::Vec, }; -/// One place in a source file. -#[derive(Clone, Eq, PartialEq)] -pub struct Point { - /// 1-indexed integer representing a line in a source file. - pub line: usize, - /// 1-indexed integer representing a column in a source file. - pub column: usize, - /// 0-indexed integer representing a character in a source file. - pub offset: usize, -} - -impl Point { - #[must_use] - pub fn new(line: usize, column: usize, offset: usize) -> Point { - Point { - line, - column, - offset, - } - } -} - -impl fmt::Debug for Point { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{} ({})", self.line, self.column, self.offset) - } -} - -/// Location of a node in a source file. -#[derive(Clone, Eq, PartialEq)] -pub struct Position { - /// Represents the place of the first character of the parsed source region. - pub start: Point, - /// Represents the place of the first character after the parsed source - /// region, whether it exists or not. - pub end: Point, -} - -impl Position { - #[must_use] - pub fn new( - start_line: usize, - start_column: usize, - start_offset: usize, - end_line: usize, - end_column: usize, - end_offset: usize, - ) -> Position { - Position { - start: Point::new(start_line, start_column, start_offset), - end: Point::new(end_line, end_column, end_offset), - } - } -} - -impl fmt::Debug for Position { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}:{}-{}:{} ({}-{})", - self.start.line, - self.start.column, - self.end.line, - self.end.column, - self.start.offset, - self.end.offset - ) - } -} - /// Explicitness of a reference. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ReferenceKind { @@ -370,7 +301,8 @@ impl Node { } } - pub fn position(&mut self) -> Option<&Position> { + #[must_use] + pub fn position(&self) -> Option<&Position> { match self { Node::Root(x) => x.position.as_ref(), Node::BlockQuote(x) => x.position.as_ref(), @@ -1204,6 +1136,7 @@ pub struct MdxJsxAttribute { #[cfg(test)] mod tests { use super::*; + use crate::unist::{Point, Position}; use alloc::{string::ToString, vec}; #[test] diff --git a/src/resolve.rs b/src/resolve.rs index 2586676..813ce52 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -64,18 +64,18 @@ pub enum Name { /// Call the corresponding resolver. pub fn call(tokenizer: &mut Tokenizer, name: Name) -> Result, String> { - let func = match name { - Name::Label => construct::label_end::resolve, - Name::Attention => construct::attention::resolve, - Name::GfmTable => construct::gfm_table::resolve, - Name::HeadingAtx => construct::heading_atx::resolve, - Name::HeadingSetext => construct::heading_setext::resolve, - Name::ListItem => construct::list_item::resolve, - Name::Content => construct::content::resolve, - Name::Data => construct::partial_data::resolve, - Name::String => construct::string::resolve, - Name::Text => construct::text::resolve, + let result = match name { + Name::Label => construct::label_end::resolve(tokenizer), + Name::Attention => construct::attention::resolve(tokenizer), + Name::GfmTable => construct::gfm_table::resolve(tokenizer), + Name::HeadingAtx => construct::heading_atx::resolve(tokenizer), + Name::HeadingSetext => construct::heading_setext::resolve(tokenizer), + Name::ListItem => construct::list_item::resolve(tokenizer), + Name::Content => construct::content::resolve(tokenizer)?, + Name::Data => construct::partial_data::resolve(tokenizer), + Name::String => construct::string::resolve(tokenizer), + Name::Text => construct::text::resolve(tokenizer), }; - func(tokenizer) + Ok(result) } diff --git a/src/to_mdast.rs b/src/to_mdast.rs index 9f03a03..42f68a0 100644 --- a/src/to_mdast.rs +++ b/src/to_mdast.rs @@ -5,10 +5,10 @@ use crate::mdast::{ AttributeContent, AttributeValue, BlockQuote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, InlineMath, Link, LinkReference, List, ListItem, Math, MdxFlowExpression, MdxJsxAttribute, - MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph, Point, - Position, ReferenceKind, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml, - Yaml, + MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph, + ReferenceKind, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml, Yaml, }; +use crate::unist::{Point, Position}; use crate::util::{ decode_character_reference::{decode_named, decode_numeric}, infer::{gfm_table_align, list_item_loose, list_loose}, diff --git a/src/unist.rs b/src/unist.rs new file mode 100644 index 0000000..75ef359 --- /dev/null +++ b/src/unist.rs @@ -0,0 +1,75 @@ +//! abstract syntax trees: [unist][]. +//! +//! [unist]: https://github.com/syntax-tree/unist + +use alloc::fmt; + +/// One place in a source file. +#[derive(Clone, Eq, PartialEq)] +pub struct Point { + /// 1-indexed integer representing a line in a source file. + pub line: usize, + /// 1-indexed integer representing a column in a source file. + pub column: usize, + /// 0-indexed integer representing a character in a source file. + pub offset: usize, +} + +impl Point { + #[must_use] + pub fn new(line: usize, column: usize, offset: usize) -> Point { + Point { + line, + column, + offset, + } + } +} + +impl fmt::Debug for Point { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{} ({})", self.line, self.column, self.offset) + } +} + +/// Location of a node in a source file. +#[derive(Clone, Eq, PartialEq)] +pub struct Position { + /// Represents the place of the first character of the parsed source region. + pub start: Point, + /// Represents the place of the first character after the parsed source + /// region, whether it exists or not. + pub end: Point, +} + +impl Position { + #[must_use] + pub fn new( + start_line: usize, + start_column: usize, + start_offset: usize, + end_line: usize, + end_column: usize, + end_offset: usize, + ) -> Position { + Position { + start: Point::new(start_line, start_column, start_offset), + end: Point::new(end_line, end_column, end_offset), + } + } +} + +impl fmt::Debug for Position { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}:{}-{}:{} ({}-{})", + self.start.line, + self.start.column, + self.end.line, + self.end.column, + self.start.offset, + self.end.offset + ) + } +} diff --git a/src/util/sanitize_uri.rs b/src/util/sanitize_uri.rs index 0099347..8e44758 100644 --- a/src/util/sanitize_uri.rs +++ b/src/util/sanitize_uri.rs @@ -26,6 +26,7 @@ use alloc::{ /// ## References /// /// * [`micromark-util-sanitize-uri` in `micromark`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri) +#[must_use] pub fn sanitize(value: &str) -> String { encode(&*normalize(value), true) } diff --git a/tests/attention.rs b/tests/attention.rs index 607af58..abed33c 100644 --- a/tests/attention.rs +++ b/tests/attention.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Emphasis, Node, Paragraph, Position, Root, Strong, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Emphasis, Node, Paragraph, Root, Strong, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/autolink.rs b/tests/autolink.rs index cc30512..78725b2 100644 --- a/tests/autolink.rs +++ b/tests/autolink.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Link, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Link, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/block_quote.rs b/tests/block_quote.rs index 9cd7d46..6b155c5 100644 --- a/tests/block_quote.rs +++ b/tests/block_quote.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{BlockQuote, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{BlockQuote, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/character_escape.rs b/tests/character_escape.rs index e0a3ed3..44eff0b 100644 --- a/tests/character_escape.rs +++ b/tests/character_escape.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/character_reference.rs b/tests/character_reference.rs index 7385734..ccf506e 100644 --- a/tests/character_reference.rs +++ b/tests/character_reference.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/code_fenced.rs b/tests/code_fenced.rs index 2f770ce..06f0d6a 100644 --- a/tests/code_fenced.rs +++ b/tests/code_fenced.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Code, Node, Position, Root}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Code, Node, Root}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/code_indented.rs b/tests/code_indented.rs index 8a15693..7ea08b5 100644 --- a/tests/code_indented.rs +++ b/tests/code_indented.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Code, Node, Position, Root}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Code, Node, Root}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/code_text.rs b/tests/code_text.rs index c1ba861..f6a3379 100644 --- a/tests/code_text.rs +++ b/tests/code_text.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{InlineCode, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{InlineCode, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/definition.rs b/tests/definition.rs index c3bf183..bf9d8ad 100644 --- a/tests/definition.rs +++ b/tests/definition.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Definition, Node, Position, Root}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Definition, Node, Root}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/frontmatter.rs b/tests/frontmatter.rs index c5b0d3a..e9f6648 100644 --- a/tests/frontmatter.rs +++ b/tests/frontmatter.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Node, Position, Root, Toml, Yaml}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Node, Root, Toml, Yaml}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/gfm_autolink_literal.rs b/tests/gfm_autolink_literal.rs index bf99071..d699343 100644 --- a/tests/gfm_autolink_literal.rs +++ b/tests/gfm_autolink_literal.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Link, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Link, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/gfm_footnote.rs b/tests/gfm_footnote.rs index 8785239..364bf90 100644 --- a/tests/gfm_footnote.rs +++ b/tests/gfm_footnote.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/gfm_strikethrough.rs b/tests/gfm_strikethrough.rs index d669a96..e392700 100644 --- a/tests/gfm_strikethrough.rs +++ b/tests/gfm_strikethrough.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Delete, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Delete, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/gfm_table.rs b/tests/gfm_table.rs index 17b31da..d6bd022 100644 --- a/tests/gfm_table.rs +++ b/tests/gfm_table.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{AlignKind, InlineCode, Node, Position, Root, Table, TableCell, TableRow, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{AlignKind, InlineCode, Node, Root, Table, TableCell, TableRow, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/gfm_task_list_item.rs b/tests/gfm_task_list_item.rs index ae6c548..1a0b682 100644 --- a/tests/gfm_task_list_item.rs +++ b/tests/gfm_task_list_item.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{List, ListItem, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{List, ListItem, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/hard_break_escape.rs b/tests/hard_break_escape.rs index ced3b3d..9a984bf 100644 --- a/tests/hard_break_escape.rs +++ b/tests/hard_break_escape.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Break, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Break, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/hard_break_trailing.rs b/tests/hard_break_trailing.rs index 042f8f0..41a320e 100644 --- a/tests/hard_break_trailing.rs +++ b/tests/hard_break_trailing.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Break, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Break, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/heading_atx.rs b/tests/heading_atx.rs index da83ff5..c13ef1a 100644 --- a/tests/heading_atx.rs +++ b/tests/heading_atx.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Heading, Node, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Heading, Node, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/heading_setext.rs b/tests/heading_setext.rs index 4292ed2..8050ed2 100644 --- a/tests/heading_setext.rs +++ b/tests/heading_setext.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Heading, Node, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Heading, Node, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/html_flow.rs b/tests/html_flow.rs index 2605105..2f1d85f 100644 --- a/tests/html_flow.rs +++ b/tests/html_flow.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Html, Node, Position, Root}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Html, Node, Root}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/html_text.rs b/tests/html_text.rs index d35bdba..40e860e 100644 --- a/tests/html_text.rs +++ b/tests/html_text.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Html, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Html, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/image.rs b/tests/image.rs index 40d1449..6669e8d 100644 --- a/tests/image.rs +++ b/tests/image.rs @@ -1,9 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{ - Definition, Image, ImageReference, Node, Paragraph, Position, ReferenceKind, Root, Text, - }, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Definition, Image, ImageReference, Node, Paragraph, ReferenceKind, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/link_reference.rs b/tests/link_reference.rs index d1d6785..680bb1d 100644 --- a/tests/link_reference.rs +++ b/tests/link_reference.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Definition, LinkReference, Node, Paragraph, Position, ReferenceKind, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Definition, LinkReference, Node, Paragraph, ReferenceKind, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/link_resource.rs b/tests/link_resource.rs index a296410..ef79653 100644 --- a/tests/link_resource.rs +++ b/tests/link_resource.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Link, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Options, + mdast::{Link, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Options, }; use pretty_assertions::assert_eq; diff --git a/tests/list.rs b/tests/list.rs index 95beeec..d485d49 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{List, ListItem, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{List, ListItem, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/math_flow.rs b/tests/math_flow.rs index 3797e83..abb1f32 100644 --- a/tests/math_flow.rs +++ b/tests/math_flow.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Math, Node, Position, Root}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Math, Node, Root}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/math_text.rs b/tests/math_text.rs index 9e20d6e..76aa0aa 100644 --- a/tests/math_text.rs +++ b/tests/math_text.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{InlineMath, Node, Paragraph, Position, Root, Text}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{InlineMath, Node, Paragraph, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/mdx_esm.rs b/tests/mdx_esm.rs index 0f8888b..a9c7f4a 100644 --- a/tests/mdx_esm.rs +++ b/tests/mdx_esm.rs @@ -1,11 +1,13 @@ extern crate micromark; mod test_utils; use micromark::{ - mdast::{MdxjsEsm, Node, Position, Root}, - micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{MdxjsEsm, Node, Root}, + micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; -use test_utils::{parse_esm, parse_expression}; +use test_utils::swc::{parse_esm, parse_expression}; #[test] fn mdx_esm() -> Result<(), String> { diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs index 1d50468..b02d32b 100644 --- a/tests/mdx_expression_flow.rs +++ b/tests/mdx_expression_flow.rs @@ -1,11 +1,13 @@ extern crate micromark; mod test_utils; use micromark::{ - mdast::{MdxFlowExpression, Node, Position, Root}, - micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{MdxFlowExpression, Node, Root}, + micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; -use test_utils::{parse_esm, parse_expression}; +use test_utils::swc::{parse_esm, parse_expression}; #[test] fn mdx_expression_flow_agnostic() -> Result<(), String> { diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs index 997e7de..9eb9dbf 100644 --- a/tests/mdx_expression_text.rs +++ b/tests/mdx_expression_text.rs @@ -1,11 +1,13 @@ extern crate micromark; mod test_utils; use micromark::{ - mdast::{MdxTextExpression, Node, Paragraph, Position, Root, Text}, - micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{MdxTextExpression, Node, Paragraph, Root, Text}, + micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; -use test_utils::{parse_esm, parse_expression}; +use test_utils::swc::{parse_esm, parse_expression}; #[test] fn mdx_expression_text_gnostic_core() -> Result<(), String> { diff --git a/tests/mdx_jsx_flow.rs b/tests/mdx_jsx_flow.rs index 14e14f0..54914e6 100644 --- a/tests/mdx_jsx_flow.rs +++ b/tests/mdx_jsx_flow.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{List, ListItem, MdxJsxFlowElement, Node, Paragraph, Position, Root, Text}, - micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{List, ListItem, MdxJsxFlowElement, Node, Paragraph, Root, Text}, + micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs index 94e7b0c..ea3502f 100644 --- a/tests/mdx_jsx_text.rs +++ b/tests/mdx_jsx_text.rs @@ -3,12 +3,14 @@ mod test_utils; use micromark::{ mdast::{ AttributeContent, AttributeValue, Emphasis, MdxJsxAttribute, MdxJsxTextElement, Node, - Paragraph, Position, Root, Text, + Paragraph, Root, Text, }, - micromark_to_mdast, micromark_with_options, Constructs, Options, + micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; -use test_utils::{parse_esm, parse_expression}; +use test_utils::swc::{parse_esm, parse_expression}; #[test] fn mdx_jsx_text_core() -> Result<(), String> { diff --git a/tests/mdx_swc.rs b/tests/mdx_swc.rs index c9a2a61..74f975a 100644 --- a/tests/mdx_swc.rs +++ b/tests/mdx_swc.rs @@ -2,7 +2,7 @@ extern crate micromark; mod test_utils; use micromark::{micromark_with_options, Constructs, Options}; use pretty_assertions::assert_eq; -use test_utils::{parse_esm, parse_expression}; +use test_utils::swc::{parse_esm, parse_expression}; #[test] fn mdx_swc() -> Result<(), String> { diff --git a/tests/misc_bom.rs b/tests/misc_bom.rs index e26b407..47ce902 100644 --- a/tests/misc_bom.rs +++ b/tests/misc_bom.rs @@ -3,7 +3,7 @@ use micromark::micromark; use pretty_assertions::assert_eq; #[test] -fn bom() -> Result<(), String> { +fn bom() { assert_eq!(micromark("\u{FEFF}"), "", "should ignore just a bom"); assert_eq!( @@ -11,6 +11,4 @@ fn bom() -> Result<(), String> { "

hea\u{FEFF}ding

", "should ignore a bom" ); - - Ok(()) } diff --git a/tests/misc_dangerous_protocol.rs b/tests/misc_dangerous_protocol.rs index 88058f2..0c25eba 100644 --- a/tests/misc_dangerous_protocol.rs +++ b/tests/misc_dangerous_protocol.rs @@ -3,7 +3,7 @@ use micromark::micromark; use pretty_assertions::assert_eq; #[test] -fn dangerous_protocol_autolink() -> Result<(), String> { +fn dangerous_protocol_autolink() { assert_eq!( micromark(""), "

javascript:alert(1)

", @@ -33,12 +33,10 @@ fn dangerous_protocol_autolink() -> Result<(), String> { "

mailto:a

", "should allow `mailto:`" ); - - Ok(()) } #[test] -fn dangerous_protocol_image() -> Result<(), String> { +fn dangerous_protocol_image() { assert_eq!( micromark("![](javascript:alert(1))"), "

\"\"

", @@ -116,12 +114,10 @@ fn dangerous_protocol_image() -> Result<(), String> { "

\"\"

", "should allow a colon in a path" ); - - Ok(()) } #[test] -fn dangerous_protocol_link() -> Result<(), String> { +fn dangerous_protocol_link() { assert_eq!( micromark("[](javascript:alert(1))"), "

", @@ -199,6 +195,4 @@ fn dangerous_protocol_link() -> Result<(), String> { "

", "should allow a colon in a path" ); - - Ok(()) } diff --git a/tests/misc_soft_break.rs b/tests/misc_soft_break.rs index 746b41d..43e2f3d 100644 --- a/tests/misc_soft_break.rs +++ b/tests/misc_soft_break.rs @@ -3,7 +3,7 @@ use micromark::micromark; use pretty_assertions::assert_eq; #[test] -fn soft_break() -> Result<(), String> { +fn soft_break() { assert_eq!( micromark("foo\nbaz"), "

foo\nbaz

", @@ -15,6 +15,4 @@ fn soft_break() -> Result<(), String> { "

foo\nbaz

", "should trim spaces around line endings" ); - - Ok(()) } diff --git a/tests/misc_tabs.rs b/tests/misc_tabs.rs index feb8177..5cd9f69 100644 --- a/tests/misc_tabs.rs +++ b/tests/misc_tabs.rs @@ -133,7 +133,7 @@ fn tabs_flow() -> Result<(), String> { } #[test] -fn tabs_text() -> Result<(), String> { +fn tabs_text() { assert_eq!( micromark(""), "

<http:\t>

", @@ -251,12 +251,10 @@ fn tabs_text() -> Result<(), String> { "

x

", "should support a tab between a link destination and title" ); - - Ok(()) } #[test] -fn tabs_virtual_spaces() -> Result<(), String> { +fn tabs_virtual_spaces() { assert_eq!( micromark("```\n\tx"), "
\tx\n
\n", @@ -288,6 +286,4 @@ fn tabs_virtual_spaces() -> Result<(), String> { // "
    \n
  • \n

    a

    \n

    b

    \n
  • \n
", "should support a part of a tab as a container, and the rest of a tab as flow" ); - - Ok(()) } diff --git a/tests/misc_url.rs b/tests/misc_url.rs index fd9ae05..4fff26d 100644 --- a/tests/misc_url.rs +++ b/tests/misc_url.rs @@ -3,7 +3,7 @@ use micromark::micromark; use pretty_assertions::assert_eq; #[test] -fn url() -> Result<(), String> { +fn url() { assert_eq!( micromark(""), "

https://%

", @@ -145,6 +145,4 @@ fn url() -> Result<(), String> { format!("

", ascii_out), "should support ascii characters" ); - - Ok(()) } diff --git a/tests/misc_zero.rs b/tests/misc_zero.rs index f8d0c56..0b54d50 100644 --- a/tests/misc_zero.rs +++ b/tests/misc_zero.rs @@ -3,7 +3,7 @@ use micromark::micromark; use pretty_assertions::assert_eq; #[test] -fn zero() -> Result<(), String> { +fn zero() { assert_eq!(micromark(""), "", "should support no markdown"); assert_eq!( @@ -25,6 +25,4 @@ fn zero() -> Result<(), String> { "

\\0

", "should not support NUL in a character escape" ); - - Ok(()) } diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs new file mode 100644 index 0000000..4adf0ca --- /dev/null +++ b/tests/test_utils/hast.rs @@ -0,0 +1,279 @@ +#![allow(dead_code)] + +// ^-- fix later + +extern crate alloc; +extern crate micromark; +use alloc::{ + fmt, + string::{String, ToString}, + vec::Vec, +}; +use micromark::{mdast::AttributeContent, unist::Position}; + +/// Nodes. +#[derive(Clone, PartialEq)] +pub enum Node { + /// Root. + Root(Root), + /// Element. + Element(Element), + /// Document type. + Doctype(Doctype), + /// Comment. + Comment(Comment), + /// Text. + Text(Text), + + // MDX being passed through. + /// MDX: JSX element. + MdxJsxElement(MdxJsxElement), + /// MDX.js ESM. + MdxjsEsm(MdxjsEsm), + // MDX: expression. + MdxExpression(MdxExpression), +} + +impl fmt::Debug for Node { + // Debug the wrapped struct. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Node::Root(x) => write!(f, "{:?}", x), + Node::Element(x) => write!(f, "{:?}", x), + Node::Doctype(x) => write!(f, "{:?}", x), + Node::Comment(x) => write!(f, "{:?}", x), + Node::Text(x) => write!(f, "{:?}", x), + Node::MdxJsxElement(x) => write!(f, "{:?}", x), + Node::MdxExpression(x) => write!(f, "{:?}", x), + Node::MdxjsEsm(x) => write!(f, "{:?}", x), + } + } +} + +fn children_to_string(children: &[Node]) -> String { + children.iter().map(ToString::to_string).collect() +} + +impl ToString for Node { + fn to_string(&self) -> String { + match self { + // Parents. + Node::Root(x) => children_to_string(&x.children), + Node::Element(x) => children_to_string(&x.children), + Node::MdxJsxElement(x) => children_to_string(&x.children), + // Literals. + Node::Comment(x) => x.value.clone(), + Node::Text(x) => x.value.clone(), + Node::MdxExpression(x) => x.value.clone(), + Node::MdxjsEsm(x) => x.value.clone(), + // Voids. + Node::Doctype(_) => "".to_string(), + } + } +} + +impl Node { + #[must_use] + pub fn children(&self) -> Option<&Vec> { + match self { + // Parent. + Node::Root(x) => Some(&x.children), + Node::Element(x) => Some(&x.children), + Node::MdxJsxElement(x) => Some(&x.children), + // Non-parent. + _ => None, + } + } + + pub fn children_mut(&mut self) -> Option<&mut Vec> { + match self { + // Parent. + Node::Root(x) => Some(&mut x.children), + Node::Element(x) => Some(&mut x.children), + Node::MdxJsxElement(x) => Some(&mut x.children), + // Non-parent. + _ => None, + } + } + + pub fn position(&self) -> Option<&Position> { + match self { + Node::Root(x) => x.position.as_ref(), + Node::Element(x) => x.position.as_ref(), + Node::Doctype(x) => x.position.as_ref(), + Node::Comment(x) => x.position.as_ref(), + Node::Text(x) => x.position.as_ref(), + Node::MdxJsxElement(x) => x.position.as_ref(), + Node::MdxExpression(x) => x.position.as_ref(), + Node::MdxjsEsm(x) => x.position.as_ref(), + } + } + + pub fn position_mut(&mut self) -> Option<&mut Position> { + match self { + Node::Root(x) => x.position.as_mut(), + Node::Element(x) => x.position.as_mut(), + Node::Doctype(x) => x.position.as_mut(), + Node::Comment(x) => x.position.as_mut(), + Node::Text(x) => x.position.as_mut(), + Node::MdxJsxElement(x) => x.position.as_mut(), + Node::MdxExpression(x) => x.position.as_mut(), + Node::MdxjsEsm(x) => x.position.as_mut(), + } + } + + pub fn position_set(&mut self, position: Option) { + match self { + Node::Root(x) => x.position = position, + Node::Element(x) => x.position = position, + Node::Doctype(x) => x.position = position, + Node::Comment(x) => x.position = position, + Node::Text(x) => x.position = position, + Node::MdxJsxElement(x) => x.position = position, + Node::MdxExpression(x) => x.position = position, + Node::MdxjsEsm(x) => x.position = position, + } + } +} + +/// Document. +/// +/// ```html +/// > | a +/// ^ +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Root { + // Parent. + /// Content model. + pub children: Vec, + /// Positional info. + pub position: Option, +} + +/// Document type. +/// +/// ```html +/// > | +/// ^^^^^^^^^^^^^^^ +/// ``` +// To do: clone. +#[derive(Clone, Debug, PartialEq)] +pub struct Element { + pub tag_name: String, + pub properties: Vec<(String, PropertyValue)>, + // Parent. + pub children: Vec, + /// Positional info. + pub position: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PropertyItem { + Number(f32), + String(String), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PropertyValue { + Number(f32), + Boolean(bool), + String(String), + CommaSeparated(Vec), + SpaceSeparated(Vec), +} + +/// Document type. +/// +/// ```html +/// > | +/// ^^^^^^^^^^^^^^^ +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Doctype { + // Void. + /// Positional info. + pub position: Option, +} + +/// Comment. +/// +/// ```html +/// > | +/// ^^^^^^^^^^ +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Comment { + // Text. + /// Content model. + pub value: String, + /// Positional info. + pub position: Option, +} + +/// Text. +/// +/// ```html +/// > | a +/// ^ +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Text { + // Text. + /// Content model. + pub value: String, + /// Positional info. + pub position: Option, +} + +/// MDX: JSX element. +/// +/// ```markdown +/// > | +/// ^^^^^ +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct MdxJsxElement { + // Parent. + /// Content model. + pub children: Vec, + /// Positional info. + pub position: Option, + // JSX element. + /// Name. + /// + /// Fragments have no name. + pub name: Option, + /// Attributes. + pub attributes: Vec, +} + +/// MDX: expression. +/// +/// ```markdown +/// > | {a} +/// ^^^ +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MdxExpression { + // Literal. + /// Content model. + pub value: String, + /// Positional info. + pub position: Option, +} + +/// MDX: ESM. +/// +/// ```markdown +/// > | import a from 'b' +/// ^^^^^^^^^^^^^^^^^ +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MdxjsEsm { + // Literal. + /// Content model. + pub value: String, + /// Positional info. + pub position: Option, +} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 10b9643..111118f 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,245 +1,3 @@ -extern crate micromark; -extern crate swc_common; -extern crate swc_ecma_ast; -extern crate swc_ecma_parser; -use micromark::{MdxExpressionKind, MdxSignal}; -use swc_common::{source_map::Pos, BytePos, FileName, SourceFile, Spanned}; -use swc_ecma_ast::{EsVersion, Expr, Module}; -use swc_ecma_parser::{ - error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, -}; - -/// Parse ESM in MDX with SWC. -pub fn parse_esm(value: &str) -> MdxSignal { - 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); - - match result { - Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"), - 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") - } - } - } -} - -/// Parse expressions in MDX with SWC. -pub fn parse_expression(value: &str, kind: MdxExpressionKind) -> MdxSignal { - // Empty expressions are OK. - if matches!(kind, MdxExpressionKind::Expression) - && matches!(whitespace_and_comments(0, value), MdxSignal::Ok) - { - return MdxSignal::Ok; - } - - // For attribute expression, a spread is needed, for which we have to prefix - // and suffix the input. - // See `check_expression_ast` for how the AST is verified. - let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) { - ("({", "})") - } else { - ("", "") - }; - - let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); - let mut errors = vec![]; - let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); - - match result { - Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"), - Ok(tree) => { - if errors.is_empty() { - let place = 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) - } 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") - } - } - } -} - -/// Check that the resulting AST of ESM is OK. -/// -/// This checks that only module declarations (import/exports) are used, not -/// statements. -fn check_esm_ast(tree: Module) -> MdxSignal { - let mut index = 0; - while index < tree.body.len() { - let node = &tree.body[index]; - - if !node.is_module_decl() { - let place = fix_swc_position(node.span().hi.to_usize(), 0); - return MdxSignal::Error( - "Unexpected statement in code: only import/exports are supported".to_string(), - place, - ); - } - - index += 1; - } - - MdxSignal::Ok -} - -/// Check that the resulting AST of an expressions is OK. -/// -/// This checks that attribute expressions are the expected spread. -fn check_expression_ast(tree: Box, kind: MdxExpressionKind) -> MdxSignal { - if matches!(kind, MdxExpressionKind::AttributeExpression) - && tree - .unwrap_parens() - .as_object() - .and_then(|object| { - if object.props.len() == 1 { - object.props[0].as_spread() - } else { - None - } - }) - .is_none() - { - MdxSignal::Error( - "Expected a single spread value, such as `...x`".to_string(), - 0, - ) - } else { - MdxSignal::Ok - } -} - -/// Turn an SWC error into an `MdxSignal`. -/// -/// * If the error happens at `value_len`, yields `MdxSignal::Eof` -/// * Else, yields `MdxSignal::Error`. -fn swc_error_to_signal( - error: &SwcError, - value_len: usize, - prefix_len: usize, - name: &str, -) -> MdxSignal { - let message = error.kind().msg().to_string(); - let place = fix_swc_position(error.span().hi.to_usize(), prefix_len); - let message = format!("Could not parse {} with swc: {}", name, message); - - if place >= value_len { - MdxSignal::Eof(message) - } else { - MdxSignal::Error(message, place) - } -} - -/// Move past JavaScript whitespace (well, actually ASCII whitespace) and -/// comments. -/// -/// This is needed because for expressions, we use an API that parses up to -/// a valid expression, but there may be more expressions after it, which we -/// don’t alow. -fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal { - let bytes = value.as_bytes(); - let len = bytes.len(); - let mut in_multiline = false; - let mut in_line = false; - - while index < len { - // In a multiline comment: `/* a */`. - if in_multiline { - if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' { - index += 1; - in_multiline = false; - } - } - // In a line comment: `// a`. - else if in_line { - if index + 1 < len && bytes[index] == b'\r' && bytes[index + 1] == b'\n' { - index += 1; - in_line = false; - } else if bytes[index] == b'\r' || bytes[index] == b'\n' { - in_line = false; - } - } - // Not in a comment, opening a multiline comment: `/* a */`. - else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' { - index += 1; - in_multiline = true; - } - // Not in a comment, opening a line comment: `// a`. - else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' { - index += 1; - in_line = true; - } - // Outside comment, whitespace. - else if bytes[index].is_ascii_whitespace() { - // Fine! - } - // Outside comment, not whitespace. - else { - return MdxSignal::Error( - "Could not parse expression with swc: Unexpected content after expression" - .to_string(), - index, - ); - } - - index += 1; - } - - if in_multiline { - MdxSignal::Error( - "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".to_string(), - index, - ) - } else if in_line { - // EOF instead of EOL is specifically not allowed, because that would - // mean the closing brace is on the commented-out line - MdxSignal::Error( - "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".to_string(), - index, - ) - } else { - MdxSignal::Ok - } -} - -/// Create configuration for SWC, shared between ESM and expressions. -/// -/// This enables modern JavaScript (ES2022) + JSX. -fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) { - ( - // File. - SourceFile::new( - FileName::Anon, - false, - FileName::Anon, - source, - BytePos::from_usize(1), - ), - // Syntax. - Syntax::Es(EsConfig { - jsx: true, - ..EsConfig::default() - }), - // Version. - EsVersion::Es2022, - ) -} - -/// Turn an SWC byte position from a resulting AST to an offset in the original -/// input string. -fn fix_swc_position(index: usize, prefix_len: usize) -> usize { - index - 1 - prefix_len -} +pub mod hast; +pub mod swc; +pub mod to_hast; diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs new file mode 100644 index 0000000..f455674 --- /dev/null +++ b/tests/test_utils/swc.rs @@ -0,0 +1,247 @@ +extern crate micromark; +extern crate swc_common; +extern crate swc_ecma_ast; +extern crate swc_ecma_parser; +use micromark::{MdxExpressionKind, MdxSignal}; +use swc_common::{source_map::Pos, BytePos, FileName, SourceFile, Spanned}; +use swc_ecma_ast::{EsVersion, Expr, Module}; +use swc_ecma_parser::{ + error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, +}; + +/// Parse ESM in MDX with SWC. +#[allow(dead_code)] +pub fn parse_esm(value: &str) -> MdxSignal { + 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); + + match result { + Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"), + 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") + } + } + } +} + +/// Parse expressions in MDX with SWC. +#[allow(dead_code)] +pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { + // Empty expressions are OK. + if matches!(kind, MdxExpressionKind::Expression) + && matches!(whitespace_and_comments(0, value), MdxSignal::Ok) + { + return MdxSignal::Ok; + } + + // For attribute expression, a spread is needed, for which we have to prefix + // and suffix the input. + // See `check_expression_ast` for how the AST is verified. + let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) { + ("({", "})") + } else { + ("", "") + }; + + let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); + let mut errors = vec![]; + let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); + + match result { + Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"), + Ok(tree) => { + if errors.is_empty() { + let place = 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) + } 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") + } + } + } +} + +/// Check that the resulting AST of ESM is OK. +/// +/// This checks that only module declarations (import/exports) are used, not +/// statements. +fn check_esm_ast(tree: &Module) -> MdxSignal { + let mut index = 0; + while index < tree.body.len() { + let node = &tree.body[index]; + + if !node.is_module_decl() { + let place = fix_swc_position(node.span().hi.to_usize(), 0); + return MdxSignal::Error( + "Unexpected statement in code: only import/exports are supported".to_string(), + place, + ); + } + + index += 1; + } + + MdxSignal::Ok +} + +/// Check that the resulting AST of an expressions is OK. +/// +/// This checks that attribute expressions are the expected spread. +fn check_expression_ast(tree: &Expr, kind: &MdxExpressionKind) -> MdxSignal { + if matches!(kind, MdxExpressionKind::AttributeExpression) + && tree + .unwrap_parens() + .as_object() + .and_then(|object| { + if object.props.len() == 1 { + object.props[0].as_spread() + } else { + None + } + }) + .is_none() + { + MdxSignal::Error( + "Expected a single spread value, such as `...x`".to_string(), + 0, + ) + } else { + MdxSignal::Ok + } +} + +/// Turn an SWC error into an `MdxSignal`. +/// +/// * If the error happens at `value_len`, yields `MdxSignal::Eof` +/// * Else, yields `MdxSignal::Error`. +fn swc_error_to_signal( + error: &SwcError, + value_len: usize, + prefix_len: usize, + name: &str, +) -> MdxSignal { + let message = error.kind().msg().to_string(); + let place = fix_swc_position(error.span().hi.to_usize(), prefix_len); + let message = format!("Could not parse {} with swc: {}", name, message); + + if place >= value_len { + MdxSignal::Eof(message) + } else { + MdxSignal::Error(message, place) + } +} + +/// Move past JavaScript whitespace (well, actually ASCII whitespace) and +/// comments. +/// +/// This is needed because for expressions, we use an API that parses up to +/// a valid expression, but there may be more expressions after it, which we +/// don’t alow. +fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal { + let bytes = value.as_bytes(); + let len = bytes.len(); + let mut in_multiline = false; + let mut in_line = false; + + while index < len { + // In a multiline comment: `/* a */`. + if in_multiline { + if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' { + index += 1; + in_multiline = false; + } + } + // In a line comment: `// a`. + else if in_line { + if index + 1 < len && bytes[index] == b'\r' && bytes[index + 1] == b'\n' { + index += 1; + in_line = false; + } else if bytes[index] == b'\r' || bytes[index] == b'\n' { + in_line = false; + } + } + // Not in a comment, opening a multiline comment: `/* a */`. + else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' { + index += 1; + in_multiline = true; + } + // Not in a comment, opening a line comment: `// a`. + else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' { + index += 1; + in_line = true; + } + // Outside comment, whitespace. + else if bytes[index].is_ascii_whitespace() { + // Fine! + } + // Outside comment, not whitespace. + else { + return MdxSignal::Error( + "Could not parse expression with swc: Unexpected content after expression" + .to_string(), + index, + ); + } + + index += 1; + } + + if in_multiline { + MdxSignal::Error( + "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".to_string(), + index, + ) + } else if in_line { + // EOF instead of EOL is specifically not allowed, because that would + // mean the closing brace is on the commented-out line + MdxSignal::Error( + "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".to_string(), + index, + ) + } else { + MdxSignal::Ok + } +} + +/// Create configuration for SWC, shared between ESM and expressions. +/// +/// This enables modern JavaScript (ES2022) + JSX. +fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) { + ( + // File. + SourceFile::new( + FileName::Anon, + false, + FileName::Anon, + source, + BytePos::from_usize(1), + ), + // Syntax. + Syntax::Es(EsConfig { + jsx: true, + ..EsConfig::default() + }), + // Version. + EsVersion::Es2022, + ) +} + +/// Turn an SWC byte position from a resulting AST to an offset in the original +/// input string. +fn fix_swc_position(index: usize, prefix_len: usize) -> usize { + index - 1 - prefix_len +} diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs new file mode 100644 index 0000000..821931c --- /dev/null +++ b/tests/test_utils/to_hast.rs @@ -0,0 +1,1457 @@ +use crate::test_utils::hast; +use micromark::{mdast, sanitize_, unist::Position}; + +// Options? +// - dangerous: raw? No +// - clobberPrefix / footnoteLabel / footnoteLabelTagName / footnoteLabelProperties / footnoteBacklabel? Later + +#[derive(Debug)] +struct State { + definitions: Vec<(String, String, Option)>, + footnote_definitions: Vec<(String, Vec)>, + footnote_calls: Vec<(String, usize)>, +} + +#[derive(Debug)] +enum Result { + Fragment(Vec), + Node(hast::Node), + None, +} + +#[allow(dead_code)] +pub fn to_hast(mdast: &mdast::Node) -> hast::Node { + let mut definitions = vec![]; + + // Collect definitions. + // Calls take info from their definition. + // Calls can come come before definitions. + // Footnote calls can also come before footnote definitions, but those + // calls *do not* take info from their definitions, so we don’t care + // about footnotes here. + visit(mdast, |node| { + if let mdast::Node::Definition(definition) = node { + definitions.push(( + definition.identifier.clone(), + definition.url.clone(), + definition.title.clone(), + )); + } + }); + + // - footnoteById: Record + // - footnoteOrder: Vec + // - footnoteCounts: Record + + let (result, mut state) = one( + mdast, + None, + State { + definitions, + footnote_definitions: vec![], + footnote_calls: vec![], + }, + ); + + if state.footnote_calls.is_empty() { + if let Result::Node(node) = result { + return node; + } + } + + // We either have to generate a footer, or we don’t have a single node. + // So we need a root. + let mut root = hast::Root { + children: vec![], + position: None, + }; + + match result { + Result::Fragment(children) => root.children = children, + Result::Node(node) => { + if let hast::Node::Root(existing) = node { + root = existing; + } else { + root.children.push(node); + } + } + Result::None => {} + } + + if !state.footnote_calls.is_empty() { + let mut items = vec![]; + + let mut index = 0; + while index < state.footnote_calls.len() { + let (id, count) = &state.footnote_calls[index]; + let safe_id = sanitize_(&id.to_lowercase()); + + // Find definition: we’ll always find it. + let mut definition_index = 0; + while definition_index < state.footnote_definitions.len() { + if &state.footnote_definitions[definition_index].0 == id { + break; + } + definition_index += 1; + } + debug_assert_ne!( + definition_index, + state.footnote_definitions.len(), + "expected definition" + ); + + // We’ll find each used definition once, so we can split off to take the content. + let mut content = state.footnote_definitions[definition_index].1.split_off(0); + + let mut reference_index = 0; + let mut backreferences = vec![]; + while reference_index < *count { + let mut backref_children = vec![hast::Node::Text(hast::Text { + value: "↩".into(), + position: None, + })]; + + if reference_index != 0 { + backreferences.push(hast::Node::Text(hast::Text { + value: " ".into(), + position: None, + })); + + backref_children.push(hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: (reference_index + 1).to_string(), + position: None, + })], + position: None, + })); + } + + backreferences.push(hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String(format!( + "#fnref-{}{}", + safe_id, + if reference_index == 0 { + "".into() + } else { + format!("-{}", &(reference_index + 1).to_string()) + } + )), + ), + ( + "dataFootnoteBackref".into(), + hast::PropertyValue::Boolean(true), + ), + ( + "ariaLabel".into(), + hast::PropertyValue::String("Back to content".into()), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "data-footnote-backref".into(), + )]), + ), + ], + children: backref_children, + position: None, + })); + + reference_index += 1; + } + + let mut backreference_opt = Some(backreferences); + + if let Some(hast::Node::Element(tail_element)) = content.last_mut() { + if tail_element.tag_name == "p" { + if let Some(hast::Node::Text(text)) = tail_element.children.last_mut() { + text.value.push(' '); + } else { + tail_element.children.push(hast::Node::Text(hast::Text { + value: " ".into(), + position: None, + })); + } + + tail_element + .children + .append(&mut backreference_opt.take().unwrap()); + } + } + + // No paragraph, just push them. + if let Some(mut backreference) = backreference_opt { + content.append(&mut backreference); + } + + items.push(hast::Node::Element(hast::Element { + tag_name: "li".into(), + // To do: support clobber prefix. + properties: vec![( + "id".into(), + hast::PropertyValue::String(format!("#fn-{}", safe_id)), + )], + children: wrap(content, true), + position: None, + })); + index += 1; + } + + root.children.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + root.children.push(hast::Node::Element(hast::Element { + tag_name: "section".into(), + properties: vec![ + ("dataFootnotes".into(), hast::PropertyValue::Boolean(true)), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "footnotes".into(), + )]), + ), + ], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "h2".into(), + properties: vec![ + ( + "id".into(), + hast::PropertyValue::String("footnote-label".into()), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "sr-only".into(), + )]), + ), + ], + children: vec![hast::Node::Text(hast::Text { + value: "Footnotes".into(), + position: None, + })], + position: None, + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "ol".into(), + properties: vec![], + children: wrap(items, true), + position: None, + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + ], + position: None, + })); + root.children.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + hast::Node::Root(root) +} + +fn one(node: &mdast::Node, parent: Option<&mdast::Node>, state: State) -> (Result, State) { + match node { + mdast::Node::BlockQuote(_) => transform_block_quote(node, state), + mdast::Node::Break(_) => transform_break(node, state), + mdast::Node::Code(_) => transform_code(node, state), + mdast::Node::Delete(_) => transform_delete(node, state), + mdast::Node::Emphasis(_) => transform_emphasis(node, state), + mdast::Node::FootnoteDefinition(_) => transform_footnote_definition(node, state), + mdast::Node::FootnoteReference(_) => transform_footnote_reference(node, state), + mdast::Node::Heading(_) => transform_heading(node, state), + mdast::Node::Image(_) => transform_image(node, state), + mdast::Node::ImageReference(_) => transform_image_reference(node, state), + mdast::Node::InlineCode(_) => transform_inline_code(node, state), + mdast::Node::InlineMath(_) => transform_inline_math(node, state), + mdast::Node::Link(_) => transform_link(node, state), + mdast::Node::LinkReference(_) => transform_link_reference(node, state), + mdast::Node::ListItem(_) => transform_list_item(node, parent, state), + mdast::Node::List(_) => transform_list(node, state), + mdast::Node::Math(_) => transform_math(node, state), + mdast::Node::MdxFlowExpression(_) | mdast::Node::MdxTextExpression(_) => { + transform_mdx_expression(node, state) + } + mdast::Node::MdxJsxFlowElement(_) | mdast::Node::MdxJsxTextElement(_) => { + transform_mdx_jsx_element(node, state) + } + mdast::Node::MdxjsEsm(_) => transform_mdxjs_esm(node, state), + mdast::Node::Paragraph(_) => transform_paragraph(node, state), + mdast::Node::Root(_) => transform_root(node, state), + mdast::Node::Strong(_) => transform_strong(node, state), + // Note: this is only called here if there is a single cell passed, not when one is found in a table. + mdast::Node::TableCell(_) => { + transform_table_cell(node, false, mdast::AlignKind::None, state) + } + // Note: this is only called here if there is a single row passed, not when one is found in a table. + mdast::Node::TableRow(_) => transform_table_row(node, false, None, state), + mdast::Node::Table(_) => transform_table(node, state), + mdast::Node::Text(_) => transform_text(node, state), + mdast::Node::ThematicBreak(_) => transform_thematic_break(node, state), + // Ignore. + // Idea: support `Raw` nodes for HTML, optionally? + mdast::Node::Definition(_) + | mdast::Node::Html(_) + | mdast::Node::Yaml(_) + | mdast::Node::Toml(_) => (Result::None, state), + } +} + +/// [`BlockQuote`][mdast::BlockQuote]. +fn transform_block_quote(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "blockquote".into(), + properties: vec![], + children: wrap(children, true), + position: None, + }), + )), + state, + ) +} + +/// [`Break`][mdast::Break]. +fn transform_break(node: &mdast::Node, state: State) -> (Result, State) { + ( + Result::Fragment(vec![ + augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "br".into(), + properties: vec![], + children: vec![], + position: None, + }), + ), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + ]), + state, + ) +} + +/// [`Code`][mdast::Code]. +fn transform_code(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::Code(code) = node { + let mut value = code.value.clone(); + value.push('\n'); + let mut properties = vec![]; + + if let Some(lang) = code.lang.as_ref() { + let mut value = "language-".to_string(); + value.push_str(lang); + let value = hast::PropertyItem::String(value); + properties.push(( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![value]), + )); + } + + // To do: option to persist `meta`? + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "pre".into(), + properties: vec![], + children: vec![augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties, + children: vec![hast::Node::Text(hast::Text { + value, + position: None, + })], + position: None, + }), + )], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `Code`") + } +} + +/// [`Delete`][mdast::Delete]. +fn transform_delete(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "del".into(), + properties: vec![], + children, + position: None, + }), + )), + state, + ) +} + +/// [`Emphasis`][mdast::Emphasis]. +fn transform_emphasis(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "em".into(), + properties: vec![], + children, + position: None, + }), + )), + state, + ) +} + +/// [`FootnoteDefinition`][mdast::FootnoteDefinition]. +fn transform_footnote_definition(node: &mdast::Node, mut state: State) -> (Result, State) { + if let mdast::Node::FootnoteDefinition(definition) = node { + let result = all(node, state); + let children = result.0; + state = result.1; + // Set aside. + state + .footnote_definitions + .push((definition.identifier.clone(), children)); + (Result::None, state) + } else { + unreachable!("expected `FootnoteDefinition`") + } +} + +/// [`FootnoteReference`][mdast::FootnoteReference]. +fn transform_footnote_reference(node: &mdast::Node, mut state: State) -> (Result, State) { + if let mdast::Node::FootnoteReference(reference) = node { + let safe_id = sanitize_(&reference.identifier.to_lowercase()); + let mut call_index = 0; + + // See if this has been called before. + while call_index < state.footnote_calls.len() { + if state.footnote_calls[call_index].0 == reference.identifier { + break; + } + call_index += 1; + } + + // New. + if call_index == state.footnote_calls.len() { + state.footnote_calls.push((reference.identifier.clone(), 0)); + } + + // Increment. + state.footnote_calls[call_index].1 += 1; + + let reuse_counter = state.footnote_calls[call_index].1; + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "a".into(), + // To do: support clobber prefix. + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String(format!("#fn-{}", safe_id)), + ), + ( + "id".into(), + hast::PropertyValue::String(format!( + "fnref-{}{}", + safe_id, + if reuse_counter > 1 { + format!("-{}", reuse_counter) + } else { + "".into() + } + )), + ), + ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true)), + ( + "ariaDescribedBy".into(), + hast::PropertyValue::String("footnote-label".into()), + ), + ], + children: vec![hast::Node::Text(hast::Text { + value: (call_index + 1).to_string(), + position: None, + })], + position: None, + })], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `FootnoteReference`") + } +} + +/// [`Heading`][mdast::Heading]. +fn transform_heading(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::Heading(heading) = node { + let (children, state) = all(node, state); + let tag_name = format!("h{}", heading.depth); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name, + properties: vec![], + children, + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `Heading`") + } +} + +/// [`Image`][mdast::Image]. +fn transform_image(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::Image(image) = node { + let mut properties = vec![]; + + properties.push(( + "src".into(), + hast::PropertyValue::String(sanitize_(&image.url)), + )); + + properties.push(("alt".into(), hast::PropertyValue::String(image.alt.clone()))); + + if let Some(value) = image.title.as_ref() { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "img".into(), + properties, + children: vec![], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `Image`") + } +} + +/// [`ImageReference`][mdast::ImageReference]. +fn transform_image_reference(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::ImageReference(reference) = node { + let mut properties = vec![]; + + let definition = state + .definitions + .iter() + .find(|d| d.0 == reference.identifier); + + // To do: revert when undefined? + let (_, url, title) = + definition.expect("expected reference to have a corresponding definition"); + + properties.push(("src".into(), hast::PropertyValue::String(sanitize_(url)))); + + properties.push(( + "alt".into(), + hast::PropertyValue::String(reference.alt.clone()), + )); + + if let Some(value) = title { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "img".into(), + properties, + children: vec![], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `ImageReference`") + } +} + +/// [`InlineCode`][mdast::InlineCode]. +fn transform_inline_code(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::InlineCode(code) = node { + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: replace_eols_with_spaces(&code.value), + position: None, + })], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `InlineCode`") + } +} + +/// [`InlineMath`][mdast::InlineMath]. +fn transform_inline_math(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::InlineMath(math) = node { + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + hast::PropertyItem::String("language-math".into()), + hast::PropertyItem::String("math-inline".into()), + ]), + )], + children: vec![hast::Node::Text(hast::Text { + value: replace_eols_with_spaces(&math.value), + position: None, + })], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `InlineMath`") + } +} + +/// [`Link`][mdast::Link]. +fn transform_link(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::Link(link) = node { + let mut properties = vec![]; + + properties.push(( + "href".into(), + hast::PropertyValue::String(sanitize_(&link.url)), + )); + + if let Some(value) = link.title.as_ref() { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties, + children, + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `Link`") + } +} + +/// [`LinkReference`][mdast::LinkReference]. +fn transform_link_reference(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::LinkReference(reference) = node { + let mut properties = vec![]; + + let definition = state + .definitions + .iter() + .find(|d| d.0 == reference.identifier); + + // To do: revert when undefined? + let (_, url, title) = + definition.expect("expected reference to have a corresponding definition"); + + properties.push(("href".into(), hast::PropertyValue::String(sanitize_(url)))); + + if let Some(value) = title { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties, + children, + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `LinkReference`") + } +} + +/// [`ListItem`][mdast::ListItem]. +fn transform_list_item( + node: &mdast::Node, + parent: Option<&mdast::Node>, + state: State, +) -> (Result, State) { + if let mdast::Node::ListItem(item) = node { + let (mut children, state) = all(node, state); + let mut loose = list_item_loose(node); + + if let Some(parent) = parent { + if matches!(parent, mdast::Node::List(_)) { + loose = list_loose(parent); + } + }; + + let mut properties = vec![]; + + // Inject a checkbox. + if let Some(checked) = item.checked { + // According to github-markdown-css, this class hides bullet. + // See: . + properties.push(( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "task-list-item".into(), + )]), + )); + + let mut input = Some(hast::Node::Element(hast::Element { + tag_name: "input".into(), + properties: vec![ + ( + "type".into(), + hast::PropertyValue::String("checkbox".into()), + ), + ("checked".into(), hast::PropertyValue::Boolean(checked)), + ("disabled".into(), hast::PropertyValue::Boolean(true)), + ], + children: vec![], + position: None, + })); + + if let Some(hast::Node::Element(x)) = children.first_mut() { + if x.tag_name == "p" { + if !x.children.is_empty() { + x.children.insert( + 0, + hast::Node::Text(hast::Text { + value: " ".into(), + position: None, + }), + ); + } + + x.children.insert(0, input.take().unwrap()); + } + } + + // If the input wasn‘t injected yet, inject a paragraph. + if let Some(input) = input { + children.insert( + 0, + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![input], + position: None, + }), + ); + } + } + + children.reverse(); + let mut result = vec![]; + let mut head = true; + let empty = children.is_empty(); + let mut tail_p = false; + + while let Some(child) = children.pop() { + let mut is_p = false; + if let hast::Node::Element(el) = &child { + if el.tag_name == "p" { + is_p = true; + } + } + + // Add eols before nodes, except if this is a tight, first paragraph. + if loose || !head || !is_p { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + if is_p && !loose { + // Unwrap the paragraph. + if let hast::Node::Element(mut el) = child { + result.append(&mut el.children); + } + } else { + result.push(child); + } + + head = false; + tail_p = is_p; + } + + // Add eol after last node, except if it is tight or a paragraph. + if !empty && (loose || !tail_p) { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties, + children: result, + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `ListItem`") + } +} + +/// [`List`][mdast::List]. +fn transform_list(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::List(list) = node { + let mut contains_task_list = false; + let mut index = 0; + + while index < list.children.len() { + if let mdast::Node::ListItem(item) = &list.children[index] { + if item.checked.is_some() { + contains_task_list = true; + } + } + + index += 1; + } + + let (children, state) = all(node, state); + let mut properties = vec![]; + let tag_name = if list.ordered { + "ol".into() + } else { + "ul".into() + }; + + // Add start. + if let Some(start) = list.start { + if list.ordered && start != 1 { + properties.push(("start".into(), hast::PropertyValue::Number(start.into()))); + } + } + + // Like GitHub, add a class for custom styling. + if contains_task_list { + properties.push(( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "contains-task-list".into(), + )]), + )); + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name, + properties, + children: wrap(children, true), + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `List`") + } +} + +/// [`Math`][mdast::Math]. +fn transform_math(node: &mdast::Node, state: State) -> (Result, State) { + if let mdast::Node::Math(math) = node { + let mut value = math.value.clone(); + value.push('\n'); + + // To do: option to persist `meta`? + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "pre".into(), + properties: vec![], + children: vec![augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + hast::PropertyItem::String("language-math".into()), + hast::PropertyItem::String("math-display".into()), + ]), + )], + children: vec![hast::Node::Text(hast::Text { + value, + position: None, + })], + position: None, + }), + )], + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `Math`") + } +} + +/// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. +fn transform_mdx_expression(node: &mdast::Node, state: State) -> (Result, State) { + ( + Result::Node(augment_node( + node, + hast::Node::MdxExpression(hast::MdxExpression { + value: node.to_string(), + position: None, + }), + )), + state, + ) +} + +/// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. +fn transform_mdx_jsx_element(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + + let (name, attributes) = match node { + mdast::Node::MdxJsxFlowElement(n) => (&n.name, &n.attributes), + mdast::Node::MdxJsxTextElement(n) => (&n.name, &n.attributes), + _ => unreachable!("expected mdx jsx element"), + }; + + ( + Result::Node(augment_node( + node, + hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: name.clone(), + attributes: attributes.clone(), + children, + position: None, + }), + )), + state, + ) +} + +/// [`MdxjsEsm`][mdast::MdxjsEsm]. +fn transform_mdxjs_esm(node: &mdast::Node, state: State) -> (Result, State) { + ( + Result::Node(augment_node( + node, + hast::Node::MdxjsEsm(hast::MdxjsEsm { + value: node.to_string(), + position: None, + }), + )), + state, + ) +} + +/// [`Paragraph`][mdast::Paragraph]. +fn transform_paragraph(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children, + position: None, + }), + )), + state, + ) +} + +/// [`Root`][mdast::Root]. +fn transform_root(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Root(hast::Root { + children: wrap(children, false), + position: None, + }), + )), + state, + ) +} + +/// [`Strong`][mdast::Strong]. +fn transform_strong(node: &mdast::Node, state: State) -> (Result, State) { + let (children, state) = all(node, state); + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "strong".into(), + properties: vec![], + children, + position: None, + }), + )), + state, + ) +} + +/// [`TableCell`][mdast::TableCell]. +fn transform_table_cell( + node: &mdast::Node, + head: bool, + align: mdast::AlignKind, + state: State, +) -> (Result, State) { + let (children, state) = all(node, state); + // To do: option to generate a `style` instead? + let align_value = match align { + mdast::AlignKind::None => None, + mdast::AlignKind::Left => Some("left"), + mdast::AlignKind::Right => Some("right"), + mdast::AlignKind::Center => Some("center"), + }; + + let mut properties = vec![]; + + if let Some(value) = align_value { + properties.push(("align".into(), hast::PropertyValue::String(value.into()))); + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: if head { "th".into() } else { "td".into() }, + properties, + children, + position: None, + }), + )), + state, + ) +} + +/// [`TableRow`][mdast::TableRow]. +fn transform_table_row( + node: &mdast::Node, + head: bool, + align: Option<&[mdast::AlignKind]>, + mut state: State, +) -> (Result, State) { + if let mdast::Node::TableRow(row) = node { + let mut children = vec![]; + let mut index = 0; + #[allow(clippy::redundant_closure_for_method_calls)] + let len = align.map_or(row.children.len(), |d| d.len()); + let empty_cell = mdast::Node::TableCell(mdast::TableCell { + children: vec![], + position: None, + }); + + while index < len { + let align_value = align + .and_then(|d| d.get(index)) + .unwrap_or(&mdast::AlignKind::None); + + let child = row.children.get(index).unwrap_or(&empty_cell); + let tuple = transform_table_cell(child, head, *align_value, state); + append_result(&mut children, tuple.0); + state = tuple.1; + index += 1; + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "tr".into(), + properties: vec![], + children: wrap(children, true), + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `TableRow`") + } +} + +/// [`Table`][mdast::Table]. +fn transform_table(node: &mdast::Node, mut state: State) -> (Result, State) { + if let mdast::Node::Table(table) = node { + let mut rows = vec![]; + // let body = hast::Element { + // tag_name: "tbody".into(), + // properties: vec![], + // children: vec![], + // position: None, + // }; + let mut index = 0; + + while index < table.children.len() { + let tuple = transform_table_row( + &table.children[index], + index == 0, + Some(&table.align), + state, + ); + append_result(&mut rows, tuple.0); + state = tuple.1; + index += 1; + } + + let body_rows = rows.split_off(1); + let head_row = rows.pop(); + let mut children = vec![]; + + if let Some(row) = head_row { + let position = row.position().cloned(); + children.push(hast::Node::Element(hast::Element { + tag_name: "thead".into(), + properties: vec![], + children: wrap(vec![row], true), + position, + })); + } + + if !body_rows.is_empty() { + let mut position = None; + + if let Some(position_start) = body_rows.first().and_then(hast::Node::position) { + if let Some(position_end) = body_rows.last().and_then(hast::Node::position) { + position = Some(Position { + start: position_start.start.clone(), + end: position_end.end.clone(), + }); + } + } + + children.push(hast::Node::Element(hast::Element { + tag_name: "tbody".into(), + properties: vec![], + children: wrap(body_rows, true), + position, + })); + } + + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "table".into(), + properties: vec![], + children: wrap(children, true), + position: None, + }), + )), + state, + ) + } else { + unreachable!("expected `Table`") + } +} + +/// [`Text`][mdast::Text]. +fn transform_text(node: &mdast::Node, state: State) -> (Result, State) { + ( + Result::Node(augment_node( + node, + hast::Node::Text(hast::Text { + value: node.to_string(), + position: None, + }), + )), + state, + ) +} + +/// [`ThematicBreak`][mdast::ThematicBreak]. +fn transform_thematic_break(node: &mdast::Node, state: State) -> (Result, State) { + ( + Result::Node(augment_node( + node, + hast::Node::Element(hast::Element { + tag_name: "hr".into(), + properties: vec![], + children: vec![], + position: None, + }), + )), + state, + ) +} + +// Transform children of `parent`. +fn all(parent: &mdast::Node, mut state: State) -> (Vec, State) { + let mut result = vec![]; + if let Some(children) = parent.children() { + let mut index = 0; + while index < children.len() { + let child = &children[index]; + let tuple = one(child, Some(parent), state); + append_result(&mut result, tuple.0); + state = tuple.1; + index += 1; + } + } + + (result, state) +} + +/// Wrap `nodes` with line feeds between each entry. +/// Optionally adds line feeds at the start and end. +fn wrap(mut nodes: Vec, loose: bool) -> Vec { + let mut result = vec![]; + let was_empty = nodes.is_empty(); + let mut head = true; + + nodes.reverse(); + + if loose { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + while let Some(item) = nodes.pop() { + // Inject when there’s more: + if !head { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + head = false; + result.push(item); + } + + if loose && !was_empty { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + result +} + +/// Patch a position from the node `left` onto `right`. +fn augment_node(left: &mdast::Node, right: hast::Node) -> hast::Node { + if let Some(position) = left.position() { + augment_position(position, right) + } else { + right + } +} + +/// Patch a position from `left` onto `right`. +fn augment_position(left: &Position, mut right: hast::Node) -> hast::Node { + right.position_set(Some(left.clone())); + right +} + +/// Visit. +fn visit(node: &mdast::Node, visitor: Visitor) +where + Visitor: FnMut(&mdast::Node), +{ + visit_impl(node, visitor); +} + +/// Visit, mutably. +// Probably useful later: +#[allow(dead_code)] +fn visit_mut(node: &mut mdast::Node, visitor: Visitor) +where + Visitor: FnMut(&mut mdast::Node), +{ + visit_mut_impl(node, visitor); +} + +/// Internal implementation to visit. +fn visit_impl(node: &mdast::Node, mut visitor: Visitor) -> Visitor +where + Visitor: FnMut(&mdast::Node), +{ + visitor(node); + + if let Some(children) = node.children() { + let mut index = 0; + while index < children.len() { + let child = &children[index]; + visitor = visit_impl(child, visitor); + index += 1; + } + } + + visitor +} + +/// Internal implementation to visit, mutably. +fn visit_mut_impl(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor +where + Visitor: FnMut(&mut mdast::Node), +{ + visitor(node); + + if let Some(children) = node.children_mut() { + let mut index = 0; + while let Some(child) = children.get_mut(index) { + visitor = visit_mut_impl(child, visitor); + index += 1; + } + } + + visitor +} + +// To do: trim arounds breaks: . +/// Append an (optional, variadic) result. +fn append_result(list: &mut Vec, result: Result) { + match result { + Result::Fragment(mut fragment) => list.append(&mut fragment), + Result::Node(node) => list.push(node), + Result::None => {} + }; +} + +/// Replace line endings (CR, LF, CRLF) with spaces. +/// +/// Used for inline code and inline math. +fn replace_eols_with_spaces(value: &str) -> String { + // It’ll grow a bit small for each CR+LF. + let mut result = String::with_capacity(value.len()); + let bytes = value.as_bytes(); + let mut index = 0; + let mut start = 0; + + while index < bytes.len() { + let byte = bytes[index]; + + if byte == b'\r' || byte == b'\n' { + result.push_str(&value[start..index]); + result.push(' '); + + if index + 1 < bytes.len() && byte == b'\r' && bytes[index + 1] == b'\n' { + index += 1; + } + + start = index + 1; + } + + index += 1; + } + + result.push_str(&value[start..]); + + result +} + +/// Check if a list is loose. +fn list_loose(node: &mdast::Node) -> bool { + if let mdast::Node::List(list) = node { + if list.spread { + return true; + } + + if let Some(children) = node.children() { + let mut index = 0; + while index < children.len() { + if list_item_loose(&children[index]) { + return true; + } + index += 1; + } + } + } + + false +} + +/// Check if a list item is loose. +fn list_item_loose(node: &mdast::Node) -> bool { + if let mdast::Node::ListItem(item) = node { + item.spread + } else { + false + } +} diff --git a/tests/text.rs b/tests/text.rs index a2ef1fb..584f463 100644 --- a/tests/text.rs +++ b/tests/text.rs @@ -3,7 +3,7 @@ use micromark::micromark; use pretty_assertions::assert_eq; #[test] -fn text() -> Result<(), String> { +fn text() { assert_eq!( micromark("hello $.;'there"), "

hello $.;'there

", @@ -21,6 +21,4 @@ fn text() -> Result<(), String> { "

Multiple spaces

", "should preserve internal spaces verbatim" ); - - Ok(()) } diff --git a/tests/thematic_break.rs b/tests/thematic_break.rs index 85ab37f..d8d8104 100644 --- a/tests/thematic_break.rs +++ b/tests/thematic_break.rs @@ -1,7 +1,9 @@ extern crate micromark; use micromark::{ - mdast::{Node, Position, Root, ThematicBreak}, - micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, + mdast::{Node, Root, ThematicBreak}, + micromark, micromark_to_mdast, micromark_with_options, + unist::Position, + Constructs, Options, }; use pretty_assertions::assert_eq; diff --git a/tests/xxx_hast.rs b/tests/xxx_hast.rs new file mode 100644 index 0000000..be42818 --- /dev/null +++ b/tests/xxx_hast.rs @@ -0,0 +1,1585 @@ +extern crate micromark; +mod test_utils; +use micromark::mdast; +use pretty_assertions::assert_eq; +use test_utils::{hast, to_hast::to_hast}; + +#[test] +fn hast() { + assert_eq!( + to_hast(&mdast::Node::BlockQuote(mdast::BlockQuote { + children: vec![], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "blockquote".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + })], + position: None + }), + "should support a `BlockQuote`", + ); + + assert_eq!( + to_hast(&mdast::Node::Break(mdast::Break { position: None })), + hast::Node::Root(hast::Root { + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "br".into(), + properties: vec![], + children: vec![], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }) + ], + position: None + }), + "should support a `Break`", + ); + + assert_eq!( + to_hast(&mdast::Node::Code(mdast::Code { + lang: Some("b".into()), + meta: None, + value: "a".into(), + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "pre".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "language-b".into() + )]), + ),], + children: vec![hast::Node::Text(hast::Text { + value: "a\n".into(), + position: None + })], + position: None + })], + position: None + }), + "should support a `Code`", + ); + + assert_eq!( + to_hast(&mdast::Node::Definition(mdast::Definition { + url: "b".into(), + title: None, + identifier: "a".into(), + label: None, + position: None + })), + hast::Node::Root(hast::Root { + children: vec![], + position: None + }), + "should support a `Definition`", + ); + + assert_eq!( + to_hast(&mdast::Node::Delete(mdast::Delete { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "del".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + "should support a `Delete`", + ); + + assert_eq!( + to_hast(&mdast::Node::Emphasis(mdast::Emphasis { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "em".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + "should support an `Emphasis`", + ); + + assert_eq!( + to_hast(&mdast::Node::FootnoteDefinition( + mdast::FootnoteDefinition { + identifier: "a".into(), + label: None, + children: vec![], + position: None + } + )), + hast::Node::Root(hast::Root { + children: vec![], + position: None + }), + "should support a `FootnoteDefinition`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![ + mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition { + children: vec![mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "b".into(), + position: None + })], + position: None + }),], + identifier: "a".into(), + label: None, + position: None + }), + mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::FootnoteReference(mdast::FootnoteReference { + identifier: "a".into(), + label: None, + position: None, + })], + position: None + }), + ], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![ + // Main. + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("href".into(), hast::PropertyValue::String("#fn-a".into()),), + ("id".into(), hast::PropertyValue::String("fnref-a".into()),), + ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),), + ( + "ariaDescribedBy".into(), + hast::PropertyValue::String("footnote-label".into()), + ) + ], + children: vec![hast::Node::Text(hast::Text { + value: "1".into(), + position: None + })], + position: None + }),], + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + // Footer. + hast::Node::Element(hast::Element { + tag_name: "section".into(), + properties: vec![ + ("dataFootnotes".into(), hast::PropertyValue::Boolean(true),), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "footnotes".into() + ),]), + ), + ], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "h2".into(), + properties: vec![ + ( + "id".into(), + hast::PropertyValue::String("footnote-label".into()), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + hast::PropertyItem::String("sr-only".into()), + ]), + ), + ], + children: vec![hast::Node::Text(hast::Text { + value: "Footnotes".into(), + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "ol".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![( + "id".into(), + hast::PropertyValue::String("#fn-a".into()), + )], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "b ".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String( + "#fnref-a".into() + ), + ), + ( + "dataFootnoteBackref".into(), + hast::PropertyValue::Boolean(true), + ), + ( + "ariaLabel".into(), + hast::PropertyValue::String( + "Back to content".into() + ), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated( + vec![hast::PropertyItem::String( + "data-footnote-backref".into() + ),] + ), + ) + ], + children: vec![hast::Node::Text(hast::Text { + value: "↩".into(), + position: None + }),], + position: None + }) + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + "should support an `FootnoteReference`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![ + mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition { + children: vec![mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "b".into(), + position: None + })], + position: None + }),], + identifier: "a".into(), + label: None, + position: None + }), + mdast::Node::Paragraph(mdast::Paragraph { + children: vec![ + mdast::Node::FootnoteReference(mdast::FootnoteReference { + identifier: "a".into(), + label: None, + position: None, + }), + mdast::Node::FootnoteReference(mdast::FootnoteReference { + identifier: "a".into(), + label: None, + position: None, + }) + ], + position: None + }), + ], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![ + // Main. + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("href".into(), hast::PropertyValue::String("#fn-a".into()),), + ("id".into(), hast::PropertyValue::String("fnref-a".into()),), + ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),), + ( + "ariaDescribedBy".into(), + hast::PropertyValue::String("footnote-label".into()), + ) + ], + children: vec![hast::Node::Text(hast::Text { + value: "1".into(), + position: None + })], + position: None + }),], + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ("href".into(), hast::PropertyValue::String("#fn-a".into()),), + ("id".into(), hast::PropertyValue::String("fnref-a-2".into()),), + ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),), + ( + "ariaDescribedBy".into(), + hast::PropertyValue::String("footnote-label".into()), + ) + ], + children: vec![hast::Node::Text(hast::Text { + value: "1".into(), + position: None + })], + position: None + }),], + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + // Footer. + hast::Node::Element(hast::Element { + tag_name: "section".into(), + properties: vec![ + ("dataFootnotes".into(), hast::PropertyValue::Boolean(true),), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "footnotes".into() + ),]), + ), + ], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "h2".into(), + properties: vec![ + ( + "id".into(), + hast::PropertyValue::String("footnote-label".into()), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + hast::PropertyItem::String("sr-only".into()), + ]), + ), + ], + children: vec![hast::Node::Text(hast::Text { + value: "Footnotes".into(), + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "ol".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![( + "id".into(), + hast::PropertyValue::String("#fn-a".into()), + )], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "b ".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String( + "#fnref-a".into() + ), + ), + ( + "dataFootnoteBackref".into(), + hast::PropertyValue::Boolean(true), + ), + ( + "ariaLabel".into(), + hast::PropertyValue::String( + "Back to content".into() + ), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated( + vec![hast::PropertyItem::String( + "data-footnote-backref".into() + ),] + ), + ) + ], + children: vec![hast::Node::Text(hast::Text { + value: "↩".into(), + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: " ".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String( + "#fnref-a-2".into() + ), + ), + ( + "dataFootnoteBackref".into(), + hast::PropertyValue::Boolean(true), + ), + ( + "ariaLabel".into(), + hast::PropertyValue::String( + "Back to content".into() + ), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated( + vec![hast::PropertyItem::String( + "data-footnote-backref".into() + ),] + ), + ) + ], + children: vec![ + hast::Node::Text(hast::Text { + value: "↩".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Text( + hast::Text { + value: "2".into(), + position: None + } + ),], + position: None + }) + ], + position: None + }) + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + "should support an `FootnoteReference` (multiple calls to same definition)", + ); + + assert_eq!( + to_hast(&mdast::Node::Heading(mdast::Heading { + depth: 1, + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "h1".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + "should support a `Heading`", + ); + + assert_eq!( + to_hast(&mdast::Node::Html(mdast::Html { + value: "
".into(), + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![], + position: None + }), + "should support an `Html`", + ); + + assert_eq!( + to_hast(&mdast::Node::Image(mdast::Image { + url: "a".into(), + alt: "b".into(), + title: None, + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "img".into(), + properties: vec![ + ("src".into(), hast::PropertyValue::String("a".into()),), + ("alt".into(), hast::PropertyValue::String("b".into()),) + ], + children: vec![], + position: None + }), + "should support an `Image`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![ + mdast::Node::Definition(mdast::Definition { + url: "b".into(), + title: None, + identifier: "a".into(), + label: None, + position: None + }), + mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::ImageReference(mdast::ImageReference { + reference_kind: mdast::ReferenceKind::Full, + identifier: "a".into(), + alt: "c".into(), + label: None, + position: None, + })], + position: None + }), + ], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "img".into(), + properties: vec![ + ("src".into(), hast::PropertyValue::String("b".into()),), + ("alt".into(), hast::PropertyValue::String("c".into()),) + ], + children: vec![], + position: None + }),], + position: None + }),], + position: None + }), + "should support an `ImageReference`", + ); + + assert_eq!( + to_hast(&mdast::Node::InlineCode(mdast::InlineCode { + value: "a\nb".into(), + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a b".into(), + position: None + })], + position: None + }), + "should support an `InlineCode`", + ); + + assert_eq!( + to_hast(&mdast::Node::InlineMath(mdast::InlineMath { + value: "a\nb".into(), + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + hast::PropertyItem::String("language-math".into()), + hast::PropertyItem::String("math-inline".into()) + ]), + ),], + children: vec![hast::Node::Text(hast::Text { + value: "a b".into(), + position: None + })], + position: None + }), + "should support an `InlineMath`", + ); + + assert_eq!( + to_hast(&mdast::Node::Link(mdast::Link { + url: "a".into(), + title: None, + children: vec![mdast::Node::Text(mdast::Text { + value: "b".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("href".into(), hast::PropertyValue::String("a".into()),),], + children: vec![hast::Node::Text(hast::Text { + value: "b".into(), + position: None + })], + position: None + }), + "should support a `Link`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![ + mdast::Node::Definition(mdast::Definition { + url: "b".into(), + title: None, + identifier: "a".into(), + label: None, + position: None + }), + mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::LinkReference(mdast::LinkReference { + reference_kind: mdast::ReferenceKind::Full, + identifier: "a".into(), + label: None, + children: vec![mdast::Node::Text(mdast::Text { + value: "c".into(), + position: None + })], + position: None, + })], + position: None + }), + ], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![("href".into(), hast::PropertyValue::String("b".into()),),], + children: vec![hast::Node::Text(hast::Text { + value: "c".into(), + position: None + })], + position: None + }),], + position: None + }),], + position: None + }), + "should support a `LinkReference`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![mdast::Node::ListItem(mdast::ListItem { + spread: false, + checked: None, + children: vec![mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None + }),], + position: None + }),], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + }),], + position: None + }),], + position: None + }), + "should support a `ListItem`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![mdast::Node::ListItem(mdast::ListItem { + spread: true, + checked: None, + children: vec![mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None + }),], + position: None + }),], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }),], + position: None + }), + "should support a `ListItem` (spread: true)", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![mdast::Node::ListItem(mdast::ListItem { + spread: false, + checked: Some(true), + children: vec![], + position: None + }),], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "task-list-item".into() + )]) + )], + children: vec![hast::Node::Element(hast::Element { + tag_name: "input".into(), + properties: vec![ + ( + "type".into(), + hast::PropertyValue::String("checkbox".into()), + ), + ("checked".into(), hast::PropertyValue::Boolean(true)), + ("disabled".into(), hast::PropertyValue::Boolean(true)), + ], + children: vec![], + position: None + }),], + position: None + }),], + position: None + }), + "should support a `ListItem` (checked, w/o paragraph)", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![mdast::Node::ListItem(mdast::ListItem { + spread: false, + checked: Some(false), + children: vec![mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None + }),], + position: None + }),], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "task-list-item".into() + )]) + )], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "input".into(), + properties: vec![ + ( + "type".into(), + hast::PropertyValue::String("checkbox".into()), + ), + ("checked".into(), hast::PropertyValue::Boolean(false)), + ("disabled".into(), hast::PropertyValue::Boolean(true)), + ], + children: vec![], + position: None + }), + hast::Node::Text(hast::Text { + value: " ".into(), + position: None + }), + hast::Node::Text(hast::Text { + value: "a".into(), + position: None + }), + ], + position: None + }),], + position: None + }), + "should support a `ListItem` (unchecked, w/ paragraph)", + ); + + assert_eq!( + to_hast(&mdast::Node::List(mdast::List { + ordered: true, + start: Some(1), + spread: false, + children: vec![mdast::Node::ListItem(mdast::ListItem { + spread: false, + checked: None, + children: vec![mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None + }),], + position: None + }),], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "ol".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + "should support a `List` (ordered, start: 1)", + ); + + assert_eq!( + to_hast(&mdast::Node::List(mdast::List { + ordered: true, + start: Some(123), + spread: false, + children: vec![], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "ol".into(), + properties: vec![("start".into(), hast::PropertyValue::Number(123.0),),], + children: vec![hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + })], + position: None + }), + "should support a `List` (ordered, start: 123)", + ); + + assert_eq!( + to_hast(&mdast::Node::List(mdast::List { + ordered: false, + start: None, + spread: false, + children: vec![], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "ul".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + })], + position: None + }), + "should support a `List` (unordered)", + ); + + assert_eq!( + to_hast(&mdast::Node::List(mdast::List { + ordered: false, + start: None, + spread: false, + children: vec![mdast::Node::ListItem(mdast::ListItem { + spread: false, + checked: Some(true), + children: vec![], + position: None + }),], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "ul".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "contains-task-list".into() + )]) + )], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String( + "task-list-item".into() + )]) + )], + children: vec![hast::Node::Element(hast::Element { + tag_name: "input".into(), + properties: vec![ + ( + "type".into(), + hast::PropertyValue::String("checkbox".into()), + ), + ("checked".into(), hast::PropertyValue::Boolean(true)), + ("disabled".into(), hast::PropertyValue::Boolean(true)), + ], + children: vec![], + position: None + }),], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + "should support a `List` (w/ checked item)", + ); + + assert_eq!( + to_hast(&mdast::Node::Math(mdast::Math { + meta: None, + value: "a".into(), + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "pre".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + hast::PropertyItem::String("language-math".into()), + hast::PropertyItem::String("math-display".into()) + ]), + ),], + children: vec![hast::Node::Text(hast::Text { + value: "a\n".into(), + position: None + })], + position: None + })], + position: None + }), + "should support a `Math`", + ); + + assert_eq!( + to_hast(&mdast::Node::MdxFlowExpression(mdast::MdxFlowExpression { + value: "a".into(), + position: None, + })), + hast::Node::MdxExpression(hast::MdxExpression { + value: "a".into(), + position: None + }), + "should support an `MdxFlowExpression`", + ); + + assert_eq!( + to_hast(&mdast::Node::MdxTextExpression(mdast::MdxTextExpression { + value: "a".into(), + position: None, + })), + hast::Node::MdxExpression(hast::MdxExpression { + value: "a".into(), + position: None + }), + "should support an `MdxTextExpression`", + ); + + assert_eq!( + to_hast(&mdast::Node::MdxJsxFlowElement(mdast::MdxJsxFlowElement { + name: None, + attributes: vec![], + children: vec![], + position: None, + })), + hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: None, + attributes: vec![], + children: vec![], + position: None, + }), + "should support an `MdxJsxFlowElement`", + ); + + assert_eq!( + to_hast(&mdast::Node::MdxJsxTextElement(mdast::MdxJsxTextElement { + name: None, + attributes: vec![], + children: vec![], + position: None, + })), + hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: None, + attributes: vec![], + children: vec![], + position: None, + }), + "should support an `MdxJsxTextElement`", + ); + + assert_eq!( + to_hast(&mdast::Node::MdxjsEsm(mdast::MdxjsEsm { + value: "a".into(), + position: None, + })), + hast::Node::MdxjsEsm(hast::MdxjsEsm { + value: "a".into(), + position: None + }), + "should support an `MdxjsEsm`", + ); + + assert_eq!( + to_hast(&mdast::Node::Paragraph(mdast::Paragraph { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + "should support a `Paragraph`", + ); + + assert_eq!( + to_hast(&mdast::Node::Root(mdast::Root { + children: vec![], + position: None, + })), + hast::Node::Root(hast::Root { + children: vec![], + position: None + }), + "should support a `Root`", + ); + + assert_eq!( + to_hast(&mdast::Node::Strong(mdast::Strong { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "strong".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + "should support a `Strong`", + ); + + assert_eq!( + to_hast(&mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "td".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + "should support a `TableCell`", + ); + + assert_eq!( + to_hast(&mdast::Node::TableRow(mdast::TableRow { + children: vec![ + mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + }), + mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "b".into(), + position: None + })], + position: None, + }) + ], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "tr".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "td".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "td".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "b".into(), + position: None + })], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + "should support a `TableRow`", + ); + + assert_eq!( + to_hast(&mdast::Node::Table(mdast::Table { + align: vec![mdast::AlignKind::Left, mdast::AlignKind::None], + children: vec![ + mdast::Node::TableRow(mdast::TableRow { + children: vec![ + mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None + })], + position: None, + }), + mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "b".into(), + position: None + })], + position: None, + }) + ], + position: None, + }), + mdast::Node::TableRow(mdast::TableRow { + children: vec![ + mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "c".into(), + position: None + })], + position: None, + }), + mdast::Node::TableCell(mdast::TableCell { + children: vec![mdast::Node::Text(mdast::Text { + value: "d".into(), + position: None + })], + position: None, + }) + ], + position: None, + }) + ], + position: None, + })), + hast::Node::Element(hast::Element { + tag_name: "table".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "thead".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "tr".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "th".into(), + properties: vec![( + "align".into(), + hast::PropertyValue::String("left".into()), + ),], + children: vec![hast::Node::Text(hast::Text { + value: "a".into(), + position: None + })], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "th".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "b".into(), + position: None + })], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "tbody".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "tr".into(), + properties: vec![], + children: vec![ + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "td".into(), + properties: vec![( + "align".into(), + hast::PropertyValue::String("left".into()), + ),], + children: vec![hast::Node::Text(hast::Text { + value: "c".into(), + position: None + })], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + hast::Node::Element(hast::Element { + tag_name: "td".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: "d".into(), + position: None + })], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None + }), + ], + position: None + }), + "should support a `Table`", + ); + + assert_eq!( + to_hast(&mdast::Node::Text(mdast::Text { + value: "a".into(), + position: None, + })), + hast::Node::Text(hast::Text { + value: "a".into(), + position: None + }), + "should support a `Text`", + ); + + assert_eq!( + to_hast(&mdast::Node::ThematicBreak(mdast::ThematicBreak { + position: None + })), + hast::Node::Element(hast::Element { + tag_name: "hr".into(), + properties: vec![], + children: vec![], + position: None + }), + "should support a `Thematicbreak`", + ); + + assert_eq!( + to_hast(&mdast::Node::Yaml(mdast::Yaml { + value: "a".into(), + position: None + })), + hast::Node::Root(hast::Root { + children: vec![], + position: None + }), + "should support a `Yaml`", + ); + + assert_eq!( + to_hast(&mdast::Node::Toml(mdast::Toml { + value: "a".into(), + position: None + })), + hast::Node::Root(hast::Root { + children: vec![], + position: None + }), + "should support a `Toml`", + ); +} -- cgit