1use crate::content::Inline;
25use crate::fonts::FontRegistry;
26use crate::linebox::{LineBox, Strut};
27use icu_segmenter::{WordSegmenter, options::WordBreakInvariantOptions};
28
29mod breaker;
30mod flatten;
31mod inline;
32mod justify;
33mod line;
34mod measure;
35mod opportunity;
36mod paragraph;
37mod shape;
38
39#[cfg(test)]
40mod testing;
41
42pub use line::{InlineFragment, Line, LineSpan, ShapedRun, Spans};
43pub use measure::{Measure, Span};
44pub use paragraph::{
45 Face, FirstLine, Generated, HangEnd, HangingPunctuation, Inherited, InlineBox, InlineStyles,
46 LineBreakOptions, Opening, ParagraphStyle, Patterns,
47};
48
49use breaker::Breaker;
50use flatten::FlatParagraph;
51use justify::adjust;
52use line::{Widths, cut_runs, gather, tile};
53use paragraph::Lead;
54use shape::ShapedSpan;
55
56pub struct LineLayout<'a> {
58 registry: &'a FontRegistry,
59 segmenter: WordSegmenterBorrowedStatic,
60}
61
62type WordSegmenterBorrowedStatic = icu_segmenter::WordSegmenterBorrowed<'static>;
64
65impl<'a> LineLayout<'a> {
66 pub fn new(registry: &'a FontRegistry) -> Self {
68 LineLayout {
69 registry,
70 segmenter: WordSegmenter::new_auto(WordBreakInvariantOptions::default()),
71 }
72 }
73}
74
75impl LineLayout<'_> {
76 pub fn strut(&self, style: &ParagraphStyle) -> Strut {
79 self.registry
80 .metrics(style.font_id)
81 .map(|m| Strut::from_metrics(m, style.size, style.line_height))
82 .unwrap_or_default()
83 }
84
85 pub fn line_box(&self, runs: &[ShapedRun], style: &ParagraphStyle) -> LineBox {
88 let strut = self.strut(style);
89 let mut above = strut.above;
90 let mut below = strut.below;
91 for run in runs {
92 let Some(metrics) = self.registry.metrics(run.font_id) else {
93 continue;
94 };
95 let run_strut = Strut::from_metrics(metrics, run.size, style.line_height);
96 above = above.max(run_strut.above);
97 below = below.max(run_strut.below);
98 }
99 LineBox {
100 baseline: above,
101 height: above + below,
102 }
103 }
104
105 pub fn layout(
108 &self,
109 inlines: &[Inline],
110 style: &ParagraphStyle,
111 measure: impl Into<Measure>,
112 options: LineBreakOptions,
113 ) -> Vec<Line> {
114 self.layout_styled(
115 inlines,
116 style,
117 &Inherited,
118 &measure.into(),
119 options,
120 Opening::default(),
121 )
122 }
123
124 pub fn layout_styled(
128 &self,
129 inlines: &[Inline],
130 style: &ParagraphStyle,
131 styles: &dyn InlineStyles,
132 measure: &Measure,
133 options: LineBreakOptions,
134 opening: Opening,
135 ) -> Vec<Line> {
136 self.broken(inlines, style, styles, measure, options, opening)
137 .0
138 }
139
140 pub fn layout_shaped(
146 &self,
147 inlines: &[Inline],
148 style: &ParagraphStyle,
149 styles: &dyn InlineStyles,
150 measure: &Measure,
151 options: LineBreakOptions,
152 opening: Opening,
153 ) -> (Broken, Option<Shaped>) {
154 let (broken, _, shaped) =
155 self.broken_shaped(inlines, style, styles, measure, options, opening);
156 (broken, shaped)
157 }
158
159 pub fn rebreak(&self, shaped: &Shaped, measure: &Measure, from: usize) -> Broken {
167 self.break_shaped(shaped, measure, from, None)
168 }
169
170 fn broken(
183 &self,
184 inlines: &[Inline],
185 style: &ParagraphStyle,
186 styles: &dyn InlineStyles,
187 measure: &Measure,
188 options: LineBreakOptions,
189 opening: Opening,
190 ) -> (Vec<Line>, u8) {
191 let (broken, runs, _) =
192 self.broken_shaped(inlines, style, styles, measure, options, opening);
193 (broken.lines, runs)
194 }
195
196 fn broken_shaped(
198 &self,
199 inlines: &[Inline],
200 style: &ParagraphStyle,
201 styles: &dyn InlineStyles,
202 measure: &Measure,
203 options: LineBreakOptions,
204 opening: Opening,
205 ) -> (Broken, u8, Option<Shaped>) {
206 let lead = Lead {
207 style: opening.first_line,
208 extent: None,
209 taken: opening.taken,
210 pseudo_element: (opening.first_line.is_some()
211 && opening.node != crate::content::NodeId::UNASSIGNED)
212 .then(|| {
213 opening
214 .node
215 .pseudo(crate::content::PseudoElement::FirstLine)
216 }),
217 };
218 let once = |lead| {
219 let shaped = self.shaped(inlines, style, styles, options, lead);
220 let broken = shaped
221 .as_ref()
222 .map(|shaped| self.break_shaped(shaped, measure, 0, shaped.opening))
223 .unwrap_or_default();
224 (broken, shaped)
225 };
226 let (broken, shaped) = once(lead);
227 if lead.style.is_none() || broken.extent == 0 {
228 return (broken, 1, shaped);
229 }
230 let lead = Lead {
231 extent: Some(broken.extent),
232 ..lead
233 };
234 let (broken, shaped) = once(lead);
235 (broken, 2, shaped)
236 }
237
238 fn shaped(
244 &self,
245 inlines: &[Inline],
246 style: &ParagraphStyle,
247 styles: &dyn InlineStyles,
248 options: LineBreakOptions,
249 lead: Lead,
250 ) -> Option<Shaped> {
251 let flat = self.flatten(inlines, style, styles, lead);
252 self.shape_flat(flat, style, options, lead.extent)
253 }
254
255 pub(crate) fn layout_generated(
258 &self,
259 text: &str,
260 node: crate::content::NodeId,
261 style: &ParagraphStyle,
262 measure: &Measure,
263 options: LineBreakOptions,
264 ) -> Vec<Line> {
265 let flat = self.flatten_generated(text, node, style);
266 self.shape_flat(flat, style, options, None)
267 .map(|shaped| self.break_shaped(&shaped, measure, 0, None).lines)
268 .unwrap_or_default()
269 }
270
271 pub(crate) fn layout_preformatted(
277 &self,
278 text: &str,
279 node: crate::content::NodeId,
280 style: &ParagraphStyle,
281 measure: &Measure,
282 options: LineBreakOptions,
283 ) -> Vec<Line> {
284 let flat = self.flatten_preformatted(text, node, style);
285 self.shape_flat(flat, style, options, None)
286 .map(|shaped| self.break_shaped(&shaped, measure, 0, None).lines)
287 .unwrap_or_default()
288 }
289
290 fn shape_flat(
293 &self,
294 flat: flatten::FlatParagraph,
295 style: &ParagraphStyle,
296 options: LineBreakOptions,
297 opening: Option<usize>,
298 ) -> Option<Shaped> {
299 if flat.text.is_empty() {
300 return None;
301 }
302 let upem = self.registry.metrics(style.font_id)?.units_per_em as f32;
303 let spans = self.shape_spans(&flat, style, upem);
304 Some(Shaped {
305 flat,
306 spans,
307 style: style.clone(),
308 options,
309 upem,
310 opening,
311 })
312 }
313
314 fn break_shaped(
320 &self,
321 shaped: &Shaped,
322 measure: &Measure,
323 from: usize,
324 opening: Option<usize>,
325 ) -> Broken {
326 let Shaped {
327 flat,
328 spans,
329 style,
330 options,
331 upem,
332 ..
333 } = shaped;
334 let (options, upem) = (*options, *upem);
335 let to_points = |units: f32| units / upem * style.size;
338
339 let units = if style.size > 0.0 {
342 upem / style.size
343 } else {
344 0.0
345 };
346 let widths = Widths::build(flat, spans, units);
347 let hyphen = self.hyphen_advance(style) as f32;
348 let breaks = self.break_points(&flat.text, &widths, hyphen, options);
349 let breaker = Breaker {
350 breaks: &breaks,
351 widths: &widths,
352 measure,
353 rest: measure.at(measure.settled()),
354 settled: measure.settled(),
355 first_band: (0..).find(|slot| measure.at(*slot).ends_band).unwrap_or(0),
356 upem,
357 size: style.size,
358 hyphen,
359 options,
360 from,
361 opening: opening
362 .and_then(|extent| breaks.iter().position(|at| at.content_end == extent)),
363 };
364
365 let mut broken = Broken::default();
366 let mut start = breaks[from.min(breaks.len() - 1)].next;
367 let mut band: Option<(Line, f32)> = None;
372 let mut spans = Vec::new();
373 for fit in breaker.run() {
374 let at = &breaks[fit.at];
375 let span = measure.at(fit.slot);
376 if at.content_end > start {
377 let (line, origin) = band.get_or_insert_with(|| {
378 let mut line = Line::empty();
379 line.protrusion = to_points(fit.protrusion);
380 (line, span.origin)
381 });
382 let first = line.runs.len();
383 line.runs
384 .extend(cut_runs(flat, &shaped.spans, start, at.content_end));
385 adjust(&mut line.runs[first..], &flat.text, fit.ratio, options);
386 if at.hyphen {
387 self.hyphenate(&mut line.runs, style);
388 }
389 let mut boxes = self.inline_fragments(
390 flat,
391 &mut line.runs[first..],
392 spans.len(),
393 start..at.content_end,
394 );
395 line.boxes.append(&mut boxes);
396 let offset = span.origin - *origin;
397 let width = line.runs[first..].iter().map(|run| run.advance).sum();
398 spans.push(LineSpan {
399 runs: first..line.runs.len(),
400 offset,
401 width,
402 });
403 line.width += width;
404 }
405 start = at.next;
406 let hard = at.forced && fit.at != breaker.end();
407 if !span.ends_band && !hard {
408 continue;
409 }
410 if band.is_none() && options.preformatted && hard {
414 spans.push(LineSpan {
415 runs: 0..0,
416 offset: 0.0,
417 width: 0,
418 });
419 band = Some((Line::empty(), span.origin));
420 }
421 let Some((mut line, _)) = band.take() else {
422 continue;
423 };
424 line.overhang = to_points(fit.overhang);
425 line.box_ = self.line_box(&line.runs, style);
426 line.spans = gather(&mut spans);
427 if broken.lines.is_empty() {
428 broken.extent = at.content_end;
429 }
430 broken.lines.push(line);
431 broken.ends.push(fit.at);
432 }
433 if let Some((mut line, _)) = band.take() {
436 line.box_ = self.line_box(&line.runs, style);
437 line.spans = gather(&mut spans);
438 broken.lines.push(line);
439 broken.ends.push(breaker.end());
440 }
441 tile(&mut broken.lines, flat);
442 broken
443 }
444}
445
446pub struct Shaped {
455 flat: FlatParagraph,
456 spans: Vec<ShapedSpan>,
457 style: ParagraphStyle,
458 options: LineBreakOptions,
459 upem: f32,
460 opening: Option<usize>,
463}
464
465impl std::fmt::Debug for Shaped {
466 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467 f.debug_struct("Shaped")
468 .field("text", &self.flat.text)
469 .finish_non_exhaustive()
470 }
471}
472
473impl Shaped {
474 pub fn style(&self) -> &ParagraphStyle {
476 &self.style
477 }
478}
479
480#[derive(Debug, Default)]
483pub struct Broken {
484 pub lines: Vec<Line>,
486 pub ends: Vec<usize>,
490 extent: usize,
492}
493
494#[cfg(test)]
495mod tests {
496 use crate::content::NodeId;
497 use crate::fonts::FaceAttributes;
498 use crate::lines::flatten::SMALL_CAPS_RATIO;
499 use crate::lines::testing::{
500 DISPLAY, OPENING, body, divided_band, drawn, emphasis, layout_body, layout_first,
501 line_text, one_run, registry, two_families, units_per_em, with_breaks,
502 };
503 use crate::lines::{
504 Face, FirstLine, Inherited, InlineStyles, Line, LineBreakOptions, LineLayout, Measure,
505 Opening, ParagraphStyle,
506 };
507 use crate::style::{FontVariantCaps, TextTransform};
508
509 fn faced(face: Face) -> Option<FirstLine> {
511 Some(FirstLine {
512 face: Some(face),
513 ..FirstLine::default()
514 })
515 }
516
517 fn cut(italic: bool, weight: u16) -> u16 {
519 registry()
520 .select("eb garamond", FaceAttributes { italic, weight })
521 .expect("the bundled family has cuts")
522 .id
523 }
524
525 fn face_of(line: &Line) -> u16 {
527 let first = line.runs[0].font_id;
528 assert!(
529 line.runs.iter().all(|run| run.font_id == first),
530 "the line is set in more than one face: {:?}",
531 line.runs.iter().map(|run| run.font_id).collect::<Vec<_>>(),
532 );
533 first
534 }
535
536 #[test]
540 fn a_hard_break_in_a_divided_band_opens_the_next_band() {
541 let layout = LineLayout::new(registry());
542 let lines = layout.layout(
543 &with_breaks("one\ntwo"),
544 &body(),
545 divided_band(100.0, 20.0),
546 LineBreakOptions::default(),
547 );
548 assert_eq!(drawn(&lines), ["one", "two"]);
549 assert_eq!(lines[0].spans.len(), 1, "{:?}", lines[0].spans);
550 assert_eq!(lines[1].spans[0].offset, 0.0);
551 }
552
553 #[test]
557 fn a_shaped_paragraph_breaks_again_to_the_same_lines() {
558 let layout = LineLayout::new(registry());
559 let measure = Measure::uniform(120.0);
560 let (first, shaped) = layout.layout_shaped(
561 &one_run(OPENING),
562 &body(),
563 &Inherited,
564 &measure,
565 LineBreakOptions::default(),
566 Opening::default(),
567 );
568 let shaped = shaped.expect("the paragraph shaped");
569 let lines = first.lines;
570 assert!(lines.len() > 3, "{} lines is too few to cut", lines.len());
571
572 let again = layout.rebreak(&shaped, &measure, 0);
573 assert_eq!(drawn(&again.lines), drawn(&lines));
574 assert_eq!(again.ends.len(), again.lines.len());
575
576 let tail = layout.rebreak(&shaped, &measure, again.ends[1]);
577 assert_eq!(drawn(&tail.lines), drawn(&lines[2..]));
578 }
579
580 #[test]
584 fn a_shaped_paragraph_breaks_again_to_a_narrower_measure() {
585 let layout = LineLayout::new(registry());
586 let (wide, shaped) = layout.layout_shaped(
587 &one_run(OPENING),
588 &body(),
589 &Inherited,
590 &Measure::uniform(200.0),
591 LineBreakOptions::default(),
592 Opening::default(),
593 );
594 let shaped = shaped.expect("the paragraph shaped");
595 let lines = wide.lines;
596 let narrow = Measure::uniform(100.0);
597 let again = layout.rebreak(&shaped, &narrow, 0);
598 assert!(again.lines.len() > lines.len());
599 assert_eq!(
600 drawn(&again.lines).join(" ").replace(" ", " "),
601 drawn(&lines).join(" ").replace(" ", " ")
602 );
603 let upem = units_per_em() as f32;
604 for line in &again.lines {
605 let width = line.width as f32 / upem * body().size;
606 assert!(width <= 100.0 + 0.5, "{width} runs past the measure");
607 }
608 }
609
610 #[test]
612 fn empty_paragraph_yields_no_lines() {
613 assert!(layout_body("", 200.0).is_empty());
614 }
615
616 #[test]
619 fn short_text_is_one_line() {
620 let lines = layout_body("hello", 200.0);
621 assert_eq!(lines.len(), 1);
622 assert_eq!(line_text(&lines[0]), "hello");
623 let glyph_count: usize = lines[0].runs.iter().map(|r| r.glyphs.len()).sum();
624 assert_eq!(glyph_count, 5);
625 }
626
627 #[test]
630 fn text_wraps_and_preserves_every_word() {
631 let text = "one two three four five six seven eight";
632 let lines = layout_body(text, 60.0);
633 assert!(lines.len() >= 2, "expected wrapping, got {lines:?}");
634 let reconstructed: String = lines
635 .iter()
636 .map(|l| line_text(l).trim_end().to_string())
637 .collect::<Vec<_>>()
638 .join(" ");
639 assert_eq!(reconstructed, text);
640 }
641
642 #[test]
645 fn a_paragraph_with_one_answer_gets_it() {
646 let text = "aa bb cc";
647 let lines = layout_body(text, 100.0);
648 assert_eq!(lines.len(), 1, "everything fits: {lines:?}");
649 let lines = layout_body(text, 30.0);
650 assert_eq!(line_text(&lines[0]), "aa bb");
651 }
652
653 #[test]
657 fn a_first_line_style_breaks_the_paragraph_twice() {
658 let layout = LineLayout::new(registry());
659 let inlines = one_run(OPENING);
660 let broken = |first_line| {
661 layout
662 .broken(
663 &inlines,
664 &body(),
665 &Inherited,
666 &Measure::uniform(160.0),
667 LineBreakOptions::default(),
668 Opening {
669 first_line,
670 ..Opening::default()
671 },
672 )
673 .1
674 };
675 assert_eq!(broken(None), 1);
676 assert_eq!(
677 broken(Some(FirstLine {
678 caps: Some(FontVariantCaps::SmallCaps),
679 ..FirstLine::default()
680 })),
681 2,
682 );
683 }
684
685 #[test]
691 fn a_first_line_is_small_capitals_as_far_as_the_break() {
692 let bare = crate::fonts::registry_without_substitutions();
695 let layout = LineLayout::new(&bare);
696 let lines = layout_first(
697 &layout,
698 160.0,
699 Some(FirstLine {
700 caps: Some(FontVariantCaps::SmallCaps),
701 ..FirstLine::default()
702 }),
703 );
704 assert!(lines.len() > 2, "the paragraph did not break");
705
706 let opening = line_text(&lines[0]);
707 let next = line_text(&lines[1]);
708 let last_word = opening.split_whitespace().next_back().expect("a word");
709 let first_word = next.split_whitespace().next().expect("a word");
710 assert_eq!(
711 last_word,
712 last_word.to_uppercase(),
713 "the first line's last word is not small capitals throughout: {opening:?}",
714 );
715 assert_eq!(
716 first_word,
717 first_word.to_lowercase(),
718 "the small capitals ran past the first line: {next:?}",
719 );
720 assert_eq!(opening, opening.to_uppercase());
721 assert_eq!(next, next.to_lowercase());
722 assert!(
723 lines[0]
724 .runs
725 .iter()
726 .any(|run| run.size == body().size * SMALL_CAPS_RATIO),
727 "nothing on the opening line was set at the reduced size",
728 );
729 assert!(
730 lines[1].runs.iter().all(|run| run.size == body().size),
731 "the reduced size reached the second line",
732 );
733
734 let written: String = lines[0]
736 .runs
737 .iter()
738 .map(|run| match run.source.is_empty() {
739 true => run.text.as_str(),
740 false => run.source.as_str(),
741 })
742 .collect();
743 assert!(
744 OPENING.starts_with(&written),
745 "the opening line lost the manuscript: {written:?}",
746 );
747 }
748
749 #[test]
753 fn a_tracked_first_line_breaks_earlier() {
754 let layout = LineLayout::new(registry());
755 let plain = layout_first(&layout, 160.0, None);
756 let tracked = layout_first(
757 &layout,
758 160.0,
759 Some(FirstLine {
760 letter_spacing: Some(0.12 * body().size),
761 ..FirstLine::default()
762 }),
763 );
764 assert!(
765 line_text(&tracked[0]).len() < line_text(&plain[0]).len(),
766 "the tracked line held as much: {:?} against {:?}",
767 line_text(&tracked[0]),
768 line_text(&plain[0]),
769 );
770 }
771
772 #[test]
776 fn a_first_line_set_larger_grows_its_line_box() {
777 let layout = LineLayout::new(registry());
778 let lines = layout_first(
779 &layout,
780 240.0,
781 Some(FirstLine {
782 size: Some(body().size * 1.6),
783 ..FirstLine::default()
784 }),
785 );
786 assert!(lines.len() > 1, "the paragraph did not break");
787 assert!(
788 lines[0].box_.height > lines[1].box_.height,
789 "the opening line did not grow: {:?} against {:?}",
790 lines[0].box_,
791 lines[1].box_,
792 );
793 assert!(
794 lines[0].box_.baseline > lines[1].box_.baseline,
795 "the opening line grew below the baseline alone",
796 );
797 assert_eq!(
798 lines[0].runs[0].size,
799 body().size * 1.6,
800 "the opening run was not set larger",
801 );
802 assert_eq!(lines[1].runs[0].size, body().size);
803 }
804
805 #[test]
809 fn an_italic_first_line_is_set_in_the_italic_cut() {
810 let layout = LineLayout::new(registry());
811 let lines = layout_first(
812 &layout,
813 160.0,
814 faced(Face {
815 italic: Some(true),
816 ..Face::default()
817 }),
818 );
819 assert!(lines.len() > 1, "the paragraph did not break");
820 assert_eq!(face_of(&lines[0]), cut(true, 400));
821 assert_eq!(face_of(&lines[1]), body().font_id);
822 }
823
824 #[test]
828 fn a_first_line_takes_another_family() {
829 let registry = two_families();
830 let layout = LineLayout::new(registry);
831 let display = registry
832 .select(DISPLAY, FaceAttributes::REGULAR)
833 .expect("the second family is registered")
834 .id;
835 let lines = layout_first(
836 &layout,
837 160.0,
838 faced(Face {
839 family: Some(display),
840 ..Face::default()
841 }),
842 );
843 assert!(lines.len() > 1, "the paragraph did not break");
844 assert_eq!(face_of(&lines[0]), display);
845 assert_eq!(face_of(&lines[1]), body().font_id);
846 }
847
848 #[test]
851 fn a_first_line_set_bold_picks_the_bold_cut() {
852 let layout = LineLayout::new(registry());
853 let lines = layout_first(
854 &layout,
855 160.0,
856 faced(Face {
857 weight: Some(700),
858 ..Face::default()
859 }),
860 );
861 assert!(lines.len() > 1, "the paragraph did not break");
862 assert_eq!(face_of(&lines[0]), cut(false, 700));
863 assert_eq!(face_of(&lines[1]), body().font_id);
864 }
865
866 #[test]
870 fn a_face_on_the_first_line_keeps_the_cut_an_inline_element_brought() {
871 struct Bold(NodeId);
874 impl InlineStyles for Bold {
875 fn style(&self, id: NodeId, block: &ParagraphStyle) -> ParagraphStyle {
876 let font_id = match id == self.0 {
877 true => cut(false, 700),
878 false => block.font_id,
879 };
880 ParagraphStyle {
881 font_id,
882 ..block.clone()
883 }
884 }
885 }
886
887 let bold = NodeId::new(3);
888 let mut inlines = one_run("she said ");
889 inlines.push(emphasis(bold, one_run("never")));
890 let layout = LineLayout::new(registry());
891 let lines = layout.layout_styled(
892 &inlines,
893 &body(),
894 &Bold(bold),
895 &Measure::uniform(200.0),
896 LineBreakOptions::default(),
897 Opening {
898 first_line: faced(Face {
899 italic: Some(true),
900 ..Face::default()
901 }),
902 ..Opening::default()
903 },
904 );
905 let faces: Vec<u16> = lines[0].runs.iter().map(|run| run.font_id).collect();
906 assert_eq!(
907 faces,
908 [cut(true, 400), cut(true, 700)],
909 "{:?}",
910 lines[0].runs
911 );
912 }
913
914 #[test]
918 fn a_face_on_the_first_line_rebreaks_it() {
919 let layout = LineLayout::new(registry());
920 let plain = layout_first(&layout, 160.0, None);
921 let bold = layout_first(
922 &layout,
923 160.0,
924 faced(Face {
925 weight: Some(700),
926 ..Face::default()
927 }),
928 );
929 assert!(
930 line_text(&bold[0]).len() < line_text(&plain[0]).len(),
931 "the bold line held as much: {:?} against {:?}",
932 line_text(&bold[0]),
933 line_text(&plain[0]),
934 );
935 let dropped = line_text(&plain[0])
936 .split_whitespace()
937 .next_back()
938 .expect("a word")
939 .to_string();
940 assert!(
941 line_text(&bold[1]).starts_with(&dropped),
942 "the word the opening line dropped did not open the second: {:?}",
943 line_text(&bold[1]),
944 );
945 }
946
947 #[test]
951 fn a_transformed_first_line_keeps_the_manuscript() {
952 let layout = LineLayout::new(registry());
953 let lines = layout_first(
954 &layout,
955 160.0,
956 Some(FirstLine {
957 transform: Some(TextTransform::Uppercase),
958 ..FirstLine::default()
959 }),
960 );
961 let opening = line_text(&lines[0]);
962 assert_eq!(opening, opening.to_uppercase());
963 assert_eq!(line_text(&lines[1]), line_text(&lines[1]).to_lowercase());
964 let written: String = lines[0]
965 .runs
966 .iter()
967 .map(|run| run.source.as_str())
968 .collect();
969 assert!(
970 OPENING.starts_with(&written) && written == written.to_lowercase(),
971 "the opening line lost the manuscript: {written:?}",
972 );
973 }
974}