use alloc::string::String;
use alloc::vec::Vec;
use brink_format::{LineEntry, PluralResolver};
use super::{OutputBuffer, OutputPart, resolve_parts};
use crate::program::Program;
#[derive(Debug, Clone, PartialEq)]
pub struct Fragment {
pub parts: Vec<OutputPart>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FragmentRef<'a> {
pub parts: &'a [OutputPart],
pub tags: &'a [String],
}
#[derive(Debug, Clone, Default)]
struct FragmentSpan {
start: u32,
end: u32,
tags: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Fragments {
arena: Vec<OutputPart>,
spans: Vec<FragmentSpan>,
}
impl Fragments {
#[must_use]
pub fn len(&self) -> usize {
self.spans.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.spans.is_empty()
}
#[must_use]
pub fn parts(&self, idx: u32) -> Option<&[OutputPart]> {
let span = self.spans.get(idx as usize)?;
self.arena.get(span.start as usize..span.end as usize)
}
#[must_use]
pub fn tags(&self, idx: u32) -> Option<&[String]> {
self.spans.get(idx as usize).map(|s| s.tags.as_slice())
}
#[must_use]
pub fn get(&self, idx: u32) -> Option<FragmentRef<'_>> {
Some(FragmentRef {
parts: self.parts(idx)?,
tags: self.tags(idx)?,
})
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = FragmentRef<'_>> + '_ {
self.spans.iter().map(|span| FragmentRef {
parts: &self.arena[span.start as usize..span.end as usize],
tags: &span.tags,
})
}
#[must_use]
pub fn to_vec(&self) -> Vec<Fragment> {
self.iter()
.map(|f| Fragment {
parts: f.parts.to_vec(),
tags: f.tags.to_vec(),
})
.collect()
}
#[expect(clippy::cast_possible_truncation)]
pub(crate) fn push(
&mut self,
parts: impl Iterator<Item = OutputPart>,
tags: Vec<String>,
) -> u32 {
let start = self.arena.len() as u32;
self.arena.extend(parts);
let end = self.arena.len() as u32;
let idx = self.spans.len() as u32;
self.spans.push(FragmentSpan { start, end, tags });
idx
}
}
impl From<Vec<Fragment>> for Fragments {
fn from(fragments: Vec<Fragment>) -> Self {
let mut store = Self::default();
for Fragment { parts, tags } in fragments {
store.push(parts.into_iter(), tags);
}
store
}
}
impl OutputBuffer {
pub fn begin_fragment(&mut self) {
self.fragment_depth += 1;
self.fragment_capture.push(OutputPart::Checkpoint);
self.fragment_pending_tags.push(Vec::new());
}
pub fn end_fragment(&mut self) -> Option<u32> {
let cp_idx = self
.fragment_capture
.iter()
.rposition(|p| matches!(p, OutputPart::Checkpoint))?;
let tags = self.fragment_pending_tags.pop().unwrap_or_default();
let idx = self
.fragments
.push(self.fragment_capture.drain(cp_idx + 1..), tags);
self.fragment_capture.pop();
self.fragment_depth = self.fragment_depth.saturating_sub(1);
Some(idx)
}
pub fn in_fragment_capture(&self) -> bool {
self.fragment_depth > 0
}
pub fn push_fragment_tag(&mut self, tag: String) {
if let Some(pending) = self.fragment_pending_tags.last_mut() {
pending.push(tag);
}
}
pub fn fragment_tags(&self, idx: u32) -> Option<&[String]> {
self.fragments.tags(idx)
}
pub fn fragments(&self) -> &Fragments {
&self.fragments
}
pub fn fragment(&self, idx: u32) -> Option<&[OutputPart]> {
self.fragments.parts(idx)
}
pub fn fragment_source(
&self,
idx: u32,
program: &Program,
line_tables: &[Vec<LineEntry>],
) -> Option<brink_format::SourceLocation> {
self.fragment(idx)?.iter().find_map(|part| match part {
OutputPart::LineRef {
container_idx,
line_idx,
..
} => {
let scope_idx = program.scope_table_idx(*container_idx) as usize;
line_tables
.get(scope_idx)
.and_then(|t| t.get(*line_idx as usize))
.and_then(|entry| entry.source_location.clone())
}
_ => None,
})
}
pub fn resolve_fragment(
&self,
idx: u32,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
) -> String {
match self.fragment(idx) {
Some(parts) => resolve_parts(parts, program, line_tables, resolver, &self.fragments),
None => String::new(),
}
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec;
use super::*;
fn text(s: &str) -> OutputPart {
OutputPart::Text(s.to_string())
}
#[test]
fn nested_captures_share_the_arena_with_disjoint_spans() {
let mut buf = OutputBuffer::new();
buf.begin_fragment();
buf.push_text("outer-a");
buf.begin_fragment();
buf.push_text("inner");
buf.push_fragment_tag("t".to_string());
let inner = buf.end_fragment().expect("inner capture open");
buf.push_text("outer-b");
let outer = buf.end_fragment().expect("outer capture open");
assert_eq!((inner, outer), (0, 1));
assert_eq!(buf.fragments().len(), 2);
assert_eq!(buf.fragment(inner), Some(&[text("inner")][..]));
assert_eq!(buf.fragment_tags(inner), Some(&["t".to_string()][..]));
assert_eq!(
buf.fragment(outer),
Some(&[text("outer-a"), text("outer-b")][..])
);
assert_eq!(buf.fragment_tags(outer), Some(&[][..]));
assert!(buf.fragment(2).is_none());
assert!(!buf.in_fragment_capture());
}
#[test]
fn owning_form_round_trips_through_the_store() {
let owned = vec![
Fragment {
parts: vec![text("a"), OutputPart::Newline],
tags: vec!["x".to_string()],
},
Fragment {
parts: vec![],
tags: vec![],
},
Fragment {
parts: vec![OutputPart::Spring, text("c")],
tags: vec!["y".to_string(), "z".to_string()],
},
];
let store = Fragments::from(owned.clone());
assert_eq!(store.len(), 3);
assert_eq!(store.parts(1), Some(&[][..]));
assert_eq!(store.get(2).map(|f| f.tags.len()), Some(2));
assert_eq!(store.to_vec(), owned);
assert_eq!(store.iter().count(), 3);
}
}