Files
Marcos Paulo 22b43c1254
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: task-line parser, Templater segmenter, icon/banner property resolver (MiniMax M3.1 Flash)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 07:03:19 -03:00

138 lines
6.1 KiB
Rust

//! Icon, colour and banner properties of a note.
use notescraft_index::{BannerTarget, ColorRef, DecorKeys, IconRef, Index, decor_from_props, parse_color, parse_icon};
use notescraft_markdown::Value;
fn props(pairs: &[(&str, Value)]) -> Vec<(String, Value)> {
pairs.iter().map(|(key, value)| ((*key).to_string(), value.clone())).collect()
}
fn text(value: &str) -> Value {
Value::String(value.to_string())
}
#[test]
fn the_lucide_spellings_name_the_same_icon() {
for raw in ["LiRocket", "lucide-rocket", "Rocket", "rocket"] {
assert_eq!(parse_icon(raw), Some(IconRef::Lucide("rocket".to_string())), "{raw}");
}
assert_eq!(parse_icon("LiArrowRight"), Some(IconRef::Lucide("arrow-right".to_string())));
assert_eq!(parse_icon("List"), Some(IconRef::Lucide("list".to_string())));
assert_eq!(parse_icon("rocket-launch"), Some(IconRef::Lucide("rocket-launch".to_string())));
}
#[test]
fn a_value_that_names_no_icon_is_ignored() {
assert_eq!(parse_icon(""), None);
assert_eq!(parse_icon(" "), None);
assert_eq!(parse_icon("rocket launch"), None);
assert_eq!(parse_icon("#ff8800"), None);
assert_eq!(parse_icon(&"a".repeat(65)), None);
assert!(parse_icon(&"a".repeat(64)).is_some());
}
#[test]
fn one_emoji_is_an_icon() {
assert_eq!(parse_icon("🚀"), Some(IconRef::Emoji("🚀".to_string())));
assert_eq!(parse_icon("👩‍💻"), Some(IconRef::Emoji("👩‍💻".to_string())));
assert_eq!(parse_icon("❤️"), Some(IconRef::Emoji("❤️".to_string())));
assert_eq!(parse_icon("👩‍💻🚀"), None);
assert_eq!(parse_icon("🚀 rocket"), None);
}
#[test]
fn a_link_names_a_file_of_the_vault() {
assert_eq!(parse_icon("[[icons/star.svg]]"), Some(IconRef::File("icons/star.svg".to_string())));
assert_eq!(parse_icon("![[x.jpg]]"), Some(IconRef::File("x.jpg".to_string())));
assert_eq!(parse_icon("[[x.jpg|alias]]"), Some(IconRef::File("x.jpg".to_string())));
assert_eq!(parse_icon("icons/star.svg"), Some(IconRef::File("icons/star.svg".to_string())));
assert_eq!(parse_icon("[[]]"), None);
}
#[test]
fn a_colour_is_hex_or_a_token() {
assert_eq!(parse_color("#f80"), Some(ColorRef::Rgb(255, 136, 0)));
assert_eq!(parse_color("#FF8800"), Some(ColorRef::Rgb(255, 136, 0)));
assert_eq!(parse_color("accent"), Some(ColorRef::Token("accent".to_string())));
assert_eq!(parse_color("danger"), Some(ColorRef::Token("danger".to_string())));
assert_eq!(parse_color("#12"), None);
assert_eq!(parse_color("#ff88"), None);
assert_eq!(parse_color("rgb(1,2,3)"), None);
assert_eq!(parse_color("Accent"), None);
assert_eq!(parse_color(""), None);
}
#[test]
fn a_banner_is_a_file_or_a_url() {
let keys = DecorKeys::default();
let decor = decor_from_props(&props(&[("banner", text("[[cover.jpg]]"))]), &keys);
let banner = decor.banner.expect("banner");
assert_eq!(banner.target, BannerTarget::File("cover.jpg".to_string()));
assert!((banner.x - 0.5).abs() < f32::EPSILON);
assert!((banner.y - 0.5).abs() < f32::EPSILON);
let decor = decor_from_props(&props(&[("banner", text("https://example.com/a.png"))]), &keys);
assert_eq!(decor.banner.expect("banner").target, BannerTarget::Url("https://example.com/a.png".to_string()));
}
#[test]
fn the_focus_of_the_banner_is_a_number_in_range() {
let keys = DecorKeys::default();
let focus = |pairs: &[(&str, Value)]| decor_from_props(&props(pairs), &keys).banner.expect("banner").y;
let banner = [("banner", text("[[cover.jpg]]"))];
let with = |key: &str, value: Value| {
let mut pairs = banner.to_vec();
pairs.push((key, value));
focus(&pairs)
};
assert!((with("banner_y", Value::Number(0.2)) - 0.2).abs() < 1e-6);
assert!((with("banner_y", text("0.2")) - 0.2).abs() < 1e-6);
assert!((with("banner_y", Value::Number(-1.0)) - 0.5).abs() < f32::EPSILON);
assert!((with("banner_y", Value::Number(1.5)) - 0.5).abs() < f32::EPSILON);
assert!((with("banner_y", text("NaN")) - 0.5).abs() < f32::EPSILON);
assert!((with("banner_y", text("top")) - 0.5).abs() < f32::EPSILON);
assert!((with("banner_x", Value::Number(0.25)) - 0.5).abs() < f32::EPSILON);
}
#[test]
fn a_long_value_is_ignored() {
let keys = DecorKeys::default();
let long = "a".repeat(2048);
let decor = decor_from_props(&props(&[("icon", text(&long)), ("banner", text(&long))]), &keys);
assert_eq!(decor.icon, None);
assert_eq!(decor.banner, None);
}
#[test]
fn the_banner_emoji_is_the_icon_when_the_note_has_none() {
let keys = DecorKeys::default();
let decor = decor_from_props(&props(&[("banner_icon", text("🚀"))]), &keys);
assert_eq!(decor.icon, Some(IconRef::Emoji("🚀".to_string())));
let decor = decor_from_props(&props(&[("icon", text("LiRocket")), ("banner_icon", text("🚀"))]), &keys);
assert_eq!(decor.icon, Some(IconRef::Lucide("rocket".to_string())));
}
#[test]
fn the_keys_are_configurable() {
let keys = DecorKeys { icon: "cover", color: "coverColor", banner: "coverImage" };
let decor = decor_from_props(&props(&[("cover", text("star")), ("coverColor", text("#ff8800")), ("coverImage", text("[[a.png]]"))]), &keys);
assert_eq!(decor.icon, Some(IconRef::Lucide("star".to_string())));
assert_eq!(decor.color, Some(ColorRef::Rgb(255, 136, 0)));
assert_eq!(decor.banner.expect("banner").target, BannerTarget::File("a.png".to_string()));
let decor = decor_from_props(&props(&[("icon", text("star"))]), &keys);
assert_eq!(decor.icon, None);
}
#[test]
fn a_note_is_decorated_from_its_frontmatter() {
let mut index = Index::new();
index.update_note("a.md", "---\nicon: LiRocket\niconColor: \"#ff8800\"\nbanner: \"[[cover.jpg]]\"\nbanner_y: 0.2\n---\nbody\n").unwrap();
let note_props = index.note_props("a.md").unwrap();
let decor = decor_from_props(note_props, &DecorKeys::default());
assert_eq!(decor.icon, Some(IconRef::Lucide("rocket".to_string())));
assert_eq!(decor.color, Some(ColorRef::Rgb(255, 136, 0)));
let banner = decor.banner.expect("banner");
assert_eq!(banner.target, BannerTarget::File("cover.jpg".to_string()));
assert!((banner.y - 0.2).abs() < 1e-6);
}