use crate::engine::ast::{Inline, TextRange};
use super::scan::Token;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum QuoteKind {
Italic,
Bold,
BoldItalic,
}
#[derive(Debug)]
struct QuoteRun {
range: TextRange,
literal: u32,
kind: QuoteKind,
}
enum WorkToken {
Node(Inline),
Quote(QuoteRun),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FrameKind {
Bold,
Italic,
}
pub(super) fn do_quotes(flat: &str, tokens: Vec<Token>) -> Vec<Inline> {
let mut work: Vec<WorkToken> = tokens
.into_iter()
.map(|t| match t {
Token::Node(n) => WorkToken::Node(n),
Token::Quotes(range) => WorkToken::Quote(normalize(range)),
})
.collect();
rebalance(flat, &mut work);
emit(work)
}
fn normalize(range: TextRange) -> QuoteRun {
let n = range.end - range.start;
let (literal, kind) = match n {
2 => (0, QuoteKind::Italic),
3 => (0, QuoteKind::Bold),
4 => (1, QuoteKind::Bold),
5 => (0, QuoteKind::BoldItalic),
_ => (n - 5, QuoteKind::BoldItalic),
};
QuoteRun {
range,
literal,
kind,
}
}
fn rebalance(flat: &str, work: &mut [WorkToken]) {
let (mut bold, mut italic) = (0u32, 0u32);
for tok in work.iter() {
if let WorkToken::Quote(run) = tok {
match run.kind {
QuoteKind::Italic => italic += 1,
QuoteKind::Bold => bold += 1,
QuoteKind::BoldItalic => {
bold += 1;
italic += 1;
}
}
}
}
if bold % 2 == 0 || italic % 2 == 0 {
return;
}
let mut first_space = None;
let mut first_single = None;
let mut first_multi = None;
let mut ordinal = 0usize; for (i, tok) in work.iter().enumerate() {
let run = match tok {
WorkToken::Quote(run) => run,
WorkToken::Node(_) => continue,
};
if run.kind == QuoteKind::Bold {
let (x1, x2) = preceding_chars(flat, work, i);
match (x1, x2) {
(None, _) => {}
(Some(' '), _) => {
if first_space.is_none() {
first_space = Some(ordinal);
}
}
(Some(_), Some(' ')) => {
first_single = Some(ordinal);
}
(Some(_), _) => {
if first_multi.is_none() {
first_multi = Some(ordinal);
}
}
}
if first_single.is_some() {
break;
}
}
ordinal += 1;
}
if let Some(target) = first_single.or(first_multi).or(first_space) {
let mut seen = 0usize;
for tok in work.iter_mut() {
if let WorkToken::Quote(run) = tok {
if seen == target {
run.literal += 1;
run.kind = QuoteKind::Italic;
break;
}
seen += 1;
}
}
}
}
fn preceding_chars(flat: &str, work: &[WorkToken], i: usize) -> (Option<char>, Option<char>) {
const PLACEHOLDER: char = '\u{fffc}';
let mut got: Vec<char> = Vec::with_capacity(2);
if let WorkToken::Quote(run) = &work[i] {
got.extend(std::iter::repeat_n('\'', run.literal.min(2) as usize));
}
let mut j = i;
while got.len() < 2 && j > 0 {
j -= 1;
match &work[j] {
WorkToken::Node(Inline::Text(r)) => {
for c in r.slice(flat).chars().rev() {
got.push(c);
if got.len() == 2 {
break;
}
}
}
WorkToken::Quote(_) => got.push('\''),
WorkToken::Node(_) => {
got.push(PLACEHOLDER);
if got.len() < 2 {
got.push(PLACEHOLDER);
}
}
}
}
(got.first().copied(), got.get(1).copied())
}
fn emit(work: Vec<WorkToken>) -> Vec<Inline> {
let mut root: Vec<Inline> = Vec::new();
let mut stack: Vec<(FrameKind, Vec<Inline>)> = Vec::new();
for tok in work {
match tok {
WorkToken::Node(node) => push_node(&mut root, &mut stack, node),
WorkToken::Quote(run) => {
if run.literal > 0 {
let text = Inline::Text(TextRange::new(
run.range.start,
run.range.start + run.literal,
));
push_node(&mut root, &mut stack, text);
}
apply_quote(&mut root, &mut stack, run.kind);
}
}
}
while let Some((kind, content)) = stack.pop() {
let node = wrap(kind, content);
push_node(&mut root, &mut stack, node);
}
root
}
fn push_node(root: &mut Vec<Inline>, stack: &mut [(FrameKind, Vec<Inline>)], node: Inline) {
match stack.last_mut() {
Some((_, content)) => content.push(node),
None => root.push(node),
}
}
fn wrap(kind: FrameKind, content: Vec<Inline>) -> Inline {
match kind {
FrameKind::Bold => Inline::Bold(content),
FrameKind::Italic => Inline::Italic(content),
}
}
fn is_open(stack: &[(FrameKind, Vec<Inline>)], kind: FrameKind) -> bool {
stack.iter().any(|(k, _)| *k == kind)
}
fn apply_quote(root: &mut Vec<Inline>, stack: &mut Vec<(FrameKind, Vec<Inline>)>, kind: QuoteKind) {
match kind {
QuoteKind::Italic => toggle(root, stack, FrameKind::Italic),
QuoteKind::Bold => toggle(root, stack, FrameKind::Bold),
QuoteKind::BoldItalic => {
let bold = is_open(stack, FrameKind::Bold);
let italic = is_open(stack, FrameKind::Italic);
match (bold, italic) {
(false, false) => {
stack.push((FrameKind::Bold, Vec::new()));
stack.push((FrameKind::Italic, Vec::new()));
}
(true, true) => {
let top = stack.last().map(|(k, _)| *k);
if top == Some(FrameKind::Italic) {
close(root, stack, FrameKind::Italic);
close(root, stack, FrameKind::Bold);
} else {
close(root, stack, FrameKind::Bold);
close(root, stack, FrameKind::Italic);
}
}
(true, false) => {
close(root, stack, FrameKind::Bold);
stack.push((FrameKind::Italic, Vec::new()));
}
(false, true) => {
close(root, stack, FrameKind::Italic);
stack.push((FrameKind::Bold, Vec::new()));
}
}
}
}
}
fn toggle(root: &mut Vec<Inline>, stack: &mut Vec<(FrameKind, Vec<Inline>)>, kind: FrameKind) {
if is_open(stack, kind) {
close(root, stack, kind);
} else {
stack.push((kind, Vec::new()));
}
}
fn close(root: &mut Vec<Inline>, stack: &mut Vec<(FrameKind, Vec<Inline>)>, kind: FrameKind) {
let mut reopen = Vec::new();
while let Some((k, content)) = stack.pop() {
let node = wrap(k, content);
push_node(root, stack, node);
if k == kind {
break;
}
reopen.push(k);
}
for k in reopen.into_iter().rev() {
stack.push((k, Vec::new()));
}
}
#[cfg(test)]
mod tests {
use super::super::testutil::check;
#[test]
fn simple_italic() {
check("''i''", "I[Text(i)]");
}
#[test]
fn simple_bold() {
check("'''b'''", "B[Text(b)]");
}
#[test]
fn bold_italic_nests_italic_inside_bold() {
check("'''''bi'''''", "B[I[Text(bi)]]");
}
#[test]
fn four_apostrophes_keep_one_literal() {
check("''''x''''", "Text(') B[Text(x) Text(')]");
}
#[test]
fn more_than_five_keep_the_surplus_literal() {
check("'''''''x", "Text('') B[I[Text(x)]]");
}
#[test]
fn single_apostrophe_is_plain_text() {
check("don't", "Text(don't)");
}
#[test]
fn line_end_auto_closes_with_multi_letter_demotion() {
check("''a'''b", "I[Text(a) Text(')] Text(b)");
}
#[test]
fn demotion_prefers_single_letter_words() {
check(
"plain l'''bold''italic",
"Text(plain l) Text(') I[Text(bold)] Text(italic)",
);
}
#[test]
fn demotion_falls_back_to_a_space() {
check("a '''b''c", "Text(a ) Text(') I[Text(b)] Text(c)");
}
#[test]
fn single_letter_beats_an_earlier_multi_letter_candidate() {
check(
"w'''a l'''b''c'''d",
"Text(w) B[Text(a l) Text(') I[Text(b)] Text(c)] Text(d)",
);
}
#[test]
fn no_candidate_leaves_the_imbalance_alone() {
check("'''a''", "B[Text(a) I[]]");
}
#[test]
fn four_run_demotes_via_its_own_literal_apostrophe() {
check("''''a''", "Text('') I[Text(a)]");
}
#[test]
fn link_before_bold_counts_as_a_multi_letter_word() {
check("[[a]]'''b''", "Link(A) Text(') I[Text(b)]");
}
#[test]
fn one_letter_after_a_link_is_still_multi_letter() {
check("[[a]]X'''b''", "Link(A) Text(X) Text(') I[Text(b)]");
}
#[test]
fn interleaved_close_reopens_the_inner_span() {
check("'''a''b'''c''", "B[Text(a) I[Text(b)]] I[Text(c)]");
}
#[test]
fn bold_italic_closes_italic_then_continues_bold() {
check("'''''a''b", "B[I[Text(a)] Text(b)]");
}
#[test]
fn bold_italic_swaps_open_italic_for_bold() {
check("''a'''''b", "I[Text(a)] B[Text(b)]");
}
#[test]
fn bold_italic_swaps_open_bold_for_italic() {
check("'''a'''''b", "B[Text(a)] I[Text(b)]");
}
#[test]
fn bold_italic_closes_both_in_stack_order() {
check("'''''a'''''", "B[I[Text(a)]]");
check("''x'''y'''''", "I[Text(x) B[Text(y)]]");
}
}