aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Titus Wormer <tituswormer@gmail.com>2022-09-28 17:54:39 +0200
committerLibravatar Titus Wormer <tituswormer@gmail.com>2022-09-28 17:55:44 +0200
commitb33a81e40620b8b3eaeeec9d0e0b34ca5958dead (patch)
treec91e56db38777b30cdcef591d0f7cd9bd1ac0ee8
parenta0c84c505d733be2e987a333a34244c1befb56cb (diff)
downloadmarkdown-rs-b33a81e40620b8b3eaeeec9d0e0b34ca5958dead.tar.gz
markdown-rs-b33a81e40620b8b3eaeeec9d0e0b34ca5958dead.tar.bz2
markdown-rs-b33a81e40620b8b3eaeeec9d0e0b34ca5958dead.zip
Add support for turning mdast to hast
Diffstat (limited to '')
-rw-r--r--.github/workflows/main.yml2
-rw-r--r--readme.md3
-rw-r--r--src/construct/attention.rs5
-rw-r--r--src/construct/gfm_table.rs6
-rw-r--r--src/construct/heading_atx.rs6
-rw-r--r--src/construct/heading_setext.rs6
-rw-r--r--src/construct/label_end.rs4
-rw-r--r--src/construct/list_item.rs6
-rw-r--r--src/construct/partial_data.rs6
-rw-r--r--src/construct/partial_mdx_expression.rs2
-rw-r--r--src/construct/string.rs6
-rw-r--r--src/construct/text.rs5
-rw-r--r--src/lib.rs12
-rw-r--r--src/mdast.rs77
-rw-r--r--src/resolve.rs24
-rw-r--r--src/to_mdast.rs6
-rw-r--r--src/unist.rs75
-rw-r--r--src/util/sanitize_uri.rs1
-rw-r--r--tests/attention.rs6
-rw-r--r--tests/autolink.rs6
-rw-r--r--tests/block_quote.rs6
-rw-r--r--tests/character_escape.rs6
-rw-r--r--tests/character_reference.rs6
-rw-r--r--tests/code_fenced.rs6
-rw-r--r--tests/code_indented.rs6
-rw-r--r--tests/code_text.rs6
-rw-r--r--tests/definition.rs6
-rw-r--r--tests/frontmatter.rs6
-rw-r--r--tests/gfm_autolink_literal.rs6
-rw-r--r--tests/gfm_footnote.rs6
-rw-r--r--tests/gfm_strikethrough.rs6
-rw-r--r--tests/gfm_table.rs6
-rw-r--r--tests/gfm_task_list_item.rs6
-rw-r--r--tests/hard_break_escape.rs6
-rw-r--r--tests/hard_break_trailing.rs6
-rw-r--r--tests/heading_atx.rs6
-rw-r--r--tests/heading_setext.rs6
-rw-r--r--tests/html_flow.rs6
-rw-r--r--tests/html_text.rs6
-rw-r--r--tests/image.rs8
-rw-r--r--tests/link_reference.rs6
-rw-r--r--tests/link_resource.rs6
-rw-r--r--tests/list.rs6
-rw-r--r--tests/math_flow.rs6
-rw-r--r--tests/math_text.rs6
-rw-r--r--tests/mdx_esm.rs8
-rw-r--r--tests/mdx_expression_flow.rs8
-rw-r--r--tests/mdx_expression_text.rs8
-rw-r--r--tests/mdx_jsx_flow.rs6
-rw-r--r--tests/mdx_jsx_text.rs8
-rw-r--r--tests/mdx_swc.rs2
-rw-r--r--tests/misc_bom.rs4
-rw-r--r--tests/misc_dangerous_protocol.rs12
-rw-r--r--tests/misc_soft_break.rs4
-rw-r--r--tests/misc_tabs.rs8
-rw-r--r--tests/misc_url.rs4
-rw-r--r--tests/misc_zero.rs4
-rw-r--r--tests/test_utils/hast.rs279
-rw-r--r--tests/test_utils/mod.rs248
-rw-r--r--tests/test_utils/swc.rs247
-rw-r--r--tests/test_utils/to_hast.rs1457
-rw-r--r--tests/text.rs4
-rw-r--r--tests/thematic_break.rs6
-rw-r--r--tests/xxx_hast.rs1585
64 files changed, 3851 insertions, 467 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d5d378b..9d45ca4 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -11,7 +11,7 @@ jobs:
with:
toolchain: stable
components: rustfmt, clippy
- - run: cargo clippy -- -D clippy::pedantic -D clippy::cargo -A clippy::doc_link_with_quotes -A clippy::unnecessary_wraps
+ - run: cargo clippy --examples --tests --benches -- -D clippy::pedantic -A clippy::doc_link_with_quotes -A clippy::too_many_lines
- run: cargo fmt --all -- --check
- run: cargo test
# - run: cargo install cargo-tarpaulin && cargo tarpaulin --out Xml
diff --git a/readme.md b/readme.md
index 3d18a00..24f2f02 100644
--- a/readme.md
+++ b/readme.md
@@ -363,7 +363,8 @@ The following scripts are useful when working on this project:
```
- lint:
```sh
- cargo fmt --check && cargo clippy -- -D clippy::pedantic -D clippy::cargo -A clippy::doc_link_with_quotes -A clippy::unnecessary_wraps
+ cargo fmt --check &&\
+ cargo clippy --examples --tests --benches -- -D clippy::pedantic -A clippy::doc_link_with_quotes -A clippy::too_many_lines
```
- test:
```sh
diff --git a/src/construct/attention.rs b/src/construct/attention.rs
index 4d58610..d99a52c 100644
--- a/src/construct/attention.rs
+++ b/src/construct/attention.rs
@@ -88,7 +88,6 @@ use crate::util::{
},
slice::Slice,
};
-use alloc::string::String;
use alloc::{vec, vec::Vec};
/// Attentention sequence that we can take markers from.
@@ -152,7 +151,7 @@ pub fn inside(tokenizer: &mut Tokenizer) -> State {
}
/// Resolve sequences.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
// Find all sequences, gather info about them.
let mut sequences = get_sequences(tokenizer);
@@ -224,7 +223,7 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
tokenizer.map.consume(&mut tokenizer.events);
- Ok(None)
+ None
}
/// Get sequences.
diff --git a/src/construct/gfm_table.rs b/src/construct/gfm_table.rs
index 63772c4..547358f 100644
--- a/src/construct/gfm_table.rs
+++ b/src/construct/gfm_table.rs
@@ -232,7 +232,7 @@ use crate::state::{Name as StateName, State};
use crate::subtokenize::Subresult;
use crate::tokenizer::Tokenizer;
use crate::util::{constant::TAB_SIZE, skip::opt_back as skip_opt_back};
-use alloc::{string::String, vec};
+use alloc::vec;
/// Start of a GFM table.
///
@@ -772,7 +772,7 @@ pub fn body_row_escape(tokenizer: &mut Tokenizer) -> State {
}
/// Resolve GFM table.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
let mut index = 0;
let mut in_first_cell_awaiting_pipe = true;
let mut in_row = false;
@@ -887,7 +887,7 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
flush_table_end(tokenizer, last_table_end, last_table_has_body);
}
- Ok(None)
+ None
}
/// Generate a cell.
diff --git a/src/construct/heading_atx.rs b/src/construct/heading_atx.rs
index b76e455..c867117 100644
--- a/src/construct/heading_atx.rs
+++ b/src/construct/heading_atx.rs
@@ -69,7 +69,7 @@ use crate::state::{Name as StateName, State};
use crate::subtokenize::Subresult;
use crate::tokenizer::Tokenizer;
use crate::util::constant::{HEADING_ATX_OPENING_FENCE_SIZE_MAX, TAB_SIZE};
-use alloc::{string::String, vec};
+use alloc::vec;
/// Start of a heading (atx).
///
@@ -223,7 +223,7 @@ pub fn data(tokenizer: &mut Tokenizer) -> State {
}
/// Resolve heading (atx).
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
let mut index = 0;
let mut heading_inside = false;
let mut data_start: Option<usize> = None;
@@ -283,5 +283,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
index += 1;
}
- Ok(None)
+ None
}
diff --git a/src/construct/heading_setext.rs b/src/construct/heading_setext.rs
index 3a484e1..1e6fd00 100644
--- a/src/construct/heading_setext.rs
+++ b/src/construct/heading_setext.rs
@@ -77,7 +77,7 @@ use crate::state::{Name as StateName, State};
use crate::subtokenize::Subresult;
use crate::tokenizer::Tokenizer;
use crate::util::{constant::TAB_SIZE, skip};
-use alloc::{string::String, vec};
+use alloc::vec;
/// At start of heading (setext) underline.
///
@@ -184,7 +184,7 @@ pub fn after(tokenizer: &mut Tokenizer) -> State {
}
/// Resolve heading (setext).
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
tokenizer.map.consume(&mut tokenizer.events);
let mut enter = skip::to(&tokenizer.events, 0, &[Name::HeadingSetextUnderline]);
@@ -281,5 +281,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
tokenizer.map.consume(&mut tokenizer.events);
- Ok(None)
+ None
}
diff --git a/src/construct/label_end.rs b/src/construct/label_end.rs
index 95b9a27..ca71245 100644
--- a/src/construct/label_end.rs
+++ b/src/construct/label_end.rs
@@ -661,7 +661,7 @@ pub fn reference_collapsed_open(tokenizer: &mut Tokenizer) -> State {
///
/// This turns matching label starts and label ends into links, images, and
/// footnotes, and turns unmatched label starts back into data.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
// Inject labels.
let labels = tokenizer.tokenize_state.labels.split_off(0);
inject_labels(tokenizer, &labels);
@@ -673,7 +673,7 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
tokenizer.map.consume(&mut tokenizer.events);
- Ok(None)
+ None
}
/// Inject links/images/footnotes.
diff --git a/src/construct/list_item.rs b/src/construct/list_item.rs
index 13b740b..a4f166d 100644
--- a/src/construct/list_item.rs
+++ b/src/construct/list_item.rs
@@ -69,7 +69,7 @@ use crate::util::{
skip,
slice::{Position, Slice},
};
-use alloc::{string::String, vec, vec::Vec};
+use alloc::{vec, vec::Vec};
/// Start of list item.
///
@@ -371,7 +371,7 @@ pub fn cont_filled(tokenizer: &mut Tokenizer) -> State {
}
/// Find adjacent list items with the same marker.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
let mut lists_wip: Vec<(u8, usize, usize, usize)> = vec![];
let mut lists: Vec<(u8, usize, usize, usize)> = vec![];
let mut index = 0;
@@ -474,5 +474,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
index += 1;
}
- Ok(None)
+ None
}
diff --git a/src/construct/partial_data.rs b/src/construct/partial_data.rs
index b36d9f0..a27730c 100644
--- a/src/construct/partial_data.rs
+++ b/src/construct/partial_data.rs
@@ -10,7 +10,7 @@ use crate::event::{Kind, Name};
use crate::state::{Name as StateName, State};
use crate::subtokenize::Subresult;
use crate::tokenizer::Tokenizer;
-use alloc::{string::String, vec};
+use alloc::vec;
/// At beginning of data.
///
@@ -73,7 +73,7 @@ pub fn inside(tokenizer: &mut Tokenizer) -> State {
}
/// Merge adjacent data events.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
let mut index = 0;
// Loop through events and merge adjacent data events.
@@ -105,5 +105,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
index += 1;
}
- Ok(None)
+ None
}
diff --git a/src/construct/partial_mdx_expression.rs b/src/construct/partial_mdx_expression.rs
index 3ebd0f0..789443e 100644
--- a/src/construct/partial_mdx_expression.rs
+++ b/src/construct/partial_mdx_expression.rs
@@ -219,7 +219,7 @@ fn parse_expression(tokenizer: &mut Tokenizer, parse: &MdxExpressionParse) -> St
};
// Parse and handle what was signaled back.
- match parse(&result.value, kind) {
+ match parse(&result.value, &kind) {
MdxSignal::Ok => State::Ok,
MdxSignal::Error(message, place) => {
let point = place_to_point(&result, place);
diff --git a/src/construct/string.rs b/src/construct/string.rs
index cf2f222..cad570d 100644
--- a/src/construct/string.rs
+++ b/src/construct/string.rs
@@ -17,7 +17,6 @@ use crate::resolve::Name as ResolveName;
use crate::state::{Name as StateName, State};
use crate::subtokenize::Subresult;
use crate::tokenizer::Tokenizer;
-use alloc::string::String;
/// Characters that can start something in string.
const MARKERS: [u8; 2] = [b'&', b'\\'];
@@ -76,8 +75,7 @@ pub fn before_data(tokenizer: &mut Tokenizer) -> State {
}
/// Resolve whitespace in string.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
resolve_whitespace(tokenizer, false, false);
-
- Ok(None)
+ None
}
diff --git a/src/construct/text.rs b/src/construct/text.rs
index 2648531..0ea0913 100644
--- a/src/construct/text.rs
+++ b/src/construct/text.rs
@@ -30,7 +30,6 @@ use crate::resolve::Name as ResolveName;
use crate::state::{Name as StateName, State};
use crate::subtokenize::Subresult;
use crate::tokenizer::Tokenizer;
-use alloc::string::String;
/// Characters that can start something in text.
const MARKERS: [u8; 16] = [
@@ -244,7 +243,7 @@ pub fn before_data(tokenizer: &mut Tokenizer) -> State {
}
/// Resolve whitespace.
-pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
+pub fn resolve(tokenizer: &mut Tokenizer) -> Option<Subresult> {
resolve_whitespace(
tokenizer,
tokenizer.parse_state.options.constructs.hard_break_trailing,
@@ -260,5 +259,5 @@ pub fn resolve(tokenizer: &mut Tokenizer) -> Result<Option<Subresult>, String> {
resolve_gfm_autolink_literal(tokenizer);
}
- Ok(None)
+ None
}
diff --git a/src/lib.rs b/src/lib.rs
index fcdab10..e552327 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,7 +17,7 @@ extern crate alloc;
mod construct;
mod event;
-pub mod mdast;
+pub mod mdast; // To do: externalize?
mod parser;
mod resolve;
mod state;
@@ -25,6 +25,7 @@ mod subtokenize;
mod to_html;
mod to_mdast;
mod tokenizer;
+pub mod unist; // To do: externalize.
mod util;
use alloc::{boxed::Box, fmt, string::String};
@@ -32,6 +33,7 @@ use mdast::Node;
use parser::parse;
use to_html::compile as to_html;
use to_mdast::compile as to_mdast;
+use util::sanitize_uri::sanitize;
/// Type of line endings in markdown.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
@@ -146,7 +148,7 @@ pub enum MdxExpressionKind {
/// Can be passed as `mdx_expression_parse` in [`Options`][] to support
/// expressions according to a certain grammar (typically, a programming
/// language).
-pub type MdxExpressionParse = dyn Fn(&str, MdxExpressionKind) -> MdxSignal;
+pub type MdxExpressionParse = dyn Fn(&str, &MdxExpressionKind) -> MdxSignal;
/// Signature of a function that parses ESM.
///
@@ -1187,3 +1189,9 @@ pub fn micromark_to_mdast(value: &str, options: &Options) -> Result<Node, String
let node = to_mdast(&events, bytes)?;
Ok(node)
}
+
+/// Do not use: exported for quick prototyping, will be removed.
+#[must_use]
+pub fn sanitize_(value: &str) -> String {
+ sanitize(value)
+}
diff --git a/src/mdast.rs b/src/mdast.rs
index 79a39dd..8b5b74d 100644
--- a/src/mdast.rs
+++ b/src/mdast.rs
@@ -1,83 +1,14 @@
-//! [mdast][] syntax tree.
+//! markdown syntax tree: [mdast][].
//!
//! [mdast]: https://github.com/syntax-tree/mdast
+use crate::unist::Position;
use alloc::{
fmt,
string::{String, ToString},
vec::Vec,
};
-/// One place in a source file.
-#[derive(Clone, Eq, PartialEq)]
-pub struct Point {
- /// 1-indexed integer representing a line in a source file.
- pub line: usize,
- /// 1-indexed integer representing a column in a source file.
- pub column: usize,
- /// 0-indexed integer representing a character in a source file.
- pub offset: usize,
-}
-
-impl Point {
- #[must_use]
- pub fn new(line: usize, column: usize, offset: usize) -> Point {
- Point {
- line,
- column,
- offset,
- }
- }
-}
-
-impl fmt::Debug for Point {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}:{} ({})", self.line, self.column, self.offset)
- }
-}
-
-/// Location of a node in a source file.
-#[derive(Clone, Eq, PartialEq)]
-pub struct Position {
- /// Represents the place of the first character of the parsed source region.
- pub start: Point,
- /// Represents the place of the first character after the parsed source
- /// region, whether it exists or not.
- pub end: Point,
-}
-
-impl Position {
- #[must_use]
- pub fn new(
- start_line: usize,
- start_column: usize,
- start_offset: usize,
- end_line: usize,
- end_column: usize,
- end_offset: usize,
- ) -> Position {
- Position {
- start: Point::new(start_line, start_column, start_offset),
- end: Point::new(end_line, end_column, end_offset),
- }
- }
-}
-
-impl fmt::Debug for Position {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(
- f,
- "{}:{}-{}:{} ({}-{})",
- self.start.line,
- self.start.column,
- self.end.line,
- self.end.column,
- self.start.offset,
- self.end.offset
- )
- }
-}
-
/// Explicitness of a reference.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReferenceKind {
@@ -370,7 +301,8 @@ impl Node {
}
}
- pub fn position(&mut self) -> Option<&Position> {
+ #[must_use]
+ pub fn position(&self) -> Option<&Position> {
match self {
Node::Root(x) => x.position.as_ref(),
Node::BlockQuote(x) => x.position.as_ref(),
@@ -1204,6 +1136,7 @@ pub struct MdxJsxAttribute {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::unist::{Point, Position};
use alloc::{string::ToString, vec};
#[test]
diff --git a/src/resolve.rs b/src/resolve.rs
index 2586676..813ce52 100644
--- a/src/resolve.rs
+++ b/src/resolve.rs
@@ -64,18 +64,18 @@ pub enum Name {
/// Call the corresponding resolver.
pub fn call(tokenizer: &mut Tokenizer, name: Name) -> Result<Option<Subresult>, String> {
- let func = match name {
- Name::Label => construct::label_end::resolve,
- Name::Attention => construct::attention::resolve,
- Name::GfmTable => construct::gfm_table::resolve,
- Name::HeadingAtx => construct::heading_atx::resolve,
- Name::HeadingSetext => construct::heading_setext::resolve,
- Name::ListItem => construct::list_item::resolve,
- Name::Content => construct::content::resolve,
- Name::Data => construct::partial_data::resolve,
- Name::String => construct::string::resolve,
- Name::Text => construct::text::resolve,
+ let result = match name {
+ Name::Label => construct::label_end::resolve(tokenizer),
+ Name::Attention => construct::attention::resolve(tokenizer),
+ Name::GfmTable => construct::gfm_table::resolve(tokenizer),
+ Name::HeadingAtx => construct::heading_atx::resolve(tokenizer),
+ Name::HeadingSetext => construct::heading_setext::resolve(tokenizer),
+ Name::ListItem => construct::list_item::resolve(tokenizer),
+ Name::Content => construct::content::resolve(tokenizer)?,
+ Name::Data => construct::partial_data::resolve(tokenizer),
+ Name::String => construct::string::resolve(tokenizer),
+ Name::Text => construct::text::resolve(tokenizer),
};
- func(tokenizer)
+ Ok(result)
}
diff --git a/src/to_mdast.rs b/src/to_mdast.rs
index 9f03a03..42f68a0 100644
--- a/src/to_mdast.rs
+++ b/src/to_mdast.rs
@@ -5,10 +5,10 @@ use crate::mdast::{
AttributeContent, AttributeValue, BlockQuote, Break, Code, Definition, Delete, Emphasis,
FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode,
InlineMath, Link, LinkReference, List, ListItem, Math, MdxFlowExpression, MdxJsxAttribute,
- MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph, Point,
- Position, ReferenceKind, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml,
- Yaml,
+ MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph,
+ ReferenceKind, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml, Yaml,
};
+use crate::unist::{Point, Position};
use crate::util::{
decode_character_reference::{decode_named, decode_numeric},
infer::{gfm_table_align, list_item_loose, list_loose},
diff --git a/src/unist.rs b/src/unist.rs
new file mode 100644
index 0000000..75ef359
--- /dev/null
+++ b/src/unist.rs
@@ -0,0 +1,75 @@
+//! abstract syntax trees: [unist][].
+//!
+//! [unist]: https://github.com/syntax-tree/unist
+
+use alloc::fmt;
+
+/// One place in a source file.
+#[derive(Clone, Eq, PartialEq)]
+pub struct Point {
+ /// 1-indexed integer representing a line in a source file.
+ pub line: usize,
+ /// 1-indexed integer representing a column in a source file.
+ pub column: usize,
+ /// 0-indexed integer representing a character in a source file.
+ pub offset: usize,
+}
+
+impl Point {
+ #[must_use]
+ pub fn new(line: usize, column: usize, offset: usize) -> Point {
+ Point {
+ line,
+ column,
+ offset,
+ }
+ }
+}
+
+impl fmt::Debug for Point {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}:{} ({})", self.line, self.column, self.offset)
+ }
+}
+
+/// Location of a node in a source file.
+#[derive(Clone, Eq, PartialEq)]
+pub struct Position {
+ /// Represents the place of the first character of the parsed source region.
+ pub start: Point,
+ /// Represents the place of the first character after the parsed source
+ /// region, whether it exists or not.
+ pub end: Point,
+}
+
+impl Position {
+ #[must_use]
+ pub fn new(
+ start_line: usize,
+ start_column: usize,
+ start_offset: usize,
+ end_line: usize,
+ end_column: usize,
+ end_offset: usize,
+ ) -> Position {
+ Position {
+ start: Point::new(start_line, start_column, start_offset),
+ end: Point::new(end_line, end_column, end_offset),
+ }
+ }
+}
+
+impl fmt::Debug for Position {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "{}:{}-{}:{} ({}-{})",
+ self.start.line,
+ self.start.column,
+ self.end.line,
+ self.end.column,
+ self.start.offset,
+ self.end.offset
+ )
+ }
+}
diff --git a/src/util/sanitize_uri.rs b/src/util/sanitize_uri.rs
index 0099347..8e44758 100644
--- a/src/util/sanitize_uri.rs
+++ b/src/util/sanitize_uri.rs
@@ -26,6 +26,7 @@ use alloc::{
/// ## References
///
/// * [`micromark-util-sanitize-uri` in `micromark`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri)
+#[must_use]
pub fn sanitize(value: &str) -> String {
encode(&*normalize(value), true)
}
diff --git a/tests/attention.rs b/tests/attention.rs
index 607af58..abed33c 100644
--- a/tests/attention.rs
+++ b/tests/attention.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Emphasis, Node, Paragraph, Position, Root, Strong, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Emphasis, Node, Paragraph, Root, Strong, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/autolink.rs b/tests/autolink.rs
index cc30512..78725b2 100644
--- a/tests/autolink.rs
+++ b/tests/autolink.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Link, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Link, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/block_quote.rs b/tests/block_quote.rs
index 9cd7d46..6b155c5 100644
--- a/tests/block_quote.rs
+++ b/tests/block_quote.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{BlockQuote, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{BlockQuote, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/character_escape.rs b/tests/character_escape.rs
index e0a3ed3..44eff0b 100644
--- a/tests/character_escape.rs
+++ b/tests/character_escape.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/character_reference.rs b/tests/character_reference.rs
index 7385734..ccf506e 100644
--- a/tests/character_reference.rs
+++ b/tests/character_reference.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/code_fenced.rs b/tests/code_fenced.rs
index 2f770ce..06f0d6a 100644
--- a/tests/code_fenced.rs
+++ b/tests/code_fenced.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Code, Node, Position, Root},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Code, Node, Root},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/code_indented.rs b/tests/code_indented.rs
index 8a15693..7ea08b5 100644
--- a/tests/code_indented.rs
+++ b/tests/code_indented.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Code, Node, Position, Root},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Code, Node, Root},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/code_text.rs b/tests/code_text.rs
index c1ba861..f6a3379 100644
--- a/tests/code_text.rs
+++ b/tests/code_text.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{InlineCode, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{InlineCode, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/definition.rs b/tests/definition.rs
index c3bf183..bf9d8ad 100644
--- a/tests/definition.rs
+++ b/tests/definition.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Definition, Node, Position, Root},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Definition, Node, Root},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/frontmatter.rs b/tests/frontmatter.rs
index c5b0d3a..e9f6648 100644
--- a/tests/frontmatter.rs
+++ b/tests/frontmatter.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Node, Position, Root, Toml, Yaml},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Node, Root, Toml, Yaml},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/gfm_autolink_literal.rs b/tests/gfm_autolink_literal.rs
index bf99071..d699343 100644
--- a/tests/gfm_autolink_literal.rs
+++ b/tests/gfm_autolink_literal.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Link, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Link, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/gfm_footnote.rs b/tests/gfm_footnote.rs
index 8785239..364bf90 100644
--- a/tests/gfm_footnote.rs
+++ b/tests/gfm_footnote.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{FootnoteDefinition, FootnoteReference, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/gfm_strikethrough.rs b/tests/gfm_strikethrough.rs
index d669a96..e392700 100644
--- a/tests/gfm_strikethrough.rs
+++ b/tests/gfm_strikethrough.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Delete, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Delete, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/gfm_table.rs b/tests/gfm_table.rs
index 17b31da..d6bd022 100644
--- a/tests/gfm_table.rs
+++ b/tests/gfm_table.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{AlignKind, InlineCode, Node, Position, Root, Table, TableCell, TableRow, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{AlignKind, InlineCode, Node, Root, Table, TableCell, TableRow, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/gfm_task_list_item.rs b/tests/gfm_task_list_item.rs
index ae6c548..1a0b682 100644
--- a/tests/gfm_task_list_item.rs
+++ b/tests/gfm_task_list_item.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{List, ListItem, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{List, ListItem, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/hard_break_escape.rs b/tests/hard_break_escape.rs
index ced3b3d..9a984bf 100644
--- a/tests/hard_break_escape.rs
+++ b/tests/hard_break_escape.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Break, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Break, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/hard_break_trailing.rs b/tests/hard_break_trailing.rs
index 042f8f0..41a320e 100644
--- a/tests/hard_break_trailing.rs
+++ b/tests/hard_break_trailing.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Break, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Break, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/heading_atx.rs b/tests/heading_atx.rs
index da83ff5..c13ef1a 100644
--- a/tests/heading_atx.rs
+++ b/tests/heading_atx.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Heading, Node, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Heading, Node, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/heading_setext.rs b/tests/heading_setext.rs
index 4292ed2..8050ed2 100644
--- a/tests/heading_setext.rs
+++ b/tests/heading_setext.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Heading, Node, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Heading, Node, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/html_flow.rs b/tests/html_flow.rs
index 2605105..2f1d85f 100644
--- a/tests/html_flow.rs
+++ b/tests/html_flow.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Html, Node, Position, Root},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Html, Node, Root},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/html_text.rs b/tests/html_text.rs
index d35bdba..40e860e 100644
--- a/tests/html_text.rs
+++ b/tests/html_text.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Html, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Html, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/image.rs b/tests/image.rs
index 40d1449..6669e8d 100644
--- a/tests/image.rs
+++ b/tests/image.rs
@@ -1,9 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{
- Definition, Image, ImageReference, Node, Paragraph, Position, ReferenceKind, Root, Text,
- },
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Definition, Image, ImageReference, Node, Paragraph, ReferenceKind, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/link_reference.rs b/tests/link_reference.rs
index d1d6785..680bb1d 100644
--- a/tests/link_reference.rs
+++ b/tests/link_reference.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Definition, LinkReference, Node, Paragraph, Position, ReferenceKind, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Definition, LinkReference, Node, Paragraph, ReferenceKind, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/link_resource.rs b/tests/link_resource.rs
index a296410..ef79653 100644
--- a/tests/link_resource.rs
+++ b/tests/link_resource.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Link, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Options,
+ mdast::{Link, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/list.rs b/tests/list.rs
index 95beeec..d485d49 100644
--- a/tests/list.rs
+++ b/tests/list.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{List, ListItem, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{List, ListItem, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/math_flow.rs b/tests/math_flow.rs
index 3797e83..abb1f32 100644
--- a/tests/math_flow.rs
+++ b/tests/math_flow.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Math, Node, Position, Root},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Math, Node, Root},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/math_text.rs b/tests/math_text.rs
index 9e20d6e..76aa0aa 100644
--- a/tests/math_text.rs
+++ b/tests/math_text.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{InlineMath, Node, Paragraph, Position, Root, Text},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{InlineMath, Node, Paragraph, Root, Text},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/mdx_esm.rs b/tests/mdx_esm.rs
index 0f8888b..a9c7f4a 100644
--- a/tests/mdx_esm.rs
+++ b/tests/mdx_esm.rs
@@ -1,11 +1,13 @@
extern crate micromark;
mod test_utils;
use micromark::{
- mdast::{MdxjsEsm, Node, Position, Root},
- micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{MdxjsEsm, Node, Root},
+ micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
-use test_utils::{parse_esm, parse_expression};
+use test_utils::swc::{parse_esm, parse_expression};
#[test]
fn mdx_esm() -> Result<(), String> {
diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs
index 1d50468..b02d32b 100644
--- a/tests/mdx_expression_flow.rs
+++ b/tests/mdx_expression_flow.rs
@@ -1,11 +1,13 @@
extern crate micromark;
mod test_utils;
use micromark::{
- mdast::{MdxFlowExpression, Node, Position, Root},
- micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{MdxFlowExpression, Node, Root},
+ micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
-use test_utils::{parse_esm, parse_expression};
+use test_utils::swc::{parse_esm, parse_expression};
#[test]
fn mdx_expression_flow_agnostic() -> Result<(), String> {
diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs
index 997e7de..9eb9dbf 100644
--- a/tests/mdx_expression_text.rs
+++ b/tests/mdx_expression_text.rs
@@ -1,11 +1,13 @@
extern crate micromark;
mod test_utils;
use micromark::{
- mdast::{MdxTextExpression, Node, Paragraph, Position, Root, Text},
- micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{MdxTextExpression, Node, Paragraph, Root, Text},
+ micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
-use test_utils::{parse_esm, parse_expression};
+use test_utils::swc::{parse_esm, parse_expression};
#[test]
fn mdx_expression_text_gnostic_core() -> Result<(), String> {
diff --git a/tests/mdx_jsx_flow.rs b/tests/mdx_jsx_flow.rs
index 14e14f0..54914e6 100644
--- a/tests/mdx_jsx_flow.rs
+++ b/tests/mdx_jsx_flow.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{List, ListItem, MdxJsxFlowElement, Node, Paragraph, Position, Root, Text},
- micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{List, ListItem, MdxJsxFlowElement, Node, Paragraph, Root, Text},
+ micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs
index 94e7b0c..ea3502f 100644
--- a/tests/mdx_jsx_text.rs
+++ b/tests/mdx_jsx_text.rs
@@ -3,12 +3,14 @@ mod test_utils;
use micromark::{
mdast::{
AttributeContent, AttributeValue, Emphasis, MdxJsxAttribute, MdxJsxTextElement, Node,
- Paragraph, Position, Root, Text,
+ Paragraph, Root, Text,
},
- micromark_to_mdast, micromark_with_options, Constructs, Options,
+ micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
-use test_utils::{parse_esm, parse_expression};
+use test_utils::swc::{parse_esm, parse_expression};
#[test]
fn mdx_jsx_text_core() -> Result<(), String> {
diff --git a/tests/mdx_swc.rs b/tests/mdx_swc.rs
index c9a2a61..74f975a 100644
--- a/tests/mdx_swc.rs
+++ b/tests/mdx_swc.rs
@@ -2,7 +2,7 @@ extern crate micromark;
mod test_utils;
use micromark::{micromark_with_options, Constructs, Options};
use pretty_assertions::assert_eq;
-use test_utils::{parse_esm, parse_expression};
+use test_utils::swc::{parse_esm, parse_expression};
#[test]
fn mdx_swc() -> Result<(), String> {
diff --git a/tests/misc_bom.rs b/tests/misc_bom.rs
index e26b407..47ce902 100644
--- a/tests/misc_bom.rs
+++ b/tests/misc_bom.rs
@@ -3,7 +3,7 @@ use micromark::micromark;
use pretty_assertions::assert_eq;
#[test]
-fn bom() -> Result<(), String> {
+fn bom() {
assert_eq!(micromark("\u{FEFF}"), "", "should ignore just a bom");
assert_eq!(
@@ -11,6 +11,4 @@ fn bom() -> Result<(), String> {
"<h1>hea\u{FEFF}ding</h1>",
"should ignore a bom"
);
-
- Ok(())
}
diff --git a/tests/misc_dangerous_protocol.rs b/tests/misc_dangerous_protocol.rs
index 88058f2..0c25eba 100644
--- a/tests/misc_dangerous_protocol.rs
+++ b/tests/misc_dangerous_protocol.rs
@@ -3,7 +3,7 @@ use micromark::micromark;
use pretty_assertions::assert_eq;
#[test]
-fn dangerous_protocol_autolink() -> Result<(), String> {
+fn dangerous_protocol_autolink() {
assert_eq!(
micromark("<javascript:alert(1)>"),
"<p><a href=\"\">javascript:alert(1)</a></p>",
@@ -33,12 +33,10 @@ fn dangerous_protocol_autolink() -> Result<(), String> {
"<p><a href=\"mailto:a\">mailto:a</a></p>",
"should allow `mailto:`"
);
-
- Ok(())
}
#[test]
-fn dangerous_protocol_image() -> Result<(), String> {
+fn dangerous_protocol_image() {
assert_eq!(
micromark("![](javascript:alert(1))"),
"<p><img src=\"\" alt=\"\" /></p>",
@@ -116,12 +114,10 @@ fn dangerous_protocol_image() -> Result<(), String> {
"<p><img src=\"a/b:c\" alt=\"\" /></p>",
"should allow a colon in a path"
);
-
- Ok(())
}
#[test]
-fn dangerous_protocol_link() -> Result<(), String> {
+fn dangerous_protocol_link() {
assert_eq!(
micromark("[](javascript:alert(1))"),
"<p><a href=\"\"></a></p>",
@@ -199,6 +195,4 @@ fn dangerous_protocol_link() -> Result<(), String> {
"<p><a href=\"a/b:c\"></a></p>",
"should allow a colon in a path"
);
-
- Ok(())
}
diff --git a/tests/misc_soft_break.rs b/tests/misc_soft_break.rs
index 746b41d..43e2f3d 100644
--- a/tests/misc_soft_break.rs
+++ b/tests/misc_soft_break.rs
@@ -3,7 +3,7 @@ use micromark::micromark;
use pretty_assertions::assert_eq;
#[test]
-fn soft_break() -> Result<(), String> {
+fn soft_break() {
assert_eq!(
micromark("foo\nbaz"),
"<p>foo\nbaz</p>",
@@ -15,6 +15,4 @@ fn soft_break() -> Result<(), String> {
"<p>foo\nbaz</p>",
"should trim spaces around line endings"
);
-
- Ok(())
}
diff --git a/tests/misc_tabs.rs b/tests/misc_tabs.rs
index feb8177..5cd9f69 100644
--- a/tests/misc_tabs.rs
+++ b/tests/misc_tabs.rs
@@ -133,7 +133,7 @@ fn tabs_flow() -> Result<(), String> {
}
#[test]
-fn tabs_text() -> Result<(), String> {
+fn tabs_text() {
assert_eq!(
micromark("<http:\t>"),
"<p>&lt;http:\t&gt;</p>",
@@ -251,12 +251,10 @@ fn tabs_text() -> Result<(), String> {
"<p><a href=\"y\" title=\"z\">x</a></p>",
"should support a tab between a link destination and title"
);
-
- Ok(())
}
#[test]
-fn tabs_virtual_spaces() -> Result<(), String> {
+fn tabs_virtual_spaces() {
assert_eq!(
micromark("```\n\tx"),
"<pre><code>\tx\n</code></pre>\n",
@@ -288,6 +286,4 @@ fn tabs_virtual_spaces() -> Result<(), String> {
// "<ul>\n<li>\n<p>a</p>\n<p>b</p>\n</li>\n</ul>",
"should support a part of a tab as a container, and the rest of a tab as flow"
);
-
- Ok(())
}
diff --git a/tests/misc_url.rs b/tests/misc_url.rs
index fd9ae05..4fff26d 100644
--- a/tests/misc_url.rs
+++ b/tests/misc_url.rs
@@ -3,7 +3,7 @@ use micromark::micromark;
use pretty_assertions::assert_eq;
#[test]
-fn url() -> Result<(), String> {
+fn url() {
assert_eq!(
micromark("<https://%>"),
"<p><a href=\"https://%25\">https://%</a></p>",
@@ -145,6 +145,4 @@ fn url() -> Result<(), String> {
format!("<p><a href=\"{}\"></a></p>", ascii_out),
"should support ascii characters"
);
-
- Ok(())
}
diff --git a/tests/misc_zero.rs b/tests/misc_zero.rs
index f8d0c56..0b54d50 100644
--- a/tests/misc_zero.rs
+++ b/tests/misc_zero.rs
@@ -3,7 +3,7 @@ use micromark::micromark;
use pretty_assertions::assert_eq;
#[test]
-fn zero() -> Result<(), String> {
+fn zero() {
assert_eq!(micromark(""), "", "should support no markdown");
assert_eq!(
@@ -25,6 +25,4 @@ fn zero() -> Result<(), String> {
"<p>\\0</p>",
"should not support NUL in a character escape"
);
-
- Ok(())
}
diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs
new file mode 100644
index 0000000..4adf0ca
--- /dev/null
+++ b/tests/test_utils/hast.rs
@@ -0,0 +1,279 @@
+#![allow(dead_code)]
+
+// ^-- fix later
+
+extern crate alloc;
+extern crate micromark;
+use alloc::{
+ fmt,
+ string::{String, ToString},
+ vec::Vec,
+};
+use micromark::{mdast::AttributeContent, unist::Position};
+
+/// Nodes.
+#[derive(Clone, PartialEq)]
+pub enum Node {
+ /// Root.
+ Root(Root),
+ /// Element.
+ Element(Element),
+ /// Document type.
+ Doctype(Doctype),
+ /// Comment.
+ Comment(Comment),
+ /// Text.
+ Text(Text),
+
+ // MDX being passed through.
+ /// MDX: JSX element.
+ MdxJsxElement(MdxJsxElement),
+ /// MDX.js ESM.
+ MdxjsEsm(MdxjsEsm),
+ // MDX: expression.
+ MdxExpression(MdxExpression),
+}
+
+impl fmt::Debug for Node {
+ // Debug the wrapped struct.
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Node::Root(x) => write!(f, "{:?}", x),
+ Node::Element(x) => write!(f, "{:?}", x),
+ Node::Doctype(x) => write!(f, "{:?}", x),
+ Node::Comment(x) => write!(f, "{:?}", x),
+ Node::Text(x) => write!(f, "{:?}", x),
+ Node::MdxJsxElement(x) => write!(f, "{:?}", x),
+ Node::MdxExpression(x) => write!(f, "{:?}", x),
+ Node::MdxjsEsm(x) => write!(f, "{:?}", x),
+ }
+ }
+}
+
+fn children_to_string(children: &[Node]) -> String {
+ children.iter().map(ToString::to_string).collect()
+}
+
+impl ToString for Node {
+ fn to_string(&self) -> String {
+ match self {
+ // Parents.
+ Node::Root(x) => children_to_string(&x.children),
+ Node::Element(x) => children_to_string(&x.children),
+ Node::MdxJsxElement(x) => children_to_string(&x.children),
+ // Literals.
+ Node::Comment(x) => x.value.clone(),
+ Node::Text(x) => x.value.clone(),
+ Node::MdxExpression(x) => x.value.clone(),
+ Node::MdxjsEsm(x) => x.value.clone(),
+ // Voids.
+ Node::Doctype(_) => "".to_string(),
+ }
+ }
+}
+
+impl Node {
+ #[must_use]
+ pub fn children(&self) -> Option<&Vec<Node>> {
+ match self {
+ // Parent.
+ Node::Root(x) => Some(&x.children),
+ Node::Element(x) => Some(&x.children),
+ Node::MdxJsxElement(x) => Some(&x.children),
+ // Non-parent.
+ _ => None,
+ }
+ }
+
+ pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
+ match self {
+ // Parent.
+ Node::Root(x) => Some(&mut x.children),
+ Node::Element(x) => Some(&mut x.children),
+ Node::MdxJsxElement(x) => Some(&mut x.children),
+ // Non-parent.
+ _ => None,
+ }
+ }
+
+ pub fn position(&self) -> Option<&Position> {
+ match self {
+ Node::Root(x) => x.position.as_ref(),
+ Node::Element(x) => x.position.as_ref(),
+ Node::Doctype(x) => x.position.as_ref(),
+ Node::Comment(x) => x.position.as_ref(),
+ Node::Text(x) => x.position.as_ref(),
+ Node::MdxJsxElement(x) => x.position.as_ref(),
+ Node::MdxExpression(x) => x.position.as_ref(),
+ Node::MdxjsEsm(x) => x.position.as_ref(),
+ }
+ }
+
+ pub fn position_mut(&mut self) -> Option<&mut Position> {
+ match self {
+ Node::Root(x) => x.position.as_mut(),
+ Node::Element(x) => x.position.as_mut(),
+ Node::Doctype(x) => x.position.as_mut(),
+ Node::Comment(x) => x.position.as_mut(),
+ Node::Text(x) => x.position.as_mut(),
+ Node::MdxJsxElement(x) => x.position.as_mut(),
+ Node::MdxExpression(x) => x.position.as_mut(),
+ Node::MdxjsEsm(x) => x.position.as_mut(),
+ }
+ }
+
+ pub fn position_set(&mut self, position: Option<Position>) {
+ match self {
+ Node::Root(x) => x.position = position,
+ Node::Element(x) => x.position = position,
+ Node::Doctype(x) => x.position = position,
+ Node::Comment(x) => x.position = position,
+ Node::Text(x) => x.position = position,
+ Node::MdxJsxElement(x) => x.position = position,
+ Node::MdxExpression(x) => x.position = position,
+ Node::MdxjsEsm(x) => x.position = position,
+ }
+ }
+}
+
+/// Document.
+///
+/// ```html
+/// > | a
+/// ^
+/// ```
+#[derive(Clone, Debug, PartialEq)]
+pub struct Root {
+ // Parent.
+ /// Content model.
+ pub children: Vec<Node>,
+ /// Positional info.
+ pub position: Option<Position>,
+}
+
+/// Document type.
+///
+/// ```html
+/// > | <!doctype html>
+/// ^^^^^^^^^^^^^^^
+/// ```
+// To do: clone.
+#[derive(Clone, Debug, PartialEq)]
+pub struct Element {
+ pub tag_name: String,
+ pub properties: Vec<(String, PropertyValue)>,
+ // Parent.
+ pub children: Vec<Node>,
+ /// Positional info.
+ pub position: Option<Position>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum PropertyItem {
+ Number(f32),
+ String(String),
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum PropertyValue {
+ Number(f32),
+ Boolean(bool),
+ String(String),
+ CommaSeparated(Vec<PropertyItem>),
+ SpaceSeparated(Vec<PropertyItem>),
+}
+
+/// Document type.
+///
+/// ```html
+/// > | <!doctype html>
+/// ^^^^^^^^^^^^^^^
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Doctype {
+ // Void.
+ /// Positional info.
+ pub position: Option<Position>,
+}
+
+/// Comment.
+///
+/// ```html
+/// > | <!-- a -->
+/// ^^^^^^^^^^
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Comment {
+ // Text.
+ /// Content model.
+ pub value: String,
+ /// Positional info.
+ pub position: Option<Position>,
+}
+
+/// Text.
+///
+/// ```html
+/// > | a
+/// ^
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Text {
+ // Text.
+ /// Content model.
+ pub value: String,
+ /// Positional info.
+ pub position: Option<Position>,
+}
+
+/// MDX: JSX element.
+///
+/// ```markdown
+/// > | <a />
+/// ^^^^^
+/// ```
+#[derive(Clone, Debug, PartialEq)]
+pub struct MdxJsxElement {
+ // Parent.
+ /// Content model.
+ pub children: Vec<Node>,
+ /// Positional info.
+ pub position: Option<Position>,
+ // JSX element.
+ /// Name.
+ ///
+ /// Fragments have no name.
+ pub name: Option<String>,
+ /// Attributes.
+ pub attributes: Vec<AttributeContent>,
+}
+
+/// MDX: expression.
+///
+/// ```markdown
+/// > | {a}
+/// ^^^
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct MdxExpression {
+ // Literal.
+ /// Content model.
+ pub value: String,
+ /// Positional info.
+ pub position: Option<Position>,
+}
+
+/// MDX: ESM.
+///
+/// ```markdown
+/// > | import a from 'b'
+/// ^^^^^^^^^^^^^^^^^
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct MdxjsEsm {
+ // Literal.
+ /// Content model.
+ pub value: String,
+ /// Positional info.
+ pub position: Option<Position>,
+}
diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs
index 10b9643..111118f 100644
--- a/tests/test_utils/mod.rs
+++ b/tests/test_utils/mod.rs
@@ -1,245 +1,3 @@
-extern crate micromark;
-extern crate swc_common;
-extern crate swc_ecma_ast;
-extern crate swc_ecma_parser;
-use micromark::{MdxExpressionKind, MdxSignal};
-use swc_common::{source_map::Pos, BytePos, FileName, SourceFile, Spanned};
-use swc_ecma_ast::{EsVersion, Expr, Module};
-use swc_ecma_parser::{
- error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax,
-};
-
-/// Parse ESM in MDX with SWC.
-pub fn parse_esm(value: &str) -> MdxSignal {
- let (file, syntax, version) = create_config(value.to_string());
- let mut errors = vec![];
- let result = parse_file_as_module(&file, syntax, version, None, &mut errors);
-
- match result {
- Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"),
- Ok(tree) => {
- if errors.is_empty() {
- check_esm_ast(tree)
- } else {
- if errors.len() > 1 {
- println!("parse_esm: todo: multiple errors? {:?}", errors);
- }
- swc_error_to_signal(&errors[0], value.len(), 0, "esm")
- }
- }
- }
-}
-
-/// Parse expressions in MDX with SWC.
-pub fn parse_expression(value: &str, kind: MdxExpressionKind) -> MdxSignal {
- // Empty expressions are OK.
- if matches!(kind, MdxExpressionKind::Expression)
- && matches!(whitespace_and_comments(0, value), MdxSignal::Ok)
- {
- return MdxSignal::Ok;
- }
-
- // For attribute expression, a spread is needed, for which we have to prefix
- // and suffix the input.
- // See `check_expression_ast` for how the AST is verified.
- let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) {
- ("({", "})")
- } else {
- ("", "")
- };
-
- let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix));
- let mut errors = vec![];
- let result = parse_file_as_expr(&file, syntax, version, None, &mut errors);
-
- match result {
- Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"),
- Ok(tree) => {
- if errors.is_empty() {
- let place = fix_swc_position(tree.span().hi.to_usize(), prefix.len());
- let result = check_expression_ast(tree, kind);
- if matches!(result, MdxSignal::Ok) {
- whitespace_and_comments(place, value)
- } else {
- result
- }
- } else {
- if errors.len() > 1 {
- unreachable!("parse_expression: todo: multiple errors? {:?}", errors);
- }
- swc_error_to_signal(&errors[0], value.len(), prefix.len(), "expression")
- }
- }
- }
-}
-
-/// Check that the resulting AST of ESM is OK.
-///
-/// This checks that only module declarations (import/exports) are used, not
-/// statements.
-fn check_esm_ast(tree: Module) -> MdxSignal {
- let mut index = 0;
- while index < tree.body.len() {
- let node = &tree.body[index];
-
- if !node.is_module_decl() {
- let place = fix_swc_position(node.span().hi.to_usize(), 0);
- return MdxSignal::Error(
- "Unexpected statement in code: only import/exports are supported".to_string(),
- place,
- );
- }
-
- index += 1;
- }
-
- MdxSignal::Ok
-}
-
-/// Check that the resulting AST of an expressions is OK.
-///
-/// This checks that attribute expressions are the expected spread.
-fn check_expression_ast(tree: Box<Expr>, kind: MdxExpressionKind) -> MdxSignal {
- if matches!(kind, MdxExpressionKind::AttributeExpression)
- && tree
- .unwrap_parens()
- .as_object()
- .and_then(|object| {
- if object.props.len() == 1 {
- object.props[0].as_spread()
- } else {
- None
- }
- })
- .is_none()
- {
- MdxSignal::Error(
- "Expected a single spread value, such as `...x`".to_string(),
- 0,
- )
- } else {
- MdxSignal::Ok
- }
-}
-
-/// Turn an SWC error into an `MdxSignal`.
-///
-/// * If the error happens at `value_len`, yields `MdxSignal::Eof`
-/// * Else, yields `MdxSignal::Error`.
-fn swc_error_to_signal(
- error: &SwcError,
- value_len: usize,
- prefix_len: usize,
- name: &str,
-) -> MdxSignal {
- let message = error.kind().msg().to_string();
- let place = fix_swc_position(error.span().hi.to_usize(), prefix_len);
- let message = format!("Could not parse {} with swc: {}", name, message);
-
- if place >= value_len {
- MdxSignal::Eof(message)
- } else {
- MdxSignal::Error(message, place)
- }
-}
-
-/// Move past JavaScript whitespace (well, actually ASCII whitespace) and
-/// comments.
-///
-/// This is needed because for expressions, we use an API that parses up to
-/// a valid expression, but there may be more expressions after it, which we
-/// don’t alow.
-fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal {
- let bytes = value.as_bytes();
- let len = bytes.len();
- let mut in_multiline = false;
- let mut in_line = false;
-
- while index < len {
- // In a multiline comment: `/* a */`.
- if in_multiline {
- if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' {
- index += 1;
- in_multiline = false;
- }
- }
- // In a line comment: `// a`.
- else if in_line {
- if index + 1 < len && bytes[index] == b'\r' && bytes[index + 1] == b'\n' {
- index += 1;
- in_line = false;
- } else if bytes[index] == b'\r' || bytes[index] == b'\n' {
- in_line = false;
- }
- }
- // Not in a comment, opening a multiline comment: `/* a */`.
- else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' {
- index += 1;
- in_multiline = true;
- }
- // Not in a comment, opening a line comment: `// a`.
- else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' {
- index += 1;
- in_line = true;
- }
- // Outside comment, whitespace.
- else if bytes[index].is_ascii_whitespace() {
- // Fine!
- }
- // Outside comment, not whitespace.
- else {
- return MdxSignal::Error(
- "Could not parse expression with swc: Unexpected content after expression"
- .to_string(),
- index,
- );
- }
-
- index += 1;
- }
-
- if in_multiline {
- MdxSignal::Error(
- "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".to_string(),
- index,
- )
- } else if in_line {
- // EOF instead of EOL is specifically not allowed, because that would
- // mean the closing brace is on the commented-out line
- MdxSignal::Error(
- "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".to_string(),
- index,
- )
- } else {
- MdxSignal::Ok
- }
-}
-
-/// Create configuration for SWC, shared between ESM and expressions.
-///
-/// This enables modern JavaScript (ES2022) + JSX.
-fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) {
- (
- // File.
- SourceFile::new(
- FileName::Anon,
- false,
- FileName::Anon,
- source,
- BytePos::from_usize(1),
- ),
- // Syntax.
- Syntax::Es(EsConfig {
- jsx: true,
- ..EsConfig::default()
- }),
- // Version.
- EsVersion::Es2022,
- )
-}
-
-/// Turn an SWC byte position from a resulting AST to an offset in the original
-/// input string.
-fn fix_swc_position(index: usize, prefix_len: usize) -> usize {
- index - 1 - prefix_len
-}
+pub mod hast;
+pub mod swc;
+pub mod to_hast;
diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs
new file mode 100644
index 0000000..f455674
--- /dev/null
+++ b/tests/test_utils/swc.rs
@@ -0,0 +1,247 @@
+extern crate micromark;
+extern crate swc_common;
+extern crate swc_ecma_ast;
+extern crate swc_ecma_parser;
+use micromark::{MdxExpressionKind, MdxSignal};
+use swc_common::{source_map::Pos, BytePos, FileName, SourceFile, Spanned};
+use swc_ecma_ast::{EsVersion, Expr, Module};
+use swc_ecma_parser::{
+ error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax,
+};
+
+/// Parse ESM in MDX with SWC.
+#[allow(dead_code)]
+pub fn parse_esm(value: &str) -> MdxSignal {
+ let (file, syntax, version) = create_config(value.to_string());
+ let mut errors = vec![];
+ let result = parse_file_as_module(&file, syntax, version, None, &mut errors);
+
+ match result {
+ Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"),
+ Ok(tree) => {
+ if errors.is_empty() {
+ check_esm_ast(&tree)
+ } else {
+ if errors.len() > 1 {
+ println!("parse_esm: todo: multiple errors? {:?}", errors);
+ }
+ swc_error_to_signal(&errors[0], value.len(), 0, "esm")
+ }
+ }
+ }
+}
+
+/// Parse expressions in MDX with SWC.
+#[allow(dead_code)]
+pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal {
+ // Empty expressions are OK.
+ if matches!(kind, MdxExpressionKind::Expression)
+ && matches!(whitespace_and_comments(0, value), MdxSignal::Ok)
+ {
+ return MdxSignal::Ok;
+ }
+
+ // For attribute expression, a spread is needed, for which we have to prefix
+ // and suffix the input.
+ // See `check_expression_ast` for how the AST is verified.
+ let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) {
+ ("({", "})")
+ } else {
+ ("", "")
+ };
+
+ let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix));
+ let mut errors = vec![];
+ let result = parse_file_as_expr(&file, syntax, version, None, &mut errors);
+
+ match result {
+ Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"),
+ Ok(tree) => {
+ if errors.is_empty() {
+ let place = fix_swc_position(tree.span().hi.to_usize(), prefix.len());
+ let result = check_expression_ast(&tree, kind);
+ if matches!(result, MdxSignal::Ok) {
+ whitespace_and_comments(place, value)
+ } else {
+ result
+ }
+ } else {
+ if errors.len() > 1 {
+ unreachable!("parse_expression: todo: multiple errors? {:?}", errors);
+ }
+ swc_error_to_signal(&errors[0], value.len(), prefix.len(), "expression")
+ }
+ }
+ }
+}
+
+/// Check that the resulting AST of ESM is OK.
+///
+/// This checks that only module declarations (import/exports) are used, not
+/// statements.
+fn check_esm_ast(tree: &Module) -> MdxSignal {
+ let mut index = 0;
+ while index < tree.body.len() {
+ let node = &tree.body[index];
+
+ if !node.is_module_decl() {
+ let place = fix_swc_position(node.span().hi.to_usize(), 0);
+ return MdxSignal::Error(
+ "Unexpected statement in code: only import/exports are supported".to_string(),
+ place,
+ );
+ }
+
+ index += 1;
+ }
+
+ MdxSignal::Ok
+}
+
+/// Check that the resulting AST of an expressions is OK.
+///
+/// This checks that attribute expressions are the expected spread.
+fn check_expression_ast(tree: &Expr, kind: &MdxExpressionKind) -> MdxSignal {
+ if matches!(kind, MdxExpressionKind::AttributeExpression)
+ && tree
+ .unwrap_parens()
+ .as_object()
+ .and_then(|object| {
+ if object.props.len() == 1 {
+ object.props[0].as_spread()
+ } else {
+ None
+ }
+ })
+ .is_none()
+ {
+ MdxSignal::Error(
+ "Expected a single spread value, such as `...x`".to_string(),
+ 0,
+ )
+ } else {
+ MdxSignal::Ok
+ }
+}
+
+/// Turn an SWC error into an `MdxSignal`.
+///
+/// * If the error happens at `value_len`, yields `MdxSignal::Eof`
+/// * Else, yields `MdxSignal::Error`.
+fn swc_error_to_signal(
+ error: &SwcError,
+ value_len: usize,
+ prefix_len: usize,
+ name: &str,
+) -> MdxSignal {
+ let message = error.kind().msg().to_string();
+ let place = fix_swc_position(error.span().hi.to_usize(), prefix_len);
+ let message = format!("Could not parse {} with swc: {}", name, message);
+
+ if place >= value_len {
+ MdxSignal::Eof(message)
+ } else {
+ MdxSignal::Error(message, place)
+ }
+}
+
+/// Move past JavaScript whitespace (well, actually ASCII whitespace) and
+/// comments.
+///
+/// This is needed because for expressions, we use an API that parses up to
+/// a valid expression, but there may be more expressions after it, which we
+/// don’t alow.
+fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal {
+ let bytes = value.as_bytes();
+ let len = bytes.len();
+ let mut in_multiline = false;
+ let mut in_line = false;
+
+ while index < len {
+ // In a multiline comment: `/* a */`.
+ if in_multiline {
+ if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' {
+ index += 1;
+ in_multiline = false;
+ }
+ }
+ // In a line comment: `// a`.
+ else if in_line {
+ if index + 1 < len && bytes[index] == b'\r' && bytes[index + 1] == b'\n' {
+ index += 1;
+ in_line = false;
+ } else if bytes[index] == b'\r' || bytes[index] == b'\n' {
+ in_line = false;
+ }
+ }
+ // Not in a comment, opening a multiline comment: `/* a */`.
+ else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' {
+ index += 1;
+ in_multiline = true;
+ }
+ // Not in a comment, opening a line comment: `// a`.
+ else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' {
+ index += 1;
+ in_line = true;
+ }
+ // Outside comment, whitespace.
+ else if bytes[index].is_ascii_whitespace() {
+ // Fine!
+ }
+ // Outside comment, not whitespace.
+ else {
+ return MdxSignal::Error(
+ "Could not parse expression with swc: Unexpected content after expression"
+ .to_string(),
+ index,
+ );
+ }
+
+ index += 1;
+ }
+
+ if in_multiline {
+ MdxSignal::Error(
+ "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".to_string(),
+ index,
+ )
+ } else if in_line {
+ // EOF instead of EOL is specifically not allowed, because that would
+ // mean the closing brace is on the commented-out line
+ MdxSignal::Error(
+ "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".to_string(),
+ index,
+ )
+ } else {
+ MdxSignal::Ok
+ }
+}
+
+/// Create configuration for SWC, shared between ESM and expressions.
+///
+/// This enables modern JavaScript (ES2022) + JSX.
+fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) {
+ (
+ // File.
+ SourceFile::new(
+ FileName::Anon,
+ false,
+ FileName::Anon,
+ source,
+ BytePos::from_usize(1),
+ ),
+ // Syntax.
+ Syntax::Es(EsConfig {
+ jsx: true,
+ ..EsConfig::default()
+ }),
+ // Version.
+ EsVersion::Es2022,
+ )
+}
+
+/// Turn an SWC byte position from a resulting AST to an offset in the original
+/// input string.
+fn fix_swc_position(index: usize, prefix_len: usize) -> usize {
+ index - 1 - prefix_len
+}
diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs
new file mode 100644
index 0000000..821931c
--- /dev/null
+++ b/tests/test_utils/to_hast.rs
@@ -0,0 +1,1457 @@
+use crate::test_utils::hast;
+use micromark::{mdast, sanitize_, unist::Position};
+
+// Options?
+// - dangerous: raw? No
+// - clobberPrefix / footnoteLabel / footnoteLabelTagName / footnoteLabelProperties / footnoteBacklabel? Later
+
+#[derive(Debug)]
+struct State {
+ definitions: Vec<(String, String, Option<String>)>,
+ footnote_definitions: Vec<(String, Vec<hast::Node>)>,
+ footnote_calls: Vec<(String, usize)>,
+}
+
+#[derive(Debug)]
+enum Result {
+ Fragment(Vec<hast::Node>),
+ Node(hast::Node),
+ None,
+}
+
+#[allow(dead_code)]
+pub fn to_hast(mdast: &mdast::Node) -> hast::Node {
+ let mut definitions = vec![];
+
+ // Collect definitions.
+ // Calls take info from their definition.
+ // Calls can come come before definitions.
+ // Footnote calls can also come before footnote definitions, but those
+ // calls *do not* take info from their definitions, so we don’t care
+ // about footnotes here.
+ visit(mdast, |node| {
+ if let mdast::Node::Definition(definition) = node {
+ definitions.push((
+ definition.identifier.clone(),
+ definition.url.clone(),
+ definition.title.clone(),
+ ));
+ }
+ });
+
+ // - footnoteById: Record<string, Node>
+ // - footnoteOrder: Vec<string>
+ // - footnoteCounts: Record<string, usize>
+
+ let (result, mut state) = one(
+ mdast,
+ None,
+ State {
+ definitions,
+ footnote_definitions: vec![],
+ footnote_calls: vec![],
+ },
+ );
+
+ if state.footnote_calls.is_empty() {
+ if let Result::Node(node) = result {
+ return node;
+ }
+ }
+
+ // We either have to generate a footer, or we don’t have a single node.
+ // So we need a root.
+ let mut root = hast::Root {
+ children: vec![],
+ position: None,
+ };
+
+ match result {
+ Result::Fragment(children) => root.children = children,
+ Result::Node(node) => {
+ if let hast::Node::Root(existing) = node {
+ root = existing;
+ } else {
+ root.children.push(node);
+ }
+ }
+ Result::None => {}
+ }
+
+ if !state.footnote_calls.is_empty() {
+ let mut items = vec![];
+
+ let mut index = 0;
+ while index < state.footnote_calls.len() {
+ let (id, count) = &state.footnote_calls[index];
+ let safe_id = sanitize_(&id.to_lowercase());
+
+ // Find definition: we’ll always find it.
+ let mut definition_index = 0;
+ while definition_index < state.footnote_definitions.len() {
+ if &state.footnote_definitions[definition_index].0 == id {
+ break;
+ }
+ definition_index += 1;
+ }
+ debug_assert_ne!(
+ definition_index,
+ state.footnote_definitions.len(),
+ "expected definition"
+ );
+
+ // We’ll find each used definition once, so we can split off to take the content.
+ let mut content = state.footnote_definitions[definition_index].1.split_off(0);
+
+ let mut reference_index = 0;
+ let mut backreferences = vec![];
+ while reference_index < *count {
+ let mut backref_children = vec![hast::Node::Text(hast::Text {
+ value: "↩".into(),
+ position: None,
+ })];
+
+ if reference_index != 0 {
+ backreferences.push(hast::Node::Text(hast::Text {
+ value: " ".into(),
+ position: None,
+ }));
+
+ backref_children.push(hast::Node::Element(hast::Element {
+ tag_name: "sup".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: (reference_index + 1).to_string(),
+ position: None,
+ })],
+ position: None,
+ }));
+ }
+
+ backreferences.push(hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ (
+ "href".into(),
+ hast::PropertyValue::String(format!(
+ "#fnref-{}{}",
+ safe_id,
+ if reference_index == 0 {
+ "".into()
+ } else {
+ format!("-{}", &(reference_index + 1).to_string())
+ }
+ )),
+ ),
+ (
+ "dataFootnoteBackref".into(),
+ hast::PropertyValue::Boolean(true),
+ ),
+ (
+ "ariaLabel".into(),
+ hast::PropertyValue::String("Back to content".into()),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "data-footnote-backref".into(),
+ )]),
+ ),
+ ],
+ children: backref_children,
+ position: None,
+ }));
+
+ reference_index += 1;
+ }
+
+ let mut backreference_opt = Some(backreferences);
+
+ if let Some(hast::Node::Element(tail_element)) = content.last_mut() {
+ if tail_element.tag_name == "p" {
+ if let Some(hast::Node::Text(text)) = tail_element.children.last_mut() {
+ text.value.push(' ');
+ } else {
+ tail_element.children.push(hast::Node::Text(hast::Text {
+ value: " ".into(),
+ position: None,
+ }));
+ }
+
+ tail_element
+ .children
+ .append(&mut backreference_opt.take().unwrap());
+ }
+ }
+
+ // No paragraph, just push them.
+ if let Some(mut backreference) = backreference_opt {
+ content.append(&mut backreference);
+ }
+
+ items.push(hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ // To do: support clobber prefix.
+ properties: vec![(
+ "id".into(),
+ hast::PropertyValue::String(format!("#fn-{}", safe_id)),
+ )],
+ children: wrap(content, true),
+ position: None,
+ }));
+ index += 1;
+ }
+
+ root.children.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ root.children.push(hast::Node::Element(hast::Element {
+ tag_name: "section".into(),
+ properties: vec![
+ ("dataFootnotes".into(), hast::PropertyValue::Boolean(true)),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "footnotes".into(),
+ )]),
+ ),
+ ],
+ children: vec![
+ hast::Node::Element(hast::Element {
+ tag_name: "h2".into(),
+ properties: vec![
+ (
+ "id".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "sr-only".into(),
+ )]),
+ ),
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "Footnotes".into(),
+ position: None,
+ })],
+ position: None,
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "ol".into(),
+ properties: vec![],
+ children: wrap(items, true),
+ position: None,
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }),
+ ],
+ position: None,
+ }));
+ root.children.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ }
+
+ hast::Node::Root(root)
+}
+
+fn one(node: &mdast::Node, parent: Option<&mdast::Node>, state: State) -> (Result, State) {
+ match node {
+ mdast::Node::BlockQuote(_) => transform_block_quote(node, state),
+ mdast::Node::Break(_) => transform_break(node, state),
+ mdast::Node::Code(_) => transform_code(node, state),
+ mdast::Node::Delete(_) => transform_delete(node, state),
+ mdast::Node::Emphasis(_) => transform_emphasis(node, state),
+ mdast::Node::FootnoteDefinition(_) => transform_footnote_definition(node, state),
+ mdast::Node::FootnoteReference(_) => transform_footnote_reference(node, state),
+ mdast::Node::Heading(_) => transform_heading(node, state),
+ mdast::Node::Image(_) => transform_image(node, state),
+ mdast::Node::ImageReference(_) => transform_image_reference(node, state),
+ mdast::Node::InlineCode(_) => transform_inline_code(node, state),
+ mdast::Node::InlineMath(_) => transform_inline_math(node, state),
+ mdast::Node::Link(_) => transform_link(node, state),
+ mdast::Node::LinkReference(_) => transform_link_reference(node, state),
+ mdast::Node::ListItem(_) => transform_list_item(node, parent, state),
+ mdast::Node::List(_) => transform_list(node, state),
+ mdast::Node::Math(_) => transform_math(node, state),
+ mdast::Node::MdxFlowExpression(_) | mdast::Node::MdxTextExpression(_) => {
+ transform_mdx_expression(node, state)
+ }
+ mdast::Node::MdxJsxFlowElement(_) | mdast::Node::MdxJsxTextElement(_) => {
+ transform_mdx_jsx_element(node, state)
+ }
+ mdast::Node::MdxjsEsm(_) => transform_mdxjs_esm(node, state),
+ mdast::Node::Paragraph(_) => transform_paragraph(node, state),
+ mdast::Node::Root(_) => transform_root(node, state),
+ mdast::Node::Strong(_) => transform_strong(node, state),
+ // Note: this is only called here if there is a single cell passed, not when one is found in a table.
+ mdast::Node::TableCell(_) => {
+ transform_table_cell(node, false, mdast::AlignKind::None, state)
+ }
+ // Note: this is only called here if there is a single row passed, not when one is found in a table.
+ mdast::Node::TableRow(_) => transform_table_row(node, false, None, state),
+ mdast::Node::Table(_) => transform_table(node, state),
+ mdast::Node::Text(_) => transform_text(node, state),
+ mdast::Node::ThematicBreak(_) => transform_thematic_break(node, state),
+ // Ignore.
+ // Idea: support `Raw` nodes for HTML, optionally?
+ mdast::Node::Definition(_)
+ | mdast::Node::Html(_)
+ | mdast::Node::Yaml(_)
+ | mdast::Node::Toml(_) => (Result::None, state),
+ }
+}
+
+/// [`BlockQuote`][mdast::BlockQuote].
+fn transform_block_quote(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "blockquote".into(),
+ properties: vec![],
+ children: wrap(children, true),
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`Break`][mdast::Break].
+fn transform_break(node: &mdast::Node, state: State) -> (Result, State) {
+ (
+ Result::Fragment(vec![
+ augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "br".into(),
+ properties: vec![],
+ children: vec![],
+ position: None,
+ }),
+ ),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }),
+ ]),
+ state,
+ )
+}
+
+/// [`Code`][mdast::Code].
+fn transform_code(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::Code(code) = node {
+ let mut value = code.value.clone();
+ value.push('\n');
+ let mut properties = vec![];
+
+ if let Some(lang) = code.lang.as_ref() {
+ let mut value = "language-".to_string();
+ value.push_str(lang);
+ let value = hast::PropertyItem::String(value);
+ properties.push((
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![value]),
+ ));
+ }
+
+ // To do: option to persist `meta`?
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "pre".into(),
+ properties: vec![],
+ children: vec![augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties,
+ children: vec![hast::Node::Text(hast::Text {
+ value,
+ position: None,
+ })],
+ position: None,
+ }),
+ )],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `Code`")
+ }
+}
+
+/// [`Delete`][mdast::Delete].
+fn transform_delete(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "del".into(),
+ properties: vec![],
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`Emphasis`][mdast::Emphasis].
+fn transform_emphasis(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "em".into(),
+ properties: vec![],
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`FootnoteDefinition`][mdast::FootnoteDefinition].
+fn transform_footnote_definition(node: &mdast::Node, mut state: State) -> (Result, State) {
+ if let mdast::Node::FootnoteDefinition(definition) = node {
+ let result = all(node, state);
+ let children = result.0;
+ state = result.1;
+ // Set aside.
+ state
+ .footnote_definitions
+ .push((definition.identifier.clone(), children));
+ (Result::None, state)
+ } else {
+ unreachable!("expected `FootnoteDefinition`")
+ }
+}
+
+/// [`FootnoteReference`][mdast::FootnoteReference].
+fn transform_footnote_reference(node: &mdast::Node, mut state: State) -> (Result, State) {
+ if let mdast::Node::FootnoteReference(reference) = node {
+ let safe_id = sanitize_(&reference.identifier.to_lowercase());
+ let mut call_index = 0;
+
+ // See if this has been called before.
+ while call_index < state.footnote_calls.len() {
+ if state.footnote_calls[call_index].0 == reference.identifier {
+ break;
+ }
+ call_index += 1;
+ }
+
+ // New.
+ if call_index == state.footnote_calls.len() {
+ state.footnote_calls.push((reference.identifier.clone(), 0));
+ }
+
+ // Increment.
+ state.footnote_calls[call_index].1 += 1;
+
+ let reuse_counter = state.footnote_calls[call_index].1;
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "sup".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ // To do: support clobber prefix.
+ properties: vec![
+ (
+ "href".into(),
+ hast::PropertyValue::String(format!("#fn-{}", safe_id)),
+ ),
+ (
+ "id".into(),
+ hast::PropertyValue::String(format!(
+ "fnref-{}{}",
+ safe_id,
+ if reuse_counter > 1 {
+ format!("-{}", reuse_counter)
+ } else {
+ "".into()
+ }
+ )),
+ ),
+ ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true)),
+ (
+ "ariaDescribedBy".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ ),
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: (call_index + 1).to_string(),
+ position: None,
+ })],
+ position: None,
+ })],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `FootnoteReference`")
+ }
+}
+
+/// [`Heading`][mdast::Heading].
+fn transform_heading(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::Heading(heading) = node {
+ let (children, state) = all(node, state);
+ let tag_name = format!("h{}", heading.depth);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name,
+ properties: vec![],
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `Heading`")
+ }
+}
+
+/// [`Image`][mdast::Image].
+fn transform_image(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::Image(image) = node {
+ let mut properties = vec![];
+
+ properties.push((
+ "src".into(),
+ hast::PropertyValue::String(sanitize_(&image.url)),
+ ));
+
+ properties.push(("alt".into(), hast::PropertyValue::String(image.alt.clone())));
+
+ if let Some(value) = image.title.as_ref() {
+ properties.push(("title".into(), hast::PropertyValue::String(value.into())));
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "img".into(),
+ properties,
+ children: vec![],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `Image`")
+ }
+}
+
+/// [`ImageReference`][mdast::ImageReference].
+fn transform_image_reference(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::ImageReference(reference) = node {
+ let mut properties = vec![];
+
+ let definition = state
+ .definitions
+ .iter()
+ .find(|d| d.0 == reference.identifier);
+
+ // To do: revert when undefined? <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a60941d8936135e05a5cc78734d87578ba/lib/revert.js>
+ let (_, url, title) =
+ definition.expect("expected reference to have a corresponding definition");
+
+ properties.push(("src".into(), hast::PropertyValue::String(sanitize_(url))));
+
+ properties.push((
+ "alt".into(),
+ hast::PropertyValue::String(reference.alt.clone()),
+ ));
+
+ if let Some(value) = title {
+ properties.push(("title".into(), hast::PropertyValue::String(value.into())));
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "img".into(),
+ properties,
+ children: vec![],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `ImageReference`")
+ }
+}
+
+/// [`InlineCode`][mdast::InlineCode].
+fn transform_inline_code(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::InlineCode(code) = node {
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: replace_eols_with_spaces(&code.value),
+ position: None,
+ })],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `InlineCode`")
+ }
+}
+
+/// [`InlineMath`][mdast::InlineMath].
+fn transform_inline_math(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::InlineMath(math) = node {
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![
+ hast::PropertyItem::String("language-math".into()),
+ hast::PropertyItem::String("math-inline".into()),
+ ]),
+ )],
+ children: vec![hast::Node::Text(hast::Text {
+ value: replace_eols_with_spaces(&math.value),
+ position: None,
+ })],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `InlineMath`")
+ }
+}
+
+/// [`Link`][mdast::Link].
+fn transform_link(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::Link(link) = node {
+ let mut properties = vec![];
+
+ properties.push((
+ "href".into(),
+ hast::PropertyValue::String(sanitize_(&link.url)),
+ ));
+
+ if let Some(value) = link.title.as_ref() {
+ properties.push(("title".into(), hast::PropertyValue::String(value.into())));
+ }
+
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties,
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `Link`")
+ }
+}
+
+/// [`LinkReference`][mdast::LinkReference].
+fn transform_link_reference(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::LinkReference(reference) = node {
+ let mut properties = vec![];
+
+ let definition = state
+ .definitions
+ .iter()
+ .find(|d| d.0 == reference.identifier);
+
+ // To do: revert when undefined? <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a60941d8936135e05a5cc78734d87578ba/lib/revert.js>
+ let (_, url, title) =
+ definition.expect("expected reference to have a corresponding definition");
+
+ properties.push(("href".into(), hast::PropertyValue::String(sanitize_(url))));
+
+ if let Some(value) = title {
+ properties.push(("title".into(), hast::PropertyValue::String(value.into())));
+ }
+
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties,
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `LinkReference`")
+ }
+}
+
+/// [`ListItem`][mdast::ListItem].
+fn transform_list_item(
+ node: &mdast::Node,
+ parent: Option<&mdast::Node>,
+ state: State,
+) -> (Result, State) {
+ if let mdast::Node::ListItem(item) = node {
+ let (mut children, state) = all(node, state);
+ let mut loose = list_item_loose(node);
+
+ if let Some(parent) = parent {
+ if matches!(parent, mdast::Node::List(_)) {
+ loose = list_loose(parent);
+ }
+ };
+
+ let mut properties = vec![];
+
+ // Inject a checkbox.
+ if let Some(checked) = item.checked {
+ // According to github-markdown-css, this class hides bullet.
+ // See: <https://github.com/sindresorhus/github-markdown-css>.
+ properties.push((
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "task-list-item".into(),
+ )]),
+ ));
+
+ let mut input = Some(hast::Node::Element(hast::Element {
+ tag_name: "input".into(),
+ properties: vec![
+ (
+ "type".into(),
+ hast::PropertyValue::String("checkbox".into()),
+ ),
+ ("checked".into(), hast::PropertyValue::Boolean(checked)),
+ ("disabled".into(), hast::PropertyValue::Boolean(true)),
+ ],
+ children: vec![],
+ position: None,
+ }));
+
+ if let Some(hast::Node::Element(x)) = children.first_mut() {
+ if x.tag_name == "p" {
+ if !x.children.is_empty() {
+ x.children.insert(
+ 0,
+ hast::Node::Text(hast::Text {
+ value: " ".into(),
+ position: None,
+ }),
+ );
+ }
+
+ x.children.insert(0, input.take().unwrap());
+ }
+ }
+
+ // If the input wasn‘t injected yet, inject a paragraph.
+ if let Some(input) = input {
+ children.insert(
+ 0,
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![input],
+ position: None,
+ }),
+ );
+ }
+ }
+
+ children.reverse();
+ let mut result = vec![];
+ let mut head = true;
+ let empty = children.is_empty();
+ let mut tail_p = false;
+
+ while let Some(child) = children.pop() {
+ let mut is_p = false;
+ if let hast::Node::Element(el) = &child {
+ if el.tag_name == "p" {
+ is_p = true;
+ }
+ }
+
+ // Add eols before nodes, except if this is a tight, first paragraph.
+ if loose || !head || !is_p {
+ result.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ }
+
+ if is_p && !loose {
+ // Unwrap the paragraph.
+ if let hast::Node::Element(mut el) = child {
+ result.append(&mut el.children);
+ }
+ } else {
+ result.push(child);
+ }
+
+ head = false;
+ tail_p = is_p;
+ }
+
+ // Add eol after last node, except if it is tight or a paragraph.
+ if !empty && (loose || !tail_p) {
+ result.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties,
+ children: result,
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `ListItem`")
+ }
+}
+
+/// [`List`][mdast::List].
+fn transform_list(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::List(list) = node {
+ let mut contains_task_list = false;
+ let mut index = 0;
+
+ while index < list.children.len() {
+ if let mdast::Node::ListItem(item) = &list.children[index] {
+ if item.checked.is_some() {
+ contains_task_list = true;
+ }
+ }
+
+ index += 1;
+ }
+
+ let (children, state) = all(node, state);
+ let mut properties = vec![];
+ let tag_name = if list.ordered {
+ "ol".into()
+ } else {
+ "ul".into()
+ };
+
+ // Add start.
+ if let Some(start) = list.start {
+ if list.ordered && start != 1 {
+ properties.push(("start".into(), hast::PropertyValue::Number(start.into())));
+ }
+ }
+
+ // Like GitHub, add a class for custom styling.
+ if contains_task_list {
+ properties.push((
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "contains-task-list".into(),
+ )]),
+ ));
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name,
+ properties,
+ children: wrap(children, true),
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `List`")
+ }
+}
+
+/// [`Math`][mdast::Math].
+fn transform_math(node: &mdast::Node, state: State) -> (Result, State) {
+ if let mdast::Node::Math(math) = node {
+ let mut value = math.value.clone();
+ value.push('\n');
+
+ // To do: option to persist `meta`?
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "pre".into(),
+ properties: vec![],
+ children: vec![augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![
+ hast::PropertyItem::String("language-math".into()),
+ hast::PropertyItem::String("math-display".into()),
+ ]),
+ )],
+ children: vec![hast::Node::Text(hast::Text {
+ value,
+ position: None,
+ })],
+ position: None,
+ }),
+ )],
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `Math`")
+ }
+}
+
+/// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression].
+fn transform_mdx_expression(node: &mdast::Node, state: State) -> (Result, State) {
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::MdxExpression(hast::MdxExpression {
+ value: node.to_string(),
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement].
+fn transform_mdx_jsx_element(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+
+ let (name, attributes) = match node {
+ mdast::Node::MdxJsxFlowElement(n) => (&n.name, &n.attributes),
+ mdast::Node::MdxJsxTextElement(n) => (&n.name, &n.attributes),
+ _ => unreachable!("expected mdx jsx element"),
+ };
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::MdxJsxElement(hast::MdxJsxElement {
+ name: name.clone(),
+ attributes: attributes.clone(),
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`MdxjsEsm`][mdast::MdxjsEsm].
+fn transform_mdxjs_esm(node: &mdast::Node, state: State) -> (Result, State) {
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::MdxjsEsm(hast::MdxjsEsm {
+ value: node.to_string(),
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`Paragraph`][mdast::Paragraph].
+fn transform_paragraph(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`Root`][mdast::Root].
+fn transform_root(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Root(hast::Root {
+ children: wrap(children, false),
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`Strong`][mdast::Strong].
+fn transform_strong(node: &mdast::Node, state: State) -> (Result, State) {
+ let (children, state) = all(node, state);
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "strong".into(),
+ properties: vec![],
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`TableCell`][mdast::TableCell].
+fn transform_table_cell(
+ node: &mdast::Node,
+ head: bool,
+ align: mdast::AlignKind,
+ state: State,
+) -> (Result, State) {
+ let (children, state) = all(node, state);
+ // To do: option to generate a `style` instead?
+ let align_value = match align {
+ mdast::AlignKind::None => None,
+ mdast::AlignKind::Left => Some("left"),
+ mdast::AlignKind::Right => Some("right"),
+ mdast::AlignKind::Center => Some("center"),
+ };
+
+ let mut properties = vec![];
+
+ if let Some(value) = align_value {
+ properties.push(("align".into(), hast::PropertyValue::String(value.into())));
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: if head { "th".into() } else { "td".into() },
+ properties,
+ children,
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`TableRow`][mdast::TableRow].
+fn transform_table_row(
+ node: &mdast::Node,
+ head: bool,
+ align: Option<&[mdast::AlignKind]>,
+ mut state: State,
+) -> (Result, State) {
+ if let mdast::Node::TableRow(row) = node {
+ let mut children = vec![];
+ let mut index = 0;
+ #[allow(clippy::redundant_closure_for_method_calls)]
+ let len = align.map_or(row.children.len(), |d| d.len());
+ let empty_cell = mdast::Node::TableCell(mdast::TableCell {
+ children: vec![],
+ position: None,
+ });
+
+ while index < len {
+ let align_value = align
+ .and_then(|d| d.get(index))
+ .unwrap_or(&mdast::AlignKind::None);
+
+ let child = row.children.get(index).unwrap_or(&empty_cell);
+ let tuple = transform_table_cell(child, head, *align_value, state);
+ append_result(&mut children, tuple.0);
+ state = tuple.1;
+ index += 1;
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "tr".into(),
+ properties: vec![],
+ children: wrap(children, true),
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `TableRow`")
+ }
+}
+
+/// [`Table`][mdast::Table].
+fn transform_table(node: &mdast::Node, mut state: State) -> (Result, State) {
+ if let mdast::Node::Table(table) = node {
+ let mut rows = vec![];
+ // let body = hast::Element {
+ // tag_name: "tbody".into(),
+ // properties: vec![],
+ // children: vec![],
+ // position: None,
+ // };
+ let mut index = 0;
+
+ while index < table.children.len() {
+ let tuple = transform_table_row(
+ &table.children[index],
+ index == 0,
+ Some(&table.align),
+ state,
+ );
+ append_result(&mut rows, tuple.0);
+ state = tuple.1;
+ index += 1;
+ }
+
+ let body_rows = rows.split_off(1);
+ let head_row = rows.pop();
+ let mut children = vec![];
+
+ if let Some(row) = head_row {
+ let position = row.position().cloned();
+ children.push(hast::Node::Element(hast::Element {
+ tag_name: "thead".into(),
+ properties: vec![],
+ children: wrap(vec![row], true),
+ position,
+ }));
+ }
+
+ if !body_rows.is_empty() {
+ let mut position = None;
+
+ if let Some(position_start) = body_rows.first().and_then(hast::Node::position) {
+ if let Some(position_end) = body_rows.last().and_then(hast::Node::position) {
+ position = Some(Position {
+ start: position_start.start.clone(),
+ end: position_end.end.clone(),
+ });
+ }
+ }
+
+ children.push(hast::Node::Element(hast::Element {
+ tag_name: "tbody".into(),
+ properties: vec![],
+ children: wrap(body_rows, true),
+ position,
+ }));
+ }
+
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "table".into(),
+ properties: vec![],
+ children: wrap(children, true),
+ position: None,
+ }),
+ )),
+ state,
+ )
+ } else {
+ unreachable!("expected `Table`")
+ }
+}
+
+/// [`Text`][mdast::Text].
+fn transform_text(node: &mdast::Node, state: State) -> (Result, State) {
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Text(hast::Text {
+ value: node.to_string(),
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+/// [`ThematicBreak`][mdast::ThematicBreak].
+fn transform_thematic_break(node: &mdast::Node, state: State) -> (Result, State) {
+ (
+ Result::Node(augment_node(
+ node,
+ hast::Node::Element(hast::Element {
+ tag_name: "hr".into(),
+ properties: vec![],
+ children: vec![],
+ position: None,
+ }),
+ )),
+ state,
+ )
+}
+
+// Transform children of `parent`.
+fn all(parent: &mdast::Node, mut state: State) -> (Vec<hast::Node>, State) {
+ let mut result = vec![];
+ if let Some(children) = parent.children() {
+ let mut index = 0;
+ while index < children.len() {
+ let child = &children[index];
+ let tuple = one(child, Some(parent), state);
+ append_result(&mut result, tuple.0);
+ state = tuple.1;
+ index += 1;
+ }
+ }
+
+ (result, state)
+}
+
+/// Wrap `nodes` with line feeds between each entry.
+/// Optionally adds line feeds at the start and end.
+fn wrap(mut nodes: Vec<hast::Node>, loose: bool) -> Vec<hast::Node> {
+ let mut result = vec![];
+ let was_empty = nodes.is_empty();
+ let mut head = true;
+
+ nodes.reverse();
+
+ if loose {
+ result.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ }
+
+ while let Some(item) = nodes.pop() {
+ // Inject when there’s more:
+ if !head {
+ result.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ }
+ head = false;
+ result.push(item);
+ }
+
+ if loose && !was_empty {
+ result.push(hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None,
+ }));
+ }
+
+ result
+}
+
+/// Patch a position from the node `left` onto `right`.
+fn augment_node(left: &mdast::Node, right: hast::Node) -> hast::Node {
+ if let Some(position) = left.position() {
+ augment_position(position, right)
+ } else {
+ right
+ }
+}
+
+/// Patch a position from `left` onto `right`.
+fn augment_position(left: &Position, mut right: hast::Node) -> hast::Node {
+ right.position_set(Some(left.clone()));
+ right
+}
+
+/// Visit.
+fn visit<Visitor>(node: &mdast::Node, visitor: Visitor)
+where
+ Visitor: FnMut(&mdast::Node),
+{
+ visit_impl(node, visitor);
+}
+
+/// Visit, mutably.
+// Probably useful later:
+#[allow(dead_code)]
+fn visit_mut<Visitor>(node: &mut mdast::Node, visitor: Visitor)
+where
+ Visitor: FnMut(&mut mdast::Node),
+{
+ visit_mut_impl(node, visitor);
+}
+
+/// Internal implementation to visit.
+fn visit_impl<Visitor>(node: &mdast::Node, mut visitor: Visitor) -> Visitor
+where
+ Visitor: FnMut(&mdast::Node),
+{
+ visitor(node);
+
+ if let Some(children) = node.children() {
+ let mut index = 0;
+ while index < children.len() {
+ let child = &children[index];
+ visitor = visit_impl(child, visitor);
+ index += 1;
+ }
+ }
+
+ visitor
+}
+
+/// Internal implementation to visit, mutably.
+fn visit_mut_impl<Visitor>(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor
+where
+ Visitor: FnMut(&mut mdast::Node),
+{
+ visitor(node);
+
+ if let Some(children) = node.children_mut() {
+ let mut index = 0;
+ while let Some(child) = children.get_mut(index) {
+ visitor = visit_mut_impl(child, visitor);
+ index += 1;
+ }
+ }
+
+ visitor
+}
+
+// To do: trim arounds breaks: <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a/lib/traverse.js>.
+/// Append an (optional, variadic) result.
+fn append_result(list: &mut Vec<hast::Node>, result: Result) {
+ match result {
+ Result::Fragment(mut fragment) => list.append(&mut fragment),
+ Result::Node(node) => list.push(node),
+ Result::None => {}
+ };
+}
+
+/// Replace line endings (CR, LF, CRLF) with spaces.
+///
+/// Used for inline code and inline math.
+fn replace_eols_with_spaces(value: &str) -> String {
+ // It’ll grow a bit small for each CR+LF.
+ let mut result = String::with_capacity(value.len());
+ let bytes = value.as_bytes();
+ let mut index = 0;
+ let mut start = 0;
+
+ while index < bytes.len() {
+ let byte = bytes[index];
+
+ if byte == b'\r' || byte == b'\n' {
+ result.push_str(&value[start..index]);
+ result.push(' ');
+
+ if index + 1 < bytes.len() && byte == b'\r' && bytes[index + 1] == b'\n' {
+ index += 1;
+ }
+
+ start = index + 1;
+ }
+
+ index += 1;
+ }
+
+ result.push_str(&value[start..]);
+
+ result
+}
+
+/// Check if a list is loose.
+fn list_loose(node: &mdast::Node) -> bool {
+ if let mdast::Node::List(list) = node {
+ if list.spread {
+ return true;
+ }
+
+ if let Some(children) = node.children() {
+ let mut index = 0;
+ while index < children.len() {
+ if list_item_loose(&children[index]) {
+ return true;
+ }
+ index += 1;
+ }
+ }
+ }
+
+ false
+}
+
+/// Check if a list item is loose.
+fn list_item_loose(node: &mdast::Node) -> bool {
+ if let mdast::Node::ListItem(item) = node {
+ item.spread
+ } else {
+ false
+ }
+}
diff --git a/tests/text.rs b/tests/text.rs
index a2ef1fb..584f463 100644
--- a/tests/text.rs
+++ b/tests/text.rs
@@ -3,7 +3,7 @@ use micromark::micromark;
use pretty_assertions::assert_eq;
#[test]
-fn text() -> Result<(), String> {
+fn text() {
assert_eq!(
micromark("hello $.;'there"),
"<p>hello $.;'there</p>",
@@ -21,6 +21,4 @@ fn text() -> Result<(), String> {
"<p>Multiple spaces</p>",
"should preserve internal spaces verbatim"
);
-
- Ok(())
}
diff --git a/tests/thematic_break.rs b/tests/thematic_break.rs
index 85ab37f..d8d8104 100644
--- a/tests/thematic_break.rs
+++ b/tests/thematic_break.rs
@@ -1,7 +1,9 @@
extern crate micromark;
use micromark::{
- mdast::{Node, Position, Root, ThematicBreak},
- micromark, micromark_to_mdast, micromark_with_options, Constructs, Options,
+ mdast::{Node, Root, ThematicBreak},
+ micromark, micromark_to_mdast, micromark_with_options,
+ unist::Position,
+ Constructs, Options,
};
use pretty_assertions::assert_eq;
diff --git a/tests/xxx_hast.rs b/tests/xxx_hast.rs
new file mode 100644
index 0000000..be42818
--- /dev/null
+++ b/tests/xxx_hast.rs
@@ -0,0 +1,1585 @@
+extern crate micromark;
+mod test_utils;
+use micromark::mdast;
+use pretty_assertions::assert_eq;
+use test_utils::{hast, to_hast::to_hast};
+
+#[test]
+fn hast() {
+ assert_eq!(
+ to_hast(&mdast::Node::BlockQuote(mdast::BlockQuote {
+ children: vec![],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "blockquote".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `BlockQuote`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Break(mdast::Break { position: None })),
+ hast::Node::Root(hast::Root {
+ children: vec![
+ hast::Node::Element(hast::Element {
+ tag_name: "br".into(),
+ properties: vec![],
+ children: vec![],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ })
+ ],
+ position: None
+ }),
+ "should support a `Break`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Code(mdast::Code {
+ lang: Some("b".into()),
+ meta: None,
+ value: "a".into(),
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "pre".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "language-b".into()
+ )]),
+ ),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a\n".into(),
+ position: None
+ })],
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Code`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Definition(mdast::Definition {
+ url: "b".into(),
+ title: None,
+ identifier: "a".into(),
+ label: None,
+ position: None
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![],
+ position: None
+ }),
+ "should support a `Definition`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Delete(mdast::Delete {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "del".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Delete`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Emphasis(mdast::Emphasis {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "em".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support an `Emphasis`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::FootnoteDefinition(
+ mdast::FootnoteDefinition {
+ identifier: "a".into(),
+ label: None,
+ children: vec![],
+ position: None
+ }
+ )),
+ hast::Node::Root(hast::Root {
+ children: vec![],
+ position: None
+ }),
+ "should support a `FootnoteDefinition`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![
+ mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition {
+ children: vec![mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ identifier: "a".into(),
+ label: None,
+ position: None
+ }),
+ mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::FootnoteReference(mdast::FootnoteReference {
+ identifier: "a".into(),
+ label: None,
+ position: None,
+ })],
+ position: None
+ }),
+ ],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![
+ // Main.
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "sup".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ ("href".into(), hast::PropertyValue::String("#fn-a".into()),),
+ ("id".into(), hast::PropertyValue::String("fnref-a".into()),),
+ ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),),
+ (
+ "ariaDescribedBy".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ )
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "1".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ // Footer.
+ hast::Node::Element(hast::Element {
+ tag_name: "section".into(),
+ properties: vec![
+ ("dataFootnotes".into(), hast::PropertyValue::Boolean(true),),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "footnotes".into()
+ ),]),
+ ),
+ ],
+ children: vec![
+ hast::Node::Element(hast::Element {
+ tag_name: "h2".into(),
+ properties: vec![
+ (
+ "id".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![
+ hast::PropertyItem::String("sr-only".into()),
+ ]),
+ ),
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "Footnotes".into(),
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "ol".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![(
+ "id".into(),
+ hast::PropertyValue::String("#fn-a".into()),
+ )],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "b ".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ (
+ "href".into(),
+ hast::PropertyValue::String(
+ "#fnref-a".into()
+ ),
+ ),
+ (
+ "dataFootnoteBackref".into(),
+ hast::PropertyValue::Boolean(true),
+ ),
+ (
+ "ariaLabel".into(),
+ hast::PropertyValue::String(
+ "Back to content".into()
+ ),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(
+ vec![hast::PropertyItem::String(
+ "data-footnote-backref".into()
+ ),]
+ ),
+ )
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "↩".into(),
+ position: None
+ }),],
+ position: None
+ })
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ "should support an `FootnoteReference`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![
+ mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition {
+ children: vec![mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ identifier: "a".into(),
+ label: None,
+ position: None
+ }),
+ mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![
+ mdast::Node::FootnoteReference(mdast::FootnoteReference {
+ identifier: "a".into(),
+ label: None,
+ position: None,
+ }),
+ mdast::Node::FootnoteReference(mdast::FootnoteReference {
+ identifier: "a".into(),
+ label: None,
+ position: None,
+ })
+ ],
+ position: None
+ }),
+ ],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![
+ // Main.
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Element(hast::Element {
+ tag_name: "sup".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ ("href".into(), hast::PropertyValue::String("#fn-a".into()),),
+ ("id".into(), hast::PropertyValue::String("fnref-a".into()),),
+ ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),),
+ (
+ "ariaDescribedBy".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ )
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "1".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "sup".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ ("href".into(), hast::PropertyValue::String("#fn-a".into()),),
+ ("id".into(), hast::PropertyValue::String("fnref-a-2".into()),),
+ ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),),
+ (
+ "ariaDescribedBy".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ )
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "1".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ // Footer.
+ hast::Node::Element(hast::Element {
+ tag_name: "section".into(),
+ properties: vec![
+ ("dataFootnotes".into(), hast::PropertyValue::Boolean(true),),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "footnotes".into()
+ ),]),
+ ),
+ ],
+ children: vec![
+ hast::Node::Element(hast::Element {
+ tag_name: "h2".into(),
+ properties: vec![
+ (
+ "id".into(),
+ hast::PropertyValue::String("footnote-label".into()),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![
+ hast::PropertyItem::String("sr-only".into()),
+ ]),
+ ),
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "Footnotes".into(),
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "ol".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![(
+ "id".into(),
+ hast::PropertyValue::String("#fn-a".into()),
+ )],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "b ".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ (
+ "href".into(),
+ hast::PropertyValue::String(
+ "#fnref-a".into()
+ ),
+ ),
+ (
+ "dataFootnoteBackref".into(),
+ hast::PropertyValue::Boolean(true),
+ ),
+ (
+ "ariaLabel".into(),
+ hast::PropertyValue::String(
+ "Back to content".into()
+ ),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(
+ vec![hast::PropertyItem::String(
+ "data-footnote-backref".into()
+ ),]
+ ),
+ )
+ ],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "↩".into(),
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: " ".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![
+ (
+ "href".into(),
+ hast::PropertyValue::String(
+ "#fnref-a-2".into()
+ ),
+ ),
+ (
+ "dataFootnoteBackref".into(),
+ hast::PropertyValue::Boolean(true),
+ ),
+ (
+ "ariaLabel".into(),
+ hast::PropertyValue::String(
+ "Back to content".into()
+ ),
+ ),
+ (
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(
+ vec![hast::PropertyItem::String(
+ "data-footnote-backref".into()
+ ),]
+ ),
+ )
+ ],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "↩".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "sup".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(
+ hast::Text {
+ value: "2".into(),
+ position: None
+ }
+ ),],
+ position: None
+ })
+ ],
+ position: None
+ })
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ "should support an `FootnoteReference` (multiple calls to same definition)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Heading(mdast::Heading {
+ depth: 1,
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "h1".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Heading`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Html(mdast::Html {
+ value: "<div>".into(),
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![],
+ position: None
+ }),
+ "should support an `Html`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Image(mdast::Image {
+ url: "a".into(),
+ alt: "b".into(),
+ title: None,
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "img".into(),
+ properties: vec![
+ ("src".into(), hast::PropertyValue::String("a".into()),),
+ ("alt".into(), hast::PropertyValue::String("b".into()),)
+ ],
+ children: vec![],
+ position: None
+ }),
+ "should support an `Image`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![
+ mdast::Node::Definition(mdast::Definition {
+ url: "b".into(),
+ title: None,
+ identifier: "a".into(),
+ label: None,
+ position: None
+ }),
+ mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::ImageReference(mdast::ImageReference {
+ reference_kind: mdast::ReferenceKind::Full,
+ identifier: "a".into(),
+ alt: "c".into(),
+ label: None,
+ position: None,
+ })],
+ position: None
+ }),
+ ],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "img".into(),
+ properties: vec![
+ ("src".into(), hast::PropertyValue::String("b".into()),),
+ ("alt".into(), hast::PropertyValue::String("c".into()),)
+ ],
+ children: vec![],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None
+ }),
+ "should support an `ImageReference`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::InlineCode(mdast::InlineCode {
+ value: "a\nb".into(),
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a b".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support an `InlineCode`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::InlineMath(mdast::InlineMath {
+ value: "a\nb".into(),
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![
+ hast::PropertyItem::String("language-math".into()),
+ hast::PropertyItem::String("math-inline".into())
+ ]),
+ ),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a b".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support an `InlineMath`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Link(mdast::Link {
+ url: "a".into(),
+ title: None,
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![("href".into(), hast::PropertyValue::String("a".into()),),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Link`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![
+ mdast::Node::Definition(mdast::Definition {
+ url: "b".into(),
+ title: None,
+ identifier: "a".into(),
+ label: None,
+ position: None
+ }),
+ mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::LinkReference(mdast::LinkReference {
+ reference_kind: mdast::ReferenceKind::Full,
+ identifier: "a".into(),
+ label: None,
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "c".into(),
+ position: None
+ })],
+ position: None,
+ })],
+ position: None
+ }),
+ ],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "a".into(),
+ properties: vec![("href".into(), hast::PropertyValue::String("b".into()),),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "c".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None
+ }),
+ "should support a `LinkReference`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![mdast::Node::ListItem(mdast::ListItem {
+ spread: false,
+ checked: None,
+ children: vec![mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None
+ }),
+ "should support a `ListItem`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![mdast::Node::ListItem(mdast::ListItem {
+ spread: true,
+ checked: None,
+ children: vec![mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),],
+ position: None
+ }),
+ "should support a `ListItem` (spread: true)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![mdast::Node::ListItem(mdast::ListItem {
+ spread: false,
+ checked: Some(true),
+ children: vec![],
+ position: None
+ }),],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "task-list-item".into()
+ )])
+ )],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "input".into(),
+ properties: vec![
+ (
+ "type".into(),
+ hast::PropertyValue::String("checkbox".into()),
+ ),
+ ("checked".into(), hast::PropertyValue::Boolean(true)),
+ ("disabled".into(), hast::PropertyValue::Boolean(true)),
+ ],
+ children: vec![],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None
+ }),
+ "should support a `ListItem` (checked, w/o paragraph)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![mdast::Node::ListItem(mdast::ListItem {
+ spread: false,
+ checked: Some(false),
+ children: vec![mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "task-list-item".into()
+ )])
+ )],
+ children: vec![
+ hast::Node::Element(hast::Element {
+ tag_name: "input".into(),
+ properties: vec![
+ (
+ "type".into(),
+ hast::PropertyValue::String("checkbox".into()),
+ ),
+ ("checked".into(), hast::PropertyValue::Boolean(false)),
+ ("disabled".into(), hast::PropertyValue::Boolean(true)),
+ ],
+ children: vec![],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: " ".into(),
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),],
+ position: None
+ }),
+ "should support a `ListItem` (unchecked, w/ paragraph)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::List(mdast::List {
+ ordered: true,
+ start: Some(1),
+ spread: false,
+ children: vec![mdast::Node::ListItem(mdast::ListItem {
+ spread: false,
+ checked: None,
+ children: vec![mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),],
+ position: None
+ }),],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "ol".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ "should support a `List` (ordered, start: 1)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::List(mdast::List {
+ ordered: true,
+ start: Some(123),
+ spread: false,
+ children: vec![],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "ol".into(),
+ properties: vec![("start".into(), hast::PropertyValue::Number(123.0),),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `List` (ordered, start: 123)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::List(mdast::List {
+ ordered: false,
+ start: None,
+ spread: false,
+ children: vec![],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "ul".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `List` (unordered)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::List(mdast::List {
+ ordered: false,
+ start: None,
+ spread: false,
+ children: vec![mdast::Node::ListItem(mdast::ListItem {
+ spread: false,
+ checked: Some(true),
+ children: vec![],
+ position: None
+ }),],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "ul".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "contains-task-list".into()
+ )])
+ )],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "li".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
+ "task-list-item".into()
+ )])
+ )],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "input".into(),
+ properties: vec![
+ (
+ "type".into(),
+ hast::PropertyValue::String("checkbox".into()),
+ ),
+ ("checked".into(), hast::PropertyValue::Boolean(true)),
+ ("disabled".into(), hast::PropertyValue::Boolean(true)),
+ ],
+ children: vec![],
+ position: None
+ }),],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ "should support a `List` (w/ checked item)",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Math(mdast::Math {
+ meta: None,
+ value: "a".into(),
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "pre".into(),
+ properties: vec![],
+ children: vec![hast::Node::Element(hast::Element {
+ tag_name: "code".into(),
+ properties: vec![(
+ "className".into(),
+ hast::PropertyValue::SpaceSeparated(vec![
+ hast::PropertyItem::String("language-math".into()),
+ hast::PropertyItem::String("math-display".into())
+ ]),
+ ),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a\n".into(),
+ position: None
+ })],
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Math`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::MdxFlowExpression(mdast::MdxFlowExpression {
+ value: "a".into(),
+ position: None,
+ })),
+ hast::Node::MdxExpression(hast::MdxExpression {
+ value: "a".into(),
+ position: None
+ }),
+ "should support an `MdxFlowExpression`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::MdxTextExpression(mdast::MdxTextExpression {
+ value: "a".into(),
+ position: None,
+ })),
+ hast::Node::MdxExpression(hast::MdxExpression {
+ value: "a".into(),
+ position: None
+ }),
+ "should support an `MdxTextExpression`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::MdxJsxFlowElement(mdast::MdxJsxFlowElement {
+ name: None,
+ attributes: vec![],
+ children: vec![],
+ position: None,
+ })),
+ hast::Node::MdxJsxElement(hast::MdxJsxElement {
+ name: None,
+ attributes: vec![],
+ children: vec![],
+ position: None,
+ }),
+ "should support an `MdxJsxFlowElement`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::MdxJsxTextElement(mdast::MdxJsxTextElement {
+ name: None,
+ attributes: vec![],
+ children: vec![],
+ position: None,
+ })),
+ hast::Node::MdxJsxElement(hast::MdxJsxElement {
+ name: None,
+ attributes: vec![],
+ children: vec![],
+ position: None,
+ }),
+ "should support an `MdxJsxTextElement`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::MdxjsEsm(mdast::MdxjsEsm {
+ value: "a".into(),
+ position: None,
+ })),
+ hast::Node::MdxjsEsm(hast::MdxjsEsm {
+ value: "a".into(),
+ position: None
+ }),
+ "should support an `MdxjsEsm`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Paragraph(mdast::Paragraph {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "p".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Paragraph`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Root(mdast::Root {
+ children: vec![],
+ position: None,
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![],
+ position: None
+ }),
+ "should support a `Root`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Strong(mdast::Strong {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "strong".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `Strong`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "td".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ "should support a `TableCell`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::TableRow(mdast::TableRow {
+ children: vec![
+ mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ }),
+ mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None,
+ })
+ ],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "tr".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "td".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "td".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ "should support a `TableRow`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Table(mdast::Table {
+ align: vec![mdast::AlignKind::Left, mdast::AlignKind::None],
+ children: vec![
+ mdast::Node::TableRow(mdast::TableRow {
+ children: vec![
+ mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None,
+ }),
+ mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None,
+ })
+ ],
+ position: None,
+ }),
+ mdast::Node::TableRow(mdast::TableRow {
+ children: vec![
+ mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "c".into(),
+ position: None
+ })],
+ position: None,
+ }),
+ mdast::Node::TableCell(mdast::TableCell {
+ children: vec![mdast::Node::Text(mdast::Text {
+ value: "d".into(),
+ position: None
+ })],
+ position: None,
+ })
+ ],
+ position: None,
+ })
+ ],
+ position: None,
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "table".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "thead".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "tr".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "th".into(),
+ properties: vec![(
+ "align".into(),
+ hast::PropertyValue::String("left".into()),
+ ),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ })],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "th".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "b".into(),
+ position: None
+ })],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "tbody".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "tr".into(),
+ properties: vec![],
+ children: vec![
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "td".into(),
+ properties: vec![(
+ "align".into(),
+ hast::PropertyValue::String("left".into()),
+ ),],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "c".into(),
+ position: None
+ })],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ hast::Node::Element(hast::Element {
+ tag_name: "td".into(),
+ properties: vec![],
+ children: vec![hast::Node::Text(hast::Text {
+ value: "d".into(),
+ position: None
+ })],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ hast::Node::Text(hast::Text {
+ value: "\n".into(),
+ position: None
+ }),
+ ],
+ position: None
+ }),
+ "should support a `Table`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Text(mdast::Text {
+ value: "a".into(),
+ position: None,
+ })),
+ hast::Node::Text(hast::Text {
+ value: "a".into(),
+ position: None
+ }),
+ "should support a `Text`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::ThematicBreak(mdast::ThematicBreak {
+ position: None
+ })),
+ hast::Node::Element(hast::Element {
+ tag_name: "hr".into(),
+ properties: vec![],
+ children: vec![],
+ position: None
+ }),
+ "should support a `Thematicbreak`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Yaml(mdast::Yaml {
+ value: "a".into(),
+ position: None
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![],
+ position: None
+ }),
+ "should support a `Yaml`",
+ );
+
+ assert_eq!(
+ to_hast(&mdast::Node::Toml(mdast::Toml {
+ value: "a".into(),
+ position: None
+ })),
+ hast::Node::Root(hast::Root {
+ children: vec![],
+ position: None
+ }),
+ "should support a `Toml`",
+ );
+}