1use ratatui::style::{Color, Modifier, Style};
10use ratatui::text::{Line, Span};
11
12use cuj::facts::ProfileConfig;
13
14pub struct Rendered {
16 pub raw: String,
17 pub lines: Vec<Line<'static>>,
18}
19
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21enum Kind {
22 Tag,
23 Category,
24 Reference,
25 Url,
26 Bookmark,
27 Todo,
28 DoneTodo,
29}
30
31fn style(kind: Kind) -> Style {
32 match kind {
33 Kind::Tag => Style::new().fg(Color::Yellow),
34 Kind::Category => Style::new().fg(Color::Green),
35 Kind::Reference => Style::new().fg(Color::Cyan),
36 Kind::Url => Style::new()
37 .fg(Color::Blue)
38 .add_modifier(Modifier::UNDERLINED),
39 Kind::Bookmark => Style::new().fg(Color::Magenta),
40 Kind::Todo => Style::new().fg(Color::LightRed),
41 Kind::DoneTodo => Style::new().add_modifier(Modifier::DIM),
42 }
43}
44
45pub fn render(text: &str, profile: &ProfileConfig) -> Rendered {
47 let result = cuj::binding::parse(text, profile);
48
49 let mut paint: Vec<Option<Kind>> = vec![None; text.len()];
53 let mut mark = |loc: &Option<jots::Loc>, kind: Kind| {
54 if let Some(l) = loc {
55 for slot in paint.iter_mut().take(l.end.min(text.len())).skip(l.start) {
56 *slot = Some(kind);
57 }
58 }
59 };
60
61 for t in &result.todos {
62 mark(&t.loc, Kind::Todo);
63 }
64 for t in &result.done_todos {
65 mark(&t.loc, Kind::DoneTodo);
66 }
67 for t in result.todos.iter().chain(&result.done_todos) {
68 for r in &t.references {
69 mark(&r.loc, Kind::Reference);
70 }
71 for r in &t.cross_references {
72 mark(&r.loc, Kind::Reference);
73 }
74 for u in &t.urls {
75 mark(&u.loc, Kind::Url);
76 }
77 }
78 for r in &result.references {
79 mark(&r.loc, Kind::Reference);
80 }
81 for r in &result.cross_references {
82 mark(&r.loc, Kind::Reference);
83 }
84 for r in &result.file_references {
85 mark(&r.loc, Kind::Reference);
86 }
87 for r in &result.cross_file_references {
88 mark(&r.loc, Kind::Reference);
89 }
90 for r in &result.resources {
91 mark(&r.loc, Kind::Reference);
92 }
93 for r in &result.inter_jot_resources {
94 mark(&r.loc, Kind::Reference);
95 }
96 for u in &result.urls {
97 mark(&u.loc, Kind::Url);
98 }
99 for b in &result.bookmarks {
100 mark(&b.loc, Kind::Bookmark);
101 }
102
103 let mut names: Vec<(String, Kind)> = Vec::new();
107 let mut collect = |tags: &[String], cats: &[String]| {
108 for t in tags {
109 names.push((format!("..{t}"), Kind::Tag));
110 }
111 for c in cats {
112 names.push((format!("::{c}"), Kind::Category));
113 }
114 };
115 collect(&result.tags, &result.categories);
116 for t in result.todos.iter().chain(&result.done_todos) {
117 collect(&t.tags, &t.categories);
118 }
119 names.sort();
120 names.dedup();
121 for (needle, kind) in &names {
122 for start in occurrences(text, needle) {
123 for slot in paint.iter_mut().skip(start).take(needle.len()) {
124 *slot = Some(*kind);
125 }
126 }
127 }
128
129 let lines = to_lines(text, &paint);
130 Rendered {
131 raw: text.to_string(),
132 lines,
133 }
134}
135
136fn occurrences(text: &str, needle: &str) -> Vec<usize> {
139 let bytes = text.as_bytes();
140 let token = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b':' || b == b'.';
141 let mut out = Vec::new();
142 let mut from = 0;
143 while let Some(i) = text[from..].find(needle) {
144 let start = from + i;
145 let end = start + needle.len();
146 let left_ok = start == 0 || !token(bytes[start - 1]);
147 let right_ok = end >= bytes.len() || !token(bytes[end]);
148 if left_ok && right_ok {
149 out.push(start);
150 }
151 from = start + 1;
152 }
153 out
154}
155
156fn to_lines(text: &str, paint: &[Option<Kind>]) -> Vec<Line<'static>> {
160 let mut lines = Vec::new();
161 let mut offset = 0;
162 for raw_line in text.split('\n') {
163 let mut spans: Vec<Span<'static>> = Vec::new();
164 let mut run_start = 0;
165 let mut run_kind: Option<Kind> = None;
166 let line_paint = &paint[offset..offset + raw_line.len()];
167 for (i, k) in line_paint.iter().enumerate() {
168 if *k != run_kind {
169 if i > run_start {
170 spans.push(span(&raw_line[run_start..i], run_kind));
171 }
172 run_start = i;
173 run_kind = *k;
174 }
175 }
176 if raw_line.len() > run_start {
177 spans.push(span(&raw_line[run_start..], run_kind));
178 }
179 lines.push(Line::from(spans));
180 offset += raw_line.len() + 1;
181 }
182 if text.ends_with('\n') {
185 lines.pop();
186 }
187 lines
188}
189
190fn span(s: &str, kind: Option<Kind>) -> Span<'static> {
191 match kind {
192 Some(k) => Span::styled(s.to_string(), style(k)),
193 None => Span::raw(s.to_string()),
194 }
195}