15d7f5e873
- 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>
130 lines
4.5 KiB
Rust
130 lines
4.5 KiB
Rust
//! One violating and one clean note per rule.
|
|
|
|
use notescraft_lint::{Context, Issue, check, rules};
|
|
|
|
fn ids(text: &str) -> Vec<String> {
|
|
check(text, &Context::default()).into_iter().map(|i| i.rule).collect()
|
|
}
|
|
|
|
fn with_titles(text: &str) -> Vec<Issue> {
|
|
let ctx = Context {
|
|
titles: vec![("notes/Release notes.md".into(), "Release notes".into()), ("Home.md".into(), "Home".into())],
|
|
current_path: "notes/a.md".into(),
|
|
..Context::default()
|
|
};
|
|
check(text, &ctx)
|
|
}
|
|
|
|
#[test]
|
|
fn clean_note_reports_nothing() {
|
|
let note = "---\ntitle: A\ntags: [x]\n---\n\n# Title\n\nText with `code span` here.\n\n## Part\n\n- one\n- two\n\n```rust\nlet x = 1; \n### not a heading\n```\n\nEnd.\n";
|
|
assert_eq!(ids(note), Vec::<String>::new());
|
|
none(&with_titles("See [[Home]] and [[Release notes#Top|rel]] and `[[nope]]`.\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn heading_increment() {
|
|
let issues = check("# A\n\n### B\n", &Context::default());
|
|
assert_eq!(issues.len(), 1);
|
|
assert_eq!(issues[0].rule, "heading-increment");
|
|
assert_eq!(issues[0].line, 3);
|
|
let fix = issues[0].fix.as_ref().expect("fix");
|
|
assert_eq!(fix.label, "Make it ##");
|
|
assert!(fix.safe);
|
|
none(&ids("# A\n\n## B\n\n### C\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn list_markers() {
|
|
assert_eq!(ids("- a\n* b\n+ c\n"), ["list-marker", "list-marker"]);
|
|
none(&ids("- a\n- b\n\n* * *\n"));
|
|
let mut ctx = Context::default();
|
|
ctx.style.list_marker = Some('*');
|
|
assert_eq!(check("- a\n- b\n", &ctx).len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn spacing() {
|
|
assert_eq!(ids("a b\n"), ["double-space"]);
|
|
assert_eq!(ids("a \n"), ["trailing-space"]);
|
|
assert_eq!(ids("a\t\n"), ["trailing-space"]);
|
|
none(&ids("hard break \nnext\n"));
|
|
none(&ids(" indented\n| a | b |\n`a b`\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn blank_lines() {
|
|
assert_eq!(ids("text\n# H\nmore\n"), ["blank-around-headings", "blank-around-headings"]);
|
|
assert_eq!(ids("text\n```\ncode\n```\nmore\n"), ["blank-around-fences", "blank-around-fences"]);
|
|
assert_eq!(ids("text\n- a\n- b\nafter\n"), ["blank-around-lists", "blank-around-lists"]);
|
|
none(&ids("text\n\n# H\n\n```\ncode\n```\n\n- a\n - b\n\nafter\n"));
|
|
none(&ids("---\na: 1\n---\n# H\n\ntext\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn frontmatter() {
|
|
let issues = check("---\ntitle: A\ntags: x\n\n# Body\n", &Context::default());
|
|
assert_eq!(issues.len(), 1);
|
|
assert_eq!(issues[0].rule, "frontmatter-unclosed");
|
|
assert!(issues[0].fix.as_ref().is_some_and(|f| f.safe));
|
|
assert_eq!(ids("---\ntitle: A\ntitle: B\n---\n"), ["frontmatter-duplicate-key"]);
|
|
none(&ids("---\n\ntext\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_headings() {
|
|
assert_eq!(ids("# A\n\n## Same\n\n## same\n"), ["duplicate-heading"]);
|
|
none(&ids("# A\n\n## One\n\n## Two\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn unresolved_links() {
|
|
let issues = with_titles("See [[Releas notes]] and [[Home|h]] and [[Nope#x]] and ![[pic.png]].\n");
|
|
assert_eq!(issues.len(), 2);
|
|
let first = &issues[0];
|
|
assert_eq!(first.rule, "unresolved-link");
|
|
let fix = first.fix.as_ref().expect("replacement");
|
|
assert_eq!(fix.label, "Replace with Release notes");
|
|
assert!(!fix.safe);
|
|
assert_eq!(first.extra_fixes[0].label, "Create note");
|
|
assert!(issues[1].fix.is_none());
|
|
none(&with_titles("```\n[[nope]]\n```\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn rule_switches_and_spelling() {
|
|
let mut ctx = Context::default();
|
|
ctx.rules.enabled.insert("double-space".into(), false);
|
|
none(&check("a b\n", &ctx));
|
|
ctx.rules.enabled.insert("trailing-space".into(), true);
|
|
ctx.rules.severity.insert("trailing-space".into(), notescraft_lint::Severity::Error);
|
|
assert_eq!(check("a \n", &ctx)[0].severity, notescraft_lint::Severity::Error);
|
|
ctx.rules.enabled.insert("spelling".into(), true);
|
|
none(&check("teh\n", &ctx));
|
|
assert!(rules().iter().any(|r| r.id == "spelling"));
|
|
}
|
|
|
|
#[test]
|
|
fn dismissed_issues_do_not_return() {
|
|
let mut ctx = Context::default();
|
|
let first = check("a b\nc d\n", &ctx);
|
|
assert_eq!(first.len(), 2);
|
|
ctx.dismissed.push(first[0].key());
|
|
let again = check("a b\nc d\n", &ctx);
|
|
assert_eq!(again.len(), 1);
|
|
assert_eq!(again[0].line, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn sorted_and_serializable() {
|
|
let issues = check("text\n# H\n\n### X \n", &Context::default());
|
|
assert!(issues.windows(2).all(|w| w[0].range.start <= w[1].range.start));
|
|
let json = serde_json::to_string(&issues).expect("json");
|
|
let back: Vec<Issue> = serde_json::from_str(&json).expect("back");
|
|
assert_eq!(back, issues);
|
|
}
|
|
|
|
fn none<T: std::fmt::Debug>(v: &[T]) {
|
|
assert_eq!(format!("{v:?}"), "[]");
|
|
}
|