mod projection;
mod types;
pub use types::{
AnnotationKind, BoutenPosition, BoutenStyle, ContainerSubtype, IrBlock, IrDocument, IrInline,
IrListItem, IrTableAlign, IrTableRow, Position, Range, SectionSubtype,
};
use core::mem;
use aozora::pipeline::BorrowedLexOutput;
use aozora::syntax::ContainerKind;
use aozora::syntax::borrowed::{HeadingHint, NodeRef};
use comrak::nodes::{AstNode, ListType, NodeHeading, NodeList, NodeValue};
use crate::sentinel_stream::{
BlockSentinelKind, ParaScan, SentinelCursor, is_sentinel_char, paragraph_sole_block_sentinel,
saturating_u32,
};
use projection::{
container_indent_level, container_subtype, project_block_leaf, project_inline,
sourcepos_to_range, table_align,
};
pub(crate) fn build_ir<'a>(
root: &'a AstNode<'a>,
lex_out: Option<&BorrowedLexOutput<'a>>,
sanitized: &str,
) -> IrDocument {
let mut walker = IrWalker::new(SentinelCursor::from_lex_out_with_source(lex_out, sanitized));
walker.walk_root(root);
IrDocument {
blocks: walker.finish(),
}
}
#[derive(Debug)]
pub struct StreamingIrBuilder<'src> {
cursor: SentinelCursor<'src>,
}
impl<'src> StreamingIrBuilder<'src> {
#[must_use]
pub fn new(lex_out: Option<&BorrowedLexOutput<'src>>, sanitized: &str) -> Self {
Self {
cursor: SentinelCursor::from_lex_out_with_source(lex_out, sanitized),
}
}
pub fn walk_block<'a>(&mut self, node: &'a AstNode<'a>) -> Vec<IrBlock> {
let cursor = mem::replace(&mut self.cursor, SentinelCursor::from_nodes(Vec::new()));
let mut walker = IrWalker::new(cursor);
walker.walk_top(node);
let (blocks, cursor) = walker.finish_keeping_cursor();
self.cursor = cursor;
blocks
}
}
struct IrWalker<'src> {
cursor: SentinelCursor<'src>,
top: Vec<IrBlock>,
open: Vec<OpenContainer>,
depth: usize,
}
struct OpenContainer {
kind: ContainerKind,
source_line: Option<u32>,
children: Vec<IrBlock>,
}
const MAX_AST_DEPTH: usize = 256;
impl<'src> IrWalker<'src> {
fn new(cursor: SentinelCursor<'src>) -> Self {
Self {
cursor,
top: Vec::new(),
open: Vec::new(),
depth: 0,
}
}
fn finish(self) -> Vec<IrBlock> {
self.finish_keeping_cursor().0
}
fn finish_keeping_cursor(mut self) -> (Vec<IrBlock>, SentinelCursor<'src>) {
while let Some(open) = self.open.pop() {
let block = open.into_block();
place_in(&mut self.open, &mut self.top, block);
}
(self.top, self.cursor)
}
fn walk_root<'a>(&mut self, root: &'a AstNode<'a>) {
for child in root.children() {
self.walk_top(child);
}
}
fn walk_top<'a>(&mut self, node: &'a AstNode<'a>) {
let (source_line, is_paragraph) = top_metadata(node);
if is_paragraph && let Some(action) = self.classify_paragraph(node) {
self.dispatch_paragraph(action, source_line);
return;
}
if let Some(block) = self.walk_block(node, true) {
place_in(&mut self.open, &mut self.top, block);
}
}
fn classify_paragraph<'a>(&self, node: &'a AstNode<'a>) -> Option<ParagraphAction<'src>> {
if let Some(kind) = paragraph_sole_block_sentinel(node) {
return Some(ParagraphAction::BlockSentinel(kind));
}
let scan = ParaScan::run(node, &self.cursor);
if let Some(hint) = scan.first_heading_hint {
return Some(ParagraphAction::HeadingHint {
hint,
sentinels_to_consume: scan.total_sentinels,
});
}
None
}
fn dispatch_paragraph(&mut self, action: ParagraphAction<'src>, source_line: u32) {
match action {
ParagraphAction::BlockSentinel(kind) => self.handle_block_sentinel(kind, source_line),
ParagraphAction::HeadingHint {
hint,
sentinels_to_consume,
} => self.handle_heading_hint(hint, sentinels_to_consume, source_line),
}
}
fn handle_block_sentinel(&mut self, kind: BlockSentinelKind, source_line: u32) {
let Some(node_ref) = self.cursor.next() else {
return;
};
match (kind, node_ref) {
(BlockSentinelKind::Leaf, NodeRef::BlockLeaf(leaf)) => {
if let Some(block) = project_block_leaf(leaf, source_line) {
place_in(&mut self.open, &mut self.top, block);
}
}
(BlockSentinelKind::Open, NodeRef::BlockOpen(ck)) => {
self.open.push(OpenContainer {
kind: ck,
source_line: Some(source_line),
children: Vec::new(),
});
}
(BlockSentinelKind::Close, NodeRef::BlockClose(_)) => {
if let Some(open) = self.open.pop() {
let block = open.into_block();
place_in(&mut self.open, &mut self.top, block);
}
}
_ => {}
}
}
fn handle_heading_hint(
&mut self,
hint: &'src HeadingHint<'src>,
sentinels_to_consume: usize,
source_line: u32,
) {
self.cursor.advance(sentinels_to_consume);
let block = IrBlock::Heading {
level: hint.level.clamp(1, 6),
children: vec![IrInline::Text {
value: hint.target.as_str().to_owned(),
range: None,
}],
source_line: Some(source_line),
range: None,
};
place_in(&mut self.open, &mut self.top, block);
}
fn walk_block<'a>(&mut self, node: &'a AstNode<'a>, top_level: bool) -> Option<IrBlock> {
let data = node.data.borrow();
let source_line = top_level.then(|| saturating_u32(data.sourcepos.start.line).max(1));
let range = sourcepos_to_range(&data.sourcepos);
match &data.value {
NodeValue::Paragraph => {
drop(data);
Some(IrBlock::Paragraph {
children: self.collect_inlines(node),
source_line,
range,
})
}
NodeValue::Heading(NodeHeading { level, .. }) => {
let level = (*level).clamp(1, 6);
drop(data);
Some(IrBlock::Heading {
level,
children: self.collect_inlines(node),
source_line,
range,
})
}
NodeValue::BlockQuote => {
drop(data);
Some(IrBlock::Blockquote {
children: self.collect_blocks(node),
source_line,
range,
})
}
NodeValue::List(NodeList {
list_type, start, ..
}) => {
let ordered = matches!(list_type, ListType::Ordered);
let start = (*start > 1).then(|| saturating_u32(*start));
drop(data);
Some(IrBlock::List {
ordered,
start,
items: self.collect_list_items(node),
source_line,
range,
})
}
NodeValue::CodeBlock(code) => {
let lang = (!code.info.is_empty()).then(|| code.info.clone());
let value = code.literal.clone();
drop(data);
Some(IrBlock::CodeBlock {
lang,
value,
source_line,
range,
})
}
NodeValue::ThematicBreak => {
drop(data);
Some(IrBlock::ThematicBreak { source_line, range })
}
NodeValue::Table(table) => {
let aligns: Vec<IrTableAlign> =
table.alignments.iter().copied().map(table_align).collect();
drop(data);
Some(self.walk_table(
node,
TableMeta {
align: aligns,
source_line,
range,
},
))
}
_ => None,
}
}
fn walk_table<'a>(&mut self, node: &'a AstNode<'a>, meta: TableMeta) -> IrBlock {
let mut rows: Vec<IrTableRow> = Vec::new();
for child in node.children() {
rows.push(self.collect_table_row(child));
}
let header = rows.first().cloned().unwrap_or(IrTableRow {
cells: Vec::new(),
range: None,
});
let body = if rows.is_empty() {
Vec::new()
} else {
rows[1..].to_vec()
};
IrBlock::Table {
header,
rows: body,
align: meta.align,
source_line: meta.source_line,
range: meta.range,
}
}
fn collect_blocks<'a>(&mut self, node: &'a AstNode<'a>) -> Vec<IrBlock> {
if self.depth >= MAX_AST_DEPTH {
return Vec::new();
}
self.depth += 1;
let mut out = Vec::new();
for child in node.children() {
if let Some(block) = self.walk_block(child, false) {
out.push(block);
}
}
self.depth -= 1;
out
}
fn collect_list_items<'a>(&mut self, node: &'a AstNode<'a>) -> Vec<IrListItem> {
let mut out = Vec::new();
for child in node.children() {
let data = child.data.borrow();
let is_item = matches!(data.value, NodeValue::Item(_));
let range = sourcepos_to_range(&data.sourcepos);
drop(data);
if !is_item {
continue;
}
out.push(IrListItem {
children: self.collect_blocks(child),
range,
});
}
out
}
fn collect_table_row<'a>(&mut self, row: &'a AstNode<'a>) -> IrTableRow {
let data = row.data.borrow();
let range = sourcepos_to_range(&data.sourcepos);
drop(data);
let mut cells = Vec::new();
for cell in row.children() {
cells.push(self.collect_inlines(cell));
}
IrTableRow { cells, range }
}
fn collect_inlines<'a>(&mut self, node: &'a AstNode<'a>) -> Vec<IrInline> {
if self.depth >= MAX_AST_DEPTH {
return Vec::new();
}
self.depth += 1;
let mut out = Vec::new();
for child in node.children() {
self.emit_inline(child, &mut out);
}
self.depth -= 1;
out
}
fn emit_inline<'a>(&mut self, node: &'a AstNode<'a>, out: &mut Vec<IrInline>) {
let data = node.data.borrow();
let range = sourcepos_to_range(&data.sourcepos);
match &data.value {
NodeValue::Text(s) => {
let s = s.clone();
drop(data);
self.project_text_with_sentinels(&s, range, out);
}
NodeValue::Code(c) => {
let literal = c.literal.clone();
drop(data);
let value = self.rewrite_literal_context(&literal);
out.push(IrInline::Code { value, range });
}
NodeValue::Strong => {
drop(data);
out.push(IrInline::Strong {
children: self.collect_inlines(node),
range,
});
}
NodeValue::Emph => {
drop(data);
out.push(IrInline::Emphasis {
children: self.collect_inlines(node),
range,
});
}
NodeValue::Link(link) => {
let url = link.url.clone();
let title = link.title.clone();
drop(data);
let children = self.collect_inlines(node);
let href = self.rewrite_literal_context(&url);
let title = self.rewrite_literal_context(&title);
out.push(IrInline::Link {
href,
title: (!title.is_empty()).then_some(title),
children,
range,
});
}
NodeValue::Image(image) => {
let url = image.url.clone();
let title = image.title.clone();
drop(data);
let alt = self.collect_inlines(node);
let url = self.rewrite_literal_context(&url);
let title = self.rewrite_literal_context(&title);
out.push(IrInline::Image {
url,
title: (!title.is_empty()).then_some(title),
alt,
range,
});
}
NodeValue::SoftBreak => {
drop(data);
out.push(IrInline::LineBreak { hard: false, range });
}
NodeValue::LineBreak => {
drop(data);
out.push(IrInline::LineBreak { hard: true, range });
}
_ => {}
}
}
fn rewrite_literal_context(&mut self, s: &str) -> String {
if !s.chars().any(is_sentinel_char) {
return s.to_owned();
}
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
if is_sentinel_char(ch) {
if let Some(literal) = self.cursor.next_literal() {
out.push_str(literal);
}
} else {
out.push(ch);
}
}
out
}
fn project_text_with_sentinels(
&mut self,
text: &str,
range: Option<Range>,
out: &mut Vec<IrInline>,
) {
if !text.chars().any(is_sentinel_char) {
if !text.is_empty() {
out.push(IrInline::Text {
value: text.to_owned(),
range,
});
}
return;
}
let mut cursor = 0;
for (idx, ch) in text.char_indices() {
if !is_sentinel_char(ch) {
continue;
}
let head = &text[cursor..idx];
if !head.is_empty() {
out.push(IrInline::Text {
value: head.to_owned(),
range,
});
}
cursor = idx + ch.len_utf8();
let Some(node_ref) = self.cursor.next() else {
continue;
};
if let NodeRef::Inline(aozora) = node_ref
&& let Some(inline) = project_inline(aozora)
{
out.push(inline);
}
}
let tail = &text[cursor..];
if !tail.is_empty() {
out.push(IrInline::Text {
value: tail.to_owned(),
range,
});
}
}
}
fn place_in(open: &mut [OpenContainer], top: &mut Vec<IrBlock>, block: IrBlock) {
if let Some(frame) = open.last_mut() {
frame.children.push(block);
} else {
top.push(block);
}
}
impl OpenContainer {
fn into_block(self) -> IrBlock {
IrBlock::Container {
subtype: container_subtype(self.kind),
children: self.children,
indent_level: container_indent_level(self.kind),
source_line: self.source_line,
range: None,
}
}
}
struct TableMeta {
align: Vec<IrTableAlign>,
source_line: Option<u32>,
range: Option<Range>,
}
#[derive(Debug, Clone, Copy)]
enum ParagraphAction<'src> {
BlockSentinel(BlockSentinelKind),
HeadingHint {
hint: &'src HeadingHint<'src>,
sentinels_to_consume: usize,
},
}
fn top_metadata(node: &AstNode<'_>) -> (u32, bool) {
let data = node.data.borrow();
let line = saturating_u32(data.sourcepos.start.line).max(1);
let is_para = matches!(data.value, NodeValue::Paragraph);
(line, is_para)
}