use std::borrow::Cow;
use crate::ast::math_text::{math_source, math_source_from_other};
use crate::ast::node::Inline;
use pulldown_cmark::Event;
pub(crate) enum TextAtom<'a> {
Verbatim(&'a str),
Math(Cow<'a, str>),
Break,
}
impl<'a> TextAtom<'a> {
fn from_tex(tex: &str, display: bool) -> TextAtom<'a> {
TextAtom::Math(Cow::Owned(math_source(tex, display)))
}
}
fn push_atom(out: &mut String, atom: TextAtom<'_>) {
match atom {
TextAtom::Verbatim(t) => out.push_str(t),
TextAtom::Math(src) => out.push_str(&src),
TextAtom::Break => out.push(' '),
}
}
pub(crate) fn events_to_text(events: &[Event<'_>], start: usize, end: usize) -> String {
let mut out = String::new();
for event in &events[start..end] {
match event {
Event::Text(t) => push_atom(&mut out, TextAtom::Verbatim(t)),
Event::Code(c) => push_atom(&mut out, TextAtom::Verbatim(c)),
Event::InlineMath(t) => push_atom(&mut out, TextAtom::from_tex(t, false)),
Event::DisplayMath(t) => push_atom(&mut out, TextAtom::from_tex(t, true)),
_ => {}
}
}
out
}
pub(crate) fn inlines_to_text(inlines: &[Inline], out: &mut String) {
for inline in inlines {
match inline {
Inline::Text(t) => push_atom(out, TextAtom::Verbatim(t)),
Inline::Code(c) => push_atom(out, TextAtom::Verbatim(c)),
Inline::Emphasis(children) | Inline::Strong(children) => inlines_to_text(children, out),
Inline::Link { children, .. } => inlines_to_text(children, out),
Inline::Image { alt, .. } => push_atom(out, TextAtom::Verbatim(alt)),
Inline::LineBreak => push_atom(out, TextAtom::Break),
Inline::Other(html) => {
if let Some(src) = math_source_from_other(html) {
push_atom(out, TextAtom::Math(Cow::Owned(src)));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::parser::ParseConfig;
use crate::ast::{parse_with_config, Block};
use crate::heading::anchor::obsidian_heading_anchor;
#[test]
fn event_walk_and_inline_walk_agree_on_every_heading() {
for md in [
"# Euler $e^{i\\pi}=-1$ identity\n",
"# Convolution $f*g$ and $h*k$ end\n",
"# Dual $V^*$ and $W^*$ end\n",
"# Case $$a+b$$ tail\n",
"# Mixed `code` and *em* and **strong** here\n",
"# A [link](/x) and  inline\n",
"# 中文 $\\alpha$ 标题\n",
"# Nested *em with `code` and $x^2$* tail\n",
] {
for math in [false, true] {
let cfg = ParseConfig { math, ..Default::default() };
let doc = parse_with_config(md, &cfg);
let Block::Heading { children, id, .. } = &doc.blocks[0] else {
panic!("expected a heading for {md:?}");
};
let mut label = String::new();
inlines_to_text(children, &mut label);
assert_eq!(
id.as_deref().expect("heading must have an id"),
obsidian_heading_anchor(&label),
"slug and label disagree for {md:?} (math={math})"
);
}
}
}
#[test]
fn setext_soft_break_divergence_is_pinned_not_fixed() {
let doc = parse_with_config("foo\nbar\n===\n", &ParseConfig::default());
let Block::Heading { children, id, .. } = &doc.blocks[0] else {
panic!("expected a setext heading, got {:?}", doc.blocks[0]);
};
let mut label = String::new();
inlines_to_text(children, &mut label);
assert_eq!(label, "foo\nbar", "AST keeps the SoftBreak as a newline");
assert_eq!(id.as_deref(), Some("foobar"), "the event walk drops it");
}
#[test]
fn policy_is_one_function() {
let mut out = String::new();
push_atom(&mut out, TextAtom::Verbatim("a"));
push_atom(&mut out, TextAtom::Break);
push_atom(&mut out, TextAtom::from_tex("x^2", false));
push_atom(&mut out, TextAtom::from_tex("y", true));
assert_eq!(out, "a $x^2$$$y$$");
}
}