//! Turn a markdown AST into an HTML AST. //! //! Port of , by the same //! author: //! //! (The MIT License) //! //! Copyright (c) 2016 Titus Wormer //! //! Permission is hereby granted, free of charge, to any person obtaining //! a copy of this software and associated documentation files (the //! 'Software'), to deal in the Software without restriction, including //! without limitation the rights to use, copy, modify, merge, publish, //! distribute, sublicense, and/or sell copies of the Software, and to //! permit persons to whom the Software is furnished to do so, subject to //! the following conditions: //! //! The above copyright notice and this permission notice shall be //! included in all copies or substantial portions of the Software. //! //! THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, //! EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF //! MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. //! IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY //! CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, //! TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE //! SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::test_utils::hast; use markdown::{mdast, sanitize, unist::Position}; // To do: support these compile options: // ``` // pub gfm_footnote_label: Option, // pub gfm_footnote_label_tag_name: Option, // pub gfm_footnote_label_attributes: Option, // pub gfm_footnote_back_label: Option, // pub gfm_footnote_clobber_prefix: Option, // ``` // // Maybe also: // * option to persist `meta`? // * option to generate a `style` attribute instead of `align`? // * support `Raw` nodes for HTML? // // To do: // * revert references when undefined? // #[derive(Debug)] struct State { definitions: Vec<(String, String, Option)>, footnote_definitions: Vec<(String, Vec)>, footnote_calls: Vec<(String, usize)>, } #[derive(Debug)] enum Result { Fragment(Vec), Node(hast::Node), None, } #[allow(dead_code)] pub fn mdast_util_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(), )); } }); let mut state = State { definitions, footnote_definitions: vec![], footnote_calls: vec![], }; let result = one(&mut state, mdast, None); 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![ "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(), 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!["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!["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(state: &mut State, node: &mdast::Node, parent: Option<&mdast::Node>) -> Result { match node { mdast::Node::BlockQuote(d) => transform_block_quote(state, node, d), mdast::Node::Break(d) => transform_break(state, node, d), mdast::Node::Code(d) => transform_code(state, node, d), mdast::Node::Delete(d) => transform_delete(state, node, d), mdast::Node::Emphasis(d) => transform_emphasis(state, node, d), mdast::Node::FootnoteDefinition(d) => transform_footnote_definition(state, node, d), mdast::Node::FootnoteReference(d) => transform_footnote_reference(state, node, d), mdast::Node::Heading(d) => transform_heading(state, node, d), mdast::Node::Image(d) => transform_image(state, node, d), mdast::Node::ImageReference(d) => transform_image_reference(state, node, d), mdast::Node::InlineCode(d) => transform_inline_code(state, node, d), mdast::Node::InlineMath(d) => transform_inline_math(state, node, d), mdast::Node::Link(d) => transform_link(state, node, d), mdast::Node::LinkReference(d) => transform_link_reference(state, node, d), mdast::Node::ListItem(d) => transform_list_item(state, node, parent, d), mdast::Node::List(d) => transform_list(state, node, d), mdast::Node::Math(d) => transform_math(state, node, d), mdast::Node::MdxFlowExpression(_) | mdast::Node::MdxTextExpression(_) => { transform_mdx_expression(state, node) } mdast::Node::MdxJsxFlowElement(_) | mdast::Node::MdxJsxTextElement(_) => { transform_mdx_jsx_element(state, node) } mdast::Node::MdxjsEsm(d) => transform_mdxjs_esm(state, node, d), mdast::Node::Paragraph(d) => transform_paragraph(state, node, d), mdast::Node::Root(d) => transform_root(state, node, d), mdast::Node::Strong(d) => transform_strong(state, node, d), // Note: this is only called here if there is a single cell passed, not when one is found in a table. mdast::Node::TableCell(d) => { transform_table_cell(state, node, false, mdast::AlignKind::None, d) } // Note: this is only called here if there is a single row passed, not when one is found in a table. mdast::Node::TableRow(d) => transform_table_row(state, node, false, None, d), mdast::Node::Table(d) => transform_table(state, node, d), mdast::Node::Text(d) => transform_text(state, node, d), mdast::Node::ThematicBreak(d) => transform_thematic_break(state, node, d), // Ignore. mdast::Node::Definition(_) | mdast::Node::Html(_) | mdast::Node::Yaml(_) | mdast::Node::Toml(_) => Result::None, } } /// [`BlockQuote`][mdast::BlockQuote]. fn transform_block_quote( state: &mut State, node: &mdast::Node, block_quote: &mdast::BlockQuote, ) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "blockquote".into(), properties: vec![], children: wrap(all(state, node), true), position: block_quote.position.clone(), })) } /// [`Break`][mdast::Break]. fn transform_break(_state: &mut State, _node: &mdast::Node, break_: &mdast::Break) -> Result { Result::Fragment(vec![ hast::Node::Element(hast::Element { tag_name: "br".into(), properties: vec![], children: vec![], position: break_.position.clone(), }), hast::Node::Text(hast::Text { value: "\n".into(), position: None, }), ]) } /// [`Code`][mdast::Code]. fn transform_code(_state: &mut State, _node: &mdast::Node, code: &mdast::Code) -> Result { let mut value = code.value.clone(); value.push('\n'); let mut properties = vec![]; if let Some(lang) = code.lang.as_ref() { properties.push(( "className".into(), hast::PropertyValue::SpaceSeparated(vec![format!("language-{}", lang)]), )); } Result::Node(hast::Node::Element(hast::Element { tag_name: "pre".into(), properties: vec![], children: vec![hast::Node::Element(hast::Element { tag_name: "code".into(), properties, children: vec![hast::Node::Text(hast::Text { value, position: None, })], position: code.position.clone(), })], position: code.position.clone(), })) } /// [`Delete`][mdast::Delete]. fn transform_delete(state: &mut State, node: &mdast::Node, delete: &mdast::Delete) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "del".into(), properties: vec![], children: all(state, node), position: delete.position.clone(), })) } /// [`Emphasis`][mdast::Emphasis]. fn transform_emphasis(state: &mut State, node: &mdast::Node, emphasis: &mdast::Emphasis) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "em".into(), properties: vec![], children: all(state, node), position: emphasis.position.clone(), })) } /// [`FootnoteDefinition`][mdast::FootnoteDefinition]. fn transform_footnote_definition( state: &mut State, node: &mdast::Node, footnote_definition: &mdast::FootnoteDefinition, ) -> Result { let children = all(state, node); // Set aside. state .footnote_definitions .push((footnote_definition.identifier.clone(), children)); Result::None } /// [`FootnoteReference`][mdast::FootnoteReference]. fn transform_footnote_reference( state: &mut State, _node: &mdast::Node, footnote_reference: &mdast::FootnoteReference, ) -> Result { let safe_id = sanitize(&footnote_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 == footnote_reference.identifier { break; } call_index += 1; } // New. if call_index == state.footnote_calls.len() { state .footnote_calls .push((footnote_reference.identifier.clone(), 0)); } // Increment. state.footnote_calls[call_index].1 += 1; let reuse_counter = state.footnote_calls[call_index].1; Result::Node(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(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: footnote_reference.position.clone(), })) } /// [`Heading`][mdast::Heading]. fn transform_heading(state: &mut State, node: &mdast::Node, heading: &mdast::Heading) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: format!("h{}", heading.depth), properties: vec![], children: all(state, node), position: heading.position.clone(), })) } /// [`Image`][mdast::Image]. fn transform_image(_state: &mut State, _node: &mdast::Node, image: &mdast::Image) -> Result { 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(hast::Node::Element(hast::Element { tag_name: "img".into(), properties, children: vec![], position: image.position.clone(), })) } /// [`ImageReference`][mdast::ImageReference]. fn transform_image_reference( state: &mut State, _node: &mdast::Node, image_reference: &mdast::ImageReference, ) -> Result { let mut properties = vec![]; let definition = state .definitions .iter() .find(|d| d.0 == image_reference.identifier); 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(image_reference.alt.clone()), )); if let Some(value) = title { properties.push(("title".into(), hast::PropertyValue::String(value.into()))); } Result::Node(hast::Node::Element(hast::Element { tag_name: "img".into(), properties, children: vec![], position: image_reference.position.clone(), })) } /// [`InlineCode`][mdast::InlineCode]. fn transform_inline_code( _state: &mut State, _node: &mdast::Node, inline_code: &mdast::InlineCode, ) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "code".into(), properties: vec![], children: vec![hast::Node::Text(hast::Text { value: replace_eols_with_spaces(&inline_code.value), position: None, })], position: inline_code.position.clone(), })) } /// [`InlineMath`][mdast::InlineMath]. fn transform_inline_math( _state: &mut State, _node: &mdast::Node, inline_math: &mdast::InlineMath, ) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "code".into(), properties: vec![( "className".into(), hast::PropertyValue::SpaceSeparated(vec!["language-math".into(), "math-inline".into()]), )], children: vec![hast::Node::Text(hast::Text { value: replace_eols_with_spaces(&inline_math.value), position: None, })], position: inline_math.position.clone(), })) } /// [`Link`][mdast::Link]. fn transform_link(state: &mut State, node: &mdast::Node, link: &mdast::Link) -> Result { 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()))); } Result::Node(hast::Node::Element(hast::Element { tag_name: "a".into(), properties, children: all(state, node), position: link.position.clone(), })) } /// [`LinkReference`][mdast::LinkReference]. fn transform_link_reference( state: &mut State, node: &mdast::Node, link_reference: &mdast::LinkReference, ) -> Result { let mut properties = vec![]; let definition = state .definitions .iter() .find(|d| d.0 == link_reference.identifier); 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()))); } Result::Node(hast::Node::Element(hast::Element { tag_name: "a".into(), properties, children: all(state, node), position: link_reference.position.clone(), })) } /// [`ListItem`][mdast::ListItem]. fn transform_list_item( state: &mut State, node: &mdast::Node, parent: Option<&mdast::Node>, list_item: &mdast::ListItem, ) -> Result { let mut children = all(state, node); 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) = list_item.checked { // According to github-markdown-css, this class hides bullet. // See: . properties.push(( "className".into(), hast::PropertyValue::SpaceSeparated(vec!["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(hast::Node::Element(hast::Element { tag_name: "li".into(), properties, children: result, position: list_item.position.clone(), })) } /// [`List`][mdast::List]. fn transform_list(state: &mut State, node: &mdast::Node, list: &mdast::List) -> Result { 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 mut properties = vec![]; // Add start. if let Some(start) = list.start { if list.ordered && start != 1 { properties.push(( "start".into(), hast::PropertyValue::String(start.to_string()), )); } } // Like GitHub, add a class for custom styling. if contains_task_list { properties.push(( "className".into(), hast::PropertyValue::SpaceSeparated(vec!["contains-task-list".into()]), )); } Result::Node(hast::Node::Element(hast::Element { tag_name: if list.ordered { "ol".into() } else { "ul".into() }, properties, children: wrap(all(state, node), true), position: list.position.clone(), })) } /// [`Math`][mdast::Math]. fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) -> Result { let mut value = math.value.clone(); value.push('\n'); Result::Node(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![ "language-math".into(), "math-display".into(), ]), )], children: vec![hast::Node::Text(hast::Text { value, position: None, })], position: math.position.clone(), })], position: math.position.clone(), })) } /// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { match node { mdast::Node::MdxFlowExpression(node) => { Result::Node(hast::Node::MdxExpression(hast::MdxExpression { value: node.value.clone(), position: node.position.clone(), stops: node.stops.clone(), })) } mdast::Node::MdxTextExpression(node) => { Result::Node(hast::Node::MdxExpression(hast::MdxExpression { value: node.value.clone(), position: node.position.clone(), stops: node.stops.clone(), })) } _ => unreachable!("expected expression"), } } /// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. fn transform_mdx_jsx_element(state: &mut State, node: &mdast::Node) -> Result { 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(hast::Node::MdxJsxElement(hast::MdxJsxElement { name: name.clone(), attributes: attributes.clone(), children: all(state, node), position: node.position().cloned(), })) } /// [`MdxjsEsm`][mdast::MdxjsEsm]. fn transform_mdxjs_esm( _state: &mut State, _node: &mdast::Node, mdxjs_esm: &mdast::MdxjsEsm, ) -> Result { Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { value: mdxjs_esm.value.clone(), position: mdxjs_esm.position.clone(), stops: mdxjs_esm.stops.clone(), })) } /// [`Paragraph`][mdast::Paragraph]. fn transform_paragraph( state: &mut State, node: &mdast::Node, paragraph: &mdast::Paragraph, ) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "p".into(), properties: vec![], children: all(state, node), position: paragraph.position.clone(), })) } /// [`Root`][mdast::Root]. fn transform_root(state: &mut State, node: &mdast::Node, root: &mdast::Root) -> Result { Result::Node(hast::Node::Root(hast::Root { children: wrap(all(state, node), false), position: root.position.clone(), })) } /// [`Strong`][mdast::Strong]. fn transform_strong(state: &mut State, node: &mdast::Node, strong: &mdast::Strong) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "strong".into(), properties: vec![], children: all(state, node), position: strong.position.clone(), })) } /// [`TableCell`][mdast::TableCell]. fn transform_table_cell( state: &mut State, node: &mdast::Node, head: bool, align: mdast::AlignKind, table_cell: &mdast::TableCell, ) -> Result { 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(hast::Node::Element(hast::Element { tag_name: if head { "th".into() } else { "td".into() }, properties, children: all(state, node), position: table_cell.position.clone(), })) } /// [`TableRow`][mdast::TableRow]. fn transform_table_row( state: &mut State, _node: &mdast::Node, head: bool, align: Option<&[mdast::AlignKind]>, table_row: &mdast::TableRow, ) -> Result { let mut children = vec![]; let mut index = 0; #[allow(clippy::redundant_closure_for_method_calls)] let len = align.map_or(table_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 = table_row.children.get(index).unwrap_or(&empty_cell); let result = if let mdast::Node::TableCell(table_cell) = child { transform_table_cell(state, child, head, *align_value, table_cell) } else { unreachable!("expected tale cell in table row") }; append_result(&mut children, result); index += 1; } Result::Node(hast::Node::Element(hast::Element { tag_name: "tr".into(), properties: vec![], children: wrap(children, true), position: table_row.position.clone(), })) } /// [`Table`][mdast::Table]. fn transform_table(state: &mut State, _node: &mdast::Node, table: &mdast::Table) -> Result { let mut rows = vec![]; let mut index = 0; while index < table.children.len() { let child = &table.children[index]; let result = if let mdast::Node::TableRow(table_row) = child { transform_table_row( state, &table.children[index], index == 0, Some(&table.align), table_row, ) } else { unreachable!("expected table row as child of table") }; append_result(&mut rows, result); 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(hast::Node::Element(hast::Element { tag_name: "table".into(), properties: vec![], children: wrap(children, true), position: table.position.clone(), })) } /// [`Text`][mdast::Text]. fn transform_text(_state: &mut State, _node: &mdast::Node, text: &mdast::Text) -> Result { Result::Node(hast::Node::Text(hast::Text { value: text.value.clone(), position: text.position.clone(), })) } /// [`ThematicBreak`][mdast::ThematicBreak]. fn transform_thematic_break( _state: &mut State, _node: &mdast::Node, thematic_break: &mdast::ThematicBreak, ) -> Result { Result::Node(hast::Node::Element(hast::Element { tag_name: "hr".into(), properties: vec![], children: vec![], position: thematic_break.position.clone(), })) } // Transform children of `parent`. fn all(state: &mut State, parent: &mdast::Node) -> Vec { let mut nodes = vec![]; if let Some(children) = parent.children() { let mut index = 0; while index < children.len() { let child = &children[index]; let result = one(state, child, Some(parent)); append_result(&mut nodes, result); index += 1; } } nodes } /// Wrap `nodes` with line feeds between each entry. /// Optionally adds line feeds at the start and end. fn wrap(mut nodes: Vec, loose: bool) -> Vec { let mut result = vec![]; let was_empty = nodes.is_empty(); let mut head = true; nodes.reverse(); if loose { result.push(hast::Node::Text(hast::Text { value: "\n".into(), position: None, })); } while let Some(item) = nodes.pop() { // Inject when there’s more: if !head { result.push(hast::Node::Text(hast::Text { value: "\n".into(), position: None, })); } head = false; result.push(item); } if loose && !was_empty { result.push(hast::Node::Text(hast::Text { value: "\n".into(), position: None, })); } result } /// Visit. fn visit(node: &mdast::Node, visitor: Visitor) where Visitor: FnMut(&mdast::Node), { visit_impl(node, visitor); } /// Visit, mutably. // Probably useful later: #[allow(dead_code)] fn visit_mut(node: &mut mdast::Node, visitor: Visitor) where Visitor: FnMut(&mut mdast::Node), { visit_mut_impl(node, visitor); } /// Internal implementation to visit. fn visit_impl(node: &mdast::Node, mut visitor: Visitor) -> Visitor where Visitor: FnMut(&mdast::Node), { visitor(node); if let Some(children) = node.children() { let mut index = 0; while index < children.len() { let child = &children[index]; visitor = visit_impl(child, visitor); index += 1; } } visitor } /// Internal implementation to visit, mutably. fn visit_mut_impl(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor where Visitor: FnMut(&mut mdast::Node), { visitor(node); if let Some(children) = node.children_mut() { let mut index = 0; while let Some(child) = children.get_mut(index) { visitor = visit_mut_impl(child, visitor); index += 1; } } visitor } // To do: trim arounds breaks: . /// Append an (optional, variadic) result. fn append_result(list: &mut Vec, result: Result) { match result { Result::Fragment(mut fragment) => list.append(&mut fragment), Result::Node(node) => list.push(node), Result::None => {} }; } /// Replace line endings (CR, LF, CRLF) with spaces. /// /// Used for inline code and inline math. fn replace_eols_with_spaces(value: &str) -> String { // It’ll grow a bit small for each CR+LF. let mut result = String::with_capacity(value.len()); let bytes = value.as_bytes(); let mut index = 0; let mut start = 0; while index < bytes.len() { let byte = bytes[index]; if byte == b'\r' || byte == b'\n' { result.push_str(&value[start..index]); result.push(' '); if index + 1 < bytes.len() && byte == b'\r' && bytes[index + 1] == b'\n' { index += 1; } start = index + 1; } index += 1; } result.push_str(&value[start..]); result } /// Check if a list is loose. fn list_loose(node: &mdast::Node) -> bool { if let mdast::Node::List(list) = node { if list.spread { return true; } if let Some(children) = node.children() { let mut index = 0; while index < children.len() { if list_item_loose(&children[index]) { return true; } index += 1; } } } false } /// Check if a list item is loose. fn list_item_loose(node: &mdast::Node) -> bool { if let mdast::Node::ListItem(item) = node { item.spread } else { false } }