aboutsummaryrefslogtreecommitdiffstats
path: root/src/to_mdast.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/to_mdast.rs1762
1 files changed, 1735 insertions, 27 deletions
diff --git a/src/to_mdast.rs b/src/to_mdast.rs
index d56134a..9f03a03 100644
--- a/src/to_mdast.rs
+++ b/src/to_mdast.rs
@@ -1,40 +1,1748 @@
//! Turn events into a syntax tree.
-// To do: example.
+use crate::event::{Event, Kind, Name};
+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,
+};
+use crate::util::{
+ decode_character_reference::{decode_named, decode_numeric},
+ infer::{gfm_table_align, list_item_loose, list_loose},
+ normalize_identifier::normalize_identifier,
+ slice::{Position as SlicePosition, Slice},
+};
+use alloc::{
+ format,
+ string::{String, ToString},
+ vec,
+ vec::Vec,
+};
+use core::str;
-use crate::event::Event;
-use crate::mdast;
-use crate::Options;
-use alloc::vec;
+#[derive(Debug)]
+struct Reference {
+ reference_kind: Option<ReferenceKind>,
+ identifier: String,
+ label: String,
+}
+
+#[derive(Debug, Clone)]
+struct JsxTag {
+ name: Option<String>,
+ attributes: Vec<AttributeContent>,
+ close: bool,
+ self_closing: bool,
+ start: Point,
+ end: Point,
+}
+
+impl Reference {
+ fn new() -> Reference {
+ Reference {
+ // Assume shortcut: removed on a resource, changed on a reference.
+ reference_kind: Some(ReferenceKind::Shortcut),
+ identifier: String::new(),
+ label: String::new(),
+ }
+ }
+}
+
+/// Context used to compile markdown.
+#[allow(clippy::struct_excessive_bools)]
+#[derive(Debug)]
+struct CompileContext<'a> {
+ // Static info.
+ /// List of events.
+ events: &'a [Event],
+ /// List of bytes.
+ bytes: &'a [u8],
+ // Fields used by handlers to track the things they need to track to
+ // compile markdown.
+ character_reference_marker: u8,
+ gfm_table_inside: bool,
+ gfm_task_list_item_check_after: bool,
+ hard_break_after: bool,
+ heading_setext_text_after: bool,
+ jsx_tag_stack: Vec<JsxTag>,
+ jsx_tag: Option<JsxTag>,
+ media_reference_stack: Vec<Reference>,
+ raw_flow_fence_seen: bool,
+ // Intermediate results.
+ /// Primary tree and buffers.
+ trees: Vec<(Node, Vec<usize>, Vec<usize>)>,
+ /// Current event index.
+ index: usize,
+}
+
+impl<'a> CompileContext<'a> {
+ /// Create a new compile context.
+ fn new(events: &'a [Event], bytes: &'a [u8]) -> CompileContext<'a> {
+ let tree = Node::Root(Root {
+ children: vec![],
+ position: Some(Position {
+ start: if events.is_empty() {
+ Point::new(1, 1, 0)
+ } else {
+ point_from_event(&events[0])
+ },
+ end: if events.is_empty() {
+ Point::new(1, 1, 0)
+ } else {
+ point_from_event(&events[events.len() - 1])
+ },
+ }),
+ });
+
+ CompileContext {
+ events,
+ bytes,
+ character_reference_marker: 0,
+ gfm_table_inside: false,
+ gfm_task_list_item_check_after: false,
+ hard_break_after: false,
+ heading_setext_text_after: false,
+ jsx_tag_stack: vec![],
+ jsx_tag: None,
+ media_reference_stack: vec![],
+ raw_flow_fence_seen: false,
+ trees: vec![(tree, vec![], vec![])],
+ index: 0,
+ }
+ }
+
+ /// Push a buffer.
+ fn buffer(&mut self) {
+ self.trees.push((
+ Node::Paragraph(Paragraph {
+ children: vec![],
+ position: None,
+ }),
+ vec![],
+ vec![],
+ ));
+ }
+
+ /// Pop a buffer, returning its value.
+ fn resume(&mut self) -> Node {
+ if let Some((node, stack_a, stack_b)) = self.trees.pop() {
+ debug_assert_eq!(
+ stack_a.len(),
+ 0,
+ "expected stack (nodes in tree) to be drained"
+ );
+ debug_assert_eq!(
+ stack_b.len(),
+ 0,
+ "expected stack (opening events) to be drained"
+ );
+ node
+ } else {
+ unreachable!("Cannot resume w/o buffer")
+ }
+ }
+
+ fn tail_mut(&mut self) -> &mut Node {
+ let (tree, stack, _) = self.trees.last_mut().expect("Cannot get tail w/o tree");
+ delve_mut(tree, stack)
+ }
+
+ fn tail_penultimate_mut(&mut self) -> &mut Node {
+ let (tree, stack, _) = self.trees.last_mut().expect("Cannot get tail w/o tree");
+ delve_mut(tree, &stack[0..(stack.len() - 1)])
+ }
+
+ fn tail_push(&mut self, mut child: Node) {
+ if child.position().is_none() {
+ child.position_set(Some(position_from_event(&self.events[self.index])));
+ }
+
+ let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree");
+ let node = delve_mut(tree, stack);
+ let children = node.children_mut().expect("Cannot push to non-parent");
+ let index = children.len();
+ children.push(child);
+ stack.push(index);
+ event_stack.push(self.index);
+ }
+
+ fn tail_push_again(&mut self) {
+ let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree");
+ let node = delve_mut(tree, stack);
+ let children = node.children().expect("Cannot push to non-parent");
+ stack.push(children.len() - 1);
+ event_stack.push(self.index);
+ }
+
+ fn tail_pop(&mut self) -> Result<(), String> {
+ let ev = &self.events[self.index];
+ let end = point_from_event(ev);
+ let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree");
+ let node = delve_mut(tree, stack);
+ node.position_mut()
+ .expect("Cannot pop manually added node")
+ .end = end;
+
+ stack.pop().unwrap();
+
+ if let Some(left_index) = event_stack.pop() {
+ let left = &self.events[left_index];
+ if left.name != ev.name {
+ on_mismatch_error(self, Some(ev), left)?;
+ }
+ } else {
+ return Err(format!(
+ "{}:{}: Cannot close `{:?}`, it’s not open",
+ ev.point.line, ev.point.column, ev.name
+ ));
+ }
+
+ Ok(())
+ }
+}
/// Turn events and bytes into a syntax tree.
-pub fn compile(events: &[Event], _bytes: &[u8], _options: &Options) -> mdast::Root {
- mdast::Root {
- kind: mdast::Kind::Root,
+pub fn compile(events: &[Event], bytes: &[u8]) -> Result<Node, String> {
+ let mut context = CompileContext::new(events, bytes);
+
+ let mut index = 0;
+ while index < events.len() {
+ handle(&mut context, index)?;
+ index += 1;
+ }
+
+ debug_assert_eq!(context.trees.len(), 1, "expected 1 final tree");
+ let (tree, _, event_stack) = context.trees.pop().unwrap();
+
+ if let Some(index) = event_stack.last() {
+ let event = &events[*index];
+ on_mismatch_error(&mut context, None, event)?;
+ }
+
+ Ok(tree)
+}
+
+/// Handle the event at `index`.
+fn handle(context: &mut CompileContext, index: usize) -> Result<(), String> {
+ context.index = index;
+
+ if context.events[index].kind == Kind::Enter {
+ enter(context)?;
+ } else {
+ exit(context)?;
+ }
+
+ Ok(())
+}
+
+/// Handle [`Enter`][Kind::Enter].
+fn enter(context: &mut CompileContext) -> Result<(), String> {
+ match context.events[context.index].name {
+ Name::AutolinkEmail
+ | Name::AutolinkProtocol
+ | Name::CharacterEscapeValue
+ | Name::CharacterReference
+ | Name::CodeFlowChunk
+ | Name::CodeTextData
+ | Name::Data
+ | Name::FrontmatterChunk
+ | Name::HtmlFlowData
+ | Name::HtmlTextData
+ | Name::MathFlowChunk
+ | Name::MathTextData
+ | Name::MdxExpressionData
+ | Name::MdxEsmData
+ | Name::MdxJsxTagAttributeValueLiteralValue => on_enter_data(context),
+ Name::CodeFencedFenceInfo
+ | Name::CodeFencedFenceMeta
+ | Name::DefinitionDestinationString
+ | Name::DefinitionLabelString
+ | Name::DefinitionTitleString
+ | Name::GfmFootnoteDefinitionLabelString
+ | Name::LabelText
+ | Name::MathFlowFenceMeta
+ | Name::MdxJsxTagAttributeValueLiteral
+ | Name::MdxJsxTagAttributeValueExpression
+ | Name::ReferenceString
+ | Name::ResourceDestinationString
+ | Name::ResourceTitleString => on_enter_buffer(context),
+ Name::Autolink => on_enter_autolink(context),
+ Name::BlockQuote => on_enter_block_quote(context),
+ Name::CodeFenced => on_enter_code_fenced(context),
+ Name::CodeIndented => on_enter_code_indented(context),
+ Name::CodeText => on_enter_code_text(context),
+ Name::Definition => on_enter_definition(context),
+ Name::Emphasis => on_enter_emphasis(context),
+ Name::Frontmatter => on_enter_frontmatter(context),
+ Name::GfmAutolinkLiteralEmail
+ | Name::GfmAutolinkLiteralMailto
+ | Name::GfmAutolinkLiteralProtocol
+ | Name::GfmAutolinkLiteralWww
+ | Name::GfmAutolinkLiteralXmpp => on_enter_gfm_autolink_literal(context),
+ Name::GfmFootnoteCall => on_enter_gfm_footnote_call(context),
+ Name::GfmFootnoteDefinition => on_enter_gfm_footnote_definition(context),
+ Name::GfmStrikethrough => on_enter_gfm_strikethrough(context),
+ Name::GfmTable => on_enter_gfm_table(context),
+ Name::GfmTableRow => on_enter_gfm_table_row(context),
+ Name::GfmTableCell => on_enter_gfm_table_cell(context),
+ Name::HardBreakEscape | Name::HardBreakTrailing => on_enter_hard_break(context),
+ Name::HeadingAtx | Name::HeadingSetext => on_enter_heading(context),
+ Name::HtmlFlow | Name::HtmlText => on_enter_html(context),
+ Name::Image => on_enter_image(context),
+ Name::Link => on_enter_link(context),
+ Name::ListItem => on_enter_list_item(context),
+ Name::ListOrdered | Name::ListUnordered => on_enter_list(context),
+ Name::MathFlow => on_enter_math_flow(context),
+ Name::MathText => on_enter_math_text(context),
+ Name::MdxEsm => on_enter_mdx_esm(context),
+ Name::MdxFlowExpression => on_enter_mdx_flow_expression(context),
+ Name::MdxTextExpression => on_enter_mdx_text_expression(context),
+ Name::MdxJsxFlowTag | Name::MdxJsxTextTag => on_enter_mdx_jsx_tag(context),
+ Name::MdxJsxTagClosingMarker => on_enter_mdx_jsx_tag_closing_marker(context)?,
+ Name::MdxJsxTagAttribute => on_enter_mdx_jsx_tag_attribute(context)?,
+ Name::MdxJsxTagAttributeExpression => on_enter_mdx_jsx_tag_attribute_expression(context)?,
+ Name::MdxJsxTagSelfClosingMarker => on_enter_mdx_jsx_tag_self_closing_marker(context)?,
+ Name::Paragraph => on_enter_paragraph(context),
+ Name::Reference => on_enter_reference(context),
+ Name::Resource => on_enter_resource(context),
+ Name::Strong => on_enter_strong(context),
+ Name::ThematicBreak => on_enter_thematic_break(context),
+ _ => {}
+ }
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit].
+fn exit(context: &mut CompileContext) -> Result<(), String> {
+ match context.events[context.index].name {
+ Name::Autolink
+ | Name::BlockQuote
+ | Name::CharacterReference
+ | Name::Definition
+ | Name::Emphasis
+ | Name::GfmFootnoteDefinition
+ | Name::GfmStrikethrough
+ | Name::GfmTableRow
+ | Name::GfmTableCell
+ | Name::HeadingAtx
+ | Name::ListItem
+ | Name::ListOrdered
+ | Name::ListUnordered
+ | Name::Paragraph
+ | Name::Strong
+ | Name::ThematicBreak => {
+ on_exit(context)?;
+ }
+ Name::CharacterEscapeValue
+ | Name::CodeFlowChunk
+ | Name::CodeTextData
+ | Name::FrontmatterChunk
+ | Name::HtmlFlowData
+ | Name::HtmlTextData
+ | Name::MathFlowChunk
+ | Name::MathTextData
+ | Name::MdxExpressionData
+ | Name::MdxEsmData
+ | Name::MdxJsxTagAttributeValueLiteralValue => {
+ on_exit_data(context)?;
+ }
+ Name::AutolinkProtocol => on_exit_autolink_protocol(context)?,
+ Name::AutolinkEmail => on_exit_autolink_email(context)?,
+ Name::CharacterReferenceMarker => on_exit_character_reference_marker(context),
+ Name::CharacterReferenceMarkerNumeric => {
+ on_exit_character_reference_marker_numeric(context);
+ }
+ Name::CharacterReferenceMarkerHexadecimal => {
+ on_exit_character_reference_marker_hexadecimal(context);
+ }
+ Name::CharacterReferenceValue => on_exit_character_reference_value(context),
+ Name::CodeFencedFenceInfo => on_exit_code_fenced_fence_info(context),
+ Name::CodeFencedFenceMeta | Name::MathFlowFenceMeta => on_exit_raw_flow_fence_meta(context),
+ Name::CodeFencedFence | Name::MathFlowFence => on_exit_raw_flow_fence(context),
+ Name::CodeFenced | Name::MathFlow => on_exit_raw_flow(context)?,
+ Name::CodeIndented => on_exit_code_indented(context)?,
+ Name::CodeText | Name::MathText => on_exit_raw_text(context)?,
+ Name::Data => on_exit_data_actual(context)?,
+ Name::DefinitionDestinationString => on_exit_definition_destination_string(context),
+ Name::DefinitionLabelString | Name::GfmFootnoteDefinitionLabelString => {
+ on_exit_definition_id(context);
+ }
+ Name::DefinitionTitleString => on_exit_definition_title_string(context),
+ Name::Frontmatter => on_exit_frontmatter(context)?,
+ Name::GfmAutolinkLiteralEmail
+ | Name::GfmAutolinkLiteralMailto
+ | Name::GfmAutolinkLiteralProtocol
+ | Name::GfmAutolinkLiteralWww
+ | Name::GfmAutolinkLiteralXmpp => on_exit_gfm_autolink_literal(context)?,
+ Name::GfmFootnoteCall | Name::Image | Name::Link => on_exit_media(context)?,
+ Name::GfmTable => on_exit_gfm_table(context)?,
+ Name::GfmTaskListItemCheck => on_exit_gfm_task_list_item_check(context),
+ Name::GfmTaskListItemValueUnchecked | Name::GfmTaskListItemValueChecked => {
+ on_exit_gfm_task_list_item_value(context);
+ }
+ Name::HardBreakEscape | Name::HardBreakTrailing => on_exit_hard_break(context)?,
+ Name::HeadingAtxSequence => on_exit_heading_atx_sequence(context),
+ Name::HeadingSetext => on_exit_heading_setext(context)?,
+ Name::HeadingSetextUnderlineSequence => on_exit_heading_setext_underline_sequence(context),
+ Name::HeadingSetextText => on_exit_heading_setext_text(context),
+ Name::HtmlFlow
+ | Name::HtmlText
+ | Name::MdxEsm
+ | Name::MdxFlowExpression
+ | Name::MdxTextExpression => on_exit_literal(context)?,
+ Name::LabelText => on_exit_label_text(context),
+ Name::LineEnding => on_exit_line_ending(context)?,
+ Name::ListItemValue => on_exit_list_item_value(context),
+ Name::MdxJsxFlowTag | Name::MdxJsxTextTag => on_exit_mdx_jsx_tag(context)?,
+ Name::MdxJsxTagClosingMarker => on_exit_mdx_jsx_tag_closing_marker(context),
+ Name::MdxJsxTagNamePrimary => on_exit_mdx_jsx_tag_name_primary(context),
+ Name::MdxJsxTagNameMember => on_exit_mdx_jsx_tag_name_member(context),
+ Name::MdxJsxTagNameLocal => on_exit_mdx_jsx_tag_name_local(context),
+ Name::MdxJsxTagAttributeExpression => on_exit_mdx_jsx_tag_attribute_expression(context),
+ Name::MdxJsxTagAttributePrimaryName => on_exit_mdx_jsx_tag_attribute_primary_name(context),
+ Name::MdxJsxTagAttributeNameLocal => on_exit_mdx_jsx_tag_attribute_name_local(context),
+ Name::MdxJsxTagAttributeValueLiteral => {
+ on_exit_mdx_jsx_tag_attribute_value_literal(context);
+ }
+ Name::MdxJsxTagAttributeValueExpression => {
+ on_exit_mdx_jsx_tag_attribute_value_expression(context);
+ }
+ Name::MdxJsxTagSelfClosingMarker => on_exit_mdx_jsx_tag_self_closing_marker(context),
+
+ Name::ReferenceString => on_exit_reference_string(context),
+ Name::ResourceDestinationString => on_exit_resource_destination_string(context),
+ Name::ResourceTitleString => on_exit_resource_title_string(context),
+ _ => {}
+ }
+
+ Ok(())
+}
+
+/// Handle [`Enter`][Kind::Enter]:`*`.
+fn on_enter_buffer(context: &mut CompileContext) {
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Data`][Name::Data] (and many text things).
+fn on_enter_data(context: &mut CompileContext) {
+ let parent = context.tail_mut();
+ let children = parent.children_mut().expect("expected parent");
+
+ // Add to stack again.
+ if let Some(Node::Text(_)) = children.last_mut() {
+ context.tail_push_again();
+ } else {
+ context.tail_push(Node::Text(Text {
+ value: String::new(),
+ position: None,
+ }));
+ }
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Autolink`][Name::Autolink].
+fn on_enter_autolink(context: &mut CompileContext) {
+ context.tail_push(Node::Link(Link {
+ url: String::new(),
+ title: None,
children: vec![],
- position: Some(mdast::Position {
- start: if events.is_empty() {
- create_point(1, 1, 0)
- } else {
- point_from_event(&events[0])
- },
- end: if events.is_empty() {
- create_point(1, 1, 0)
- } else {
- point_from_event(&events[events.len() - 1])
- },
- }),
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`BlockQuote`][Name::BlockQuote].
+fn on_enter_block_quote(context: &mut CompileContext) {
+ context.tail_push(Node::BlockQuote(BlockQuote {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`CodeFenced`][Name::CodeFenced].
+fn on_enter_code_fenced(context: &mut CompileContext) {
+ context.tail_push(Node::Code(Code {
+ lang: None,
+ meta: None,
+ value: String::new(),
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`CodeIndented`][Name::CodeIndented].
+fn on_enter_code_indented(context: &mut CompileContext) {
+ on_enter_code_fenced(context);
+ on_enter_buffer(context);
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`CodeText`][Name::CodeText].
+fn on_enter_code_text(context: &mut CompileContext) {
+ context.tail_push(Node::InlineCode(InlineCode {
+ value: String::new(),
+ position: None,
+ }));
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MathText`][Name::MathText].
+fn on_enter_math_text(context: &mut CompileContext) {
+ context.tail_push(Node::InlineMath(InlineMath {
+ value: String::new(),
+ position: None,
+ }));
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MdxEsm`][Name::MdxEsm].
+fn on_enter_mdx_esm(context: &mut CompileContext) {
+ context.tail_push(Node::MdxjsEsm(MdxjsEsm {
+ value: String::new(),
+ position: None,
+ }));
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MdxFlowExpression`][Name::MdxFlowExpression].
+fn on_enter_mdx_flow_expression(context: &mut CompileContext) {
+ context.tail_push(Node::MdxFlowExpression(MdxFlowExpression {
+ value: String::new(),
+ position: None,
+ }));
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MdxTextExpression`][Name::MdxTextExpression].
+fn on_enter_mdx_text_expression(context: &mut CompileContext) {
+ context.tail_push(Node::MdxTextExpression(MdxTextExpression {
+ value: String::new(),
+ position: None,
+ }));
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Definition`][Name::Definition].
+fn on_enter_definition(context: &mut CompileContext) {
+ context.tail_push(Node::Definition(Definition {
+ url: String::new(),
+ identifier: String::new(),
+ label: None,
+ title: None,
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Emphasis`][Name::Emphasis].
+fn on_enter_emphasis(context: &mut CompileContext) {
+ context.tail_push(Node::Emphasis(Emphasis {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:{[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail],[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto],[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol],[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww],[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp]}.
+fn on_enter_gfm_autolink_literal(context: &mut CompileContext) {
+ on_enter_autolink(context);
+ on_enter_data(context);
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteCall`][Name::GfmFootnoteCall].
+fn on_enter_gfm_footnote_call(context: &mut CompileContext) {
+ context.tail_push(Node::FootnoteReference(FootnoteReference {
+ identifier: String::new(),
+ label: None,
+ position: None,
+ }));
+ context.media_reference_stack.push(Reference::new());
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`GfmFootnoteDefinition`][Name::GfmFootnoteDefinition].
+fn on_enter_gfm_footnote_definition(context: &mut CompileContext) {
+ context.tail_push(Node::FootnoteDefinition(FootnoteDefinition {
+ identifier: String::new(),
+ label: None,
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`GfmStrikethrough`][Name::GfmStrikethrough].
+fn on_enter_gfm_strikethrough(context: &mut CompileContext) {
+ context.tail_push(Node::Delete(Delete {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`GfmTable`][Name::GfmTable].
+fn on_enter_gfm_table(context: &mut CompileContext) {
+ let align = gfm_table_align(context.events, context.index);
+ context.tail_push(Node::Table(Table {
+ align,
+ children: vec![],
+ position: None,
+ }));
+ context.gfm_table_inside = true;
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`GfmTableRow`][Name::GfmTableRow].
+fn on_enter_gfm_table_row(context: &mut CompileContext) {
+ context.tail_push(Node::TableRow(TableRow {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`GfmTableCell`][Name::GfmTableCell].
+fn on_enter_gfm_table_cell(context: &mut CompileContext) {
+ context.tail_push(Node::TableCell(TableCell {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`HardBreakEscape`][Name::HardBreakEscape].
+fn on_enter_hard_break(context: &mut CompileContext) {
+ context.tail_push(Node::Break(Break { position: None }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Frontmatter`][Name::Frontmatter].
+fn on_enter_frontmatter(context: &mut CompileContext) {
+ let index = context.events[context.index].point.index;
+ let byte = context.bytes[index];
+ let node = if byte == b'+' {
+ Node::Toml(Toml {
+ value: String::new(),
+ position: None,
+ })
+ } else {
+ Node::Yaml(Yaml {
+ value: String::new(),
+ position: None,
+ })
+ };
+
+ context.tail_push(node);
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Reference`][Name::Reference].
+fn on_enter_reference(context: &mut CompileContext) {
+ let reference = context
+ .media_reference_stack
+ .last_mut()
+ .expect("expected reference on media stack");
+ // Assume collapsed.
+ // If there’s a string after it, we set `Full`.
+ reference.reference_kind = Some(ReferenceKind::Collapsed);
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Resource`][Name::Resource].
+fn on_enter_resource(context: &mut CompileContext) {
+ let reference = context
+ .media_reference_stack
+ .last_mut()
+ .expect("expected reference on media stack");
+ // It’s not a reference.
+ reference.reference_kind = None;
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Strong`][Name::Strong].
+fn on_enter_strong(context: &mut CompileContext) {
+ context.tail_push(Node::Strong(Strong {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`ThematicBreak`][Name::ThematicBreak].
+fn on_enter_thematic_break(context: &mut CompileContext) {
+ context.tail_push(Node::ThematicBreak(ThematicBreak { position: None }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`HeadingAtx`][Name::HeadingAtx].
+fn on_enter_heading(context: &mut CompileContext) {
+ context.tail_push(Node::Heading(Heading {
+ depth: 0, // Will be set later.
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:{[`HtmlFlow`][Name::HtmlFlow],[`HtmlText`][Name::HtmlText]}.
+fn on_enter_html(context: &mut CompileContext) {
+ context.tail_push(Node::Html(Html {
+ value: String::new(),
+ position: None,
+ }));
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Image`][Name::Image].
+fn on_enter_image(context: &mut CompileContext) {
+ context.tail_push(Node::Image(Image {
+ url: String::new(),
+ title: None,
+ alt: String::new(),
+ position: None,
+ }));
+ context.media_reference_stack.push(Reference::new());
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`Link`][Name::Link].
+fn on_enter_link(context: &mut CompileContext) {
+ context.tail_push(Node::Link(Link {
+ url: String::new(),
+ title: None,
+ children: vec![],
+ position: None,
+ }));
+ context.media_reference_stack.push(Reference::new());
+}
+
+/// Handle [`Enter`][Kind::Enter]:{[`ListOrdered`][Name::ListOrdered],[`ListUnordered`][Name::ListUnordered]}.
+fn on_enter_list(context: &mut CompileContext) {
+ let ordered = context.events[context.index].name == Name::ListOrdered;
+ let spread = list_loose(context.events, context.index, false);
+
+ context.tail_push(Node::List(List {
+ ordered,
+ spread,
+ start: None,
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`ListItem`][Name::ListItem].
+fn on_enter_list_item(context: &mut CompileContext) {
+ let spread = list_item_loose(context.events, context.index);
+
+ context.tail_push(Node::ListItem(ListItem {
+ spread,
+ checked: None,
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MathFlow`][Name::MathFlow].
+fn on_enter_math_flow(context: &mut CompileContext) {
+ context.tail_push(Node::Math(Math {
+ meta: None,
+ value: String::new(),
+ position: None,
+ }));
+}
+
+/// Handle [`Enter`][Kind::Enter]:{[`MdxJsxFlowTag`][Name::MdxJsxFlowTag],[`MdxJsxTextTag`][Name::MdxJsxTextTag]}.
+fn on_enter_mdx_jsx_tag(context: &mut CompileContext) {
+ let point = point_from_event(&context.events[context.index]);
+ context.jsx_tag = Some(JsxTag {
+ name: None,
+ attributes: vec![],
+ start: point.clone(),
+ end: point,
+ close: false,
+ self_closing: false,
+ });
+ context.buffer();
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagClosingMarker`][Name::MdxJsxTagClosingMarker].
+fn on_enter_mdx_jsx_tag_closing_marker(context: &mut CompileContext) -> Result<(), String> {
+ if context.jsx_tag_stack.is_empty() {
+ let event = &context.events[context.index];
+ Err(format!(
+ "{}:{}: Unexpected closing slash `/` in tag, expected an open tag first (mdx-jsx:unexpected-closing-slash)",
+ event.point.line,
+ event.point.column,
+ ))
+ } else {
+ Ok(())
+ }
+}
+
+/// Handle [`Enter`][Kind::Enter]:{[`MdxJsxTagAttribute`][Name::MdxJsxTagAttribute],[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression]}.
+fn on_enter_mdx_jsx_tag_any_attribute(context: &mut CompileContext) -> Result<(), String> {
+ if context.jsx_tag.as_ref().expect("expected tag").close {
+ let event = &context.events[context.index];
+ Err(format!(
+ "{}:{}: Unexpected attribute in closing tag, expected the end of the tag (mdx-jsx:unexpected-attribute)",
+ event.point.line,
+ event.point.column,
+ ))
+ } else {
+ Ok(())
}
}
-fn point_from_event(event: &Event) -> mdast::Point {
- create_point(event.point.line, event.point.column, event.point.index)
+/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagAttribute`][Name::MdxJsxTagAttribute].
+fn on_enter_mdx_jsx_tag_attribute(context: &mut CompileContext) -> Result<(), String> {
+ on_enter_mdx_jsx_tag_any_attribute(context)?;
+
+ context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .push(AttributeContent::Property(MdxJsxAttribute {
+ name: String::new(),
+ value: None,
+ }));
+
+ Ok(())
}
-fn create_point(line: usize, column: usize, offset: usize) -> mdast::Point {
- mdast::Point {
- line,
- column,
- offset,
+/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression].
+fn on_enter_mdx_jsx_tag_attribute_expression(context: &mut CompileContext) -> Result<(), String> {
+ on_enter_mdx_jsx_tag_any_attribute(context)?;
+
+ context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .push(AttributeContent::Expression(String::new()));
+
+ context.buffer();
+
+ Ok(())
+}
+
+/// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagSelfClosingMarker`][Name::MdxJsxTagSelfClosingMarker].
+fn on_enter_mdx_jsx_tag_self_closing_marker(context: &mut CompileContext) -> Result<(), String> {
+ let tag = context.jsx_tag.as_ref().expect("expected tag");
+ if tag.close {
+ let event = &context.events[context.index];
+ Err(format!(
+ "{}:{}: Unexpected self-closing slash `/` in closing tag, expected the end of the tag (mdx-jsx:unexpected-self-closing-slash)",
+ event.point.line,
+ event.point.column,
+ ))
+ } else {
+ Ok(())
}
}
+
+/// Handle [`Enter`][Kind::Enter]:[`Paragraph`][Name::Paragraph].
+fn on_enter_paragraph(context: &mut CompileContext) {
+ context.tail_push(Node::Paragraph(Paragraph {
+ children: vec![],
+ position: None,
+ }));
+}
+
+/// Handle [`Exit`][Kind::Exit]:`*`.
+fn on_exit(context: &mut CompileContext) -> Result<(), String> {
+ context.tail_pop()?;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`AutolinkProtocol`][Name::AutolinkProtocol].
+fn on_exit_autolink_protocol(context: &mut CompileContext) -> Result<(), String> {
+ on_exit_data(context)?;
+ let value = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ if let Node::Link(link) = context.tail_mut() {
+ link.url.push_str(value.as_str());
+ } else {
+ unreachable!("expected link on stack");
+ }
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`AutolinkEmail`][Name::AutolinkEmail].
+fn on_exit_autolink_email(context: &mut CompileContext) -> Result<(), String> {
+ on_exit_data(context)?;
+ let value = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ if let Node::Link(link) = context.tail_mut() {
+ link.url.push_str("mailto:");
+ link.url.push_str(value.as_str());
+ } else {
+ unreachable!("expected link on stack");
+ }
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarker`][Name::CharacterReferenceMarker].
+fn on_exit_character_reference_marker(context: &mut CompileContext) {
+ context.character_reference_marker = b'&';
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerHexadecimal`][Name::CharacterReferenceMarkerHexadecimal].
+fn on_exit_character_reference_marker_hexadecimal(context: &mut CompileContext) {
+ context.character_reference_marker = b'x';
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceMarkerNumeric`][Name::CharacterReferenceMarkerNumeric].
+fn on_exit_character_reference_marker_numeric(context: &mut CompileContext) {
+ context.character_reference_marker = b'#';
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`CharacterReferenceValue`][Name::CharacterReferenceValue].
+fn on_exit_character_reference_value(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let value = slice.as_str();
+
+ let value = match context.character_reference_marker {
+ b'#' => decode_numeric(value, 10),
+ b'x' => decode_numeric(value, 16),
+ b'&' => decode_named(value),
+ _ => panic!("impossible"),
+ };
+
+ if let Node::Text(node) = context.tail_mut() {
+ node.value.push_str(value.as_str());
+ } else {
+ unreachable!("expected text on stack");
+ }
+
+ context.character_reference_marker = 0;
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`CodeFencedFenceInfo`][Name::CodeFencedFenceInfo].
+fn on_exit_code_fenced_fence_info(context: &mut CompileContext) {
+ let value = context.resume().to_string();
+ if let Node::Code(node) = context.tail_mut() {
+ node.lang = Some(value);
+ } else {
+ unreachable!("expected code on stack");
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFenceMeta`][Name::CodeFencedFenceMeta],[`MathFlowFenceMeta`][Name::MathFlowFenceMeta]}.
+fn on_exit_raw_flow_fence_meta(context: &mut CompileContext) {
+ let value = context.resume().to_string();
+ match context.tail_mut() {
+ Node::Code(node) => node.meta = Some(value),
+ Node::Math(node) => node.meta = Some(value),
+ _ => {
+ unreachable!("expected code or math on stack");
+ }
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`CodeFencedFence`][Name::CodeFencedFence],[`MathFlowFence`][Name::MathFlowFence]}.
+fn on_exit_raw_flow_fence(context: &mut CompileContext) {
+ if context.raw_flow_fence_seen {
+ // Second fence, ignore.
+ } else {
+ context.buffer();
+ context.raw_flow_fence_seen = true;
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`CodeFenced`][Name::CodeFenced],[`MathFlow`][Name::MathFlow]}.
+fn on_exit_raw_flow(context: &mut CompileContext) -> Result<(), String> {
+ let value = trim_eol(context.resume().to_string(), true, true);
+
+ match context.tail_mut() {
+ Node::Code(node) => node.value = value,
+ Node::Math(node) => node.value = value,
+ _ => unreachable!("expected code or math on stack for value"),
+ }
+
+ on_exit(context)?;
+ context.raw_flow_fence_seen = false;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`CodeIndented`][Name::CodeIndented].
+fn on_exit_code_indented(context: &mut CompileContext) -> Result<(), String> {
+ let value = context.resume().to_string();
+
+ if let Node::Code(node) = context.tail_mut() {
+ node.value = trim_eol(value, false, true);
+ } else {
+ unreachable!("expected code on stack for value");
+ }
+ on_exit(context)?;
+ context.raw_flow_fence_seen = false;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`CodeText`][Name::CodeText],[`MathText`][Name::MathText]}.
+fn on_exit_raw_text(context: &mut CompileContext) -> Result<(), String> {
+ let mut value = context.resume().to_string();
+
+ // To do: share with `to_html`.
+ // If we are in a GFM table, we need to decode escaped pipes.
+ // This is a rather weird GFM feature.
+ if context.gfm_table_inside {
+ let mut bytes = value.as_bytes().to_vec();
+ let mut index = 0;
+ let mut len = bytes.len();
+ let mut replace = false;
+
+ while index < len {
+ if index + 1 < len && bytes[index] == b'\\' && bytes[index + 1] == b'|' {
+ replace = true;
+ bytes.remove(index);
+ len -= 1;
+ }
+
+ index += 1;
+ }
+
+ if replace {
+ value = str::from_utf8(&bytes).unwrap().to_string();
+ }
+ }
+
+ match context.tail_mut() {
+ Node::InlineCode(node) => node.value = value,
+ Node::InlineMath(node) => node.value = value,
+ _ => unreachable!("expected inline code or math on stack for value"),
+ }
+
+ on_exit(context)?;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`Data`][Name::Data] (and many text things).
+fn on_exit_data(context: &mut CompileContext) -> Result<(), String> {
+ let value = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ if let Node::Text(text) = context.tail_mut() {
+ text.value.push_str(value.as_str());
+ } else {
+ unreachable!("expected text on stack");
+ }
+ on_exit(context)?;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`Data`][Name::Data] itself.
+fn on_exit_data_actual(context: &mut CompileContext) -> Result<(), String> {
+ on_exit_data(context)?;
+
+ // This field is set when a check exits.
+ // When that’s the case, there’s always a `data` event right after it.
+ // That data event is the first child (after the check) of the paragraph.
+ // We update the text positional info (from the already fixed paragraph),
+ // and remove the first byte, which is always a space or tab.
+ if context.gfm_task_list_item_check_after {
+ let parent = context.tail_mut();
+ let start = parent.position().unwrap().start.clone();
+ let node = parent.children_mut().unwrap().last_mut().unwrap();
+ node.position_mut().unwrap().start = start;
+ if let Node::Text(node) = node {
+ node.value.remove(0);
+ }
+ context.gfm_task_list_item_check_after = false;
+ }
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`DefinitionDestinationString`][Name::DefinitionDestinationString].
+fn on_exit_definition_destination_string(context: &mut CompileContext) {
+ let value = context.resume().to_string();
+ if let Node::Definition(node) = context.tail_mut() {
+ node.url = value;
+ } else {
+ unreachable!("expected definition on stack");
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`DefinitionLabelString`][Name::DefinitionLabelString],[`GfmFootnoteDefinitionLabelString`][Name::GfmFootnoteDefinitionLabelString]}.
+fn on_exit_definition_id(context: &mut CompileContext) {
+ let label = context.resume().to_string();
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let identifier = normalize_identifier(slice.as_str()).to_lowercase();
+
+ match context.tail_mut() {
+ Node::Definition(node) => {
+ node.label = Some(label);
+ node.identifier = identifier;
+ }
+ Node::FootnoteDefinition(node) => {
+ node.label = Some(label);
+ node.identifier = identifier;
+ }
+ _ => unreachable!("expected definition or footnote definition on stack"),
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`DefinitionTitleString`][Name::DefinitionTitleString].
+fn on_exit_definition_title_string(context: &mut CompileContext) {
+ let value = context.resume().to_string();
+ if let Node::Definition(node) = context.tail_mut() {
+ node.title = Some(value);
+ } else {
+ unreachable!("expected definition on stack");
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`Frontmatter`][Name::Frontmatter].
+fn on_exit_frontmatter(context: &mut CompileContext) -> Result<(), String> {
+ let value = trim_eol(context.resume().to_string(), true, true);
+
+ match context.tail_mut() {
+ Node::Yaml(node) => node.value = value,
+ Node::Toml(node) => node.value = value,
+ _ => unreachable!("expected yaml/toml on stack for value"),
+ }
+
+ on_exit(context)?;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`GfmAutolinkLiteralEmail`][Name::GfmAutolinkLiteralEmail],[`GfmAutolinkLiteralMailto`][Name::GfmAutolinkLiteralMailto],[`GfmAutolinkLiteralProtocol`][Name::GfmAutolinkLiteralProtocol],[`GfmAutolinkLiteralWww`][Name::GfmAutolinkLiteralWww],[`GfmAutolinkLiteralXmpp`][Name::GfmAutolinkLiteralXmpp]}.
+fn on_exit_gfm_autolink_literal(context: &mut CompileContext) -> Result<(), String> {
+ on_exit_data(context)?;
+
+ let value = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+
+ let prefix = match &context.events[context.index].name {
+ Name::GfmAutolinkLiteralEmail => Some("mailto:"),
+ Name::GfmAutolinkLiteralWww => Some("http://"),
+ // `GfmAutolinkLiteralMailto`, `GfmAutolinkLiteralProtocol`, `GfmAutolinkLiteralXmpp`.
+ _ => None,
+ };
+
+ if let Node::Link(link) = context.tail_mut() {
+ if let Some(prefix) = prefix {
+ link.url.push_str(prefix);
+ }
+ link.url.push_str(value.as_str());
+ } else {
+ unreachable!("expected link on stack");
+ }
+
+ on_exit(context)?;
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`GfmTable`][Name::GfmTable].
+fn on_exit_gfm_table(context: &mut CompileContext) -> Result<(), String> {
+ on_exit(context)?;
+ context.gfm_table_inside = false;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`GfmTaskListItemCheck`][Name::GfmTaskListItemCheck].
+fn on_exit_gfm_task_list_item_check(context: &mut CompileContext) {
+ // This field is set when a check exits.
+ // When that’s the case, there’s always a `data` event right after it.
+ // That data event is the first child (after the check) of the paragraph.
+ // We update the paragraph positional info to start after the check.
+ let mut start = point_from_event(&context.events[context.index]);
+ debug_assert!(
+ matches!(context.bytes[start.offset], b'\t' | b' '),
+ "expected tab or space after check"
+ );
+ start.column += 1;
+ start.offset += 1;
+ context.tail_mut().position_mut().unwrap().start = start;
+ context.gfm_task_list_item_check_after = true;
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`GfmTaskListItemValueChecked`][Name::GfmTaskListItemValueChecked],[`GfmTaskListItemValueUnchecked`][Name::GfmTaskListItemValueUnchecked]}.
+fn on_exit_gfm_task_list_item_value(context: &mut CompileContext) {
+ let checked = context.events[context.index].name == Name::GfmTaskListItemValueChecked;
+ let ancestor = context.tail_penultimate_mut();
+
+ if let Node::ListItem(node) = ancestor {
+ node.checked = Some(checked);
+ } else {
+ unreachable!("expected list item on stack");
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`HardBreakEscape`][Name::HardBreakEscape],[`HardBreakTrailing`][Name::HardBreakTrailing]}.
+fn on_exit_hard_break(context: &mut CompileContext) -> Result<(), String> {
+ on_exit(context)?;
+ context.hard_break_after = true;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`HeadingAtxSequence`][Name::HeadingAtxSequence].
+fn on_exit_heading_atx_sequence(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+
+ if let Node::Heading(node) = context.tail_mut() {
+ if node.depth == 0 {
+ #[allow(clippy::cast_possible_truncation)]
+ let depth = slice.len() as u8;
+ node.depth = depth;
+ }
+ } else {
+ unreachable!("expected heading on stack");
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`HeadingSetext`][Name::HeadingSetext].
+fn on_exit_heading_setext(context: &mut CompileContext) -> Result<(), String> {
+ context.heading_setext_text_after = false;
+ on_exit(context)?;
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextText`][Name::HeadingSetextText].
+fn on_exit_heading_setext_text(context: &mut CompileContext) {
+ context.heading_setext_text_after = true;
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`HeadingSetextUnderlineSequence`][Name::HeadingSetextUnderlineSequence].
+fn on_exit_heading_setext_underline_sequence(context: &mut CompileContext) {
+ let head = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ )
+ .head();
+ let depth = if head == Some(b'-') { 2 } else { 1 };
+
+ if let Node::Heading(node) = context.tail_mut() {
+ node.depth = depth;
+ } else {
+ unreachable!("expected heading on stack");
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`LabelText`][Name::LabelText].
+fn on_exit_label_text(context: &mut CompileContext) {
+ let mut fragment = context.resume();
+ let label = fragment.to_string();
+ let children = fragment.children_mut().unwrap().split_off(0);
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let identifier = normalize_identifier(slice.as_str()).to_lowercase();
+
+ let reference = context
+ .media_reference_stack
+ .last_mut()
+ .expect("expected reference on media stack");
+ reference.label = label.clone();
+ reference.identifier = identifier;
+
+ match context.tail_mut() {
+ Node::Link(node) => node.children = children,
+ Node::Image(node) => node.alt = label,
+ Node::FootnoteReference(_) => {}
+ _ => unreachable!("expected footnote refereence, image, or link on stack"),
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`LineEnding`][Name::LineEnding].
+fn on_exit_line_ending(context: &mut CompileContext) -> Result<(), String> {
+ if context.heading_setext_text_after {
+ // Ignore.
+ }
+ // Line ending position after hard break is part of it.
+ else if context.hard_break_after {
+ let end = point_from_event(&context.events[context.index]);
+ let node = context.tail_mut();
+ let tail = node
+ .children_mut()
+ .expect("expected parent")
+ .last_mut()
+ .expect("expected tail (break)");
+ tail.position_mut().unwrap().end = end;
+ context.hard_break_after = false;
+ }
+ // Line ending is a part of nodes that accept phrasing.
+ else if matches!(
+ context.tail_mut(),
+ Node::Emphasis(_)
+ | Node::Heading(_)
+ | Node::Paragraph(_)
+ | Node::Strong(_)
+ | Node::Delete(_)
+ ) {
+ context.index -= 1;
+ on_enter_data(context);
+ context.index += 1;
+ on_exit_data(context)?;
+ }
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`HtmlFlow`][Name::HtmlFlow],[`MdxFlowExpression`][Name::MdxFlowExpression],etc}.
+fn on_exit_literal(context: &mut CompileContext) -> Result<(), String> {
+ let value = context.resume().to_string();
+
+ match context.tail_mut() {
+ Node::Html(node) => node.value = value,
+ Node::MdxFlowExpression(node) => node.value = value,
+ Node::MdxTextExpression(node) => node.value = value,
+ Node::MdxjsEsm(node) => node.value = value,
+ _ => unreachable!("expected html, mdx expression, etc on stack for value"),
+ }
+
+ on_exit(context)?;
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:{[`GfmFootnoteCall`][Name::GfmFootnoteCall],[`Image`][Name::Image],[`Link`][Name::Link]}.
+fn on_exit_media(context: &mut CompileContext) -> Result<(), String> {
+ let reference = context
+ .media_reference_stack
+ .pop()
+ .expect("expected reference on media stack");
+ on_exit(context)?;
+
+ // It’s a reference.
+ if let Some(kind) = reference.reference_kind {
+ let parent = context.tail_mut();
+ let siblings = parent.children_mut().unwrap();
+
+ match siblings.last_mut().unwrap() {
+ Node::FootnoteReference(node) => {
+ node.identifier = reference.identifier;
+ node.label = Some(reference.label);
+ }
+ Node::Image(_) => {
+ // Need to swap it with a reference version of the node.
+ if let Some(Node::Image(node)) = siblings.pop() {
+ siblings.push(Node::ImageReference(ImageReference {
+ reference_kind: kind,
+ identifier: reference.identifier,
+ label: Some(reference.label),
+ alt: node.alt,
+ position: node.position,
+ }));
+ } else {
+ unreachable!("impossible: it’s an image")
+ }
+ }
+ Node::Link(_) => {
+ // Need to swap it with a reference version of the node.
+ if let Some(Node::Link(node)) = siblings.pop() {
+ siblings.push(Node::LinkReference(LinkReference {
+ reference_kind: kind,
+ identifier: reference.identifier,
+ label: Some(reference.label),
+ children: node.children,
+ position: node.position,
+ }));
+ } else {
+ unreachable!("impossible: it’s a link")
+ }
+ }
+ _ => unreachable!("expected footnote reference, image, or link on stack"),
+ }
+ }
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`ListItemValue`][Name::ListItemValue].
+fn on_exit_list_item_value(context: &mut CompileContext) {
+ let start = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ )
+ .as_str()
+ .parse()
+ .expect("expected list value up to u8");
+
+ if let Node::List(node) = context.tail_penultimate_mut() {
+ debug_assert!(node.ordered, "expected list to be ordered");
+ node.start = Some(start);
+ } else {
+ unreachable!("expected list on stack");
+ }
+}
+
+/// Handle [`Enter`][Kind::Enter]:{[`MdxJsxFlowTag`][Name::MdxJsxFlowTag],[`MdxJsxTextTag`][Name::MdxJsxTextTag]}.
+fn on_exit_mdx_jsx_tag(context: &mut CompileContext) -> Result<(), String> {
+ let mut tag = context.jsx_tag.as_ref().expect("expected tag").clone();
+
+ // End of a tag, so drop the buffer.
+ context.resume();
+ // Set end point.
+ tag.end = point_from_event(&context.events[context.index]);
+
+ let stack = &context.jsx_tag_stack;
+ let tail = stack.last();
+
+ if tag.close {
+ // Unwrap: we crashed earlier if there’s nothing on the stack.
+ let tail = tail.unwrap();
+
+ if tail.name != tag.name {
+ return Err(format!(
+ "{}:{}: Unexpected closing tag `{}`, expected corresponding closing tag for `{}` ({}:{}) (mdx-jsx:end-tag-mismatch)",
+ tag.start.line,
+ tag.start.column,
+ serialize_abbreviated_tag(&tag),
+ serialize_abbreviated_tag(tail),
+ tail.start.line,
+ tail.start.column,
+ ));
+ }
+
+ // Remove from our custom stack.
+ // Note that this does not exit the node.
+ context.jsx_tag_stack.pop();
+ } else {
+ let node = if context.events[context.index].name == Name::MdxJsxFlowTag {
+ Node::MdxJsxFlowElement(MdxJsxFlowElement {
+ name: tag.name.clone(),
+ attributes: tag.attributes.clone(),
+ children: vec![],
+ position: Some(Position {
+ start: tag.start.clone(),
+ end: tag.end.clone(),
+ }),
+ })
+ } else {
+ Node::MdxJsxTextElement(MdxJsxTextElement {
+ name: tag.name.clone(),
+ attributes: tag.attributes.clone(),
+ children: vec![],
+ position: Some(Position {
+ start: tag.start.clone(),
+ end: tag.end.clone(),
+ }),
+ })
+ };
+
+ context.tail_push(node);
+
+ // this.enter(
+ // node,
+ // token,
+ // onErrorRightIsTag
+ // )
+ }
+
+ if tag.self_closing || tag.close {
+ context.tail_pop()?;
+ // this.exit(token, onErrorLeftIsTag)
+ } else {
+ context.jsx_tag_stack.push(tag);
+ }
+
+ Ok(())
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagClosingMarker`][Name::MdxJsxTagClosingMarker].
+fn on_exit_mdx_jsx_tag_closing_marker(context: &mut CompileContext) {
+ context.jsx_tag.as_mut().expect("expected tag").close = true;
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagNamePrimary`][Name::MdxJsxTagNamePrimary].
+fn on_exit_mdx_jsx_tag_name_primary(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let value = slice.serialize();
+ context.jsx_tag.as_mut().expect("expected tag").name = Some(value);
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagNameMember`][Name::MdxJsxTagNameMember].
+fn on_exit_mdx_jsx_tag_name_member(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let name = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .name
+ .as_mut()
+ .expect("expected primary before member");
+ name.push('.');
+ name.push_str(slice.as_str());
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagNameLocal`][Name::MdxJsxTagNameLocal].
+fn on_exit_mdx_jsx_tag_name_local(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let name = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .name
+ .as_mut()
+ .expect("expected primary before local");
+ name.push(':');
+ name.push_str(slice.as_str());
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeExpression`][Name::MdxJsxTagAttributeExpression].
+fn on_exit_mdx_jsx_tag_attribute_expression(context: &mut CompileContext) {
+ let value = context.resume();
+
+ if let Some(AttributeContent::Expression(expression)) = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .last_mut()
+ {
+ expression.push_str(value.to_string().as_str());
+ } else {
+ unreachable!("expected expression")
+ }
+}
+
+// Name:: => (context),
+// Name:: => (context),
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributePrimaryName`][Name::MdxJsxTagAttributePrimaryName].
+fn on_exit_mdx_jsx_tag_attribute_primary_name(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let value = slice.serialize();
+
+ if let Some(AttributeContent::Property(attribute)) = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .last_mut()
+ {
+ attribute.name = value;
+ } else {
+ unreachable!("expected property")
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeNameLocal`][Name::MdxJsxTagAttributeNameLocal].
+fn on_exit_mdx_jsx_tag_attribute_name_local(context: &mut CompileContext) {
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ if let Some(AttributeContent::Property(attribute)) = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .last_mut()
+ {
+ attribute.name.push(':');
+ attribute.name.push_str(slice.as_str());
+ } else {
+ unreachable!("expected property")
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeValueLiteral`][Name::MdxJsxTagAttributeValueLiteral].
+fn on_exit_mdx_jsx_tag_attribute_value_literal(context: &mut CompileContext) {
+ let value = context.resume();
+
+ if let Some(AttributeContent::Property(node)) = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .last_mut()
+ {
+ // To do: character references.
+ node.value = Some(AttributeValue::Literal(value.to_string()));
+ } else {
+ unreachable!("expected property")
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagAttributeValueExpression`][Name::MdxJsxTagAttributeValueExpression].
+fn on_exit_mdx_jsx_tag_attribute_value_expression(context: &mut CompileContext) {
+ let value = context.resume();
+
+ if let Some(AttributeContent::Property(node)) = context
+ .jsx_tag
+ .as_mut()
+ .expect("expected tag")
+ .attributes
+ .last_mut()
+ {
+ node.value = Some(AttributeValue::Expression(value.to_string()));
+ } else {
+ unreachable!("expected property")
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`MdxJsxTagSelfClosingMarker`][Name::MdxJsxTagSelfClosingMarker].
+fn on_exit_mdx_jsx_tag_self_closing_marker(context: &mut CompileContext) {
+ context.jsx_tag.as_mut().expect("expected tag").self_closing = true;
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`ReferenceString`][Name::ReferenceString].
+fn on_exit_reference_string(context: &mut CompileContext) {
+ let label = context.resume().to_string();
+ let slice = Slice::from_position(
+ context.bytes,
+ &SlicePosition::from_exit_event(context.events, context.index),
+ );
+ let identifier = normalize_identifier(slice.as_str()).to_lowercase();
+ let reference = context
+ .media_reference_stack
+ .last_mut()
+ .expect("expected reference on media stack");
+ reference.reference_kind = Some(ReferenceKind::Full);
+ reference.label = label;
+ reference.identifier = identifier;
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`ResourceDestinationString`][Name::ResourceDestinationString].
+fn on_exit_resource_destination_string(context: &mut CompileContext) {
+ let value = context.resume().to_string();
+
+ match context.tail_mut() {
+ Node::Link(node) => node.url = value,
+ Node::Image(node) => node.url = value,
+ _ => unreachable!("expected link, image on stack"),
+ }
+}
+
+/// Handle [`Exit`][Kind::Exit]:[`ResourceTitleString`][Name::ResourceTitleString].
+fn on_exit_resource_title_string(context: &mut CompileContext) {
+ let value = Some(context.resume().to_string());
+
+ match context.tail_mut() {
+ Node::Link(node) => node.title = value,
+ Node::Image(node) => node.title = value,
+ _ => unreachable!("expected link, image on stack"),
+ }
+}
+
+// Create a point from an event.
+fn point_from_event(event: &Event) -> Point {
+ Point::new(event.point.line, event.point.column, event.point.index)
+}
+
+// Create a position from an event.
+fn position_from_event(event: &Event) -> Position {
+ let end = Point::new(event.point.line, event.point.column, event.point.index);
+ Position {
+ start: end.clone(),
+ end,
+ }
+}
+
+fn delve_mut<'tree>(mut node: &'tree mut Node, stack: &'tree [usize]) -> &'tree mut Node {
+ let mut stack_index = 0;
+ while stack_index < stack.len() {
+ let index = stack[stack_index];
+ node = &mut node.children_mut().expect("Cannot delve into non-parent")[index];
+ stack_index += 1;
+ }
+ node
+}
+
+fn trim_eol(value: String, at_start: bool, at_end: bool) -> String {
+ let bytes = value.as_bytes();
+ let mut start = 0;
+ let mut end = bytes.len();
+
+ if at_start && !bytes.is_empty() {
+ if bytes[0] == b'\n' {
+ start += 1;
+ } else if bytes[0] == b'\r' {
+ start += 1;
+ if bytes.len() > 1 && bytes[1] == b'\n' {
+ start += 1;
+ }
+ }
+ }
+
+ if at_end && end > start {
+ if bytes[end - 1] == b'\n' {
+ end -= 1;
+ } else if bytes[end - 1] == b'\r' {
+ end -= 1;
+ if end > start && bytes[end - 1] == b'\n' {
+ end -= 1;
+ }
+ }
+ }
+
+ if start > 0 || end < bytes.len() {
+ str::from_utf8(&bytes[start..end]).unwrap().to_string()
+ } else {
+ value
+ }
+}
+
+fn on_mismatch_error(
+ context: &mut CompileContext,
+ left: Option<&Event>,
+ right: &Event,
+) -> Result<(), String> {
+ if right.name == Name::MdxJsxFlowTag || right.name == Name::MdxJsxTextTag {
+ let point = if let Some(left) = left {
+ &left.point
+ } else {
+ &context.events[context.events.len() - 1].point
+ };
+ let tag = context.jsx_tag.as_ref().unwrap();
+
+ return Err(format!(
+ "{}:{}: Expected a closing tag for `{}` ({}:{}){} (mdx-jsx:end-tag-mismatch)",
+ point.line,
+ point.column,
+ serialize_abbreviated_tag(tag),
+ tag.start.line,
+ tag.start.column,
+ if let Some(left) = left {
+ format!(" before the end of `{:?}`", left.name)
+ } else {
+ "".to_string()
+ }
+ ));
+ }
+
+ if let Some(left) = left {
+ if left.name == Name::MdxJsxFlowTag || left.name == Name::MdxJsxTextTag {
+ let tag = context.jsx_tag.as_ref().unwrap();
+
+ return Err(format!(
+ "{}:{}: Expected the closing tag `{}` either before the start of `{:?}` ({}:{}), or another opening tag after that start (mdx-jsx:end-tag-mismatch)",
+ tag.start.line,
+ tag.start.column,
+ serialize_abbreviated_tag(tag),
+ &right.name,
+ &right.point.line,
+ &right.point.column,
+ ));
+ }
+ unreachable!("mismatched (non-jsx): {:?} / {:?}", left.name, right.name);
+ } else {
+ unreachable!("mismatched (non-jsx): document / {:?}", right.name);
+ }
+}
+
+fn serialize_abbreviated_tag(tag: &JsxTag) -> String {
+ format!(
+ "<{}{}>",
+ if tag.close { "/" } else { "" },
+ if let Some(name) = &tag.name { name } else { "" },
+ )
+}