Skip to main content

brink_runtime/output/
fragment.rs

1//! Fragment model for locale-safe slots.
2//!
3//! A fragment is a captured sub-region of output whose parts are stored
4//! structurally (not eagerly stringified), so it can be resolved against
5//! whatever line tables/locale are active at read time — the same
6//! locale-hot-swap property `OutputPart` documents at the module level.
7//! Fragments are how string-typed slot values in a template line (e.g.
8//! `"{~x}"` where `x` is itself templated) stay locale-safe rather than
9//! collapsing to a fixed-locale string at push time.
10
11use alloc::string::String;
12use alloc::vec::Vec;
13
14use brink_format::{LineEntry, PluralResolver};
15
16use super::{OutputBuffer, OutputPart, resolve_parts};
17use crate::program::Program;
18
19/// A finalized fragment — structural output parts plus any associated tags.
20#[derive(Debug, Clone, PartialEq)]
21pub struct Fragment {
22    pub parts: Vec<OutputPart>,
23    pub tags: Vec<String>,
24}
25
26impl OutputBuffer {
27    // ── Fragment capture ───────────────────────────────────────────────
28
29    /// Begin capturing output into a new fragment.
30    pub fn begin_fragment(&mut self) {
31        self.fragment_depth += 1;
32        self.fragment_capture.push(OutputPart::Checkpoint);
33        self.fragment_pending_tags.push(Vec::new());
34    }
35
36    /// End the current fragment capture: drain from the last checkpoint,
37    /// store the parts in the fragment store, return the fragment index.
38    #[expect(clippy::cast_possible_truncation)]
39    pub fn end_fragment(&mut self) -> Option<u32> {
40        let cp_idx = self
41            .fragment_capture
42            .iter()
43            .rposition(|p| matches!(p, OutputPart::Checkpoint))?;
44
45        let captured: Vec<OutputPart> = self.fragment_capture.drain(cp_idx..).collect();
46        // Skip the checkpoint itself (first element).
47        let parts: Vec<OutputPart> = captured.into_iter().skip(1).collect();
48        let tags = self.fragment_pending_tags.pop().unwrap_or_default();
49        let idx = self.fragments.len() as u32;
50        self.fragments.push(Fragment { parts, tags });
51
52        self.fragment_depth = self.fragment_depth.saturating_sub(1);
53
54        Some(idx)
55    }
56
57    /// Returns true if currently inside a fragment capture.
58    pub fn in_fragment_capture(&self) -> bool {
59        self.fragment_depth > 0
60    }
61
62    /// Push a tag onto the current fragment being captured.
63    pub fn push_fragment_tag(&mut self, tag: String) {
64        if let Some(pending) = self.fragment_pending_tags.last_mut() {
65            pending.push(tag);
66        }
67    }
68
69    /// Read access to a finalized fragment's tags.
70    pub fn fragment_tags(&self, idx: u32) -> Option<&[String]> {
71        self.fragments.get(idx as usize).map(|f| f.tags.as_slice())
72    }
73
74    /// Read access to all finalized fragments.
75    pub fn fragments(&self) -> &[Fragment] {
76        &self.fragments
77    }
78
79    /// Read access to a finalized fragment's parts.
80    pub fn fragment(&self, idx: u32) -> Option<&[OutputPart]> {
81        self.fragments.get(idx as usize).map(|f| f.parts.as_slice())
82    }
83
84    /// Resolve a fragment's parts against the current line tables.
85    pub fn resolve_fragment(
86        &self,
87        idx: u32,
88        program: &Program,
89        line_tables: &[Vec<LineEntry>],
90        resolver: Option<&dyn PluralResolver>,
91    ) -> String {
92        match self.fragment(idx) {
93            Some(parts) => resolve_parts(parts, program, line_tables, resolver, &self.fragments),
94            None => String::new(),
95        }
96    }
97}