aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_utils/to_hast.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_utils/to_hast.rs')
-rw-r--r--tests/test_utils/to_hast.rs1457
1 files changed, 1457 insertions, 0 deletions
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
+ }
+}