use core::mem;
use alloc::collections::BTreeMap;
use alloc::string::String;
#[cfg(test)]
use alloc::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 completion;
mod consume;
mod fragment;
use completion::LineCompletion;
pub use fragment::{Fragment, FragmentRef, Fragments};
#[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, &Fragments::default())
}
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 is_visible(&self) -> bool {
match self {
Self::ValueRef(Value::String(s)) => !s.trim().is_empty(),
Self::ValueRef(Value::List(lv)) => !lv.items.is_empty(),
_ => self.is_content(),
}
}
}
fn resolve_part(
part: &OutputPart,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) -> String {
let mut out = String::new();
resolve_part_into(part, &mut out, program, line_tables, resolver, fragments);
out
}
fn resolve_part_into(
part: &OutputPart,
out: &mut String,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) {
match part {
OutputPart::Text(s) => {
out.reserve(s.len() + 1);
out.push_str(s);
}
OutputPart::LineRef {
container_idx,
line_idx,
slots,
..
} => resolve_line_ref_into(
out,
program,
line_tables,
*container_idx,
*line_idx,
slots,
resolver,
fragments,
),
OutputPart::ValueRef(Value::FragmentRef(idx)) => {
if let Some(parts) = fragments.parts(*idx) {
let s = resolve_parts(parts, program, line_tables, resolver, fragments);
out.push_str(&s);
}
}
OutputPart::ValueRef(val) => out.push_str(&value_ops::stringify_display(val, program)),
OutputPart::Newline
| OutputPart::Spring
| OutputPart::Glue
| OutputPart::Checkpoint
| OutputPart::Tag(_)
| OutputPart::ElementAttach(..)
| OutputPart::ElementAttachEnd => {}
}
}
fn collapse_join(out: &mut String, start: usize) -> bool {
let segment = &out[start..];
if segment.is_empty() {
return false;
}
let non_blank = !segment.trim().is_empty();
if segment.starts_with(char::is_whitespace) && out[..start].ends_with(char::is_whitespace) {
let lead = segment.len() - segment.trim_start().len();
out.replace_range(start..start + lead, "");
}
non_blank
}
#[cfg(test)]
fn resolve_line_ref(
program: &Program,
line_tables: &[Vec<LineEntry>],
container_idx: u32,
line_idx: u16,
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) -> String {
let mut out = String::new();
resolve_line_ref_into(
&mut out,
program,
line_tables,
container_idx,
line_idx,
slots,
resolver,
fragments,
);
out
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors `resolve_line_ref`'s parameter list"
)]
fn resolve_line_ref_into(
out: &mut String,
program: &Program,
line_tables: &[Vec<LineEntry>],
container_idx: u32,
line_idx: u16,
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) {
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;
};
match &entry.content {
LineContent::Plain(s) => {
out.reserve(s.len() + 1);
out.push_str(s);
}
LineContent::Template(parts) => {
resolve_line_parts_into(out, parts, program, line_tables, slots, resolver, fragments);
}
}
}
fn resolve_line_parts_into(
out: &mut String,
parts: &[LinePart],
program: &Program,
line_tables: &[Vec<LineEntry>],
slots: &[Value],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) {
let base = out.len();
for part in parts {
let start = out.len();
match part {
LinePart::Literal(s) => out.push_str(s),
LinePart::Slot(n) => match slots.get(*n as usize) {
Some(Value::FragmentRef(idx)) => {
if let Some(parts) = fragments.parts(*idx) {
let s = resolve_parts(parts, program, line_tables, resolver, fragments);
out.push_str(&s);
}
}
Some(other) => out.push_str(&value_ops::stringify_display(other, program)),
None => {}
},
LinePart::Select {
slot,
variants,
default,
} => out.push_str(resolve_select(*slot, variants, default, slots, resolver)),
LinePart::Span { children, .. } => {
resolve_line_parts_into(
out,
children,
program,
line_tables,
slots,
resolver,
fragments,
);
}
}
if out.len() == start {
continue;
}
let result_empty_or_space = start == base || out[..start].ends_with(' ');
if result_empty_or_space && out[start..].starts_with(' ') {
let lead = out[start..].len() - out[start..].trim_start().len();
out.replace_range(start..start + lead, "");
}
}
}
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, Copy, PartialEq, Eq)]
pub(crate) struct OutputMark {
pub(crate) len: usize,
pub(crate) capture_depth: usize,
pub(crate) fragment_depth: usize,
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OutputBuffer>();
assert_send_sync::<OutputPart>();
};
#[derive(Debug, Clone)]
pub(crate) struct OutputBuffer {
line_scan: Vec<bool>,
completion: LineCompletion,
pub(crate) transcript: Vec<OutputPart>,
pub(crate) cursor: usize,
capture: Vec<OutputPart>,
capture_depth: usize,
fragments: Fragments,
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 {
line_scan: Vec::new(),
completion: LineCompletion::default(),
transcript: Vec::new(),
cursor: 0,
capture: Vec::new(),
capture_depth: 0,
fragments: Fragments::default(),
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
}
}
fn push_part(&mut self, part: OutputPart) {
if self.capture_depth == 0 && self.fragment_depth == 0 {
self.completion.feed(&part);
self.transcript.push(part);
} else {
self.target().push(part);
}
}
pub(crate) fn mark(&self) -> OutputMark {
OutputMark {
len: self.target_len(),
capture_depth: self.capture_depth,
fragment_depth: self.fragment_depth,
}
}
pub(crate) fn push_newline_in_function(&mut self, function: Option<OutputMark>) {
if let Some(mark) = function
&& mark.capture_depth == self.capture_depth
&& mark.fragment_depth == self.fragment_depth
&& self
.target_ref()
.get(mark.len..)
.is_some_and(|since_call| !since_call.iter().any(OutputPart::is_content))
{
return;
}
self.push_newline();
}
fn target_ref(&self) -> &Vec<OutputPart> {
if self.capture_depth > 0 {
&self.capture
} else if self.fragment_depth > 0 {
&self.fragment_capture
} else {
&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 floor = if self.capture_depth == 0 && self.fragment_depth == 0 {
start.max(self.cursor)
} else {
start
};
let on_transcript = self.capture_depth == 0 && self.fragment_depth == 0;
let target = self.target();
let mut removed = false;
let mut i = target.len();
while i > floor {
i -= 1;
let trimmable = match &target[i] {
OutputPart::Glue => continue,
OutputPart::Newline | OutputPart::Spring => true,
OutputPart::Text(s) => s.trim().is_empty(),
OutputPart::LineRef { flags, .. } => {
flags.contains(brink_format::LineFlags::ALL_WS)
}
part @ OutputPart::ValueRef(_) => !part.is_visible(),
_ => false,
};
if !trimmable {
break;
}
target.remove(i);
removed = true;
}
if removed && on_transcript {
self.rescan_completion();
}
}
#[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.push_part(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.push_part(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.push_part(OutputPart::Glue);
}
pub fn push_spring(&mut self) {
if !matches!(self.target_ref().last(), Some(OutputPart::Spring)) {
self.push_part(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.push_part(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.push_part(OutputPart::ValueRef(value));
}
pub fn push_tag(&mut self, tag: String) {
self.push_part(OutputPart::Tag(tag));
}
pub(crate) fn push_element_attach(&mut self, key: String, value: String) {
self.push_part(OutputPart::ElementAttach(key, value));
}
pub(crate) fn push_element_attach_end(&mut self) {
self.push_part(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::LineRef { flags, .. }
if flags.contains(brink_format::LineFlags::ALL_WS)
|| flags.contains(brink_format::LineFlags::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: &Fragments,
) -> 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;
let mut since_newline = 0usize;
for (i, part) in parts.iter().enumerate() {
if remove[i] {
match part {
OutputPart::Glue => {
after_glue = true;
if out[since_newline..].trim().is_empty() {
out.truncate(since_newline);
}
}
OutputPart::Newline => since_newline = out.len(),
_ => {}
}
continue;
}
match part {
OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
if part_involves_fragment_ref(part) {
saw_fragment_ref = true;
}
let start = out.len();
resolve_part_into(part, &mut out, program, line_tables, resolver, fragments);
if collapse_join(&mut out, start) {
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;
}
since_newline = out.len();
}
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>,
Option<brink_format::SourceLocation>,
);
pub(crate) type AnnotatedResolvedLine = (
String,
Vec<String>,
bool,
BTreeMap<String, String>,
Option<brink_format::SourceLocation>,
);
pub(crate) fn resolve_lines(
parts: &[OutputPart],
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) -> Vec<ResolvedLine> {
if parts.is_empty() {
return Vec::new();
}
let mut remove = vec![false; parts.len()];
mark_glue_removals(parts, &mut remove);
resolve_lines_marked(
parts,
&remove,
BTreeMap::new(),
program,
line_tables,
resolver,
fragments,
)
.0
}
pub(crate) fn resolve_lines_marked(
parts: &[OutputPart],
remove: &[bool],
seed_element: BTreeMap<String, String>,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) -> (Vec<ResolvedLine>, BTreeMap<String, String>) {
if parts.is_empty() {
return (Vec::new(), seed_element);
}
let mut lines: Vec<ResolvedLine> = Vec::new();
let (text, tags, suppressed, element, source) = drive_lines(
parts,
remove,
seed_element,
program,
line_tables,
resolver,
fragments,
|(text, tags, suppressed, element, source)| {
if !suppressed {
lines.push((text, tags, element, source));
}
},
);
if suppressed {
(lines, element)
} else {
let carried = element.clone();
lines.push((text, tags, element, source));
(lines, carried)
}
}
fn widen_source(
current: &mut Option<brink_format::SourceLocation>,
entry: Option<&brink_format::SourceLocation>,
) {
match (current, entry) {
(current @ None, Some(src)) => *current = Some(src.clone()),
(Some(cur), Some(src)) if cur.file == src.file => {
cur.range_start = cur.range_start.min(src.range_start);
cur.range_end = cur.range_end.max(src.range_end);
}
_ => {}
}
}
#[cfg(test)]
pub(crate) fn resolve_lines_annotated_marked(
parts: &[OutputPart],
remove: &[bool],
seed_element: BTreeMap<String, String>,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) -> Vec<AnnotatedResolvedLine> {
if parts.is_empty() {
return Vec::new();
}
let mut lines: Vec<AnnotatedResolvedLine> = Vec::new();
let trailing = drive_lines(
parts,
remove,
seed_element,
program,
line_tables,
resolver,
fragments,
|line| lines.push(line),
);
lines.push(trailing);
lines
}
pub(crate) fn resolve_first_line_annotated(
parts: &[OutputPart],
remove: &[bool],
seed_element: BTreeMap<String, String>,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
) -> (AnnotatedResolvedLine, BTreeMap<String, String>) {
let mut first: Option<AnnotatedResolvedLine> = None;
let mut next_element: Option<BTreeMap<String, String>> = None;
let trailing = drive_lines(
parts,
remove,
seed_element,
program,
line_tables,
resolver,
fragments,
|line| {
if first.is_none() {
first = Some(line);
} else if next_element.is_none() {
next_element = Some(line.3);
}
},
);
match first {
Some(line) => (line, next_element.unwrap_or(trailing.3)),
None => (trailing, BTreeMap::new()),
}
}
fn trim_in_place(s: &mut String) {
trim_in_place_matches(s, char::is_whitespace);
}
pub(crate) fn trim_in_place_matches(s: &mut String, pred: impl Fn(char) -> bool + Copy) {
let end = s.trim_end_matches(pred).len();
s.truncate(end);
let lead = s.len() - s.trim_start_matches(pred).len();
if lead > 0 {
s.replace_range(..lead, "");
}
}
#[expect(
clippy::too_many_arguments,
reason = "the resolver context plus the sink"
)]
fn drive_lines(
parts: &[OutputPart],
remove: &[bool],
seed_element: BTreeMap<String, String>,
program: &Program,
line_tables: &[Vec<LineEntry>],
resolver: Option<&dyn PluralResolver>,
fragments: &Fragments,
mut emit: impl FnMut(AnnotatedResolvedLine),
) -> AnnotatedResolvedLine {
debug_assert_eq!(remove.len(), parts.len(), "one glue mark per part");
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 current_source: Option<brink_format::SourceLocation> = None;
let mut saw_fragment_ref = false;
let mut after_glue = false;
let mut since_newline = 0usize;
for (i, part) in parts.iter().enumerate() {
if remove[i] {
match part {
OutputPart::Glue => {
after_glue = true;
if current_text[since_newline..].trim().is_empty() {
current_text.truncate(since_newline);
}
}
OutputPart::Newline => since_newline = current_text.len(),
_ => {}
}
continue;
}
match part {
OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
if let OutputPart::LineRef {
container_idx,
line_idx,
..
} = part
{
let scope_idx = program.scope_table_idx(*container_idx) as usize;
let entry_source = line_tables
.get(scope_idx)
.and_then(|t| t.get(*line_idx as usize))
.and_then(|entry| entry.source_location.as_ref());
widen_source(&mut current_source, entry_source);
}
if part_involves_fragment_ref(part) {
saw_fragment_ref = true;
}
let start = current_text.len();
resolve_part_into(
part,
&mut current_text,
program,
line_tables,
resolver,
fragments,
);
if collapse_join(&mut current_text, start) {
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 {
trim_in_place(&mut current_text);
let suppressed =
current_text.is_empty() && current_tags.is_empty() && saw_fragment_ref;
emit((
mem::take(&mut current_text),
mem::take(&mut current_tags),
suppressed,
current_element.clone(),
current_source.take(),
));
saw_fragment_ref = false;
}
since_newline = current_text.len();
}
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;
}
}
}
trim_in_place(&mut current_text);
let suppressed = current_text.is_empty() && current_tags.is_empty() && saw_fragment_ref;
(
current_text,
current_tags,
suppressed,
current_element,
current_source,
)
}
#[cfg(test)]
fn test_dummy_program() -> Program {
use std::collections::HashMap;
Program {
link: crate::program::LinkTables::default(),
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(),
container_paths: 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(),
debug_info: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_line_resolver_matches_batch_resolver() {
let program = test_dummy_program();
let seed = |k: &str, v: &str| {
let mut m = BTreeMap::new();
m.insert(k.to_string(), v.to_string());
m
};
let cases: Vec<(Vec<OutputPart>, BTreeMap<String, String>)> = vec![
(
vec![
OutputPart::Text("hello ".to_string()),
OutputPart::Text(" world".to_string()),
OutputPart::Newline,
OutputPart::Text("next".to_string()),
],
BTreeMap::new(),
),
(
vec![
OutputPart::Text("a".to_string()),
OutputPart::Newline,
OutputPart::Glue,
OutputPart::Text("b".to_string()),
OutputPart::Newline,
],
BTreeMap::new(),
),
(
vec![
OutputPart::Tag("t".to_string()),
OutputPart::Text(" tagged ".to_string()),
OutputPart::Newline,
],
BTreeMap::new(),
),
(
vec![
OutputPart::ElementAttach("k".to_string(), "v".to_string()),
OutputPart::Text("in run".to_string()),
OutputPart::Newline,
OutputPart::ElementAttachEnd,
],
seed("outer", "x"),
),
(
vec![
OutputPart::Text("carried".to_string()),
OutputPart::Newline,
OutputPart::ElementAttach("k2".to_string(), "v2".to_string()),
],
seed("outer", "x"),
),
];
for (parts, seed_element) in cases {
let mut remove = vec![false; parts.len()];
mark_glue_removals(&parts, &mut remove);
let split_at = parts
.iter()
.enumerate()
.position(|(i, p)| matches!(p, OutputPart::Newline) && !remove[i])
.expect("every case carries a kept newline");
let slice = &parts[..=split_at];
let marks = &remove[..=split_at];
let batch = resolve_lines_annotated_marked(
slice,
marks,
seed_element.clone(),
&program,
&[],
None,
&Fragments::default(),
);
let (line, next_element) = resolve_first_line_annotated(
slice,
marks,
seed_element,
&program,
&[],
None,
&Fragments::default(),
);
assert_eq!(
batch.len(),
2,
"one line plus the trailing entry: {parts:?}"
);
assert_eq!(line, batch[0], "first line differs: {parts:?}");
assert_eq!(
next_element, batch[1].3,
"carried element differs: {parts:?}"
);
}
}
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, _source)| (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, _source)| (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(), "ab");
}
#[test]
fn spring_before_glue_survives_only_after_line_content() {
let mut buf = OutputBuffer::new();
buf.push_text("a");
buf.push_newline();
buf.push_spring();
buf.push_glue();
buf.push_text("b");
assert_eq!(buf.flush(), "ab");
let mut buf = OutputBuffer::new();
buf.push_text("a");
buf.push_newline();
buf.push_text("0");
buf.push_spring();
buf.push_glue();
buf.push_text("world");
assert_eq!(buf.flush(), "a\n0 world");
}
#[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 {
link: crate::program::LinkTables::default(),
containers: vec![LinkedContainer {
id,
bytecode: vec![],
counting_flags: CountingFlags::empty(),
path_hash: 0,
param_count: 0,
params: Vec::new(),
scope_table_idx: 0,
scope_id: id,
}],
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(),
container_paths: 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(),
debug_info: None,
};
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,
&Fragments::default(),
)
}
#[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 {
link: crate::program::LinkTables::default(),
containers: vec![LinkedContainer {
id,
bytecode: vec![],
counting_flags: CountingFlags::empty(),
path_hash: 0,
param_count: 0,
params: Vec::new(),
scope_table_idx: 0,
scope_id: id,
}],
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(),
container_paths: 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(),
debug_info: None,
};
(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 = Fragments::from(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, _source)| (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 = Fragments::from(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, _source)| (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 = Fragments::default();
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, _source)| (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:?}"
);
}
#[test]
fn trim_function_end_stops_at_the_read_cursor() {
let mut buf = OutputBuffer::new();
let start = buf.target_len();
buf.push_text("1");
buf.push_newline();
buf.push_value_ref(Value::List(alloc::sync::Arc::new(
brink_format::ListValue {
items: Vec::new(),
origins: Vec::new(),
},
)));
buf.push_newline();
assert_eq!(
buf.test_take_first_line().map(|(t, _)| t),
Some("1\n".to_owned())
);
let cursor = buf.cursor;
assert_eq!(cursor, 2, "the delivered line is the first two parts");
buf.trim_function_end(start);
assert!(
buf.transcript.len() >= cursor,
"the trim walked behind the cursor: transcript is {} parts, \
cursor is at {cursor}",
buf.transcript.len()
);
assert_eq!(buf.transcript.len(), cursor, "the unread tail is trimmed");
assert!(buf.test_take_first_line().is_none());
}
}