Skip to main content

htmlp/
rendered.rs

1use crate::{Bindings, Diagnostic, Document, Node, Position, Report, TokenCounter};
2use std::{collections::BTreeMap, ops::Range};
3
4/// A tokenizer-specific count of a complete rendered document, excluding
5/// provider message framing. Distinct from a runtime request estimate.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(feature = "json", derive(serde::Serialize))]
8pub struct TextMeasurement {
9    pub tokenizer: String,
10    pub tokens: u64,
11}
12
13/// A containing element, including anonymous elements and their source position.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[cfg_attr(feature = "json", derive(serde::Serialize))]
16pub struct ElementOrigin {
17    pub name: String,
18    pub path: Vec<usize>,
19    pub id: Option<String>,
20    pub position: Position,
21}
22
23/// Non-overlapping UTF-8 byte span in the final text. Element ancestry preserves
24/// nesting without counting a parent's text a second time. Variables remain
25/// literal values and inherit their containing element's attribution.
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[cfg_attr(feature = "json", derive(serde::Serialize))]
28pub struct RenderedSpan {
29    pub range: Range<usize>,
30    pub elements: Vec<ElementOrigin>,
31    pub variable: Option<String>,
32}
33
34/// One element's rendered range, including empty and anonymous elements.
35/// These ranges may overlap; use spans for a non-overlapping partition.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(feature = "json", derive(serde::Serialize))]
38pub struct RenderedElement {
39    pub origin: ElementOrigin,
40    pub range: Range<usize>,
41}
42
43/// Immutable output of a complete successful validation. No constructor or
44/// deserializer can fabricate checked output. Extracted text is ordinary text;
45/// modifying it does not preserve this object's validation guarantee.
46#[derive(Debug, Clone)]
47pub struct RenderedDocument {
48    text: String,
49    measurement: TextMeasurement,
50    report: Report,
51    spans: Vec<RenderedSpan>,
52    by_id: BTreeMap<String, Range<usize>>,
53    elements: Vec<RenderedElement>,
54}
55impl RenderedDocument {
56    pub fn text(&self) -> &str {
57        &self.text
58    }
59    pub fn measurement(&self) -> &TextMeasurement {
60        &self.measurement
61    }
62    pub fn report(&self) -> &Report {
63        &self.report
64    }
65    pub fn spans(&self) -> &[RenderedSpan] {
66        &self.spans
67    }
68    /// Select from an already checked whole document. Ancestor and sibling
69    /// budgets were validated before any element became available.
70    pub fn element(&self, id: &str) -> Option<&str> {
71        self.by_id.get(id).map(|range| &self.text[range.clone()])
72    }
73    pub fn elements(&self) -> &[RenderedElement] {
74        &self.elements
75    }
76    pub fn elements_by_name(&self, name: &str) -> Vec<&str> {
77        self.elements
78            .iter()
79            .filter(|e| e.origin.name == name)
80            .map(|e| &self.text[e.range.clone()])
81            .collect()
82    }
83    pub fn into_text(self) -> String {
84        self.text
85    }
86}
87
88/// Bind and validate the entire document, returning immutable text, exact
89/// tokenizer measurement, budget report, and non-overlapping element provenance.
90pub fn render_checked(
91    doc: &Document,
92    bindings: &Bindings,
93    counter: &impl TokenCounter,
94) -> Result<RenderedDocument, Vec<Diagnostic>> {
95    let (text, report) = crate::check::render_with_report(doc, bindings, counter)?;
96    let mut output = RenderedDocument {
97        measurement: TextMeasurement {
98            tokenizer: counter.name().into(),
99            tokens: report
100                .measurements
101                .last()
102                .and_then(|m| m.tokens)
103                .expect("render counts root"),
104        },
105        text,
106        report,
107        spans: Vec::new(),
108        by_id: BTreeMap::new(),
109        elements: Vec::new(),
110    };
111    fn walk(
112        nodes: &[Node],
113        bindings: &Bindings,
114        ancestry: &mut Vec<ElementOrigin>,
115        path: &mut Vec<usize>,
116        offset: &mut usize,
117        out: &mut RenderedDocument,
118    ) {
119        for (index, node) in nodes.iter().enumerate() {
120            let (length, variable) = match node {
121                Node::Element(section) => {
122                    let start = *offset;
123                    path.push(index);
124                    let origin = ElementOrigin {
125                        name: section.name.clone(),
126                        id: section.id.clone(),
127                        position: section.position,
128                        path: path.clone(),
129                    };
130                    let item = out.elements.len();
131                    out.elements.push(RenderedElement {
132                        origin: origin.clone(),
133                        range: start..start,
134                    });
135                    ancestry.push(origin);
136                    walk(&section.children, bindings, ancestry, path, offset, out);
137                    ancestry.pop();
138                    path.pop();
139                    out.elements[item].range.end = *offset;
140                    if let Some(id) = &section.id {
141                        out.by_id.insert(id.clone(), start..*offset);
142                    }
143                    continue;
144                }
145                Node::Text { value } => (value.len(), None),
146                Node::Variable(variable) => {
147                    (bindings[&variable.id].len(), Some(variable.id.clone()))
148                }
149            };
150            if length > 0 {
151                out.spans.push(RenderedSpan {
152                    range: *offset..*offset + length,
153                    elements: ancestry.clone(),
154                    variable,
155                });
156            }
157            *offset += length;
158        }
159    }
160    walk(
161        &doc.children,
162        bindings,
163        &mut Vec::new(),
164        &mut Vec::new(),
165        &mut 0,
166        &mut output,
167    );
168    Ok(output)
169}