use oxc_allocator::{Allocator, ArenaBox, ArenaVec};
use crate::ast::*;
use crate::inline::RefMap;
use crate::options::Constructs;
use crate::pos::Span;
use super::ir::{Ir, IrItem};
pub fn build<'a>(
allocator: &'a Allocator,
source: &str,
blanks: &[Span],
irs: Vec<Ir>,
span: Span,
constructs: &Constructs,
refs: &RefMap,
) -> Root<'a> {
let cx = Cx { allocator, source, blanks, refs, constructs };
Root { children: cx.blocks(irs), span }
}
struct Cx<'x, 'a> {
allocator: &'a Allocator,
source: &'x str,
blanks: &'x [Span],
refs: &'x RefMap,
constructs: &'x Constructs,
}
impl<'a> Cx<'_, 'a> {
fn blocks(&self, irs: Vec<Ir>) -> ArenaVec<'a, Block<'a>> {
let mut out = ArenaVec::with_capacity_in(irs.len(), &self.allocator);
for ir in irs {
out.push(self.block(ir));
}
out
}
fn lazy_lines(&self, segments: &[super::IrSeg]) -> ArenaVec<'a, Span> {
let mut out = ArenaVec::new_in(&self.allocator);
out.extend(segments.iter().filter(|s| s.lazy).map(|s| s.seg.span));
out
}
fn inlines(&self, segments: &[super::IrSeg]) -> ArenaVec<'a, Inline<'a>> {
crate::inline::inlines(self.allocator, self.source, segments, self.refs, self.constructs)
}
fn block(&self, ir: Ir) -> Block<'a> {
match ir {
Ir::Paragraph { segments, span } => {
let lazy_lines = self.lazy_lines(&segments);
let children = self.inlines(&segments);
Block::Paragraph(ArenaBox::new_in(
Paragraph { children, lazy_lines, span },
&self.allocator,
))
}
Ir::Heading { level, underline, segments, span } => {
let kind = match underline {
Some(underline) => {
HeadingKind::Setext { underline, lazy_lines: self.lazy_lines(&segments) }
}
None => HeadingKind::Atx,
};
let children = self.inlines(&segments);
Block::Heading(ArenaBox::new_in(
Heading { level, kind, children, span },
&self.allocator,
))
}
Ir::ThematicBreak { span } => Block::ThematicBreak(ThematicBreak { span }),
Ir::Code { fenced, lines, span } => {
let kind = match fenced {
Some((fence, info)) => {
let (lang, meta) = info.map_or((None, None), |info| {
let raw = info.slice(self.source);
let (lang, rest) = raw.split_once([' ', '\t']).unwrap_or((raw, ""));
let meta = rest.trim_start_matches([' ', '\t']);
(
Some(Span::at(info.start, 0..lang.len())),
(!meta.is_empty()).then(|| {
Span::at(info.start, raw.len() - meta.len()..raw.len())
}),
)
});
CodeBlockKind::Fenced { fence, lang, meta }
}
None => CodeBlockKind::Indented,
};
let lines = ArenaVec::from_iter_in(lines, &self.allocator);
Block::CodeBlock(ArenaBox::new_in(CodeBlock { kind, lines, span }, &self.allocator))
}
Ir::Html { kind, lines, trailing_newline, span } => {
let lines = ArenaVec::from_iter_in(lines, &self.allocator);
Block::HtmlBlock(ArenaBox::new_in(
HtmlBlock { kind, lines, trailing_newline, span },
&self.allocator,
))
}
Ir::Quote { children, span } => {
let children = self.blocks(children);
Block::Blockquote(ArenaBox::new_in(Blockquote { children, span }, &self.allocator))
}
Ir::Definition { label, destination, title, span } => {
let label = ArenaVec::from_iter_in(label, &self.allocator);
let title = title.map(|t| ArenaVec::from_iter_in(t, &self.allocator));
Block::Definition(ArenaBox::new_in(
Definition { label, destination, title, span },
&self.allocator,
))
}
Ir::FootnoteDefinition { label, children, span } => {
let children = self.blocks(children);
Block::FootnoteDefinition(ArenaBox::new_in(
FootnoteDefinition { label, children, span },
&self.allocator,
))
}
Ir::Table { align, rows, span } => {
let align = ArenaVec::from_iter_in(align, &self.allocator);
let mut children = ArenaVec::with_capacity_in(rows.len(), &self.allocator);
for row in rows {
let mut cells = ArenaVec::with_capacity_in(row.cells.len(), &self.allocator);
for cell in row.cells {
let seg =
super::IrSeg { seg: crate::pos::Segment::new(cell, 0), lazy: false };
cells.push(TableCell { children: self.inlines(&[seg]), span: cell });
}
children.push(TableRow { children: cells, span: row.span });
}
Block::Table(ArenaBox::new_in(Table { align, children, span }, &self.allocator))
}
Ir::MathBlock { meta, lines, span } => {
let lines = ArenaVec::from_iter_in(lines, &self.allocator);
Block::MathBlock(ArenaBox::new_in(MathBlock { meta, lines, span }, &self.allocator))
}
Ir::Liquid { pieces, span } => {
let pieces = ArenaVec::from_iter_in(pieces, &self.allocator);
Block::Liquid(ArenaBox::new_in(Liquid { pieces, span }, &self.allocator))
}
Ir::ContainerDirective { opening, closing, children, span } => {
let children = self.blocks(children);
Block::ContainerDirective(ArenaBox::new_in(
ContainerDirective { opening, closing, children, span },
&self.allocator,
))
}
Ir::List { ordered, marker, items, span } => {
let gap = items.windows(2).any(|pair| {
has_blank_between(self.blanks, pair[0].span.end, pair[1].span.start)
});
let marker = if ordered {
ListMarker::Ordered { delimiter: marker }
} else {
ListMarker::Bullet { marker }
};
let mut children = ArenaVec::with_capacity_in(items.len(), &self.allocator);
for item in items {
children.push(self.item(item));
}
let tight = !gap && !children.iter().any(|item| item.spread);
Block::List(ArenaBox::new_in(
List { marker, tight, children, span },
&self.allocator,
))
}
}
}
fn item(&self, item: IrItem) -> ListItem<'a> {
let IrItem { marker, padding, mut children, span } = item;
let checkbox = if self.constructs.gfm_task_list_item {
self.take_checkbox(&mut children)
} else {
None
};
let spread = item_is_spread(self.blanks, &children);
let children = self.blocks(children);
ListItem { marker, padding, checkbox, spread, children, span }
}
fn take_checkbox(&self, children: &mut [Ir]) -> Option<TaskCheckbox> {
let mut at = 0;
while let Some(Ir::Definition { span, .. }) = children.get(at) {
let next_start = children.get(at + 1)?.span().start;
if has_blank_between(self.blanks, span.end, next_start) {
return None;
}
at += 1;
}
let Some(Ir::Paragraph { segments, span }) = children.get_mut(at) else { return None };
let multiline = segments.len() > 1;
let first = segments.first_mut()?;
let raw = first.seg.span.slice(self.source);
let checked = match raw.as_bytes() {
[b'[', b' ' | b'\t', b']', ..] => false,
[b'[', b'x' | b'X', b']', ..] => true,
_ => return None,
};
let rest = &raw[3..];
if !rest.is_empty() && !rest.starts_with([' ', '\t']) {
return None;
}
let after_ws = rest.trim_start_matches([' ', '\t']);
if !multiline && after_ws.is_empty() {
return None;
}
let checkbox = TaskCheckbox { span: Span::at(first.seg.span.start, 0..3), checked };
let skip = if after_ws.is_empty() { 3 } else { raw.len() - after_ws.len() };
first.seg.span.start += u32::try_from(skip).unwrap_or(0);
span.start = first.seg.span.start;
Some(checkbox)
}
}
fn item_is_spread(blanks: &[Span], children: &[Ir]) -> bool {
children
.windows(2)
.any(|pair| has_blank_between(blanks, pair[0].span().end, pair[1].span().start))
}
fn has_blank_between(blanks: &[Span], a: u32, b: u32) -> bool {
let from = blanks.partition_point(|s| s.start < a);
blanks[from..].iter().take_while(|s| s.start < b).any(|s| s.end <= b)
}