1use std::borrow::Cow;
11
12use crate::{
13 ansi::{Color, Ground, NamedColor, Style},
14 errors::LexError,
15 registry::search_registry,
16};
17
18#[derive(Debug, PartialEq, Clone, Copy)]
20pub enum EmphasisType {
21 Dim,
23 Italic,
25 Underline,
27 DoubleUnderline,
29 Bold,
31 Strikethrough,
33 Blink,
35 Overline,
37 Invisible,
39 Reverse,
41 RapidBlink,
43}
44
45#[derive(Debug, PartialEq, Clone)]
50pub enum ResetKind {
51 Emphasis(EmphasisType),
53 Color {
55 color: Color,
57 ground: Ground,
59 },
60}
61
62impl ResetKind {
63 pub(crate) fn matches_tag(&self, tag: &TagType) -> bool {
65 match (self, tag) {
66 (Self::Emphasis(a), TagType::Emphasis(b)) => a == b,
67 (
68 Self::Color {
69 color: ca,
70 ground: ga,
71 },
72 TagType::Color {
73 color: cb,
74 ground: gb,
75 },
76 ) => ca == cb && ga == gb,
77 _ => false,
78 }
79 }
80
81 fn from_tag(tag: &TagType) -> Option<Self> {
86 match tag {
87 TagType::Emphasis(e) => Some(Self::Emphasis(*e)),
88 TagType::Color { color, ground } => Some(Self::Color {
89 color: color.clone(),
90 ground: *ground,
91 }),
92 _ => None,
93 }
94 }
95}
96
97#[derive(Debug, PartialEq, Clone)]
99pub enum TagType {
100 ResetAll,
102 ResetOne(ResetKind),
105 Emphasis(EmphasisType),
107 Color {
109 color: Color,
111 ground: Ground,
113 },
114 Prefix(String),
116}
117
118#[derive(Debug, PartialEq)]
120pub enum Token {
121 Tag(TagType),
123 Text(Cow<'static, str>),
125}
126
127impl EmphasisType {
128 fn from_str(input: &str) -> Option<Self> {
133 match input {
134 "dim" => Some(Self::Dim),
135 "italic" => Some(Self::Italic),
136 "underline" => Some(Self::Underline),
137 "double-underline" => Some(Self::DoubleUnderline),
138 "bold" => Some(Self::Bold),
139 "strikethrough" => Some(Self::Strikethrough),
140 "blink" => Some(Self::Blink),
141 "overline" => Some(Self::Overline),
142 "invisible" => Some(Self::Invisible),
143 "reverse" => Some(Self::Reverse),
144 "rapid-blink" => Some(Self::RapidBlink),
145 _ => None,
146 }
147 }
148}
149
150fn style_to_tags(style: &Style) -> Vec<TagType> {
155 let mut res: Vec<TagType> = Vec::new();
156 let prefix = style.prefix.clone();
157
158 if style.reset {
159 if let Some(p) = prefix {
160 res.push(TagType::Prefix(p));
161 }
162 res.push(TagType::ResetAll);
163 return res;
164 }
165
166 for (enabled, tag) in [
167 (style.bold, TagType::Emphasis(EmphasisType::Bold)),
168 (style.blink, TagType::Emphasis(EmphasisType::Blink)),
169 (style.dim, TagType::Emphasis(EmphasisType::Dim)),
170 (style.italic, TagType::Emphasis(EmphasisType::Italic)),
171 (
172 style.strikethrough,
173 TagType::Emphasis(EmphasisType::Strikethrough),
174 ),
175 (style.underline, TagType::Emphasis(EmphasisType::Underline)),
176 (
177 style.double_underline,
178 TagType::Emphasis(EmphasisType::DoubleUnderline),
179 ),
180 (style.overline, TagType::Emphasis(EmphasisType::Overline)),
181 (style.invisible, TagType::Emphasis(EmphasisType::Invisible)),
182 (style.reverse, TagType::Emphasis(EmphasisType::Reverse)),
183 (
184 style.rapid_blink,
185 TagType::Emphasis(EmphasisType::RapidBlink),
186 ),
187 ] {
188 if enabled {
189 res.push(tag);
190 }
191 }
192
193 if let Some(fg) = style.fg.clone() {
194 res.push(TagType::Color {
195 color: fg,
196 ground: Ground::Foreground,
197 });
198 }
199 if let Some(bg) = style.bg.clone() {
200 res.push(TagType::Color {
201 color: bg,
202 ground: Ground::Background,
203 });
204 }
205
206 if let Some(p) = prefix {
207 res.push(TagType::Prefix(p));
208 }
209
210 res
211}
212
213#[allow(clippy::too_many_lines)]
240pub(crate) fn parse_part(
241 part: &str,
242 position: usize,
243 out: &mut Vec<TagType>,
244) -> Result<(), LexError> {
245 let (ground, part) = if let Some(rest) = part.strip_prefix("bg:") {
246 (Ground::Background, rest)
247 } else if let Some(rest) = part.strip_prefix("fg:") {
248 (Ground::Foreground, rest)
249 } else {
250 (Ground::Foreground, part)
251 };
252 if let Some(remainder) = part.strip_prefix('/') {
253 if remainder.is_empty() {
254 out.push(TagType::ResetAll);
255 Ok(())
256 } else {
257 let mut inner = Vec::new();
258 parse_part(remainder, position + 1, &mut inner)?;
259 if let [tag] = inner.as_slice() {
260 match tag {
261 TagType::ResetAll | TagType::ResetOne(_) | TagType::Prefix(_) => {
262 Err(LexError::InvalidResetTarget(position))
263 }
264 _ => {
265 out.push(TagType::ResetOne(ResetKind::from_tag(tag).unwrap()));
266 Ok(())
267 }
268 }
269 } else {
270 let count_before = out.len();
271 for t in &inner {
272 if !matches!(
273 t,
274 TagType::Prefix(_) | TagType::ResetAll | TagType::ResetOne(_)
275 ) && let Some(kind) = ResetKind::from_tag(t)
276 {
277 out.push(TagType::ResetOne(kind));
278 }
279 }
280 if out.len() == count_before {
281 Err(LexError::InvalidResetTarget(position))
282 } else {
283 Ok(())
284 }
285 }
286 }
287 } else if let Some(color) = NamedColor::from_str(part) {
288 out.push(TagType::Color {
289 color: Color::Named(color),
290 ground,
291 });
292 Ok(())
293 } else if let Some(emphasis) = EmphasisType::from_str(part) {
294 out.push(TagType::Emphasis(emphasis));
295 Ok(())
296 } else if let Some(rest) = part.strip_prefix("ansi(") {
297 if !rest.ends_with(')') {
298 return Err(LexError::UnclosedValue(position));
299 }
300 let ansi_val = &rest[..rest.len() - 1];
301 match ansi_val.trim().parse::<u8>() {
302 Ok(code) => {
303 out.push(TagType::Color {
304 color: Color::Ansi256(code),
305 ground,
306 });
307 Ok(())
308 }
309 Err(_) => Err(LexError::InvalidValue {
310 value: ansi_val.to_string(),
311 position,
312 }),
313 }
314 } else if let Some(rest) = part.strip_prefix("rgb(") {
315 if !rest.ends_with(')') {
316 return Err(LexError::UnclosedValue(position));
317 }
318 let rgb_val = &rest[..rest.len() - 1];
319 let parts: Result<Vec<u8>, _> =
320 rgb_val.split(',').map(|v| v.trim().parse::<u8>()).collect();
321 match parts {
322 Ok(v) if v.len() == 3 => {
323 out.push(TagType::Color {
324 color: Color::Rgb(v[0], v[1], v[2]),
325 ground,
326 });
327 Ok(())
328 }
329 Ok(v) => Err(LexError::InvalidArgumentCount {
330 expected: 3,
331 got: v.len(),
332 position,
333 }),
334 Err(_) => Err(LexError::InvalidValue {
335 value: rgb_val.to_string(),
336 position,
337 }),
338 }
339 } else if let Some(rest) = part.strip_prefix("hsl(") {
340 if !rest.ends_with(')') {
341 return Err(LexError::UnclosedValue(position));
342 }
343 let inner = &rest[..rest.len() - 1];
344 let raw: Vec<&str> = inner.split(',').collect();
345 if raw.len() != 3 {
346 return Err(LexError::InvalidArgumentCount {
347 expected: 3,
348 got: raw.len(),
349 position,
350 });
351 }
352 let vals: Vec<f64> = raw
353 .iter()
354 .map(|v| v.trim().parse::<f64>())
355 .collect::<Result<_, _>>()
356 .map_err(|_| LexError::InvalidValue {
357 value: inner.to_string(),
358 position,
359 })?;
360 if !(0.0..=360.0).contains(&vals[0]) {
361 return Err(LexError::InvalidValue {
362 value: format!("hue {} out of range (0-360)", vals[0]),
363 position,
364 });
365 }
366 if !(0.0..=100.0).contains(&vals[1]) {
367 return Err(LexError::InvalidValue {
368 value: format!("saturation {} out of range (0-100)", vals[1]),
369 position,
370 });
371 }
372 if !(0.0..=100.0).contains(&vals[2]) {
373 return Err(LexError::InvalidValue {
374 value: format!("lightness {} out of range (0-100)", vals[2]),
375 position,
376 });
377 }
378 let (r, g, b) = hsl_to_rgb(vals[0], vals[1], vals[2]);
379 out.push(TagType::Color {
380 color: Color::Rgb(r, g, b),
381 ground,
382 });
383 Ok(())
384 } else if let Some(rest) = part.strip_prefix("hsv(") {
385 let [h, s, v] =
386 parse_color_triple(rest, "hsv", position, 0.0, 360.0, 0.0, 100.0, 0.0, 100.0)?;
387 let (r, g, b) = hsv_to_rgb(h, s, v);
388 out.push(TagType::Color {
389 color: Color::Rgb(r, g, b),
390 ground,
391 });
392 Ok(())
393 } else if let Some(rest) = part.strip_prefix("hsb(") {
394 let [h, s, v] =
395 parse_color_triple(rest, "hsb", position, 0.0, 360.0, 0.0, 100.0, 0.0, 100.0)?;
396 let (r, g, b) = hsv_to_rgb(h, s, v);
397 out.push(TagType::Color {
398 color: Color::Rgb(r, g, b),
399 ground,
400 });
401 Ok(())
402 } else if let Some(rest) = part.strip_prefix("hwb(") {
403 let [h, w, blk] =
404 parse_color_triple(rest, "hwb", position, 0.0, 360.0, 0.0, 100.0, 0.0, 100.0)?;
405 if w + blk > 100.0 {
406 return Err(LexError::InvalidValue {
407 value: format!("whiteness+blackness {} exceeds 100", w + blk),
408 position,
409 });
410 }
411 let (r, g, b) = hwb_to_rgb(h, w, blk);
412 out.push(TagType::Color {
413 color: Color::Rgb(r, g, b),
414 ground,
415 });
416 Ok(())
417 } else if let Some(rest) = part.strip_prefix("lab(") {
418 let [l, a, b_val] = parse_color_triple(
419 rest, "lab", position, 0.0, 100.0, -128.0, 127.0, -128.0, 127.0,
420 )?;
421 let (r, g, b) = lab_to_rgb(l, a, b_val);
422 out.push(TagType::Color {
423 color: Color::Rgb(r, g, b),
424 ground,
425 });
426 Ok(())
427 } else if let Some(rest) = part.strip_prefix("lch(") {
428 let [l, c, h] =
429 parse_color_triple(rest, "lch", position, 0.0, 100.0, 0.0, 150.0, 0.0, 360.0)?;
430 let (r, g, b) = lch_to_rgb(l, c, h);
431 out.push(TagType::Color {
432 color: Color::Rgb(r, g, b),
433 ground,
434 });
435 Ok(())
436 } else if let Some(rest) = part.strip_prefix("oklch(") {
437 let [l, c, h] =
438 parse_color_triple(rest, "oklch", position, 0.0, 1.0, 0.0, 0.4, 0.0, 360.0)?;
439 let (r, g, b) = oklch_to_rgb(l, c, h);
440 out.push(TagType::Color {
441 color: Color::Rgb(r, g, b),
442 ground,
443 });
444 Ok(())
445 } else if let Some(hex) = part.strip_prefix('#') {
446 if hex.is_empty() {
447 return Err(LexError::InvalidValue {
448 value: String::new(),
449 position,
450 });
451 }
452 let (r, g, b) = match hex.len() {
453 3 => {
454 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).map_err(|_| {
455 LexError::InvalidValue {
456 value: hex.to_string(),
457 position,
458 }
459 })?;
460 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).map_err(|_| {
461 LexError::InvalidValue {
462 value: hex.to_string(),
463 position,
464 }
465 })?;
466 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).map_err(|_| {
467 LexError::InvalidValue {
468 value: hex.to_string(),
469 position,
470 }
471 })?;
472 (r, g, b)
473 }
474 6 => {
475 let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| LexError::InvalidValue {
476 value: hex.to_string(),
477 position,
478 })?;
479 let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| LexError::InvalidValue {
480 value: hex.to_string(),
481 position,
482 })?;
483 let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| LexError::InvalidValue {
484 value: hex.to_string(),
485 position,
486 })?;
487 (r, g, b)
488 }
489 _ => {
490 return Err(LexError::InvalidValue {
491 value: hex.to_string(),
492 position,
493 });
494 }
495 };
496 out.push(TagType::Color {
497 color: Color::Rgb(r, g, b),
498 ground,
499 });
500 Ok(())
501 } else {
502 match search_registry(part) {
503 Ok(style) => {
504 for tag in style_to_tags(&style) {
505 out.push(tag);
506 }
507 Ok(())
508 }
509 Err(_) => Err(LexError::InvalidTag {
510 tag_content: part.to_string(),
511 position,
512 }),
513 }
514 }
515}
516
517pub(crate) fn split_tag_parts(raw_tag: &str) -> Vec<(usize, &str)> {
522 let mut parts = Vec::new();
523 let mut part_start = 0;
524 let mut depth = 0usize;
525
526 for (i, c) in raw_tag.char_indices() {
527 match c {
528 '(' => depth += 1,
529 ')' => depth = depth.saturating_sub(1),
530 c if c.is_whitespace() && depth == 0 => {
531 if i > part_start {
532 parts.push((part_start, &raw_tag[part_start..i]));
533 }
534 part_start = i + c.len_utf8();
535 }
536 _ => {}
537 }
538 }
539 if part_start < raw_tag.len() {
540 parts.push((part_start, &raw_tag[part_start..]));
541 }
542 parts
543}
544
545fn parse_tag(raw_tag: &str, tag_start: usize, tokens: &mut Vec<Token>) -> Result<(), LexError> {
554 let mut parts = Vec::new();
555
556 for (offset, part) in split_tag_parts(raw_tag) {
557 let abs_position = tag_start + offset;
558 parse_part(part, abs_position, &mut parts)?;
559 }
560
561 for tag in parts {
562 tokens.push(Token::Tag(tag));
563 }
564
565 Ok(())
566}
567
568pub fn tokenize(input: impl Into<String>) -> Result<Vec<Token>, LexError> {
587 let input = input.into();
588 let mut tokens: Vec<Token> = Vec::with_capacity(input.len() / 4);
589 let mut pos = 0;
590 loop {
591 let next = [
592 input[pos..].find('[').map(|i| (i, b'[')),
593 input[pos..].find(']').map(|i| (i, b']')),
594 ]
595 .into_iter()
596 .flatten()
597 .min_by_key(|(i, _)| *i);
598
599 let Some((starting, kind)) = next else {
600 if pos < input.len() {
601 tokens.push(Token::Text(Cow::Owned(input[pos..].to_string())));
602 }
603 break;
604 };
605 let abs_starting = starting + pos;
606
607 if kind == b']' {
608 if pos != abs_starting {
609 tokens.push(Token::Text(Cow::Owned(
610 input[pos..abs_starting].to_string(),
611 )));
612 }
613 if input.as_bytes().get(abs_starting + 1) == Some(&b']') {
614 tokens.push(Token::Text(Cow::Borrowed("]")));
615 pos = abs_starting + 2;
616 } else {
617 tokens.push(Token::Text(Cow::Borrowed("]")));
618 pos = abs_starting + 1;
619 }
620 continue;
621 }
622
623 if abs_starting > 0 && input.as_bytes().get(abs_starting.wrapping_sub(1)) == Some(&b'\x1b')
625 {
626 tokens.push(Token::Text(Cow::Owned(
627 input[pos..=abs_starting].to_string(),
628 )));
629 pos = abs_starting + 1;
630 continue;
631 }
632
633 if input.as_bytes().get(abs_starting + 1) == Some(&b'[') {
634 let before = &input[pos..abs_starting];
635 if !before.is_empty() {
636 tokens.push(Token::Text(Cow::Owned(before.to_string())));
637 }
638 tokens.push(Token::Text(Cow::Borrowed("[")));
639 pos = abs_starting + 2;
640 continue;
641 }
642
643 if pos != abs_starting {
644 tokens.push(Token::Text(Cow::Owned(
645 input[pos..abs_starting].to_string(),
646 )));
647 }
648
649 let Some(closing) = input[abs_starting..].find(']') else {
650 return Err(LexError::UnclosedTag(abs_starting));
651 };
652 let abs_closing = closing + abs_starting;
653 let raw_tag = &input[abs_starting + 1..abs_closing];
654 parse_tag(raw_tag, abs_starting, &mut tokens)?;
655 pos = abs_closing + 1;
656 }
657 Ok(tokens)
658}
659
660#[allow(clippy::too_many_arguments)]
668fn parse_color_triple(
669 rest: &str,
670 func_name: &str,
671 position: usize,
672 min1: f64,
673 max1: f64,
674 min2: f64,
675 max2: f64,
676 min3: f64,
677 max3: f64,
678) -> Result<[f64; 3], LexError> {
679 if !rest.ends_with(')') {
680 return Err(LexError::UnclosedValue(position));
681 }
682 let inner = &rest[..rest.len() - 1];
683 let raw: Vec<&str> = inner.split(',').collect();
684 if raw.len() != 3 {
685 return Err(LexError::InvalidArgumentCount {
686 expected: 3,
687 got: raw.len(),
688 position,
689 });
690 }
691 let vals: Vec<f64> = raw
692 .iter()
693 .map(|v| v.trim().parse::<f64>())
694 .collect::<Result<_, _>>()
695 .map_err(|_| LexError::InvalidValue {
696 value: inner.to_string(),
697 position,
698 })?;
699
700 if !(min1..=max1).contains(&vals[0]) {
701 return Err(LexError::InvalidValue {
702 value: format!("{func_name} arg1 {} out of range ({min1}-{max1})", vals[0]),
703 position,
704 });
705 }
706 if !(min2..=max2).contains(&vals[1]) {
707 return Err(LexError::InvalidValue {
708 value: format!("{func_name} arg2 {} out of range ({min2}-{max2})", vals[1]),
709 position,
710 });
711 }
712 if !(min3..=max3).contains(&vals[2]) {
713 return Err(LexError::InvalidValue {
714 value: format!("{func_name} arg3 {} out of range ({min3}-{max3})", vals[2]),
715 position,
716 });
717 }
718
719 Ok([vals[0], vals[1], vals[2]])
720}
721
722#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
729fn hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> (u8, u8, u8) {
730 let saturation = saturation / 100.0;
731 let lightness = lightness / 100.0;
732
733 let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
734 let x = chroma * (1.0 - ((hue / 60.0) % 2.0 - 1.0).abs());
735 let match_lightness = lightness - chroma / 2.0;
736
737 let (red, green, blue) = match hue as u16 % 360 {
738 0..=59 => (chroma, x, 0.0),
739 60..=119 => (x, chroma, 0.0),
740 120..=179 => (0.0, chroma, x),
741 180..=239 => (0.0, x, chroma),
742 240..=299 => (x, 0.0, chroma),
743 _ => (chroma, 0.0, x),
744 };
745
746 (
747 ((red + match_lightness) * 255.0).round() as u8,
748 ((green + match_lightness) * 255.0).round() as u8,
749 ((blue + match_lightness) * 255.0).round() as u8,
750 )
751}
752
753#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
757fn hsv_to_rgb(hue: f64, saturation: f64, value: f64) -> (u8, u8, u8) {
758 let s = saturation / 100.0;
759 let v = value / 100.0;
760
761 let chroma = v * s;
762 let x = chroma * (1.0 - ((hue / 60.0) % 2.0 - 1.0).abs());
763 let m = v - chroma;
764
765 let (red, green, blue) = match hue as u16 % 360 {
766 0..=59 => (chroma, x, 0.0),
767 60..=119 => (x, chroma, 0.0),
768 120..=179 => (0.0, chroma, x),
769 180..=239 => (0.0, x, chroma),
770 240..=299 => (x, 0.0, chroma),
771 _ => (chroma, 0.0, x),
772 };
773
774 (
775 ((red + m) * 255.0).round() as u8,
776 ((green + m) * 255.0).round() as u8,
777 ((blue + m) * 255.0).round() as u8,
778 )
779}
780
781#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
786fn hwb_to_rgb(hue: f64, whiteness: f64, blackness: f64) -> (u8, u8, u8) {
787 let w = whiteness / 100.0;
788 let b = blackness / 100.0;
789
790 if w + b >= 1.0 {
792 let gray = (w / (w + b) * 255.0).round() as u8;
793 return (gray, gray, gray);
794 }
795
796 let x = 1.0 - ((hue / 60.0) % 2.0 - 1.0).abs();
798
799 let (red, green, blue) = match hue as u16 % 360 {
800 0..=59 => (1.0, x, 0.0),
801 60..=119 => (x, 1.0, 0.0),
802 120..=179 => (0.0, 1.0, x),
803 180..=239 => (0.0, x, 1.0),
804 240..=299 => (x, 0.0, 1.0),
805 _ => (1.0, 0.0, x),
806 };
807
808 let factor = 1.0 - w - b;
809 (
810 ((red * factor + w) * 255.0).round() as u8,
811 ((green * factor + w) * 255.0).round() as u8,
812 ((blue * factor + w) * 255.0).round() as u8,
813 )
814}
815
816fn srgb_gamma(c: f64) -> f64 {
818 let c = c.clamp(0.0, 1.0);
819 if c <= 0.003_130_8 {
820 12.92 * c
821 } else {
822 1.055 * c.powf(1.0 / 2.4) - 0.055
823 }
824}
825
826#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
831fn lab_to_rgb(l: f64, a: f64, b: f64) -> (u8, u8, u8) {
832 const XN: f64 = 0.950_47;
834 const YN: f64 = 1.0;
835 const ZN: f64 = 1.088_83;
836
837 const DELTA: f64 = 6.0 / 29.0;
838 const DELTA_SQ: f64 = DELTA * DELTA; let fy = (l + 16.0) / 116.0;
842 let fx = a / 500.0 + fy;
843 let fz = fy - b / 200.0;
844
845 let x = if fx > DELTA {
846 fx * fx * fx
847 } else {
848 3.0 * DELTA_SQ * (fx - 4.0 / 29.0)
849 };
850 let y = if fy > DELTA {
851 fy * fy * fy
852 } else {
853 3.0 * DELTA_SQ * (fy - 4.0 / 29.0)
854 };
855 let z = if fz > DELTA {
856 fz * fz * fz
857 } else {
858 3.0 * DELTA_SQ * (fz - 4.0 / 29.0)
859 };
860
861 let x = x * XN;
862 let y = y * YN;
863 let z = z * ZN;
864
865 let r_lin = 3.240_454_2 * x - 1.537_138_5 * y - 0.498_531_4 * z;
867 let g_lin = -0.969_266_0 * x + 1.876_010_8 * y + 0.041_556_0 * z;
868 let b_lin = 0.055_643_4 * x - 0.204_025_9 * y + 1.057_225_2 * z;
869
870 (
871 (srgb_gamma(r_lin) * 255.0).round() as u8,
872 (srgb_gamma(g_lin) * 255.0).round() as u8,
873 (srgb_gamma(b_lin) * 255.0).round() as u8,
874 )
875}
876
877#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
882fn lch_to_rgb(l: f64, c: f64, h: f64) -> (u8, u8, u8) {
883 let h_rad = h.to_radians();
884 let a = c * h_rad.cos();
885 let b = c * h_rad.sin();
886 lab_to_rgb(l, a, b)
887}
888
889#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
893fn oklch_to_rgb(l: f64, c: f64, h: f64) -> (u8, u8, u8) {
894 let h_rad = h.to_radians();
895 let a = c * h_rad.cos();
896 let b = c * h_rad.sin();
897
898 let lms_l = l + 0.396_337_777_4 * a + 0.215_803_757_3 * b;
900 let lms_m = l - 0.105_561_345_8 * a - 0.063_854_172_8 * b;
901 let lms_s = l - 0.089_484_177_5 * a - 1.291_485_548_0 * b;
902
903 let l_lms = lms_l * lms_l * lms_l;
905 let m_lms = lms_m * lms_m * lms_m;
906 let s_lms = lms_s * lms_s * lms_s;
907
908 let r_lin = 4.076_741_662_1 * l_lms - 3.307_711_591_3 * m_lms + 0.230_969_929_2 * s_lms;
910 let g_lin = -1.268_438_004_6 * l_lms + 2.609_757_401_1 * m_lms - 0.341_319_396_5 * s_lms;
911 let b_lin = -0.004_196_086_3 * l_lms - 0.703_418_614_7 * m_lms + 1.707_614_701_0 * s_lms;
912
913 (
914 (srgb_gamma(r_lin) * 255.0).round() as u8,
915 (srgb_gamma(g_lin) * 255.0).round() as u8,
916 (srgb_gamma(b_lin) * 255.0).round() as u8,
917 )
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923 use crate::ansi::{Color, Ground, NamedColor};
924
925 fn parse_part(part: &str, position: usize) -> Result<Vec<TagType>, LexError> {
927 let mut out = Vec::new();
928 super::parse_part(part, position, &mut out).map(|_| out)
929 }
930
931 #[test]
934 fn test_emphasis_from_str_all_known() {
935 assert_eq!(EmphasisType::from_str("dim"), Some(EmphasisType::Dim));
936 assert_eq!(EmphasisType::from_str("italic"), Some(EmphasisType::Italic));
937 assert_eq!(
938 EmphasisType::from_str("underline"),
939 Some(EmphasisType::Underline)
940 );
941 assert_eq!(EmphasisType::from_str("bold"), Some(EmphasisType::Bold));
942 assert_eq!(
943 EmphasisType::from_str("strikethrough"),
944 Some(EmphasisType::Strikethrough)
945 );
946 assert_eq!(EmphasisType::from_str("blink"), Some(EmphasisType::Blink));
947 }
948
949 #[test]
950 fn test_emphasis_from_str_unknown_returns_none() {
951 assert_eq!(EmphasisType::from_str("flash"), None);
952 }
953
954 #[test]
955 fn test_emphasis_from_str_case_sensitive() {
956 assert_eq!(EmphasisType::from_str("Bold"), None);
957 }
958
959 #[test]
962 fn test_parse_part_reset() {
963 assert_eq!(parse_part("/", 0).unwrap(), vec![TagType::ResetAll]);
964 }
965
966 #[test]
967 fn test_parse_part_named_color_foreground_default() {
968 assert_eq!(
969 parse_part("red", 0).unwrap(),
970 vec![TagType::Color {
971 color: Color::Named(NamedColor::Red),
972 ground: Ground::Foreground,
973 }]
974 );
975 }
976
977 #[test]
978 fn test_parse_part_named_color_explicit_fg() {
979 assert_eq!(
980 parse_part("fg:red", 0).unwrap(),
981 vec![TagType::Color {
982 color: Color::Named(NamedColor::Red),
983 ground: Ground::Foreground,
984 }]
985 );
986 }
987
988 #[test]
989 fn test_parse_part_named_color_bg() {
990 assert_eq!(
991 parse_part("bg:red", 0).unwrap(),
992 vec![TagType::Color {
993 color: Color::Named(NamedColor::Red),
994 ground: Ground::Background,
995 }]
996 );
997 }
998
999 #[test]
1000 fn test_parse_part_emphasis_bold() {
1001 assert_eq!(
1002 parse_part("bold", 0).unwrap(),
1003 vec![TagType::Emphasis(EmphasisType::Bold)]
1004 );
1005 }
1006
1007 #[test]
1008 fn test_parse_part_ansi256_valid() {
1009 assert_eq!(
1010 parse_part("ansi(200)", 0).unwrap(),
1011 vec![TagType::Color {
1012 color: Color::Ansi256(200),
1013 ground: Ground::Foreground,
1014 }]
1015 );
1016 }
1017
1018 #[test]
1019 fn test_parse_part_ansi256_bg() {
1020 assert_eq!(
1021 parse_part("bg:ansi(200)", 0).unwrap(),
1022 vec![TagType::Color {
1023 color: Color::Ansi256(200),
1024 ground: Ground::Background,
1025 }]
1026 );
1027 }
1028
1029 #[test]
1030 fn test_parse_part_ansi256_with_whitespace() {
1031 assert_eq!(
1032 parse_part("ansi( 42 )", 0).unwrap(),
1033 vec![TagType::Color {
1034 color: Color::Ansi256(42),
1035 ground: Ground::Foreground,
1036 }]
1037 );
1038 }
1039
1040 #[test]
1041 fn test_parse_part_ansi256_invalid_value() {
1042 assert!(parse_part("ansi(abc)", 0).is_err());
1043 }
1044
1045 #[test]
1046 fn test_parse_part_rgb_valid() {
1047 assert_eq!(
1048 parse_part("rgb(255,128,0)", 0).unwrap(),
1049 vec![TagType::Color {
1050 color: Color::Rgb(255, 128, 0),
1051 ground: Ground::Foreground,
1052 }]
1053 );
1054 }
1055
1056 #[test]
1057 fn test_parse_part_rgb_bg() {
1058 assert_eq!(
1059 parse_part("bg:rgb(255,128,0)", 0).unwrap(),
1060 vec![TagType::Color {
1061 color: Color::Rgb(255, 128, 0),
1062 ground: Ground::Background,
1063 }]
1064 );
1065 }
1066
1067 #[test]
1068 fn test_parse_part_rgb_with_spaces() {
1069 assert_eq!(
1070 parse_part("rgb( 10 , 20 , 30 )", 0).unwrap(),
1071 vec![TagType::Color {
1072 color: Color::Rgb(10, 20, 30),
1073 ground: Ground::Foreground,
1074 }]
1075 );
1076 }
1077
1078 #[test]
1079 fn test_parse_part_rgb_wrong_arg_count() {
1080 let result = parse_part("rgb(1,2)", 0);
1081 assert!(result.is_err());
1082 if let Err(crate::errors::LexError::InvalidArgumentCount { expected, got, .. }) = result {
1083 assert_eq!(expected, 3);
1084 assert_eq!(got, 2);
1085 }
1086 }
1087
1088 #[test]
1089 fn test_parse_part_rgb_invalid_value() {
1090 assert!(parse_part("rgb(r,g,b)", 0).is_err());
1091 }
1092
1093 #[test]
1096 fn test_parse_part_hex_6digit() {
1097 assert_eq!(
1098 parse_part("#ff0000", 0).unwrap(),
1099 vec![TagType::Color {
1100 color: Color::Rgb(255, 0, 0),
1101 ground: Ground::Foreground,
1102 }]
1103 );
1104 }
1105
1106 #[test]
1107 fn test_parse_part_hex_3digit() {
1108 assert_eq!(
1109 parse_part("#f00", 0).unwrap(),
1110 vec![TagType::Color {
1111 color: Color::Rgb(255, 0, 0),
1112 ground: Ground::Foreground,
1113 }]
1114 );
1115 }
1116
1117 #[test]
1118 fn test_parse_part_hex_bg() {
1119 assert_eq!(
1120 parse_part("bg:#ffffff", 0).unwrap(),
1121 vec![TagType::Color {
1122 color: Color::Rgb(255, 255, 255),
1123 ground: Ground::Background,
1124 }]
1125 );
1126 }
1127
1128 #[test]
1129 fn test_parse_part_hex_invalid_length() {
1130 assert!(parse_part("#ff", 0).is_err());
1131 assert!(parse_part("#ffff", 0).is_err());
1132 assert!(parse_part("#fffffff", 0).is_err());
1133 }
1134
1135 #[test]
1136 fn test_parse_part_hex_invalid_chars() {
1137 assert!(parse_part("#xyz", 0).is_err());
1138 assert!(parse_part("#xyzzzz", 0).is_err());
1139 }
1140
1141 #[test]
1142 fn test_parse_part_hex_empty() {
1143 assert!(parse_part("#", 0).is_err());
1144 }
1145
1146 #[test]
1149 fn test_parse_part_hsl_red() {
1150 assert_eq!(
1151 parse_part("hsl(0,100,50)", 0).unwrap(),
1152 vec![TagType::Color {
1153 color: Color::Rgb(255, 0, 0),
1154 ground: Ground::Foreground,
1155 }]
1156 );
1157 }
1158
1159 #[test]
1160 fn test_parse_part_hsl_green() {
1161 assert_eq!(
1162 parse_part("hsl(120,100,50)", 0).unwrap(),
1163 vec![TagType::Color {
1164 color: Color::Rgb(0, 255, 0),
1165 ground: Ground::Foreground,
1166 }]
1167 );
1168 }
1169
1170 #[test]
1171 fn test_parse_part_hsl_blue() {
1172 assert_eq!(
1173 parse_part("hsl(240,100,50)", 0).unwrap(),
1174 vec![TagType::Color {
1175 color: Color::Rgb(0, 0, 255),
1176 ground: Ground::Foreground,
1177 }]
1178 );
1179 }
1180
1181 #[test]
1182 fn test_parse_part_hsl_bg() {
1183 assert_eq!(
1184 parse_part("bg:hsl(0,100,50)", 0).unwrap(),
1185 vec![TagType::Color {
1186 color: Color::Rgb(255, 0, 0),
1187 ground: Ground::Background,
1188 }]
1189 );
1190 }
1191
1192 #[test]
1193 fn test_parse_part_hsl_wrong_arg_count() {
1194 let result = parse_part("hsl(0,50)", 0);
1195 assert!(result.is_err());
1196 if let Err(crate::errors::LexError::InvalidArgumentCount { expected, got, .. }) = result {
1197 assert_eq!(expected, 3);
1198 assert_eq!(got, 2);
1199 }
1200 }
1201
1202 #[test]
1203 fn test_parse_part_hsl_hue_out_of_range() {
1204 let result = parse_part("hsl(400,50,50)", 0);
1205 assert!(result.is_err());
1206 }
1207
1208 #[test]
1209 fn test_parse_part_hsl_sat_out_of_range() {
1210 let result = parse_part("hsl(0,150,50)", 0);
1211 assert!(result.is_err());
1212 }
1213
1214 #[test]
1215 fn test_parse_part_hsl_light_out_of_range() {
1216 let result = parse_part("hsl(0,50,110)", 0);
1217 assert!(result.is_err());
1218 }
1219
1220 #[test]
1221 fn test_parse_part_hsl_invalid_value() {
1222 assert!(parse_part("hsl(a,b,c)", 0).is_err());
1223 }
1224
1225 #[test]
1226 fn test_parse_part_hsl_unclosed() {
1227 assert!(parse_part("hsl(0,50,50", 0).is_err());
1228 }
1229
1230 #[test]
1231 fn test_parse_part_hsl_with_spaces() {
1232 assert_eq!(
1233 parse_part("hsl( 120 , 100 , 50 )", 0).unwrap(),
1234 vec![TagType::Color {
1235 color: Color::Rgb(0, 255, 0),
1236 ground: Ground::Foreground,
1237 }]
1238 );
1239 }
1240
1241 #[test]
1244 fn test_parse_part_hsv_red() {
1245 assert_eq!(
1246 parse_part("hsv(0,100,100)", 0).unwrap(),
1247 vec![TagType::Color {
1248 color: Color::Rgb(255, 0, 0),
1249 ground: Ground::Foreground,
1250 }]
1251 );
1252 }
1253
1254 #[test]
1255 fn test_parse_part_hsv_green() {
1256 assert_eq!(
1257 parse_part("hsv(120,100,100)", 0).unwrap(),
1258 vec![TagType::Color {
1259 color: Color::Rgb(0, 255, 0),
1260 ground: Ground::Foreground,
1261 }]
1262 );
1263 }
1264
1265 #[test]
1266 fn test_parse_part_hsv_blue() {
1267 assert_eq!(
1268 parse_part("hsv(240,100,100)", 0).unwrap(),
1269 vec![TagType::Color {
1270 color: Color::Rgb(0, 0, 255),
1271 ground: Ground::Foreground,
1272 }]
1273 );
1274 }
1275
1276 #[test]
1277 fn test_parse_part_hsv_gray() {
1278 assert_eq!(
1279 parse_part("hsv(0,0,50)", 0).unwrap(),
1280 vec![TagType::Color {
1281 color: Color::Rgb(128, 128, 128),
1282 ground: Ground::Foreground,
1283 }]
1284 );
1285 }
1286
1287 #[test]
1288 fn test_parse_part_hsv_bg() {
1289 assert_eq!(
1290 parse_part("bg:hsv(0,100,100)", 0).unwrap(),
1291 vec![TagType::Color {
1292 color: Color::Rgb(255, 0, 0),
1293 ground: Ground::Background,
1294 }]
1295 );
1296 }
1297
1298 #[test]
1299 fn test_parse_part_hsv_wrong_arg_count() {
1300 let result = parse_part("hsv(0,50)", 0);
1301 assert!(result.is_err());
1302 if let Err(crate::errors::LexError::InvalidArgumentCount { expected, got, .. }) = result {
1303 assert_eq!(expected, 3);
1304 assert_eq!(got, 2);
1305 }
1306 }
1307
1308 #[test]
1309 fn test_parse_part_hsv_hue_out_of_range() {
1310 assert!(parse_part("hsv(400,50,50)", 0).is_err());
1311 }
1312
1313 #[test]
1314 fn test_parse_part_hsv_sat_out_of_range() {
1315 assert!(parse_part("hsv(0,150,50)", 0).is_err());
1316 }
1317
1318 #[test]
1319 fn test_parse_part_hsv_val_out_of_range() {
1320 assert!(parse_part("hsv(0,50,110)", 0).is_err());
1321 }
1322
1323 #[test]
1324 fn test_parse_part_hsv_invalid_value() {
1325 assert!(parse_part("hsv(a,b,c)", 0).is_err());
1326 }
1327
1328 #[test]
1329 fn test_parse_part_hsv_unclosed() {
1330 assert!(parse_part("hsv(0,50,50", 0).is_err());
1331 }
1332
1333 #[test]
1334 fn test_parse_part_hsv_with_spaces() {
1335 assert_eq!(
1336 parse_part("hsv( 120 , 100 , 100 )", 0).unwrap(),
1337 vec![TagType::Color {
1338 color: Color::Rgb(0, 255, 0),
1339 ground: Ground::Foreground,
1340 }]
1341 );
1342 }
1343
1344 #[test]
1347 fn test_parse_part_hsb_alias() {
1348 assert_eq!(
1349 parse_part("hsb(0,100,100)", 0).unwrap(),
1350 parse_part("hsv(0,100,100)", 0).unwrap(),
1351 );
1352 }
1353
1354 #[test]
1357 fn test_parse_part_hwb_red() {
1358 assert_eq!(
1359 parse_part("hwb(0,0,0)", 0).unwrap(),
1360 vec![TagType::Color {
1361 color: Color::Rgb(255, 0, 0),
1362 ground: Ground::Foreground,
1363 }]
1364 );
1365 }
1366
1367 #[test]
1368 fn test_parse_part_hwb_white() {
1369 assert_eq!(
1370 parse_part("hwb(0,100,0)", 0).unwrap(),
1371 vec![TagType::Color {
1372 color: Color::Rgb(255, 255, 255),
1373 ground: Ground::Foreground,
1374 }]
1375 );
1376 }
1377
1378 #[test]
1379 fn test_parse_part_hwb_black() {
1380 assert_eq!(
1381 parse_part("hwb(0,0,100)", 0).unwrap(),
1382 vec![TagType::Color {
1383 color: Color::Rgb(0, 0, 0),
1384 ground: Ground::Foreground,
1385 }]
1386 );
1387 }
1388
1389 #[test]
1390 fn test_parse_part_hwb_pink() {
1391 assert_eq!(
1392 parse_part("hwb(0,50,0)", 0).unwrap(),
1393 vec![TagType::Color {
1394 color: Color::Rgb(255, 128, 128),
1395 ground: Ground::Foreground,
1396 }]
1397 );
1398 }
1399
1400 #[test]
1401 fn test_parse_part_hwb_wb_too_high() {
1402 assert!(parse_part("hwb(0,60,60)", 0).is_err());
1403 }
1404
1405 #[test]
1406 fn test_parse_part_hwb_bg() {
1407 assert_eq!(
1408 parse_part("bg:hwb(0,0,0)", 0).unwrap(),
1409 vec![TagType::Color {
1410 color: Color::Rgb(255, 0, 0),
1411 ground: Ground::Background,
1412 }]
1413 );
1414 }
1415
1416 #[test]
1419 fn test_parse_part_lab_black() {
1420 assert_eq!(
1421 parse_part("lab(0,0,0)", 0).unwrap(),
1422 vec![TagType::Color {
1423 color: Color::Rgb(0, 0, 0),
1424 ground: Ground::Foreground,
1425 }]
1426 );
1427 }
1428
1429 #[test]
1430 fn test_parse_part_lab_white() {
1431 assert_eq!(
1432 parse_part("lab(100,0,0)", 0).unwrap(),
1433 vec![TagType::Color {
1434 color: Color::Rgb(255, 255, 255),
1435 ground: Ground::Foreground,
1436 }]
1437 );
1438 }
1439
1440 #[test]
1441 fn test_parse_part_lab_bg() {
1442 assert_eq!(
1443 parse_part("bg:lab(53,80,67)", 0).unwrap(),
1444 vec![TagType::Color {
1445 color: Color::Rgb(254, 0, 0),
1446 ground: Ground::Background,
1447 }]
1448 );
1449 }
1450
1451 #[test]
1452 fn test_parse_part_lab_wrong_arg_count() {
1453 assert!(parse_part("lab(50,20)", 0).is_err());
1454 }
1455
1456 #[test]
1457 fn test_parse_part_lab_l_out_of_range() {
1458 assert!(parse_part("lab(110,0,0)", 0).is_err());
1459 }
1460
1461 #[test]
1462 fn test_parse_part_lab_unclosed() {
1463 assert!(parse_part("lab(50,20,30", 0).is_err());
1464 }
1465
1466 #[test]
1469 fn test_parse_part_lch_black() {
1470 assert_eq!(
1471 parse_part("lch(0,0,0)", 0).unwrap(),
1472 vec![TagType::Color {
1473 color: Color::Rgb(0, 0, 0),
1474 ground: Ground::Foreground,
1475 }]
1476 );
1477 }
1478
1479 #[test]
1480 fn test_parse_part_lch_white() {
1481 assert_eq!(
1482 parse_part("lch(100,0,0)", 0).unwrap(),
1483 vec![TagType::Color {
1484 color: Color::Rgb(255, 255, 255),
1485 ground: Ground::Foreground,
1486 }]
1487 );
1488 }
1489
1490 #[test]
1491 fn test_parse_part_lch_bg() {
1492 assert_eq!(
1493 parse_part("bg:lch(50,0,0)", 0).unwrap(),
1494 vec![TagType::Color {
1495 color: Color::Rgb(119, 119, 119),
1496 ground: Ground::Background,
1497 }]
1498 );
1499 }
1500
1501 #[test]
1502 fn test_parse_part_lch_chroma_out_of_range() {
1503 assert!(parse_part("lch(50,200,0)", 0).is_err());
1504 }
1505
1506 #[test]
1509 fn test_parse_part_oklch_black() {
1510 assert_eq!(
1511 parse_part("oklch(0,0,0)", 0).unwrap(),
1512 vec![TagType::Color {
1513 color: Color::Rgb(0, 0, 0),
1514 ground: Ground::Foreground,
1515 }]
1516 );
1517 }
1518
1519 #[test]
1520 fn test_parse_part_oklch_white() {
1521 assert_eq!(
1522 parse_part("oklch(1,0,0)", 0).unwrap(),
1523 vec![TagType::Color {
1524 color: Color::Rgb(255, 255, 255),
1525 ground: Ground::Foreground,
1526 }]
1527 );
1528 }
1529
1530 #[test]
1531 fn test_parse_part_oklch_l_out_of_range() {
1532 assert!(parse_part("oklch(1.5,0,0)", 0).is_err());
1533 }
1534
1535 #[test]
1536 fn test_parse_part_oklch_chroma_out_of_range() {
1537 assert!(parse_part("oklch(0.5,0.5,0)", 0).is_err());
1538 }
1539
1540 #[test]
1543 fn test_parse_part_unknown_tag_returns_error() {
1544 assert!(parse_part("fuchsia", 0).is_err());
1545 }
1546
1547 #[test]
1550 fn test_tokenize_plain_text() {
1551 let tokens = tokenize("hello world").unwrap();
1552 assert_eq!(tokens, vec![Token::Text("hello world".into())]);
1553 }
1554
1555 #[test]
1556 fn test_tokenize_empty_string() {
1557 assert!(tokenize("").unwrap().is_empty());
1558 }
1559
1560 #[test]
1561 fn test_tokenize_single_color_tag() {
1562 let tokens = tokenize("[red]text").unwrap();
1563 assert_eq!(
1564 tokens,
1565 vec![
1566 Token::Tag(TagType::Color {
1567 color: Color::Named(NamedColor::Red),
1568 ground: Ground::Foreground
1569 }),
1570 Token::Text("text".into()),
1571 ]
1572 );
1573 }
1574
1575 #[test]
1576 fn test_tokenize_bg_color_tag() {
1577 let tokens = tokenize("[bg:red]text").unwrap();
1578 assert_eq!(
1579 tokens,
1580 vec![
1581 Token::Tag(TagType::Color {
1582 color: Color::Named(NamedColor::Red),
1583 ground: Ground::Background
1584 }),
1585 Token::Text("text".into()),
1586 ]
1587 );
1588 }
1589
1590 #[test]
1591 fn test_tokenize_fg_and_bg_in_same_bracket() {
1592 let tokens = tokenize("[fg:white bg:blue]text").unwrap();
1593 assert_eq!(
1594 tokens,
1595 vec![
1596 Token::Tag(TagType::Color {
1597 color: Color::Named(NamedColor::White),
1598 ground: Ground::Foreground
1599 }),
1600 Token::Tag(TagType::Color {
1601 color: Color::Named(NamedColor::Blue),
1602 ground: Ground::Background
1603 }),
1604 Token::Text("text".into()),
1605 ]
1606 );
1607 }
1608
1609 #[test]
1610 fn test_tokenize_reset_tag() {
1611 assert_eq!(
1612 tokenize("[/]").unwrap(),
1613 vec![Token::Tag(TagType::ResetAll)]
1614 );
1615 }
1616
1617 #[test]
1618 fn test_tokenize_compound_tag() {
1619 let tokens = tokenize("[bold red]hi").unwrap();
1620 assert_eq!(
1621 tokens,
1622 vec![
1623 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
1624 Token::Tag(TagType::Color {
1625 color: Color::Named(NamedColor::Red),
1626 ground: Ground::Foreground
1627 }),
1628 Token::Text("hi".into()),
1629 ]
1630 );
1631 }
1632
1633 #[test]
1634 fn test_tokenize_double_bracket_escape() {
1635 let tokens = tokenize("[[not a tag]").unwrap();
1636 assert_eq!(
1637 tokens,
1638 vec![
1639 Token::Text("[".into()),
1640 Token::Text("not a tag".into()),
1641 Token::Text("]".into()),
1642 ]
1643 );
1644 }
1645
1646 #[test]
1647 fn test_tokenize_double_bracket_escape_with_prefix() {
1648 let tokens = tokenize("before[[not a tag]").unwrap();
1649 assert_eq!(
1650 tokens,
1651 vec![
1652 Token::Text("before".into()),
1653 Token::Text("[".into()),
1654 Token::Text("not a tag".into()),
1655 Token::Text("]".into()),
1656 ]
1657 );
1658 }
1659
1660 #[test]
1661 fn test_tokenize_double_bracket_symmetric() {
1662 let tokens = tokenize("[[thing]]").unwrap();
1663 assert_eq!(
1664 tokens,
1665 vec![
1666 Token::Text("[".into()),
1667 Token::Text("thing".into()),
1668 Token::Text("]".into()),
1669 ]
1670 );
1671 }
1672
1673 #[test]
1674 fn test_tokenize_bare_close_bracket_is_text() {
1675 let tokens = tokenize("hello]world").unwrap();
1676 assert_eq!(
1677 tokens,
1678 vec![
1679 Token::Text("hello".into()),
1680 Token::Text("]".into()),
1681 Token::Text("world".into()),
1682 ]
1683 );
1684 }
1685
1686 #[test]
1687 fn test_tokenize_double_close_bracket_emits_one() {
1688 let tokens = tokenize("]]").unwrap();
1689 assert_eq!(tokens, vec![Token::Text("]".into())]);
1690 }
1691
1692 #[test]
1693 fn test_tokenize_triple_close_bracket_emits_two() {
1694 let tokens = tokenize("]]]").unwrap();
1695 assert_eq!(
1696 tokens,
1697 vec![Token::Text("]".into()), Token::Text("]".into())]
1698 );
1699 }
1700
1701 #[test]
1702 fn test_tokenize_unclosed_tag_returns_error() {
1703 assert!(tokenize("[red").is_err());
1704 }
1705
1706 #[test]
1707 fn test_tokenize_invalid_tag_name_returns_error() {
1708 assert!(tokenize("[fuchsia]").is_err());
1709 }
1710
1711 #[test]
1712 fn test_tokenize_text_before_and_after_tag() {
1713 let tokens = tokenize("before[red]after").unwrap();
1714 assert_eq!(
1715 tokens,
1716 vec![
1717 Token::Text("before".into()),
1718 Token::Tag(TagType::Color {
1719 color: Color::Named(NamedColor::Red),
1720 ground: Ground::Foreground
1721 }),
1722 Token::Text("after".into()),
1723 ]
1724 );
1725 }
1726
1727 #[test]
1728 fn test_tokenize_ansi256_tag() {
1729 let tokens = tokenize("[ansi(1)]text").unwrap();
1730 assert_eq!(
1731 tokens[0],
1732 Token::Tag(TagType::Color {
1733 color: Color::Ansi256(1),
1734 ground: Ground::Foreground,
1735 })
1736 );
1737 }
1738
1739 #[test]
1740 fn test_split_tag_parts_simple() {
1741 assert_eq!(
1742 split_tag_parts("bold red"),
1743 vec![(0usize, "bold"), (5, "red")]
1744 );
1745 }
1746
1747 #[test]
1748 fn test_split_tag_parts_respects_parens() {
1749 assert_eq!(
1750 split_tag_parts("rgb(1, 2, 3)"),
1751 vec![(0usize, "rgb(1, 2, 3)")]
1752 );
1753 }
1754
1755 #[test]
1756 fn test_split_tag_parts_mixed() {
1757 assert_eq!(
1758 split_tag_parts("bold rgb(255, 128, 0)"),
1759 vec![(0usize, "bold"), (5, "rgb(255, 128, 0)")]
1760 );
1761 }
1762
1763 #[test]
1764 fn test_split_tag_parts_ansi_with_spaces() {
1765 assert_eq!(
1766 split_tag_parts("fg:ansi( 93 )"),
1767 vec![(0usize, "fg:ansi( 93 )")]
1768 );
1769 }
1770
1771 #[test]
1772 fn test_tokenize_rgb_with_spaces_inside_parens() {
1773 let tokens = tokenize("[rgb(1, 2, 3)]text").unwrap();
1774 assert_eq!(
1775 tokens[0],
1776 Token::Tag(TagType::Color {
1777 color: Color::Rgb(1, 2, 3),
1778 ground: Ground::Foreground,
1779 })
1780 );
1781 }
1782
1783 #[test]
1784 fn test_tokenize_mixed_bold_rgb_with_spaces() {
1785 let tokens = tokenize("[bold rgb(255, 128, 0)]text").unwrap();
1786 assert_eq!(
1787 tokens,
1788 vec![
1789 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
1790 Token::Tag(TagType::Color {
1791 color: Color::Rgb(255, 128, 0),
1792 ground: Ground::Foreground,
1793 }),
1794 Token::Text("text".into()),
1795 ]
1796 );
1797 }
1798
1799 #[test]
1800 fn test_tokenize_rgb_tag() {
1801 let tokens = tokenize("[rgb(255,0,128)]text").unwrap();
1802 assert_eq!(
1803 tokens[0],
1804 Token::Tag(TagType::Color {
1805 color: Color::Rgb(255, 0, 128),
1806 ground: Ground::Foreground,
1807 })
1808 );
1809 }
1810
1811 #[test]
1812 fn test_tokenize_bg_rgb_tag() {
1813 let tokens = tokenize("[bg:rgb(0,255,0)]text").unwrap();
1814 assert_eq!(
1815 tokens[0],
1816 Token::Tag(TagType::Color {
1817 color: Color::Rgb(0, 255, 0),
1818 ground: Ground::Background,
1819 })
1820 );
1821 }
1822
1823 #[test]
1826 fn test_tokenize_hex_tag() {
1827 let tokens = tokenize("[#ff0000]text").unwrap();
1828 assert_eq!(
1829 tokens[0],
1830 Token::Tag(TagType::Color {
1831 color: Color::Rgb(255, 0, 0),
1832 ground: Ground::Foreground,
1833 })
1834 );
1835 }
1836
1837 #[test]
1838 fn test_tokenize_hex_3digit_tag() {
1839 let tokens = tokenize("[#f00]text").unwrap();
1840 assert_eq!(
1841 tokens[0],
1842 Token::Tag(TagType::Color {
1843 color: Color::Rgb(255, 0, 0),
1844 ground: Ground::Foreground,
1845 })
1846 );
1847 }
1848
1849 #[test]
1850 fn test_tokenize_bg_hex_tag() {
1851 let tokens = tokenize("[bg:#fff]text").unwrap();
1852 assert_eq!(
1853 tokens[0],
1854 Token::Tag(TagType::Color {
1855 color: Color::Rgb(255, 255, 255),
1856 ground: Ground::Background,
1857 })
1858 );
1859 }
1860
1861 #[test]
1864 fn test_tokenize_hsl_tag() {
1865 let tokens = tokenize("[hsl(0,100,50)]text").unwrap();
1866 assert_eq!(
1867 tokens[0],
1868 Token::Tag(TagType::Color {
1869 color: Color::Rgb(255, 0, 0),
1870 ground: Ground::Foreground,
1871 })
1872 );
1873 }
1874
1875 #[test]
1876 fn test_tokenize_bg_hsl_tag() {
1877 let tokens = tokenize("[bg:hsl(0,100,50)]text").unwrap();
1878 assert_eq!(
1879 tokens[0],
1880 Token::Tag(TagType::Color {
1881 color: Color::Rgb(255, 0, 0),
1882 ground: Ground::Background,
1883 })
1884 );
1885 }
1886
1887 #[test]
1888 fn test_tokenize_hsl_with_spaces() {
1889 let tokens = tokenize("[hsl( 120 , 100 , 50 )]text").unwrap();
1890 assert_eq!(
1891 tokens[0],
1892 Token::Tag(TagType::Color {
1893 color: Color::Rgb(0, 255, 0),
1894 ground: Ground::Foreground,
1895 })
1896 );
1897 }
1898
1899 #[test]
1900 fn test_tokenize_mixed_bold_hsl() {
1901 let tokens = tokenize("[bold hsl(0,100,50)]text").unwrap();
1902 assert_eq!(
1903 tokens,
1904 vec![
1905 Token::Tag(TagType::Emphasis(EmphasisType::Bold)),
1906 Token::Tag(TagType::Color {
1907 color: Color::Rgb(255, 0, 0),
1908 ground: Ground::Foreground,
1909 }),
1910 Token::Text("text".into()),
1911 ]
1912 );
1913 }
1914
1915 #[test]
1918 fn test_tokenize_hsv_tag() {
1919 let tokens = tokenize("[hsv(0,100,100)]text").unwrap();
1920 assert_eq!(
1921 tokens[0],
1922 Token::Tag(TagType::Color {
1923 color: Color::Rgb(255, 0, 0),
1924 ground: Ground::Foreground,
1925 })
1926 );
1927 }
1928
1929 #[test]
1930 fn test_tokenize_bg_hsv_tag() {
1931 let tokens = tokenize("[bg:hsv(120,100,100)]text").unwrap();
1932 assert_eq!(
1933 tokens[0],
1934 Token::Tag(TagType::Color {
1935 color: Color::Rgb(0, 255, 0),
1936 ground: Ground::Background,
1937 })
1938 );
1939 }
1940
1941 #[test]
1944 fn test_tokenize_hsb_tag() {
1945 let tokens = tokenize("[hsb(0,100,100)]text").unwrap();
1946 assert_eq!(
1947 tokens[0],
1948 Token::Tag(TagType::Color {
1949 color: Color::Rgb(255, 0, 0),
1950 ground: Ground::Foreground,
1951 })
1952 );
1953 }
1954
1955 #[test]
1958 fn test_tokenize_hwb_tag() {
1959 let tokens = tokenize("[hwb(0,0,0)]text").unwrap();
1960 assert_eq!(
1961 tokens[0],
1962 Token::Tag(TagType::Color {
1963 color: Color::Rgb(255, 0, 0),
1964 ground: Ground::Foreground,
1965 })
1966 );
1967 }
1968
1969 #[test]
1970 fn test_tokenize_bg_hwb_tag() {
1971 let tokens = tokenize("[bg:hwb(0,0,0)]text").unwrap();
1972 assert_eq!(
1973 tokens[0],
1974 Token::Tag(TagType::Color {
1975 color: Color::Rgb(255, 0, 0),
1976 ground: Ground::Background,
1977 })
1978 );
1979 }
1980
1981 #[test]
1984 fn test_tokenize_lab_tag() {
1985 let tokens = tokenize("[lab(0,0,0)]text").unwrap();
1986 assert_eq!(
1987 tokens[0],
1988 Token::Tag(TagType::Color {
1989 color: Color::Rgb(0, 0, 0),
1990 ground: Ground::Foreground,
1991 })
1992 );
1993 }
1994
1995 #[test]
1996 fn test_tokenize_bg_lab_tag() {
1997 let tokens = tokenize("[bg:lab(100,0,0)]text").unwrap();
1998 assert_eq!(
1999 tokens[0],
2000 Token::Tag(TagType::Color {
2001 color: Color::Rgb(255, 255, 255),
2002 ground: Ground::Background,
2003 })
2004 );
2005 }
2006
2007 #[test]
2010 fn test_tokenize_lch_tag() {
2011 let tokens = tokenize("[lch(0,0,0)]text").unwrap();
2012 assert_eq!(
2013 tokens[0],
2014 Token::Tag(TagType::Color {
2015 color: Color::Rgb(0, 0, 0),
2016 ground: Ground::Foreground,
2017 })
2018 );
2019 }
2020
2021 #[test]
2024 fn test_tokenize_oklch_tag() {
2025 let tokens = tokenize("[oklch(0,0,0)]text").unwrap();
2026 assert_eq!(
2027 tokens[0],
2028 Token::Tag(TagType::Color {
2029 color: Color::Rgb(0, 0, 0),
2030 ground: Ground::Foreground,
2031 })
2032 );
2033 }
2034
2035 #[test]
2036 fn test_tokenize_bg_oklch_tag() {
2037 let tokens = tokenize("[bg:oklch(1,0,0)]text").unwrap();
2038 assert_eq!(
2039 tokens[0],
2040 Token::Tag(TagType::Color {
2041 color: Color::Rgb(255, 255, 255),
2042 ground: Ground::Background,
2043 })
2044 );
2045 }
2046
2047 #[test]
2048 fn test_parse_part_custom_style_from_registry() {
2049 crate::registry::insert_style("danger", crate::ansi::Style::parse("[bold red]").unwrap())
2050 .unwrap();
2051 let result = parse_part("danger", 0).unwrap();
2052 assert_eq!(
2053 result,
2054 vec![
2055 TagType::Emphasis(EmphasisType::Bold),
2056 TagType::Color {
2057 color: Color::Named(NamedColor::Red),
2058 ground: Ground::Foreground
2059 },
2060 ]
2061 );
2062 }
2063}