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

103 lines
2.9 KiB
Rust

//! Hostile input must not panic or return unbounded output.
use notescraft_lint::{Context, MAX_ISSUES, check, fix, style::Vote};
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
self.0 >> 33
}
}
fn exercise(text: &str, ctx: &Context) {
let issues = check(text, ctx);
assert!(issues.len() <= MAX_ISSUES);
let _ = fix::apply_all_safe(text, &issues);
for i in &issues {
for f in i.fix.iter().chain(&i.extra_fixes) {
let _ = fix::apply(text, f);
}
}
let mut vote = Vote::default();
vote.add_note(text);
vote.remove_note(text);
}
#[test]
fn pseudo_random_bytes_never_panic() {
let ctx = Context { titles: vec![("a.md".into(), "a".into())], ..Context::default() };
let alphabet: Vec<&str> = vec![
"#",
"##",
"-",
"*",
"+",
" ",
" ",
"\n",
"\r\n",
"`",
"```",
"~~~",
"[[",
"]]",
"|",
"---",
"é",
"漢",
"\u{1f600}",
"a",
"1.",
"2026-10-17",
":",
"\t",
">",
"!",
];
let mut rng = Lcg(42);
for _ in 0..400 {
let len = usize::try_from(rng.next() % 200).unwrap_or(0);
let mut text = String::new();
for _ in 0..len {
text.push_str(alphabet[usize::try_from(rng.next()).unwrap_or(0) % alphabet.len()]);
}
exercise(&text, &ctx);
}
for _ in 0..200 {
let bytes: Vec<u8> = (0..300).map(|_| u8::try_from(rng.next() & 0xff).unwrap_or(0)).collect();
exercise(&String::from_utf8_lossy(&bytes), &ctx);
}
}
#[test]
fn many_headings_and_unclosed_fences() {
let many = "#\n###\n".repeat(50_000);
exercise(&many, &Context::default());
exercise(&"```\n- a\n* b\n".repeat(10), &Context::default());
exercise(&"---\n".repeat(1000), &Context::default());
exercise(&format!("{}\n", "[[x]] `".repeat(50_000)), &Context { titles: vec![("a.md".into(), "a".into())], ..Context::default() });
exercise(&" ".repeat(3_000_000), &Context::default());
exercise("", &Context::default());
}
#[test]
fn issues_are_capped() {
let text = "a \n".repeat(10_000);
assert_eq!(check(&text, &Context::default()).len(), MAX_ISSUES);
}
#[test]
#[ignore = "timing"]
fn hundred_kb_note_checks_under_ten_ms() {
let para = "# Heading\n\nSome text with a [[link]] and `code`.\n\n- item one\n- item two\n\n";
let text = para.repeat(100_000 / para.len());
let ctx = Context { titles: vec![("link.md".into(), "link".into())], ..Context::default() };
let _ = check(&text, &ctx);
let start = std::time::Instant::now();
let _ = check(&text, &ctx);
let took = start.elapsed();
assert!(took.as_millis() < 10, "took {took:?}");
}