hi_doc/
lib.rs

1use std::{
2	collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3	mem,
4	ops::RangeInclusive,
5};
6
7use annotated_string::AnnotatedRope;
8use annotation::{Annotation, AnnotationId, Opts};
9use anomaly_fixer::{apply_fixup, fixup_byte_to_char, fixup_char_to_display};
10pub use formatting::Text;
11use rand::{rngs::SmallRng, Rng, SeedableRng};
12use random_color::{
13	options::{Gamut, Luminosity},
14	RandomColor,
15};
16use range_map::{Range, RangeSet};
17use single_line::LineAnnotation;
18use unicode_box_drawing::bc;
19
20pub use crate::formatting::Formatting;
21
22use self::{
23	annotation::AnnotationLocation,
24	chars::{cross_horizontal, BoxCharacterExt, PreserveStyle},
25};
26
27mod annotation;
28mod anomaly_fixer;
29mod chars;
30mod formatting;
31mod inline;
32mod single_line;
33
34#[derive(Clone, Debug)]
35struct RawLine {
36	data: Text,
37}
38
39#[derive(Debug)]
40struct AnnotationLine {
41	prefix: Text,
42	line: Text,
43	/// There will be lines drawn to connect lines with the same annotation id specified
44	annotation: Option<AnnotationId>,
45}
46
47#[derive(Debug)]
48struct GapLine {
49	prefix: Text,
50	line: Text,
51}
52
53#[derive(Debug)]
54struct BorderLine {
55	prefix: Text,
56	line: Text,
57	top_border: bool,
58}
59
60#[derive(Debug)]
61struct TextLine {
62	prefix: Text,
63	line_num: usize,
64	line: Text,
65	/// Is this line allowed to be hidden by fold?
66	fold: bool,
67	annotation: Option<AnnotationId>,
68	annotations: Vec<LineAnnotation>,
69	top_annotations: Vec<(Option<AnnotationId>, Text)>,
70	bottom_annotations: Vec<(Option<AnnotationId>, Text)>,
71}
72impl TextLine {
73	#[allow(dead_code)]
74	fn add_prefix(&mut self, this: Text, annotations: Text) {
75		self.prefix.extend([this]);
76		for (_, ele) in self.bottom_annotations.iter_mut() {
77			ele.splice(0..0, Some(annotations.clone()));
78		}
79	}
80	fn len(&self) -> usize {
81		self.line.len()
82	}
83	fn is_empty(&self) -> bool {
84		self.line.is_empty()
85	}
86	// fn trim_end(&mut self) {
87	// 	self.line.truncate(self.line.trim_end().len());
88	// }
89}
90
91fn cons_slices<T>(mut slice: &mut [T], test: impl Fn(&T) -> bool) -> Vec<&mut [T]> {
92	let mut out = Vec::new();
93
94	while !slice.is_empty() {
95		let mut skip = 0;
96		while !slice.get(skip).map(&test).unwrap_or(true) {
97			skip += 1;
98		}
99		let mut take = 0;
100		while slice.get(skip + take).map(&test).unwrap_or(false) {
101			take += 1;
102		}
103		let (_skipped, rest) = slice.split_at_mut(skip);
104		let (taken, rest) = rest.split_at_mut(take);
105		if !taken.is_empty() {
106			out.push(taken);
107		}
108		slice = rest;
109	}
110
111	out
112}
113
114#[derive(Debug)]
115enum Line {
116	Text(TextLine),
117	Annotation(AnnotationLine),
118	Raw(RawLine),
119	Nop,
120	Gap(GapLine),
121	Border(BorderLine),
122}
123impl Line {
124	fn text_mut(&mut self) -> Option<&mut Text> {
125		Some(match self {
126			Line::Text(t) => &mut t.line,
127			Line::Gap(t) => &mut t.line,
128			Line::Annotation(t) => &mut t.line,
129			Line::Border(t) => &mut t.line,
130			_ => return None,
131		})
132	}
133	fn is_text(&self) -> bool {
134		matches!(self, Self::Text(_))
135	}
136	fn is_annotation(&self) -> bool {
137		matches!(self, Self::Annotation(_))
138	}
139	fn is_border(&self) -> bool {
140		matches!(self, Self::Border(_))
141	}
142	fn as_annotation(&self) -> Option<&AnnotationLine> {
143		match self {
144			Self::Annotation(a) => Some(a),
145			_ => None,
146		}
147	}
148	fn is_gap(&self) -> bool {
149		matches!(self, Self::Gap(_))
150	}
151	fn as_text_mut(&mut self) -> Option<&mut TextLine> {
152		match self {
153			Line::Text(t) => Some(t),
154			_ => None,
155		}
156	}
157	#[allow(dead_code)]
158	fn as_gap_mut(&mut self) -> Option<&mut GapLine> {
159		match self {
160			Line::Gap(t) => Some(t),
161			_ => None,
162		}
163	}
164	fn as_text(&self) -> Option<&TextLine> {
165		match self {
166			Line::Text(t) => Some(t),
167			_ => None,
168		}
169	}
170	fn as_raw(&self) -> Option<&RawLine> {
171		match self {
172			Line::Raw(r) => Some(r),
173			_ => None,
174		}
175	}
176	fn is_nop(&self) -> bool {
177		matches!(self, Self::Nop)
178	}
179}
180
181#[derive(Debug)]
182pub struct Source {
183	lines: Vec<Line>,
184}
185
186fn cleanup_nops(source: &mut Source) {
187	let mut i = 0;
188	while i < source.lines.len() {
189		if source.lines[i].is_nop() {
190			source.lines.remove(i);
191		} else {
192			i += 1;
193		}
194	}
195}
196
197/// Remove NOP/empty annotation lines
198fn cleanup(source: &mut Source) {
199	for slice in cons_slices(&mut source.lines, Line::is_text) {
200		for line in slice
201			.iter_mut()
202			.take_while(|l| l.as_text().unwrap().is_empty())
203		{
204			*line = Line::Nop;
205		}
206		for line in slice
207			.iter_mut()
208			.rev()
209			.take_while(|l| l.as_text().unwrap().is_empty())
210		{
211			*line = Line::Nop;
212		}
213	}
214	cleanup_nops(source);
215	for slice in cons_slices(&mut source.lines, Line::is_gap) {
216		if slice.len() == 1 {
217			continue;
218		}
219		for ele in slice.iter_mut().skip(1) {
220			*ele = Line::Nop;
221		}
222	}
223	cleanup_nops(source);
224}
225
226fn fold(source: &mut Source, opts: &Opts) {
227	for slice in cons_slices(&mut source.lines, Line::is_text) {
228		'line: for i in 0..slice.len() {
229			for j in i.saturating_sub(opts.context_lines)..=(i + opts.context_lines) {
230				let Some(ctx) = slice.get(j) else {
231					continue;
232				};
233				let Line::Text(t) = ctx else {
234					continue;
235				};
236				if t.fold {
237					continue;
238				}
239				continue 'line;
240			}
241			slice[i] = Line::Gap(GapLine {
242				prefix: Text::new(),
243				line: Text::new(),
244			});
245		}
246	}
247	cleanup(source);
248}
249
250fn draw_line_numbers(source: &mut Source) {
251	for lines in &mut cons_slices(&mut source.lines, |l| {
252		l.is_annotation() || l.is_text() || l.is_gap() || l.is_border()
253	}) {
254		let max_num = lines
255			.iter()
256			.filter_map(|l| match l {
257				Line::Text(t) => Some(t.line_num),
258				_ => None,
259			})
260			.max()
261			.unwrap_or(0);
262		let max_len = max_num.to_string().len();
263		let prefix_segment =
264			AnnotatedRope::fragment(" ".repeat(max_len - 1), Formatting::line_number());
265		for line in lines.iter_mut() {
266			match line {
267				Line::Text(t) => t.prefix.extend([
268					AnnotatedRope::fragment(
269						format!("{:>width$}  ", t.line_num, width = max_len),
270						Formatting::line_number(),
271					),
272					AnnotatedRope::fragment("│ ", Formatting::border()),
273				]),
274				Line::Annotation(a) => a.prefix.extend([
275					prefix_segment.clone(),
276					AnnotatedRope::fragment("   · ", Formatting::border()),
277				]),
278				Line::Border(a) => a.prefix.extend([
279					prefix_segment.clone(),
280					AnnotatedRope::fragment(
281						format!(
282							"   {}{}{}",
283							bc!(tr).mirror_vertical_if(a.top_border).char_round(),
284							bc!(rl),
285							bc!(rl),
286						),
287						Formatting::border(),
288					),
289				]),
290				Line::Gap(a) => a.prefix.extend([
291					prefix_segment.clone(),
292					AnnotatedRope::fragment(
293						format!("   {} ", bc!(tb).char_dotted_w4()),
294						Formatting::border(),
295					),
296				]),
297				_ => unreachable!(),
298			}
299		}
300	}
301}
302
303fn draw_line_connections(
304	source: &mut Source,
305	annotation_formats: HashMap<AnnotationId, Formatting>,
306) {
307	for lines in &mut cons_slices(&mut source.lines, |l| {
308		l.is_annotation() || l.is_text() || l.is_gap()
309	}) {
310		#[derive(Debug)]
311		struct Connection {
312			range: Range<usize>,
313			connected: Vec<usize>,
314		}
315
316		let mut connected_annotations = HashMap::new();
317		for (i, line) in lines.iter().enumerate() {
318			let annotation = if let Some(annotation) = line.as_annotation() {
319				annotation.annotation
320			} else if let Some(text) = line.as_text() {
321				text.annotation
322			} else {
323				None
324			};
325			if let Some(annotation) = annotation {
326				let conn = connected_annotations
327					.entry(annotation)
328					.or_insert(Connection {
329						range: Range::new(i, i),
330						connected: Vec::new(),
331					});
332				conn.range.start = conn.range.start.min(i);
333				conn.range.end = conn.range.end.max(i);
334				conn.connected.push(i);
335			}
336		}
337		let mut grouped = connected_annotations
338			.iter()
339			.map(|(k, v)| (*k, vec![v.range].into_iter().collect::<RangeSet<usize>>()))
340			.collect::<Vec<_>>();
341
342		grouped.sort_by_key(|a| a.1.num_elements());
343		let grouped = single_line::group_nonconflicting(&grouped, &HashSet::new());
344
345		for group in grouped {
346			for annotation in group {
347				let annotation_fmt = annotation_formats
348					.get(&annotation)
349					.expect("id is used in string but not defined")
350					.clone()
351					.decoration();
352				let conn = connected_annotations.get(&annotation).expect("exists");
353				let range = conn.range;
354				let mut max_index = usize::MAX;
355				for line in range.start..=range.end {
356					match &lines[line] {
357						Line::Text(t) if t.line.chars().all(|c| c.is_whitespace()) => {}
358						Line::Text(t) => {
359							let whitespaces =
360								t.line.chars().take_while(|i| i.is_whitespace()).count();
361							max_index = max_index.min(whitespaces)
362						}
363						Line::Annotation(t) if t.line.chars().all(|c| c.is_whitespace()) => {}
364						Line::Annotation(t) => {
365							let whitespaces =
366								t.line.chars().take_while(|i| i.is_whitespace()).count();
367							max_index = max_index.min(whitespaces)
368						}
369						Line::Gap(_) => {}
370						_ => unreachable!(),
371					}
372				}
373				while max_index < 2 {
374					let seg = Some(AnnotatedRope::fragment(
375						" ".repeat(2 - max_index),
376						annotation_fmt.clone(),
377					));
378					for line in lines.iter_mut() {
379						match line {
380							Line::Text(t) => t.line.splice(0..0, seg.clone()),
381							Line::Annotation(t) => t.line.splice(0..0, seg.clone()),
382							Line::Gap(t) => t.line.splice(0..0, seg.clone()),
383							_ => unreachable!(),
384						}
385					}
386					max_index = 2;
387				}
388				if max_index >= 2 {
389					let offset = max_index - 2;
390
391					for line in range.start..=range.end {
392						let char = if range.start == range.end {
393							bc!(r)
394						} else if line == range.start {
395							bc!(rb)
396						} else if line == range.end {
397							bc!(tr)
398						} else if conn.connected.contains(&line) {
399							bc!(trb)
400						} else {
401							bc!(tb)
402						}
403						.char_round();
404						let text = lines[line].text_mut().expect("only with text reachable");
405						if text.len() <= offset {
406							text.resize(offset + 1, ' ', annotation_fmt.clone());
407						}
408						text.splice(
409							offset..offset + 1,
410							Some(AnnotatedRope::fragment(
411								char.to_string(),
412								annotation_fmt.clone(),
413							)),
414						);
415
416						if conn.connected.contains(&line) {
417							for i in offset + 1..text.len() {
418								let (char, fmt) = text.get(i).expect("in bounds");
419								if !text.get(i).expect("in bounds").0.is_whitespace()
420									&& !fmt.decoration
421								{
422									break;
423								}
424								if let Some((keep_style, replacement)) = cross_horizontal(char) {
425									text.splice(
426										i..=i,
427										Some(AnnotatedRope::fragment(
428											replacement.to_string(),
429											match keep_style {
430												PreserveStyle::Keep => fmt.clone(),
431												PreserveStyle::Replace => annotation_fmt.clone(),
432											},
433										)),
434									)
435								}
436							}
437						}
438					}
439				}
440			}
441		}
442	}
443}
444
445fn generate_annotations(source: &mut Source, opts: &Opts) {
446	for line in source
447		.lines
448		.iter_mut()
449		.flat_map(Line::as_text_mut)
450		.filter(|t| !t.annotations.is_empty())
451	{
452		generate_annotations_line(line, opts);
453	}
454}
455
456fn generate_annotations_line(line: &mut TextLine, opts: &Opts) {
457	// We don't need ranges for those lines, because they are embedded into the code itself.
458	let hide_ranges_for = if opts.colored_range_display && !opts.colorblind_output {
459		let parsed = inline::group_singleline(&line.annotations);
460		assert!(line.annotation.is_none());
461		line.annotation = parsed.annotation;
462		inline::apply_inline_annotations(&mut line.line, &parsed.inline, parsed.right);
463
464		line.annotations
465			.retain(|a| !parsed.processed.contains(&a.id));
466		line.fold = false;
467
468		parsed.hide_ranges_for
469	} else {
470		HashSet::new()
471	};
472
473	let char_to_display_fixup = fixup_char_to_display(line.line.chars());
474
475	let total = line.annotations.len();
476
477	let (mut above, rest) = mem::take(&mut line.annotations)
478		.into_iter()
479		.partition::<Vec<LineAnnotation>, _>(|v| v.location.is_above() && !v.location.is_below());
480	let (mut below, mut both) = rest
481		.into_iter()
482		.partition::<Vec<LineAnnotation>, _>(|v| v.location.is_below() && !v.location.is_above());
483
484	let target_above = (total + above.len() - below.len()).div_ceil(2);
485	let needed_above = target_above.saturating_sub(above.len());
486
487	let below_both = both.split_off(needed_above.min(both.len()));
488	let above_both = both;
489
490	above.extend(above_both);
491	below.extend(below_both);
492
493	for (annotations, above) in [(above, true), (below, false)] {
494		let mut extra = single_line::generate_range_annotations(
495			annotations,
496			&char_to_display_fixup,
497			&hide_ranges_for,
498			!above,
499		);
500		if above {
501			extra.reverse();
502			line.top_annotations = extra;
503		} else {
504			line.bottom_annotations = extra;
505		}
506	}
507}
508
509fn apply_annotations(source: &mut Source) {
510	// Top
511	{
512		let mut insertions = vec![];
513		for (i, line) in source
514			.lines
515			.iter_mut()
516			.enumerate()
517			.flat_map(|(i, l)| l.as_text_mut().map(|t| (i, t)))
518		{
519			for buf in line.top_annotations.drain(..) {
520				insertions.push((i + 1, buf))
521			}
522		}
523		insertions.reverse();
524		for (i, (annotation, line)) in insertions {
525			source.lines.insert(
526				i - 1,
527				Line::Annotation(AnnotationLine {
528					line,
529					annotation,
530					prefix: AnnotatedRope::new(),
531				}),
532			);
533		}
534	}
535	// Bottom
536	{
537		let mut insertions = vec![];
538		for (i, line) in source
539			.lines
540			.iter_mut()
541			.enumerate()
542			.flat_map(|(i, l)| l.as_text_mut().map(|t| (i, t)))
543		{
544			for buf in line.bottom_annotations.drain(..) {
545				insertions.push((i + 1, buf))
546			}
547		}
548		insertions.reverse();
549		for (i, (annotation, line)) in insertions {
550			source.lines.insert(
551				i,
552				Line::Annotation(AnnotationLine {
553					line,
554					annotation,
555					prefix: AnnotatedRope::new(),
556				}),
557			);
558		}
559	}
560}
561
562fn process(
563	source: &mut Source,
564	annotation_formats: HashMap<AnnotationId, Formatting>,
565	opts: &Opts,
566) {
567	cleanup(source);
568	// Format inline annotations
569	generate_annotations(source, opts);
570	// Make gaps in files
571	if opts.fold {
572		fold(source, opts)
573	}
574	// Expand annotation buffers
575	apply_annotations(source);
576	// Connect annotation lines
577	draw_line_connections(source, annotation_formats);
578
579	cleanup(source);
580}
581
582fn to_raw(source: &mut Source) {
583	// To raw
584	for line in &mut source.lines {
585		match line {
586			Line::Text(t) => {
587				let mut buf = AnnotatedRope::new();
588				buf.extend([t.prefix.clone(), t.line.clone()]);
589				*line = Line::Raw(RawLine { data: buf });
590			}
591			Line::Annotation(t) => {
592				let mut buf = AnnotatedRope::new();
593				buf.extend([t.prefix.clone(), t.line.clone()]);
594				*line = Line::Raw(RawLine { data: buf })
595			}
596			Line::Gap(t) => {
597				let mut buf = AnnotatedRope::new();
598				buf.extend([t.prefix.clone(), t.line.clone()]);
599				*line = Line::Raw(RawLine { data: buf })
600			}
601			Line::Border(t) => {
602				let mut buf = AnnotatedRope::new();
603				buf.extend([t.prefix.clone(), t.line.clone()]);
604				*line = Line::Raw(RawLine { data: buf })
605			}
606			Line::Raw(_) | Line::Nop => {}
607		}
608	}
609}
610
611fn linestarts(str: &str) -> BTreeSet<usize> {
612	let mut linestarts = BTreeSet::new();
613	for (i, c) in str.chars().enumerate() {
614		if c == '\n' {
615			linestarts.insert(i + 1);
616		}
617	}
618	linestarts
619}
620#[derive(Debug, Clone, Copy)]
621struct LineCol {
622	line: usize,
623	column: usize,
624}
625fn offset_to_linecol(mut offset: usize, linestarts: &BTreeSet<usize>) -> LineCol {
626	let mut line = 0;
627	let last_offset = linestarts
628		.range(..=offset)
629		.inspect(|_| line += 1)
630		.last()
631		.copied()
632		.unwrap_or(0);
633	offset -= last_offset;
634	LineCol {
635		line,
636		column: offset,
637	}
638}
639
640fn parse(
641	txt: &str,
642	annotations: &[Annotation],
643	opts: &Opts,
644	mut highlights: Vec<(RangeInclusive<usize>, Formatting)>,
645) -> Source {
646	let (txt, byte_to_char_fixup, decorations) = fixup_byte_to_char(txt, opts.tab_width);
647
648	let mut annotations = annotations.to_vec();
649
650	for (r, _) in highlights.iter_mut() {
651		let (mut start, mut end_exclusive) = (*r.start(), *r.end() + 1);
652		apply_fixup(&mut start, &byte_to_char_fixup);
653		apply_fixup(&mut end_exclusive, &byte_to_char_fixup);
654		*r = start..=end_exclusive - 1;
655	}
656
657	// Convert byte offsets to char offsets
658	for annotation in annotations.iter_mut() {
659		let ranges: RangeSet<usize> = annotation
660			.ranges
661			.ranges()
662			.map(|r| {
663				let mut start = r.start;
664				let mut end = r.end;
665				apply_fixup(&mut start, &byte_to_char_fixup);
666				apply_fixup(&mut end, &byte_to_char_fixup);
667				Range::new(start, end)
668			})
669			.collect();
670		annotation.ranges = ranges;
671	}
672	let linestarts = linestarts(&txt);
673
674	let mut lines: Vec<Line> = txt
675		.split('\n')
676		.map(|s| s.to_string())
677		.enumerate()
678		.map(|(num, line)| {
679			let chars = line.chars().chain([' ']).collect::<String>();
680			TextLine {
681				line_num: num + 1,
682				line: AnnotatedRope::fragment(
683					// Reserve 1 char for the spans pointing to EOL
684					chars,
685					Formatting::default(),
686				),
687				annotation: None,
688				prefix: AnnotatedRope::new(),
689				annotations: Vec::new(),
690				bottom_annotations: Vec::new(),
691				top_annotations: Vec::new(),
692				fold: true,
693			}
694		})
695		.map(Line::Text)
696		.collect();
697
698	for (r, f) in highlights.iter() {
699		let start = *r.start();
700		let end = *r.end();
701		let start = offset_to_linecol(start, &linestarts);
702		let end = offset_to_linecol(end, &linestarts);
703
704		for (relative_linenumber, line) in lines[start.line..=end.line].iter_mut().enumerate() {
705			let i = relative_linenumber + start.line;
706			if let Line::Text(text_line) = line {
707				let start = if i == start.line { start.column } else { 0 };
708				let end = if i == end.line {
709					end.column
710				} else {
711					text_line.line.len() - 1
712				};
713				text_line.line.annotate_range(start..=end, f);
714			}
715		}
716	}
717
718	for pos in decorations.iter().copied() {
719		let start = offset_to_linecol(pos, &linestarts);
720		let line = &mut lines[start.line];
721		if let Line::Text(text_line) = line {
722			text_line
723				.line
724				.annotate_range(start.column..=start.column, &Formatting::listchar());
725		}
726	}
727
728	for (aid, annotation) in annotations.iter().enumerate() {
729		let mut line_ranges: BTreeMap<usize, RangeSet<usize>> = BTreeMap::new();
730		for range in annotation.ranges.ranges() {
731			let start = offset_to_linecol(range.start, &linestarts);
732			let end = offset_to_linecol(range.end, &linestarts);
733
734			if start.line == end.line {
735				let set = line_ranges.entry(start.line).or_insert_with(RangeSet::new);
736				*set = set.union(&[Range::new(start.column, end.column)].into_iter().collect());
737			} else {
738				{
739					let set = line_ranges.entry(start.line).or_insert_with(RangeSet::new);
740					let line = lines[start.line].as_text().expect("annotation OOB");
741					*set = set.union(
742						&[Range::new(start.column, line.len() - 1)]
743							.into_iter()
744							.collect(),
745					);
746				}
747				{
748					let set = line_ranges.entry(end.line).or_insert_with(RangeSet::new);
749					*set = set.union(&[Range::new(0, end.column)].into_iter().collect());
750				}
751			}
752		}
753		let left = line_ranges.len() > 1;
754		let line_ranges_len = line_ranges.len();
755
756		for (i, (line, ranges)) in line_ranges.into_iter().enumerate() {
757			let last = i == line_ranges_len - 1;
758			let line = lines[line].as_text_mut().expect("annotation OOB");
759			line.annotations.push(LineAnnotation {
760				id: AnnotationId(aid),
761				priority: annotation.priority,
762				ranges,
763				formatting: annotation.formatting.clone(),
764				left,
765				right: if last {
766					annotation.text.clone()
767				} else {
768					Text::new()
769				},
770				location: annotation.location,
771			});
772			line.fold = false;
773		}
774	}
775
776	let mut source = Source { lines };
777
778	let annotation_formats = annotations
779		.iter()
780		.enumerate()
781		.map(|(aid, a)| (AnnotationId(aid), a.formatting.clone()))
782		.collect();
783
784	process(&mut source, annotation_formats, opts);
785
786	source
787}
788
789pub fn source_to_ansi(source: &Source) -> String {
790	let mut out = String::new();
791	for line in &source.lines {
792		let line = line
793			.as_raw()
794			.expect("after processing all lines should turn raw");
795		let data = line.data.clone();
796		formatting::text_to_ansi(&data, &mut out);
797		out.push('\n');
798	}
799	out
800}
801
802#[derive(Debug)]
803pub struct FormattingGenerator {
804	rand: SmallRng,
805}
806impl FormattingGenerator {
807	pub fn new(src: &[u8]) -> Self {
808		let mut rng_seed = [0; 8];
809		// let seed = seed.to_value();
810		for chunk in src.chunks(8) {
811			for (s, c) in rng_seed.iter_mut().zip(chunk.iter()) {
812				*s ^= *c;
813			}
814		}
815
816		Self {
817			rand: SmallRng::seed_from_u64(u64::from_be_bytes(rng_seed)),
818		}
819	}
820	fn next(&mut self) -> RandomColor {
821		let mut color = RandomColor::new();
822		color.seed(self.rand.random::<u64>());
823		color.luminosity(Luminosity::Bright);
824		color
825	}
826}
827
828#[derive(Debug)]
829pub struct SnippetBuilder {
830	src: String,
831	generator: FormattingGenerator,
832	annotations: Vec<Annotation>,
833	highlights_before: Vec<(RangeInclusive<usize>, Formatting)>,
834
835	file_name: Option<Text>,
836}
837impl SnippetBuilder {
838	pub fn new(src: impl AsRef<str>) -> Self {
839		Self {
840			src: src.as_ref().to_string(),
841			generator: FormattingGenerator::new(src.as_ref().as_bytes()),
842			annotations: Vec::new(),
843			highlights_before: Vec::new(),
844			file_name: None,
845		}
846	}
847	pub fn with_file_name(mut self, filename: impl AsRef<str>, url: Option<String>) -> Self {
848		let mut formatting = Formatting::filename();
849		if let Some(url) = url {
850			formatting = formatting.url(url);
851		}
852		self.file_name = Some(Text::fragment(filename, formatting));
853		self
854	}
855	#[cfg(feature = "tree-sitter")]
856	pub fn highlight(
857		&mut self,
858		config: tree_sitter_highlight::HighlightConfiguration,
859		fmt: impl Fn(usize, &str) -> Formatting,
860	) {
861		use tree_sitter_highlight::{Highlight, Highlighter};
862
863		let mut highlighter = Highlighter::new();
864		let iter = highlighter
865			.highlight(&config, self.src.as_bytes(), None, |_| None)
866			.expect("highlight");
867
868		let mut highlights = Vec::new();
869		let mut highlight: Option<Highlight> = None;
870		for v in iter {
871			let v = v.expect("e");
872			match v {
873				tree_sitter_highlight::HighlightEvent::Source { start, end } => {
874					if let Some(hi) = &highlight {
875						let f = fmt(hi.0, &self.src[start..end]);
876						highlights.push((start..=end - 1, f));
877					}
878				}
879				tree_sitter_highlight::HighlightEvent::HighlightStart(s) => {
880					assert!(highlight.is_none());
881					highlight = Some(s);
882				}
883				tree_sitter_highlight::HighlightEvent::HighlightEnd => {
884					assert!(highlight.is_some());
885					highlight = None;
886				}
887			}
888		}
889		self.highlights_before.extend(highlights);
890	}
891	fn custom(&mut self, custom_color: Gamut, text: Text) -> AnnotationBuilder<'_> {
892		let mut color = self.generator.next();
893		color.hue(custom_color);
894		let formatting = Formatting::rgb(color.to_rgb_array());
895		// FIXME: apply_meta is not implemented
896		// let [r, g, b] = color.luminosity(Luminosity::Light).to_rgb_array();
897		// text.apply_meta(
898		// 	0..text.len(),
899		// 	&AddColorToUncolored(u32::from_be_bytes([r, g, b, 0])),
900		// );
901		AnnotationBuilder {
902			location: AnnotationLocation::AnyNotInline,
903			snippet: self,
904			priority: 0,
905			formatting,
906			ranges: Vec::new(),
907			text,
908		}
909	}
910	pub fn error(&mut self, text: Text) -> AnnotationBuilder<'_> {
911		self.custom(Gamut::Red, text)
912	}
913	pub fn warning(&mut self, text: Text) -> AnnotationBuilder<'_> {
914		self.custom(Gamut::Orange, text)
915	}
916	pub fn note(&mut self, text: Text) -> AnnotationBuilder<'_> {
917		self.custom(Gamut::Green, text)
918	}
919	pub fn info(&mut self, text: Text) -> AnnotationBuilder<'_> {
920		self.custom(Gamut::Blue, text)
921	}
922	pub fn build(self) -> Source {
923		let mut source = parse(
924			&self.src,
925			&self.annotations,
926			&Opts {
927				colored_range_display: true,
928				fold: true,
929				tab_width: 4,
930				context_lines: 2,
931				colorblind_output: false,
932			},
933			self.highlights_before,
934		);
935
936		if let Some(file_name) = self.file_name {
937			draw_file_name(&mut source, file_name);
938		}
939
940		let line_numbers = true;
941		if line_numbers {
942			draw_line_numbers(&mut source);
943		}
944
945		to_raw(&mut source);
946		source
947	}
948}
949
950fn draw_file_name(source: &mut Source, file_name: Text) {
951	source.lines.insert(
952		0,
953		Line::Border(BorderLine {
954			prefix: AnnotatedRope::new(),
955			line: [
956				AnnotatedRope::fragment("[", Formatting::listchar()),
957				file_name,
958				AnnotatedRope::fragment("]", Formatting::listchar()),
959			]
960			.into_iter()
961			.collect(),
962			top_border: true,
963		}),
964	);
965}
966
967#[must_use]
968#[derive(Debug)]
969pub struct AnnotationBuilder<'s> {
970	snippet: &'s mut SnippetBuilder,
971	priority: usize,
972	formatting: Formatting,
973	ranges: Vec<Range<usize>>,
974	text: Text,
975	location: AnnotationLocation,
976}
977
978impl AnnotationBuilder<'_> {
979	pub fn range(mut self, range: RangeInclusive<usize>) -> Self {
980		assert!(
981			*range.end() < self.snippet.src.len(),
982			"out of bounds annotation"
983		);
984		self.ranges.push(Range::new(*range.start(), *range.end()));
985		self
986	}
987	pub fn ranges(mut self, ranges: impl IntoIterator<Item = RangeInclusive<usize>>) -> Self {
988		for range in ranges {
989			self = self.range(range);
990		}
991		self
992	}
993	fn location(mut self, location: AnnotationLocation) -> Self {
994		assert!(
995			matches!(self.location, AnnotationLocation::AnyNotInline),
996			"location methods should only be called once"
997		);
998		self.location = location;
999		self
1000	}
1001	pub fn any_inline(self) -> Self {
1002		self.location(AnnotationLocation::Any)
1003	}
1004	pub fn above(self) -> Self {
1005		self.location(AnnotationLocation::Above)
1006	}
1007	pub fn below(self) -> Self {
1008		self.location(AnnotationLocation::Below)
1009	}
1010	pub fn above_or_inline(self) -> Self {
1011		self.location(AnnotationLocation::AboveOrInline)
1012	}
1013	pub fn below_or_inline(self) -> Self {
1014		self.location(AnnotationLocation::BelowOrInline)
1015	}
1016	pub fn build(self) {
1017		self.snippet.annotations.push(Annotation {
1018			priority: self.priority,
1019			formatting: self.formatting,
1020			ranges: self.ranges.into_iter().collect(),
1021			text: self.text,
1022			location: self.location,
1023		});
1024	}
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029	// #[test]
1030	// fn fullwidth_marker_apply() {
1031	// 	let s = parse(
1032	// 		"ABC",
1033	// 		&[
1034	// 			Annotation {
1035	// 				priority: 0,
1036	// 				formatting: Formatting::color(0xff000000),
1037	// 				ranges: [Range::new(0, 2)].into_iter().collect(),
1038	// 				text: Text::fragment("a", default()),
1039	// 				location: AnnotationLocation::BelowOrInline,
1040	// 			},
1041	// 			Annotation {
1042	// 				priority: 0,
1043	// 				formatting: Formatting::color(0x00ff0000),
1044	// 				ranges: [Range::new(3, 5)].into_iter().collect(),
1045	// 				text: Text::fragment("b", default()),
1046	// 				location: AnnotationLocation::BelowOrInline,
1047	// 			},
1048	// 			Annotation {
1049	// 				priority: 0,
1050	// 				formatting: Formatting::color(0x0000ff00),
1051	// 				ranges: [Range::new(6, 8)].into_iter().collect(),
1052	// 				text: Text::fragment("c", default()),
1053	// 				location: AnnotationLocation::BelowOrInline,
1054	// 			},
1055	// 		],
1056	// 		&Opts {
1057	// 			colored_range_display: true,
1058	// 			fold: true,
1059	// 			tab_width: 4,
1060	// 			context_lines: 2,
1061	// 			colorblind_output: true,
1062	// 		},
1063	// 		vec![],
1064	// 	);
1065	// 	println!("{}", source_to_ansi(&s))
1066	// }
1067
1068	// #[test]
1069	// fn tab_in_normal_and_fullwidth() {
1070	// 	let s = parse(
1071	// 		"A\tB\n\tB\na\tb\n\tb",
1072	// 		&[
1073	// 			Annotation {
1074	// 				priority: 0,
1075	// 				formatting: Formatting::color(0xff000000),
1076	// 				ranges: [Range::new(17, 17)].into_iter().collect(),
1077	// 				text: Text::fragment("Line start", default()),
1078	// 				location: AnnotationLocation::Below,
1079	// 			},
1080	// 			Annotation {
1081	// 				priority: 0,
1082	// 				formatting: Formatting::color(0x00ff0000),
1083	// 				ranges: [Range::new(18, 18)].into_iter().collect(),
1084	// 				text: Text::fragment("Aligned", default()),
1085	// 				location: AnnotationLocation::Below,
1086	// 			},
1087	// 		],
1088	// 		&Opts {
1089	// 			colored_range_display: true,
1090	// 			fold: false,
1091	// 			tab_width: 4,
1092	// 			context_lines: 2,
1093	// 			colorblind_output: true,
1094	// 		},
1095	// 		vec![],
1096	// 	);
1097	// 	println!("{}", source_to_ansi(&s))
1098	// }
1099}