use std::{collections::HashMap, iter::once, num::NonZero, vec};
use document_tree::{
Document, HasChildren, LabelledFootnote as _,
attribute_types::{AutoFootnoteType, ID, NameToken},
element_categories as c,
elements::{self as e, Element},
extra_attributes::{ExtraAttributes, FootnoteType},
url::Url,
};
use super::{Transform, Visit};
#[must_use]
pub fn standard_transform(doc: Document) -> Document {
let mut pass1 = Pass1::default();
let doc = pass1.transform(doc);
let mut pass2 = Pass2::from(&pass1);
pass2.visit(&doc);
Pass3::from(&pass2).transform(doc)
}
#[derive(Debug)]
#[allow(dead_code)]
enum NamedTargetType {
Citation,
InternalLink,
ExternalLink(Url),
IndirectLink(NameToken),
SectionTitle,
}
impl NamedTargetType {
#[allow(dead_code)]
fn is_implicit_target(&self) -> bool {
use NamedTargetType as T;
matches!(self, T::SectionTitle | T::Citation)
}
}
const ONE: NonZero<usize> = NonZero::<usize>::MIN;
#[derive(Default, Debug)]
struct Pass1 {
footnotes_symbol: HashMap<ID, NonZero<usize>>,
footnotes_number: HashMap<ID, NonZero<usize>>,
auto_numbered_anon_footnotes: Vec<NonZero<usize>>,
auto_numbered_named_footnotes: Vec<NonZero<usize>>,
n_anon_footnotes: usize,
n_footnote_refs: usize,
}
impl Pass1 {
fn next_footnote(&mut self, typ: AutoFootnoteType) -> NonZero<usize> {
match typ {
AutoFootnoteType::Number => {
let Some(n) = NonZero::new(self.footnotes_number.len()) else {
return ONE;
};
let mut ordered: Vec<_> = self.footnotes_number.values().copied().collect();
ordered.sort_unstable();
ordered
.iter()
.copied()
.zip(1usize..) .enumerate()
.find_map(|(i, (n1, n2))| (n1.get() != n2).then_some(ONE.saturating_add(i)))
.unwrap_or(n)
}
AutoFootnoteType::Symbol => {
if cfg!(debug_assertions) {
let mut vals: Vec<usize> = self
.footnotes_symbol
.values()
.copied()
.map(Into::into)
.collect();
vals.sort_unstable();
assert_eq!(vals, (1..=self.footnotes_symbol.len()).collect::<Vec<_>>());
}
ONE.saturating_add(self.footnotes_symbol.len())
}
}
}
}
impl Transform for Pass1 {
fn transform_footnote(&mut self, mut e: e::Footnote) -> impl Iterator<Item = c::BodyElement> {
let n = match e
.extra()
.auto
.map(|t| self.next_footnote(t))
.ok_or(())
.or_else::<anyhow::Error, _>(|()| Ok(e.get_label()?.parse()?))
{
Ok(n) => n,
Err(err) => {
let t = e::Problematic::with_children(vec![err.to_string().into()]).into();
return once(e::Paragraph::with_children(vec![t]).into());
}
};
let id = if let Some(name) = e.names().first() {
name.0.as_str().into()
} else {
self.n_anon_footnotes += 1;
ID(format!("footnote-{}", self.n_anon_footnotes))
};
e.ids_mut().push(id.clone());
if e.is_symbol() {
self.footnotes_symbol.insert(id.clone(), n);
} else {
self.footnotes_number.insert(id.clone(), n);
}
if matches!(e.extra().auto, Some(AutoFootnoteType::Number)) {
if e.names().is_empty() {
self.auto_numbered_anon_footnotes.push(n);
} else {
self.auto_numbered_named_footnotes.push(n);
}
}
self.transform_children(&mut e, Self::transform_sub_footnote);
once(e.into())
}
fn transform_footnote_reference(
&mut self,
mut e: e::FootnoteReference,
) -> impl Iterator<Item = c::TextOrInlineElement> {
self.n_footnote_refs += 1;
e.ids_mut()
.push(ID(format!("footnote-reference-{}", self.n_footnote_refs)));
self.transform_children(&mut e, Self::transform_text_or_inline_element);
once(e.into())
}
}
#[derive(Clone, Debug)]
struct Substitution {
content: Vec<c::TextOrInlineElement>,
ltrim: bool,
rtrim: bool,
}
#[derive(Debug)]
struct Pass2<'p1> {
pass1: &'p1 Pass1,
named_targets: HashMap<NameToken, NamedTargetType>,
substitutions: HashMap<NameToken, Substitution>,
normalized_substitutions: HashMap<String, Substitution>,
symbol_footnote_refs: HashMap<ID, NonZero<usize>>,
numbered_footnote_refs: HashMap<ID, NonZero<usize>>,
n_symbol_footnote_refs: usize,
n_numbered_anon_footnote_refs: usize,
n_numbered_named_footnote_refs: usize,
}
impl<'p1> From<&'p1 Pass1> for Pass2<'p1> {
fn from(pass1: &'p1 Pass1) -> Self {
Self {
pass1,
named_targets: HashMap::new(),
substitutions: HashMap::new(),
normalized_substitutions: HashMap::new(),
symbol_footnote_refs: HashMap::new(),
numbered_footnote_refs: HashMap::new(),
n_symbol_footnote_refs: 0,
n_numbered_anon_footnote_refs: 0,
n_numbered_named_footnote_refs: 0,
}
}
}
impl<'tree> Visit<'tree> for Pass2<'_> {
fn visit_substitution_definition(&mut self, e: &'tree e::SubstitutionDefinition) {
let subst = Substitution {
content: e.children().clone(),
ltrim: e.extra().ltrim,
rtrim: e.extra().rtrim,
};
for name in e.names() {
if self.substitutions.contains_key(name) {
}
self.substitutions.insert(name.clone(), subst.clone());
self.normalized_substitutions
.insert(name.0.to_lowercase(), subst.clone());
}
}
fn visit_target(&mut self, e: &'tree e::Target) {
if let Some(uri) = &e.extra().refuri {
for name in e.names() {
self.named_targets
.insert(name.clone(), NamedTargetType::ExternalLink(uri.clone()));
}
}
}
fn visit_footnote_reference(&mut self, e: &'tree e::FootnoteReference) {
let id = e.ids().first().unwrap();
let name = e.names().first();
let n = match e.extra().auto {
Some(AutoFootnoteType::Symbol) => {
self.n_symbol_footnote_refs += 1;
NonZero::new(self.n_symbol_footnote_refs).unwrap()
}
Some(AutoFootnoteType::Number) => {
if name.is_some() {
self.n_numbered_named_footnote_refs += 1;
self.pass1.auto_numbered_named_footnotes
[self.n_numbered_named_footnote_refs - 1]
} else {
self.n_numbered_anon_footnote_refs += 1;
self.pass1.auto_numbered_anon_footnotes[self.n_numbered_anon_footnote_refs - 1]
}
}
None => e.get_label().unwrap().parse().unwrap(),
};
if e.is_symbol() {
self.symbol_footnote_refs.insert(id.clone(), n);
} else {
self.numbered_footnote_refs.insert(id.clone(), n);
}
for c in e.children() {
self.visit_text_or_inline_element(c);
}
}
}
#[derive(Debug)]
struct Pass3<'p1, 'p2: 'p1>(&'p2 Pass2<'p1>);
impl<'p2> Pass3<'_, 'p2> {
fn target_url<'t>(self: &'t Pass3<'_, 'p2>, refname: &[NameToken]) -> Option<&'t Url> {
assert!(
refname.len() == 1,
"Expected exactly one name in a reference."
);
let name = refname[0].clone();
match self.0.named_targets.get(&name)? {
NamedTargetType::ExternalLink(url) => Some(url),
_ => unimplemented!(),
}
}
fn substitution<'t>(
self: &'t Pass3<'_, 'p2>,
refname: &[NameToken],
) -> Option<&'t Substitution> {
assert!(
refname.len() == 1,
"Expected exactly one name in a substitution reference."
);
let name = refname[0].clone();
self.0
.substitutions
.get(&name)
.or_else(|| self.0.normalized_substitutions.get(&name.0.to_lowercase()))
}
}
impl<'p1, 'p2: 'p1> From<&'p2 Pass2<'p1>> for Pass3<'p1, 'p2> {
fn from(p: &'p2 Pass2<'p1>) -> Self {
Pass3(p)
}
}
impl Transform for Pass3<'_, '_> {
fn transform_substitution_definition(
&mut self,
_: e::SubstitutionDefinition,
) -> impl Iterator<Item = c::BodyElement> {
None.into_iter()
}
fn transform_substitution_reference(
&mut self,
e: e::SubstitutionReference,
) -> impl Iterator<Item = c::TextOrInlineElement> {
let r: Box<dyn Iterator<Item = c::TextOrInlineElement>> = if let Some(Substitution {
content,
ltrim,
rtrim,
}) =
self.substitution(&e.extra().refname)
{
if *ltrim || *rtrim {
dbg!(content, ltrim, rtrim);
}
Box::new(content.clone().into_iter())
} else {
let mut replacement: Box<e::Problematic> = Box::default();
replacement
.children_mut()
.push(c::TextOrInlineElement::String(Box::new(format!(
"|{}|",
e.extra().refname[0].0
))));
Box::new(once(c::TextOrInlineElement::Problematic(replacement)))
};
r
}
fn transform_reference(
&mut self,
mut e: e::Reference,
) -> impl Iterator<Item = c::TextOrInlineElement> {
if e.extra().refuri.is_none() {
if let Some(uri) = self.target_url(&e.extra().refname) {
e.extra_mut().refuri = Some(uri.clone());
}
}
once(e.into())
}
fn transform_footnote(&mut self, mut e: e::Footnote) -> impl Iterator<Item = c::BodyElement> {
let id = e.ids().first().unwrap();
let id2num = if e.is_symbol() {
&self.0.pass1.footnotes_symbol
} else {
&self.0.pass1.footnotes_number
};
let num = id2num.get(id).unwrap();
if e.get_label().is_err() {
e.children_mut().insert(
0,
e::Label::with_children(vec![num.to_string().into()]).into(),
);
}
let refid2num = if e.is_symbol() {
&self.0.symbol_footnote_refs
} else {
&self.0.numbered_footnote_refs
};
e.extra_mut().backrefs = refid2num
.iter()
.filter(|&(_, num2)| num == num2)
.map(|(refid, _)| refid.clone())
.collect();
self.transform_children(&mut e, Self::transform_sub_footnote);
once(e.into())
}
fn transform_footnote_reference(
&mut self,
mut e: e::FootnoteReference,
) -> impl Iterator<Item = c::TextOrInlineElement> {
let refid = e.ids().first().unwrap();
let refid2num = if e.is_symbol() {
&self.0.symbol_footnote_refs
} else {
&self.0.numbered_footnote_refs
};
let n = refid2num.get(refid).unwrap();
let footnote2num = if e.is_symbol() {
&self.0.pass1.footnotes_symbol
} else {
&self.0.pass1.footnotes_number
};
let num2footnote: HashMap<_, _> =
footnote2num.iter().map(|(k, v)| (*v, k.clone())).collect();
e.extra_mut().refid = num2footnote.get(n).cloned();
if e.get_label().is_err() {
e.children_mut().insert(0, n.to_string().into());
}
self.transform_children(&mut e, Self::transform_text_or_inline_element);
once(e.into())
}
}