From a0c84c505d733be2e987a333a34244c1befb56cb Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 26 Sep 2022 16:12:25 +0200 Subject: Add support for compiling to mdast See: . --- Untitled.txt | 10 + build.rs | 2 +- examples/lib.rs | 25 +- readme.md | 97 ++- src/lib.rs | 30 +- src/mdast.rs | 795 ++++++++++++------- src/to_html.rs | 248 +----- src/to_mdast.rs | 1762 ++++++++++++++++++++++++++++++++++++++++- src/util/constant.rs | 4 +- src/util/infer.rs | 192 +++++ src/util/mod.rs | 1 + tests/attention.rs | 44 +- tests/autolink.rs | 51 +- tests/block_quote.rs | 23 +- tests/character_escape.rs | 20 +- tests/character_reference.rs | 29 +- tests/code_fenced.rs | 36 +- tests/code_indented.rs | 22 +- tests/code_text.rs | 30 +- tests/commonmark.rs | 2 +- tests/definition.rs | 20 +- tests/frontmatter.rs | 29 +- tests/gfm_autolink_literal.rs | 90 ++- tests/gfm_footnote.rs | 45 +- tests/gfm_strikethrough.rs | 33 +- tests/gfm_table.rs | 132 ++- tests/gfm_task_list_item.rs | 57 +- tests/hard_break_escape.rs | 29 +- tests/hard_break_trailing.rs | 29 +- tests/heading_atx.rs | 21 +- tests/heading_setext.rs | 21 +- tests/html_flow.rs | 17 +- tests/html_text.rs | 38 +- tests/image.rs | 108 ++- tests/link_reference.rs | 73 +- tests/link_resource.rs | 51 +- tests/list.rs | 106 ++- tests/math_flow.rs | 18 +- tests/math_text.rs | 30 +- tests/mdx_esm.rs | 17 +- tests/mdx_expression_flow.rs | 17 +- tests/mdx_expression_text.rs | 30 +- tests/mdx_jsx_flow.rs | 36 +- tests/mdx_jsx_text.rs | 274 ++++++- tests/thematic_break.rs | 16 +- 45 files changed, 4105 insertions(+), 655 deletions(-) create mode 100644 src/util/infer.rs diff --git a/Untitled.txt b/Untitled.txt index fb1e53c..94d805f 100644 --- a/Untitled.txt +++ b/Untitled.txt @@ -1,3 +1,13 @@ +Rename all the β€œchunks” (as terms in `Name`s) to data? + +Divide parse options from to_html options? +- constructs +- gfm_strikethrough_single_tilde +- mdx_expression_parse +- mdx_esm_parse + +--- + micromark.js: unquoted: is `completeAttributeValueUnquoted`s case for `completeAttributeNameAfter` missing a `/`?. I’ve added it here. micromark.js: `]` case in cdata_end does not need to consume, it can defer to `cdata_close`, which should save 1 line micromark.js: should `tagOpenAttributeValueUnquoted` also support a slash? diff --git a/build.rs b/build.rs index e32493c..a971d27 100644 --- a/build.rs +++ b/build.rs @@ -62,7 +62,7 @@ async fn commonmark() { } let doc = format!( - "//! CommonMark test suite. + "//! `CommonMark` test suite. // > πŸ‘‰ **Important**: this module is generated by `build.rs`. // > It is generate from the latest CommonMark website. diff --git a/examples/lib.rs b/examples/lib.rs index 98089de..ed108d2 100644 --- a/examples/lib.rs +++ b/examples/lib.rs @@ -1,5 +1,5 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{micromark, micromark_to_mdast, micromark_with_options, Constructs, Options}; fn main() -> Result<(), String> { // Turn on debugging. @@ -35,30 +35,13 @@ fn main() -> Result<(), String> { )? ); - // Support MDX extensions. - // Note: mdx compiles to nothing, handle it yourself. + // Access syntax tree and support MDX extensions: println!( - "{}", - micromark_with_options( + "{:?}", + micromark_to_mdast( "# , {username}!", &Options { constructs: Constructs::mdx(), - gfm_tagfilter: true, - ..Options::default() - } - )? - ); - - // Support other extensions that are not in CommonMark. - println!( - "{:?}", - micromark_with_options( - "---\ntitle: Neptune\n---\nSome stuff on the moons of Neptune.", - &Options { - constructs: Constructs { - frontmatter: true, - ..Constructs::default() - }, ..Options::default() } )? diff --git a/readme.md b/readme.md index 0a03e0d..3d18a00 100644 --- a/readme.md +++ b/readme.md @@ -22,11 +22,11 @@ positional info, concrete tokens, and extensions. - [x] **[compliant][commonmark]** (100% to CommonMark) - [x] **[extensions][]** (100% GFM, 100% MDX, frontmatter, math) +- [x] **[ast][mdast]** (mdast) - [x] **[safe][security]** (100% safe rust, also 100% safe HTML by default) - [x] **[robust][test]** (2300+ tests, 100% coverage, fuzz testing) -It’s also `#![no_std]` + `alloc`, has tons of docs, and has a single dependency -(for optional debug logging). +It’s also `#![no_std]` + `alloc` and has tons of docs. > 🐣 **Note**: coverage is currently within progress. @@ -58,7 +58,6 @@ generate syntax trees or compile to other output formats. - [Use](#use) - [API](#api) - [Extensions](#extensions) -- [Architecture](#architecture) - [Examples](#examples) - [Markdown](#markdown) - [Project](#project) @@ -128,6 +127,7 @@ Yields: `micromark` exposes [`micromark`](https://wooorm.com/micromark-rs/micromark/fn.micromark.html), [`micromark_with_options`](https://wooorm.com/micromark-rs/micromark/fn.micromark_with_options.html), and +[`micromark_to_mdast`](https://wooorm.com/micromark-rs/micromark/fn.micromark_to_mdast.html), and [`Options`](https://wooorm.com/micromark-rs/micromark/struct.Options.html). See [crate docs][docs] for more info. @@ -155,52 +155,6 @@ It is not a goal of this project to support lots of different extensions. It’s instead a goal to support incredibly common, somewhat standardized, extensions. -## Architecture - -micromark is maintained as a single monolithic package. - -### Overview - -The process to parse markdown looks like this: - -```txt - micromark -+------------------------------------------------+ -| +-------+ +---------+ | -| -markdown->+ parse +-events->+ compile +-html- | -| +-------+ +---------+ | -+------------------------------------------------+ -``` - -### File structure - -The files in `src/` are as follows: - -- `construct/*.rs` - β€” CommonMark, GFM, and other extension constructs used in micromark -- `util/*.rs` - β€” helpers often needed when parsing markdown -- `event.rs` - β€” things with meaning happening somewhere -- `lib.rs` - β€” core module -- `mdast.rs` - β€” syntax tree -- `parser.rs` - β€” turn a string of markdown into events -- `resolve.rs` - β€” steps to process events -- `state.rs` - β€” steps of the state machine -- `subtokenize.rs` - β€” handle content in other content -- `to_html.rs` - β€” turns events into a string of HTML -- `to_mdast.rs` - β€” turns events into a syntax tree -- `tokenizer.rs` - β€” glue the states of the state machine together - ## Examples @@ -338,6 +292,50 @@ For more practical examples of how things roughly work in BNF, see the module do ## Project +micromark is maintained as a single monolithic package. + +### Overview + +The process to parse markdown looks like this: + +```txt + micromark ++-------------------------------------------------+ +| +-------+ +---------+--html- | +| -markdown->+ parse +-events->+ compile + | +| +-------+ +---------+-mdast- | ++-------------------------------------------------+ +``` + +### File structure + +The files in `src/` are as follows: + +- `construct/*.rs` + β€” CommonMark, GFM, and other extension constructs used in micromark +- `util/*.rs` + β€” helpers often needed when parsing markdown +- `event.rs` + β€” things with meaning happening somewhere +- `lib.rs` + β€” core module +- `mdast.rs` + β€” syntax tree +- `parser.rs` + β€” turn a string of markdown into events +- `resolve.rs` + β€” steps to process events +- `state.rs` + β€” steps of the state machine +- `subtokenize.rs` + β€” handle content in other content +- `to_html.rs` + β€” turns events into a string of HTML +- `to_mdast.rs` + β€” turns events into a syntax tree +- `tokenizer.rs` + β€” glue the states of the state machine together + ### Comparison > 🚧 **To do**. @@ -465,6 +463,7 @@ Support this effort and give back by sponsoring: [chalker]: https://github.com/ChALkeR [license]: https://github.com/micromark/micromark/blob/main/license [author]: https://wooorm.com +[mdast]: https://github.com/syntax-tree/mdast [starry-night]: https://github.com/wooorm/starry-night [contribute]: #contribute [sponsor]: #sponsor diff --git a/src/lib.rs b/src/lib.rs index 669660b..fcdab10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ mod tokenizer; mod util; use alloc::{boxed::Box, fmt, string::String}; -use mdast::Root; +use mdast::Node; use parser::parse; use to_html::compile as to_html; use to_mdast::compile as to_mdast; @@ -1082,19 +1082,11 @@ impl fmt::Debug for Options { .field("math_text_single_dollar", &self.math_text_single_dollar) .field( "mdx_expression_parse", - if self.mdx_expression_parse.is_none() { - &"None" - } else { - &"Some([Function])" - }, + &self.mdx_expression_parse.as_ref().map(|_d| "[Function]"), ) .field( "mdx_esm_parse", - if self.mdx_esm_parse.is_none() { - &"None" - } else { - &"Some([Function])" - }, + &self.mdx_esm_parse.as_ref().map(|_d| "[Function]"), ) .finish() } @@ -1180,8 +1172,18 @@ pub fn micromark_with_options(value: &str, options: &Options) -> Result Result { +/// ``` +/// use micromark::{micromark_to_mdast, Options}; +/// # fn main() -> Result<(), String> { +/// +/// let tree = micromark_to_mdast("# hi!", &Options::default())?; +/// +/// println!("{:?}", tree); +/// # Ok(()) +/// # } +/// ``` +pub fn micromark_to_mdast(value: &str, options: &Options) -> Result { let (events, bytes) = parse(value, options)?; - Ok(to_mdast(&events, bytes, options)) + let node = to_mdast(&events, bytes)?; + Ok(node) } diff --git a/src/mdast.rs b/src/mdast.rs index b60e891..79a39dd 100644 --- a/src/mdast.rs +++ b/src/mdast.rs @@ -2,13 +2,14 @@ //! //! [mdast]: https://github.com/syntax-tree/mdast -// To do: example. -// To do: math. - -use alloc::{string::String, vec::Vec}; +use alloc::{ + fmt, + string::{String, ToString}, + vec::Vec, +}; /// One place in a source file. -#[derive(Clone, Debug)] +#[derive(Clone, Eq, PartialEq)] pub struct Point { /// 1-indexed integer representing a line in a source file. pub line: usize, @@ -18,8 +19,25 @@ pub struct Point { 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, Debug)] +#[derive(Clone, Eq, PartialEq)] pub struct Position { /// Represents the place of the first character of the parsed source region. pub start: Point, @@ -28,8 +46,40 @@ pub struct Position { 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, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ReferenceKind { /// The reference is implicit, its identifier inferred from its content. Shortcut, @@ -40,141 +90,88 @@ pub enum ReferenceKind { } /// Represents how phrasing content is aligned. -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum AlignKind { + /// Left alignment. + /// /// See the `left` value of the `text-align` CSS property. + /// + /// ```markdown + /// | | aaa | + /// > | | :-- | + /// ^^^ + /// ``` Left, + /// Right alignment. + /// /// See the `right` value of the `text-align` CSS property. + /// + /// ```markdown + /// | | aaa | + /// > | | --: | + /// ^^^ + /// ``` Right, + /// Center alignment. + /// /// See the `center` value of the `text-align` CSS property. + /// + /// ```markdown + /// | | aaa | + /// > | | :-: | + /// ^^^ + /// ``` Center, + /// No alignment. + /// /// Phrasing content is aligned as defined by the host environment. + /// + /// ```markdown + /// | | aaa | + /// > | | --- | + /// ^^^ + /// ``` None, } -/// Node type. -#[derive(Clone, Debug)] -pub enum Kind { - /// Root node. - Root, - /// Paragraph node. - Paragraph, - /// Heading node. - Heading, - /// Thematic break node. - ThematicBreak, - /// Block quote node. - BlockQuote, - /// List node. - List, - /// List item node. - ListItem, - /// Html node. - Html, - /// Code node. - Code, - /// Definition node. - Definition, - /// Text node. - Text, - /// Emphasis node. - Emphasis, - /// Strong node. - Strong, - /// Code (inline) node. - InlineCode, - /// Break node. - Break, - /// Link node. - Link, - /// Image node. - Image, - /// Link reference node. - LinkReference, - /// Image reference node. - ImageReference, - /// Footnote definition node. - FootnoteDefinition, - /// Footnote reference node. - FootnoteReference, - /// Table node. - Table, - /// Table row node. - TableRow, - /// Table cell node. - TableCell, - /// Strong node. - Delete, - /// Yaml node. - Yaml, - /// Toml node. - Toml, - /// MDX: ESM node. - MdxjsEsm, - /// MDX: expression (flow). - MdxFlowExpression, - /// MDX: expression (phrasing). - MdxTextExpression, - /// MDX: JSX element (flow). - MdxJsxFlowElement, - /// MDX: JSX element (phrasing). - MdxJsxTextElement, - /// MDX: JSX attribute expression. - MdxJsxExpressionAttribute, - /// MDX: JSX attribute. - MdxJsxAttribute, - /// MDX: JSX attribute value expression. - MdxJsxAttributeValueExpression, -} - -/// Document content. -#[derive(Clone, Debug)] -pub enum DocumentContent { - /// Container content. - Container(ContainerContent), - /// Frontmatter content. - Frontmatter(FrontmatterContent), -} +/// Nodes. +#[derive(Clone, Eq, PartialEq)] +pub enum Node { + // Document: + /// Root. + Root(Root), -/// Container content. -#[derive(Clone, Debug)] -pub enum ContainerContent { + // Container: /// Block quote. BlockQuote(BlockQuote), - /// Flow content. - Flow(FlowContent), /// Footnote definition. FootnoteDefinition(FootnoteDefinition), /// MDX: JSX element (container). - JsxElement(MdxJsxFlowElement), + MdxJsxFlowElement(MdxJsxFlowElement), /// List. List(List), -} -/// Frontmatter content. -#[derive(Clone, Debug)] -pub enum FrontmatterContent { + // Frontmatter: /// MDX.js ESM. - Esm(MdxjsEsm), + MdxjsEsm(MdxjsEsm), /// Toml. Toml(Toml), /// Yaml. Yaml(Yaml), -} -/// Phrasing content. -#[derive(Clone, Debug)] -pub enum PhrasingContent { + // Phrasing: /// Break. Break(Break), /// Code (phrasing). - Code(InlineCode), + InlineCode(InlineCode), + /// Math (phrasing). + InlineMath(InlineMath), /// Delete. Delete(Delete), /// Emphasis. Emphasis(Emphasis), // MDX: expression (text). - Expression(MdxTextExpression), + MdxTextExpression(MdxTextExpression), /// Footnote reference. FootnoteReference(FootnoteReference), /// Html (phrasing). @@ -184,7 +181,7 @@ pub enum PhrasingContent { /// Image reference. ImageReference(ImageReference), // MDX: JSX element (text). - JsxElement(MdxJsxTextElement), + MdxJsxTextElement(MdxJsxTextElement), /// Link. Link(Link), /// Link reference. @@ -193,72 +190,339 @@ pub enum PhrasingContent { Strong(Strong), /// Text. Text(Text), -} -/// Flow content. -#[derive(Clone, Debug)] -pub enum FlowContent { + // Flow: /// Code (flow). Code(Code), - /// Content. - Content(ContentContent), + /// Math (flow). + Math(Math), // MDX: expression (flow). - Expression(MdxFlowExpression), + MdxFlowExpression(MdxFlowExpression), /// Heading. Heading(Heading), /// Html (flow). - Html(Html), + // Html(Html), /// Table. Table(Table), /// Thematic break. ThematicBreak(ThematicBreak), -} -/// Table content. -#[derive(Clone, Debug)] -pub enum TableContent { + // Table content. /// Table row. - Row(TableRow), -} + TableRow(TableRow), -/// Row content. -#[derive(Clone, Debug)] -pub enum RowContent { + // Row content. /// Table cell. - Cell(TableCell), -} + TableCell(TableCell), -/// List content. -#[derive(Clone, Debug)] -pub enum ListContent { + // List content. /// List item. - Item(ListItem), -} + ListItem(ListItem), -/// Content. -#[derive(Clone, Debug)] -pub enum ContentContent { + // Content. /// Definition. Definition(Definition), /// Paragraph. Paragraph(Paragraph), } +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::BlockQuote(x) => write!(f, "{:?}", x), + Node::FootnoteDefinition(x) => write!(f, "{:?}", x), + Node::MdxJsxFlowElement(x) => write!(f, "{:?}", x), + Node::List(x) => write!(f, "{:?}", x), + Node::MdxjsEsm(x) => write!(f, "{:?}", x), + Node::Toml(x) => write!(f, "{:?}", x), + Node::Yaml(x) => write!(f, "{:?}", x), + Node::Break(x) => write!(f, "{:?}", x), + Node::InlineCode(x) => write!(f, "{:?}", x), + Node::InlineMath(x) => write!(f, "{:?}", x), + Node::Delete(x) => write!(f, "{:?}", x), + Node::Emphasis(x) => write!(f, "{:?}", x), + Node::MdxTextExpression(x) => write!(f, "{:?}", x), + Node::FootnoteReference(x) => write!(f, "{:?}", x), + Node::Html(x) => write!(f, "{:?}", x), + Node::Image(x) => write!(f, "{:?}", x), + Node::ImageReference(x) => write!(f, "{:?}", x), + Node::MdxJsxTextElement(x) => write!(f, "{:?}", x), + Node::Link(x) => write!(f, "{:?}", x), + Node::LinkReference(x) => write!(f, "{:?}", x), + Node::Strong(x) => write!(f, "{:?}", x), + Node::Text(x) => write!(f, "{:?}", x), + Node::Code(x) => write!(f, "{:?}", x), + Node::Math(x) => write!(f, "{:?}", x), + Node::MdxFlowExpression(x) => write!(f, "{:?}", x), + Node::Heading(x) => write!(f, "{:?}", x), + Node::Table(x) => write!(f, "{:?}", x), + Node::ThematicBreak(x) => write!(f, "{:?}", x), + Node::TableRow(x) => write!(f, "{:?}", x), + Node::TableCell(x) => write!(f, "{:?}", x), + Node::ListItem(x) => write!(f, "{:?}", x), + Node::Definition(x) => write!(f, "{:?}", x), + Node::Paragraph(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::BlockQuote(x) => children_to_string(&x.children), + Node::FootnoteDefinition(x) => children_to_string(&x.children), + Node::MdxJsxFlowElement(x) => children_to_string(&x.children), + Node::List(x) => children_to_string(&x.children), + Node::Delete(x) => children_to_string(&x.children), + Node::Emphasis(x) => children_to_string(&x.children), + Node::MdxJsxTextElement(x) => children_to_string(&x.children), + Node::Link(x) => children_to_string(&x.children), + Node::LinkReference(x) => children_to_string(&x.children), + Node::Strong(x) => children_to_string(&x.children), + Node::Heading(x) => children_to_string(&x.children), + Node::Table(x) => children_to_string(&x.children), + Node::TableRow(x) => children_to_string(&x.children), + Node::TableCell(x) => children_to_string(&x.children), + Node::ListItem(x) => children_to_string(&x.children), + Node::Paragraph(x) => children_to_string(&x.children), + + // Literals. + Node::MdxjsEsm(x) => x.value.clone(), + Node::Toml(x) => x.value.clone(), + Node::Yaml(x) => x.value.clone(), + Node::InlineCode(x) => x.value.clone(), + Node::InlineMath(x) => x.value.clone(), + Node::MdxTextExpression(x) => x.value.clone(), + Node::Html(x) => x.value.clone(), + Node::Text(x) => x.value.clone(), + Node::Code(x) => x.value.clone(), + Node::Math(x) => x.value.clone(), + Node::MdxFlowExpression(x) => x.value.clone(), + + // Voids. + Node::Break(_) + | Node::FootnoteReference(_) + | Node::Image(_) + | Node::ImageReference(_) + | Node::ThematicBreak(_) + | Node::Definition(_) => "".to_string(), + } + } +} + +impl Node { + #[must_use] + pub fn children(&self) -> Option<&Vec> { + match self { + // Parent. + Node::Root(x) => Some(&x.children), + Node::Paragraph(x) => Some(&x.children), + Node::Heading(x) => Some(&x.children), + Node::BlockQuote(x) => Some(&x.children), + Node::List(x) => Some(&x.children), + Node::ListItem(x) => Some(&x.children), + Node::Emphasis(x) => Some(&x.children), + Node::Strong(x) => Some(&x.children), + Node::Link(x) => Some(&x.children), + Node::LinkReference(x) => Some(&x.children), + Node::FootnoteDefinition(x) => Some(&x.children), + Node::Table(x) => Some(&x.children), + Node::TableRow(x) => Some(&x.children), + Node::TableCell(x) => Some(&x.children), + Node::Delete(x) => Some(&x.children), + Node::MdxJsxFlowElement(x) => Some(&x.children), + Node::MdxJsxTextElement(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::Paragraph(x) => Some(&mut x.children), + Node::Heading(x) => Some(&mut x.children), + Node::BlockQuote(x) => Some(&mut x.children), + Node::List(x) => Some(&mut x.children), + Node::ListItem(x) => Some(&mut x.children), + Node::Emphasis(x) => Some(&mut x.children), + Node::Strong(x) => Some(&mut x.children), + Node::Link(x) => Some(&mut x.children), + Node::LinkReference(x) => Some(&mut x.children), + Node::FootnoteDefinition(x) => Some(&mut x.children), + Node::Table(x) => Some(&mut x.children), + Node::TableRow(x) => Some(&mut x.children), + Node::TableCell(x) => Some(&mut x.children), + Node::Delete(x) => Some(&mut x.children), + Node::MdxJsxFlowElement(x) => Some(&mut x.children), + Node::MdxJsxTextElement(x) => Some(&mut x.children), + // Non-parent. + _ => None, + } + } + + pub fn position(&mut self) -> Option<&Position> { + match self { + Node::Root(x) => x.position.as_ref(), + Node::BlockQuote(x) => x.position.as_ref(), + Node::FootnoteDefinition(x) => x.position.as_ref(), + Node::MdxJsxFlowElement(x) => x.position.as_ref(), + Node::List(x) => x.position.as_ref(), + Node::MdxjsEsm(x) => x.position.as_ref(), + Node::Toml(x) => x.position.as_ref(), + Node::Yaml(x) => x.position.as_ref(), + Node::Break(x) => x.position.as_ref(), + Node::InlineCode(x) => x.position.as_ref(), + Node::InlineMath(x) => x.position.as_ref(), + Node::Delete(x) => x.position.as_ref(), + Node::Emphasis(x) => x.position.as_ref(), + Node::MdxTextExpression(x) => x.position.as_ref(), + Node::FootnoteReference(x) => x.position.as_ref(), + Node::Html(x) => x.position.as_ref(), + Node::Image(x) => x.position.as_ref(), + Node::ImageReference(x) => x.position.as_ref(), + Node::MdxJsxTextElement(x) => x.position.as_ref(), + Node::Link(x) => x.position.as_ref(), + Node::LinkReference(x) => x.position.as_ref(), + Node::Strong(x) => x.position.as_ref(), + Node::Text(x) => x.position.as_ref(), + Node::Code(x) => x.position.as_ref(), + Node::Math(x) => x.position.as_ref(), + Node::MdxFlowExpression(x) => x.position.as_ref(), + Node::Heading(x) => x.position.as_ref(), + Node::Table(x) => x.position.as_ref(), + Node::ThematicBreak(x) => x.position.as_ref(), + Node::TableRow(x) => x.position.as_ref(), + Node::TableCell(x) => x.position.as_ref(), + Node::ListItem(x) => x.position.as_ref(), + Node::Definition(x) => x.position.as_ref(), + Node::Paragraph(x) => x.position.as_ref(), + } + } + + pub fn position_mut(&mut self) -> Option<&mut Position> { + match self { + Node::Root(x) => x.position.as_mut(), + Node::BlockQuote(x) => x.position.as_mut(), + Node::FootnoteDefinition(x) => x.position.as_mut(), + Node::MdxJsxFlowElement(x) => x.position.as_mut(), + Node::List(x) => x.position.as_mut(), + Node::MdxjsEsm(x) => x.position.as_mut(), + Node::Toml(x) => x.position.as_mut(), + Node::Yaml(x) => x.position.as_mut(), + Node::Break(x) => x.position.as_mut(), + Node::InlineCode(x) => x.position.as_mut(), + Node::InlineMath(x) => x.position.as_mut(), + Node::Delete(x) => x.position.as_mut(), + Node::Emphasis(x) => x.position.as_mut(), + Node::MdxTextExpression(x) => x.position.as_mut(), + Node::FootnoteReference(x) => x.position.as_mut(), + Node::Html(x) => x.position.as_mut(), + Node::Image(x) => x.position.as_mut(), + Node::ImageReference(x) => x.position.as_mut(), + Node::MdxJsxTextElement(x) => x.position.as_mut(), + Node::Link(x) => x.position.as_mut(), + Node::LinkReference(x) => x.position.as_mut(), + Node::Strong(x) => x.position.as_mut(), + Node::Text(x) => x.position.as_mut(), + Node::Code(x) => x.position.as_mut(), + Node::Math(x) => x.position.as_mut(), + Node::MdxFlowExpression(x) => x.position.as_mut(), + Node::Heading(x) => x.position.as_mut(), + Node::Table(x) => x.position.as_mut(), + Node::ThematicBreak(x) => x.position.as_mut(), + Node::TableRow(x) => x.position.as_mut(), + Node::TableCell(x) => x.position.as_mut(), + Node::ListItem(x) => x.position.as_mut(), + Node::Definition(x) => x.position.as_mut(), + Node::Paragraph(x) => x.position.as_mut(), + } + } + + pub fn position_set(&mut self, position: Option) { + match self { + Node::Root(x) => x.position = position, + Node::BlockQuote(x) => x.position = position, + Node::FootnoteDefinition(x) => x.position = position, + Node::MdxJsxFlowElement(x) => x.position = position, + Node::List(x) => x.position = position, + Node::MdxjsEsm(x) => x.position = position, + Node::Toml(x) => x.position = position, + Node::Yaml(x) => x.position = position, + Node::Break(x) => x.position = position, + Node::InlineCode(x) => x.position = position, + Node::InlineMath(x) => x.position = position, + Node::Delete(x) => x.position = position, + Node::Emphasis(x) => x.position = position, + Node::MdxTextExpression(x) => x.position = position, + Node::FootnoteReference(x) => x.position = position, + Node::Html(x) => x.position = position, + Node::Image(x) => x.position = position, + Node::ImageReference(x) => x.position = position, + Node::MdxJsxTextElement(x) => x.position = position, + Node::Link(x) => x.position = position, + Node::LinkReference(x) => x.position = position, + Node::Strong(x) => x.position = position, + Node::Text(x) => x.position = position, + Node::Code(x) => x.position = position, + Node::Math(x) => x.position = position, + Node::MdxFlowExpression(x) => x.position = position, + Node::Heading(x) => x.position = position, + Node::Table(x) => x.position = position, + Node::ThematicBreak(x) => x.position = position, + Node::TableRow(x) => x.position = position, + Node::TableCell(x) => x.position = position, + Node::ListItem(x) => x.position = position, + Node::Definition(x) => x.position = position, + Node::Paragraph(x) => x.position = position, + } + } +} + /// MDX: attribute content. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum AttributeContent { - /// MDX: JSX attribute expression. - Expression(MdxJsxExpressionAttribute), - /// MDX: JSX attribute. + /// JSX expression. + /// + /// ```markdown + /// > | + /// ^^^^^^ + /// ``` + Expression(String), + /// JSX property. + /// + /// ```markdown + /// > | + /// ^ + /// ``` Property(MdxJsxAttribute), } /// MDX: attribute value. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum AttributeValue { /// Expression value. - Expression(MdxJsxAttributeValueExpression), + /// + /// ```markdown + /// > | + /// ^^^ + /// ``` + Expression(String), /// Static value. + /// + /// ```markdown + /// > | + /// ^^^ + /// ``` Literal(String), } @@ -268,13 +532,11 @@ pub enum AttributeValue { /// > | a /// ^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Root { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Root`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -285,13 +547,11 @@ pub struct Root { /// > | a /// ^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Paragraph { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Paragraph`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -302,13 +562,11 @@ pub struct Paragraph { /// > | # a /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Heading { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Heading`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Extra. @@ -322,11 +580,9 @@ pub struct Heading { /// > | *** /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct ThematicBreak { // Void. - /// Node type. - pub kind: Kind, // `Kind::ThematicBreak`. /// Positional info. pub position: Option, } @@ -337,13 +593,11 @@ pub struct ThematicBreak { /// > | > a /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct BlockQuote { // Parent. - /// Node type. - pub kind: Kind, // `Kind::BlockQuote`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -354,13 +608,11 @@ pub struct BlockQuote { /// > | * a /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct List { // Parent. - /// Node type. - pub kind: Kind, // `Kind::List`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Extra. @@ -380,13 +632,11 @@ pub struct List { /// > | * a /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct ListItem { // Parent. - /// Node type. - pub kind: Kind, // `Kind::ListItem`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Extra. @@ -404,11 +654,9 @@ pub struct ListItem { /// > | /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Html { // Text. - /// Node type. - pub kind: Kind, // `Kind::Html`. /// Content model. pub value: String, /// Positional info. @@ -421,15 +669,13 @@ pub struct Html { /// > | ~~~ /// ^^^ /// > | a -/// ^^^ +/// ^ /// > | ~~~ /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Code { // Text. - /// Node type. - pub kind: Kind, // `Kind::Code`. /// Content model. pub value: String, /// Positional info. @@ -441,17 +687,37 @@ pub struct Code { pub meta: Option, } +/// Math (flow). +/// +/// ```markdown +/// > | $$ +/// ^^ +/// > | a +/// ^ +/// > | $$ +/// ^^ +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Math { + // Text. + /// Content model. + pub value: String, + /// Positional info. + pub position: Option, + // Extra. + /// Custom info relating to the node. + pub meta: Option, +} + /// Definition. /// /// ```markdown /// > | [a]: b /// ^^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Definition { // Void. - /// Node type. - pub kind: Kind, // `Kind::Definition`. /// Positional info. pub position: Option, // Resource. @@ -481,11 +747,9 @@ pub struct Definition { /// > | a /// ^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Text { // Text. - /// Node type. - pub kind: Kind, // `Kind::Text`. /// Content model. pub value: String, /// Positional info. @@ -498,13 +762,11 @@ pub struct Text { /// > | *a* /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Emphasis { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Emphasis`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -515,13 +777,11 @@ pub struct Emphasis { /// > | **a** /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Strong { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Strong`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -532,11 +792,24 @@ pub struct Strong { /// > | `a` /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct InlineCode { // Text. - /// Node type. - pub kind: Kind, // `Kind::InlineCode`. + /// Content model. + pub value: String, + /// Positional info. + pub position: Option, +} + +/// Math (phrasing). +/// +/// ```markdown +/// > | $a$ +/// ^^^ +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InlineMath { + // Text. /// Content model. pub value: String, /// Positional info. @@ -550,11 +823,9 @@ pub struct InlineCode { /// ^ /// | b /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Break { // Void. - /// Node type. - pub kind: Kind, // `Kind::Break`. /// Positional info. pub position: Option, } @@ -565,13 +836,11 @@ pub struct Break { /// > | [a](b) /// ^^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Link { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Link`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Resource. @@ -588,11 +857,9 @@ pub struct Link { /// > | ![a](b) /// ^^^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Image { // Void. - /// Node type. - pub kind: Kind, // `Kind::Image`. /// Positional info. pub position: Option, // Alternative. @@ -613,13 +880,11 @@ pub struct Image { /// > | [a] /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct LinkReference { // Parent. - /// Node type. - pub kind: Kind, // `Kind::LinkReference`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Reference. @@ -646,11 +911,9 @@ pub struct LinkReference { /// > | ![a] /// ^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct ImageReference { // Void. - /// Node type. - pub kind: Kind, // `Kind::ImageReference`. /// Positional info. pub position: Option, // Alternative. @@ -681,13 +944,11 @@ pub struct ImageReference { /// > | [^a]: b /// ^^^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct FootnoteDefinition { // Parent. - /// Node type. - pub kind: Kind, // `Kind::FootnoteDefinition`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Association. @@ -711,11 +972,9 @@ pub struct FootnoteDefinition { /// > | [^a] /// ^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct FootnoteReference { // Void. - /// Node type. - pub kind: Kind, // `Kind::FootnoteReference`. /// Positional info. pub position: Option, // Association. @@ -741,13 +1000,11 @@ pub struct FootnoteReference { /// > | | - | /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Table { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Table`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // Extra. @@ -761,13 +1018,11 @@ pub struct Table { /// > | | a | /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct TableRow { // Parent. - /// Node type. - pub kind: Kind, // `Kind::TableRow`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -778,13 +1033,11 @@ pub struct TableRow { /// > | | a | /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct TableCell { // Parent. - /// Node type. - pub kind: Kind, // `Kind::TableCell`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -795,13 +1048,11 @@ pub struct TableCell { /// > | ~~a~~ /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Delete { // Parent. - /// Node type. - pub kind: Kind, // `Kind::Delete`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, } @@ -816,11 +1067,9 @@ pub struct Delete { /// > | --- /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Yaml { // Void. - /// Node type. - pub kind: Kind, // `Kind::Yaml`. /// Content model. pub value: String, /// Positional info. @@ -837,11 +1086,9 @@ pub struct Yaml { /// > | +++ /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Toml { // Void. - /// Node type. - pub kind: Kind, // `Kind::Toml`. /// Content model. pub value: String, /// Positional info. @@ -854,11 +1101,9 @@ pub struct Toml { /// > | import a from 'b' /// ^^^^^^^^^^^^^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MdxjsEsm { // Literal. - /// Node type. - pub kind: Kind, // `Kind::MdxjsEsm`. /// Content model. pub value: String, /// Positional info. @@ -871,11 +1116,9 @@ pub struct MdxjsEsm { /// > | {a} /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MdxFlowExpression { // Literal. - /// Node type. - pub kind: Kind, // `Kind::MdxFlowExpression`. /// Content model. pub value: String, /// Positional info. @@ -888,11 +1131,9 @@ pub struct MdxFlowExpression { /// > | a {b} /// ^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MdxTextExpression { // Literal. - /// Node type. - pub kind: Kind, // `Kind::MdxTextExpression`. /// Content model. pub value: String, /// Positional info. @@ -905,13 +1146,11 @@ pub struct MdxTextExpression { /// > | /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MdxJsxFlowElement { // Parent. - /// Node type. - pub kind: Kind, // `Kind::MdxJsxFlowElement`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // JSX element. @@ -929,13 +1168,11 @@ pub struct MdxJsxFlowElement { /// > | . /// ^^^^^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MdxJsxTextElement { // Parent. - /// Node type. - pub kind: Kind, // `Kind::MdxJsxTextElement`. /// Content model. - pub children: Vec, + pub children: Vec, /// Positional info. pub position: Option, // JSX element. @@ -947,59 +1184,23 @@ pub struct MdxJsxTextElement { pub attributes: Vec, } -/// MDX: JSX attribute expression. -/// -/// ```markdown -/// > | -/// ^^^^^^ -/// ``` -#[derive(Clone, Debug)] -pub struct MdxJsxExpressionAttribute { - // Literal. - /// Node type. - pub kind: Kind, // `Kind::MdxJsxExpressionAttribute`. - /// Content model. - pub value: String, - /// Positional info. - pub position: Option, -} - /// MDX: JSX attribute. /// /// ```markdown /// > | /// ^ /// ``` -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MdxJsxAttribute { // Void. - /// Node type. - pub kind: Kind, // `Kind::MdxJsxAttribute`. /// Positional info. - pub position: Option, + // pub position: Option, /// Key. pub name: String, /// Value. pub value: Option, } -/// MDX: JSX attribute value expression. -/// -/// ```markdown -/// > | -/// ^^^ -/// ``` -#[derive(Clone, Debug)] -pub struct MdxJsxAttributeValueExpression { - // Literal. - /// Node type. - pub kind: Kind, // `Kind::MdxJsxAttributeValueExpression`. - /// Content model. - pub value: String, - /// Positional info. - pub position: Option, -} - #[cfg(test)] mod tests { use super::*; @@ -1008,7 +1209,6 @@ mod tests { #[test] fn test() { let text = Text { - kind: Kind::Text, value: "a".to_string(), position: Some(Position { start: Point { @@ -1025,8 +1225,7 @@ mod tests { }; let paragraph = Paragraph { - kind: Kind::Paragraph, - children: vec![PhrasingContent::Text(text)], + children: vec![Node::Text(text)], position: Some(Position { start: Point { line: 1, @@ -1042,6 +1241,6 @@ mod tests { }; assert_eq!(paragraph.children.len(), 1); - assert!(matches!(¶graph.children[0], PhrasingContent::Text(_))); + assert!(matches!(¶graph.children[0], Node::Text(_))); } } diff --git a/src/to_html.rs b/src/to_html.rs index 43be6a7..d7d054d 100644 --- a/src/to_html.rs +++ b/src/to_html.rs @@ -1,10 +1,12 @@ //! Turn events into a string of HTML. use crate::event::{Event, Kind, Name}; +use crate::mdast::AlignKind; use crate::util::{ constant::{SAFE_PROTOCOL_HREF, SAFE_PROTOCOL_SRC}, decode_character_reference::{decode_named, decode_numeric}, encode::encode, gfm_tagfilter::gfm_tagfilter, + infer::{gfm_table_align, list_loose}, normalize_identifier::normalize_identifier, sanitize_uri::{sanitize, sanitize_with_protocols}, skip, @@ -69,107 +71,69 @@ struct Definition { title: Option, } -/// GFM table: column alignment. -// To do: share with `mdast`. -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum GfmTableAlign { - /// No alignment. - /// - /// ```markdown - /// | | aaa | - /// > | | --- | - /// ^^^ - /// ``` - None, - /// Left alignment. - /// - /// ```markdown - /// | | aaa | - /// > | | :-- | - /// ^^^ - /// ``` - Left, - /// Center alignment. - /// - /// ```markdown - /// | | aaa | - /// > | | :-: | - /// ^^^ - /// ``` - Center, - /// Right alignment. - /// - /// ```markdown - /// | | aaa | - /// > | | --: | - /// ^^^ - /// ``` - Right, -} - /// Context used to compile markdown. #[allow(clippy::struct_excessive_bools)] #[derive(Debug)] struct CompileContext<'a> { // Static info. /// List of events. - pub events: &'a [Event], + events: &'a [Event], /// List of bytes. - pub bytes: &'a [u8], + bytes: &'a [u8], /// Configuration. - pub options: &'a Options, + options: &'a Options, // Fields used by handlers to track the things they need to track to // compile markdown. /// Rank of heading (atx). - pub heading_atx_rank: Option, + heading_atx_rank: Option, /// Buffer of heading (setext) text. - pub heading_setext_buffer: Option, + heading_setext_buffer: Option, /// Whether raw (flow) (code (fenced), math (flow)) or code (indented) contains data. - pub raw_flow_seen_data: Option, + raw_flow_seen_data: Option, /// Number of raw (flow) fences. - pub raw_flow_fences_count: Option, + raw_flow_fences_count: Option, /// Whether we are in code (text). - pub raw_text_inside: bool, + raw_text_inside: bool, /// Whether we are in image text. - pub image_alt_inside: bool, + image_alt_inside: bool, /// Marker of character reference. - pub character_reference_marker: Option, + character_reference_marker: Option, /// Whether we are expecting the first list item marker. - pub list_expect_first_marker: Option, + list_expect_first_marker: Option, /// Stack of media (link, image). - pub media_stack: Vec, + media_stack: Vec, /// Stack of containers. - pub tight_stack: Vec, + tight_stack: Vec, /// List of definitions. - pub definitions: Vec, + definitions: Vec, /// List of definitions. - pub gfm_footnote_definitions: Vec<(String, String)>, - pub gfm_footnote_definition_calls: Vec<(String, usize)>, - pub gfm_footnote_definition_stack: Vec<(usize, usize)>, + gfm_footnote_definitions: Vec<(String, String)>, + gfm_footnote_definition_calls: Vec<(String, usize)>, + gfm_footnote_definition_stack: Vec<(usize, usize)>, /// Whether we are in a GFM table head. - pub gfm_table_in_head: bool, + gfm_table_in_head: bool, /// Current GFM table alignment. - pub gfm_table_align: Option>, + gfm_table_align: Option>, /// Current GFM table column. - pub gfm_table_column: usize, + gfm_table_column: usize, // Fields used to influance the current compilation. /// Ignore the next line ending. - pub slurp_one_line_ending: bool, + slurp_one_line_ending: bool, /// Whether to encode HTML. - pub encode_html: bool, + encode_html: bool, // Configuration /// Line ending to use. - pub line_ending_default: LineEnding, + line_ending_default: LineEnding, // Intermediate results. /// Stack of buffers. - pub buffers: Vec, + buffers: Vec, /// Current event index. - pub index: usize, + index: usize, } impl<'a> CompileContext<'a> { /// Create a new compile context. - pub fn new( + fn new( events: &'a [Event], bytes: &'a [u8], options: &'a Options, @@ -205,17 +169,17 @@ impl<'a> CompileContext<'a> { } /// Push a buffer. - pub fn buffer(&mut self) { + fn buffer(&mut self) { self.buffers.push(String::new()); } /// Pop a buffer, returning its value. - pub fn resume(&mut self) -> String { + fn resume(&mut self) -> String { self.buffers.pop().expect("Cannot resume w/o buffer") } /// Push a str to the last buffer. - pub fn push(&mut self, value: &str) { + fn push(&mut self, value: &str) { self.buffers .last_mut() .expect("Cannot push w/o buffer") @@ -223,13 +187,13 @@ impl<'a> CompileContext<'a> { } /// Add a line ending. - pub fn line_ending(&mut self) { + fn line_ending(&mut self) { let eol = self.line_ending_default.as_str().to_string(); self.push(&eol); } /// Add a line ending if needed (as in, there’s no eol/eof already). - pub fn line_ending_if_needed(&mut self) { + fn line_ending_if_needed(&mut self) { let tail = self .buffers .last() @@ -306,7 +270,7 @@ pub fn compile(events: &[Event], bytes: &[u8], options: &Options) -> String { index += 1; } - index = 0; + let mut index = 0; let jump_default = (events.len(), events.len()); let mut definition_index = 0; let mut jump = definition_indices @@ -586,51 +550,7 @@ fn on_enter_gfm_strikethrough(context: &mut CompileContext) { /// Handle [`Enter`][Kind::Enter]:[`GfmTable`][Name::GfmTable]. fn on_enter_gfm_table(context: &mut CompileContext) { - // Find the alignment. - let mut index = context.index; - let mut in_delimiter_row = false; - let mut align = vec![]; - - while index < context.events.len() { - let event = &context.events[index]; - - if in_delimiter_row { - if event.kind == Kind::Enter { - // Start of alignment value: set a new column. - if event.name == Name::GfmTableDelimiterCellValue { - align.push( - if context.events[index + 1].name == Name::GfmTableDelimiterMarker { - GfmTableAlign::Left - } else { - GfmTableAlign::None - }, - ); - } - } else { - // End of alignment value: change the column. - if event.name == Name::GfmTableDelimiterCellValue { - if context.events[index - 1].name == Name::GfmTableDelimiterMarker { - let align_index = align.len() - 1; - align[align_index] = if align[align_index] == GfmTableAlign::Left { - GfmTableAlign::Center - } else { - GfmTableAlign::Right - } - } - } - // Done! - else if event.name == Name::GfmTableDelimiterRow { - break; - } - } - } else if event.kind == Kind::Enter && event.name == Name::GfmTableDelimiterRow { - in_delimiter_row = true; - } - - index += 1; - } - - // Generate. + let align = gfm_table_align(context.events, context.index); context.gfm_table_align = Some(align); context.line_ending_if_needed(); context.push(""); @@ -660,10 +580,10 @@ fn on_enter_gfm_table_cell(context: &mut CompileContext) { } match value { - GfmTableAlign::Left => context.push(" align=\"left\""), - GfmTableAlign::Right => context.push(" align=\"right\""), - GfmTableAlign::Center => context.push(" align=\"center\""), - GfmTableAlign::None => {} + AlignKind::Left => context.push(" align=\"left\""), + AlignKind::Right => context.push(" align=\"right\""), + AlignKind::Center => context.push(" align=\"center\""), + AlignKind::None => {} } context.push(">"); @@ -732,95 +652,12 @@ fn on_enter_link(context: &mut CompileContext) { /// Handle [`Enter`][Kind::Enter]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}. fn on_enter_list(context: &mut CompileContext) { - let events = &context.events; - let mut index = context.index; - let mut balance = 0; - let mut loose = false; - let name = &events[index].name; - - while index < events.len() { - let event = &events[index]; - - if event.kind == Kind::Enter { - balance += 1; - } else { - balance -= 1; - - if balance < 3 && event.name == Name::BlankLineEnding { - // Blank line directly after a prefix: - // - // ```markdown - // > | -␊ - // ^ - // | a - // ``` - let mut at_prefix = false; - // Blank line directly after item, which is just a prefix. - // - // ```markdown - // > | -␊ - // ^ - // | - a - // ``` - let mut at_empty_list_item = false; - // Blank line at block quote prefix: - // - // ```markdown - // > | * >␊ - // ^ - // | * a - // ``` - let mut at_empty_block_quote = false; - - if balance == 1 { - let mut before = index - 2; - - if events[before].name == Name::ListItem { - before -= 1; - - if events[before].name == Name::SpaceOrTab { - before -= 2; - } - - if events[before].name == Name::BlockQuote - && events[before - 1].name == Name::BlockQuotePrefix - { - at_empty_block_quote = true; - } else if events[before].name == Name::ListItemPrefix { - at_empty_list_item = true; - } - } - } else { - let mut before = index - 2; - - if events[before].name == Name::SpaceOrTab { - before -= 2; - } - - if events[before].name == Name::ListItemPrefix { - at_prefix = true; - } - } - - if !at_prefix && !at_empty_list_item && !at_empty_block_quote { - loose = true; - break; - } - } - - // Done. - if balance == 0 && event.name == *name { - break; - } - } - - index += 1; - } - + let loose = list_loose(context.events, context.index, true); context.tight_stack.push(!loose); context.line_ending_if_needed(); + // Note: no `>`. - context.push(if *name == Name::ListOrdered { + context.push(if context.events[context.index].name == Name::ListOrdered { ", + identifier: String, + label: String, +} + +#[derive(Debug, Clone)] +struct JsxTag { + name: Option, + attributes: Vec, + close: bool, + self_closing: bool, + start: Point, + end: Point, +} + +impl Reference { + fn new() -> Reference { + Reference { + // Assume shortcut: removed on a resource, changed on a reference. + reference_kind: Some(ReferenceKind::Shortcut), + identifier: String::new(), + label: String::new(), + } + } +} + +/// Context used to compile markdown. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug)] +struct CompileContext<'a> { + // Static info. + /// List of events. + events: &'a [Event], + /// List of bytes. + bytes: &'a [u8], + // Fields used by handlers to track the things they need to track to + // compile markdown. + character_reference_marker: u8, + gfm_table_inside: bool, + gfm_task_list_item_check_after: bool, + hard_break_after: bool, + heading_setext_text_after: bool, + jsx_tag_stack: Vec, + jsx_tag: Option, + media_reference_stack: Vec, + raw_flow_fence_seen: bool, + // Intermediate results. + /// Primary tree and buffers. + trees: Vec<(Node, Vec, Vec)>, + /// Current event index. + index: usize, +} + +impl<'a> CompileContext<'a> { + /// Create a new compile context. + fn new(events: &'a [Event], bytes: &'a [u8]) -> CompileContext<'a> { + let tree = Node::Root(Root { + children: vec![], + position: Some(Position { + start: if events.is_empty() { + Point::new(1, 1, 0) + } else { + point_from_event(&events[0]) + }, + end: if events.is_empty() { + Point::new(1, 1, 0) + } else { + point_from_event(&events[events.len() - 1]) + }, + }), + }); + + CompileContext { + events, + bytes, + character_reference_marker: 0, + gfm_table_inside: false, + gfm_task_list_item_check_after: false, + hard_break_after: false, + heading_setext_text_after: false, + jsx_tag_stack: vec![], + jsx_tag: None, + media_reference_stack: vec![], + raw_flow_fence_seen: false, + trees: vec![(tree, vec![], vec![])], + index: 0, + } + } + + /// Push a buffer. + fn buffer(&mut self) { + self.trees.push(( + Node::Paragraph(Paragraph { + children: vec![], + position: None, + }), + vec![], + vec![], + )); + } + + /// Pop a buffer, returning its value. + fn resume(&mut self) -> Node { + if let Some((node, stack_a, stack_b)) = self.trees.pop() { + debug_assert_eq!( + stack_a.len(), + 0, + "expected stack (nodes in tree) to be drained" + ); + debug_assert_eq!( + stack_b.len(), + 0, + "expected stack (opening events) to be drained" + ); + node + } else { + unreachable!("Cannot resume w/o buffer") + } + } + + fn tail_mut(&mut self) -> &mut Node { + let (tree, stack, _) = self.trees.last_mut().expect("Cannot get tail w/o tree"); + delve_mut(tree, stack) + } + + fn tail_penultimate_mut(&mut self) -> &mut Node { + let (tree, stack, _) = self.trees.last_mut().expect("Cannot get tail w/o tree"); + delve_mut(tree, &stack[0..(stack.len() - 1)]) + } + + fn tail_push(&mut self, mut child: Node) { + if child.position().is_none() { + child.position_set(Some(position_from_event(&self.events[self.index]))); + } + + let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree"); + let node = delve_mut(tree, stack); + let children = node.children_mut().expect("Cannot push to non-parent"); + let index = children.len(); + children.push(child); + stack.push(index); + event_stack.push(self.index); + } + + fn tail_push_again(&mut self) { + let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree"); + let node = delve_mut(tree, stack); + let children = node.children().expect("Cannot push to non-parent"); + stack.push(children.len() - 1); + event_stack.push(self.index); + } + + fn tail_pop(&mut self) -> Result<(), String> { + let ev = &self.events[self.index]; + let end = point_from_event(ev); + let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree"); + let node = delve_mut(tree, stack); + node.position_mut() + .expect("Cannot pop manually added node") + .end = end; + + stack.pop().unwrap(); + + if let Some(left_index) = event_stack.pop() { + let left = &self.events[left_index]; + if left.name != ev.name { + on_mismatch_error(self, Some(ev), left)?; + } + } else { + return Err(format!( + "{}:{}: Cannot close `{:?}`, it’s not open", + ev.point.line, ev.point.column, ev.name + )); + } + + Ok(()) + } +} /// Turn events and bytes into a syntax tree. -pub fn compile(events: &[Event], _bytes: &[u8], _options: &Options) -> mdast::Root { - mdast::Root { - kind: mdast::Kind::Root, +pub fn compile(events: &[Event], bytes: &[u8]) -> Result { + let mut context = CompileContext::new(events, bytes); + + let mut index = 0; + while index < events.len() { + handle(&mut context, index)?; + index += 1; + } + + debug_assert_eq!(context.trees.len(), 1, "expected 1 final tree"); + let (tree, _, event_stack) = context.trees.pop().unwrap(); + + if let Some(index) = event_stack.last() { + let event = &events[*index]; + on_mismatch_error(&mut context, None, event)?; + } + + Ok(tree) +} + +/// Handle the event at `index`. +fn handle(context: &mut CompileContext, index: usize) -> Result<(), String> { + context.index = index; + + if context.events[index].kind == Kind::Enter { + enter(context)?; + } else { + exit(context)?; + } + + Ok(()) +} + +/// Handle [`Enter`][Kind::Enter]. +fn enter(context: &mut CompileContext) -> Result<(), String> { + match context.events[context.index].name { + Name::AutolinkEmail + | Name::AutolinkProtocol + | Name::CharacterEscapeValue + | Name::CharacterReference + | Name::CodeFlowChunk + | Name::CodeTextData + | Name::Data + | Name::FrontmatterChunk + | Name::HtmlFlowData + | Name::HtmlTextData + | Name::MathFlowChunk + | Name::MathTextData + | Name::MdxExpressionData + | Name::MdxEsmData + | Name::MdxJsxTagAttributeValueLiteralValue => on_enter_data(context), + Name::CodeFencedFenceInfo + | Name::CodeFencedFenceMeta + | Name::DefinitionDestinationString + | Name::DefinitionLabelString + | Name::DefinitionTitleString + | Name::GfmFootnoteDefinitionLabelString + | Name::LabelText + | Name::MathFlowFenceMeta + | Name::MdxJsxTagAttributeValueLiteral + | Name::MdxJsxTagAttributeValueExpression + | Name::ReferenceString + | Name::ResourceDestinationString + | Name::ResourceTitleString => on_enter_buffer(context), + Name::Autolink => on_enter_autolink(context), + Name::BlockQuote => on_enter_block_quote(context), + Name::CodeFenced => on_enter_code_fenced(context), + Name::CodeIndented => on_enter_code_indented(context), + Name::CodeText => on_enter_code_text(context), + Name::Definition => on_enter_definition(context), + Name::Emphasis => on_enter_emphasis(context), + Name::Frontmatter => on_enter_frontmatter(context), + Name::GfmAutolinkLiteralEmail + | Name::GfmAutolinkLiteralMailto + | Name::GfmAutolinkLiteralProtocol + | Name::GfmAutolinkLiteralWww + | Name::GfmAutolinkLiteralXmpp => on_enter_gfm_autolink_literal(context), + Name::GfmFootnoteCall => on_enter_gfm_footnote_call(context), + Name::GfmFootnoteDefinition => on_enter_gfm_footnote_definition(context), + Name::GfmStrikethrough => on_enter_gfm_strikethrough(context), + Name::GfmTable => on_enter_gfm_table(context), + Name::GfmTableRow => on_enter_gfm_table_row(context), + Name::GfmTableCell => on_enter_gfm_table_cell(context), + Name::HardBreakEscape | Name::HardBreakTrailing => on_enter_hard_break(context), + Name::HeadingAtx | Name::HeadingSetext => on_enter_heading(context), + Name::HtmlFlow | Name::HtmlText => on_enter_html(context), + Name::Image => on_enter_image(context), + Name::Link => on_enter_link(context), + Name::ListItem => on_enter_list_item(context), + Name::ListOrdered | Name::ListUnordered => on_enter_list(context), + Name::MathFlow => on_enter_math_flow(context), + Name::MathText => on_enter_math_text(context), + Name::MdxEsm => on_enter_mdx_esm(context), + Name::MdxFlowExpression => on_enter_mdx_flow_expression(context), + Name::MdxTextExpression => on_enter_mdx_text_expression(context), + Name::MdxJsxFlowTag | Name::MdxJsxTextTag => on_enter_mdx_jsx_tag(context), + Name::MdxJsxTagClosingMarker => on_enter_mdx_jsx_tag_closing_marker(context)?, + Name::MdxJsxTagAttribute => on_enter_mdx_jsx_tag_attribute(context)?, + Name::MdxJsxTagAttributeExpression => on_enter_mdx_jsx_tag_attribute_expression(context)?, + Name::MdxJsxTagSelfClosingMarker => on_enter_mdx_jsx_tag_self_closing_marker(context)?, + Name::Paragraph => on_enter_paragraph(context), + Name::Reference => on_enter_reference(context), + Name::Resource => on_enter_resource(context), + Name::Strong => on_enter_strong(context), + Name::ThematicBreak => on_enter_thematic_break(context), + _ => {} + } + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]. +fn exit(context: &mut CompileContext) -> Result<(), String> { + match context.events[context.index].name { + Name::Autolink + | Name::BlockQuote + | Name::CharacterReference + | Name::Definition + | Name::Emphasis + | Name::GfmFootnoteDefinition + | Name::GfmStrikethrough + | Name::GfmTableRow + | Name::GfmTableCell + | Name::HeadingAtx + | Name::ListItem + | Name::ListOrdered + | Name::ListUnordered + | Name::Paragraph + | Name::Strong + | Name::ThematicBreak => { + on_exit(context)?; + } + Name::CharacterEscapeValue + | Name::CodeFlowChunk + | Name::CodeTextData + | Name::FrontmatterChunk + | Name::HtmlFlowData + | Name::HtmlTextData + | Name::MathFlowChunk + | Name::MathTextData + | Name::MdxExpressionData + | Name::MdxEsmData + | Name::MdxJsxTagAttributeValueLiteralValue => { + on_exit_data(context)?; + } + Name::AutolinkProtocol => on_exit_autolink_protocol(context)?, + Name::AutolinkEmail => on_exit_autolink_email(context)?, + Name::CharacterReferenceMarker => on_exit_character_reference_marker(context), + Name::CharacterReferenceMarkerNumeric => { + on_exit_character_reference_marker_numeric(context); + } + Name::CharacterReferenceMarkerHexadecimal => { + on_exit_character_reference_marker_hexadecimal(context); + } + Name::CharacterReferenceValue => on_exit_character_reference_value(context), + Name::CodeFencedFenceInfo => on_exit_code_fenced_fence_info(context), + Name::CodeFencedFenceMeta | Name::MathFlowFenceMeta => on_exit_raw_flow_fence_meta(context), + Name::CodeFencedFence | Name::MathFlowFence => on_exit_raw_flow_fence(context), + Name::CodeFenced | Name::MathFlow => on_exit_raw_flow(context)?, + Name::CodeIndented => on_exit_code_indented(context)?, + Name::CodeText | Name::MathText => on_exit_raw_text(context)?, + Name::Data => on_exit_data_actual(context)?, + Name::DefinitionDestinationString => on_exit_definition_destination_string(context), + Name::DefinitionLabelString | Name::GfmFootnoteDefinitionLabelString => { + on_exit_definition_id(context); + } + Name::DefinitionTitleString => on_exit_definition_title_string(context), + Name::Frontmatter => on_exit_frontmatter(context)?, + Name::GfmAutolinkLiteralEmail + | Name::GfmAutolinkLiteralMailto + | Name::GfmAutolinkLiteralProtocol + | Name::GfmAutolinkLiteralWww + | Name::GfmAutolinkLiteralXmpp => on_exit_gfm_autolink_literal(context)?, + Name::GfmFootnoteCall | Name::Image | Name::Link => on_exit_media(context)?, + Name::GfmTable => on_exit_gfm_table(context)?, + Name::GfmTaskListItemCheck => on_exit_gfm_task_list_item_check(context), + Name::GfmTaskListItemValueUnchecked | Name::GfmTaskListItemValueChecked => { + on_exit_gfm_task_list_item_value(context); + } + Name::HardBreakEscape | Name::HardBreakTrailing => on_exit_hard_break(context)?, + Name::HeadingAtxSequence => on_exit_heading_atx_sequence(context), + Name::HeadingSetext => on_exit_heading_setext(context)?, + Name::HeadingSetextUnderlineSequence => on_exit_heading_setext_underline_sequence(context), + Name::HeadingSetextText => on_exit_heading_setext_text(context), + Name::HtmlFlow + | Name::HtmlText + | Name::MdxEsm + | Name::MdxFlowExpression + | Name::MdxTextExpression => on_exit_literal(context)?, + Name::LabelText => on_exit_label_text(context), + Name::LineEnding => on_exit_line_ending(context)?, + Name::ListItemValue => on_exit_list_item_value(context), + Name::MdxJsxFlowTag | Name::MdxJsxTextTag => on_exit_mdx_jsx_tag(context)?, + Name::MdxJsxTagClosingMarker => on_exit_mdx_jsx_tag_closing_marker(context), + Name::MdxJsxTagNamePrimary => on_exit_mdx_jsx_tag_name_primary(context), + Name::MdxJsxTagNameMember => on_exit_mdx_jsx_tag_name_member(context), + Name::MdxJsxTagNameLocal => on_exit_mdx_jsx_tag_name_local(context), + Name::MdxJsxTagAttributeExpression => on_exit_mdx_jsx_tag_attribute_expression(context), + Name::MdxJsxTagAttributePrimaryName => on_exit_mdx_jsx_tag_attribute_primary_name(context), + Name::MdxJsxTagAttributeNameLocal => on_exit_mdx_jsx_tag_attribute_name_local(context), + Name::MdxJsxTagAttributeValueLiteral => { + on_exit_mdx_jsx_tag_attribute_value_literal(context); + } + Name::MdxJsxTagAttributeValueExpression => { + on_exit_mdx_jsx_tag_attribute_value_expression(context); + } + Name::MdxJsxTagSelfClosingMarker => on_exit_mdx_jsx_tag_self_closing_marker(context), + + Name::ReferenceString => on_exit_reference_string(context), + Name::ResourceDestinationString => on_exit_resource_destination_string(context), + Name::ResourceTitleString => on_exit_resource_title_string(context), + _ => {} + } + + Ok(()) +} + +/// Handle [`Enter`][Kind::Enter]:`*`. +fn on_enter_buffer(context: &mut CompileContext) { + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`Data`][Name::Data] (and many text things). +fn on_enter_data(context: &mut CompileContext) { + let parent = context.tail_mut(); + let children = parent.children_mut().expect("expected parent"); + + // Add to stack again. + if let Some(Node::Text(_)) = children.last_mut() { + context.tail_push_again(); + } else { + context.tail_push(Node::Text(Text { + value: String::new(), + position: None, + })); + } +} + +/// Handle [`Enter`][Kind::Enter]:[`Autolink`][Name::Autolink]. +fn on_enter_autolink(context: &mut CompileContext) { + context.tail_push(Node::Link(Link { + url: String::new(), + title: None, children: vec![], - position: Some(mdast::Position { - start: if events.is_empty() { - create_point(1, 1, 0) - } else { - point_from_event(&events[0]) - }, - end: if events.is_empty() { - create_point(1, 1, 0) - } else { - point_from_event(&events[events.len() - 1]) - }, - }), + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`BlockQuote`][Name::BlockQuote]. +fn on_enter_block_quote(context: &mut CompileContext) { + context.tail_push(Node::BlockQuote(BlockQuote { + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`CodeFenced`][Name::CodeFenced]. +fn on_enter_code_fenced(context: &mut CompileContext) { + context.tail_push(Node::Code(Code { + lang: None, + meta: None, + value: String::new(), + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`CodeIndented`][Name::CodeIndented]. +fn on_enter_code_indented(context: &mut CompileContext) { + on_enter_code_fenced(context); + on_enter_buffer(context); +} + +/// Handle [`Enter`][Kind::Enter]:[`CodeText`][Name::CodeText]. +fn on_enter_code_text(context: &mut CompileContext) { + context.tail_push(Node::InlineCode(InlineCode { + value: String::new(), + position: None, + })); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`MathText`][Name::MathText]. +fn on_enter_math_text(context: &mut CompileContext) { + context.tail_push(Node::InlineMath(InlineMath { + value: String::new(), + position: None, + })); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`MdxEsm`][Name::MdxEsm]. +fn on_enter_mdx_esm(context: &mut CompileContext) { + context.tail_push(Node::MdxjsEsm(MdxjsEsm { + value: String::new(), + position: None, + })); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`MdxFlowExpression`][Name::MdxFlowExpression]. +fn on_enter_mdx_flow_expression(context: &mut CompileContext) { + context.tail_push(Node::MdxFlowExpression(MdxFlowExpression { + value: String::new(), + position: None, + })); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`MdxTextExpression`][Name::MdxTextExpression]. +fn on_enter_mdx_text_expression(context: &mut CompileContext) { + context.tail_push(Node::MdxTextExpression(MdxTextExpression { + value: String::new(), + position: None, + })); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`Definition`][Name::Definition]. +fn on_enter_definition(context: &mut CompileContext) { + context.tail_push(Node::Definition(Definition { + url: String::new(), + identifier: String::new(), + label: None, + title: None, + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`Emphasis`][Name::Emphasis]. +fn on_enter_emphasis(context: &mut CompileContext) { + context.tail_push(Node::Emphasis(Emphasis { + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:{[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail],[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto],[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol],[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww],[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp]}. +fn on_enter_gfm_autolink_literal(context: &mut CompileContext) { + on_enter_autolink(context); + on_enter_data(context); +} + +/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteCall`][Name::GfmFootnoteCall]. +fn on_enter_gfm_footnote_call(context: &mut CompileContext) { + context.tail_push(Node::FootnoteReference(FootnoteReference { + identifier: String::new(), + label: None, + position: None, + })); + context.media_reference_stack.push(Reference::new()); +} + +/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition]. +fn on_enter_gfm_footnote_definition(context: &mut CompileContext) { + context.tail_push(Node::FootnoteDefinition(FootnoteDefinition { + identifier: String::new(), + label: None, + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`GfmStrikethrough`][Name::GfmStrikethrough]. +fn on_enter_gfm_strikethrough(context: &mut CompileContext) { + context.tail_push(Node::Delete(Delete { + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`GfmTable`][Name::GfmTable]. +fn on_enter_gfm_table(context: &mut CompileContext) { + let align = gfm_table_align(context.events, context.index); + context.tail_push(Node::Table(Table { + align, + children: vec![], + position: None, + })); + context.gfm_table_inside = true; +} + +/// Handle [`Enter`][Kind::Enter]:[`GfmTableRow`][Name::GfmTableRow]. +fn on_enter_gfm_table_row(context: &mut CompileContext) { + context.tail_push(Node::TableRow(TableRow { + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`GfmTableCell`][Name::GfmTableCell]. +fn on_enter_gfm_table_cell(context: &mut CompileContext) { + context.tail_push(Node::TableCell(TableCell { + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`HardBreakEscape`][Name::HardBreakEscape]. +fn on_enter_hard_break(context: &mut CompileContext) { + context.tail_push(Node::Break(Break { position: None })); +} + +/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter]. +fn on_enter_frontmatter(context: &mut CompileContext) { + let index = context.events[context.index].point.index; + let byte = context.bytes[index]; + let node = if byte == b'+' { + Node::Toml(Toml { + value: String::new(), + position: None, + }) + } else { + Node::Yaml(Yaml { + value: String::new(), + position: None, + }) + }; + + context.tail_push(node); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`Reference`][Name::Reference]. +fn on_enter_reference(context: &mut CompileContext) { + let reference = context + .media_reference_stack + .last_mut() + .expect("expected reference on media stack"); + // Assume collapsed. + // If there’s a string after it, we set `Full`. + reference.reference_kind = Some(ReferenceKind::Collapsed); +} + +/// Handle [`Enter`][Kind::Enter]:[`Resource`][Name::Resource]. +fn on_enter_resource(context: &mut CompileContext) { + let reference = context + .media_reference_stack + .last_mut() + .expect("expected reference on media stack"); + // It’s not a reference. + reference.reference_kind = None; +} + +/// Handle [`Enter`][Kind::Enter]:[`Strong`][Name::Strong]. +fn on_enter_strong(context: &mut CompileContext) { + context.tail_push(Node::Strong(Strong { + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`ThematicBreak`][Name::ThematicBreak]. +fn on_enter_thematic_break(context: &mut CompileContext) { + context.tail_push(Node::ThematicBreak(ThematicBreak { position: None })); +} + +/// Handle [`Enter`][Kind::Enter]:[`HeadingAtx`][Name::HeadingAtx]. +fn on_enter_heading(context: &mut CompileContext) { + context.tail_push(Node::Heading(Heading { + depth: 0, // Will be set later. + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:{[`HtmlFlow`][Name::HtmlFlow],[`HtmlText`][Name::HtmlText]}. +fn on_enter_html(context: &mut CompileContext) { + context.tail_push(Node::Html(Html { + value: String::new(), + position: None, + })); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`Image`][Name::Image]. +fn on_enter_image(context: &mut CompileContext) { + context.tail_push(Node::Image(Image { + url: String::new(), + title: None, + alt: String::new(), + position: None, + })); + context.media_reference_stack.push(Reference::new()); +} + +/// Handle [`Enter`][Kind::Enter]:[`Link`][Name::Link]. +fn on_enter_link(context: &mut CompileContext) { + context.tail_push(Node::Link(Link { + url: String::new(), + title: None, + children: vec![], + position: None, + })); + context.media_reference_stack.push(Reference::new()); +} + +/// Handle [`Enter`][Kind::Enter]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}. +fn on_enter_list(context: &mut CompileContext) { + let ordered = context.events[context.index].name == Name::ListOrdered; + let spread = list_loose(context.events, context.index, false); + + context.tail_push(Node::List(List { + ordered, + spread, + start: None, + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`ListItem`][Name::ListItem]. +fn on_enter_list_item(context: &mut CompileContext) { + let spread = list_item_loose(context.events, context.index); + + context.tail_push(Node::ListItem(ListItem { + spread, + checked: None, + children: vec![], + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:[`MathFlow`][Name::MathFlow]. +fn on_enter_math_flow(context: &mut CompileContext) { + context.tail_push(Node::Math(Math { + meta: None, + value: String::new(), + position: None, + })); +} + +/// Handle [`Enter`][Kind::Enter]:{[`MdxJsxFlowTag`][Name::MdxJsxFlowTag],[`MdxJsxTextTag`][Name::MdxJsxTextTag]}. +fn on_enter_mdx_jsx_tag(context: &mut CompileContext) { + let point = point_from_event(&context.events[context.index]); + context.jsx_tag = Some(JsxTag { + name: None, + attributes: vec![], + start: point.clone(), + end: point, + close: false, + self_closing: false, + }); + context.buffer(); +} + +/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagClosingMarker`][Name::MdxJsxTagClosingMarker]. +fn on_enter_mdx_jsx_tag_closing_marker(context: &mut CompileContext) -> Result<(), String> { + if context.jsx_tag_stack.is_empty() { + let event = &context.events[context.index]; + Err(format!( + "{}:{}: Unexpected closing slash `/` in tag, expected an open tag first (mdx-jsx:unexpected-closing-slash)", + event.point.line, + event.point.column, + )) + } else { + Ok(()) + } +} + +/// Handle [`Enter`][Kind::Enter]:{[`MdxJsxTagAttribute`][Name::MdxJsxTagAttribute],[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression]}. +fn on_enter_mdx_jsx_tag_any_attribute(context: &mut CompileContext) -> Result<(), String> { + if context.jsx_tag.as_ref().expect("expected tag").close { + let event = &context.events[context.index]; + Err(format!( + "{}:{}: Unexpected attribute in closing tag, expected the end of the tag (mdx-jsx:unexpected-attribute)", + event.point.line, + event.point.column, + )) + } else { + Ok(()) } } -fn point_from_event(event: &Event) -> mdast::Point { - create_point(event.point.line, event.point.column, event.point.index) +/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagAttribute`][Name::MdxJsxTagAttribute]. +fn on_enter_mdx_jsx_tag_attribute(context: &mut CompileContext) -> Result<(), String> { + on_enter_mdx_jsx_tag_any_attribute(context)?; + + context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .push(AttributeContent::Property(MdxJsxAttribute { + name: String::new(), + value: None, + })); + + Ok(()) } -fn create_point(line: usize, column: usize, offset: usize) -> mdast::Point { - mdast::Point { - line, - column, - offset, +/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression]. +fn on_enter_mdx_jsx_tag_attribute_expression(context: &mut CompileContext) -> Result<(), String> { + on_enter_mdx_jsx_tag_any_attribute(context)?; + + context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .push(AttributeContent::Expression(String::new())); + + context.buffer(); + + Ok(()) +} + +/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagSelfClosingMarker`][Name::MdxJsxTagSelfClosingMarker]. +fn on_enter_mdx_jsx_tag_self_closing_marker(context: &mut CompileContext) -> Result<(), String> { + let tag = context.jsx_tag.as_ref().expect("expected tag"); + if tag.close { + let event = &context.events[context.index]; + Err(format!( + "{}:{}: Unexpected self-closing slash `/` in closing tag, expected the end of the tag (mdx-jsx:unexpected-self-closing-slash)", + event.point.line, + event.point.column, + )) + } else { + Ok(()) } } + +/// Handle [`Enter`][Kind::Enter]:[`Paragraph`][Name::Paragraph]. +fn on_enter_paragraph(context: &mut CompileContext) { + context.tail_push(Node::Paragraph(Paragraph { + children: vec![], + position: None, + })); +} + +/// Handle [`Exit`][Kind::Exit]:`*`. +fn on_exit(context: &mut CompileContext) -> Result<(), String> { + context.tail_pop()?; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`AutolinkProtocol`][Name::AutolinkProtocol]. +fn on_exit_autolink_protocol(context: &mut CompileContext) -> Result<(), String> { + on_exit_data(context)?; + let value = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + if let Node::Link(link) = context.tail_mut() { + link.url.push_str(value.as_str()); + } else { + unreachable!("expected link on stack"); + } + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`AutolinkEmail`][Name::AutolinkEmail]. +fn on_exit_autolink_email(context: &mut CompileContext) -> Result<(), String> { + on_exit_data(context)?; + let value = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + if let Node::Link(link) = context.tail_mut() { + link.url.push_str("mailto:"); + link.url.push_str(value.as_str()); + } else { + unreachable!("expected link on stack"); + } + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarker`][Name::CharacterReferenceMarker]. +fn on_exit_character_reference_marker(context: &mut CompileContext) { + context.character_reference_marker = b'&'; +} + +/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerHexadecimal`][Name::CharacterReferenceMarkerHexadecimal]. +fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) { + context.character_reference_marker = b'x'; +} + +/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerNumeric`][Name::CharacterReferenceMarkerNumeric]. +fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) { + context.character_reference_marker = b'#'; +} + +/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceValue`][Name::CharacterReferenceValue]. +fn on_exit_character_reference_value(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let value = slice.as_str(); + + let value = match context.character_reference_marker { + b'#' => decode_numeric(value, 10), + b'x' => decode_numeric(value, 16), + b'&' => decode_named(value), + _ => panic!("impossible"), + }; + + if let Node::Text(node) = context.tail_mut() { + node.value.push_str(value.as_str()); + } else { + unreachable!("expected text on stack"); + } + + context.character_reference_marker = 0; +} + +/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFenceInfo`][Name::CodeFencedFenceInfo]. +fn on_exit_code_fenced_fence_info(context: &mut CompileContext) { + let value = context.resume().to_string(); + if let Node::Code(node) = context.tail_mut() { + node.lang = Some(value); + } else { + unreachable!("expected code on stack"); + } +} + +/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFenceMeta`][Name::CodeFencedFenceMeta],[`MathFlowFenceMeta`][Name::MathFlowFenceMeta]}. +fn on_exit_raw_flow_fence_meta(context: &mut CompileContext) { + let value = context.resume().to_string(); + match context.tail_mut() { + Node::Code(node) => node.meta = Some(value), + Node::Math(node) => node.meta = Some(value), + _ => { + unreachable!("expected code or math on stack"); + } + } +} + +/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFence`][Name::CodeFencedFence],[`MathFlowFence`][Name::MathFlowFence]}. +fn on_exit_raw_flow_fence(context: &mut CompileContext) { + if context.raw_flow_fence_seen { + // Second fence, ignore. + } else { + context.buffer(); + context.raw_flow_fence_seen = true; + } +} + +/// Handle [`Exit`][Kind::Exit]:{[`CodeFenced`][Name::CodeFenced],[`MathFlow`][Name::MathFlow]}. +fn on_exit_raw_flow(context: &mut CompileContext) -> Result<(), String> { + let value = trim_eol(context.resume().to_string(), true, true); + + match context.tail_mut() { + Node::Code(node) => node.value = value, + Node::Math(node) => node.value = value, + _ => unreachable!("expected code or math on stack for value"), + } + + on_exit(context)?; + context.raw_flow_fence_seen = false; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`CodeIndented`][Name::CodeIndented]. +fn on_exit_code_indented(context: &mut CompileContext) -> Result<(), String> { + let value = context.resume().to_string(); + + if let Node::Code(node) = context.tail_mut() { + node.value = trim_eol(value, false, true); + } else { + unreachable!("expected code on stack for value"); + } + on_exit(context)?; + context.raw_flow_fence_seen = false; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}. +fn on_exit_raw_text(context: &mut CompileContext) -> Result<(), String> { + let mut value = context.resume().to_string(); + + // To do: share with `to_html`. + // If we are in a GFM table, we need to decode escaped pipes. + // This is a rather weird GFM feature. + if context.gfm_table_inside { + let mut bytes = value.as_bytes().to_vec(); + let mut index = 0; + let mut len = bytes.len(); + let mut replace = false; + + while index < len { + if index + 1 < len && bytes[index] == b'\\' && bytes[index + 1] == b'|' { + replace = true; + bytes.remove(index); + len -= 1; + } + + index += 1; + } + + if replace { + value = str::from_utf8(&bytes).unwrap().to_string(); + } + } + + match context.tail_mut() { + Node::InlineCode(node) => node.value = value, + Node::InlineMath(node) => node.value = value, + _ => unreachable!("expected inline code or math on stack for value"), + } + + on_exit(context)?; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`Data`][Name::Data] (and many text things). +fn on_exit_data(context: &mut CompileContext) -> Result<(), String> { + let value = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + if let Node::Text(text) = context.tail_mut() { + text.value.push_str(value.as_str()); + } else { + unreachable!("expected text on stack"); + } + on_exit(context)?; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`Data`][Name::Data] itself. +fn on_exit_data_actual(context: &mut CompileContext) -> Result<(), String> { + on_exit_data(context)?; + + // This field is set when a check exits. + // When that’s the case, there’s always a `data` event right after it. + // That data event is the first child (after the check) of the paragraph. + // We update the text positional info (from the already fixed paragraph), + // and remove the first byte, which is always a space or tab. + if context.gfm_task_list_item_check_after { + let parent = context.tail_mut(); + let start = parent.position().unwrap().start.clone(); + let node = parent.children_mut().unwrap().last_mut().unwrap(); + node.position_mut().unwrap().start = start; + if let Node::Text(node) = node { + node.value.remove(0); + } + context.gfm_task_list_item_check_after = false; + } + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`DefinitionDestinationString`][Name::DefinitionDestinationString]. +fn on_exit_definition_destination_string(context: &mut CompileContext) { + let value = context.resume().to_string(); + if let Node::Definition(node) = context.tail_mut() { + node.url = value; + } else { + unreachable!("expected definition on stack"); + } +} + +/// Handle [`Exit`][Kind::Exit]:{[`DefinitionLabelString`][Name::DefinitionLabelString],[`GfmFootnoteDefinitionLabelString`][Name::GfmFootnoteDefinitionLabelString]}. +fn on_exit_definition_id(context: &mut CompileContext) { + let label = context.resume().to_string(); + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let identifier = normalize_identifier(slice.as_str()).to_lowercase(); + + match context.tail_mut() { + Node::Definition(node) => { + node.label = Some(label); + node.identifier = identifier; + } + Node::FootnoteDefinition(node) => { + node.label = Some(label); + node.identifier = identifier; + } + _ => unreachable!("expected definition or footnote definition on stack"), + } +} + +/// Handle [`Exit`][Kind::Exit]:[`DefinitionTitleString`][Name::DefinitionTitleString]. +fn on_exit_definition_title_string(context: &mut CompileContext) { + let value = context.resume().to_string(); + if let Node::Definition(node) = context.tail_mut() { + node.title = Some(value); + } else { + unreachable!("expected definition on stack"); + } +} + +/// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter]. +fn on_exit_frontmatter(context: &mut CompileContext) -> Result<(), String> { + let value = trim_eol(context.resume().to_string(), true, true); + + match context.tail_mut() { + Node::Yaml(node) => node.value = value, + Node::Toml(node) => node.value = value, + _ => unreachable!("expected yaml/toml on stack for value"), + } + + on_exit(context)?; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:{[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail],[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto],[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol],[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww],[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp]}. +fn on_exit_gfm_autolink_literal(context: &mut CompileContext) -> Result<(), String> { + on_exit_data(context)?; + + let value = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + + let prefix = match &context.events[context.index].name { + Name::GfmAutolinkLiteralEmail => Some("mailto:"), + Name::GfmAutolinkLiteralWww => Some("http://"), + // `GfmAutolinkLiteralMailto`, `GfmAutolinkLiteralProtocol`, `GfmAutolinkLiteralXmpp`. + _ => None, + }; + + if let Node::Link(link) = context.tail_mut() { + if let Some(prefix) = prefix { + link.url.push_str(prefix); + } + link.url.push_str(value.as_str()); + } else { + unreachable!("expected link on stack"); + } + + on_exit(context)?; + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`GfmTable`][Name::GfmTable]. +fn on_exit_gfm_table(context: &mut CompileContext) -> Result<(), String> { + on_exit(context)?; + context.gfm_table_inside = false; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck]. +fn on_exit_gfm_task_list_item_check(context: &mut CompileContext) { + // This field is set when a check exits. + // When that’s the case, there’s always a `data` event right after it. + // That data event is the first child (after the check) of the paragraph. + // We update the paragraph positional info to start after the check. + let mut start = point_from_event(&context.events[context.index]); + debug_assert!( + matches!(context.bytes[start.offset], b'\t' | b' '), + "expected tab or space after check" + ); + start.column += 1; + start.offset += 1; + context.tail_mut().position_mut().unwrap().start = start; + context.gfm_task_list_item_check_after = true; +} + +/// Handle [`Exit`][Kind::Exit]:{[`GfmTaskListItemValueChecked`][Name::GfmTaskListItemValueChecked],[`GfmTaskListItemValueUnchecked`][Name::GfmTaskListItemValueUnchecked]}. +fn on_exit_gfm_task_list_item_value(context: &mut CompileContext) { + let checked = context.events[context.index].name == Name::GfmTaskListItemValueChecked; + let ancestor = context.tail_penultimate_mut(); + + if let Node::ListItem(node) = ancestor { + node.checked = Some(checked); + } else { + unreachable!("expected list item on stack"); + } +} + +/// Handle [`Exit`][Kind::Exit]:{[`HardBreakEscape`][Name::HardBreakEscape],[`HardBreakTrailing`][Name::HardBreakTrailing]}. +fn on_exit_hard_break(context: &mut CompileContext) -> Result<(), String> { + on_exit(context)?; + context.hard_break_after = true; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxSequence`][Name::HeadingAtxSequence]. +fn on_exit_heading_atx_sequence(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + + if let Node::Heading(node) = context.tail_mut() { + if node.depth == 0 { + #[allow(clippy::cast_possible_truncation)] + let depth = slice.len() as u8; + node.depth = depth; + } + } else { + unreachable!("expected heading on stack"); + } +} + +/// Handle [`Exit`][Kind::Exit]:[`HeadingSetext`][Name::HeadingSetext]. +fn on_exit_heading_setext(context: &mut CompileContext) -> Result<(), String> { + context.heading_setext_text_after = false; + on_exit(context)?; + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextText`][Name::HeadingSetextText]. +fn on_exit_heading_setext_text(context: &mut CompileContext) { + context.heading_setext_text_after = true; +} + +/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextUnderlineSequence`][Name::HeadingSetextUnderlineSequence]. +fn on_exit_heading_setext_underline_sequence(context: &mut CompileContext) { + let head = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ) + .head(); + let depth = if head == Some(b'-') { 2 } else { 1 }; + + if let Node::Heading(node) = context.tail_mut() { + node.depth = depth; + } else { + unreachable!("expected heading on stack"); + } +} + +/// Handle [`Exit`][Kind::Exit]:[`LabelText`][Name::LabelText]. +fn on_exit_label_text(context: &mut CompileContext) { + let mut fragment = context.resume(); + let label = fragment.to_string(); + let children = fragment.children_mut().unwrap().split_off(0); + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let identifier = normalize_identifier(slice.as_str()).to_lowercase(); + + let reference = context + .media_reference_stack + .last_mut() + .expect("expected reference on media stack"); + reference.label = label.clone(); + reference.identifier = identifier; + + match context.tail_mut() { + Node::Link(node) => node.children = children, + Node::Image(node) => node.alt = label, + Node::FootnoteReference(_) => {} + _ => unreachable!("expected footnote refereence, image, or link on stack"), + } +} + +/// Handle [`Exit`][Kind::Exit]:[`LineEnding`][Name::LineEnding]. +fn on_exit_line_ending(context: &mut CompileContext) -> Result<(), String> { + if context.heading_setext_text_after { + // Ignore. + } + // Line ending position after hard break is part of it. + else if context.hard_break_after { + let end = point_from_event(&context.events[context.index]); + let node = context.tail_mut(); + let tail = node + .children_mut() + .expect("expected parent") + .last_mut() + .expect("expected tail (break)"); + tail.position_mut().unwrap().end = end; + context.hard_break_after = false; + } + // Line ending is a part of nodes that accept phrasing. + else if matches!( + context.tail_mut(), + Node::Emphasis(_) + | Node::Heading(_) + | Node::Paragraph(_) + | Node::Strong(_) + | Node::Delete(_) + ) { + context.index -= 1; + on_enter_data(context); + context.index += 1; + on_exit_data(context)?; + } + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`MdxFlowExpression`][Name::MdxFlowExpression],etc}. +fn on_exit_literal(context: &mut CompileContext) -> Result<(), String> { + let value = context.resume().to_string(); + + match context.tail_mut() { + Node::Html(node) => node.value = value, + Node::MdxFlowExpression(node) => node.value = value, + Node::MdxTextExpression(node) => node.value = value, + Node::MdxjsEsm(node) => node.value = value, + _ => unreachable!("expected html, mdx expression, etc on stack for value"), + } + + on_exit(context)?; + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:{[`GfmFootnoteCall`][Name::GfmFootnoteCall],[`Image`][Name::Image],[`Link`][Name::Link]}. +fn on_exit_media(context: &mut CompileContext) -> Result<(), String> { + let reference = context + .media_reference_stack + .pop() + .expect("expected reference on media stack"); + on_exit(context)?; + + // It’s a reference. + if let Some(kind) = reference.reference_kind { + let parent = context.tail_mut(); + let siblings = parent.children_mut().unwrap(); + + match siblings.last_mut().unwrap() { + Node::FootnoteReference(node) => { + node.identifier = reference.identifier; + node.label = Some(reference.label); + } + Node::Image(_) => { + // Need to swap it with a reference version of the node. + if let Some(Node::Image(node)) = siblings.pop() { + siblings.push(Node::ImageReference(ImageReference { + reference_kind: kind, + identifier: reference.identifier, + label: Some(reference.label), + alt: node.alt, + position: node.position, + })); + } else { + unreachable!("impossible: it’s an image") + } + } + Node::Link(_) => { + // Need to swap it with a reference version of the node. + if let Some(Node::Link(node)) = siblings.pop() { + siblings.push(Node::LinkReference(LinkReference { + reference_kind: kind, + identifier: reference.identifier, + label: Some(reference.label), + children: node.children, + position: node.position, + })); + } else { + unreachable!("impossible: it’s a link") + } + } + _ => unreachable!("expected footnote reference, image, or link on stack"), + } + } + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`ListItemValue`][Name::ListItemValue]. +fn on_exit_list_item_value(context: &mut CompileContext) { + let start = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ) + .as_str() + .parse() + .expect("expected list value up to u8"); + + if let Node::List(node) = context.tail_penultimate_mut() { + debug_assert!(node.ordered, "expected list to be ordered"); + node.start = Some(start); + } else { + unreachable!("expected list on stack"); + } +} + +/// Handle [`Enter`][Kind::Enter]:{[`MdxJsxFlowTag`][Name::MdxJsxFlowTag],[`MdxJsxTextTag`][Name::MdxJsxTextTag]}. +fn on_exit_mdx_jsx_tag(context: &mut CompileContext) -> Result<(), String> { + let mut tag = context.jsx_tag.as_ref().expect("expected tag").clone(); + + // End of a tag, so drop the buffer. + context.resume(); + // Set end point. + tag.end = point_from_event(&context.events[context.index]); + + let stack = &context.jsx_tag_stack; + let tail = stack.last(); + + if tag.close { + // Unwrap: we crashed earlier if there’s nothing on the stack. + let tail = tail.unwrap(); + + if tail.name != tag.name { + return Err(format!( + "{}:{}: Unexpected closing tag `{}`, expected corresponding closing tag for `{}` ({}:{}) (mdx-jsx:end-tag-mismatch)", + tag.start.line, + tag.start.column, + serialize_abbreviated_tag(&tag), + serialize_abbreviated_tag(tail), + tail.start.line, + tail.start.column, + )); + } + + // Remove from our custom stack. + // Note that this does not exit the node. + context.jsx_tag_stack.pop(); + } else { + let node = if context.events[context.index].name == Name::MdxJsxFlowTag { + Node::MdxJsxFlowElement(MdxJsxFlowElement { + name: tag.name.clone(), + attributes: tag.attributes.clone(), + children: vec![], + position: Some(Position { + start: tag.start.clone(), + end: tag.end.clone(), + }), + }) + } else { + Node::MdxJsxTextElement(MdxJsxTextElement { + name: tag.name.clone(), + attributes: tag.attributes.clone(), + children: vec![], + position: Some(Position { + start: tag.start.clone(), + end: tag.end.clone(), + }), + }) + }; + + context.tail_push(node); + + // this.enter( + // node, + // token, + // onErrorRightIsTag + // ) + } + + if tag.self_closing || tag.close { + context.tail_pop()?; + // this.exit(token, onErrorLeftIsTag) + } else { + context.jsx_tag_stack.push(tag); + } + + Ok(()) +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagClosingMarker`][Name::MdxJsxTagClosingMarker]. +fn on_exit_mdx_jsx_tag_closing_marker(context: &mut CompileContext) { + context.jsx_tag.as_mut().expect("expected tag").close = true; +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagNamePrimary`][Name::MdxJsxTagNamePrimary]. +fn on_exit_mdx_jsx_tag_name_primary(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let value = slice.serialize(); + context.jsx_tag.as_mut().expect("expected tag").name = Some(value); +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagNameMember`][Name::MdxJsxTagNameMember]. +fn on_exit_mdx_jsx_tag_name_member(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let name = context + .jsx_tag + .as_mut() + .expect("expected tag") + .name + .as_mut() + .expect("expected primary before member"); + name.push('.'); + name.push_str(slice.as_str()); +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagNameLocal`][Name::MdxJsxTagNameLocal]. +fn on_exit_mdx_jsx_tag_name_local(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let name = context + .jsx_tag + .as_mut() + .expect("expected tag") + .name + .as_mut() + .expect("expected primary before local"); + name.push(':'); + name.push_str(slice.as_str()); +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression]. +fn on_exit_mdx_jsx_tag_attribute_expression(context: &mut CompileContext) { + let value = context.resume(); + + if let Some(AttributeContent::Expression(expression)) = context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .last_mut() + { + expression.push_str(value.to_string().as_str()); + } else { + unreachable!("expected expression") + } +} + +// Name:: => (context), +// Name:: => (context), + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributePrimaryName`][Name::MdxJsxTagAttributePrimaryName]. +fn on_exit_mdx_jsx_tag_attribute_primary_name(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let value = slice.serialize(); + + if let Some(AttributeContent::Property(attribute)) = context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .last_mut() + { + attribute.name = value; + } else { + unreachable!("expected property") + } +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeNameLocal`][Name::MdxJsxTagAttributeNameLocal]. +fn on_exit_mdx_jsx_tag_attribute_name_local(context: &mut CompileContext) { + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + if let Some(AttributeContent::Property(attribute)) = context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .last_mut() + { + attribute.name.push(':'); + attribute.name.push_str(slice.as_str()); + } else { + unreachable!("expected property") + } +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeValueLiteral`][Name::MdxJsxTagAttributeValueLiteral]. +fn on_exit_mdx_jsx_tag_attribute_value_literal(context: &mut CompileContext) { + let value = context.resume(); + + if let Some(AttributeContent::Property(node)) = context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .last_mut() + { + // To do: character references. + node.value = Some(AttributeValue::Literal(value.to_string())); + } else { + unreachable!("expected property") + } +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeValueExpression`][Name::MdxJsxTagAttributeValueExpression]. +fn on_exit_mdx_jsx_tag_attribute_value_expression(context: &mut CompileContext) { + let value = context.resume(); + + if let Some(AttributeContent::Property(node)) = context + .jsx_tag + .as_mut() + .expect("expected tag") + .attributes + .last_mut() + { + node.value = Some(AttributeValue::Expression(value.to_string())); + } else { + unreachable!("expected property") + } +} + +/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagSelfClosingMarker`][Name::MdxJsxTagSelfClosingMarker]. +fn on_exit_mdx_jsx_tag_self_closing_marker(context: &mut CompileContext) { + context.jsx_tag.as_mut().expect("expected tag").self_closing = true; +} + +/// Handle [`Exit`][Kind::Exit]:[`ReferenceString`][Name::ReferenceString]. +fn on_exit_reference_string(context: &mut CompileContext) { + let label = context.resume().to_string(); + let slice = Slice::from_position( + context.bytes, + &SlicePosition::from_exit_event(context.events, context.index), + ); + let identifier = normalize_identifier(slice.as_str()).to_lowercase(); + let reference = context + .media_reference_stack + .last_mut() + .expect("expected reference on media stack"); + reference.reference_kind = Some(ReferenceKind::Full); + reference.label = label; + reference.identifier = identifier; +} + +/// Handle [`Exit`][Kind::Exit]:[`ResourceDestinationString`][Name::ResourceDestinationString]. +fn on_exit_resource_destination_string(context: &mut CompileContext) { + let value = context.resume().to_string(); + + match context.tail_mut() { + Node::Link(node) => node.url = value, + Node::Image(node) => node.url = value, + _ => unreachable!("expected link, image on stack"), + } +} + +/// Handle [`Exit`][Kind::Exit]:[`ResourceTitleString`][Name::ResourceTitleString]. +fn on_exit_resource_title_string(context: &mut CompileContext) { + let value = Some(context.resume().to_string()); + + match context.tail_mut() { + Node::Link(node) => node.title = value, + Node::Image(node) => node.title = value, + _ => unreachable!("expected link, image on stack"), + } +} + +// Create a point from an event. +fn point_from_event(event: &Event) -> Point { + Point::new(event.point.line, event.point.column, event.point.index) +} + +// Create a position from an event. +fn position_from_event(event: &Event) -> Position { + let end = Point::new(event.point.line, event.point.column, event.point.index); + Position { + start: end.clone(), + end, + } +} + +fn delve_mut<'tree>(mut node: &'tree mut Node, stack: &'tree [usize]) -> &'tree mut Node { + let mut stack_index = 0; + while stack_index < stack.len() { + let index = stack[stack_index]; + node = &mut node.children_mut().expect("Cannot delve into non-parent")[index]; + stack_index += 1; + } + node +} + +fn trim_eol(value: String, at_start: bool, at_end: bool) -> String { + let bytes = value.as_bytes(); + let mut start = 0; + let mut end = bytes.len(); + + if at_start && !bytes.is_empty() { + if bytes[0] == b'\n' { + start += 1; + } else if bytes[0] == b'\r' { + start += 1; + if bytes.len() > 1 && bytes[1] == b'\n' { + start += 1; + } + } + } + + if at_end && end > start { + if bytes[end - 1] == b'\n' { + end -= 1; + } else if bytes[end - 1] == b'\r' { + end -= 1; + if end > start && bytes[end - 1] == b'\n' { + end -= 1; + } + } + } + + if start > 0 || end < bytes.len() { + str::from_utf8(&bytes[start..end]).unwrap().to_string() + } else { + value + } +} + +fn on_mismatch_error( + context: &mut CompileContext, + left: Option<&Event>, + right: &Event, +) -> Result<(), String> { + if right.name == Name::MdxJsxFlowTag || right.name == Name::MdxJsxTextTag { + let point = if let Some(left) = left { + &left.point + } else { + &context.events[context.events.len() - 1].point + }; + let tag = context.jsx_tag.as_ref().unwrap(); + + return Err(format!( + "{}:{}: Expected a closing tag for `{}` ({}:{}){} (mdx-jsx:end-tag-mismatch)", + point.line, + point.column, + serialize_abbreviated_tag(tag), + tag.start.line, + tag.start.column, + if let Some(left) = left { + format!(" before the end of `{:?}`", left.name) + } else { + "".to_string() + } + )); + } + + if let Some(left) = left { + if left.name == Name::MdxJsxFlowTag || left.name == Name::MdxJsxTextTag { + let tag = context.jsx_tag.as_ref().unwrap(); + + return Err(format!( + "{}:{}: Expected the closing tag `{}` either before the start of `{:?}` ({}:{}), or another opening tag after that start (mdx-jsx:end-tag-mismatch)", + tag.start.line, + tag.start.column, + serialize_abbreviated_tag(tag), + &right.name, + &right.point.line, + &right.point.column, + )); + } + unreachable!("mismatched (non-jsx): {:?} / {:?}", left.name, right.name); + } else { + unreachable!("mismatched (non-jsx): document / {:?}", right.name); + } +} + +fn serialize_abbreviated_tag(tag: &JsxTag) -> String { + format!( + "<{}{}>", + if tag.close { "/" } else { "" }, + if let Some(name) = &tag.name { name } else { "" }, + ) +} diff --git a/src/util/constant.rs b/src/util/constant.rs index a9a7895..e9deac2 100644 --- a/src/util/constant.rs +++ b/src/util/constant.rs @@ -2442,13 +2442,13 @@ mod tests { fn constants() { assert_eq!( CHARACTER_REFERENCE_DECIMAL_SIZE_MAX, - format!("{}", 0x10ffff).len(), + format!("{}", 0x0010_ffff).len(), "`CHARACTER_REFERENCE_DECIMAL_SIZE_MAX`" ); assert_eq!( CHARACTER_REFERENCE_HEXADECIMAL_SIZE_MAX, - format!("{:x}", 0x10ffff).len(), + format!("{:x}", 0x0010_ffff).len(), "`CHARACTER_REFERENCE_HEXADECIMAL_SIZE_MAX`" ); diff --git a/src/util/infer.rs b/src/util/infer.rs new file mode 100644 index 0000000..0973913 --- /dev/null +++ b/src/util/infer.rs @@ -0,0 +1,192 @@ +//! Infer things from events. +//! +//! Used to share between `to_html` and `to_mdast`. + +use crate::event::{Event, Kind, Name}; +use crate::mdast::AlignKind; +use alloc::{vec, vec::Vec}; + +/// Figure out if a list is spread or not. +/// +/// When `include_items: true` is passed, infers whether the list as a whole +/// is β€œloose”. +pub fn list_loose(events: &[Event], mut index: usize, include_items: bool) -> bool { + let mut balance = 0; + let name = &events[index].name; + debug_assert!( + matches!(name, Name::ListOrdered | Name::ListUnordered), + "expected list" + ); + + while index < events.len() { + let event = &events[index]; + + if event.kind == Kind::Enter { + balance += 1; + + if include_items + && balance == 2 + && event.name == Name::ListItem + && list_item_loose(events, index) + { + return true; + } + } else { + balance -= 1; + + if balance == 1 && event.name == Name::BlankLineEnding { + // Blank line directly after item, which is just a prefix. + // + // ```markdown + // > | -␊ + // ^ + // | - a + // ``` + let mut at_empty_list_item = false; + // Blank line at block quote prefix: + // + // ```markdown + // > | * >␊ + // ^ + // | * a + // ``` + let mut at_empty_block_quote = false; + + // List. + let mut before = index - 2; + + if events[before].name == Name::ListItem { + before -= 1; + + if events[before].name == Name::SpaceOrTab { + before -= 2; + } + + if events[before].name == Name::BlockQuote + && events[before - 1].name == Name::BlockQuotePrefix + { + at_empty_block_quote = true; + } else if events[before].name == Name::ListItemPrefix { + at_empty_list_item = true; + } + } + + if !at_empty_list_item && !at_empty_block_quote { + return true; + } + } + + // Done. + if balance == 0 && event.name == *name { + break; + } + } + + index += 1; + } + + false +} + +/// Figure out if an item is spread or not. +pub fn list_item_loose(events: &[Event], mut index: usize) -> bool { + debug_assert!( + matches!(events[index].name, Name::ListItem), + "expected list item" + ); + let mut balance = 0; + + while index < events.len() { + let event = &events[index]; + + if event.kind == Kind::Enter { + balance += 1; + } else { + balance -= 1; + + if balance == 1 && event.name == Name::BlankLineEnding { + // Blank line directly after a prefix: + // + // ```markdown + // > | -␊ + // ^ + // | a + // ``` + let mut at_prefix = false; + + // List item. + let mut before = index - 2; + + if events[before].name == Name::SpaceOrTab { + before -= 2; + } + + if events[before].name == Name::ListItemPrefix { + at_prefix = true; + } + + if !at_prefix { + return true; + } + } + + // Done. + if balance == 0 && event.name == Name::ListItem { + break; + } + } + + index += 1; + } + + false +} + +/// Figure out the alignment of a GFM table. +pub fn gfm_table_align(events: &[Event], mut index: usize) -> Vec { + debug_assert!( + matches!(events[index].name, Name::GfmTable), + "expected table" + ); + let mut in_delimiter_row = false; + let mut align = vec![]; + + while index < events.len() { + let event = &events[index]; + + if in_delimiter_row { + if event.kind == Kind::Enter { + // Start of alignment value: set a new column. + if event.name == Name::GfmTableDelimiterCellValue { + align.push(if events[index + 1].name == Name::GfmTableDelimiterMarker { + AlignKind::Left + } else { + AlignKind::None + }); + } + } else { + // End of alignment value: change the column. + if event.name == Name::GfmTableDelimiterCellValue { + if events[index - 1].name == Name::GfmTableDelimiterMarker { + let align_index = align.len() - 1; + align[align_index] = if align[align_index] == AlignKind::Left { + AlignKind::Center + } else { + AlignKind::Right + } + } + } + // Done! + else if event.name == Name::GfmTableDelimiterRow { + break; + } + } + } else if event.kind == Kind::Enter && event.name == Name::GfmTableDelimiterRow { + in_delimiter_row = true; + } + + index += 1; + } + + align +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 6281356..dcbf1ae 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -6,6 +6,7 @@ pub mod decode_character_reference; pub mod edit_map; pub mod encode; pub mod gfm_tagfilter; +pub mod infer; pub mod mdx_collect; pub mod normalize_identifier; pub mod sanitize_uri; diff --git a/tests/attention.rs b/tests/attention.rs index 93c3a50..607af58 100644 --- a/tests/attention.rs +++ b/tests/attention.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Emphasis, Node, Paragraph, Position, Root, Strong, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -827,5 +830,44 @@ fn attention() -> Result<(), String> { "should support turning off attention" ); + assert_eq!( + micromark_to_mdast("a *alpha* b **bravo** c.", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::Emphasis(Emphasis { + children: vec![Node::Text(Text { + value: "alpha".to_string(), + position: Some(Position::new(1, 4, 3, 1, 9, 8)) + }),], + position: Some(Position::new(1, 3, 2, 1, 10, 9)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(1, 10, 9, 1, 13, 12)) + }), + Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "bravo".to_string(), + position: Some(Position::new(1, 15, 14, 1, 20, 19)) + }),], + position: Some(Position::new(1, 13, 12, 1, 22, 21)) + }), + Node::Text(Text { + value: " c.".to_string(), + position: Some(Position::new(1, 22, 21, 1, 25, 24)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 25, 24)) + })], + position: Some(Position::new(1, 1, 0, 1, 25, 24)) + }), + "should support attention as `Emphasis`, `Strong`s in mdast" + ); + Ok(()) } diff --git a/tests/autolink.rs b/tests/autolink.rs index 4cf357a..cc30512 100644 --- a/tests/autolink.rs +++ b/tests/autolink.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Link, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -259,5 +262,51 @@ fn autolink() -> Result<(), String> { "should support turning off autolinks" ); + assert_eq!( + micromark_to_mdast( + "a b c.", + &Options::default() + )?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::Link(Link { + url: "https://alpha.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "https://alpha.com".to_string(), + position: Some(Position::new(1, 4, 3, 1, 21, 20)) + }),], + position: Some(Position::new(1, 3, 2, 1, 22, 21)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(1, 22, 21, 1, 25, 24)) + }), + Node::Link(Link { + url: "mailto:bravo@charlie.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "bravo@charlie.com".to_string(), + position: Some(Position::new(1, 26, 25, 1, 43, 42)) + }),], + position: Some(Position::new(1, 25, 24, 1, 44, 43)) + }), + Node::Text(Text { + value: " c.".to_string(), + position: Some(Position::new(1, 44, 43, 1, 47, 46)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 47, 46)) + })], + position: Some(Position::new(1, 1, 0, 1, 47, 46)) + }), + "should support autolinks as `Link`s in mdast" + ); + Ok(()) } diff --git a/tests/block_quote.rs b/tests/block_quote.rs index 6947ef3..9cd7d46 100644 --- a/tests/block_quote.rs +++ b/tests/block_quote.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{BlockQuote, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -211,5 +214,23 @@ fn block_quote() -> Result<(), String> { "should support turning off block quotes" ); + assert_eq!( + micromark_to_mdast("> a", &Options::default())?, + Node::Root(Root { + children: vec![Node::BlockQuote(BlockQuote { + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 3, 2, 1, 4, 3)) + }),], + position: Some(Position::new(1, 3, 2, 1, 4, 3)) + })], + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + })], + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + }), + "should support block quotes as `BlockQuote`s in mdast" + ); + Ok(()) } diff --git a/tests/character_escape.rs b/tests/character_escape.rs index e76e3e9..e0a3ed3 100644 --- a/tests/character_escape.rs +++ b/tests/character_escape.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -93,5 +96,20 @@ fn character_escape() -> Result<(), String> { "should support turning off character escapes" ); + assert_eq!( + micromark_to_mdast("a \\* b", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a * b".to_string(), + position: Some(Position::new(1, 1, 0, 1, 7, 6)) + }),], + position: Some(Position::new(1, 1, 0, 1, 7, 6)) + })], + position: Some(Position::new(1, 1, 0, 1, 7, 6)) + }), + "should support character escapes as `Text`s in mdast" + ); + Ok(()) } diff --git a/tests/character_reference.rs b/tests/character_reference.rs index a08c3f9..7385734 100644 --- a/tests/character_reference.rs +++ b/tests/character_reference.rs @@ -1,14 +1,18 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] fn character_reference() -> Result<(), String> { assert_eq!( - micromark( - "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸"), - "

\u{a0} & Β© Γ† Ď\nΒΎ β„‹ β…†\n∲ ≧̸

", - "should support named character references" + micromark( + "  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸" + ), + "

\u{a0} & Β© Γ† Ď\nΒΎ β„‹ β…†\n∲ ≧̸

", + "should support named character references" ); assert_eq!( @@ -202,5 +206,20 @@ fn character_reference() -> Result<(), String> { "should support turning off character references" ); + assert_eq!( + micromark_to_mdast("  & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "\u{a0} & Β© Γ† Ď\nΒΎ β„‹ β…†\n∲ ≧̸".to_string(), + position: Some(Position::new(1, 1, 0, 3, 33, 109)) + }),], + position: Some(Position::new(1, 1, 0, 3, 33, 109)) + })], + position: Some(Position::new(1, 1, 0, 3, 33, 109)) + }), + "should support character references as `Text`s in mdast" + ); + Ok(()) } diff --git a/tests/code_fenced.rs b/tests/code_fenced.rs index 850bd1b..2f770ce 100644 --- a/tests/code_fenced.rs +++ b/tests/code_fenced.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Code, Node, Position, Root}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -272,5 +275,36 @@ fn code_fenced() -> Result<(), String> { "should support turning off code (fenced)" ); + assert_eq!( + micromark_to_mdast( + "```js extra\nconsole.log(1)\nconsole.log(2)\n```", + &Options::default() + )?, + Node::Root(Root { + children: vec![Node::Code(Code { + lang: Some("js".to_string()), + meta: Some("extra".to_string()), + value: "console.log(1)\nconsole.log(2)".to_string(), + position: Some(Position::new(1, 1, 0, 4, 4, 45)) + })], + position: Some(Position::new(1, 1, 0, 4, 4, 45)) + }), + "should support code (fenced) as `Code`s in mdast" + ); + + assert_eq!( + micromark_to_mdast("```\nasd", &Options::default())?, + Node::Root(Root { + children: vec![Node::Code(Code { + lang: None, + meta: None, + value: "asd".to_string(), + position: Some(Position::new(1, 1, 0, 2, 4, 7)) + })], + position: Some(Position::new(1, 1, 0, 2, 4, 7)) + }), + "should support code (fenced) w/o closing fence in mdast" + ); + Ok(()) } diff --git a/tests/code_indented.rs b/tests/code_indented.rs index bf39fa3..8a15693 100644 --- a/tests/code_indented.rs +++ b/tests/code_indented.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Code, Node, Position, Root}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -190,5 +193,22 @@ fn code_indented() -> Result<(), String> { "should support turning off code (indented, 9)" ); + assert_eq!( + micromark_to_mdast( + "\tconsole.log(1)\n console.log(2)\n", + &Options::default() + )?, + Node::Root(Root { + children: vec![Node::Code(Code { + lang: None, + meta: None, + value: "console.log(1)\nconsole.log(2)".to_string(), + position: Some(Position::new(1, 1, 0, 2, 19, 34)) + })], + position: Some(Position::new(1, 1, 0, 3, 1, 35)) + }), + "should support code (indented) as `Code`s in mdast" + ); + Ok(()) } diff --git a/tests/code_text.rs b/tests/code_text.rs index a0ed13e..c1ba861 100644 --- a/tests/code_text.rs +++ b/tests/code_text.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{InlineCode, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -170,5 +173,30 @@ fn code_text() -> Result<(), String> { "should support turning off code (text)" ); + assert_eq!( + micromark_to_mdast("a `alpha` b.", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::InlineCode(InlineCode { + value: "alpha".to_string(), + position: Some(Position::new(1, 3, 2, 1, 10, 9)) + }), + Node::Text(Text { + value: " b.".to_string(), + position: Some(Position::new(1, 10, 9, 1, 13, 12)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + })], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + }), + "should support code (text) as `InlineCode`s in mdast" + ); + Ok(()) } diff --git a/tests/commonmark.rs b/tests/commonmark.rs index b5bb40e..e96623a 100644 --- a/tests/commonmark.rs +++ b/tests/commonmark.rs @@ -1,4 +1,4 @@ -//! CommonMark test suite. +//! `CommonMark` test suite. // > πŸ‘‰ **Important**: this module is generated by `build.rs`. // > It is generate from the latest CommonMark website. diff --git a/tests/definition.rs b/tests/definition.rs index 6f680ff..c3bf183 100644 --- a/tests/definition.rs +++ b/tests/definition.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Definition, Node, Position, Root}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -491,5 +494,20 @@ fn definition() -> Result<(), String> { "should support turning off definitions" ); + assert_eq!( + micromark_to_mdast("[a]: 'c'", &Options::default())?, + Node::Root(Root { + children: vec![Node::Definition(Definition { + url: "b".to_string(), + identifier: "a".to_string(), + label: Some("a".to_string()), + title: Some("c".to_string()), + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + })], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + }), + "should support definitions as `Definition`s in mdast" + ); + Ok(()) } diff --git a/tests/frontmatter.rs b/tests/frontmatter.rs index 6195a5a..c5b0d3a 100644 --- a/tests/frontmatter.rs +++ b/tests/frontmatter.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Node, Position, Root, Toml, Yaml}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -66,5 +69,29 @@ fn frontmatter() -> Result<(), String> { "should not support a missing closing fence" ); + assert_eq!( + micromark_to_mdast("---\na: b\n---", &frontmatter)?, + Node::Root(Root { + children: vec![Node::Yaml(Yaml { + value: "a: b".to_string(), + position: Some(Position::new(1, 1, 0, 3, 4, 12)) + })], + position: Some(Position::new(1, 1, 0, 3, 4, 12)) + }), + "should support yaml as `Yaml`s in mdast" + ); + + assert_eq!( + micromark_to_mdast("+++\ntitle = \"Jupyter\"\n+++", &frontmatter)?, + Node::Root(Root { + children: vec![Node::Toml(Toml { + value: "title = \"Jupyter\"".to_string(), + position: Some(Position::new(1, 1, 0, 3, 4, 25)) + })], + position: Some(Position::new(1, 1, 0, 3, 4, 25)) + }), + "should support toml as `Toml`s in mdast" + ); + Ok(()) } diff --git a/tests/gfm_autolink_literal.rs b/tests/gfm_autolink_literal.rs index bcb0797..bf99071 100644 --- a/tests/gfm_autolink_literal.rs +++ b/tests/gfm_autolink_literal.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Link, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -2735,5 +2738,90 @@ www.a/~ "should match www (path start) like GitHub does (except for the bracket bug)" ); + assert_eq!( + micromark_to_mdast( + "a https://alpha.com b bravo@charlie.com c www.delta.com d xmpp:echo@foxtrot.com e mailto:golf@hotel.com f.", + &gfm + )?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::Link(Link { + url: "https://alpha.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "https://alpha.com".to_string(), + position: Some(Position::new(1, 3, 2, 1, 20, 19)) + }),], + position: Some(Position::new(1, 3, 2, 1, 20, 19)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(1, 20, 19, 1, 23, 22)) + }), + Node::Link(Link { + url: "mailto:bravo@charlie.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "bravo@charlie.com".to_string(), + position: Some(Position::new(1, 23, 22, 1, 40, 39)) + }),], + position: Some(Position::new(1, 23, 22, 1, 40, 39)) + }), + Node::Text(Text { + value: " c ".to_string(), + position: Some(Position::new(1, 40, 39, 1, 43, 42)) + }), + Node::Link(Link { + url: "http://www.delta.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "www.delta.com".to_string(), + position: Some(Position::new(1, 43, 42, 1, 56, 55)) + }),], + position: Some(Position::new(1, 43, 42, 1, 56, 55)) + }), + Node::Text(Text { + value: " d ".to_string(), + position: Some(Position::new(1, 56, 55, 1, 59, 58)) + }), + Node::Link(Link { + url: "xmpp:echo@foxtrot.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "xmpp:echo@foxtrot.com".to_string(), + position: Some(Position::new(1, 59, 58, 1, 80, 79)) + }),], + position: Some(Position::new(1, 59, 58, 1, 80, 79)) + }), + Node::Text(Text { + value: " e ".to_string(), + position: Some(Position::new(1, 80, 79, 1, 83, 82)) + }), + Node::Link(Link { + url: "mailto:golf@hotel.com".to_string(), + title: None, + children: vec![Node::Text(Text { + value: "mailto:golf@hotel.com".to_string(), + position: Some(Position::new(1, 83, 82, 1, 104, 103)) + }),], + position: Some(Position::new(1, 83, 82, 1, 104, 103)) + }), + Node::Text(Text { + value: " f.".to_string(), + position: Some(Position::new(1, 104, 103, 1, 107, 106)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 107, 106)) + })], + position: Some(Position::new(1, 1, 0, 1, 107, 106)) + }), + "should support GFM autolink literals as `Link`s in mdast" + ); + Ok(()) } diff --git a/tests/gfm_footnote.rs b/tests/gfm_footnote.rs index 42c70d3..8785239 100644 --- a/tests/gfm_footnote.rs +++ b/tests/gfm_footnote.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -1597,5 +1600,45 @@ multi-paragraph list items. https:// "should match loose escapes like GitHub" ); + assert_eq!( + micromark_to_mdast( + "| none | left | right | center |\n| - | :- | -: | :-: |\n| a |\n| b | c | d | e | f |", + &gfm + )?, + Node::Root(Root { + children: vec![Node::Table(Table { + align: vec![ + AlignKind::None, + AlignKind::Left, + AlignKind::Right, + AlignKind::Center + ], + children: vec![ + Node::TableRow(TableRow { + children: vec![ + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "none".to_string(), + position: Some(Position::new(1, 3, 2, 1, 7, 6)) + }),], + position: Some(Position::new(1, 1, 0, 1, 8, 7)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "left".to_string(), + position: Some(Position::new(1, 10, 9, 1, 14, 13)) + }),], + position: Some(Position::new(1, 8, 7, 1, 15, 14)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "right".to_string(), + position: Some(Position::new(1, 17, 16, 1, 22, 21)) + }),], + position: Some(Position::new(1, 15, 14, 1, 23, 22)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "center".to_string(), + position: Some(Position::new(1, 25, 24, 1, 31, 30)) + }),], + position: Some(Position::new(1, 23, 22, 1, 33, 32)) + }), + ], + position: Some(Position::new(1, 1, 0, 1, 33, 32)) + }), + Node::TableRow(TableRow { + children: vec![Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(3, 3, 57, 3, 4, 58)) + }),], + position: Some(Position::new(3, 1, 55, 3, 6, 60)) + }),], + position: Some(Position::new(3, 1, 55, 3, 6, 60)) + }), + Node::TableRow(TableRow { + children: vec![ + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "b".to_string(), + position: Some(Position::new(4, 3, 63, 4, 4, 64)) + }),], + position: Some(Position::new(4, 1, 61, 4, 5, 65)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "c".to_string(), + position: Some(Position::new(4, 7, 67, 4, 8, 68)) + }),], + position: Some(Position::new(4, 5, 65, 4, 9, 69)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "d".to_string(), + position: Some(Position::new(4, 11, 71, 4, 12, 72)) + }),], + position: Some(Position::new(4, 9, 69, 4, 13, 73)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "e".to_string(), + position: Some(Position::new(4, 15, 75, 4, 16, 76)) + }),], + position: Some(Position::new(4, 13, 73, 4, 17, 77)) + }), + Node::TableCell(TableCell { + children: vec![Node::Text(Text { + value: "f".to_string(), + position: Some(Position::new(4, 19, 79, 4, 20, 80)) + }),], + position: Some(Position::new(4, 17, 77, 4, 22, 82)) + }), + ], + position: Some(Position::new(4, 1, 61, 4, 22, 82)) + }), + ], + position: Some(Position::new(1, 1, 0, 4, 22, 82)) + })], + position: Some(Position::new(1, 1, 0, 4, 22, 82)) + }), + "should support GFM tables as `Table`, `TableRow`, `TableCell`s in mdast" + ); + + assert_eq!( + micromark_to_mdast("| `a\\|b` |\n| - |", &gfm)?, + Node::Root(Root { + children: vec![Node::Table(Table { + align: vec![AlignKind::None,], + children: vec![Node::TableRow(TableRow { + children: vec![Node::TableCell(TableCell { + children: vec![Node::InlineCode(InlineCode { + value: "a|b".to_string(), + position: Some(Position::new(1, 3, 2, 1, 9, 8)) + }),], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + }),], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + }),], + position: Some(Position::new(1, 1, 0, 2, 6, 16)) + })], + position: Some(Position::new(1, 1, 0, 2, 6, 16)) + }), + "should support weird pipe escapes in code in tables" + ); + Ok(()) } diff --git a/tests/gfm_task_list_item.rs b/tests/gfm_task_list_item.rs index b824730..ae6c548 100644 --- a/tests/gfm_task_list_item.rs +++ b/tests/gfm_task_list_item.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{List, ListItem, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -240,5 +243,57 @@ Text. "should handle things like GitHub" ); + assert_eq!( + micromark_to_mdast("* [x] a\n* [ ] b\n* c", &gfm)?, + Node::Root(Root { + children: vec![Node::List(List { + ordered: false, + spread: false, + start: None, + children: vec![ + Node::ListItem(ListItem { + checked: Some(true), + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 7, 6, 1, 8, 7)) + }),], + position: Some(Position::new(1, 7, 6, 1, 8, 7)) + })], + position: Some(Position::new(1, 1, 0, 1, 8, 7)) + }), + Node::ListItem(ListItem { + checked: Some(false), + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "b".to_string(), + position: Some(Position::new(2, 7, 14, 2, 8, 15)) + }),], + position: Some(Position::new(2, 7, 14, 2, 8, 15)) + })], + position: Some(Position::new(2, 1, 8, 2, 8, 15)) + }), + Node::ListItem(ListItem { + checked: None, + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "c".to_string(), + position: Some(Position::new(3, 3, 18, 3, 4, 19)) + }),], + position: Some(Position::new(3, 3, 18, 3, 4, 19)) + })], + position: Some(Position::new(3, 1, 16, 3, 4, 19)) + }), + ], + position: Some(Position::new(1, 1, 0, 3, 4, 19)) + })], + position: Some(Position::new(1, 1, 0, 3, 4, 19)) + }), + "should support task list items as `checked` fields on `ListItem`s in mdast" + ); + Ok(()) } diff --git a/tests/hard_break_escape.rs b/tests/hard_break_escape.rs index 2510860..ced3b3d 100644 --- a/tests/hard_break_escape.rs +++ b/tests/hard_break_escape.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Break, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -55,5 +58,29 @@ fn hard_break_escape() -> Result<(), String> { "should support turning off hard break (escape)" ); + assert_eq!( + micromark_to_mdast("a\\\nb.", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 1, 0, 1, 2, 1)) + }), + Node::Break(Break { + position: Some(Position::new(1, 2, 1, 2, 1, 3)) + }), + Node::Text(Text { + value: "b.".to_string(), + position: Some(Position::new(2, 1, 3, 2, 3, 5)) + }), + ], + position: Some(Position::new(1, 1, 0, 2, 3, 5)) + })], + position: Some(Position::new(1, 1, 0, 2, 3, 5)) + }), + "should support hard break (escape) as `Break`s in mdast" + ); + Ok(()) } diff --git a/tests/hard_break_trailing.rs b/tests/hard_break_trailing.rs index b5577fd..042f8f0 100644 --- a/tests/hard_break_trailing.rs +++ b/tests/hard_break_trailing.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Break, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -121,5 +124,29 @@ fn hard_break_trailing() -> Result<(), String> { "should support turning off hard break (trailing)" ); + assert_eq!( + micromark_to_mdast("a \nb.", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 1, 0, 1, 2, 1)) + }), + Node::Break(Break { + position: Some(Position::new(1, 2, 1, 2, 1, 4)) + }), + Node::Text(Text { + value: "b.".to_string(), + position: Some(Position::new(2, 1, 4, 2, 3, 6)) + }), + ], + position: Some(Position::new(1, 1, 0, 2, 3, 6)) + })], + position: Some(Position::new(1, 1, 0, 2, 3, 6)) + }), + "should support hard break (trailing) as `Break`s in mdast" + ); + Ok(()) } diff --git a/tests/heading_atx.rs b/tests/heading_atx.rs index 1bd437c..da83ff5 100644 --- a/tests/heading_atx.rs +++ b/tests/heading_atx.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Heading, Node, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -217,5 +220,21 @@ fn heading_atx() -> Result<(), String> { "should support turning off heading (atx)" ); + assert_eq!( + micromark_to_mdast("## alpha #", &Options::default())?, + Node::Root(Root { + children: vec![Node::Heading(Heading { + depth: 2, + children: vec![Node::Text(Text { + value: "alpha".to_string(), + position: Some(Position::new(1, 4, 3, 1, 9, 8)) + }),], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + })], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + }), + "should support heading (atx) as `Heading`s in mdast" + ); + Ok(()) } diff --git a/tests/heading_setext.rs b/tests/heading_setext.rs index b2889f5..4292ed2 100644 --- a/tests/heading_setext.rs +++ b/tests/heading_setext.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Heading, Node, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -284,5 +287,21 @@ fn heading_setext() -> Result<(), String> { "should support turning off setext underlines" ); + assert_eq!( + micromark_to_mdast("alpha\nbravo\n==", &Options::default())?, + Node::Root(Root { + children: vec![Node::Heading(Heading { + depth: 1, + children: vec![Node::Text(Text { + value: "alpha\nbravo".to_string(), + position: Some(Position::new(1, 1, 0, 2, 6, 11)) + }),], + position: Some(Position::new(1, 1, 0, 3, 3, 14)) + })], + position: Some(Position::new(1, 1, 0, 3, 3, 14)) + }), + "should support heading (atx) as `Heading`s in mdast" + ); + Ok(()) } diff --git a/tests/html_flow.rs b/tests/html_flow.rs index c411036..2605105 100644 --- a/tests/html_flow.rs +++ b/tests/html_flow.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Html, Node, Position, Root}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -36,6 +39,18 @@ fn html_flow() -> Result<(), String> { "should support turning off html (flow)" ); + assert_eq!( + micromark_to_mdast("
\nstuff\n
", &Options::default())?, + Node::Root(Root { + children: vec![Node::Html(Html { + value: "
\nstuff\n
".to_string(), + position: Some(Position::new(1, 1, 0, 3, 7, 18)) + })], + position: Some(Position::new(1, 1, 0, 3, 7, 18)) + }), + "should support HTML (flow) as `Html`s in mdast" + ); + Ok(()) } diff --git a/tests/html_text.rs b/tests/html_text.rs index 8fdbbd2..d35bdba 100644 --- a/tests/html_text.rs +++ b/tests/html_text.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Html, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -433,5 +436,38 @@ micromark_with_options(" a", &danger)?, "should support turning off html (text)" ); + assert_eq!( + micromark_to_mdast("alpha bravo
charlie.", &Options::default())?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "alpha ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 7, 6)) + }), + Node::Html(Html { + value: "".to_string(), + position: Some(Position::new(1, 7, 6, 1, 10, 9)) + }), + Node::Text(Text { + value: "bravo".to_string(), + position: Some(Position::new(1, 10, 9, 1, 15, 14)) + }), + Node::Html(Html { + value: "".to_string(), + position: Some(Position::new(1, 15, 14, 1, 19, 18)) + }), + Node::Text(Text { + value: " charlie.".to_string(), + position: Some(Position::new(1, 19, 18, 1, 28, 27)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 28, 27)) + })], + position: Some(Position::new(1, 1, 0, 1, 28, 27)) + }), + "should support HTML (text) as `Html`s in mdast" + ); + Ok(()) } diff --git a/tests/image.rs b/tests/image.rs index 966b653..40d1449 100644 --- a/tests/image.rs +++ b/tests/image.rs @@ -1,5 +1,10 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{ + Definition, Image, ImageReference, Node, Paragraph, Position, ReferenceKind, Root, Text, + }, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -225,5 +230,106 @@ fn image() -> Result<(), String> { "should allow non-http protocols w/ `allowDangerousProtocol`" ); + assert_eq!( + micromark_to_mdast( + "a ![alpha]() b ![bravo](charlie 'delta') c.", + &Options::default() + )?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::Image(Image { + alt: "alpha".to_string(), + url: String::new(), + title: None, + position: Some(Position::new(1, 3, 2, 1, 13, 12)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(1, 13, 12, 1, 16, 15)) + }), + Node::Image(Image { + alt: "bravo".to_string(), + url: "charlie".to_string(), + title: Some("delta".to_string()), + position: Some(Position::new(1, 16, 15, 1, 41, 40)) + }), + Node::Text(Text { + value: " c.".to_string(), + position: Some(Position::new(1, 41, 40, 1, 44, 43)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 44, 43)) + })], + position: Some(Position::new(1, 1, 0, 1, 44, 43)) + }), + "should support image (resource) as `Image`s in mdast" + ); + + assert_eq!( + micromark_to_mdast( + "[x]: y\n\na ![x] b ![x][] c ![d][x] e.", + &Options::default() + )?, + Node::Root(Root { + children: vec![ + Node::Definition(Definition { + identifier: "x".to_string(), + label: Some("x".to_string()), + url: "y".to_string(), + title: None, + position: Some(Position::new(1, 1, 0, 1, 7, 6)) + }), + Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(3, 1, 8, 3, 3, 10)) + }), + Node::ImageReference(ImageReference { + reference_kind: ReferenceKind::Shortcut, + identifier: "x".to_string(), + label: Some("x".to_string()), + alt: "x".to_string(), + position: Some(Position::new(3, 3, 10, 3, 7, 14)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(3, 7, 14, 3, 10, 17)) + }), + Node::ImageReference(ImageReference { + reference_kind: ReferenceKind::Collapsed, + identifier: "x".to_string(), + label: Some("x".to_string()), + alt: "x".to_string(), + position: Some(Position::new(3, 10, 17, 3, 16, 23)) + }), + Node::Text(Text { + value: " c ".to_string(), + position: Some(Position::new(3, 16, 23, 3, 19, 26)) + }), + Node::ImageReference(ImageReference { + reference_kind: ReferenceKind::Full, + identifier: "x".to_string(), + label: Some("x".to_string()), + alt: "d".to_string(), + position: Some(Position::new(3, 19, 26, 3, 26, 33)) + }), + Node::Text(Text { + value: " e.".to_string(), + position: Some(Position::new(3, 26, 33, 3, 29, 36)) + }), + ], + position: Some(Position::new(3, 1, 8, 3, 29, 36)) + }), + ], + position: Some(Position::new(1, 1, 0, 3, 29, 36)) + }), + "should support image (reference) as `ImageReference`s in mdast" + ); Ok(()) } diff --git a/tests/link_reference.rs b/tests/link_reference.rs index 9c853f7..d1d6785 100644 --- a/tests/link_reference.rs +++ b/tests/link_reference.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Definition, LinkReference, Node, Paragraph, Position, ReferenceKind, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -414,5 +417,73 @@ fn link_reference() -> Result<(), String> { "should support turning off label end" ); + assert_eq!( + micromark_to_mdast("[x]: y\n\na [x] b [x][] c [d][x] e.", &Options::default())?, + Node::Root(Root { + children: vec![ + Node::Definition(Definition { + identifier: "x".to_string(), + label: Some("x".to_string()), + url: "y".to_string(), + title: None, + position: Some(Position::new(1, 1, 0, 1, 7, 6)) + }), + Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(3, 1, 8, 3, 3, 10)) + }), + Node::LinkReference(LinkReference { + reference_kind: ReferenceKind::Shortcut, + identifier: "x".to_string(), + label: Some("x".to_string()), + children: vec![Node::Text(Text { + value: "x".to_string(), + position: Some(Position::new(3, 4, 11, 3, 5, 12)) + }),], + position: Some(Position::new(3, 3, 10, 3, 6, 13)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(3, 6, 13, 3, 9, 16)) + }), + Node::LinkReference(LinkReference { + reference_kind: ReferenceKind::Collapsed, + identifier: "x".to_string(), + label: Some("x".to_string()), + children: vec![Node::Text(Text { + value: "x".to_string(), + position: Some(Position::new(3, 10, 17, 3, 11, 18)) + }),], + position: Some(Position::new(3, 9, 16, 3, 14, 21)) + }), + Node::Text(Text { + value: " c ".to_string(), + position: Some(Position::new(3, 14, 21, 3, 17, 24)) + }), + Node::LinkReference(LinkReference { + reference_kind: ReferenceKind::Full, + identifier: "x".to_string(), + label: Some("x".to_string()), + children: vec![Node::Text(Text { + value: "d".to_string(), + position: Some(Position::new(3, 18, 25, 3, 19, 26)) + }),], + position: Some(Position::new(3, 17, 24, 3, 23, 30)) + }), + Node::Text(Text { + value: " e.".to_string(), + position: Some(Position::new(3, 23, 30, 3, 26, 33)) + }), + ], + position: Some(Position::new(3, 1, 8, 3, 26, 33)) + }), + ], + position: Some(Position::new(1, 1, 0, 3, 26, 33)) + }), + "should support link (reference) as `LinkReference`s in mdast" + ); + Ok(()) } diff --git a/tests/link_resource.rs b/tests/link_resource.rs index 6bcbb8c..a296410 100644 --- a/tests/link_resource.rs +++ b/tests/link_resource.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Options}; +use micromark::{ + mdast::{Link, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -458,5 +461,51 @@ fn link_resource() -> Result<(), String> { "should support a single NUL character as a link resource" ); + assert_eq!( + micromark_to_mdast( + "a [alpha]() b [bravo](charlie 'delta') c.", + &Options::default() + )?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::Link(Link { + url: String::new(), + title: None, + children: vec![Node::Text(Text { + value: "alpha".to_string(), + position: Some(Position::new(1, 4, 3, 1, 9, 8)) + }),], + position: Some(Position::new(1, 3, 2, 1, 12, 11)) + }), + Node::Text(Text { + value: " b ".to_string(), + position: Some(Position::new(1, 12, 11, 1, 15, 14)) + }), + Node::Link(Link { + url: "charlie".to_string(), + title: Some("delta".to_string()), + children: vec![Node::Text(Text { + value: "bravo".to_string(), + position: Some(Position::new(1, 16, 15, 1, 21, 20)) + }),], + position: Some(Position::new(1, 15, 14, 1, 39, 38)) + }), + Node::Text(Text { + value: " c.".to_string(), + position: Some(Position::new(1, 39, 38, 1, 42, 41)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 42, 41)) + })], + position: Some(Position::new(1, 1, 0, 1, 42, 41)) + }), + "should support link (resource) as `Link`s in mdast" + ); + Ok(()) } diff --git a/tests/list.rs b/tests/list.rs index 4007251..95beeec 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{List, ListItem, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -578,5 +581,106 @@ fn list() -> Result<(), String> { "should support turning off lists" ); + assert_eq!( + micromark_to_mdast("* a", &Options::default())?, + Node::Root(Root { + children: vec![Node::List(List { + ordered: false, + spread: false, + start: None, + children: vec![Node::ListItem(ListItem { + checked: None, + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 3, 2, 1, 4, 3)) + }),], + position: Some(Position::new(1, 3, 2, 1, 4, 3)) + })], + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + })], + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + })], + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + }), + "should support lists, list items as `List`, `ListItem`s in mdast" + ); + + assert_eq!( + micromark_to_mdast("3. a", &Options::default())?, + Node::Root(Root { + children: vec![Node::List(List { + ordered: true, + spread: false, + start: Some(3), + children: vec![Node::ListItem(ListItem { + checked: None, + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 4, 3, 1, 5, 4)) + }),], + position: Some(Position::new(1, 4, 3, 1, 5, 4)) + })], + position: Some(Position::new(1, 1, 0, 1, 5, 4)) + })], + position: Some(Position::new(1, 1, 0, 1, 5, 4)) + })], + position: Some(Position::new(1, 1, 0, 1, 5, 4)) + }), + "should support `start` fields on `List` w/ `ordered: true` in mdast" + ); + + assert_eq!( + micromark_to_mdast("* a\n\n b\n* c", &Options::default())?, + Node::Root(Root { + children: vec![Node::List(List { + ordered: false, + spread: false, + start: None, + children: vec![ + Node::ListItem(ListItem { + checked: None, + spread: true, + children: vec![ + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(1, 3, 2, 1, 4, 3)) + }),], + position: Some(Position::new(1, 3, 2, 1, 4, 3)) + }), + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "b".to_string(), + position: Some(Position::new(3, 3, 7, 3, 4, 8)) + }),], + position: Some(Position::new(3, 3, 7, 3, 4, 8)) + }) + ], + position: Some(Position::new(1, 1, 0, 3, 4, 8)) + }), + Node::ListItem(ListItem { + checked: None, + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "c".to_string(), + position: Some(Position::new(4, 3, 11, 4, 4, 12)) + }),], + position: Some(Position::new(4, 3, 11, 4, 4, 12)) + })], + position: Some(Position::new(4, 1, 9, 4, 4, 12)) + }) + ], + position: Some(Position::new(1, 1, 0, 4, 4, 12)) + })], + position: Some(Position::new(1, 1, 0, 4, 4, 12)) + }), + "should support `spread` fields on `List`, `ListItem`s in mdast" + ); + Ok(()) } diff --git a/tests/math_flow.rs b/tests/math_flow.rs index c277326..3797e83 100644 --- a/tests/math_flow.rs +++ b/tests/math_flow.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Math, Node, Position, Root}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -248,5 +251,18 @@ fn math_flow() -> Result<(), String> { "should not support lazyness (3)" ); + assert_eq!( + micromark_to_mdast("$$extra\nabc\ndef\n$$", &math)?, + Node::Root(Root { + children: vec![Node::Math(Math { + meta: Some("extra".to_string()), + value: "abc\ndef".to_string(), + position: Some(Position::new(1, 1, 0, 4, 3, 18)) + })], + position: Some(Position::new(1, 1, 0, 4, 3, 18)) + }), + "should support math (flow) as `Math`s in mdast" + ); + Ok(()) } diff --git a/tests/math_text.rs b/tests/math_text.rs index 7b53268..9e20d6e 100644 --- a/tests/math_text.rs +++ b/tests/math_text.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{InlineMath, Node, Paragraph, Position, Root, Text}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -192,5 +195,30 @@ fn math_text() -> Result<(), String> { "should support an escaped initial dollar" ); + assert_eq!( + micromark_to_mdast("a $alpha$ b.", &math)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::InlineMath(InlineMath { + value: "alpha".to_string(), + position: Some(Position::new(1, 3, 2, 1, 10, 9)) + }), + Node::Text(Text { + value: " b.".to_string(), + position: Some(Position::new(1, 10, 9, 1, 13, 12)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + })], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + }), + "should support math (text) as `InlineMath`s in mdast" + ); + Ok(()) } diff --git a/tests/mdx_esm.rs b/tests/mdx_esm.rs index f1ea122..0f8888b 100644 --- a/tests/mdx_esm.rs +++ b/tests/mdx_esm.rs @@ -1,6 +1,9 @@ extern crate micromark; mod test_utils; -use micromark::{micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{MdxjsEsm, Node, Position, Root}, + micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; use test_utils::{parse_esm, parse_expression}; @@ -237,5 +240,17 @@ fn mdx_esm() -> Result<(), String> { ); } + assert_eq!( + micromark_to_mdast("import a from 'b'\nexport {a}", &swc)?, + Node::Root(Root { + children: vec![Node::MdxjsEsm(MdxjsEsm { + value: "import a from 'b'\nexport {a}".to_string(), + position: Some(Position::new(1, 1, 0, 2, 11, 28)) + })], + position: Some(Position::new(1, 1, 0, 2, 11, 28)) + }), + "should support mdx esm as `MdxjsEsm`s in mdast" + ); + Ok(()) } diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs index 81a31a7..1d50468 100644 --- a/tests/mdx_expression_flow.rs +++ b/tests/mdx_expression_flow.rs @@ -1,6 +1,9 @@ extern crate micromark; mod test_utils; -use micromark::{micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{MdxFlowExpression, Node, Position, Root}, + micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; use test_utils::{parse_esm, parse_expression}; @@ -81,6 +84,18 @@ fn mdx_expression_flow_agnostic() -> Result<(), String> { "should not support lazyness (4)" ); + assert_eq!( + micromark_to_mdast("{alpha +\nbravo}", &mdx)?, + Node::Root(Root { + children: vec![Node::MdxFlowExpression(MdxFlowExpression { + value: "alpha +\nbravo".to_string(), + position: Some(Position::new(1, 1, 0, 2, 7, 15)) + })], + position: Some(Position::new(1, 1, 0, 2, 7, 15)) + }), + "should support mdx expressions (flow) as `MdxFlowExpression`s in mdast" + ); + Ok(()) } diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs index 3a48965..997e7de 100644 --- a/tests/mdx_expression_text.rs +++ b/tests/mdx_expression_text.rs @@ -1,6 +1,9 @@ extern crate micromark; mod test_utils; -use micromark::{micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{MdxTextExpression, Node, Paragraph, Position, Root, Text}, + micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; use test_utils::{parse_esm, parse_expression}; @@ -191,6 +194,31 @@ fn mdx_expression_text_agnostic() -> Result<(), String> { "should support expressions as the first thing when following by other things" ); + assert_eq!( + micromark_to_mdast("a {alpha} b.", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::MdxTextExpression(MdxTextExpression { + value: "alpha".to_string(), + position: Some(Position::new(1, 3, 2, 1, 10, 9)) + }), + Node::Text(Text { + value: " b.".to_string(), + position: Some(Position::new(1, 10, 9, 1, 13, 12)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + })], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + }), + "should support mdx expressions (text) as `MdxTextExpression`s in mdast" + ); + Ok(()) } diff --git a/tests/mdx_jsx_flow.rs b/tests/mdx_jsx_flow.rs index 9b0453f..14e14f0 100644 --- a/tests/mdx_jsx_flow.rs +++ b/tests/mdx_jsx_flow.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{List, ListItem, MdxJsxFlowElement, Node, Paragraph, Position, Root, Text}, + micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -141,5 +144,36 @@ fn mdx_jsx_flow_essence() -> Result<(), String> { "should not support lazy flow (7)" ); + assert_eq!( + micromark_to_mdast("<>\n * a\n", &mdx)?, + Node::Root(Root { + children: vec![Node::MdxJsxFlowElement(MdxJsxFlowElement { + name: None, + attributes: vec![], + children: vec![Node::List(List { + ordered: false, + spread: false, + start: None, + children: vec![Node::ListItem(ListItem { + checked: None, + spread: false, + children: vec![Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: Some(Position::new(2, 5, 7, 2, 6, 8)) + }),], + position: Some(Position::new(2, 5, 7, 2, 6, 8)) + })], + position: Some(Position::new(2, 1, 3, 2, 6, 8)) + })], + position: Some(Position::new(2, 1, 3, 2, 6, 8)) + })], + position: Some(Position::new(1, 1, 0, 3, 4, 12)) + })], + position: Some(Position::new(1, 1, 0, 3, 4, 12)) + }), + "should support mdx jsx (flow) as `MdxJsxFlowElement`s in mdast" + ); + Ok(()) } diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs index be76d6f..94e7b0c 100644 --- a/tests/mdx_jsx_text.rs +++ b/tests/mdx_jsx_text.rs @@ -1,6 +1,12 @@ extern crate micromark; mod test_utils; -use micromark::{micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{ + AttributeContent, AttributeValue, Emphasis, MdxJsxAttribute, MdxJsxTextElement, Node, + Paragraph, Position, Root, Text, + }, + micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; use test_utils::{parse_esm, parse_expression}; @@ -41,6 +47,272 @@ fn mdx_jsx_text_core() -> Result<(), String> { "should support markdown inside elements" ); + assert_eq!( + micromark_to_mdast("a c.", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("b".to_string()), + attributes: vec![], + children: vec![], + position: Some(Position::new(1, 3, 2, 1, 8, 7)) + }), + Node::Text(Text { + value: " c.".to_string(), + position: Some(Position::new(1, 8, 7, 1, 11, 10)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + })], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (self-closing)" + ); + + assert_eq!( + micromark_to_mdast("a *c* d.", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: Some(Position::new(1, 1, 0, 1, 3, 2)) + }), + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("b".to_string()), + attributes: vec![], + children: vec![ + Node::Emphasis(Emphasis { + children: vec![ + Node::Text(Text { + value: "c".to_string(), + position: Some(Position::new(1, 7, 6, 1, 8, 7)) + }), + ], + position: Some(Position::new(1, 6, 5, 1, 9, 8)) + }), + ], + position: Some(Position::new(1, 3, 2, 1, 13, 12)) + }), + Node::Text(Text { + value: " d.".to_string(), + position: Some(Position::new(1, 13, 12, 1, 16, 15)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 16, 15)) + })], + position: Some(Position::new(1, 1, 0, 1, 16, 15)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (matched open and close tags)" + ); + + assert_eq!( + micromark_to_mdast(".", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("a:b".to_string()), + attributes: vec![], + children: vec![], + position: Some(Position::new(1, 1, 0, 1, 8, 7)) + }), + Node::Text(Text { + value: ".".to_string(), + position: Some(Position::new(1, 8, 7, 1, 9, 8)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 9, 8)) + })], + position: Some(Position::new(1, 1, 0, 1, 9, 8)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (namespace in tag name)" + ); + + assert_eq!( + micromark_to_mdast(".", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("a.b.c".to_string()), + attributes: vec![], + children: vec![], + position: Some(Position::new(1, 1, 0, 1, 10, 9)) + }), + Node::Text(Text { + value: ".".to_string(), + position: Some(Position::new(1, 10, 9, 1, 11, 10)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + })], + position: Some(Position::new(1, 1, 0, 1, 11, 10)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (members in tag name)" + ); + + assert_eq!( + micromark_to_mdast(".", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("a".to_string()), + attributes: vec![AttributeContent::Expression("...b".to_string())], + children: vec![], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + }), + Node::Text(Text { + value: ".".to_string(), + position: Some(Position::new(1, 13, 12, 1, 14, 13)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 14, 13)) + })], + position: Some(Position::new(1, 1, 0, 1, 14, 13)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (attribute expression)" + ); + + assert_eq!( + micromark_to_mdast(".", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("a".to_string()), + attributes: vec![ + AttributeContent::Property(MdxJsxAttribute { + name: "b".to_string(), + value: None, + }), + AttributeContent::Property(MdxJsxAttribute { + name: "c:d".to_string(), + value: None, + }) + ], + children: vec![], + position: Some(Position::new(1, 1, 0, 1, 12, 11)) + }), + Node::Text(Text { + value: ".".to_string(), + position: Some(Position::new(1, 12, 11, 1, 13, 12)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + })], + position: Some(Position::new(1, 1, 0, 1, 13, 12)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (property names)" + ); + + assert_eq!( + micromark_to_mdast(".", &mdx)?, + Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::MdxJsxTextElement(MdxJsxTextElement { + name: Some("a".to_string()), + attributes: vec![ + AttributeContent::Property(MdxJsxAttribute { + name: "b".to_string(), + value: Some(AttributeValue::Literal("c".to_string())), + }), + AttributeContent::Property(MdxJsxAttribute { + name: "d".to_string(), + value: Some(AttributeValue::Literal("e".to_string())), + }), + AttributeContent::Property(MdxJsxAttribute { + name: "f".to_string(), + value: Some(AttributeValue::Expression("g".to_string())), + }), + ], + children: vec![], + position: Some(Position::new(1, 1, 0, 1, 24, 23)) + }), + Node::Text(Text { + value: ".".to_string(), + position: Some(Position::new(1, 24, 23, 1, 25, 24)) + }) + ], + position: Some(Position::new(1, 1, 0, 1, 25, 24)) + })], + position: Some(Position::new(1, 1, 0, 1, 25, 24)) + }), + "should support mdx jsx (text) as `MdxJsxTextElement`s in mdast (attribute values)" + ); + + assert_eq!( + micromark_to_mdast("a c", &mdx) + .err() + .unwrap(), + "1:4: Unexpected closing slash `/` in tag, expected an open tag first (mdx-jsx:unexpected-closing-slash)", + "should crash when building the ast on a closing tag if none is open" + ); + + assert_eq!( + micromark_to_mdast("a c d", &mdx) + .err() + .unwrap(), + "1:12: Unexpected self-closing slash `/` in closing tag, expected the end of the tag (mdx-jsx:unexpected-self-closing-slash)", + "should crash when building the ast on a closing tag with a self-closing slash" + ); + + assert_eq!( + micromark_to_mdast("a c e", &mdx) + .err() + .unwrap(), + "1:13: Unexpected attribute in closing tag, expected the end of the tag (mdx-jsx:unexpected-attribute)", + "should crash when building the ast on a closing tag with an attribute" + ); + + assert_eq!( + micromark_to_mdast("a <>b d", &mdx) + .err() + .unwrap(), + "1:6: Unexpected closing tag ``, expected corresponding closing tag for `<>` (1:3) (mdx-jsx:end-tag-mismatch)", + "should crash when building the ast on mismatched tags (1)" + ); + + assert_eq!( + micromark_to_mdast("a c d", &mdx) + .err() + .unwrap(), + "1:7: Unexpected closing tag ``, expected corresponding closing tag for `` (1:3) (mdx-jsx:end-tag-mismatch)", + "should crash when building the ast on mismatched tags (2)" + ); + + assert_eq!( + micromark_to_mdast("*a c* d.", &mdx).err().unwrap(), + "1:9: Expected a closing tag for `` (1:4) before the end of `Emphasis` (mdx-jsx:end-tag-mismatch)", + "should crash when building the ast on mismatched interleaving (1)" + ); + + assert_eq!( + micromark_to_mdast("b *c d*.", &mdx).err().unwrap(), + "1:8: Expected the closing tag `` either before the start of `Emphasis` (1:6), or another opening tag after that start (mdx-jsx:end-tag-mismatch)", + "should crash when building the ast on mismatched interleaving (2)" + ); + + assert_eq!( + micromark_to_mdast("a .", &mdx).err().unwrap(), + "1:7: Expected a closing tag for `` (1:3) before the end of `Paragraph` (mdx-jsx:end-tag-mismatch)", + "should crash when building the ast on mismatched interleaving (3)" + ); + + // Note: this is flow, not text. + assert_eq!( + micromark_to_mdast("", &mdx).err().unwrap(), + "1:4: Expected a closing tag for `` (1:1) (mdx-jsx:end-tag-mismatch)", + "should crash when building the ast on mismatched interleaving (4)" + ); + Ok(()) } diff --git a/tests/thematic_break.rs b/tests/thematic_break.rs index f4cb376..85ab37f 100644 --- a/tests/thematic_break.rs +++ b/tests/thematic_break.rs @@ -1,5 +1,8 @@ extern crate micromark; -use micromark::{micromark, micromark_with_options, Constructs, Options}; +use micromark::{ + mdast::{Node, Position, Root, ThematicBreak}, + micromark, micromark_to_mdast, micromark_with_options, Constructs, Options, +}; use pretty_assertions::assert_eq; #[test] @@ -181,5 +184,16 @@ fn thematic_break() -> Result<(), String> { "should support turning off thematic breaks" ); + assert_eq!( + micromark_to_mdast("***", &Options::default())?, + Node::Root(Root { + children: vec![Node::ThematicBreak(ThematicBreak { + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + })], + position: Some(Position::new(1, 1, 0, 1, 4, 3)) + }), + "should support thematic breaks as `ThematicBreak`s in mdast" + ); + Ok(()) } -- cgit