1use crate::FxIndexMap;
4use crate::print::pretty::{self, TextOp};
5use internal_iterator::{
6 FromInternalIterator, InternalIterator, IntoInternalIterator, IteratorExt,
7};
8use itertools::{Either, Itertools};
9use smallvec::SmallVec;
10use std::fmt::Write;
11use std::{fmt, iter, mem};
12
13#[allow(rustdoc::private_intra_doc_links)]
14pub enum Versions<PF> {
20 Single(PF),
21 Multiple {
22 version_names: Vec<String>,
24
25 per_row_versions_with_repeat_count: Vec<SmallVec<[(PF, usize); 1]>>,
33 },
34}
35
36impl fmt::Display for Versions<pretty::FragmentPostLayout> {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::Single(fragment) => fragment.fmt(f),
40 Self::Multiple { version_names, per_row_versions_with_repeat_count } => {
41 let mut first = true;
42
43 for versions_with_repeat_count in per_row_versions_with_repeat_count {
46 if !first {
47 writeln!(f)?;
48 }
49 first = false;
50
51 let mut next_version_idx = 0;
52 let mut any_headings = false;
53 for (fragment, repeat_count) in versions_with_repeat_count {
54 if (next_version_idx, *repeat_count) != (0, version_names.len()) {
56 any_headings = true;
57
58 if next_version_idx == 0 {
59 write!(f, "//#IF ")?;
60 } else {
61 write!(f, "//#ELSEIF ")?;
62 }
63 let mut first_name = true;
64 for name in &version_names[next_version_idx..][..*repeat_count] {
65 if !first_name {
66 write!(f, " | ")?;
67 }
68 first_name = false;
69
70 write!(f, "`{name}`")?;
71 }
72 writeln!(f)?;
73 }
74
75 writeln!(f, "{fragment}")?;
76
77 next_version_idx += repeat_count;
78 }
79 if any_headings {
80 writeln!(f, "//#ENDIF")?;
81 }
82 }
83
84 Ok(())
85 }
86 }
87 }
88}
89
90impl Versions<pretty::FragmentPostLayout> {
91 pub fn render_to_html(&self) -> pretty::HtmlSnippet {
93 match self {
94 Self::Single(fragment) => fragment.render_to_html(),
95 Self::Multiple { version_names, per_row_versions_with_repeat_count } => {
96 const TABLE_CLASS_NAME: &str = "spirt-table-90c2056d-5b38-4644-824a-b4be1c82f14d";
98
99 let mut html = pretty::HtmlSnippet::default();
100 html.head_deduplicatable_elements.insert(
101 "
102<style>
103 SCOPE {
104 border-collapse: collapse;
105 }
106 SCOPE>tbody>tr>*:not(:only-child) {
107 border: solid 1px;
108 }
109 SCOPE>tbody>tr>th {
110 /* HACK(eddyb) these are relative to `pretty`'s own HTML styles. */
111 font-size: 19px;
112 font-weight: 700;
113
114 font-style: italic;
115 }
116 SCOPE>tbody>tr>td {
117 vertical-align: top;
118 }
119 SCOPE>tbody>tr>td>pre {
120 max-width: MAX_LINE_WIDTHch;
121
122 overflow-x: auto;
123
124 /* HACK(eddyb) this shouldn't be needed but `visible` will turn into
125 `auto` because `overflow-x` is `auto` (for cursed browser reasons),
126 and some table cells are always assumed to need vertical scroll,
127 e.g. because they have `<sub>` elements on the last line (which don't
128 contribute to the surrounding bounding box, due to `line-height: 0`)
129 */
130 overflow-y: hidden;
131 }
132</style>
133 "
134 .replace("SCOPE", &format!("table.{TABLE_CLASS_NAME}"))
135 .replace("MAX_LINE_WIDTH", &super::MAX_LINE_WIDTH.to_string()),
136 );
137
138 let headings = {
139 let mut h = "<tr>".to_string();
140 for name in version_names {
141 write!(h, "<th><code>{name}</code></th>").unwrap();
142 }
143 h + "</tr>\n"
144 };
145
146 html.body = format!("<table class=\"{TABLE_CLASS_NAME}\">\n");
147 let mut last_was_uniform = true;
148 for versions_with_repeat_count in per_row_versions_with_repeat_count {
149 let is_uniform = match versions_with_repeat_count[..] {
151 [(_, repeat_count)] => repeat_count == version_names.len(),
152 _ => false,
153 };
154
155 if last_was_uniform && is_uniform {
156 } else {
159 html.body += &headings;
161 }
162 last_was_uniform = is_uniform;
163
164 let mut anchor_aligner = AnchorAligner::default();
167 for (fragment, _) in versions_with_repeat_count {
168 anchor_aligner
169 .add_column_and_align_anchors(fragment.render_to_text_ops().collect());
170 }
171
172 html.body += "<tr>\n";
173 if is_uniform {
174 let pretty::HtmlSnippet {
176 head_deduplicatable_elements: fragment_head,
177 body: fragment_body,
178 } = anchor_aligner
179 .merged_columns()
180 .next()
181 .unwrap()
182 .lines()
183 .intersperse(&[TextOp::Text("\n")])
184 .flatten()
185 .copied()
186 .into_internal()
187 .collect();
188
189 html.head_deduplicatable_elements.extend(fragment_head);
190
191 writeln!(html.body, "<td colspan=\"{}\">", version_names.len()).unwrap();
192 html.body += &fragment_body;
193 html.body += "</td>\n";
194 } else {
195 let mut merged_columns = versions_with_repeat_count
196 .iter()
197 .zip(anchor_aligner.merged_columns())
198 .flat_map(|(&(_, repeat_count), column)| {
199 iter::repeat(column).take(repeat_count)
200 })
201 .peekable();
202
203 let mut prev_column = None;
204 while let Some(column) = merged_columns.next() {
205 let prev_column = prev_column.replace(column);
206 let next_column = merged_columns.peek().copied();
207
208 let unchanged_line_style = pretty::Styles {
209 desaturate_and_dim_for_unchanged_multiversion_line: true,
210 ..Default::default()
211 };
212
213 let prev_lines = prev_column
216 .iter()
217 .flat_map(|prev| prev.lines().map(Some))
218 .chain(iter::repeat(prev_column.map(|_| &[][..])));
219 let next_lines = next_column
220 .iter()
221 .flat_map(|next| next.lines().map(Some))
222 .chain(iter::repeat(next_column.map(|_| &[][..])));
223
224 let lines = column.lines().zip(prev_lines).zip(next_lines).map(
225 |((line, prev_line), next_line)| {
226 let diff = |other: Option<_>| {
231 fn strip_indents<'a, 'b>(
233 mut line: &'b [TextOp<'a>],
234 ) -> &'b [TextOp<'a>]
235 {
236 while let [TextOp::Text(pretty::INDENT), rest @ ..]
239 | [
240 TextOp::PushAnchor { .. },
241 TextOp::PopAnchor { .. },
242 rest @ ..,
243 ] = line
244 {
245 line = rest;
246 }
247 line
248 }
249 other.map_or(false, |other| {
250 strip_indents(line) != strip_indents(other)
251 })
252 };
253 let line_style = if !diff(prev_line) && !diff(next_line) {
254 Some(&unchanged_line_style)
255 } else {
256 None
257 };
258 line_style
259 .map(TextOp::PushStyles)
260 .into_iter()
261 .chain(line.iter().copied())
262 .chain(line_style.map(TextOp::PopStyles))
263 },
264 );
265
266 let pretty::HtmlSnippet {
267 head_deduplicatable_elements: fragment_head,
268 body: fragment_body,
269 } = lines
270 .map(Either::Left)
271 .intersperse(Either::Right([TextOp::Text("\n")].into_iter()))
272 .flatten()
273 .into_internal()
274 .collect();
275
276 html.head_deduplicatable_elements.extend(fragment_head);
277
278 html.body += "<td>\n";
279 html.body += &fragment_body;
280 html.body += "</td>\n";
281 }
282 }
283 html.body += "</tr>\n";
284 }
285 html.body += "</table>";
286
287 html
288 }
289 }
290 }
291}
292
293impl<PF> Versions<PF> {
294 pub fn map_pretty_fragments<PF2>(self, f: impl Fn(PF) -> PF2) -> Versions<PF2> {
295 match self {
296 Versions::Single(fragment) => Versions::Single(f(fragment)),
297 Versions::Multiple { version_names, per_row_versions_with_repeat_count } => {
298 Versions::Multiple {
299 version_names,
300 per_row_versions_with_repeat_count: per_row_versions_with_repeat_count
301 .into_iter()
302 .map(|versions_with_repeat_count| {
303 versions_with_repeat_count
304 .into_iter()
305 .map(|(fragment, repeat_count)| (f(fragment), repeat_count))
306 .collect()
307 })
308 .collect(),
309 }
310 }
311 }
312 }
313}
314
315#[derive(Default)]
318struct AnchorAligner<'a> {
319 merged_lines: Vec<AAMergedLine>,
320
321 anchor_def_to_merged_line_idx: FxIndexMap<&'a str, usize>,
326
327 original_columns: SmallVec<[AAColumn<'a>; 4]>,
330}
331
332struct AAMergedLine {
334 per_column_line_lengths: SmallVec<[usize; 4]>,
337}
338
339struct AAColumn<'a> {
340 text_ops: Vec<TextOp<'a>>,
342
343 line_lengths: Vec<usize>,
347}
348
349impl<'a> AAColumn<'a> {
350 fn lines(
352 &self,
353 line_lengths: impl Iterator<Item = usize>,
354 ) -> impl Iterator<Item = &[TextOp<'a>]> {
355 let mut next_start = 0;
356 line_lengths.map(move |len| {
357 let start = next_start;
358 let end = start + len;
359 next_start = end;
360 &self.text_ops[start..end]
361 })
362 }
363}
364
365impl<'a> FromInternalIterator<TextOp<'a>> for AAColumn<'a> {
367 fn from_iter<T>(text_ops: T) -> Self
368 where
369 T: IntoInternalIterator<Item = TextOp<'a>>,
370 {
371 let mut column = AAColumn { text_ops: vec![], line_lengths: vec![0] };
372 text_ops.into_internal_iter().for_each(|op| {
373 if let TextOp::Text("\n") = op {
374 column.line_lengths.push(0);
375 } else {
376 if let TextOp::Text(text) = op {
380 assert!(!text.contains('\n'));
381 }
382 column.text_ops.push(op);
383 *column.line_lengths.last_mut().unwrap() += 1;
384 }
385 });
386 column
387 }
388}
389
390#[derive(Copy, Clone)]
391struct AAMergedColumn<'a, 'b> {
392 original_column: &'b AAColumn<'a>,
393 column_idx: usize,
394 merged_lines: &'b [AAMergedLine],
395}
396
397impl<'a, 'b> AAMergedColumn<'a, 'b> {
398 fn lines(&self) -> impl Iterator<Item = &'b [TextOp<'a>]> + '_ {
399 let column_idx = self.column_idx;
400 let line_lengths =
401 self.merged_lines.iter().map(move |line| line.per_column_line_lengths[column_idx]);
402 self.original_column.lines(line_lengths)
403 }
404}
405
406impl<'a> AnchorAligner<'a> {
407 fn merged_columns(&self) -> impl Iterator<Item = AAMergedColumn<'a, '_>> {
409 self.original_columns.iter().enumerate().map(|(column_idx, original_column)| {
410 let mut merged_lines = &self.merged_lines[..];
411
412 while let Some((last, before_last)) = merged_lines.split_last() {
414 if last.per_column_line_lengths[column_idx] > 0 {
415 break;
416 }
417 merged_lines = before_last;
418 }
419
420 AAMergedColumn { original_column, column_idx, merged_lines }
421 })
422 }
423
424 fn add_column_and_align_anchors(&mut self, new_column: AAColumn<'a>) {
427 let old_lines = mem::take(&mut self.merged_lines);
431 let old_anchor_def_to_line_idx = mem::take(&mut self.anchor_def_to_merged_line_idx);
432
433 let mut new_anchor_def_to_line_idx = FxIndexMap::default();
435 for (new_line_idx, new_line_text_ops) in
436 new_column.lines(new_column.line_lengths.iter().copied()).enumerate()
437 {
438 for op in new_line_text_ops {
439 if let TextOp::PushAnchor { is_def: true, anchor } = *op {
440 new_anchor_def_to_line_idx.entry(anchor).or_insert(new_line_idx);
441 }
442 }
443 }
444
445 let common_anchors: SmallVec<[_; 8]> = new_anchor_def_to_line_idx
451 .iter()
452 .filter_map(|(anchor, &new_line_idx)| {
453 Some((*old_anchor_def_to_line_idx.get(anchor)?, new_line_idx))
454 })
455 .collect();
456
457 let is_already_monotonic = {
460 common_anchors.windows(2).all(|w| w[0].0 <= w[1].0)
463 };
464 let monotonic_common_anchors = if is_already_monotonic {
465 common_anchors
466 } else {
467 longest_increasing_subsequence::lis(&common_anchors)
469 .into_iter()
470 .map(|i| common_anchors[i])
471 .collect()
472 };
473
474 let mut merged_lines = Vec::with_capacity({
476 let &(old_last, new_last) = monotonic_common_anchors.last().unwrap_or(&(0, 0));
479 let min_before_last = old_last.max(new_last);
480 let after_last =
481 (old_lines.len() - old_last).max(new_column.line_lengths.len() - new_last);
482 (min_before_last + after_last).next_power_of_two()
483 });
484
485 let mut old_lines = old_lines.into_iter().enumerate().peekable();
488 let mut new_lines = new_column.line_lengths.iter().copied().enumerate().peekable();
489 let mut monotonic_common_anchors = monotonic_common_anchors.into_iter().peekable();
490 let mut fixup_new_to_merged = new_anchor_def_to_line_idx.values_mut().peekable();
491 while old_lines.len() > 0 || new_lines.len() > 0 {
492 let old_line_idx = old_lines.peek().map(|&(i, _)| i);
493 let new_line_idx = new_lines.peek().map(|&(i, _)| i);
494 let mut next_anchor = monotonic_common_anchors.peek().copied();
495
496 while let Some((anchor_old, anchor_new)) = next_anchor {
500 let relevant = match (old_line_idx, new_line_idx) {
508 (Some(old), Some(new)) => old < anchor_old || new < anchor_new,
509 _ => false,
510 };
511 if relevant {
512 break;
513 }
514 monotonic_common_anchors.next().unwrap();
515 next_anchor = monotonic_common_anchors.peek().copied();
516 }
517
518 let (old_at_anchor, new_at_anchor) =
520 next_anchor.map_or((false, false), |(anchor_old, anchor_new)| {
521 (
522 old_line_idx.map_or(false, |old| old == anchor_old),
523 new_line_idx.map_or(false, |new| new == anchor_new),
524 )
525 });
526 let old_line = if old_at_anchor && !new_at_anchor {
527 None
529 } else {
530 old_lines.next().map(|(_, old_line)| old_line)
531 };
532 let new_line_len = if !old_at_anchor && new_at_anchor {
533 None
535 } else {
536 new_lines.next().map(|(_, new_line_len)| new_line_len)
537 };
538
539 if new_line_len.is_some() {
542 let new_line_idx = new_line_idx.unwrap();
543 let merged_line_idx = merged_lines.len();
544 while fixup_new_to_merged.peek().map(|i| **i) == Some(new_line_idx) {
545 *fixup_new_to_merged.next().unwrap() = merged_line_idx;
546 }
547 }
548
549 let new_line_len = new_line_len.unwrap_or(0);
550 let merged_line = match old_line {
551 Some(mut line) => {
552 line.per_column_line_lengths.push(new_line_len);
553 line
554 }
555 None => AAMergedLine {
556 per_column_line_lengths: (0..self.original_columns.len())
557 .map(|_| 0)
558 .chain([new_line_len])
559 .collect(),
560 },
561 };
562 merged_lines.push(merged_line);
563 }
564
565 self.merged_lines = merged_lines;
566 self.anchor_def_to_merged_line_idx = new_anchor_def_to_line_idx;
567 self.original_columns.push(new_column);
568 }
569}