Files
Marcos Paulo 15d7f5e873 feat: editor blocks and lint, vault knowledge, calendar plugin, macOS menu
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity
- Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys
- Visual view: tables, note embeds, %% comments, wrapping property chips
- Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin
- Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 12:36:00 -03:00

99 lines
5.0 KiB
Rust

//! The iCalendar reader: real-looking feeds and hostile ones.
#![allow(clippy::unwrap_used)]
use notescraft_calendar::{MAX_ICS_BYTES, Priority, Status, parse_ics};
const FEED: &str = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//y//EN\r\n\
BEGIN:VTIMEZONE\r\nTZID:Europe/Lisbon\r\nBEGIN:STANDARD\r\nDTSTART:19701025T020000\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\n\
BEGIN:VEVENT\r\nUID:a@x\r\nDTSTART;TZID=Europe/Lisbon:20261019T093000\r\nDTEND;TZID=Europe/Lisbon:20261019T094500\r\n\
SUMMARY:Stand\\, up\\; daily\r\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE;UNTIL=20261231T000000Z\r\nEXDATE;TZID=Europe/Lisbon:20261021T093000\r\n\
CATEGORIES:team,Q4\r\nPRIORITY:1\r\nSTATUS:TENTATIVE\r\nURL:https://example.com/call\r\n\
BEGIN:VALARM\r\nACTION:DISPLAY\r\nTRIGGER:-PT15M\r\nEND:VALARM\r\nBEGIN:VALARM\r\nTRIGGER:-P1DT2H\r\nEND:VALARM\r\nEND:VEVENT\r\n\
BEGIN:VEVENT\r\nUID:b@x\r\nDTSTART;VALUE=DATE:20261224\r\nDTEND;VALUE=DATE:20261227\r\nSUMMARY:Holiday\r\n\
DESCRIPTION:folded\r\n text\r\nEND:VEVENT\r\n\
BEGIN:VEVENT\r\nUID:c@x\r\nDTSTART:20261001T080000Z\r\nSUMMARY:Monthly\r\nRRULE:FREQ=MONTHLY;BYMONTHDAY=-1\r\nEND:VEVENT\r\n\
BEGIN:VEVENT\r\nUID:d@x\r\nDTSTART;VALUE=DATE:20261010\r\nDTEND;VALUE=DATE:20261011\r\nSUMMARY:One day\r\nEND:VEVENT\r\n\
BEGIN:VEVENT\r\nUID:bad@x\r\nDTSTART:2026-10-10\r\nSUMMARY:Broken\r\nEND:VEVENT\r\n\
BEGIN:VEVENT\r\nUID:none@x\r\nSUMMARY:No start\r\nEND:VEVENT\r\n\
END:VCALENDAR\r\n";
#[test]
fn a_feed_imports() {
let import = parse_ics(FEED).unwrap();
assert_eq!(import.events.len(), 4);
assert_eq!(import.skipped.len(), 2, "{:?}", import.skipped);
assert!(import.skipped.iter().all(|s| s.contains("bad@x") || s.contains("none@x")));
let a = &import.events[0];
assert_eq!(a.uid.as_deref(), Some("a@x"));
let e = &a.event;
assert_eq!(e.title, "Stand, up; daily");
assert_eq!((e.start.to_string().as_str(), e.end.unwrap().to_string().as_str()), ("2026-10-19T09:30", "2026-10-19T09:45"));
assert_eq!(e.tz.as_deref(), Some("Europe/Lisbon"));
assert_eq!(e.repeat.as_deref(), Some("FREQ=WEEKLY;BYDAY=MO,WE;UNTIL=2026-12-31"));
assert_eq!(e.exceptions[0].to_string(), "2026-10-21T09:30");
assert_eq!((e.tags.as_slice(), e.priority, e.status), (["team".to_owned(), "Q4".to_owned()].as_slice(), Some(Priority::High), Some(Status::Tentative)));
assert_eq!(e.reminders.iter().map(|r| r.minutes_before).collect::<Vec<_>>(), [15, 1560]);
assert_eq!(e.link.as_deref(), Some("https://example.com/call"));
assert!(a.unsupported_rule.is_none());
let holiday = &import.events[1].event;
assert_eq!((holiday.start.to_string().as_str(), holiday.end.unwrap().to_string().as_str()), ("2026-12-24", "2026-12-26"));
let monthly = &import.events[2];
assert_eq!(monthly.unsupported_rule.as_deref(), Some("FREQ=MONTHLY;BYMONTHDAY=-1"));
assert!(monthly.event.repeat.is_none());
assert_eq!(monthly.event.tz.as_deref(), Some("UTC"));
assert!(import.events[3].event.end.is_none(), "a one-day all-day event has no end");
}
#[test]
fn lf_only_and_folded_lines_work() {
let text = "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20261001T0900\n 00\nSUMMARY:Long\n title\nEND:VEVENT\nEND:VCALENDAR";
let import = parse_ics(text).unwrap();
assert_eq!(import.events[0].event.title, "Long title");
assert_eq!(import.events[0].event.start.to_string(), "2026-10-01T09:00");
}
#[test]
fn whole_file_problems_are_errors() {
for text in ["", "hello", "BEGIN:VEVENT\nEND:VEVENT", " \n"] {
assert!(parse_ics(text).is_err(), "{text:?}");
}
assert!(parse_ics(&format!("BEGIN:VCALENDAR\nX-BIG:{}\nEND:VCALENDAR", "a".repeat(MAX_ICS_BYTES))).is_err(), "too large");
assert!(parse_ics(&format!("BEGIN:VCALENDAR\nX-LONG:{}\nEND:VCALENDAR", "a".repeat(9000))).is_err(), "line too long");
let deep = format!("BEGIN:VCALENDAR\n{}{}END:VCALENDAR", "BEGIN:X\n".repeat(20), "END:X\n".repeat(20));
assert!(parse_ics(&deep).is_err(), "too deep");
let many = format!("BEGIN:VCALENDAR\n{}END:VCALENDAR", "BEGIN:VEVENT\nDTSTART:20261001\nEND:VEVENT\n".repeat(10_001));
assert!(parse_ics(&many).is_err(), "too many events");
}
#[test]
fn hostile_properties_never_panic() {
let junk = [
"DTSTART:",
"DTSTART:ABC",
"DTSTART:99999999",
"DTSTART:20261301",
"DTSTART:20261001T256000",
"DTSTART;VALUE=DATE:20261001T090000",
"DTSTART:20261001\nDTEND:19990101",
"DTSTART:20261001\nRRULE:FREQ=DAILY;COUNT=99999999999999",
"DTSTART:20261001\nRRULE:;;;=",
"DTSTART:20261001\nRRULE:FREQ=WEEKLY;BYDAY=1MO",
"DTSTART:20261001\nEXDATE:zzz",
"DTSTART:20261001\nBEGIN:VALARM\nTRIGGER:-P99999999999D\nEND:VALARM",
"DTSTART:20261001\nBEGIN:VALARM\nTRIGGER:-PT\nEND:VALARM",
"DTSTART:20261001\nSUMMARY:\\",
"DTSTART:20261001\nPRIORITY:999",
":",
";;;:::",
"\u{0}\u{1}",
];
for body in junk {
let text = format!("BEGIN:VCALENDAR\nBEGIN:VEVENT\n{body}\nEND:VEVENT\nEND:VCALENDAR");
let _ = parse_ics(&text);
}
}