use crate::block::CodeBlockKind;
use crate::{
BlockEvent, BlockParser, InlineEvent, InlineParser, LinkRefDef, LinkRefStore, Options, Range,
fixup_list_tight,
};
use super::{MdxDiagnostic, Segment, segment_spanned};
pub const MDX_EVENT_STREAM_VERSION: u16 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MdxEvent {
FrontMatter {
range: Range,
content: Range,
},
Esm(Range),
FlowExpression(Range),
FlowJsxOpen(Range),
FlowJsxClose(Range),
FlowJsxSelfClose(Range),
Block(BlockEvent),
Inline(InlineEvent),
}
impl MdxEvent {
#[must_use]
pub fn source_range(&self) -> Option<Range> {
match self {
Self::FrontMatter { range, .. }
| Self::Esm(range)
| Self::FlowExpression(range)
| Self::FlowJsxOpen(range)
| Self::FlowJsxClose(range)
| Self::FlowJsxSelfClose(range) => Some(*range),
Self::Block(event) => block_event_range(event),
Self::Inline(event) => inline_event_range(event),
}
}
}
#[derive(Debug)]
pub struct MdxEventStream {
pub version: u16,
pub events: Vec<MdxEvent>,
pub link_references: Vec<LinkRefDef>,
}
impl MdxEventStream {
#[must_use]
pub fn link_reference(&self, index: u32) -> Option<&LinkRefDef> {
self.link_references.get(index as usize)
}
}
#[must_use]
pub fn parse_events(input: &str) -> MdxEventStream {
assert!(
u32::try_from(input.len()).is_ok(),
"MDX input exceeds the supported u32 source range"
);
let (content_start, front_matter) = front_matter_event(input);
let content = &input[content_start..];
let segments = segment_spanned(content);
build_event_stream(input, content_start, front_matter, segments)
}
pub fn parse_events_strict(input: &str) -> Result<MdxEventStream, Vec<MdxDiagnostic>> {
assert!(
u32::try_from(input.len()).is_ok(),
"MDX input exceeds the supported u32 source range"
);
let (content_start, front_matter) = front_matter_event(input);
match super::segment_strict(&input[content_start..]) {
Ok(segments) => Ok(build_event_stream(
input,
content_start,
front_matter,
segments,
)),
Err(mut diagnostics) => {
for diagnostic in &mut diagnostics {
diagnostic.primary_range = offset_range(diagnostic.primary_range, content_start);
diagnostic.related_range = diagnostic
.related_range
.map(|range| offset_range(range, content_start));
}
Err(diagnostics)
}
}
}
fn build_event_stream(
input: &str,
content_start: usize,
front_matter: Option<MdxEvent>,
segments: Vec<super::SpannedSegment<'_>>,
) -> MdxEventStream {
let mut stream = MdxEventStream {
version: MDX_EVENT_STREAM_VERSION,
events: Vec::with_capacity((input.len() / 12).max(32)),
link_references: Vec::new(),
};
if let Some(event) = front_matter {
stream.events.push(event);
}
let (link_refs, markdown_block_events) = parse_markdown_segments(&segments);
link_refs.append_definitions_to(&mut stream.link_references);
let mut markdown_block_events = markdown_block_events.into_iter();
for spanned in segments {
let segment_start = content_start + spanned.range.start_usize();
match spanned.segment {
Segment::Esm(_) => stream
.events
.push(MdxEvent::Esm(offset_range(spanned.range, content_start))),
Segment::Markdown(markdown) => {
let block_events = markdown_block_events
.next()
.expect("every Markdown segment must have parsed block events");
emit_markdown_events(
markdown,
segment_start,
&link_refs,
block_events,
&mut stream.events,
);
}
Segment::JsxBlockOpen(_) => stream.events.push(MdxEvent::FlowJsxOpen(offset_range(
spanned.range,
content_start,
))),
Segment::JsxBlockClose(_) => stream.events.push(MdxEvent::FlowJsxClose(offset_range(
spanned.range,
content_start,
))),
Segment::JsxBlockSelfClose(_) => stream.events.push(MdxEvent::FlowJsxSelfClose(
offset_range(spanned.range, content_start),
)),
Segment::Expression(_) => stream.events.push(MdxEvent::FlowExpression(offset_range(
spanned.range,
content_start,
))),
}
}
debug_assert!(markdown_block_events.next().is_none());
promote_container_flow_events(input.as_bytes(), &mut stream.events);
stream
}
fn promote_container_flow_events(source: &[u8], events: &mut Vec<MdxEvent>) {
let input = std::mem::take(events);
let mut output = Vec::with_capacity(input.len());
let mut paragraph = Vec::new();
let mut input = input.into_iter();
let mut container_depth = 0usize;
while let Some(event) = input.next() {
match event {
MdxEvent::Block(BlockEvent::BlockQuoteStart { .. })
| MdxEvent::Block(BlockEvent::ListItemStart { .. }) => {
container_depth += 1;
output.push(event);
}
MdxEvent::Block(BlockEvent::BlockQuoteEnd)
| MdxEvent::Block(BlockEvent::ListItemEnd) => {
container_depth = container_depth
.checked_sub(1)
.expect("Markdown container events must be balanced");
output.push(event);
}
MdxEvent::Block(BlockEvent::ParagraphStart) if container_depth > 0 => {
paragraph.clear();
let mut found_end = false;
for paragraph_event in input.by_ref() {
if paragraph_event == MdxEvent::Block(BlockEvent::ParagraphEnd) {
found_end = true;
break;
}
paragraph.push(paragraph_event);
}
if found_end {
if let Some(promoted) = promotable_paragraph(source, ¶graph) {
output.push(promoted);
} else {
output.push(MdxEvent::Block(BlockEvent::ParagraphStart));
output.append(&mut paragraph);
output.push(MdxEvent::Block(BlockEvent::ParagraphEnd));
}
} else {
output.push(MdxEvent::Block(BlockEvent::ParagraphStart));
output.append(&mut paragraph);
}
}
event => output.push(event),
}
}
debug_assert_eq!(container_depth, 0);
*events = output;
}
fn promotable_paragraph(source: &[u8], events: &[MdxEvent]) -> Option<MdxEvent> {
let mut promoted = None;
for event in events {
let candidate = match event {
MdxEvent::Inline(InlineEvent::Text(range))
if range.slice(source).iter().all(u8::is_ascii_whitespace) =>
{
continue;
}
MdxEvent::Inline(InlineEvent::SoftBreak) => continue,
MdxEvent::Inline(InlineEvent::MdxExpression(range)) => MdxEvent::FlowExpression(*range),
MdxEvent::Inline(InlineEvent::MdxJsxOpen(range)) => MdxEvent::FlowJsxOpen(*range),
MdxEvent::Inline(InlineEvent::MdxJsxClose(range)) => MdxEvent::FlowJsxClose(*range),
MdxEvent::Inline(InlineEvent::MdxJsxSelfClose(range)) => {
MdxEvent::FlowJsxSelfClose(*range)
}
_ => return None,
};
if promoted.replace(candidate).is_some() {
return None;
}
}
promoted
}
fn front_matter_event(input: &str) -> (usize, Option<MdxEvent>) {
let Some((content, rest_offset)) = crate::extract_front_matter(input) else {
return (0, None);
};
let input_start = input.as_ptr() as usize;
let content_start = (content.as_ptr() as usize)
.checked_sub(input_start)
.expect("front matter must borrow from its input");
let content_end = content_start + content.len();
(
rest_offset,
Some(MdxEvent::FrontMatter {
range: Range::from_usize(0, rest_offset),
content: Range::from_usize(content_start, content_end),
}),
)
}
fn semantic_options() -> Options {
Options {
allow_html: false,
front_matter: false,
..Options::default()
}
}
fn parse_markdown_segments(
segments: &[super::SpannedSegment<'_>],
) -> (LinkRefStore, Vec<Vec<BlockEvent>>) {
let mut link_refs = LinkRefStore::new();
let mut parsed = Vec::new();
for segment in segments {
let Segment::Markdown(markdown) = segment.segment else {
continue;
};
let mut parser = BlockParser::new_with_options(markdown.as_bytes(), semantic_options());
let mut events = Vec::new();
parser.parse(&mut events);
fixup_list_tight(&mut events);
link_refs.merge_first_wins(parser.take_link_refs());
parsed.push(events);
}
(link_refs, parsed)
}
fn emit_markdown_events(
markdown: &str,
source_offset: usize,
link_refs: &LinkRefStore,
block_events: Vec<BlockEvent>,
events: &mut Vec<MdxEvent>,
) {
let mut inline_parser = InlineParser::new();
let mut inline_events = Vec::new();
let mut inline_group = Vec::new();
for block_event in block_events {
match block_event {
BlockEvent::Text(range) => {
inline_group.push(InlineSourcePart::Text(range));
}
BlockEvent::SoftBreak => {
inline_group.push(InlineSourcePart::SoftBreak);
}
event => {
flush_inline_group(
markdown,
source_offset,
link_refs,
&mut inline_parser,
&mut inline_events,
&mut inline_group,
events,
);
events.push(MdxEvent::Block(offset_block_event(event, source_offset)));
}
}
}
flush_inline_group(
markdown,
source_offset,
link_refs,
&mut inline_parser,
&mut inline_events,
&mut inline_group,
events,
);
}
#[derive(Debug, Clone, Copy)]
enum InlineSourcePart {
Text(Range),
SoftBreak,
}
#[allow(clippy::too_many_arguments)]
fn flush_inline_group(
markdown: &str,
source_offset: usize,
link_refs: &LinkRefStore,
inline_parser: &mut InlineParser,
inline_events: &mut Vec<InlineEvent>,
parts: &mut Vec<InlineSourcePart>,
events: &mut Vec<MdxEvent>,
) {
if parts.is_empty() {
return;
}
if let Some(range) = contiguous_inline_range(markdown.as_bytes(), parts) {
emit_inline_range(
range,
markdown.as_bytes(),
source_offset,
link_refs,
inline_parser,
inline_events,
events,
);
} else {
for part in parts.iter().copied() {
match part {
InlineSourcePart::Text(range) => emit_inline_range(
range,
markdown.as_bytes(),
source_offset,
link_refs,
inline_parser,
inline_events,
events,
),
InlineSourcePart::SoftBreak => {
events.push(MdxEvent::Inline(InlineEvent::SoftBreak));
}
}
}
}
parts.clear();
}
fn contiguous_inline_range(input: &[u8], parts: &[InlineSourcePart]) -> Option<Range> {
let mut parts = parts.iter();
let InlineSourcePart::Text(first) = *parts.next()? else {
return None;
};
let mut previous = first;
let mut expect_text = false;
for part in parts {
match (*part, expect_text) {
(InlineSourcePart::SoftBreak, false) => {
expect_text = true;
}
(InlineSourcePart::Text(range), true) => {
let gap = &input[previous.end_usize()..range.start_usize()];
if gap != b"\n" && gap != b"\r\n" {
return None;
}
previous = range;
expect_text = false;
}
_ => return None,
}
}
if expect_text {
return None;
}
Some(Range::from_usize(first.start_usize(), previous.end_usize()))
}
#[allow(clippy::too_many_arguments)]
fn emit_inline_range(
range: Range,
markdown: &[u8],
source_offset: usize,
link_refs: &LinkRefStore,
inline_parser: &mut InlineParser,
inline_events: &mut Vec<InlineEvent>,
events: &mut Vec<MdxEvent>,
) {
inline_events.clear();
inline_parser.parse_mdx(range.slice(markdown), Some(link_refs), inline_events);
let inline_offset = source_offset + range.start_usize();
for event in inline_events.drain(..) {
events.push(MdxEvent::Inline(offset_inline_event(event, inline_offset)));
}
}
fn offset_range(range: Range, offset: usize) -> Range {
Range::from_usize(offset + range.start_usize(), offset + range.end_usize())
}
fn offset_block_event(mut event: BlockEvent, offset: usize) -> BlockEvent {
match &mut event {
BlockEvent::CodeBlockStart {
kind: CodeBlockKind::Fenced { info: Some(range) },
}
| BlockEvent::HtmlBlockText(range)
| BlockEvent::Comment(range)
| BlockEvent::Code(range)
| BlockEvent::ThematicBreak(range) => *range = offset_range(*range, offset),
_ => {}
}
event
}
fn offset_inline_event(mut event: InlineEvent, offset: usize) -> InlineEvent {
match &mut event {
InlineEvent::Text(range)
| InlineEvent::Code(range)
| InlineEvent::Html(range)
| InlineEvent::MathInline(range)
| InlineEvent::MathDisplay(range)
| InlineEvent::MdxExpression(range)
| InlineEvent::MdxJsxOpen(range)
| InlineEvent::MdxJsxClose(range)
| InlineEvent::MdxJsxSelfClose(range) => *range = offset_range(*range, offset),
InlineEvent::LinkStart { url, title } | InlineEvent::ImageStart { url, title } => {
*url = offset_range(*url, offset);
if let Some(range) = title {
*range = offset_range(*range, offset);
}
}
InlineEvent::Autolink { url, .. } | InlineEvent::AutolinkLiteral { url, .. } => {
*url = offset_range(*url, offset);
}
_ => {}
}
event
}
fn block_event_range(event: &BlockEvent) -> Option<Range> {
match event {
BlockEvent::CodeBlockStart {
kind: CodeBlockKind::Fenced { info: Some(range) },
}
| BlockEvent::HtmlBlockText(range)
| BlockEvent::Comment(range)
| BlockEvent::Text(range)
| BlockEvent::Code(range)
| BlockEvent::ThematicBreak(range) => Some(*range),
_ => None,
}
}
fn inline_event_range(event: &InlineEvent) -> Option<Range> {
match event {
InlineEvent::Text(range)
| InlineEvent::Code(range)
| InlineEvent::Html(range)
| InlineEvent::MathInline(range)
| InlineEvent::MathDisplay(range)
| InlineEvent::MdxExpression(range)
| InlineEvent::MdxJsxOpen(range)
| InlineEvent::MdxJsxClose(range)
| InlineEvent::MdxJsxSelfClose(range) => Some(*range),
InlineEvent::LinkStart { url, .. }
| InlineEvent::ImageStart { url, .. }
| InlineEvent::Autolink { url, .. }
| InlineEvent::AutolinkLiteral { url, .. } => Some(*url),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_event_stays_compact() {
assert!(std::mem::size_of::<MdxEvent>() <= 40);
}
}