use core::mem;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use brink_format::{
LineContent, LineEntry, LinePart, PluralCategory, PluralResolver, SelectKey, Value,
};
use crate::program::Program;
use crate::value_ops;
mod consume;
mod fragment;
pub use fragment::Fragment;
#[derive(Debug, Clone, PartialEq)]
pub enum OutputPart {
Text(String),
LineRef {
container_idx: u32,
line_idx: u16,
slots: Vec<Value>,
flags: brink_format::LineFlags,
},
ValueRef(Value),
Newline,
Spring,
Glue,
Checkpoint,
Tag(String),
ElementAttach(String, String),
ElementAttachEnd,
}
impl OutputPart {
pub fn resolve(
&self,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
) -> String {
resolve_part(self, program, line_tables, resolver, &[])
}
fn is_content(&self) -> bool {
match self {
Self::Text(s) => !s.trim().is_empty(),
Self::LineRef { flags, .. } => {
!flags.contains(brink_format::LineFlags::ALL_WS)
&& !flags.contains(brink_format::LineFlags::EMPTY)
}
Self::ValueRef(Value::OptionVal(None)) => false,
Self::ValueRef(_) => true,
_ => false,
}
}
}
fn resolve_part(
part: &OutputPart,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &[Fragment],
) -> String {
match part {
OutputPart::Text(s) => s.clone(),
OutputPart::LineRef {
container_idx,
line_idx,
slots,
..
} => resolve_line_ref(
program,
line_tables,
*container_idx,
*line_idx,
slots,
resolver,
fragments,
),
OutputPart::ValueRef(Value::FragmentRef(idx)) => {
let idx = *idx as usize;
if let Some(frag) = fragments.get(idx) {
resolve_parts(&frag.parts, program, line_tables, resolver, fragments)
} else {
String::new()
}
}
OutputPart::ValueRef(val) => value_ops::stringify_display(val, program),
OutputPart::Newline
| OutputPart::Spring
| OutputPart::Glue
| OutputPart::Checkpoint
| OutputPart::Tag(_)
| OutputPart::ElementAttach(..)
| OutputPart::ElementAttachEnd => String::new(),
}
}
fn resolve_line_ref(
program: &Program,
line_tables: &[Vec<LineEntry>],
container_idx: u32,
line_idx: u16,
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
fragments: &[Fragment],
) -> String {
let scope_idx = program.scope_table_idx(container_idx) as usize;
let lines = &line_tables[scope_idx];
let Some(entry) = lines.get(line_idx as usize) else {
return String::new();
};
match &entry.content {
LineContent::Plain(s) => s.clone(),
LineContent::Template(parts) => {
resolve_line_parts(parts, program, line_tables, slots, resolver, fragments)
}
}
}
fn resolve_line_parts(
parts: &[LinePart],
program: &Program,
line_tables: &[Vec<LineEntry>],
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
fragments: &[Fragment],
) -> String {
let mut result = String::new();
for part in parts {
let owned;
let fragment: &str = match part {
LinePart::Literal(s) => s.as_str(),
LinePart::Slot(n) => {
owned = slots
.get(*n as usize)
.map(|v| match v {
Value::FragmentRef(idx) => {
let idx = *idx as usize;
fragments.get(idx).map_or_else(String::new, |frag| {
resolve_parts(
&frag.parts,
program,
line_tables,
resolver,
fragments,
)
})
}
other => value_ops::stringify_display(other, program),
})
.unwrap_or_default();
owned.as_str()
}
LinePart::Select {
slot,
variants,
default,
} => {
owned = resolve_select(*slot, variants, default, slots, resolver).to_string();
owned.as_str()
}
LinePart::Span { children, .. } => {
owned =
resolve_line_parts(children, program, line_tables, slots, resolver, fragments);
owned.as_str()
}
};
if fragment.is_empty() {
continue;
}
if (result.is_empty() || result.ends_with(' ')) && fragment.starts_with(' ') {
result.push_str(fragment.trim_start());
} else {
result.push_str(fragment);
}
}
result
}
fn resolve_select<'a>(
slot: u8,
variants: &'a [(SelectKey, String)],
default: &'a str,
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
) -> &'a str {
let Some(val) = slots.get(slot as usize) else {
return default;
};
#[expect(clippy::cast_possible_truncation)]
let n: Option<i64> = match val {
Value::Int(i) => Some(i64::from(*i)),
Value::Float(f) => Some(*f as i64),
_ => None,
};
if let Some(n) = n {
#[expect(clippy::cast_possible_truncation)]
let n32 = n as i32;
for (key, text) in variants {
if let SelectKey::Exact(e) = key
&& *e == n32
{
return text;
}
}
}
if let Value::String(s) = val {
for (key, text) in variants {
if let SelectKey::Keyword(k) = key
&& k == s.as_ref()
{
return text;
}
}
}
if let (Some(n), Some(r)) = (n, resolver) {
let cardinal: PluralCategory = r.cardinal(n, None);
for (key, text) in variants {
if let SelectKey::Cardinal(cat) = key
&& *cat == cardinal
{
return text;
}
}
let ordinal: PluralCategory = r.ordinal(n);
for (key, text) in variants {
if let SelectKey::Ordinal(cat) = key
&& *cat == ordinal
{
return text;
}
}
}
default
}
#[derive(Debug, Clone)]
pub(crate) struct OutputBuffer {
pub(crate) transcript: Vec<OutputPart>,
pub(crate) cursor: usize,
capture: Vec<OutputPart>,
capture_depth: usize,
fragments: Vec<Fragment>,
fragment_capture: Vec<OutputPart>,
fragment_depth: usize,
fragment_pending_tags: Vec<Vec<String>>,
pending_element: BTreeMap<String, String>,
}
impl OutputBuffer {
pub fn new() -> Self {
Self {
transcript: Vec::new(),
cursor: 0,
capture: Vec::new(),
capture_depth: 0,
fragments: Vec::new(),
fragment_capture: Vec::new(),
fragment_depth: 0,
fragment_pending_tags: Vec::new(),
pending_element: BTreeMap::new(),
}
}
fn target(&mut self) -> &mut Vec<OutputPart> {
if self.capture_depth > 0 {
&mut self.capture
} else if self.fragment_depth > 0 {
&mut self.fragment_capture
} else {
&mut self.transcript
}
}
pub(crate) fn target_len(&self) -> usize {
if self.capture_depth > 0 {
self.capture.len()
} else if self.fragment_depth > 0 {
self.fragment_capture.len()
} else {
self.transcript.len()
}
}
pub(crate) fn trim_function_end(&mut self, start: usize) {
let target = self.target();
while target.len() > start {
match target.last() {
Some(OutputPart::Newline | OutputPart::Spring) => {
target.pop();
}
Some(OutputPart::Text(s)) if s.trim().is_empty() => {
target.pop();
}
Some(OutputPart::LineRef { flags, .. })
if flags.contains(brink_format::LineFlags::ALL_WS) =>
{
target.pop();
}
_ => break,
}
}
}
#[cfg(test)]
pub fn push_text(&mut self, text: &str) {
if text.is_empty() {
return;
}
if !self.has_content() && text.trim().is_empty() {
return;
}
let text = if text.starts_with(char::is_whitespace) && self.ends_in_whitespace() {
text.trim_start()
} else {
text
};
if !text.is_empty() {
self.target().push(OutputPart::Text(text.to_owned()));
}
}
pub fn push_newline(&mut self) {
let has_content = if self.capture_depth > 0 || self.fragment_depth > 0 {
self.has_content()
} else {
self.unread_has_content_or_spring()
};
if !has_content || self.ends_in_newline() {
return;
}
self.target().push(OutputPart::Newline);
}
fn has_content(&self) -> bool {
if self.capture_depth > 0 {
self.capture
.iter()
.rev()
.take_while(|p| !matches!(p, OutputPart::Checkpoint))
.any(OutputPart::is_content)
} else if self.fragment_depth > 0 {
self.fragment_capture
.iter()
.rev()
.take_while(|p| !matches!(p, OutputPart::Checkpoint))
.any(OutputPart::is_content)
} else {
self.transcript[self.cursor..]
.iter()
.rev()
.any(OutputPart::is_content)
}
}
fn unread_has_content_or_spring(&self) -> bool {
self.transcript[self.cursor..]
.iter()
.any(|p| p.is_content() || matches!(p, OutputPart::Spring))
}
fn ends_in_newline(&self) -> bool {
let target = if self.capture_depth > 0 {
&self.capture
} else if self.fragment_depth > 0 {
&self.fragment_capture
} else {
&self.transcript
};
matches!(target.last(), Some(OutputPart::Newline))
}
#[cfg(test)]
fn ends_in_whitespace(&self) -> bool {
let target = if self.capture_depth > 0 {
&self.capture
} else if self.fragment_depth > 0 {
&self.fragment_capture
} else {
&self.transcript
};
matches!(target.last(), Some(OutputPart::Text(s)) if s.ends_with(char::is_whitespace))
}
pub fn push_glue(&mut self) {
self.target().push(OutputPart::Glue);
}
pub fn push_spring(&mut self) {
let target = self.target();
if !matches!(target.last(), Some(OutputPart::Spring)) {
target.push(OutputPart::Spring);
}
}
pub fn push_line_ref(
&mut self,
container_idx: u32,
line_idx: u16,
slots: Vec<Value>,
flags: brink_format::LineFlags,
) {
if !self.has_content()
&& (flags.contains(brink_format::LineFlags::ALL_WS)
|| flags.contains(brink_format::LineFlags::EMPTY))
{
return;
}
self.target().push(OutputPart::LineRef {
container_idx,
line_idx,
slots,
flags,
});
}
pub fn push_value_ref(&mut self, value: Value) {
if matches!(value, Value::Null) {
return;
}
if !self.has_content()
&& let Value::String(ref s) = value
&& s.trim().is_empty()
{
return;
}
self.target().push(OutputPart::ValueRef(value));
}
pub fn push_tag(&mut self, tag: String) {
self.target().push(OutputPart::Tag(tag));
}
pub(crate) fn push_element_attach(&mut self, key: String, value: String) {
self.target().push(OutputPart::ElementAttach(key, value));
}
pub(crate) fn push_element_attach_end(&mut self) {
self.target().push(OutputPart::ElementAttachEnd);
}
#[cfg_attr(not(feature = "effect-trace"), expect(dead_code))]
pub fn in_capture(&self) -> bool {
self.capture_depth > 0
}
pub fn has_checkpoint(&self) -> bool {
self.capture_depth > 0
}
pub fn begin_capture(&mut self) {
self.capture_depth += 1;
self.capture.push(OutputPart::Checkpoint);
}
pub fn end_capture(
&mut self,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
) -> Option<String> {
let cp_idx = self
.capture
.iter()
.rposition(|p| matches!(p, OutputPart::Checkpoint))?;
let captured: Vec<OutputPart> = self.capture.drain(cp_idx..).collect();
let captured = &captured[1..];
self.capture_depth = self.capture_depth.saturating_sub(1);
Some(resolve_parts(
captured,
program,
line_tables,
resolver,
&self.fragments,
))
}
}
fn mark_glue_removals(parts: &[OutputPart], remove: &mut [bool]) {
for (i, part) in parts.iter().enumerate() {
if matches!(part, OutputPart::Glue) {
for j in (0..i).rev() {
if remove[j] {
continue;
}
match &parts[j] {
OutputPart::Newline => {
remove[j] = true;
break;
}
OutputPart::Glue
| OutputPart::Checkpoint
| OutputPart::Tag(_)
| OutputPart::Spring
| OutputPart::ElementAttach(..)
| OutputPart::ElementAttachEnd
| OutputPart::ValueRef(Value::OptionVal(None)) => {}
OutputPart::Text(s) if s.trim().is_empty() => {}
OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
break;
}
}
}
remove[i] = true;
}
}
}
fn resolve_parts(
parts: &[OutputPart],
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &[Fragment],
) -> String {
let mut remove = vec![false; parts.len()];
mark_glue_removals(parts, &mut remove);
let mut out = String::new();
let mut after_glue = false;
let mut line_start = 0usize;
let mut saw_fragment_ref = false;
for (i, part) in parts.iter().enumerate() {
if remove[i] {
if matches!(part, OutputPart::Glue) {
after_glue = true;
}
continue;
}
match part {
OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
if part_involves_fragment_ref(part) {
saw_fragment_ref = true;
}
let s = resolve_part(part, program, line_tables, resolver, fragments);
let s = if s.starts_with(char::is_whitespace) && out.ends_with(char::is_whitespace)
{
s.trim_start()
} else {
&s
};
out.push_str(s);
if !s.trim().is_empty() {
after_glue = false;
}
}
OutputPart::Spring => {
if !out.is_empty() && !out.ends_with(' ') && !out.ends_with('\n') {
out.push(' ');
}
}
OutputPart::Newline => {
if !after_glue {
let trimmed_len = out.trim_end_matches([' ', '\t']).len();
out.truncate(trimmed_len);
if saw_fragment_ref && out[line_start..].trim().is_empty() {
out.truncate(line_start);
} else {
out.push('\n');
line_start = out.len();
}
saw_fragment_ref = false;
}
}
OutputPart::Glue
| OutputPart::Checkpoint
| OutputPart::Tag(_)
| OutputPart::ElementAttach(..)
| OutputPart::ElementAttachEnd => {
after_glue = true;
}
}
}
if saw_fragment_ref && line_start > 0 && out[line_start..].trim().is_empty() {
out.truncate(line_start - 1);
}
out
}
fn part_involves_fragment_ref(part: &OutputPart) -> bool {
match part {
OutputPart::LineRef { slots, .. } => {
slots.iter().any(|v| matches!(v, Value::FragmentRef(_)))
}
OutputPart::ValueRef(Value::FragmentRef(_)) => true,
_ => false,
}
}
pub(crate) type ResolvedLine = (String, Vec<String>, BTreeMap<String, String>);
pub(crate) type AnnotatedResolvedLine = (String, Vec<String>, bool, BTreeMap<String, String>);
pub(crate) fn resolve_lines(
parts: &[OutputPart],
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &[Fragment],
) -> Vec<ResolvedLine> {
resolve_lines_annotated(
parts,
BTreeMap::new(),
program,
line_tables,
resolver,
fragments,
)
.into_iter()
.filter_map(|(text, tags, suppressed, element)| (!suppressed).then_some((text, tags, element)))
.collect()
}
pub(crate) fn resolve_lines_annotated(
parts: &[OutputPart],
seed_element: BTreeMap<String, String>,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &[Fragment],
) -> Vec<AnnotatedResolvedLine> {
if parts.is_empty() {
return Vec::new();
}
let mut remove = vec![false; parts.len()];
mark_glue_removals(parts, &mut remove);
let mut lines: Vec<AnnotatedResolvedLine> = Vec::new();
let mut current_text = String::new();
let mut current_tags: Vec<String> = Vec::new();
let mut current_element: BTreeMap<String, String> = seed_element;
let mut saw_fragment_ref = false;
let mut after_glue = false;
for (i, part) in parts.iter().enumerate() {
if remove[i] {
if matches!(part, OutputPart::Glue) {
after_glue = true;
}
continue;
}
match part {
OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
if part_involves_fragment_ref(part) {
saw_fragment_ref = true;
}
let s = resolve_part(part, program, line_tables, resolver, fragments);
let s = if s.starts_with(char::is_whitespace)
&& current_text.ends_with(char::is_whitespace)
{
s.trim_start()
} else {
&s
};
current_text.push_str(s);
if !s.trim().is_empty() {
after_glue = false;
}
}
OutputPart::Spring => {
if !current_text.is_empty()
&& !current_text.ends_with(' ')
&& !current_text.ends_with('\n')
{
current_text.push(' ');
}
}
OutputPart::Newline => {
if !after_glue {
let trimmed = current_text.trim().to_string();
let suppressed =
trimmed.is_empty() && current_tags.is_empty() && saw_fragment_ref;
lines.push((
trimmed,
mem::take(&mut current_tags),
suppressed,
current_element.clone(),
));
current_text = String::new();
saw_fragment_ref = false;
}
}
OutputPart::Tag(tag) => {
current_tags.push(tag.clone());
}
OutputPart::ElementAttach(key, value) => {
current_element.insert(key.clone(), value.clone());
}
OutputPart::ElementAttachEnd => {
current_element.clear();
}
OutputPart::Glue | OutputPart::Checkpoint => {
after_glue = true;
}
}
}
let trimmed = current_text.trim().to_string();
let suppressed = trimmed.is_empty() && current_tags.is_empty() && saw_fragment_ref;
lines.push((trimmed, current_tags, suppressed, current_element));
lines
}
#[cfg(test)]
fn test_dummy_program() -> Program {
use std::collections::HashMap;
Program {
containers: vec![],
address_map: HashMap::new(),
scope_ids: vec![],
source_checksum: 0,
globals: vec![],
global_map: HashMap::new(),
name_table: vec![],
address_by_path: HashMap::new(),
root_idx: 0,
list_literals: vec![],
literal_pool: vec![],
list_item_map: HashMap::new(),
list_defs: vec![],
list_def_map: HashMap::new(),
external_fns: HashMap::new(),
local_scope_defaults: Vec::new(),
struct_shapes: Vec::new(),
private_defs: Vec::new(),
alias_table: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
impl OutputBuffer {
fn test_flush_lines(&mut self) -> Vec<(String, Vec<String>)> {
let p = test_dummy_program();
self.flush_lines(&p, &[], None)
.into_iter()
.map(|(text, tags, _element)| (text, tags))
.collect()
}
fn test_take_first_line(&mut self) -> Option<(String, Vec<String>)> {
let p = test_dummy_program();
self.take_first_line(&p, &[], None)
.map(|(text, tags, _element)| (text, tags))
}
fn test_end_capture(&mut self) -> Option<String> {
let p = test_dummy_program();
self.end_capture(&p, &[], None)
}
}
#[test]
fn simple_text() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
assert_eq!(buf.flush(), "hello");
}
#[test]
fn text_with_newline() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_text("world");
assert_eq!(buf.flush(), "hello\nworld");
}
#[test]
fn glue_removes_newline() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_glue();
buf.push_text("world");
assert_eq!(buf.flush(), "helloworld");
}
#[test]
fn glue_preserves_leading_whitespace_in_text() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_glue();
buf.push_text(" world");
assert_eq!(buf.flush(), "hello world");
}
#[test]
fn double_flush_is_empty() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
let _ = buf.flush();
assert_eq!(buf.flush(), "");
}
#[test]
fn leading_newline_suppressed() {
let mut buf = OutputBuffer::new();
buf.push_newline();
buf.push_text("hello");
assert_eq!(buf.flush(), "hello");
}
#[test]
fn leading_whitespace_only_text_suppressed() {
let mut buf = OutputBuffer::new();
buf.push_text(" ");
buf.push_text("hello");
assert_eq!(buf.flush(), "hello");
}
#[test]
fn adjacent_whitespace_collapsed() {
let mut buf = OutputBuffer::new();
buf.push_text("Hello ");
buf.push_text(" right back");
assert_eq!(buf.flush(), "Hello right back");
}
#[test]
fn leading_whitespace_after_flush_suppressed() {
let mut buf = OutputBuffer::new();
buf.push_text("first");
let _ = buf.flush();
buf.push_text(" ");
buf.push_text("second");
assert_eq!(buf.flush(), "second");
}
#[test]
fn duplicate_newline_suppressed() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_newline();
buf.push_text("world");
assert_eq!(buf.flush(), "hello\nworld");
}
#[test]
fn leading_newline_after_flush_suppressed() {
let mut buf = OutputBuffer::new();
buf.push_text("first");
let _ = buf.flush();
buf.push_newline();
buf.push_text("second");
assert_eq!(buf.flush(), "second");
}
#[test]
fn begin_end_capture_basic() {
let mut buf = OutputBuffer::new();
buf.push_text("before");
buf.begin_capture();
buf.push_text("captured");
let result = buf.test_end_capture();
assert_eq!(result, Some("captured".to_owned()));
assert_eq!(buf.flush(), "before");
}
#[test]
fn nested_captures() {
let mut buf = OutputBuffer::new();
buf.push_text("outer");
buf.begin_capture();
buf.push_text("middle");
buf.begin_capture();
buf.push_text("inner");
let inner = buf.test_end_capture();
assert_eq!(inner, Some("inner".to_owned()));
let middle = buf.test_end_capture();
assert_eq!(middle, Some("middle".to_owned()));
assert_eq!(buf.flush(), "outer");
}
#[test]
fn capture_with_glue() {
let mut buf = OutputBuffer::new();
buf.begin_capture();
buf.push_text("hello");
buf.push_newline();
buf.push_glue();
buf.push_text(" world");
let result = buf.test_end_capture();
assert_eq!(result, Some("hello world".to_owned()));
}
#[test]
fn end_capture_no_checkpoint_returns_none() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
assert_eq!(buf.test_end_capture(), None);
}
#[test]
fn has_content_respects_checkpoint() {
let mut buf = OutputBuffer::new();
buf.push_text("before");
buf.begin_capture();
assert!(!buf.has_content());
buf.push_text("after");
assert!(buf.has_content());
}
#[test]
fn glue_eats_following_newline() {
let mut buf = OutputBuffer::new();
buf.push_text("fifty");
buf.push_newline();
buf.push_glue();
buf.push_text("-");
buf.push_glue();
buf.push_newline();
buf.push_text("eight");
assert_eq!(buf.flush(), "fifty-eight");
}
#[test]
fn trailing_whitespace_before_newline_trimmed() {
let mut buf = OutputBuffer::new();
buf.push_text("A ");
buf.push_newline();
buf.push_text("X");
assert_eq!(buf.flush(), "A\nX");
}
#[test]
fn glue_preserves_text_whitespace() {
let mut buf = OutputBuffer::new();
buf.push_text("Some ");
buf.push_glue();
buf.push_newline();
buf.push_text("content");
buf.push_glue();
buf.push_text(" with glue.");
assert_eq!(buf.flush(), "Some content with glue.");
}
#[test]
fn glue_skips_whitespace_only_text_to_find_newline() {
let mut buf = OutputBuffer::new();
buf.push_text("a");
buf.push_newline();
buf.push_text(" ");
buf.push_glue();
buf.push_text("b");
assert_eq!(buf.flush(), "a b");
}
#[test]
fn flush_lines_associates_tags_with_lines() {
let mut buf = OutputBuffer::new();
buf.push_text("line one");
buf.push_newline();
buf.push_text("line two");
buf.push_tag("my_tag".to_string());
buf.push_newline();
buf.push_text("line three");
let lines = buf.test_flush_lines();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].0, "line one");
assert!(lines[0].1.is_empty());
assert_eq!(lines[1].0, "line two");
assert_eq!(lines[1].1, vec!["my_tag"]);
assert_eq!(lines[2].0, "line three");
assert!(lines[2].1.is_empty());
}
#[test]
fn flush_lines_tag_on_last_line() {
let mut buf = OutputBuffer::new();
buf.push_text("only line");
buf.push_tag("t".to_string());
let lines = buf.test_flush_lines();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].0, "only line");
assert_eq!(lines[0].1, vec!["t"]);
}
#[test]
fn flush_lines_resolves_glue() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_glue();
buf.push_text(" world");
let lines = buf.test_flush_lines();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].0, "hello world");
}
#[test]
fn flush_lines_empty_buffer_returns_no_lines() {
let mut buf = OutputBuffer::new();
let lines = buf.test_flush_lines();
assert!(
lines.is_empty(),
"empty buffer should produce no lines, got: {lines:?}"
);
}
#[test]
fn has_completed_line_empty() {
let buf = OutputBuffer::new();
assert!(!buf.has_completed_line());
}
#[test]
fn has_completed_line_text_only() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
assert!(!buf.has_completed_line());
}
#[test]
fn has_completed_line_text_newline_only() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
assert!(!buf.has_completed_line());
}
#[test]
fn has_completed_line_text_newline_text() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_text("world");
assert!(buf.has_completed_line());
}
#[test]
fn has_completed_line_glue_eats_newline() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_glue();
buf.push_text("world");
assert!(!buf.has_completed_line());
}
#[test]
fn has_completed_line_during_capture() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_text("world");
buf.begin_capture();
assert!(!buf.has_completed_line());
}
#[test]
fn take_first_line_basic() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_text("world");
let result = buf.test_take_first_line();
assert!(result.is_some());
let (text, tags) = result.unwrap();
assert_eq!(text, "hello\n");
assert!(tags.is_empty());
assert_eq!(buf.flush(), "world");
}
#[test]
fn take_first_line_with_tags() {
let mut buf = OutputBuffer::new();
buf.push_text("tagged line");
buf.push_tag("my_tag".to_string());
buf.push_newline();
buf.push_text("next line");
let (text, tags) = buf.test_take_first_line().unwrap();
assert_eq!(text, "tagged line\n");
assert_eq!(tags, vec!["my_tag"]);
assert_eq!(buf.flush(), "next line");
}
#[test]
fn take_first_line_multiple_lines() {
let mut buf = OutputBuffer::new();
buf.push_text("line one");
buf.push_newline();
buf.push_text("line two");
buf.push_newline();
buf.push_text("line three");
let (text1, _) = buf.test_take_first_line().unwrap();
assert_eq!(text1, "line one\n");
let (text2, _) = buf.test_take_first_line().unwrap();
assert_eq!(text2, "line two\n");
assert!(!buf.has_completed_line());
assert_eq!(buf.flush(), "line three");
}
#[test]
fn take_first_line_matches_flush_lines() {
let parts = |buf: &mut OutputBuffer| {
buf.push_text("A ");
buf.push_tag("t1".to_string());
buf.push_newline();
buf.push_text("B");
buf.push_newline();
buf.push_text("C");
};
let mut buf1 = OutputBuffer::new();
parts(&mut buf1);
let all_lines = buf1.test_flush_lines();
let first_from_flush = &all_lines[0].0;
let mut buf2 = OutputBuffer::new();
parts(&mut buf2);
let (first_from_take, tags) = buf2.test_take_first_line().unwrap();
let first_trimmed = first_from_take.trim_end_matches('\n');
assert_eq!(first_trimmed, first_from_flush);
assert_eq!(tags, all_lines[0].1);
}
#[test]
fn take_first_line_glue_preserves_subsequent() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_glue();
buf.push_text(" world");
buf.push_newline();
buf.push_text("next");
let (text, _) = buf.test_take_first_line().unwrap();
assert_eq!(text, "hello world\n");
assert_eq!(buf.flush(), "next");
}
#[test]
fn take_first_line_none_when_empty() {
let mut buf = OutputBuffer::new();
assert!(buf.test_take_first_line().is_none());
}
#[test]
fn take_first_line_none_when_no_newline() {
let mut buf = OutputBuffer::new();
buf.push_text("no newline");
assert!(buf.test_take_first_line().is_none());
}
fn resolve_template(parts: Vec<LinePart>, slots: &[Value]) -> String {
use crate::program::LinkedContainer;
use brink_format::{CountingFlags, DefinitionId, DefinitionTag, LineEntry, LineFlags};
use std::collections::HashMap;
let id = DefinitionId::new(DefinitionTag::Address, 0);
let program = Program {
containers: vec![LinkedContainer {
id,
bytecode: vec![],
counting_flags: CountingFlags::empty(),
path_hash: 0,
param_count: 0,
params: Vec::new(),
scope_table_idx: 0,
}],
address_map: HashMap::new(),
scope_ids: vec![id],
source_checksum: 0,
globals: vec![],
global_map: HashMap::new(),
name_table: vec![],
address_by_path: HashMap::new(),
root_idx: 0,
list_literals: vec![],
literal_pool: vec![],
list_item_map: HashMap::new(),
list_defs: vec![],
list_def_map: HashMap::new(),
external_fns: HashMap::new(),
local_scope_defaults: Vec::new(),
struct_shapes: Vec::new(),
private_defs: Vec::new(),
alias_table: Vec::new(),
};
let line_tables = vec![vec![LineEntry {
content: LineContent::Template(parts),
source_hash: 0,
flags: LineFlags::empty(),
audio_ref: None,
slot_info: vec![],
source_location: None,
}]];
resolve_line_ref(&program, &line_tables, 0, 0, slots, None, &[])
}
#[test]
fn template_collapses_double_space_from_empty_slot() {
let result = resolve_template(
vec![
LinePart::Literal("Hello ".into()),
LinePart::Slot(0),
LinePart::Literal(" world".into()),
],
&[Value::Null],
);
assert_eq!(result, "Hello world");
}
#[test]
fn template_preserves_spaces_with_nonempty_slot() {
let result = resolve_template(
vec![
LinePart::Literal("Hello ".into()),
LinePart::Slot(0),
LinePart::Literal(" world".into()),
],
&[Value::String("dear".into())],
);
assert_eq!(result, "Hello dear world");
}
#[test]
fn template_multiple_empty_slots_collapse() {
let result = resolve_template(
vec![
LinePart::Literal("a ".into()),
LinePart::Slot(0),
LinePart::Literal(" ".into()),
LinePart::Slot(1),
LinePart::Literal(" b".into()),
],
&[Value::Null, Value::Null],
);
assert_eq!(result, "a b");
}
#[test]
fn template_empty_string_slot_same_as_null() {
let result = resolve_template(
vec![
LinePart::Literal("Hello ".into()),
LinePart::Slot(0),
LinePart::Literal(" world".into()),
],
&[Value::String("".into())],
);
assert_eq!(result, "Hello world");
}
#[test]
fn span_resolves_to_its_children_text_tag_stripped() {
let result = resolve_template(
vec![
LinePart::Literal("Hello ".into()),
LinePart::Span {
name: "wave".into(),
attrs: vec![],
children: vec![LinePart::Literal("world".into())],
},
],
&[],
);
assert_eq!(result, "Hello world");
}
#[test]
fn a_self_closing_span_with_no_children_resolves_to_nothing() {
let result = resolve_template(
vec![
LinePart::Literal("Bell tolls. ".into()),
LinePart::Span {
name: "pause".into(),
attrs: vec![],
children: vec![],
},
LinePart::Literal(" Door slams.".into()),
],
&[],
);
assert_eq!(result, "Bell tolls. Door slams.");
}
#[test]
fn a_span_containing_a_slot_resolves_the_slot() {
let result = resolve_template(
vec![LinePart::Span {
name: "b".into(),
attrs: vec![],
children: vec![LinePart::Literal("hello ".into()), LinePart::Slot(0)],
}],
&[Value::String("Fogg".into())],
);
assert_eq!(result, "hello Fogg");
}
#[test]
fn nested_spans_resolve_recursively() {
let result = resolve_template(
vec![LinePart::Span {
name: "b".into(),
attrs: vec![],
children: vec![LinePart::Span {
name: "i".into(),
attrs: vec![],
children: vec![LinePart::Literal("hi".into())],
}],
}],
&[],
);
assert_eq!(result, "hi");
}
#[test]
fn template_none_option_slot_renders_as_nothing() {
let result = resolve_template(
vec![
LinePart::Literal("Hello ".into()),
LinePart::Slot(0),
LinePart::Literal(" world".into()),
],
&[Value::none()],
);
assert_eq!(result, "Hello world");
}
#[test]
fn template_some_option_slot_renders_totally() {
let result = resolve_template(
vec![LinePart::Literal("val: ".into()), LinePart::Slot(0)],
&[Value::some(Value::Int(3))],
);
assert_eq!(result, "val: some(3)");
}
#[test]
fn value_ref_none_option_renders_as_nothing() {
let mut buf = OutputBuffer::new();
buf.push_text("before ");
buf.push_value_ref(Value::none());
buf.push_text(" after");
assert_eq!(buf.flush(), "before after");
}
#[test]
fn none_render_is_traceable_in_the_raw_transcript() {
let mut buf = OutputBuffer::new();
buf.push_value_ref(Value::none());
assert!(
buf.transcript()
.iter()
.any(|p| matches!(p, OutputPart::ValueRef(Value::OptionVal(None)))),
"the raw None value must survive in the transcript: {:?}",
buf.transcript()
);
assert_eq!(buf.flush(), "");
}
#[test]
fn leading_none_option_value_does_not_block_newline_suppression() {
let mut buf = OutputBuffer::new();
buf.push_value_ref(Value::none());
buf.push_newline();
buf.push_text("hello");
assert_eq!(buf.flush(), "hello");
}
#[test]
fn none_option_value_does_not_block_glue_scan() {
let mut buf = OutputBuffer::new();
buf.push_text("hello");
buf.push_newline();
buf.push_value_ref(Value::none());
buf.push_glue();
buf.push_text("world");
assert_eq!(buf.flush(), "helloworld");
}
fn program_with_line_table(entries: Vec<LineEntry>) -> (Program, Vec<Vec<LineEntry>>) {
use crate::program::LinkedContainer;
use brink_format::{CountingFlags, DefinitionId, DefinitionTag};
use std::collections::HashMap;
let id = DefinitionId::new(DefinitionTag::Address, 0);
let program = Program {
containers: vec![LinkedContainer {
id,
bytecode: vec![],
counting_flags: CountingFlags::empty(),
path_hash: 0,
param_count: 0,
params: Vec::new(),
scope_table_idx: 0,
}],
address_map: HashMap::new(),
scope_ids: vec![id],
source_checksum: 0,
globals: vec![],
global_map: HashMap::new(),
name_table: vec![],
address_by_path: HashMap::new(),
root_idx: 0,
list_literals: vec![],
literal_pool: vec![],
list_item_map: HashMap::new(),
list_defs: vec![],
list_def_map: HashMap::new(),
external_fns: HashMap::new(),
local_scope_defaults: Vec::new(),
struct_shapes: Vec::new(),
private_defs: Vec::new(),
alias_table: Vec::new(),
};
(program, vec![entries])
}
fn plain_entry(s: &str) -> LineEntry {
LineEntry {
content: LineContent::Plain(s.to_string()),
source_hash: 0,
flags: brink_format::LineFlags::from_plain(s),
audio_ref: None,
slot_info: vec![],
source_location: None,
}
}
fn one_slot_template_entry() -> LineEntry {
LineEntry {
content: LineContent::Template(vec![LinePart::Slot(0)]),
source_hash: 0,
flags: brink_format::LineFlags::empty(),
audio_ref: None,
slot_info: vec![],
source_location: None,
}
}
fn line_ref(line_idx: u16, slots: Vec<Value>, flags: brink_format::LineFlags) -> OutputPart {
OutputPart::LineRef {
container_idx: 0,
line_idx,
slots,
flags,
}
}
#[test]
fn resolve_lines_suppresses_a_blank_line_from_an_empty_content_capture() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("VENDOR"),
one_slot_template_entry(),
plain_entry("(hushed)"),
]);
let fragments = vec![Fragment {
parts: vec![],
tags: vec![],
}];
let parts = vec![
line_ref(0, vec![], brink_format::LineFlags::from_plain("VENDOR")),
OutputPart::Newline,
line_ref(
1,
vec![Value::FragmentRef(0)],
brink_format::LineFlags::empty(),
),
OutputPart::Newline,
line_ref(2, vec![], brink_format::LineFlags::from_plain("(hushed)")),
];
let lines: Vec<(String, Vec<String>)> =
resolve_lines(&parts, &program, &line_tables, None, &fragments)
.into_iter()
.map(|(text, tags, _element)| (text, tags))
.collect();
assert_eq!(
lines,
vec![
("VENDOR".to_string(), Vec::<String>::new()),
("(hushed)".to_string(), Vec::<String>::new()),
],
"an empty content/Fragment capture must not render its own blank \
line between real content: {lines:?}"
);
}
#[test]
fn resolve_lines_suppresses_a_blank_line_from_an_empty_display_position_call_composition() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("Before."),
one_slot_template_entry(),
plain_entry("After."),
]);
let fragments = vec![Fragment {
parts: vec![],
tags: vec![],
}];
let parts = vec![
line_ref(0, vec![], brink_format::LineFlags::from_plain("Before.")),
OutputPart::Newline,
line_ref(
1,
vec![Value::FragmentRef(0)],
brink_format::LineFlags::empty(),
),
OutputPart::Newline,
line_ref(2, vec![], brink_format::LineFlags::from_plain("After.")),
];
let lines: Vec<(String, Vec<String>)> =
resolve_lines(&parts, &program, &line_tables, None, &fragments)
.into_iter()
.map(|(text, tags, _element)| (text, tags))
.collect();
assert_eq!(
lines,
vec![
("Before.".to_string(), Vec::<String>::new()),
("After.".to_string(), Vec::<String>::new()),
],
"an empty display-position call-composition FragmentRef must be \
suppressed identically to a block capture — this is the \
broader scope the discriminator actually covers, not just \
#1839's block-capture receiver: {lines:?}"
);
}
#[test]
fn resolve_lines_does_not_suppress_a_blank_line_from_a_non_fragment_empty_slot() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("VENDOR"),
one_slot_template_entry(),
plain_entry("(hushed)"),
]);
let fragments: Vec<Fragment> = vec![];
let parts = vec![
line_ref(0, vec![], brink_format::LineFlags::from_plain("VENDOR")),
OutputPart::Newline,
line_ref(1, vec![Value::Null], brink_format::LineFlags::empty()),
OutputPart::Newline,
line_ref(2, vec![], brink_format::LineFlags::from_plain("(hushed)")),
];
let lines: Vec<(String, Vec<String>)> =
resolve_lines(&parts, &program, &line_tables, None, &fragments)
.into_iter()
.map(|(text, tags, _element)| (text, tags))
.collect();
assert_eq!(
lines,
vec![
("VENDOR".to_string(), Vec::<String>::new()),
(String::new(), Vec::<String>::new()),
("(hushed)".to_string(), Vec::<String>::new()),
],
"a non-Fragment empty slot must keep rendering its blank line: {lines:?}"
);
}
#[test]
fn take_first_line_skips_a_suppressed_line_and_returns_the_next_real_line() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("VENDOR"),
one_slot_template_entry(),
plain_entry("(hushed)"),
]);
let mut buf = OutputBuffer::new();
buf.push_line_ref(0, 0, vec![], brink_format::LineFlags::from_plain("VENDOR"));
buf.push_newline();
buf.begin_fragment();
let frag_idx = buf.end_fragment().expect("checkpoint was just pushed");
buf.push_line_ref(
0,
1,
vec![Value::FragmentRef(frag_idx)],
brink_format::LineFlags::empty(),
);
buf.push_newline();
buf.push_line_ref(
0,
2,
vec![],
brink_format::LineFlags::from_plain("(hushed)"),
);
buf.push_newline();
let mut got = Vec::new();
for _ in 0..5 {
match buf.take_first_line(&program, &line_tables, None) {
Some((text, _, _)) => got.push(text),
None => break,
}
}
assert_eq!(
got,
vec!["VENDOR\n".to_string(), "(hushed)\n".to_string()],
"the empty content capture must not surface as its own \
(blank) streamed line: {got:?}"
);
}
#[test]
fn end_capture_suppresses_a_blank_line_from_an_empty_content_capture() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("VENDOR"),
one_slot_template_entry(),
plain_entry("(hushed)"),
]);
let mut buf = OutputBuffer::new();
buf.begin_fragment();
let frag_idx = buf.end_fragment().expect("checkpoint was just pushed");
buf.begin_capture();
buf.push_line_ref(0, 0, vec![], brink_format::LineFlags::from_plain("VENDOR"));
buf.push_newline();
buf.push_line_ref(
0,
1,
vec![Value::FragmentRef(frag_idx)],
brink_format::LineFlags::empty(),
);
buf.push_newline();
buf.push_line_ref(
0,
2,
vec![],
brink_format::LineFlags::from_plain("(hushed)"),
);
let text = buf
.end_capture(&program, &line_tables, None)
.expect("checkpoint was just pushed");
assert_eq!(
text, "VENDOR\n(hushed)",
"an empty content/Fragment capture inside a captured string \
must not leave a stray blank line — must match resolve_lines' \
suppression: {text:?}"
);
}
#[test]
fn end_capture_does_not_suppress_a_blank_line_from_a_non_fragment_empty_slot() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("VENDOR"),
one_slot_template_entry(),
plain_entry("(hushed)"),
]);
let mut buf = OutputBuffer::new();
buf.begin_capture();
buf.push_line_ref(0, 0, vec![], brink_format::LineFlags::from_plain("VENDOR"));
buf.push_newline();
buf.push_line_ref(0, 1, vec![Value::Null], brink_format::LineFlags::empty());
buf.push_newline();
buf.push_line_ref(
0,
2,
vec![],
brink_format::LineFlags::from_plain("(hushed)"),
);
let text = buf
.end_capture(&program, &line_tables, None)
.expect("checkpoint was just pushed");
assert_eq!(
text, "VENDOR\n\n(hushed)",
"a non-Fragment empty slot must keep its blank line inside a \
captured string: {text:?}"
);
}
#[test]
fn end_capture_drops_trailing_newline_before_an_unterminated_empty_fragment() {
let (program, line_tables) =
program_with_line_table(vec![plain_entry("a"), one_slot_template_entry()]);
let mut buf = OutputBuffer::new();
buf.begin_fragment();
let frag_idx = buf.end_fragment().expect("checkpoint was just pushed");
buf.begin_capture();
buf.push_line_ref(0, 0, vec![], brink_format::LineFlags::from_plain("a"));
buf.push_newline();
buf.push_line_ref(
0,
1,
vec![Value::FragmentRef(frag_idx)],
brink_format::LineFlags::empty(),
);
let text = buf
.end_capture(&program, &line_tables, None)
.expect("checkpoint was just pushed");
assert_eq!(
text, "a",
"an unterminated trailing empty Fragment interpolation must \
drop its introducing newline too, matching resolve_lines' \
final-entry suppression: {text:?}"
);
}
#[test]
fn resolve_fragment_suppresses_a_blank_line_from_a_nested_empty_fragment_interior() {
let (program, line_tables) = program_with_line_table(vec![
plain_entry("VENDOR"),
one_slot_template_entry(),
plain_entry("(hushed)"),
]);
let mut buf = OutputBuffer::new();
buf.begin_fragment();
let inner_idx = buf.end_fragment().expect("checkpoint was just pushed");
buf.begin_fragment();
buf.push_line_ref(0, 0, vec![], brink_format::LineFlags::from_plain("VENDOR"));
buf.push_newline();
buf.push_line_ref(
0,
1,
vec![Value::FragmentRef(inner_idx)],
brink_format::LineFlags::empty(),
);
buf.push_newline();
buf.push_line_ref(
0,
2,
vec![],
brink_format::LineFlags::from_plain("(hushed)"),
);
let outer_idx = buf.end_fragment().expect("checkpoint was just pushed");
let text = buf.resolve_fragment(outer_idx, &program, &line_tables, None);
assert_eq!(
text, "VENDOR\n(hushed)",
"a multi-line fragment's own interior must suppress a blank \
line from a nested, rendered-empty fragment the same way the \
top-level resolve_lines/end_capture paths do: {text:?}"
);
}
#[test]
fn flush_lines_writes_back_pending_element_past_the_closed_run() {
let p = test_dummy_program();
let mut buf = OutputBuffer::new();
buf.push_element_attach("speaker".to_string(), "VENDOR".to_string());
buf.push_text("Line one.");
buf.push_newline();
buf.push_text("Line two.");
buf.push_newline();
buf.push_element_attach_end();
let (first_text, _, first_element) = buf
.take_first_line(&p, &[], None)
.expect("first line of the attach run");
assert_eq!(first_text, "Line one.\n");
assert_eq!(
first_element.get("speaker").map(String::as_str),
Some("VENDOR")
);
let rest = buf.flush_lines(&p, &[], None);
let line_two = rest
.iter()
.find(|(text, ..)| text == "Line two.")
.expect("Line two. present in the flush");
assert_eq!(
line_two.2.get("speaker").map(String::as_str),
Some("VENDOR"),
"the last line of the run itself must still carry the attach data: {rest:?}"
);
buf.push_text("Unattached.");
buf.push_newline();
let (after_text, _, after_element) = buf
.take_first_line(&p, &[], None)
.expect("line after the closed run");
assert_eq!(after_text, "Unattached.\n");
assert!(
after_element.is_empty(),
"flush_lines must write pending_element back to empty once it \
consumes the run-closing ElementAttachEnd: {after_element:?}"
);
}
#[test]
fn reset_cursor_clears_pending_element() {
let p = test_dummy_program();
let mut buf = OutputBuffer::new();
buf.push_text("Intro.");
buf.push_newline();
buf.push_element_attach("speaker".to_string(), "VENDOR".to_string());
buf.push_text("Dialogue.");
buf.push_newline();
let (first_text, _, first_element) =
buf.take_first_line(&p, &[], None).expect("narration line");
assert_eq!(first_text, "Intro.\n");
assert!(first_element.is_empty(), "{first_element:?}");
let (second_text, _, second_element) =
buf.take_first_line(&p, &[], None).expect("dialogue line");
assert_eq!(second_text, "Dialogue.\n");
assert_eq!(
second_element.get("speaker").map(String::as_str),
Some("VENDOR")
);
buf.reset_cursor();
let (text_after_reset, _, element_after_reset) = buf
.take_first_line(&p, &[], None)
.expect("re-drained narration line after reset_cursor");
assert_eq!(text_after_reset, "Intro.\n");
assert!(
element_after_reset.is_empty(),
"reset_cursor must clear pending_element — no attach run has \
accumulated yet at index 0, so the re-drained leading line \
must not inherit the previous pass's speaker: \
{element_after_reset:?}"
);
}
}