use core::ops::ControlFlow;
use aozora::pipeline::{
BLOCK_CLOSE_SENTINEL, BLOCK_LEAF_SENTINEL, BLOCK_OPEN_SENTINEL, BorrowedLexOutput,
INLINE_SENTINEL,
};
use aozora::syntax::borrowed::{AozoraNode, HeadingHint, NodeRef};
use comrak::nodes::{AstNode, NodeValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BlockSentinelKind {
Leaf,
Open,
Close,
}
impl BlockSentinelKind {
#[inline]
pub(crate) const fn from_char(ch: char) -> Option<Self> {
match ch {
BLOCK_LEAF_SENTINEL => Some(Self::Leaf),
BLOCK_OPEN_SENTINEL => Some(Self::Open),
BLOCK_CLOSE_SENTINEL => Some(Self::Close),
_ => None,
}
}
}
#[inline]
#[must_use]
pub(crate) fn saturating_u32(n: usize) -> u32 {
u32::try_from(n).unwrap_or(u32::MAX)
}
#[inline]
pub(crate) const fn is_sentinel_char(ch: char) -> bool {
(ch as u32).wrapping_sub(INLINE_SENTINEL as u32) < 4
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum InlineDescend {
StopAtNonText,
DescendThrough,
}
pub(crate) fn visit_text_leaves<'a, F>(
node: &'a AstNode<'a>,
mode: InlineDescend,
mut visit: F,
) -> Result<(), ()>
where
F: FnMut(&str) -> ControlFlow<()>,
{
let mut stack: Vec<&'a AstNode<'a>> = Vec::new();
extend_children_rev(&mut stack, node);
while let Some(child) = stack.pop() {
let data = child.data.borrow();
match &data.value {
NodeValue::Text(s) => {
let flow = visit(s);
drop(data);
if flow == ControlFlow::Break(()) {
return Err(());
}
extend_children_rev(&mut stack, child);
}
_ => match mode {
InlineDescend::StopAtNonText => return Err(()),
InlineDescend::DescendThrough => {
drop(data);
extend_children_rev(&mut stack, child);
}
},
}
}
Ok(())
}
fn extend_children_rev<'a>(stack: &mut Vec<&'a AstNode<'a>>, parent: &'a AstNode<'a>) {
let start = stack.len();
stack.extend(parent.children());
stack[start..].reverse();
}
pub(crate) fn paragraph_sole_block_sentinel<'a>(
node: &'a AstNode<'a>,
) -> Option<BlockSentinelKind> {
let mut found: Option<BlockSentinelKind> = None;
let walk_ok = visit_text_leaves(node, InlineDescend::StopAtNonText, |s| {
for ch in s.chars() {
if matches!(ch, ' ' | '\t' | '\n' | '\r') {
continue;
}
let Some(kind) = BlockSentinelKind::from_char(ch) else {
return ControlFlow::Break(());
};
if found.is_some() {
return ControlFlow::Break(());
}
found = Some(kind);
}
ControlFlow::Continue(())
})
.is_ok();
walk_ok.then_some(()).and(found)
}
pub(crate) fn for_each_text_descendant<'a, F>(node: &'a AstNode<'a>, mut visit: F)
where
F: FnMut(&str),
{
let _result = visit_text_leaves(node, InlineDescend::DescendThrough, |s| {
visit(s);
ControlFlow::Continue(())
});
}
#[derive(Debug)]
pub(crate) struct SentinelCursor<'src> {
nodes: Vec<(NodeRef<'src>, String)>,
idx: usize,
}
impl<'src> SentinelCursor<'src> {
pub(crate) fn from_lex_out_with_source(
lex_out: Option<&BorrowedLexOutput<'src>>,
sanitized: &str,
) -> Self {
let nodes = lex_out.map_or_else(Vec::new, |lo| {
lo.registry
.iter_sorted()
.zip(lo.source_nodes.iter())
.map(|((_pos, node), sn)| {
let span = sn.source_span;
let literal = sanitized
.get(span.start as usize..span.end as usize)
.unwrap_or_default()
.to_owned();
(node, literal)
})
.collect()
});
Self { nodes, idx: 0 }
}
pub(crate) fn from_nodes(nodes: Vec<NodeRef<'src>>) -> Self {
Self {
nodes: nodes
.into_iter()
.map(|node| (node, String::new()))
.collect(),
idx: 0,
}
}
pub(crate) fn peek(&self, offset: usize) -> Option<NodeRef<'src>> {
self.nodes.get(self.idx + offset).map(|(node, _)| *node)
}
pub(crate) fn next(&mut self) -> Option<NodeRef<'src>> {
let n = self.nodes.get(self.idx).map(|(node, _)| *node);
if n.is_some() {
self.idx += 1;
}
n
}
pub(crate) fn next_literal(&mut self) -> Option<&str> {
if self.idx >= self.nodes.len() {
return None;
}
let i = self.idx;
self.idx += 1;
Some(self.nodes[i].1.as_str())
}
pub(crate) fn advance(&mut self, n: usize) {
self.idx = self.idx.saturating_add(n).min(self.nodes.len());
}
}
#[derive(Debug)]
pub(crate) struct ParaScan<'src> {
pub(crate) total_sentinels: usize,
pub(crate) first_heading_hint: Option<&'src HeadingHint<'src>>,
}
impl<'src> ParaScan<'src> {
pub(crate) fn run<'a>(node: &'a AstNode<'a>, cursor: &SentinelCursor<'src>) -> Self {
let mut total_sentinels = 0usize;
let mut first_heading_hint = None;
for_each_text_descendant(node, |text| {
for ch in text.chars() {
if !is_sentinel_char(ch) {
continue;
}
if first_heading_hint.is_none()
&& let Some(NodeRef::Inline(AozoraNode::HeadingHint(h))) =
cursor.peek(total_sentinels)
{
first_heading_hint = Some(h);
}
total_sentinels += 1;
}
});
Self {
total_sentinels,
first_heading_hint,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_sentinel_char_recognises_all_four() {
for ch in [
INLINE_SENTINEL,
BLOCK_LEAF_SENTINEL,
BLOCK_OPEN_SENTINEL,
BLOCK_CLOSE_SENTINEL,
] {
assert!(is_sentinel_char(ch), "{ch:?} should be a sentinel");
}
}
#[test]
fn is_sentinel_char_rejects_neighbours() {
assert!(!is_sentinel_char('\u{E000}'));
assert!(!is_sentinel_char('\u{E005}'));
assert!(!is_sentinel_char('a'));
assert!(!is_sentinel_char('\0'));
}
#[test]
fn block_sentinel_kind_from_char_round_trips() {
assert_eq!(
BlockSentinelKind::from_char(BLOCK_LEAF_SENTINEL),
Some(BlockSentinelKind::Leaf)
);
assert_eq!(
BlockSentinelKind::from_char(BLOCK_OPEN_SENTINEL),
Some(BlockSentinelKind::Open)
);
assert_eq!(
BlockSentinelKind::from_char(BLOCK_CLOSE_SENTINEL),
Some(BlockSentinelKind::Close)
);
assert!(BlockSentinelKind::from_char(INLINE_SENTINEL).is_none());
assert!(BlockSentinelKind::from_char('a').is_none());
}
#[test]
fn sentinel_cursor_peeks_and_consumes_in_order() {
use aozora::syntax::ContainerKind;
use aozora::syntax::borrowed::AozoraNode;
let entries: Vec<NodeRef<'static>> = vec![
NodeRef::Inline(AozoraNode::PageBreak),
NodeRef::BlockOpen(ContainerKind::Keigakomi),
NodeRef::BlockClose(ContainerKind::Keigakomi),
];
let mut cursor = SentinelCursor::from_nodes(entries);
assert!(matches!(
cursor.peek(0),
Some(NodeRef::Inline(AozoraNode::PageBreak))
));
assert!(matches!(
cursor.peek(2),
Some(NodeRef::BlockClose(ContainerKind::Keigakomi))
));
assert!(cursor.peek(3).is_none());
let _ = cursor.next();
assert!(matches!(
cursor.next(),
Some(NodeRef::BlockOpen(ContainerKind::Keigakomi))
));
cursor.advance(99); assert!(cursor.next().is_none());
}
#[test]
fn flatten_matches_normalized_scan() {
use aozora::NormalizedOffset;
use aozora::pipeline::lex_into_arena;
use aozora::syntax::borrowed::Arena;
const REPRESENTATIVE: &str = "見出し\n\n本文に|青空《あおぞら》のルビと\
[#「強調」に傍点]を混ぜた段落。\n\n次の段落も|漢字《かんじ》。";
const PATHOLOGICAL: &str = "|A《a》|B《b》|C《c》[#「D」に傍点]|E《e》";
for src in [REPRESENTATIVE, PATHOLOGICAL] {
let arena = Arena::new();
let lex_out = lex_into_arena(src, &arena);
let via_iter_sorted: Vec<u32> =
lex_out.registry.iter_sorted().map(|(pos, _)| pos).collect();
let mut via_scan: Vec<u32> = Vec::new();
for (idx, ch) in lex_out.normalized.char_indices() {
if !is_sentinel_char(ch) {
continue;
}
let pos = u32::try_from(idx).expect("normalized fits u32");
if lex_out.registry.node_at(NormalizedOffset(pos)).is_some() {
via_scan.push(pos);
}
}
assert_eq!(
via_iter_sorted, via_scan,
"iter_sorted order must match the normalized-scan order for {src:?}"
);
assert_eq!(
via_iter_sorted.len(),
lex_out.registry.len(),
"one node per registry entry for {src:?}"
);
}
}
#[test]
fn source_nodes_parallel_to_registry() {
use aozora::pipeline::lex_into_arena;
use aozora::syntax::borrowed::Arena;
const REPRESENTATIVE: &str = "本文に|青空《あおぞら》のルビと\
[#「強調」に傍点]を混ぜた段落。";
const PATHOLOGICAL: &str = "|A《a》|B《b》|C《c》[#「D」に傍点]|E《e》";
for src in [REPRESENTATIVE, PATHOLOGICAL] {
let arena = Arena::new();
let lex_out = lex_into_arena(src, &arena);
assert_eq!(
lex_out.registry.len(),
lex_out.source_nodes.len(),
"registry and source_nodes must have equal length for {src:?}"
);
for sn in lex_out.source_nodes {
assert!(
!sn.source_span.slice(src).is_empty(),
"source span must slice a non-empty run for {src:?}"
);
}
}
}
}