use crate::parser::ast::{Node, NodeKind};
use crate::parser::inlines::shared::{opt_span_range, GrammarSpan};
use nom::Input;
use unicode_general_category::{get_general_category, GeneralCategory};
#[derive(Debug, Clone, Copy)]
pub(super) struct Delimiter<'a> {
run: GrammarSpan<'a>,
ch: char,
original_count: usize,
can_open: bool,
can_close: bool,
}
impl<'a> Delimiter<'a> {
fn count(&self) -> usize {
self.run.fragment().len()
}
fn consume_as_opener(&mut self, n: usize) -> GrammarSpan<'a> {
let keep = self.count() - n;
let consumed = self.run.take_from(keep);
self.run = self.run.take(keep);
consumed
}
fn consume_as_closer(&mut self, n: usize) -> GrammarSpan<'a> {
let consumed = self.run.take(n);
self.run = self.run.take_from(n);
consumed
}
fn into_text_node(self) -> Node {
Node {
kind: NodeKind::Text(self.ch.to_string().repeat(self.count())),
span: crate::parser::shared::opt_span(self.run),
children: Vec::new(),
}
}
}
pub(super) enum Item<'a> {
Node(Node),
Delim(Delimiter<'a>),
Consumed,
}
impl<'a> Item<'a> {
pub(super) fn last_char(&self) -> Option<char> {
match self {
Item::Node(n) => last_char_in_node(n),
Item::Delim(d) => Some(d.ch),
Item::Consumed => None,
}
}
}
fn last_char_in_node(node: &Node) -> Option<char> {
match &node.kind {
NodeKind::Text(t) => t.chars().last(),
_ => node.children.iter().rev().find_map(last_char_in_node),
}
}
fn is_unicode_whitespace(c: char) -> bool {
c.is_whitespace()
}
fn is_unicode_punctuation(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::ConnectorPunctuation
| GeneralCategory::DashPunctuation
| GeneralCategory::OpenPunctuation
| GeneralCategory::ClosePunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::FinalPunctuation
| GeneralCategory::OtherPunctuation
| GeneralCategory::MathSymbol
| GeneralCategory::CurrencySymbol
| GeneralCategory::ModifierSymbol
| GeneralCategory::OtherSymbol
)
}
fn classify(ch: char, before: Option<char>, after: Option<char>) -> (bool, bool) {
let before_is_ws = before.map(is_unicode_whitespace).unwrap_or(true);
let after_is_ws = after.map(is_unicode_whitespace).unwrap_or(true);
let before_is_punct = before.map(is_unicode_punctuation).unwrap_or(false);
let after_is_punct = after.map(is_unicode_punctuation).unwrap_or(false);
let left_flanking = !after_is_ws && (!after_is_punct || before_is_ws || before_is_punct);
let right_flanking = !before_is_ws && (!before_is_punct || after_is_ws || after_is_punct);
if ch == '*' {
(left_flanking, right_flanking)
} else {
(
left_flanking && (!right_flanking || before_is_punct),
right_flanking && (!left_flanking || after_is_punct),
)
}
}
pub(super) fn tokenize_delimiter_run(
remaining: GrammarSpan,
before: Option<char>,
) -> (Delimiter, GrammarSpan) {
let ch = remaining
.fragment()
.chars()
.next()
.expect("caller guarantees non-empty input starting with a delimiter char");
let run_len = remaining
.fragment()
.chars()
.take_while(|&c| c == ch)
.count();
let (rest, run) = remaining.take_split(run_len);
let after = rest.fragment().chars().next();
let (can_open, can_close) = classify(ch, before, after);
(
Delimiter {
run,
ch,
original_count: run_len,
can_open,
can_close,
},
rest,
)
}
struct StackEntry {
item_idx: usize,
ch: char,
original_count: usize,
can_open: bool,
can_close: bool,
prev: Option<usize>,
next: Option<usize>,
}
fn bucket(ch: char, original_count: usize) -> (char, u8) {
(ch, (original_count % 3) as u8)
}
pub(super) fn resolve_emphasis(mut items: Vec<Item>) -> Vec<Node> {
let mut arena: Vec<StackEntry> = Vec::new();
for (idx, item) in items.iter().enumerate() {
if let Item::Delim(d) = item {
let prev_idx = arena.len().checked_sub(1);
if let Some(p) = prev_idx {
arena[p].next = Some(arena.len());
}
arena.push(StackEntry {
item_idx: idx,
ch: d.ch,
original_count: d.original_count,
can_open: d.can_open,
can_close: d.can_close,
prev: prev_idx,
next: None,
});
}
}
use std::collections::{HashMap, HashSet};
let mut openers_bottom: HashMap<(char, u8), Option<usize>> = HashMap::new();
let mut matched_pairs: HashSet<(usize, usize)> = HashSet::new();
let mut closer_idx = if arena.is_empty() { None } else { Some(0) };
while let Some(ci) = closer_idx {
if !arena[ci].can_close {
closer_idx = arena[ci].next;
continue;
}
let b = bucket(arena[ci].ch, arena[ci].original_count);
let bound = openers_bottom.get(&b).copied().flatten();
let mut oi_opt = arena[ci].prev;
let mut found: Option<usize> = None;
while let Some(oi) = oi_opt {
if let Some(b) = bound {
if oi <= b {
break;
}
}
if arena[oi].ch == arena[ci].ch && arena[oi].can_open {
let either_both = (arena[oi].can_open && arena[oi].can_close)
|| (arena[ci].can_open && arena[ci].can_close);
let compatible = if either_both {
let sum_mod3_zero =
(arena[oi].original_count + arena[ci].original_count).is_multiple_of(3);
let both_mod3_zero = arena[oi].original_count.is_multiple_of(3)
&& arena[ci].original_count.is_multiple_of(3);
!sum_mod3_zero || both_mod3_zero
} else {
true
};
if compatible {
found = Some(oi);
break;
}
}
oi_opt = arena[oi].prev;
}
match found {
Some(oi) => {
let opener_count = current_count(&items, arena[oi].item_idx);
let closer_count = current_count(&items, arena[ci].item_idx);
let use_n = if opener_count >= 2 && closer_count >= 2 {
2
} else {
1
};
let is_repeat_pair = !matched_pairs.insert((oi, ci));
let mut k = arena[oi].next;
while let Some(kk) = k {
if kk == ci {
break;
}
let next_k = arena[kk].next;
unlink(&mut arena, kk);
k = next_k;
}
arena[oi].next = Some(ci);
arena[ci].prev = Some(oi);
let oi_item = arena[oi].item_idx;
let ci_item = arena[ci].item_idx;
let mut children: Vec<Node> = Vec::new();
for slot in items.iter_mut().take(ci_item).skip(oi_item + 1) {
match std::mem::replace(slot, Item::Consumed) {
Item::Node(n) => children.push(n),
Item::Delim(d) => children.push(d.into_text_node()),
Item::Consumed => {}
}
}
let opener_span = match &mut items[oi_item] {
Item::Delim(d) => d.consume_as_opener(use_n),
_ => unreachable!("opener slot must still hold its delimiter"),
};
let closer_end_span = match &mut items[ci_item] {
Item::Delim(d) => {
let _ = d.consume_as_closer(use_n);
d.run
}
_ => unreachable!("closer slot must still hold its delimiter"),
};
let span = opt_span_range(opener_span, closer_end_span);
let kind_is_strong = use_n == 2;
let is_pure_triple_run = is_repeat_pair
&& arena[oi].original_count == 3
&& arena[ci].original_count == 3;
let wrapped = if is_pure_triple_run
&& children.len() == 1
&& is_pure_triple_pair(&children[0], kind_is_strong)
{
let inner_children = match children.into_iter().next() {
Some(Node { children, .. }) => children,
None => unreachable!(),
};
Node {
kind: NodeKind::StrongEmphasis,
span,
children: inner_children,
}
} else {
Node {
kind: if kind_is_strong {
NodeKind::Strong
} else {
NodeKind::Emphasis
},
span,
children,
}
};
items[oi_item + 1] = Item::Node(wrapped);
let opener_remaining = current_count(&items, oi_item);
if opener_remaining == 0 {
items[oi_item] = Item::Consumed;
unlink(&mut arena, oi);
}
let closer_remaining = current_count(&items, ci_item);
if closer_remaining == 0 {
items[ci_item] = Item::Consumed;
unlink(&mut arena, ci);
closer_idx = arena[ci].next;
}
}
None => {
openers_bottom.insert(b, arena[ci].prev);
if !arena[ci].can_open {
unlink(&mut arena, ci);
}
closer_idx = arena[ci].next;
}
}
}
items
.into_iter()
.filter_map(|item| match item {
Item::Node(n) => Some(n),
Item::Delim(d) => Some(d.into_text_node()),
Item::Consumed => None,
})
.collect()
}
fn current_count(items: &[Item], item_idx: usize) -> usize {
match &items[item_idx] {
Item::Delim(d) => d.count(),
_ => 0,
}
}
fn unlink(arena: &mut [StackEntry], i: usize) {
let (p, n) = (arena[i].prev, arena[i].next);
if let Some(p) = p {
arena[p].next = n;
}
if let Some(n) = n {
arena[n].prev = p;
}
}
fn is_pure_triple_pair(node: &Node, outer_is_strong: bool) -> bool {
match node.kind {
NodeKind::Strong => !outer_is_strong,
NodeKind::Emphasis => outer_is_strong,
_ => false,
}
}