1pub use lattice_cells::style::{Style, StyledSpan};
9
10pub(crate) const CAPTURE_NAMES: &[&str] = &[
19 "comment.line",
20 "comment",
21 "string.escape",
22 "string",
23 "keyword.control",
24 "keyword.function",
25 "keyword.operator",
26 "keyword",
27 "type.builtin",
28 "type",
29 "number",
30 "constant.builtin",
31 "constant",
32 "function.method",
33 "function.macro",
34 "function",
35 "variable.parameter",
36 "variable.builtin",
37 "variable",
38 "operator",
39 "punctuation.bracket",
40 "punctuation.special",
41 "punctuation.delimiter",
42 "punctuation",
43 "attribute",
44 "tag",
45 "label",
46 "text.title.1",
48 "text.title.2",
49 "text.title.3",
50 "text.title.4",
51 "text.title.5",
52 "text.title.6",
53 "text.title",
54 "text.strong",
55 "text.emphasis",
56 "text.uri",
57 "text.reference",
58 "text.literal",
59 "none",
60];
61
62pub fn name_to_style_with_theme(
83 name: &str,
84 theme: Option<&dyn lattice_theme::ThemeRegistry>,
85) -> Style {
86 let builtin = name_to_style(name);
87 if builtin != Style::Default {
88 return builtin;
89 }
90 let Some(theme) = theme else {
94 return Style::Default;
95 };
96 theme
97 .id(&lattice_theme::ElementName::from(name.to_string()))
98 .map(Style::Element)
99 .unwrap_or(Style::Default)
100}
101
102pub fn name_to_style_pub(name: &str) -> Style {
104 name_to_style(name)
105}
106
107pub fn capture_priority(name: &str) -> u32 {
110 let mut best: Option<usize> = None;
111 let mut probe = name;
112 loop {
113 if let Some(pos) = CAPTURE_NAMES.iter().position(|n| *n == probe) {
114 best = Some(pos);
115 break;
116 }
117 match probe.rfind('.') {
118 Some(i) => probe = &probe[..i],
119 None => break,
120 }
121 }
122 best.map(|p| p as u32).unwrap_or(*ELEMENT_CAPTURE_PRIORITY)
123}
124
125fn element_capture_priority() -> u32 {
140 CAPTURE_NAMES
143 .iter()
144 .position(|n| *n == "keyword")
145 .map(|p| p as u32)
146 .unwrap_or(u32::MAX)
147}
148
149static ELEMENT_CAPTURE_PRIORITY: std::sync::LazyLock<u32> =
151 std::sync::LazyLock::new(element_capture_priority);
152
153pub(crate) fn name_to_style(name: &str) -> Style {
154 let head = name.split('.').next().unwrap_or(name);
155 match head {
156 "comment" if name.starts_with("comment.line") => Style::LineComment,
157 "comment" => Style::Comment,
158 "string" => Style::String,
159 "keyword" => Style::Keyword,
160 "type" => Style::Type,
161 "number" => Style::Number,
162 "function" => Style::Function,
163 "constant" => Style::Constant,
164 "variable" => Style::Variable,
165 "operator" => Style::Operator,
166 "punctuation" if name == "punctuation.special" => Style::Markup,
167 "punctuation" => Style::Punctuation,
168 "attribute" => Style::Attribute,
169 "tag" => Style::Type,
170 "label" => Style::Constant,
171 "text" => match name {
172 "text.title.1" => Style::Heading1,
173 "text.title.2" => Style::Heading2,
174 "text.title.3" => Style::Heading3,
175 "text.title.4" => Style::Heading4,
176 "text.title.5" => Style::Heading5,
177 "text.title.6" => Style::Heading6,
178 "text.title" => Style::Heading1,
179 "text.strong" => Style::Bold,
180 "text.emphasis" => Style::Italic,
181 "text.uri" => Style::Url,
182 "text.reference" => Style::Link,
183 "text.literal" => Style::MarkupRaw,
184 _ => Style::Default,
185 },
186 "none" => Style::Default,
187 _ => Style::Default,
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 #![allow(clippy::unwrap_used, clippy::panic)]
194 use super::*;
195
196 #[test]
197 fn known_names_map_to_distinct_styles() {
198 assert_eq!(name_to_style("keyword"), Style::Keyword);
199 assert_eq!(name_to_style("string"), Style::String);
200 assert_eq!(name_to_style("type"), Style::Type);
201 assert_eq!(name_to_style("number"), Style::Number);
202 }
203
204 #[test]
205 fn dot_prefixed_names_use_the_head() {
206 assert_eq!(name_to_style("keyword.control"), Style::Keyword);
207 assert_eq!(name_to_style("type.builtin"), Style::Type);
208 assert_eq!(name_to_style("string.escape"), Style::String);
209 }
210
211 #[test]
212 fn comment_line_specialization() {
213 assert_eq!(name_to_style("comment.line"), Style::LineComment);
214 assert_eq!(name_to_style("comment"), Style::Comment);
215 }
216
217 #[test]
218 fn unknown_names_fall_back_to_default() {
219 assert_eq!(name_to_style("unknown"), Style::Default);
220 assert_eq!(name_to_style(""), Style::Default);
221 }
222
223 #[test]
224 fn capture_names_table_is_non_empty_and_has_no_duplicates() {
225 assert!(!CAPTURE_NAMES.is_empty());
226 let mut seen = std::collections::HashSet::new();
227 for &n in CAPTURE_NAMES {
228 assert!(seen.insert(n), "duplicate capture name: {n}");
229 }
230 }
231
232 #[test]
233 fn markup_heading_levels_are_distinct() {
234 assert_eq!(name_to_style("text.title.1"), Style::Heading1);
235 assert_eq!(name_to_style("text.title.2"), Style::Heading2);
236 assert_eq!(name_to_style("text.title.6"), Style::Heading6);
237 assert_eq!(name_to_style("text.title"), Style::Heading1);
238 }
239
240 #[test]
241 fn markup_emphasis_styles_resolve() {
242 assert_eq!(name_to_style("text.strong"), Style::Bold);
243 assert_eq!(name_to_style("text.emphasis"), Style::Italic);
244 assert_eq!(name_to_style("text.uri"), Style::Url);
245 assert_eq!(name_to_style("text.reference"), Style::Link);
246 assert_eq!(name_to_style("text.literal"), Style::MarkupRaw);
247 }
248
249 #[test]
250 fn punctuation_special_maps_to_markup() {
251 assert_eq!(name_to_style("punctuation.special"), Style::Markup);
252 assert_eq!(name_to_style("punctuation"), Style::Punctuation);
253 assert_eq!(name_to_style("punctuation.bracket"), Style::Punctuation);
254 }
255
256 use lattice_theme::{
259 ColorRef, ElementName, ElementOwner, InMemoryThemeRegistry, Palette, StyleSpec,
260 ThemeRegistry,
261 };
262
263 fn registry_with(names: &[&str]) -> InMemoryThemeRegistry {
264 let r = InMemoryThemeRegistry::new(Palette::default());
265 for n in names {
266 r.register(
267 ElementName::from((*n).to_string()),
268 ElementOwner::Plugin("org".into()),
269 StyleSpec {
270 fg: Some(ColorRef::Palette("red".into())),
271 ..Default::default()
272 },
273 "test element",
274 );
275 }
276 r
277 }
278
279 #[test]
280 fn tk1_a_registered_element_name_resolves_to_that_element() {
281 let r = registry_with(&["org.todo.WAITING"]);
282 let id = r
283 .id(&ElementName::from("org.todo.WAITING".to_string()))
284 .expect("registered");
285 assert_eq!(
286 name_to_style_with_theme("org.todo.WAITING", Some(&r)),
287 Style::Element(id)
288 );
289 }
290
291 #[test]
292 fn tk1_an_unregistered_dotted_name_is_still_default() {
293 let r = registry_with(&["org.todo.WAITING"]);
296 assert_eq!(
297 name_to_style_with_theme("org.todo.NOPE", Some(&r)),
298 Style::Default
299 );
300 }
301
302 #[test]
306 fn tk1_a_builtin_capture_name_is_never_shadowed_by_an_element() {
307 let r = registry_with(&["keyword", "comment", "text.title.1"]);
308 assert_eq!(
309 name_to_style_with_theme("keyword", Some(&r)),
310 Style::Keyword
311 );
312 assert_eq!(
313 name_to_style_with_theme("comment", Some(&r)),
314 Style::Comment
315 );
316 assert_eq!(
317 name_to_style_with_theme("text.title.1", Some(&r)),
318 Style::Heading1
319 );
320 }
321
322 #[test]
323 fn tk1_no_registry_is_exactly_the_pre_tk1_mapping() {
324 for n in [
325 "keyword",
326 "comment",
327 "text.title.1",
328 "org.todo.WAITING",
329 "utterly.unknown",
330 ] {
331 assert_eq!(
332 name_to_style_with_theme(n, None),
333 name_to_style(n),
334 "{n} must be unchanged without a registry"
335 );
336 }
337 }
338
339 #[test]
348 fn tk1_an_element_capture_outranks_a_title_capture() {
349 let element = capture_priority("org.todo.WAITING");
350 let title = capture_priority("text.title.1");
351 assert!(
352 element < title,
353 "element ({element}) must outrank text.title.1 ({title}), \
354 or the keyword never paints"
355 );
356 }
357
358 #[test]
359 fn tk1_an_element_capture_has_exactly_keywords_priority() {
360 assert_eq!(
363 capture_priority("org.todo.WAITING"),
364 capture_priority("keyword")
365 );
366 }
367
368 #[test]
369 fn tk1_builtin_capture_priorities_are_unchanged() {
370 assert!(capture_priority("comment.line") < capture_priority("comment"));
373 assert!(capture_priority("keyword.control") < capture_priority("keyword"));
374 assert!(capture_priority("text.title.1") < capture_priority("none"));
375 }
376}