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) 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 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() {
for xref in deferred.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,
});
}
}
}
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();
}
}
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(),
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
}
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());
}
}
}