1use indexmap::IndexSet;
4use internal_iterator::{
5 FromInternalIterator, InternalIterator, IntoInternalIterator, IteratorExt,
6};
7use smallvec::SmallVec;
8use std::borrow::Cow;
9use std::fmt::Write as _;
10use std::ops::ControlFlow;
11use std::rc::Rc;
12use std::{fmt, iter, mem};
13
14#[derive(Clone, Default, PartialEq)]
18pub struct Fragment {
19 pub nodes: SmallVec<[Node; 8]>,
20}
21
22#[derive(Clone, PartialEq)]
23pub enum Node {
24 Text(Option<Styles>, Cow<'static, str>),
25
26 Anchor {
31 is_def: bool,
32 anchor: Rc<str>,
33 text: Box<[(Option<Styles>, Cow<'static, str>)]>,
34 },
35
36 IndentedBlock(Vec<Fragment>),
38
39 InlineOrIndentedBlock(Vec<Fragment>),
42
43 BreakingOnlySpace,
52
53 ForceLineSeparation,
59
60 IfBlockLayout(&'static str),
62}
63
64#[derive(Copy, Clone, Default, PartialEq)]
65pub struct Styles {
66 pub color: Option<[u8; 3]>,
68
69 pub color_opacity: Option<f32>,
73
74 pub thickness: Option<i8>,
79
80 pub size: Option<i8>,
85
86 pub subscript: bool,
87 pub superscript: bool,
88
89 pub desaturate_and_dim_for_unchanged_multiversion_line: bool,
91}
92
93impl Styles {
94 pub fn color(color: [u8; 3]) -> Self {
95 Self { color: Some(color), ..Self::default() }
96 }
97
98 pub fn apply(self, text: impl Into<Cow<'static, str>>) -> Node {
99 Node::Text(Some(self), text.into())
100 }
101
102 fn effective_size(&self) -> Option<i8> {
105 self.size.or(if self.subscript || self.superscript { Some(-2) } else { None })
106 }
107}
108
109pub mod palettes {
111 pub mod simple {
113 pub const DARK_GRAY: [u8; 3] = [0x44, 0x44, 0x44];
114 pub const LIGHT_GRAY: [u8; 3] = [0x88, 0x88, 0x88];
115
116 pub const RED: [u8; 3] = [0xcc, 0x55, 0x55];
117 pub const GREEN: [u8; 3] = [0x44, 0x99, 0x44];
118 pub const BLUE: [u8; 3] = [0x44, 0x66, 0xcc];
119
120 pub const YELLOW: [u8; 3] = [0xcc, 0x99, 0x44];
121 pub const MAGENTA: [u8; 3] = [0xcc, 0x44, 0xcc];
122 pub const CYAN: [u8; 3] = [0x44, 0x99, 0xcc];
123
124 pub const ORANGE: [u8; 3] = [0xcc, 0x77, 0x55];
125 }
126}
127
128impl From<&'static str> for Node {
129 fn from(text: &'static str) -> Self {
130 Self::Text(None, text.into())
131 }
132}
133
134impl From<String> for Node {
135 fn from(text: String) -> Self {
136 Self::Text(None, text.into())
137 }
138}
139
140impl<T: Into<Node>> From<T> for Fragment {
141 fn from(x: T) -> Self {
142 Self { nodes: [x.into()].into_iter().collect() }
143 }
144}
145
146impl Fragment {
147 pub fn new(fragments: impl IntoIterator<Item = impl Into<Self>>) -> Self {
148 Self { nodes: fragments.into_iter().flat_map(|fragment| fragment.into().nodes).collect() }
149 }
150
151 pub fn layout_with_max_line_width(mut self, max_line_width: usize) -> FragmentPostLayout {
154 let max_line_width =
156 Columns { char_width_tenths: max_line_width.try_into().unwrap_or(u16::MAX) * 10 };
157
158 self.approx_layout(MaxWidths { inline: max_line_width, block: max_line_width });
159 FragmentPostLayout(self)
160 }
161}
162
163pub struct FragmentPostLayout(Fragment);
165
166impl fmt::Display for FragmentPostLayout {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 let result = self
169 .render_to_text_ops()
170 .filter_map(|op| match op {
171 TextOp::Text(text) => Some(text),
172 _ => None,
173 })
174 .try_for_each(|text| {
175 f.write_str(text).map_or_else(ControlFlow::Break, ControlFlow::Continue)
176 });
177 match result {
178 ControlFlow::Continue(()) => Ok(()),
179 ControlFlow::Break(e) => Err(e),
180 }
181 }
182}
183
184impl FragmentPostLayout {
185 pub(super) fn render_to_text_ops(&self) -> impl InternalIterator<Item = TextOp<'_>> {
187 self.0.render_to_text_ops()
188 }
189
190 pub fn render_to_html(&self) -> HtmlSnippet {
194 self.render_to_text_ops().collect()
195 }
196}
197
198#[derive(Default)]
199pub struct HtmlSnippet {
200 pub head_deduplicatable_elements: IndexSet<String>,
201 pub body: String,
202}
203
204impl HtmlSnippet {
205 pub fn with_dark_mode_support(&mut self) -> &mut Self {
209 self.head_deduplicatable_elements.insert(
210 r#"
211<script>
212 (function() {
213 var params = new URLSearchParams(document.location.search);
214 var dark = params.has("dark"), light = params.has("light");
215 if(dark || light) {
216 if(dark && !light) {
217 document.documentElement.classList.add("simple-dark-theme");
218
219 // HACK(eddyb) forcefully disable Dark Reader, for two reasons:
220 // - its own detection of websites with built-in dark themes
221 // (https://github.com/darkreader/darkreader/pull/7995)
222 // isn't on by default, and the combination is jarring
223 // - it interacts badly with whole-document-replacement
224 // (as used by htmlpreview.github.io)
225 document.documentElement.removeAttribute('data-darkreader-scheme');
226 document.querySelectorAll('style.darkreader')
227 .forEach(style => style.disabled = true);
228 }
229 } else if(matchMedia("(prefers-color-scheme: dark)").matches) {
230 // FIXME(eddyb) also use media queries in CSS directly, to ensure dark mode
231 // still works with JS disabled (sadly that likely requires CSS duplication).
232 document.location.search += (document.location.search ? "&" : "?") + "dark";
233 }
234 })();
235</script>
236
237<style>
238 /* HACK(eddyb) `[data-darkreader-scheme="dark"]` is for detecting Dark Reader,
239 to avoid transient interactions (see also comment in the `<script>`). */
240
241 html.simple-dark-theme:not([data-darkreader-scheme="dark"]) {
242 background: #16181a;
243 color: #dbd8d6;
244
245 /* Request browser UI elements to be dark-themed if possible. */
246 color-scheme: dark;
247 }
248</style>
249 "#
250 .into(),
251 );
252 self
253 }
254
255 pub fn to_html_doc(&self) -> String {
260 let mut html = String::new();
261 html += "<!doctype html>\n";
262 html += "<html>\n";
263
264 html += "<head>\n";
265 html += "<meta charset=\"utf-8\">\n";
266 for elem in &self.head_deduplicatable_elements {
267 html += elem;
268 html += "\n";
269 }
270 html += "</head>\n";
271
272 html += "<body>";
273 html += &self.body;
274 html += "</body>\n";
275
276 html += "</html>\n";
277
278 html
279 }
280}
281
282impl<'a> FromInternalIterator<TextOp<'a>> for HtmlSnippet {
284 fn from_iter<T>(text_ops: T) -> Self
285 where
286 T: IntoInternalIterator<Item = TextOp<'a>>,
287 {
288 const ROOT_CLASS_NAME: &str = "spirt-90c2056d-5b38-4644-824a-b4be1c82f14d";
290
291 let style_elem = "
294<style>
295 SCOPE {
296 /* HACK(eddyb) reset default margin to something reasonable. */
297 margin: 1ch;
298
299 /* HACK(eddyb) avoid unnecessarily small or thin text. */
300 font-size: 17px;
301 font-weight: 500;
302 }
303 SCOPE a {
304 color: unset;
305 font-weight: 900;
306 }
307 SCOPE a:not(:hover) {
308 text-decoration: unset;
309 }
310 SCOPE sub, SCOPE sup {
311 line-height: 0;
312 }
313
314 /* HACK(eddyb) using a class (instead of an inline style) so that hovering
315 over a multiversion table cell can disable its desaturation/dimming */
316 SCOPE:not(:hover) .unchanged {
317 filter: saturate(0.3) opacity(0.5);
318 }
319</style>
320"
321 .replace("SCOPE", &format!("pre.{ROOT_CLASS_NAME}"));
322
323 let push_attr = |body: &mut String, attr, value: &str| {
324 assert!(value.chars().all(|c| !(c == '"' || c == '&')));
326
327 body.extend([" ", attr, "=\"", value, "\""]);
328 };
329
330 let mut body = format!("<pre class=\"{ROOT_CLASS_NAME}\">\n");
334 text_ops.into_internal_iter().for_each(|op| match op {
335 TextOp::PushStyles(styles) | TextOp::PopStyles(styles) => {
336 let mut special_tags = [("sub", styles.subscript), ("sup", styles.superscript)]
337 .into_iter()
338 .filter(|&(_, cond)| cond)
339 .map(|(tag, _)| tag);
340 let tag = special_tags.next().unwrap_or("span");
341 if let Some(other_tag) = special_tags.next() {
342 panic!("`<{tag}>` conflicts with `<{other_tag}>`");
344 }
345
346 body += "<";
347 if let TextOp::PopStyles(_) = op {
348 body += "/";
349 }
350 body += tag;
351
352 if let TextOp::PushStyles(_) = op {
353 let Styles {
354 color,
355 color_opacity,
356 thickness,
357 size: _,
358 subscript,
359 superscript,
360 desaturate_and_dim_for_unchanged_multiversion_line,
361 } = *styles;
362
363 let mut css_style = String::new();
364
365 if let Some(a) = color_opacity {
366 let [r, g, b] = color.expect("color_opacity without color");
367 write!(css_style, "color:rgba({r},{g},{b},{a});").unwrap();
368 } else if let Some([r, g, b]) = color {
369 write!(css_style, "color:#{r:02x}{g:02x}{b:02x};").unwrap();
370 }
371 if let Some(thickness) = thickness {
372 write!(css_style, "font-weight:{};", 500 + (thickness as i32) * 100)
373 .unwrap();
374 }
375 if let Some(size) = styles.effective_size() {
376 write!(css_style, "font-size:{}em;", 1.0 + (size as f64) * 0.1).unwrap();
377 if !(subscript || superscript) {
378 write!(css_style, "vertical-align:middle;").unwrap();
380 }
381 }
382 if !css_style.is_empty() {
383 push_attr(&mut body, "style", &css_style);
384 }
385
386 if desaturate_and_dim_for_unchanged_multiversion_line {
387 push_attr(&mut body, "class", "unchanged");
388 }
389 }
390
391 body += ">";
392 }
393 TextOp::PushAnchor { is_def, anchor } => {
394 body += "<a";
395
396 assert!(anchor.chars().all(|c| c != '"'));
399 if is_def {
400 write!(body, " id=\"{anchor}\"").unwrap();
401 }
402 write!(body, " href=\"#{anchor}\">").unwrap();
403 }
404 TextOp::PopAnchor { .. } => body += "</a>",
405 TextOp::Text(text) => {
406 let escape_from = ['&', '<'];
408 let escape_to = ["&", "<"];
409 for piece in text.split_inclusive(escape_from) {
410 let mut chars = piece.chars();
411 let maybe_needs_escape = chars.next_back();
412 body += chars.as_str();
413
414 if let Some(maybe_needs_escape) = maybe_needs_escape {
415 match escape_from.iter().position(|&c| maybe_needs_escape == c) {
416 Some(escape_idx) => body += escape_to[escape_idx],
417 None => body.push(maybe_needs_escape),
418 }
419 }
420 }
421 }
422 });
423 body += "</pre>";
424
425 HtmlSnippet { head_deduplicatable_elements: [style_elem].into_iter().collect(), body }
426 }
427}
428
429#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
437struct Columns {
438 char_width_tenths: u16,
442}
443
444impl Columns {
445 const ZERO: Self = Self { char_width_tenths: 0 };
446
447 fn text_width(text: &str) -> Self {
448 Self::maybe_styled_text_width(text, None)
449 }
450
451 fn maybe_styled_text_width(text: &str, style: Option<&Styles>) -> Self {
452 assert!(!text.contains('\n'));
453
454 let font_size =
455 u16::try_from(10 + style.and_then(|style| style.effective_size()).unwrap_or(0))
456 .unwrap_or(0);
457
458 Self {
460 char_width_tenths: text.len().try_into().unwrap_or(u16::MAX).saturating_mul(font_size),
461 }
462 }
463
464 fn saturating_add(self, other: Self) -> Self {
465 Self { char_width_tenths: self.char_width_tenths.saturating_add(other.char_width_tenths) }
466 }
467
468 fn saturating_sub(self, other: Self) -> Self {
469 Self { char_width_tenths: self.char_width_tenths.saturating_sub(other.char_width_tenths) }
470 }
471}
472
473#[derive(Copy, Clone)]
475enum ApproxLayout {
476 Inline {
481 worst_width: Columns,
482
483 excess_width_from_only_if_block: Columns,
489 },
490
491 BlockOrMixed { pre_worst_width: Columns, post_worst_width: Columns },
496}
497
498impl ApproxLayout {
499 fn append(self, other: Self) -> Self {
500 match (self, other) {
501 (
502 Self::Inline { worst_width: a, excess_width_from_only_if_block: a_excess_foib },
503 Self::Inline { worst_width: b, excess_width_from_only_if_block: b_excess_foib },
504 ) => Self::Inline {
505 worst_width: a.saturating_add(b),
506 excess_width_from_only_if_block: a_excess_foib.saturating_add(b_excess_foib),
507 },
508 (
509 Self::BlockOrMixed { pre_worst_width, .. },
510 Self::BlockOrMixed { post_worst_width, .. },
511 ) => Self::BlockOrMixed { pre_worst_width, post_worst_width },
512 (
513 Self::BlockOrMixed { pre_worst_width, post_worst_width: post_a },
514 Self::Inline { worst_width: post_b, excess_width_from_only_if_block: _ },
515 ) => Self::BlockOrMixed {
516 pre_worst_width,
517 post_worst_width: post_a.saturating_add(post_b),
518 },
519 (
520 Self::Inline { worst_width: pre_a, excess_width_from_only_if_block: _ },
521 Self::BlockOrMixed { pre_worst_width: pre_b, post_worst_width },
522 ) => Self::BlockOrMixed {
523 pre_worst_width: pre_a.saturating_add(pre_b),
524 post_worst_width,
525 },
526 }
527 }
528}
529
530#[derive(Copy, Clone)]
537struct MaxWidths {
538 inline: Columns,
539 block: Columns,
540}
541
542pub(super) const INDENT: &str = " ";
544
545impl Node {
546 fn approx_rigid_layout(&self) -> ApproxLayout {
551 let text_approx_rigid_layout = |styles: &Option<_>, text: &str| {
554 let styles = styles.as_ref();
555 if let Some((pre, non_pre)) = text.split_once('\n') {
556 let (_, post) = non_pre.rsplit_once('\n').unwrap_or(("", non_pre));
557
558 ApproxLayout::BlockOrMixed {
559 pre_worst_width: Columns::maybe_styled_text_width(pre, styles),
560 post_worst_width: Columns::maybe_styled_text_width(post, styles),
561 }
562 } else {
563 ApproxLayout::Inline {
564 worst_width: Columns::maybe_styled_text_width(text, styles),
565 excess_width_from_only_if_block: Columns::ZERO,
566 }
567 }
568 };
569
570 #[allow(clippy::match_same_arms)]
571 match self {
572 Self::Text(styles, text) => text_approx_rigid_layout(styles, text),
573
574 Self::Anchor { is_def: _, anchor: _, text } => text
575 .iter()
576 .map(|(styles, text)| text_approx_rigid_layout(styles, text))
577 .reduce(ApproxLayout::append)
578 .unwrap_or(ApproxLayout::Inline {
579 worst_width: Columns::ZERO,
580 excess_width_from_only_if_block: Columns::ZERO,
581 }),
582
583 Self::IndentedBlock(_) => ApproxLayout::BlockOrMixed {
584 pre_worst_width: Columns::ZERO,
585 post_worst_width: Columns::ZERO,
586 },
587
588 Self::BreakingOnlySpace => ApproxLayout::Inline {
589 worst_width: Columns::text_width(" "),
590 excess_width_from_only_if_block: Columns::ZERO,
591 },
592 Self::ForceLineSeparation => ApproxLayout::BlockOrMixed {
593 pre_worst_width: Columns::ZERO,
594 post_worst_width: Columns::ZERO,
595 },
596 &Self::IfBlockLayout(text) => {
597 let text_layout = Self::Text(None, text.into()).approx_rigid_layout();
602 let worst_width = match text_layout {
603 ApproxLayout::Inline { worst_width, excess_width_from_only_if_block: _ } => {
604 worst_width
605 }
606 ApproxLayout::BlockOrMixed { .. } => Columns::ZERO,
607 };
608 ApproxLayout::Inline { worst_width, excess_width_from_only_if_block: worst_width }
609 }
610
611 Self::InlineOrIndentedBlock(_) => ApproxLayout::Inline {
613 worst_width: Columns::ZERO,
614 excess_width_from_only_if_block: Columns::ZERO,
615 },
616 }
617 }
618
619 fn approx_flex_layout(&mut self, max_widths: MaxWidths) -> ApproxLayout {
625 match self {
626 Self::IndentedBlock(fragments) => {
627 let indented_block_max_width =
629 max_widths.block.saturating_sub(Columns::text_width(INDENT));
630
631 for fragment in &mut fragments[..] {
633 fragment.approx_layout(MaxWidths {
634 inline: indented_block_max_width,
635 block: indented_block_max_width,
636 });
637 }
638
639 ApproxLayout::BlockOrMixed {
640 pre_worst_width: Columns::ZERO,
641 post_worst_width: Columns::ZERO,
642 }
643 }
644
645 Self::InlineOrIndentedBlock(fragments) => {
646 let indented_block_max_width =
648 max_widths.block.saturating_sub(Columns::text_width(INDENT));
649
650 let inner_max_widths = MaxWidths {
656 inline: max_widths.inline.max(indented_block_max_width),
657 block: indented_block_max_width,
658 };
659
660 let mut layout = ApproxLayout::Inline {
661 worst_width: Columns::ZERO,
662 excess_width_from_only_if_block: Columns::ZERO,
663 };
664 for fragment in &mut fragments[..] {
665 layout = layout.append(fragment.approx_layout(inner_max_widths));
670 }
671
672 let candidate_inline_worst_width = match layout {
676 ApproxLayout::Inline { worst_width, excess_width_from_only_if_block } => {
677 Some(worst_width.saturating_sub(excess_width_from_only_if_block))
678 }
679
680 ApproxLayout::BlockOrMixed { .. } => None,
681 };
682
683 let inline_layout = candidate_inline_worst_width
684 .filter(|&worst_width| worst_width <= max_widths.inline)
685 .map(|worst_width| ApproxLayout::Inline {
686 worst_width,
687 excess_width_from_only_if_block: Columns::ZERO,
688 });
689
690 layout = inline_layout.unwrap_or(
691 ApproxLayout::BlockOrMixed {
694 pre_worst_width: Columns::ZERO,
695 post_worst_width: Columns::ZERO,
696 },
697 );
698
699 match layout {
700 ApproxLayout::Inline { .. } => {
701 }
704 ApproxLayout::BlockOrMixed { .. } => {
705 *self = Self::IndentedBlock(mem::take(fragments));
706 }
707 }
708
709 layout
710 }
711
712 Self::Text(..)
714 | Self::Anchor { .. }
715 | Self::BreakingOnlySpace
716 | Self::ForceLineSeparation
717 | Self::IfBlockLayout(_) => ApproxLayout::Inline {
718 worst_width: Columns::ZERO,
719 excess_width_from_only_if_block: Columns::ZERO,
720 },
721 }
722 }
723}
724
725impl Fragment {
726 fn approx_layout(&mut self, max_widths: MaxWidths) -> ApproxLayout {
729 let mut layout = ApproxLayout::Inline {
730 worst_width: Columns::ZERO,
731 excess_width_from_only_if_block: Columns::ZERO,
732 };
733
734 let child_max_widths = |layout| MaxWidths {
735 inline: match layout {
736 ApproxLayout::Inline { worst_width, excess_width_from_only_if_block: _ } => {
737 max_widths.inline.saturating_sub(worst_width)
738 }
739 ApproxLayout::BlockOrMixed { post_worst_width, .. } => {
740 max_widths.block.saturating_sub(post_worst_width)
741 }
742 },
743 block: max_widths.block,
744 };
745
746 let mut next_flex_idx = 0;
750 for rigid_idx in 0..self.nodes.len() {
751 match self.nodes[rigid_idx].approx_rigid_layout() {
752 rigid_layout @ ApproxLayout::Inline { .. } => {
753 layout = layout.append(rigid_layout);
754 }
755 ApproxLayout::BlockOrMixed { pre_worst_width, post_worst_width } => {
756 layout = layout.append(ApproxLayout::Inline {
759 worst_width: pre_worst_width,
760 excess_width_from_only_if_block: Columns::ZERO,
761 });
762 while next_flex_idx <= rigid_idx {
765 layout = layout.append(
766 self.nodes[next_flex_idx].approx_flex_layout(child_max_widths(layout)),
767 );
768 next_flex_idx += 1;
769 }
770 layout = layout.append(ApproxLayout::BlockOrMixed {
771 pre_worst_width: Columns::ZERO,
772 post_worst_width,
773 });
774 }
775 }
776 }
777
778 for flex_idx in next_flex_idx..self.nodes.len() {
780 layout =
781 layout.append(self.nodes[flex_idx].approx_flex_layout(child_max_widths(layout)));
782 }
783
784 layout
785 }
786}
787
788#[derive(Copy, Clone)]
794enum LineOp<'a> {
795 PushIndent,
796 PopIndent,
797 PushStyles(&'a Styles),
798 PopStyles(&'a Styles),
799 PushAnchor { is_def: bool, anchor: &'a str },
800 PopAnchor { is_def: bool, anchor: &'a str },
801
802 EmptyAnchor { is_def: bool, anchor: &'a str },
805
806 AppendToLine(&'a str),
807 StartNewLine,
808 BreakIfWithinLine(Break),
809}
810
811#[derive(Copy, Clone)]
812enum Break {
813 Space,
814 NewLine,
815}
816
817impl Node {
818 fn render_to_line_ops(
820 &self,
821 directly_in_block: bool,
822 ) -> impl InternalIterator<Item = LineOp<'_>> {
823 struct RenderToLineOps<'a>(&'a Node, bool);
825 impl<'a> InternalIterator for RenderToLineOps<'a> {
826 type Item = LineOp<'a>;
827
828 fn try_for_each<T, F>(self, mut f: F) -> ControlFlow<T>
829 where
830 F: FnMut(LineOp<'a>) -> ControlFlow<T>,
831 {
832 let f = &mut f as &mut dyn FnMut(_) -> _;
837
838 self.0.render_to_line_ops_try_for_each_helper(self.1, f)
839 }
840 }
841 RenderToLineOps(self, directly_in_block)
842 }
843
844 fn render_to_line_ops_try_for_each_helper<'a, T>(
846 &'a self,
847 directly_in_block: bool,
848 mut each_line_op: impl FnMut(LineOp<'a>) -> ControlFlow<T>,
849 ) -> ControlFlow<T> {
850 let text_render_to_line_ops = |styles: &'a Option<Styles>, text: &'a str| {
851 let styles = styles.as_ref();
852 let mut lines = text.split('\n');
853 styles
854 .map(LineOp::PushStyles)
855 .into_internal_iter()
856 .chain([LineOp::AppendToLine(lines.next().unwrap())])
857 .chain(
858 lines
859 .into_internal()
860 .flat_map(|line| [LineOp::StartNewLine, LineOp::AppendToLine(line)]),
861 )
862 .chain(styles.map(LineOp::PopStyles))
863 };
864 match self {
865 Self::Text(styles, text) => {
866 text_render_to_line_ops(styles, text).try_for_each(each_line_op)?;
867 }
868
869 &Self::Anchor { is_def, ref anchor, ref text } => {
870 if text.is_empty() {
871 each_line_op(LineOp::EmptyAnchor { is_def, anchor })?;
872 } else {
873 [LineOp::PushAnchor { is_def, anchor }]
874 .into_internal_iter()
875 .chain(
876 text.into_internal_iter()
877 .flat_map(|(styles, text)| text_render_to_line_ops(styles, text)),
878 )
879 .chain([LineOp::PopAnchor { is_def, anchor }])
880 .try_for_each(each_line_op)?;
881 }
882 }
883
884 Self::IndentedBlock(fragments) => {
885 [LineOp::PushIndent, LineOp::BreakIfWithinLine(Break::NewLine)]
886 .into_internal_iter()
887 .chain(fragments.into_internal_iter().flat_map(|fragment| {
888 fragment
889 .render_to_line_ops(true)
890 .chain([LineOp::BreakIfWithinLine(Break::NewLine)])
891 }))
892 .chain([LineOp::PopIndent])
893 .try_for_each(each_line_op)?;
894 }
895 Self::InlineOrIndentedBlock(fragments) => {
897 fragments
898 .into_internal_iter()
899 .flat_map(|fragment| fragment.render_to_line_ops(false))
900 .try_for_each(each_line_op)?;
901 }
902
903 Self::BreakingOnlySpace => each_line_op(LineOp::BreakIfWithinLine(Break::Space))?,
904 Self::ForceLineSeparation => each_line_op(LineOp::BreakIfWithinLine(Break::NewLine))?,
905 &Self::IfBlockLayout(text) => {
906 if directly_in_block {
907 text_render_to_line_ops(&None, text).try_for_each(each_line_op)?;
908 }
909 }
910 }
911 ControlFlow::Continue(())
912 }
913}
914
915impl Fragment {
916 fn render_to_line_ops(
918 &self,
919 directly_in_block: bool,
920 ) -> impl InternalIterator<Item = LineOp<'_>> {
921 self.nodes
922 .iter()
923 .into_internal()
924 .flat_map(move |node| node.render_to_line_ops(directly_in_block))
925 }
926
927 fn render_to_text_ops(&self) -> impl InternalIterator<Item = TextOp<'_>> {
929 LineOp::interpret(self.render_to_line_ops(false))
930 }
931}
932
933#[derive(Copy, Clone, PartialEq)]
935pub(super) enum TextOp<'a> {
936 PushStyles(&'a Styles),
937 PopStyles(&'a Styles),
938 PushAnchor { is_def: bool, anchor: &'a str },
939 PopAnchor { is_def: bool, anchor: &'a str },
940
941 Text(&'a str),
942}
943
944impl<'a> LineOp<'a> {
945 fn interpret(
947 line_ops: impl InternalIterator<Item = LineOp<'a>>,
948 ) -> impl InternalIterator<Item = TextOp<'a>> {
949 struct Interpret<I>(I);
951 impl<'a, I: InternalIterator<Item = LineOp<'a>>> InternalIterator for Interpret<I> {
952 type Item = TextOp<'a>;
953
954 fn try_for_each<T, F>(self, f: F) -> ControlFlow<T>
955 where
956 F: FnMut(TextOp<'a>) -> ControlFlow<T>,
957 {
958 LineOp::interpret_try_for_each_helper(self.0, f)
959 }
960 }
961 Interpret(line_ops)
962 }
963
964 fn interpret_try_for_each_helper<T>(
966 line_ops: impl InternalIterator<Item = LineOp<'a>>,
967 mut each_text_op: impl FnMut(TextOp<'a>) -> ControlFlow<T>,
968 ) -> ControlFlow<T> {
969 let mut indent = 0;
970
971 enum LineState {
972 Empty,
981
982 OnlyIndentedOrAnchored { indent_so_far: usize },
995
996 HasText,
998 }
999 let mut line_state = LineState::Empty;
1000
1001 let mut pending_break_if_within_line = None;
1005
1006 line_ops.try_for_each(move |op| {
1007 if let LineOp::AppendToLine("") = op {
1009 return ControlFlow::Continue(());
1010 }
1011
1012 if let LineOp::AppendToLine(_)
1013 | LineOp::PushStyles(_)
1014 | LineOp::PopStyles(_)
1015 | LineOp::PushAnchor { .. }
1016 | LineOp::PopAnchor { .. }
1017 | LineOp::EmptyAnchor { .. } = op
1018 {
1019 if let Some(br) = pending_break_if_within_line.take() {
1020 each_text_op(TextOp::Text(match br {
1021 Break::Space => " ",
1022 Break::NewLine => "\n",
1023 }))?;
1024 if matches!(br, Break::NewLine) {
1025 line_state = LineState::Empty;
1026 }
1027 }
1028
1029 let target_indent = match line_state {
1030 LineState::Empty | LineState::OnlyIndentedOrAnchored { indent_so_far: 0 }
1033 if matches!(op, LineOp::EmptyAnchor { .. }) =>
1034 {
1035 Some(0)
1036 }
1037
1038 LineState::Empty | LineState::OnlyIndentedOrAnchored { .. } => Some(indent),
1039 LineState::HasText => None,
1040 };
1041
1042 if let Some(target_indent) = target_indent {
1043 let indent_so_far = match line_state {
1044 LineState::Empty => 0,
1045
1046 LineState::OnlyIndentedOrAnchored { indent_so_far } => {
1049 if indent_so_far > target_indent {
1051 each_text_op(TextOp::Text("\n"))?;
1052 line_state = LineState::Empty;
1053 0
1054 } else {
1055 indent_so_far
1056 }
1057 }
1058
1059 LineState::HasText => unreachable!(),
1060 };
1061 for _ in indent_so_far..target_indent {
1062 each_text_op(TextOp::Text(INDENT))?;
1063 }
1064 line_state = LineState::OnlyIndentedOrAnchored { indent_so_far: target_indent };
1065 }
1066 }
1067
1068 match op {
1069 LineOp::PushIndent => {
1070 indent += 1;
1071 }
1072
1073 LineOp::PopIndent => {
1074 assert!(indent > 0);
1075 indent -= 1;
1076 }
1077
1078 LineOp::PushStyles(styles) => each_text_op(TextOp::PushStyles(styles))?,
1079 LineOp::PopStyles(styles) => each_text_op(TextOp::PopStyles(styles))?,
1080
1081 LineOp::PushAnchor { is_def, anchor } => {
1082 each_text_op(TextOp::PushAnchor { is_def, anchor })?;
1083 }
1084 LineOp::PopAnchor { is_def, anchor } => {
1085 each_text_op(TextOp::PopAnchor { is_def, anchor })?;
1086 }
1087
1088 LineOp::EmptyAnchor { is_def, anchor } => {
1089 each_text_op(TextOp::PushAnchor { is_def, anchor })?;
1090 each_text_op(TextOp::PopAnchor { is_def, anchor })?;
1091 }
1092
1093 LineOp::AppendToLine(text) => {
1094 each_text_op(TextOp::Text(text))?;
1095
1096 line_state = LineState::HasText;
1097 }
1098
1099 LineOp::StartNewLine => {
1100 each_text_op(TextOp::Text("\n"))?;
1101
1102 line_state = LineState::Empty;
1103 pending_break_if_within_line = None;
1104 }
1105
1106 LineOp::BreakIfWithinLine(br) => {
1107 let elide = match line_state {
1108 LineState::Empty => true,
1109 LineState::OnlyIndentedOrAnchored { indent_so_far } => {
1110 indent_so_far <= indent
1111 }
1112 LineState::HasText => false,
1113 };
1114 if !elide {
1115 let br = match (pending_break_if_within_line, br) {
1118 (Some(Break::NewLine), _) | (_, Break::NewLine) => Break::NewLine,
1119 (None | Some(Break::Space), Break::Space) => Break::Space,
1120 };
1121
1122 pending_break_if_within_line = Some(br);
1123 }
1124 }
1125 }
1126 ControlFlow::Continue(())
1127 })
1128 }
1129}
1130
1131pub fn join_space(
1139 header: impl Into<Node>,
1140 contents: impl IntoIterator<Item = impl Into<Fragment>>,
1141) -> Fragment {
1142 Fragment::new([
1143 header.into(),
1144 Node::InlineOrIndentedBlock(
1145 contents
1146 .into_iter()
1147 .map(|entry| {
1148 Fragment::new(iter::once(Node::BreakingOnlySpace).chain(entry.into().nodes))
1149 })
1150 .collect(),
1151 ),
1152 ])
1153}
1154
1155pub fn join_comma_sep(
1159 prefix: impl Into<Node>,
1160 contents: impl IntoIterator<Item = impl Into<Fragment>>,
1161 suffix: impl Into<Node>,
1162) -> Fragment {
1163 let mut children: Vec<_> = contents.into_iter().map(Into::into).collect();
1164
1165 if let Some((last_child, non_last_children)) = children.split_last_mut() {
1166 for non_last_child in non_last_children {
1167 non_last_child.nodes.extend([",".into(), Node::BreakingOnlySpace]);
1168 }
1169
1170 last_child.nodes.push(Node::IfBlockLayout(","));
1172 }
1173
1174 Fragment::new([prefix.into(), Node::InlineOrIndentedBlock(children), suffix.into()])
1175}