//! Turn events into a syntax tree. use crate::event::{Event, Kind, Name, Point as EventPoint}; 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, ReferenceKind, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml, Yaml, }; use crate::unist::{Point, Position}; use crate::util::{ character_reference::{ decode as decode_character_reference, parse as parse_character_reference, }, infer::{gfm_table_align, list_item_loose, list_loose}, mdx_collect::collect, normalize_identifier::normalize_identifier, slice::{Position as SlicePosition, Slice}, }; use alloc::{ format, string::{String, ToString}, vec, vec::Vec, }; use core::str; /// A reference to something. #[derive(Debug)] struct Reference { reference_kind: Option, identifier: String, label: String, } /// Info on a tag. /// /// JSX tags are parsed on their own. /// They’re matched together here. #[derive(Debug, Clone)] struct JsxTag { /// Optional tag name. /// /// `None` means that it’s a fragment. name: Option, /// List of attributes. attributes: Vec, /// Whether this is a closing tag. /// /// ```markdown /// > | /// ^ /// ``` close: bool, /// Whether this is a self-closing tag. /// /// ```markdown /// > | /// ^ /// ``` self_closing: bool, /// Starting point. start: Point, /// Ending point. end: Point, } impl Reference { fn new() -> Reference { Reference { // Assume shortcut: removed on a resource, changed on a reference. reference_kind: Some(ReferenceKind::Shortcut), identifier: String::new(), label: String::new(), } } } /// Context used to compile markdown. #[allow(clippy::struct_excessive_bools)] #[derive(Debug)] struct CompileContext<'a> { // Static info. /// List of events. events: &'a [Event], /// List of bytes. bytes: &'a [u8], // Fields used by handlers to track the things they need to track to // compile markdown. character_reference_marker: u8, gfm_table_inside: bool, gfm_task_list_item_check_after: bool, hard_break_after: bool, heading_setext_text_after: bool, jsx_tag_stack: Vec, jsx_tag: Option, media_reference_stack: Vec, raw_flow_fence_seen: bool, // Intermediate results. /// Primary tree and buffers. trees: Vec<(Node, Vec, Vec)>, /// Current event index. index: usize, } impl<'a> CompileContext<'a> { /// Create a new compile context. fn new(events: &'a [Event], bytes: &'a [u8]) -> CompileContext<'a> { let tree = Node::Root(Root { children: vec![], position: Some(Position { start: if events.is_empty() { Point::new(1, 1, 0) } else { point_from_event(&events[0]) }, end: if events.is_empty() { Point::new(1, 1, 0) } else { point_from_event(&events[events.len() - 1]) }, }), }); CompileContext { events, bytes, character_reference_marker: 0, gfm_table_inside: false, gfm_task_list_item_check_after: false, hard_break_after: false, heading_setext_text_after: false, jsx_tag_stack: vec![], jsx_tag: None, media_reference_stack: vec![], raw_flow_fence_seen: false, trees: vec![(tree, vec![], vec![])], index: 0, } } /// Push a buffer. fn buffer(&mut self) { self.trees.push(( Node::Paragraph(Paragraph { children: vec![], position: None, }), vec![], vec![], )); } /// Pop a buffer, returning its value. fn resume(&mut self) -> Node { if let Some((node, stack_a, stack_b)) = self.trees.pop() { debug_assert_eq!( stack_a.len(), 0, "expected stack (nodes in tree) to be drained" ); debug_assert_eq!( stack_b.len(), 0, "expected stack (opening events) to be drained" ); node } else { unreachable!("Cannot resume w/o buffer") } } fn tail_mut(&mut self) -> &mut Node { let (tree, stack, _) = self.trees.last_mut().expect("Cannot get tail w/o tree"); delve_mut(tree, stack) } fn tail_penultimate_mut(&mut self) -> &mut Node { let (tree, stack, _) = self.trees.last_mut().expect("Cannot get tail w/o tree"); delve_mut(tree, &stack[0..(stack.len() - 1)]) } fn tail_push(&mut self, mut child: Node) { if child.position().is_none() { child.position_set(Some(position_from_event(&self.events[self.index]))); } let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree"); let node = delve_mut(tree, stack); let children = node.children_mut().expect("Cannot push to non-parent"); let index = children.len(); children.push(child); stack.push(index); event_stack.push(self.index); } fn tail_push_again(&mut self) { let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree"); let node = delve_mut(tree, stack); let children = node.children().expect("Cannot push to non-parent"); stack.push(children.len() - 1); event_stack.push(self.index); } fn tail_pop(&mut self) -> Result<(), String> { let ev = &self.events[self.index]; let end = point_from_event(ev); let (tree, stack, event_stack) = self.trees.last_mut().expect("Cannot get tail w/o tree"); let node = delve_mut(tree, stack); let pos = node.position_mut().expect("Cannot pop manually added node"); pos.end = end; stack.pop().unwrap(); let left_index = event_stack.pop().unwrap(); let left = &self.events[left_index]; if left.name != ev.name { on_mismatch_error(self, Some(ev), left)?; } Ok(()) } } /// Turn events and bytes into a syntax tree. pub fn compile(events: &[Event], bytes: &[u8]) -> Result { let mut context = CompileContext::new(events, bytes); let mut index = 0; while index < events.len() { handle(&mut context, index)?; index += 1; } debug_assert_eq!(context.trees.len(), 1, "expected 1 final tree"); let (tree, _, event_stack) = context.trees.pop().unwrap(); if let Some(index) = event_stack.last() { let event = &events[*index]; on_mismatch_error(&mut context, None, event)?; } Ok(tree) } /// Handle the event at `index`. fn handle(context: &mut CompileContext, index: usize) -> Result<(), String> { context.index = index; if context.events[index].kind == Kind::Enter { enter(context)?; } else { exit(context)?; } Ok(()) } /// Handle [`Enter`][Kind::Enter]. fn enter(context: &mut CompileContext) -> Result<(), String> { match context.events[context.index].name { Name::AutolinkEmail | Name::AutolinkProtocol | Name::CharacterEscapeValue | Name::CharacterReference | Name::CodeFlowChunk | Name::CodeTextData | Name::Data | Name::FrontmatterChunk | Name::HtmlFlowData | Name::HtmlTextData | Name::MathFlowChunk | Name::MathTextData | Name::MdxJsxTagAttributeValueLiteralValue => on_enter_data(context), Name::CodeFencedFenceInfo | Name::CodeFencedFenceMeta | Name::DefinitionDestinationString | Name::DefinitionLabelString | Name::DefinitionTitleString | Name::GfmFootnoteDefinitionLabelString | Name::LabelText | Name::MathFlowFenceMeta | Name::MdxJsxTagAttributeValueLiteral | 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::MdxJsxTagAttributeValueExpression => { on_enter_mdx_jsx_tag_attribute_value_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::MdxJsxTagAttributeValueLiteralValue => { on_exit_data(context)?; } Name::MdxJsxTagAttributeExpression | Name::MdxJsxTagAttributeValueExpression => { on_exit_drop(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 => on_exit_html(context)?, Name::LabelText => on_exit_label_text(context), Name::LineEnding => on_exit_line_ending(context)?, Name::ListItemValue => on_exit_list_item_value(context), Name::MdxEsm | Name::MdxFlowExpression | Name::MdxTextExpression => { on_exit_mdx_esm_or_expression(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::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::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: 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) { let result = collect( context.events, context.bytes, context.index, &[Name::MdxEsmData, Name::LineEnding], &[Name::MdxEsm], ); context.tail_push(Node::MdxjsEsm(MdxjsEsm { value: result.value, position: None, stops: result.stops, })); context.buffer(); } /// Handle [`Enter`][Kind::Enter]:[`MdxFlowExpression`][Name::MdxFlowExpression]. fn on_enter_mdx_flow_expression(context: &mut CompileContext) { let result = collect( context.events, context.bytes, context.index, &[Name::MdxExpressionData, Name::LineEnding], &[Name::MdxFlowExpression], ); context.tail_push(Node::MdxFlowExpression(MdxFlowExpression { value: result.value, position: None, stops: result.stops, })); context.buffer(); } /// Handle [`Enter`][Kind::Enter]:[`MdxTextExpression`][Name::MdxTextExpression]. fn on_enter_mdx_text_expression(context: &mut CompileContext) { let result = collect( context.events, context.bytes, context.index, &[Name::MdxExpressionData, Name::LineEnding], &[Name::MdxTextExpression], ); context.tail_push(Node::MdxTextExpression(MdxTextExpression { value: result.value, position: None, stops: result.stops, })); 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(()) } } /// 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(()) } /// 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)?; let result = collect( context.events, context.bytes, context.index, &[Name::MdxExpressionData, Name::LineEnding], &[Name::MdxJsxTagAttributeExpression], ); context .jsx_tag .as_mut() .expect("expected tag") .attributes .push(AttributeContent::Expression(result.value, result.stops)); context.buffer(); Ok(()) } /// Handle [`Enter`][Kind::Enter]:[`MdxJsxTagAttributeValueExpression`][Name::MdxJsxTagAttributeValueExpression]. fn on_enter_mdx_jsx_tag_attribute_value_expression(context: &mut CompileContext) { let result = collect( context.events, context.bytes, context.index, &[Name::MdxExpressionData, Name::LineEnding], &[Name::MdxJsxTagAttributeValueExpression], ); if let Some(AttributeContent::Property(node)) = context .jsx_tag .as_mut() .expect("expected tag") .attributes .last_mut() { node.value = Some(AttributeValue::Expression(result.value, result.stops)); } else { unreachable!("expected property") } context.buffer(); } /// 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 = decode_character_reference(slice.as_str(), context.character_reference_marker, true) .expect("expected to parse only valid named references"); 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().into(); } } 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]:*, by dropping the current buffer. fn on_exit_drop(context: &mut CompileContext) { context.resume(); } /// 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 position = SlicePosition::from_exit_event(context.events, context.index); let head = context.bytes[position.start.index]; let depth = if head == 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],[`HtmlText`][Name::HtmlText]}. fn on_exit_html(context: &mut CompileContext) -> Result<(), String> { let value = context.resume().to_string(); match context.tail_mut() { Node::Html(node) => node.value = value, _ => unreachable!("expected html 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 [`Exit`][Kind::Exit]:{[`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); } if tag.self_closing || tag.close { context.tail_pop()?; } 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]:{[`MdxEsm`][Name::MdxEsm],[`MdxFlowExpression`][Name::MdxFlowExpression],[`MdxTextExpression`][Name::MdxTextExpression]}. fn on_exit_mdx_esm_or_expression(context: &mut CompileContext) -> Result<(), String> { on_exit_drop(context); context.tail_pop()?; Ok(()) } /// 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() { node.value = Some(AttributeValue::Literal(parse_character_reference( &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_point(point: &EventPoint) -> Point { Point::new(point.line, point.column, point.index) } /// Create a point from an event. fn point_from_event(event: &Event) -> Point { point_from_event_point(&event.point) } /// 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, } } /// Resolve the current stack on the tree. 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 } /// Remove initial/final EOLs. 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; if end > start && bytes[end - 1] == b'\r' { end -= 1; } } else if bytes[end - 1] == b'\r' { end -= 1; } } if start > 0 || end < bytes.len() { str::from_utf8(&bytes[start..end]).unwrap().into() } else { value } } /// Handle a mismatch. /// /// Mismatches can occur with MDX JSX tags. 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 { "".into() } )); } 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); } } /// Format a JSX tag, ignoring its attributes. fn serialize_abbreviated_tag(tag: &JsxTag) -> String { format!( "<{}{}>", if tag.close { "/" } else { "" }, if let Some(name) = &tag.name { name } else { "" }, ) }