1use brink_format::{
4 LineContent, LineEntry, LinePart, PluralCategory, PluralResolver, SelectKey, Value,
5};
6
7use crate::program::Program;
8use crate::value_ops;
9
10#[derive(Debug, Clone)]
17pub enum OutputPart {
18 Text(String),
21 LineRef {
24 container_idx: u32,
25 line_idx: u16,
26 slots: Vec<Value>,
27 flags: brink_format::LineFlags,
28 },
29 ValueRef(Value),
31 Newline,
32 Spring,
34 Glue,
35 Checkpoint,
37 Tag(String),
39}
40
41impl OutputPart {
42 pub fn resolve(
49 &self,
50 program: &Program,
51 line_tables: &[Vec<LineEntry>],
52 resolver: Option<&dyn PluralResolver>,
53 ) -> String {
54 resolve_part(self, program, line_tables, resolver, &[])
55 }
56
57 fn is_content(&self) -> bool {
59 match self {
60 Self::Text(s) => !s.trim().is_empty(),
61 Self::LineRef { flags, .. } => {
62 !flags.contains(brink_format::LineFlags::ALL_WS)
63 && !flags.contains(brink_format::LineFlags::EMPTY)
64 }
65 Self::ValueRef(_) => true,
66 _ => false,
67 }
68 }
69}
70
71fn resolve_part(
76 part: &OutputPart,
77 program: &Program,
78 line_tables: &[Vec<LineEntry>],
79 resolver: Option<&dyn PluralResolver>,
80 fragments: &[Fragment],
81) -> String {
82 match part {
83 OutputPart::Text(s) => s.clone(),
84 OutputPart::LineRef {
85 container_idx,
86 line_idx,
87 slots,
88 ..
89 } => resolve_line_ref(
90 program,
91 line_tables,
92 *container_idx,
93 *line_idx,
94 slots,
95 resolver,
96 fragments,
97 ),
98 OutputPart::ValueRef(Value::FragmentRef(idx)) => {
99 let idx = *idx as usize;
101 if let Some(frag) = fragments.get(idx) {
102 resolve_parts(&frag.parts, program, line_tables, resolver, fragments)
103 } else {
104 String::new()
105 }
106 }
107 OutputPart::ValueRef(val) => value_ops::stringify(val, program),
108 OutputPart::Newline
109 | OutputPart::Spring
110 | OutputPart::Glue
111 | OutputPart::Checkpoint
112 | OutputPart::Tag(_) => String::new(),
113 }
114}
115
116fn resolve_line_ref(
118 program: &Program,
119 line_tables: &[Vec<LineEntry>],
120 container_idx: u32,
121 line_idx: u16,
122 slots: &[Value],
123 resolver: Option<&dyn PluralResolver>,
124 fragments: &[Fragment],
125) -> String {
126 let scope_idx = program.scope_table_idx(container_idx) as usize;
127 let lines = &line_tables[scope_idx];
128 let Some(entry) = lines.get(line_idx as usize) else {
129 return String::new();
130 };
131
132 match &entry.content {
133 LineContent::Plain(s) => s.clone(),
134 LineContent::Template(parts) => {
135 let mut result = String::new();
136 for part in parts {
137 let owned;
138 let fragment: &str = match part {
139 LinePart::Literal(s) => s.as_str(),
140 LinePart::Slot(n) => {
141 owned = slots
142 .get(*n as usize)
143 .map(|v| match v {
144 Value::FragmentRef(idx) => {
145 let idx = *idx as usize;
146 fragments.get(idx).map_or_else(String::new, |frag| {
147 resolve_parts(
148 &frag.parts,
149 program,
150 line_tables,
151 resolver,
152 fragments,
153 )
154 })
155 }
156 other => value_ops::stringify(other, program),
157 })
158 .unwrap_or_default();
159 owned.as_str()
160 }
161 LinePart::Select {
162 slot,
163 variants,
164 default,
165 } => {
166 owned =
167 resolve_select(*slot, variants, default, slots, resolver).to_string();
168 owned.as_str()
169 }
170 };
171 if fragment.is_empty() {
175 continue;
176 }
177 if (result.is_empty() || result.ends_with(' ')) && fragment.starts_with(' ') {
178 result.push_str(fragment.trim_start());
179 } else {
180 result.push_str(fragment);
181 }
182 }
183 result
184 }
185 }
186}
187
188fn resolve_select<'a>(
192 slot: u8,
193 variants: &'a [(SelectKey, String)],
194 default: &'a str,
195 slots: &[Value],
196 resolver: Option<&dyn PluralResolver>,
197) -> &'a str {
198 let Some(val) = slots.get(slot as usize) else {
199 return default;
200 };
201
202 #[expect(clippy::cast_possible_truncation)]
203 let n: Option<i64> = match val {
204 Value::Int(i) => Some(i64::from(*i)),
205 Value::Float(f) => Some(*f as i64),
206 _ => None,
207 };
208
209 if let Some(n) = n {
211 #[expect(clippy::cast_possible_truncation)]
212 let n32 = n as i32;
213 for (key, text) in variants {
214 if let SelectKey::Exact(e) = key
215 && *e == n32
216 {
217 return text;
218 }
219 }
220 }
221
222 if let Value::String(s) = val {
224 for (key, text) in variants {
225 if let SelectKey::Keyword(k) = key
226 && k == s.as_ref()
227 {
228 return text;
229 }
230 }
231 }
232
233 if let (Some(n), Some(r)) = (n, resolver) {
235 let cardinal: PluralCategory = r.cardinal(n, None);
236 for (key, text) in variants {
237 if let SelectKey::Cardinal(cat) = key
238 && *cat == cardinal
239 {
240 return text;
241 }
242 }
243 let ordinal: PluralCategory = r.ordinal(n);
244 for (key, text) in variants {
245 if let SelectKey::Ordinal(cat) = key
246 && *cat == ordinal
247 {
248 return text;
249 }
250 }
251 }
252
253 default
254}
255
256#[derive(Debug, Clone)]
258pub struct Fragment {
259 pub parts: Vec<OutputPart>,
260 pub tags: Vec<String>,
261}
262
263#[derive(Debug, Clone)]
271pub(crate) struct OutputBuffer {
272 pub(crate) transcript: Vec<OutputPart>,
274 pub(crate) cursor: usize,
276 capture: Vec<OutputPart>,
278 capture_depth: usize,
280 fragments: Vec<Fragment>,
282 fragment_capture: Vec<OutputPart>,
284 fragment_depth: usize,
286 fragment_pending_tags: Vec<Vec<String>>,
288}
289
290impl OutputBuffer {
291 pub fn new() -> Self {
292 Self {
293 transcript: Vec::new(),
294 cursor: 0,
295 capture: Vec::new(),
296 capture_depth: 0,
297 fragments: Vec::new(),
298 fragment_capture: Vec::new(),
299 fragment_depth: 0,
300 fragment_pending_tags: Vec::new(),
301 }
302 }
303
304 fn target(&mut self) -> &mut Vec<OutputPart> {
307 if self.capture_depth > 0 {
308 &mut self.capture
309 } else if self.fragment_depth > 0 {
310 &mut self.fragment_capture
311 } else {
312 &mut self.transcript
313 }
314 }
315
316 pub(crate) fn target_len(&self) -> usize {
319 if self.capture_depth > 0 {
320 self.capture.len()
321 } else if self.fragment_depth > 0 {
322 self.fragment_capture.len()
323 } else {
324 self.transcript.len()
325 }
326 }
327
328 pub(crate) fn trim_function_end(&mut self, start: usize) {
334 let target = self.target();
335 while target.len() > start {
336 match target.last() {
337 Some(OutputPart::Newline | OutputPart::Spring) => {
338 target.pop();
339 }
340 Some(OutputPart::Text(s)) if s.trim().is_empty() => {
341 target.pop();
342 }
343 Some(OutputPart::LineRef { flags, .. })
344 if flags.contains(brink_format::LineFlags::ALL_WS) =>
345 {
346 target.pop();
347 }
348 _ => break,
349 }
350 }
351 }
352
353 #[cfg(test)]
355 pub fn push_text(&mut self, text: &str) {
356 if text.is_empty() {
357 return;
358 }
359 if !self.has_content() && text.trim().is_empty() {
363 return;
364 }
365 let text = if text.starts_with(char::is_whitespace) && self.ends_in_whitespace() {
369 text.trim_start()
370 } else {
371 text
372 };
373 if !text.is_empty() {
374 self.target().push(OutputPart::Text(text.to_owned()));
375 }
376 }
377
378 pub fn push_newline(&mut self) {
379 let has_content = if self.capture_depth > 0 {
389 self.has_content()
390 } else {
391 self.unread_has_content_or_spring()
392 };
393 if !has_content || self.ends_in_newline() {
394 return;
395 }
396 self.target().push(OutputPart::Newline);
397 }
398
399 fn has_content(&self) -> bool {
403 if self.capture_depth > 0 {
404 self.capture
405 .iter()
406 .rev()
407 .take_while(|p| !matches!(p, OutputPart::Checkpoint))
408 .any(OutputPart::is_content)
409 } else {
410 self.transcript[self.cursor..]
411 .iter()
412 .rev()
413 .any(OutputPart::is_content)
414 }
415 }
416
417 fn unread_has_content_or_spring(&self) -> bool {
428 self.transcript[self.cursor..]
429 .iter()
430 .any(|p| p.is_content() || matches!(p, OutputPart::Spring))
431 }
432
433 fn ends_in_newline(&self) -> bool {
435 let target = if self.capture_depth > 0 {
436 &self.capture
437 } else {
438 &self.transcript
439 };
440 matches!(target.last(), Some(OutputPart::Newline))
441 }
442
443 #[cfg(test)]
447 fn ends_in_whitespace(&self) -> bool {
448 let target = if self.capture_depth > 0 {
449 &self.capture
450 } else {
451 &self.transcript
452 };
453 match target.last() {
454 Some(OutputPart::Text(s)) => s.ends_with(char::is_whitespace),
455 Some(OutputPart::LineRef { flags, .. }) => {
456 flags.contains(brink_format::LineFlags::ENDS_WITH_WS)
457 }
458 _ => false,
459 }
460 }
461
462 pub fn push_glue(&mut self) {
463 self.target().push(OutputPart::Glue);
464 }
465
466 pub fn push_spring(&mut self) {
468 let target = self.target();
469 if !matches!(target.last(), Some(OutputPart::Spring)) {
470 target.push(OutputPart::Spring);
471 }
472 }
473
474 pub fn push_line_ref(
477 &mut self,
478 container_idx: u32,
479 line_idx: u16,
480 slots: Vec<Value>,
481 flags: brink_format::LineFlags,
482 ) {
483 if !self.has_content()
485 && (flags.contains(brink_format::LineFlags::ALL_WS)
486 || flags.contains(brink_format::LineFlags::EMPTY))
487 {
488 return;
489 }
490 self.target().push(OutputPart::LineRef {
491 container_idx,
492 line_idx,
493 slots,
494 flags,
495 });
496 }
497
498 pub fn push_value_ref(&mut self, value: Value) {
501 if matches!(value, Value::Null) {
502 return;
503 }
504 if !self.has_content()
506 && let Value::String(ref s) = value
507 && s.trim().is_empty()
508 {
509 return;
510 }
511 self.target().push(OutputPart::ValueRef(value));
512 }
513
514 pub fn push_tag(&mut self, tag: String) {
516 self.target().push(OutputPart::Tag(tag));
517 }
518
519 pub fn has_checkpoint(&self) -> bool {
521 self.capture_depth > 0
522 }
523
524 pub fn begin_capture(&mut self) {
527 self.capture_depth += 1;
528 self.capture.push(OutputPart::Checkpoint);
529 }
530
531 pub fn end_capture(
536 &mut self,
537 program: &Program,
538 line_tables: &[Vec<LineEntry>],
539 resolver: Option<&dyn PluralResolver>,
540 ) -> Option<String> {
541 let cp_idx = self
542 .capture
543 .iter()
544 .rposition(|p| matches!(p, OutputPart::Checkpoint))?;
545
546 let captured: Vec<OutputPart> = self.capture.drain(cp_idx..).collect();
547 let captured = &captured[1..];
549
550 self.capture_depth = self.capture_depth.saturating_sub(1);
551
552 Some(resolve_parts(
553 captured,
554 program,
555 line_tables,
556 resolver,
557 &self.fragments,
558 ))
559 }
560
561 pub fn begin_fragment(&mut self) {
565 self.fragment_depth += 1;
566 self.fragment_capture.push(OutputPart::Checkpoint);
567 self.fragment_pending_tags.push(Vec::new());
568 }
569
570 #[expect(clippy::cast_possible_truncation)]
573 pub fn end_fragment(&mut self) -> Option<u32> {
574 let cp_idx = self
575 .fragment_capture
576 .iter()
577 .rposition(|p| matches!(p, OutputPart::Checkpoint))?;
578
579 let captured: Vec<OutputPart> = self.fragment_capture.drain(cp_idx..).collect();
580 let parts: Vec<OutputPart> = captured.into_iter().skip(1).collect();
582 let tags = self.fragment_pending_tags.pop().unwrap_or_default();
583 let idx = self.fragments.len() as u32;
584 self.fragments.push(Fragment { parts, tags });
585
586 self.fragment_depth = self.fragment_depth.saturating_sub(1);
587
588 Some(idx)
589 }
590
591 pub fn in_fragment_capture(&self) -> bool {
593 self.fragment_depth > 0
594 }
595
596 pub fn push_fragment_tag(&mut self, tag: String) {
598 if let Some(pending) = self.fragment_pending_tags.last_mut() {
599 pending.push(tag);
600 }
601 }
602
603 pub fn fragment_tags(&self, idx: u32) -> Option<&[String]> {
605 self.fragments.get(idx as usize).map(|f| f.tags.as_slice())
606 }
607
608 pub fn fragments(&self) -> &[Fragment] {
610 &self.fragments
611 }
612
613 pub fn fragment(&self, idx: u32) -> Option<&[OutputPart]> {
615 self.fragments.get(idx as usize).map(|f| f.parts.as_slice())
616 }
617
618 pub fn resolve_fragment(
620 &self,
621 idx: u32,
622 program: &Program,
623 line_tables: &[Vec<LineEntry>],
624 resolver: Option<&dyn PluralResolver>,
625 ) -> String {
626 match self.fragment(idx) {
627 Some(parts) => resolve_parts(parts, program, line_tables, resolver, &self.fragments),
628 None => String::new(),
629 }
630 }
631
632 pub(crate) fn has_completed_line(&self) -> bool {
640 if self.has_checkpoint() {
641 return false;
642 }
643 let unread = &self.transcript[self.cursor..];
644 if unread.is_empty() {
645 return false;
646 }
647
648 if !unread.iter().any(|p| matches!(p, OutputPart::Newline)) {
650 return false;
651 }
652
653 let mut remove = vec![false; unread.len()];
655 mark_glue_removals(unread, &mut remove);
656
657 let mut after_glue = false;
660 let mut found_newline = false;
661
662 for (i, part) in unread.iter().enumerate() {
663 if remove[i] {
664 if matches!(part, OutputPart::Glue) {
665 after_glue = true;
666 }
667 continue;
668 }
669 if part.is_content() {
670 if found_newline {
671 return true;
672 }
673 after_glue = false;
674 } else {
675 match part {
676 OutputPart::Newline if !after_glue => {
677 found_newline = true;
678 }
679 OutputPart::Glue => {
680 after_glue = true;
681 }
682 _ => {}
683 }
684 }
685 }
686
687 false
688 }
689
690 pub(crate) fn take_first_line(
701 &mut self,
702 program: &Program,
703 line_tables: &[Vec<LineEntry>],
704 resolver: Option<&dyn PluralResolver>,
705 ) -> Option<(String, Vec<String>)> {
706 if self.has_checkpoint() {
707 return None;
708 }
709 let unread = &self.transcript[self.cursor..];
710 if unread.is_empty() {
711 return None;
712 }
713
714 let mut remove = vec![false; unread.len()];
715 mark_glue_removals(unread, &mut remove);
716
717 let mut after_glue = false;
720 let mut candidate_newline: Option<usize> = None;
721
722 for (i, part) in unread.iter().enumerate() {
723 if remove[i] {
724 if matches!(part, OutputPart::Glue) {
725 after_glue = true;
726 }
727 continue;
728 }
729 if part.is_content() {
730 if candidate_newline.is_some() {
731 break;
732 }
733 after_glue = false;
734 } else {
735 match part {
736 OutputPart::Newline if !after_glue => {
737 candidate_newline = Some(i);
738 }
739 OutputPart::Glue => {
740 after_glue = true;
741 }
742 _ => {}
743 }
744 }
745 }
746
747 let split_at = candidate_newline?;
748
749 let slice = &self.transcript[self.cursor..=self.cursor + split_at];
751 let mut lines = resolve_lines(slice, program, line_tables, resolver, &self.fragments);
752 if lines.is_empty() {
753 return None;
754 }
755
756 self.cursor += split_at + 1;
758
759 let (mut text, tags) = lines.swap_remove(0);
760 text.push('\n');
761 Some((text, tags))
762 }
763
764 #[cfg(test)]
771 pub fn flush(&mut self) -> String {
772 debug_assert!(
773 !self.has_checkpoint(),
774 "flush() called with active checkpoints"
775 );
776 let unread = &self.transcript[self.cursor..];
777 let program = test_dummy_program();
778 let result = resolve_parts(unread, &program, &[], None, &self.fragments);
779 self.cursor = self.transcript.len();
780 result
781 }
782
783 pub fn flush_lines(
788 &mut self,
789 program: &Program,
790 line_tables: &[Vec<LineEntry>],
791 resolver: Option<&dyn PluralResolver>,
792 ) -> Vec<(String, Vec<String>)> {
793 debug_assert!(
794 !self.has_checkpoint(),
795 "flush_lines() called with active checkpoints"
796 );
797 let unread = &self.transcript[self.cursor..];
798 let result = resolve_lines(unread, program, line_tables, resolver, &self.fragments);
799 self.cursor = self.transcript.len();
800 result
801 }
802
803 pub(crate) fn has_unread(&self) -> bool {
805 self.cursor < self.transcript.len()
806 }
807
808 pub fn transcript(&self) -> &[OutputPart] {
810 &self.transcript
811 }
812
813 pub fn reset_cursor(&mut self) {
815 self.cursor = 0;
816 }
817
818 pub fn transcript_len(&self) -> usize {
820 self.transcript.len()
821 }
822}
823
824fn mark_glue_removals(parts: &[OutputPart], remove: &mut [bool]) {
830 for (i, part) in parts.iter().enumerate() {
831 if matches!(part, OutputPart::Glue) {
832 for j in (0..i).rev() {
833 if remove[j] {
834 continue;
835 }
836 match &parts[j] {
837 OutputPart::Newline => {
838 remove[j] = true;
839 break;
840 }
841 OutputPart::Glue
842 | OutputPart::Checkpoint
843 | OutputPart::Tag(_)
844 | OutputPart::Spring => {}
845 OutputPart::Text(s) if s.trim().is_empty() => {}
846 OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
848 break;
849 }
850 }
851 }
852 remove[i] = true;
853 }
854 }
855}
856
857fn resolve_parts(
859 parts: &[OutputPart],
860 program: &Program,
861 line_tables: &[Vec<LineEntry>],
862 resolver: Option<&dyn PluralResolver>,
863 fragments: &[Fragment],
864) -> String {
865 let mut remove = vec![false; parts.len()];
867 mark_glue_removals(parts, &mut remove);
868
869 let mut out = String::new();
870 let mut after_glue = false;
871
872 for (i, part) in parts.iter().enumerate() {
873 if remove[i] {
874 if matches!(part, OutputPart::Glue) {
875 after_glue = true;
876 }
877 continue;
878 }
879 match part {
880 OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
881 let s = resolve_part(part, program, line_tables, resolver, fragments);
882 let s = if s.starts_with(char::is_whitespace) && out.ends_with(char::is_whitespace)
884 {
885 s.trim_start()
886 } else {
887 &s
888 };
889 out.push_str(s);
890 if !s.trim().is_empty() {
891 after_glue = false;
892 }
893 }
894 OutputPart::Spring => {
895 if !out.is_empty() && !out.ends_with(' ') && !out.ends_with('\n') {
897 out.push(' ');
898 }
899 }
900 OutputPart::Newline => {
901 if !after_glue {
902 let trimmed_len = out.trim_end_matches([' ', '\t']).len();
903 out.truncate(trimmed_len);
904 out.push('\n');
905 }
906 }
907 OutputPart::Glue | OutputPart::Checkpoint | OutputPart::Tag(_) => {
908 after_glue = true;
909 }
910 }
911 }
912
913 out
914}
915
916pub(crate) fn resolve_lines(
922 parts: &[OutputPart],
923 program: &Program,
924 line_tables: &[Vec<LineEntry>],
925 resolver: Option<&dyn PluralResolver>,
926 fragments: &[Fragment],
927) -> Vec<(String, Vec<String>)> {
928 if parts.is_empty() {
929 return Vec::new();
930 }
931
932 let mut remove = vec![false; parts.len()];
934 mark_glue_removals(parts, &mut remove);
935
936 let mut lines: Vec<(String, Vec<String>)> = Vec::new();
937 let mut current_text = String::new();
938 let mut current_tags: Vec<String> = Vec::new();
939 let mut after_glue = false;
940
941 for (i, part) in parts.iter().enumerate() {
942 if remove[i] {
943 if matches!(part, OutputPart::Glue) {
944 after_glue = true;
945 }
946 continue;
947 }
948 match part {
949 OutputPart::Text(_) | OutputPart::LineRef { .. } | OutputPart::ValueRef(_) => {
950 let s = resolve_part(part, program, line_tables, resolver, fragments);
951 let s = if s.starts_with(char::is_whitespace)
953 && current_text.ends_with(char::is_whitespace)
954 {
955 s.trim_start()
956 } else {
957 &s
958 };
959 current_text.push_str(s);
960 if !s.trim().is_empty() {
961 after_glue = false;
962 }
963 }
964 OutputPart::Spring => {
965 if !current_text.is_empty()
966 && !current_text.ends_with(' ')
967 && !current_text.ends_with('\n')
968 {
969 current_text.push(' ');
970 }
971 }
972 OutputPart::Newline => {
973 if !after_glue {
974 let trimmed = current_text.trim().to_string();
975 lines.push((trimmed, std::mem::take(&mut current_tags)));
976 current_text = String::new();
977 }
978 }
979 OutputPart::Tag(tag) => {
980 current_tags.push(tag.clone());
981 }
982 OutputPart::Glue | OutputPart::Checkpoint => {
983 after_glue = true;
984 }
985 }
986 }
987
988 let trimmed = current_text.trim().to_string();
991 lines.push((trimmed, current_tags));
992
993 lines
994}
995
996#[cfg(test)]
998fn test_dummy_program() -> Program {
999 use std::collections::HashMap;
1000 Program {
1001 containers: vec![],
1002 address_map: HashMap::new(),
1003 scope_ids: vec![],
1004 source_checksum: 0,
1005 globals: vec![],
1006 global_map: HashMap::new(),
1007 name_table: vec![],
1008 address_by_path: HashMap::new(),
1009 root_idx: 0,
1010 list_literals: vec![],
1011 list_item_map: HashMap::new(),
1012 list_defs: vec![],
1013 list_def_map: HashMap::new(),
1014 external_fns: HashMap::new(),
1015 local_scope_defaults: Vec::new(),
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 impl OutputBuffer {
1026 fn test_flush_lines(&mut self) -> Vec<(String, Vec<String>)> {
1027 let p = test_dummy_program();
1028 self.flush_lines(&p, &[], None)
1029 }
1030
1031 fn test_take_first_line(&mut self) -> Option<(String, Vec<String>)> {
1032 let p = test_dummy_program();
1033 self.take_first_line(&p, &[], None)
1034 }
1035
1036 fn test_end_capture(&mut self) -> Option<String> {
1037 let p = test_dummy_program();
1038 self.end_capture(&p, &[], None)
1039 }
1040 }
1041
1042 #[test]
1043 fn simple_text() {
1044 let mut buf = OutputBuffer::new();
1045 buf.push_text("hello");
1046 assert_eq!(buf.flush(), "hello");
1047 }
1048
1049 #[test]
1050 fn text_with_newline() {
1051 let mut buf = OutputBuffer::new();
1052 buf.push_text("hello");
1053 buf.push_newline();
1054 buf.push_text("world");
1055 assert_eq!(buf.flush(), "hello\nworld");
1056 }
1057
1058 #[test]
1059 fn glue_removes_newline() {
1060 let mut buf = OutputBuffer::new();
1061 buf.push_text("hello");
1062 buf.push_newline();
1063 buf.push_glue();
1064 buf.push_text("world");
1065 assert_eq!(buf.flush(), "helloworld");
1066 }
1067
1068 #[test]
1069 fn glue_preserves_leading_whitespace_in_text() {
1070 let mut buf = OutputBuffer::new();
1071 buf.push_text("hello");
1072 buf.push_newline();
1073 buf.push_glue();
1074 buf.push_text(" world");
1075 assert_eq!(buf.flush(), "hello world");
1076 }
1077
1078 #[test]
1079 fn double_flush_is_empty() {
1080 let mut buf = OutputBuffer::new();
1081 buf.push_text("hello");
1082 let _ = buf.flush();
1083 assert_eq!(buf.flush(), "");
1084 }
1085
1086 #[test]
1087 fn leading_newline_suppressed() {
1088 let mut buf = OutputBuffer::new();
1089 buf.push_newline();
1090 buf.push_text("hello");
1091 assert_eq!(buf.flush(), "hello");
1092 }
1093
1094 #[test]
1098 fn leading_whitespace_only_text_suppressed() {
1099 let mut buf = OutputBuffer::new();
1100 buf.push_text(" ");
1101 buf.push_text("hello");
1102 assert_eq!(buf.flush(), "hello");
1103 }
1104
1105 #[test]
1109 fn adjacent_whitespace_collapsed() {
1110 let mut buf = OutputBuffer::new();
1111 buf.push_text("Hello ");
1112 buf.push_text(" right back");
1113 assert_eq!(buf.flush(), "Hello right back");
1114 }
1115
1116 #[test]
1117 fn leading_whitespace_after_flush_suppressed() {
1118 let mut buf = OutputBuffer::new();
1119 buf.push_text("first");
1120 let _ = buf.flush();
1121 buf.push_text(" ");
1122 buf.push_text("second");
1123 assert_eq!(buf.flush(), "second");
1124 }
1125
1126 #[test]
1127 fn duplicate_newline_suppressed() {
1128 let mut buf = OutputBuffer::new();
1129 buf.push_text("hello");
1130 buf.push_newline();
1131 buf.push_newline();
1132 buf.push_text("world");
1133 assert_eq!(buf.flush(), "hello\nworld");
1134 }
1135
1136 #[test]
1137 fn leading_newline_after_flush_suppressed() {
1138 let mut buf = OutputBuffer::new();
1139 buf.push_text("first");
1140 let _ = buf.flush();
1141 buf.push_newline();
1143 buf.push_text("second");
1144 assert_eq!(buf.flush(), "second");
1145 }
1146
1147 #[test]
1148 fn begin_end_capture_basic() {
1149 let mut buf = OutputBuffer::new();
1150 buf.push_text("before");
1151 buf.begin_capture();
1152 buf.push_text("captured");
1153 let result = buf.test_end_capture();
1154 assert_eq!(result, Some("captured".to_owned()));
1155 assert_eq!(buf.flush(), "before");
1156 }
1157
1158 #[test]
1159 fn nested_captures() {
1160 let mut buf = OutputBuffer::new();
1161 buf.push_text("outer");
1162 buf.begin_capture();
1163 buf.push_text("middle");
1164 buf.begin_capture();
1165 buf.push_text("inner");
1166 let inner = buf.test_end_capture();
1167 assert_eq!(inner, Some("inner".to_owned()));
1168 let middle = buf.test_end_capture();
1169 assert_eq!(middle, Some("middle".to_owned()));
1170 assert_eq!(buf.flush(), "outer");
1171 }
1172
1173 #[test]
1174 fn capture_with_glue() {
1175 let mut buf = OutputBuffer::new();
1176 buf.begin_capture();
1177 buf.push_text("hello");
1178 buf.push_newline();
1179 buf.push_glue();
1180 buf.push_text(" world");
1181 let result = buf.test_end_capture();
1182 assert_eq!(result, Some("hello world".to_owned()));
1183 }
1184
1185 #[test]
1186 fn end_capture_no_checkpoint_returns_none() {
1187 let mut buf = OutputBuffer::new();
1188 buf.push_text("hello");
1189 assert_eq!(buf.test_end_capture(), None);
1190 }
1191
1192 #[test]
1193 fn has_content_respects_checkpoint() {
1194 let mut buf = OutputBuffer::new();
1195 buf.push_text("before");
1196 buf.begin_capture();
1197 assert!(!buf.has_content());
1199 buf.push_text("after");
1200 assert!(buf.has_content());
1201 }
1202
1203 #[test]
1206 fn glue_eats_following_newline() {
1207 let mut buf = OutputBuffer::new();
1208 buf.push_text("fifty");
1209 buf.push_newline();
1210 buf.push_glue();
1211 buf.push_text("-");
1212 buf.push_glue();
1213 buf.push_newline();
1214 buf.push_text("eight");
1215 assert_eq!(buf.flush(), "fifty-eight");
1216 }
1217
1218 #[test]
1223 fn trailing_whitespace_before_newline_trimmed() {
1224 let mut buf = OutputBuffer::new();
1225 buf.push_text("A ");
1226 buf.push_newline();
1227 buf.push_text("X");
1228 assert_eq!(buf.flush(), "A\nX");
1229 }
1230
1231 #[test]
1235 fn glue_preserves_text_whitespace() {
1236 let mut buf = OutputBuffer::new();
1237 buf.push_text("Some ");
1238 buf.push_glue();
1239 buf.push_newline();
1240 buf.push_text("content");
1241 buf.push_glue();
1242 buf.push_text(" with glue.");
1243 assert_eq!(buf.flush(), "Some content with glue.");
1244 }
1245
1246 #[test]
1250 fn glue_skips_whitespace_only_text_to_find_newline() {
1251 let mut buf = OutputBuffer::new();
1252 buf.push_text("a");
1253 buf.push_newline();
1254 buf.push_text(" ");
1255 buf.push_glue();
1256 buf.push_text("b");
1257 assert_eq!(buf.flush(), "a b");
1258 }
1259
1260 #[test]
1264 fn flush_lines_associates_tags_with_lines() {
1265 let mut buf = OutputBuffer::new();
1266 buf.push_text("line one");
1267 buf.push_newline();
1268 buf.push_text("line two");
1269 buf.push_tag("my_tag".to_string());
1270 buf.push_newline();
1271 buf.push_text("line three");
1272 let lines = buf.test_flush_lines();
1273 assert_eq!(lines.len(), 3);
1274 assert_eq!(lines[0].0, "line one");
1275 assert!(lines[0].1.is_empty());
1276 assert_eq!(lines[1].0, "line two");
1277 assert_eq!(lines[1].1, vec!["my_tag"]);
1278 assert_eq!(lines[2].0, "line three");
1279 assert!(lines[2].1.is_empty());
1280 }
1281
1282 #[test]
1284 fn flush_lines_tag_on_last_line() {
1285 let mut buf = OutputBuffer::new();
1286 buf.push_text("only line");
1287 buf.push_tag("t".to_string());
1288 let lines = buf.test_flush_lines();
1289 assert_eq!(lines.len(), 1);
1290 assert_eq!(lines[0].0, "only line");
1291 assert_eq!(lines[0].1, vec!["t"]);
1292 }
1293
1294 #[test]
1296 fn flush_lines_resolves_glue() {
1297 let mut buf = OutputBuffer::new();
1298 buf.push_text("hello");
1299 buf.push_newline();
1300 buf.push_glue();
1301 buf.push_text(" world");
1302 let lines = buf.test_flush_lines();
1303 assert_eq!(lines.len(), 1);
1304 assert_eq!(lines[0].0, "hello world");
1305 }
1306
1307 #[test]
1312 fn flush_lines_empty_buffer_returns_no_lines() {
1313 let mut buf = OutputBuffer::new();
1314 let lines = buf.test_flush_lines();
1315 assert!(
1316 lines.is_empty(),
1317 "empty buffer should produce no lines, got: {lines:?}"
1318 );
1319 }
1320
1321 #[test]
1324 fn has_completed_line_empty() {
1325 let buf = OutputBuffer::new();
1326 assert!(!buf.has_completed_line());
1327 }
1328
1329 #[test]
1330 fn has_completed_line_text_only() {
1331 let mut buf = OutputBuffer::new();
1332 buf.push_text("hello");
1333 assert!(!buf.has_completed_line());
1334 }
1335
1336 #[test]
1337 fn has_completed_line_text_newline_only() {
1338 let mut buf = OutputBuffer::new();
1339 buf.push_text("hello");
1340 buf.push_newline();
1341 assert!(!buf.has_completed_line());
1343 }
1344
1345 #[test]
1346 fn has_completed_line_text_newline_text() {
1347 let mut buf = OutputBuffer::new();
1348 buf.push_text("hello");
1349 buf.push_newline();
1350 buf.push_text("world");
1351 assert!(buf.has_completed_line());
1352 }
1353
1354 #[test]
1355 fn has_completed_line_glue_eats_newline() {
1356 let mut buf = OutputBuffer::new();
1357 buf.push_text("hello");
1358 buf.push_newline();
1359 buf.push_glue();
1360 buf.push_text("world");
1361 assert!(!buf.has_completed_line());
1363 }
1364
1365 #[test]
1366 fn has_completed_line_during_capture() {
1367 let mut buf = OutputBuffer::new();
1368 buf.push_text("hello");
1369 buf.push_newline();
1370 buf.push_text("world");
1371 buf.begin_capture();
1372 assert!(!buf.has_completed_line());
1374 }
1375
1376 #[test]
1377 fn take_first_line_basic() {
1378 let mut buf = OutputBuffer::new();
1379 buf.push_text("hello");
1380 buf.push_newline();
1381 buf.push_text("world");
1382
1383 let result = buf.test_take_first_line();
1384 assert!(result.is_some());
1385 let (text, tags) = result.unwrap();
1386 assert_eq!(text, "hello\n");
1387 assert!(tags.is_empty());
1388
1389 assert_eq!(buf.flush(), "world");
1391 }
1392
1393 #[test]
1394 fn take_first_line_with_tags() {
1395 let mut buf = OutputBuffer::new();
1396 buf.push_text("tagged line");
1397 buf.push_tag("my_tag".to_string());
1398 buf.push_newline();
1399 buf.push_text("next line");
1400
1401 let (text, tags) = buf.test_take_first_line().unwrap();
1402 assert_eq!(text, "tagged line\n");
1403 assert_eq!(tags, vec!["my_tag"]);
1404
1405 assert_eq!(buf.flush(), "next line");
1406 }
1407
1408 #[test]
1409 fn take_first_line_multiple_lines() {
1410 let mut buf = OutputBuffer::new();
1411 buf.push_text("line one");
1412 buf.push_newline();
1413 buf.push_text("line two");
1414 buf.push_newline();
1415 buf.push_text("line three");
1416
1417 let (text1, _) = buf.test_take_first_line().unwrap();
1418 assert_eq!(text1, "line one\n");
1419
1420 let (text2, _) = buf.test_take_first_line().unwrap();
1421 assert_eq!(text2, "line two\n");
1422
1423 assert!(!buf.has_completed_line());
1425 assert_eq!(buf.flush(), "line three");
1426 }
1427
1428 #[test]
1429 fn take_first_line_matches_flush_lines() {
1430 let parts = |buf: &mut OutputBuffer| {
1432 buf.push_text("A ");
1433 buf.push_tag("t1".to_string());
1434 buf.push_newline();
1435 buf.push_text("B");
1436 buf.push_newline();
1437 buf.push_text("C");
1438 };
1439
1440 let mut buf1 = OutputBuffer::new();
1441 parts(&mut buf1);
1442 let all_lines = buf1.test_flush_lines();
1443 let first_from_flush = &all_lines[0].0;
1444
1445 let mut buf2 = OutputBuffer::new();
1446 parts(&mut buf2);
1447 let (first_from_take, tags) = buf2.test_take_first_line().unwrap();
1448 let first_trimmed = first_from_take.trim_end_matches('\n');
1450
1451 assert_eq!(first_trimmed, first_from_flush);
1452 assert_eq!(tags, all_lines[0].1);
1453 }
1454
1455 #[test]
1456 fn take_first_line_glue_preserves_subsequent() {
1457 let mut buf = OutputBuffer::new();
1459 buf.push_text("hello");
1460 buf.push_newline();
1461 buf.push_glue();
1462 buf.push_text(" world");
1463 buf.push_newline();
1464 buf.push_text("next");
1465
1466 let (text, _) = buf.test_take_first_line().unwrap();
1467 assert_eq!(text, "hello world\n");
1468 assert_eq!(buf.flush(), "next");
1469 }
1470
1471 #[test]
1472 fn take_first_line_none_when_empty() {
1473 let mut buf = OutputBuffer::new();
1474 assert!(buf.test_take_first_line().is_none());
1475 }
1476
1477 #[test]
1478 fn take_first_line_none_when_no_newline() {
1479 let mut buf = OutputBuffer::new();
1480 buf.push_text("no newline");
1481 assert!(buf.test_take_first_line().is_none());
1482 }
1483
1484 fn resolve_template(parts: Vec<LinePart>, slots: &[Value]) -> String {
1489 use crate::program::LinkedContainer;
1490 use brink_format::{CountingFlags, DefinitionId, DefinitionTag, LineEntry, LineFlags};
1491 use std::collections::HashMap;
1492
1493 let id = DefinitionId::new(DefinitionTag::Address, 0);
1494 let program = Program {
1495 containers: vec![LinkedContainer {
1496 id,
1497 bytecode: vec![],
1498 counting_flags: CountingFlags::empty(),
1499 path_hash: 0,
1500 param_count: 0,
1501 scope_table_idx: 0,
1502 }],
1503 address_map: HashMap::new(),
1504 scope_ids: vec![id],
1505 source_checksum: 0,
1506 globals: vec![],
1507 global_map: HashMap::new(),
1508 name_table: vec![],
1509 address_by_path: HashMap::new(),
1510 root_idx: 0,
1511 list_literals: vec![],
1512 list_item_map: HashMap::new(),
1513 list_defs: vec![],
1514 list_def_map: HashMap::new(),
1515 external_fns: HashMap::new(),
1516 local_scope_defaults: Vec::new(),
1517 };
1518
1519 let line_tables = vec![vec![LineEntry {
1520 content: LineContent::Template(parts),
1521 source_hash: 0,
1522 flags: LineFlags::empty(),
1523 audio_ref: None,
1524 slot_info: vec![],
1525 source_location: None,
1526 }]];
1527
1528 resolve_line_ref(&program, &line_tables, 0, 0, slots, None, &[])
1529 }
1530
1531 #[test]
1532 fn template_collapses_double_space_from_empty_slot() {
1533 let result = resolve_template(
1534 vec![
1535 LinePart::Literal("Hello ".into()),
1536 LinePart::Slot(0),
1537 LinePart::Literal(" world".into()),
1538 ],
1539 &[Value::Null],
1540 );
1541 assert_eq!(result, "Hello world");
1542 }
1543
1544 #[test]
1545 fn template_preserves_spaces_with_nonempty_slot() {
1546 let result = resolve_template(
1547 vec![
1548 LinePart::Literal("Hello ".into()),
1549 LinePart::Slot(0),
1550 LinePart::Literal(" world".into()),
1551 ],
1552 &[Value::String("dear".into())],
1553 );
1554 assert_eq!(result, "Hello dear world");
1555 }
1556
1557 #[test]
1558 fn template_multiple_empty_slots_collapse() {
1559 let result = resolve_template(
1560 vec![
1561 LinePart::Literal("a ".into()),
1562 LinePart::Slot(0),
1563 LinePart::Literal(" ".into()),
1564 LinePart::Slot(1),
1565 LinePart::Literal(" b".into()),
1566 ],
1567 &[Value::Null, Value::Null],
1568 );
1569 assert_eq!(result, "a b");
1570 }
1571
1572 #[test]
1573 fn template_empty_string_slot_same_as_null() {
1574 let result = resolve_template(
1575 vec![
1576 LinePart::Literal("Hello ".into()),
1577 LinePart::Slot(0),
1578 LinePart::Literal(" world".into()),
1579 ],
1580 &[Value::String("".into())],
1581 );
1582 assert_eq!(result, "Hello world");
1583 }
1584}