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 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 impl OutputBuffer {
1025 fn test_flush_lines(&mut self) -> Vec<(String, Vec<String>)> {
1026 let p = test_dummy_program();
1027 self.flush_lines(&p, &[], None)
1028 }
1029
1030 fn test_take_first_line(&mut self) -> Option<(String, Vec<String>)> {
1031 let p = test_dummy_program();
1032 self.take_first_line(&p, &[], None)
1033 }
1034
1035 fn test_end_capture(&mut self) -> Option<String> {
1036 let p = test_dummy_program();
1037 self.end_capture(&p, &[], None)
1038 }
1039 }
1040
1041 #[test]
1042 fn simple_text() {
1043 let mut buf = OutputBuffer::new();
1044 buf.push_text("hello");
1045 assert_eq!(buf.flush(), "hello");
1046 }
1047
1048 #[test]
1049 fn text_with_newline() {
1050 let mut buf = OutputBuffer::new();
1051 buf.push_text("hello");
1052 buf.push_newline();
1053 buf.push_text("world");
1054 assert_eq!(buf.flush(), "hello\nworld");
1055 }
1056
1057 #[test]
1058 fn glue_removes_newline() {
1059 let mut buf = OutputBuffer::new();
1060 buf.push_text("hello");
1061 buf.push_newline();
1062 buf.push_glue();
1063 buf.push_text("world");
1064 assert_eq!(buf.flush(), "helloworld");
1065 }
1066
1067 #[test]
1068 fn glue_preserves_leading_whitespace_in_text() {
1069 let mut buf = OutputBuffer::new();
1070 buf.push_text("hello");
1071 buf.push_newline();
1072 buf.push_glue();
1073 buf.push_text(" world");
1074 assert_eq!(buf.flush(), "hello world");
1075 }
1076
1077 #[test]
1078 fn double_flush_is_empty() {
1079 let mut buf = OutputBuffer::new();
1080 buf.push_text("hello");
1081 let _ = buf.flush();
1082 assert_eq!(buf.flush(), "");
1083 }
1084
1085 #[test]
1086 fn leading_newline_suppressed() {
1087 let mut buf = OutputBuffer::new();
1088 buf.push_newline();
1089 buf.push_text("hello");
1090 assert_eq!(buf.flush(), "hello");
1091 }
1092
1093 #[test]
1097 fn leading_whitespace_only_text_suppressed() {
1098 let mut buf = OutputBuffer::new();
1099 buf.push_text(" ");
1100 buf.push_text("hello");
1101 assert_eq!(buf.flush(), "hello");
1102 }
1103
1104 #[test]
1108 fn adjacent_whitespace_collapsed() {
1109 let mut buf = OutputBuffer::new();
1110 buf.push_text("Hello ");
1111 buf.push_text(" right back");
1112 assert_eq!(buf.flush(), "Hello right back");
1113 }
1114
1115 #[test]
1116 fn leading_whitespace_after_flush_suppressed() {
1117 let mut buf = OutputBuffer::new();
1118 buf.push_text("first");
1119 let _ = buf.flush();
1120 buf.push_text(" ");
1121 buf.push_text("second");
1122 assert_eq!(buf.flush(), "second");
1123 }
1124
1125 #[test]
1126 fn duplicate_newline_suppressed() {
1127 let mut buf = OutputBuffer::new();
1128 buf.push_text("hello");
1129 buf.push_newline();
1130 buf.push_newline();
1131 buf.push_text("world");
1132 assert_eq!(buf.flush(), "hello\nworld");
1133 }
1134
1135 #[test]
1136 fn leading_newline_after_flush_suppressed() {
1137 let mut buf = OutputBuffer::new();
1138 buf.push_text("first");
1139 let _ = buf.flush();
1140 buf.push_newline();
1142 buf.push_text("second");
1143 assert_eq!(buf.flush(), "second");
1144 }
1145
1146 #[test]
1147 fn begin_end_capture_basic() {
1148 let mut buf = OutputBuffer::new();
1149 buf.push_text("before");
1150 buf.begin_capture();
1151 buf.push_text("captured");
1152 let result = buf.test_end_capture();
1153 assert_eq!(result, Some("captured".to_owned()));
1154 assert_eq!(buf.flush(), "before");
1155 }
1156
1157 #[test]
1158 fn nested_captures() {
1159 let mut buf = OutputBuffer::new();
1160 buf.push_text("outer");
1161 buf.begin_capture();
1162 buf.push_text("middle");
1163 buf.begin_capture();
1164 buf.push_text("inner");
1165 let inner = buf.test_end_capture();
1166 assert_eq!(inner, Some("inner".to_owned()));
1167 let middle = buf.test_end_capture();
1168 assert_eq!(middle, Some("middle".to_owned()));
1169 assert_eq!(buf.flush(), "outer");
1170 }
1171
1172 #[test]
1173 fn capture_with_glue() {
1174 let mut buf = OutputBuffer::new();
1175 buf.begin_capture();
1176 buf.push_text("hello");
1177 buf.push_newline();
1178 buf.push_glue();
1179 buf.push_text(" world");
1180 let result = buf.test_end_capture();
1181 assert_eq!(result, Some("hello world".to_owned()));
1182 }
1183
1184 #[test]
1185 fn end_capture_no_checkpoint_returns_none() {
1186 let mut buf = OutputBuffer::new();
1187 buf.push_text("hello");
1188 assert_eq!(buf.test_end_capture(), None);
1189 }
1190
1191 #[test]
1192 fn has_content_respects_checkpoint() {
1193 let mut buf = OutputBuffer::new();
1194 buf.push_text("before");
1195 buf.begin_capture();
1196 assert!(!buf.has_content());
1198 buf.push_text("after");
1199 assert!(buf.has_content());
1200 }
1201
1202 #[test]
1205 fn glue_eats_following_newline() {
1206 let mut buf = OutputBuffer::new();
1207 buf.push_text("fifty");
1208 buf.push_newline();
1209 buf.push_glue();
1210 buf.push_text("-");
1211 buf.push_glue();
1212 buf.push_newline();
1213 buf.push_text("eight");
1214 assert_eq!(buf.flush(), "fifty-eight");
1215 }
1216
1217 #[test]
1222 fn trailing_whitespace_before_newline_trimmed() {
1223 let mut buf = OutputBuffer::new();
1224 buf.push_text("A ");
1225 buf.push_newline();
1226 buf.push_text("X");
1227 assert_eq!(buf.flush(), "A\nX");
1228 }
1229
1230 #[test]
1234 fn glue_preserves_text_whitespace() {
1235 let mut buf = OutputBuffer::new();
1236 buf.push_text("Some ");
1237 buf.push_glue();
1238 buf.push_newline();
1239 buf.push_text("content");
1240 buf.push_glue();
1241 buf.push_text(" with glue.");
1242 assert_eq!(buf.flush(), "Some content with glue.");
1243 }
1244
1245 #[test]
1249 fn glue_skips_whitespace_only_text_to_find_newline() {
1250 let mut buf = OutputBuffer::new();
1251 buf.push_text("a");
1252 buf.push_newline();
1253 buf.push_text(" ");
1254 buf.push_glue();
1255 buf.push_text("b");
1256 assert_eq!(buf.flush(), "a b");
1257 }
1258
1259 #[test]
1263 fn flush_lines_associates_tags_with_lines() {
1264 let mut buf = OutputBuffer::new();
1265 buf.push_text("line one");
1266 buf.push_newline();
1267 buf.push_text("line two");
1268 buf.push_tag("my_tag".to_string());
1269 buf.push_newline();
1270 buf.push_text("line three");
1271 let lines = buf.test_flush_lines();
1272 assert_eq!(lines.len(), 3);
1273 assert_eq!(lines[0].0, "line one");
1274 assert!(lines[0].1.is_empty());
1275 assert_eq!(lines[1].0, "line two");
1276 assert_eq!(lines[1].1, vec!["my_tag"]);
1277 assert_eq!(lines[2].0, "line three");
1278 assert!(lines[2].1.is_empty());
1279 }
1280
1281 #[test]
1283 fn flush_lines_tag_on_last_line() {
1284 let mut buf = OutputBuffer::new();
1285 buf.push_text("only line");
1286 buf.push_tag("t".to_string());
1287 let lines = buf.test_flush_lines();
1288 assert_eq!(lines.len(), 1);
1289 assert_eq!(lines[0].0, "only line");
1290 assert_eq!(lines[0].1, vec!["t"]);
1291 }
1292
1293 #[test]
1295 fn flush_lines_resolves_glue() {
1296 let mut buf = OutputBuffer::new();
1297 buf.push_text("hello");
1298 buf.push_newline();
1299 buf.push_glue();
1300 buf.push_text(" world");
1301 let lines = buf.test_flush_lines();
1302 assert_eq!(lines.len(), 1);
1303 assert_eq!(lines[0].0, "hello world");
1304 }
1305
1306 #[test]
1311 fn flush_lines_empty_buffer_returns_no_lines() {
1312 let mut buf = OutputBuffer::new();
1313 let lines = buf.test_flush_lines();
1314 assert!(
1315 lines.is_empty(),
1316 "empty buffer should produce no lines, got: {lines:?}"
1317 );
1318 }
1319
1320 #[test]
1323 fn has_completed_line_empty() {
1324 let buf = OutputBuffer::new();
1325 assert!(!buf.has_completed_line());
1326 }
1327
1328 #[test]
1329 fn has_completed_line_text_only() {
1330 let mut buf = OutputBuffer::new();
1331 buf.push_text("hello");
1332 assert!(!buf.has_completed_line());
1333 }
1334
1335 #[test]
1336 fn has_completed_line_text_newline_only() {
1337 let mut buf = OutputBuffer::new();
1338 buf.push_text("hello");
1339 buf.push_newline();
1340 assert!(!buf.has_completed_line());
1342 }
1343
1344 #[test]
1345 fn has_completed_line_text_newline_text() {
1346 let mut buf = OutputBuffer::new();
1347 buf.push_text("hello");
1348 buf.push_newline();
1349 buf.push_text("world");
1350 assert!(buf.has_completed_line());
1351 }
1352
1353 #[test]
1354 fn has_completed_line_glue_eats_newline() {
1355 let mut buf = OutputBuffer::new();
1356 buf.push_text("hello");
1357 buf.push_newline();
1358 buf.push_glue();
1359 buf.push_text("world");
1360 assert!(!buf.has_completed_line());
1362 }
1363
1364 #[test]
1365 fn has_completed_line_during_capture() {
1366 let mut buf = OutputBuffer::new();
1367 buf.push_text("hello");
1368 buf.push_newline();
1369 buf.push_text("world");
1370 buf.begin_capture();
1371 assert!(!buf.has_completed_line());
1373 }
1374
1375 #[test]
1376 fn take_first_line_basic() {
1377 let mut buf = OutputBuffer::new();
1378 buf.push_text("hello");
1379 buf.push_newline();
1380 buf.push_text("world");
1381
1382 let result = buf.test_take_first_line();
1383 assert!(result.is_some());
1384 let (text, tags) = result.unwrap();
1385 assert_eq!(text, "hello\n");
1386 assert!(tags.is_empty());
1387
1388 assert_eq!(buf.flush(), "world");
1390 }
1391
1392 #[test]
1393 fn take_first_line_with_tags() {
1394 let mut buf = OutputBuffer::new();
1395 buf.push_text("tagged line");
1396 buf.push_tag("my_tag".to_string());
1397 buf.push_newline();
1398 buf.push_text("next line");
1399
1400 let (text, tags) = buf.test_take_first_line().unwrap();
1401 assert_eq!(text, "tagged line\n");
1402 assert_eq!(tags, vec!["my_tag"]);
1403
1404 assert_eq!(buf.flush(), "next line");
1405 }
1406
1407 #[test]
1408 fn take_first_line_multiple_lines() {
1409 let mut buf = OutputBuffer::new();
1410 buf.push_text("line one");
1411 buf.push_newline();
1412 buf.push_text("line two");
1413 buf.push_newline();
1414 buf.push_text("line three");
1415
1416 let (text1, _) = buf.test_take_first_line().unwrap();
1417 assert_eq!(text1, "line one\n");
1418
1419 let (text2, _) = buf.test_take_first_line().unwrap();
1420 assert_eq!(text2, "line two\n");
1421
1422 assert!(!buf.has_completed_line());
1424 assert_eq!(buf.flush(), "line three");
1425 }
1426
1427 #[test]
1428 fn take_first_line_matches_flush_lines() {
1429 let parts = |buf: &mut OutputBuffer| {
1431 buf.push_text("A ");
1432 buf.push_tag("t1".to_string());
1433 buf.push_newline();
1434 buf.push_text("B");
1435 buf.push_newline();
1436 buf.push_text("C");
1437 };
1438
1439 let mut buf1 = OutputBuffer::new();
1440 parts(&mut buf1);
1441 let all_lines = buf1.test_flush_lines();
1442 let first_from_flush = &all_lines[0].0;
1443
1444 let mut buf2 = OutputBuffer::new();
1445 parts(&mut buf2);
1446 let (first_from_take, tags) = buf2.test_take_first_line().unwrap();
1447 let first_trimmed = first_from_take.trim_end_matches('\n');
1449
1450 assert_eq!(first_trimmed, first_from_flush);
1451 assert_eq!(tags, all_lines[0].1);
1452 }
1453
1454 #[test]
1455 fn take_first_line_glue_preserves_subsequent() {
1456 let mut buf = OutputBuffer::new();
1458 buf.push_text("hello");
1459 buf.push_newline();
1460 buf.push_glue();
1461 buf.push_text(" world");
1462 buf.push_newline();
1463 buf.push_text("next");
1464
1465 let (text, _) = buf.test_take_first_line().unwrap();
1466 assert_eq!(text, "hello world\n");
1467 assert_eq!(buf.flush(), "next");
1468 }
1469
1470 #[test]
1471 fn take_first_line_none_when_empty() {
1472 let mut buf = OutputBuffer::new();
1473 assert!(buf.test_take_first_line().is_none());
1474 }
1475
1476 #[test]
1477 fn take_first_line_none_when_no_newline() {
1478 let mut buf = OutputBuffer::new();
1479 buf.push_text("no newline");
1480 assert!(buf.test_take_first_line().is_none());
1481 }
1482
1483 fn resolve_template(parts: Vec<LinePart>, slots: &[Value]) -> String {
1488 use crate::program::LinkedContainer;
1489 use brink_format::{CountingFlags, DefinitionId, DefinitionTag, LineEntry, LineFlags};
1490 use std::collections::HashMap;
1491
1492 let id = DefinitionId::new(DefinitionTag::Address, 0);
1493 let program = Program {
1494 containers: vec![LinkedContainer {
1495 id,
1496 bytecode: vec![],
1497 counting_flags: CountingFlags::empty(),
1498 path_hash: 0,
1499 param_count: 0,
1500 scope_table_idx: 0,
1501 }],
1502 address_map: HashMap::new(),
1503 scope_ids: vec![id],
1504 source_checksum: 0,
1505 globals: vec![],
1506 global_map: HashMap::new(),
1507 name_table: vec![],
1508 address_by_path: HashMap::new(),
1509 root_idx: 0,
1510 list_literals: vec![],
1511 list_item_map: HashMap::new(),
1512 list_defs: vec![],
1513 list_def_map: HashMap::new(),
1514 external_fns: HashMap::new(),
1515 };
1516
1517 let line_tables = vec![vec![LineEntry {
1518 content: LineContent::Template(parts),
1519 source_hash: 0,
1520 flags: LineFlags::empty(),
1521 audio_ref: None,
1522 slot_info: vec![],
1523 source_location: None,
1524 }]];
1525
1526 resolve_line_ref(&program, &line_tables, 0, 0, slots, None, &[])
1527 }
1528
1529 #[test]
1530 fn template_collapses_double_space_from_empty_slot() {
1531 let result = resolve_template(
1532 vec![
1533 LinePart::Literal("Hello ".into()),
1534 LinePart::Slot(0),
1535 LinePart::Literal(" world".into()),
1536 ],
1537 &[Value::Null],
1538 );
1539 assert_eq!(result, "Hello world");
1540 }
1541
1542 #[test]
1543 fn template_preserves_spaces_with_nonempty_slot() {
1544 let result = resolve_template(
1545 vec![
1546 LinePart::Literal("Hello ".into()),
1547 LinePart::Slot(0),
1548 LinePart::Literal(" world".into()),
1549 ],
1550 &[Value::String("dear".into())],
1551 );
1552 assert_eq!(result, "Hello dear world");
1553 }
1554
1555 #[test]
1556 fn template_multiple_empty_slots_collapse() {
1557 let result = resolve_template(
1558 vec![
1559 LinePart::Literal("a ".into()),
1560 LinePart::Slot(0),
1561 LinePart::Literal(" ".into()),
1562 LinePart::Slot(1),
1563 LinePart::Literal(" b".into()),
1564 ],
1565 &[Value::Null, Value::Null],
1566 );
1567 assert_eq!(result, "a b");
1568 }
1569
1570 #[test]
1571 fn template_empty_string_slot_same_as_null() {
1572 let result = resolve_template(
1573 vec![
1574 LinePart::Literal("Hello ".into()),
1575 LinePart::Slot(0),
1576 LinePart::Literal(" world".into()),
1577 ],
1578 &[Value::String("".into())],
1579 );
1580 assert_eq!(result, "Hello world");
1581 }
1582}