use crate::{
Span,
parser::{
InlineSubstitutionRenderer, ReferenceResolver, ReferenceWarning, ReferenceWarningKind,
ResolutionContext, XrefRenderParams,
},
strings::CowStr,
};
#[derive(Clone, Eq, PartialEq)]
pub struct Content<'src> {
original: Span<'src>,
pub(crate) rendered: CowStr<'src>,
deferred: Option<Box<DeferredContent>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct DeferredContent {
template: String,
xrefs: Vec<XrefSegment>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct XrefSegment {
pub(crate) target: String,
pub(crate) provided_text: Option<String>,
pub(crate) window: Option<String>,
pub(crate) roles: Vec<String>,
pub(crate) resolved: Option<crate::parser::ResolvedReference>,
}
const XREF_PLACEHOLDER_START: char = '\u{E000}';
const XREF_PLACEHOLDER_END: char = '\u{E001}';
impl<'src> Content<'src> {
pub(crate) fn from_filtered<T: AsRef<str>>(span: Span<'src>, filtered: T) -> Self {
Self {
original: span,
rendered: filtered.as_ref().to_string().into(),
deferred: None,
}
}
pub fn original(&self) -> Span<'src> {
self.original
}
pub fn rendered(&'src self) -> &'src str {
self.rendered.as_ref()
}
pub(crate) fn rendered_owned(&self) -> String {
self.rendered.as_ref().to_string()
}
pub fn is_empty(&self) -> bool {
self.rendered.as_ref().is_empty()
}
pub fn has_unresolved_refs(&self) -> bool {
self.deferred
.as_ref()
.is_some_and(|d| d.xrefs.iter().any(|x| x.resolved.is_none()))
}
pub(crate) fn set_deferred_xrefs(&mut self, xrefs: Vec<XrefSegment>) {
if xrefs.is_empty() {
return;
}
debug_assert!(
self.deferred.is_none(),
"set_deferred_xrefs must be called at most once per Content"
);
self.deferred = Some(Box::new(DeferredContent {
template: String::new(),
xrefs,
}));
}
pub(crate) fn xref_placeholder(index: usize) -> String {
format!("{XREF_PLACEHOLDER_START}{index}{XREF_PLACEHOLDER_END}")
}
pub(crate) fn finalize_deferred(&mut self, renderer: &dyn InlineSubstitutionRenderer) {
if self.deferred.is_none() {
return;
}
let template = self.rendered.as_ref().to_string();
if let Some(deferred) = self.deferred.as_mut() {
deferred.template = template;
}
self.rebuild_rendered(renderer);
}
pub(crate) fn resolve_references(
&mut self,
resolver: &dyn ReferenceResolver,
renderer: &dyn InlineSubstitutionRenderer,
warnings: &mut Vec<ReferenceWarning>,
) {
if let Some(deferred) = self.deferred.as_mut() {
let DeferredContent { template, xrefs } = deferred.as_mut();
debug_assert!(!template.is_empty());
for (index, xref) in xrefs.iter_mut().enumerate() {
xref.resolved = resolver.resolve(&ResolutionContext {
target: &xref.target,
provided_text: xref.provided_text.as_deref(),
});
if xref.resolved.is_none() && template.contains(&Content::xref_placeholder(index)) {
warnings.push(ReferenceWarning {
target: xref.target.clone(),
kind: ReferenceWarningKind::Unresolved,
});
}
}
}
self.rebuild_rendered(renderer);
}
fn rebuild_rendered(&mut self, renderer: &dyn InlineSubstitutionRenderer) {
let Some(deferred) = self.deferred.as_ref() else {
return;
};
self.rendered = render_template(&deferred.template, &deferred.xrefs, renderer).into();
}
}
pub(crate) fn rehome_xref_placeholders(
text: &str,
all: &[XrefSegment],
) -> (String, Vec<XrefSegment>) {
let mut local: Vec<XrefSegment> = vec![];
if !text.contains(XREF_PLACEHOLDER_START) {
return (text.to_string(), local);
}
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(start) = rest.find(XREF_PLACEHOLDER_START) {
out.push_str(&rest[..start]);
let after = &rest[start + XREF_PLACEHOLDER_START.len_utf8()..];
let Some(end) = after.find(XREF_PLACEHOLDER_END) else {
out.push(XREF_PLACEHOLDER_START);
rest = after;
continue;
};
let body = &after[..end];
rest = &after[end + XREF_PLACEHOLDER_END.len_utf8()..];
match body.parse::<usize>().ok().and_then(|index| all.get(index)) {
Some(segment) => {
let local_index = local.len();
local.push(segment.clone());
out.push_str(&Content::xref_placeholder(local_index));
}
None => {
out.push(XREF_PLACEHOLDER_START);
out.push_str(body);
out.push(XREF_PLACEHOLDER_END);
}
}
}
out.push_str(rest);
(out, local)
}
fn render_template(
template: &str,
xrefs: &[XrefSegment],
renderer: &dyn InlineSubstitutionRenderer,
) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(start) = rest.find(XREF_PLACEHOLDER_START) {
out.push_str(&rest[..start]);
let after = &rest[start + XREF_PLACEHOLDER_START.len_utf8()..];
let Some(end) = after.find(XREF_PLACEHOLDER_END) else {
out.push(XREF_PLACEHOLDER_START);
rest = after;
continue;
};
let body = &after[..end];
rest = &after[end + XREF_PLACEHOLDER_END.len_utf8()..];
match body
.parse::<usize>()
.ok()
.and_then(|index| xrefs.get(index))
{
Some(xref) => {
renderer.render_xref(
&XrefRenderParams {
target: &xref.target,
provided_text: xref.provided_text.as_deref(),
window: xref.window.as_deref(),
roles: &xref.roles,
resolved: xref.resolved.as_ref(),
},
&mut out,
);
}
None => {
debug_assert!(false, "xref placeholder index {body:?} out of range");
out.push(XREF_PLACEHOLDER_START);
out.push_str(body);
out.push(XREF_PLACEHOLDER_END);
}
}
}
out.push_str(rest);
out
}
#[derive(Clone, Eq, PartialEq)]
pub(crate) struct FootnoteDeferred {
template: String,
xrefs: Vec<XrefSegment>,
}
impl FootnoteDeferred {
pub(crate) fn new(template: String, xrefs: Vec<XrefSegment>) -> Self {
Self { template, xrefs }
}
pub(crate) fn render(&self, renderer: &dyn InlineSubstitutionRenderer) -> String {
render_template(&self.template, &self.xrefs, renderer)
}
pub(crate) fn resolve(
&mut self,
resolver: &dyn ReferenceResolver,
warnings: &mut Vec<ReferenceWarning>,
) {
for xref in self.xrefs.iter_mut() {
xref.resolved = resolver.resolve(&ResolutionContext {
target: &xref.target,
provided_text: xref.provided_text.as_deref(),
});
if xref.resolved.is_none() {
warnings.push(ReferenceWarning {
target: xref.target.clone(),
kind: ReferenceWarningKind::Unresolved,
});
}
}
}
}
impl std::fmt::Debug for FootnoteDeferred {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FootnoteDeferred")
.field("template", &self.template)
.field("xrefs", &self.xrefs)
.finish()
}
}
impl<'src> From<Span<'src>> for Content<'src> {
fn from(span: Span<'src>) -> Self {
Self {
original: span,
rendered: CowStr::from(span.data()),
deferred: None,
}
}
}
impl std::fmt::Debug for Content<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("Content");
s.field("original", &self.original);
s.field("rendered", &self.rendered);
if let Some(deferred) = self.deferred.as_ref() {
s.field("deferred", deferred);
}
s.finish()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
mod is_empty {
#[test]
fn basic_empty_span() {
let content = crate::content::Content::from(crate::Span::default());
assert!(content.is_empty());
}
#[test]
fn basic_non_empty_span() {
let content = crate::content::Content::from(crate::Span::new("blah"));
assert!(!content.is_empty());
}
}
mod footnote_deferred {
use super::super::{
FootnoteDeferred, XREF_PLACEHOLDER_END, XREF_PLACEHOLDER_START, XrefSegment,
rehome_xref_placeholders,
};
fn segment(target: &str) -> XrefSegment {
XrefSegment {
target: target.to_string(),
provided_text: None,
window: None,
roles: vec![],
resolved: None,
}
}
#[test]
fn rehomes_a_placeholder_into_a_local_segment() {
let all = vec![segment("a"), segment("b")];
let text = format!("see {XREF_PLACEHOLDER_START}1{XREF_PLACEHOLDER_END} here");
let (template, local) = rehome_xref_placeholders(&text, &all);
assert_eq!(local.len(), 1);
assert_eq!(local.first().unwrap().target, "b");
assert_eq!(
template,
format!("see {XREF_PLACEHOLDER_START}0{XREF_PLACEHOLDER_END} here")
);
}
#[test]
fn text_without_placeholders_is_returned_unchanged() {
let (template, local) = rehome_xref_placeholders("plain text", &[segment("a")]);
assert_eq!(template, "plain text");
assert!(local.is_empty());
}
#[test]
fn malformed_placeholders_are_passed_through_literally() {
let bad_index = format!("a{XREF_PLACEHOLDER_START}xyz{XREF_PLACEHOLDER_END}b");
let (template, local) = rehome_xref_placeholders(&bad_index, &[]);
assert_eq!(template, bad_index);
assert!(local.is_empty());
let unterminated = format!("a{XREF_PLACEHOLDER_START}0 no end");
let (template, local) = rehome_xref_placeholders(&unterminated, &[]);
assert_eq!(template, unterminated);
assert!(local.is_empty());
}
#[test]
fn out_of_range_placeholder_index_is_passed_through() {
let text = format!("x{XREF_PLACEHOLDER_START}9{XREF_PLACEHOLDER_END}y");
let (template, local) = rehome_xref_placeholders(&text, &[segment("a")]);
assert_eq!(template, text);
assert!(local.is_empty());
}
#[test]
fn debug_includes_template_and_xrefs() {
let deferred = FootnoteDeferred::new("t".to_string(), vec![segment("a")]);
let rendered = format!("{deferred:?}");
assert!(rendered.contains("FootnoteDeferred"));
assert!(rendered.contains("template"));
}
}
}