use tramlines_md::{
Finding, Locator, RepoPath,
check::{CheckOptions, NullHost, check_against, check_schema},
derive::derive,
document::Document,
edit::{AnchorEdit, apply, line_edits},
grammar::{FormatRegistry, Grammar, RegexCache, Schema, compile, load_grammar},
markdown::Dialect,
yaml,
};
fn compile_schema(grammar: &Grammar, text: &str) -> Result<Schema, Vec<Finding>> {
let written = yaml::parse(text, 1).expect("the schema is YAML");
let schema = compile(
&written,
grammar,
&FormatRegistry::default(),
&RegexCache::new(),
&RepoPath::parse("/list.yaml").expect("a path"),
"list",
&Dialect::default(),
)?;
let findings = check_schema(&schema, &NullHost, &RepoPath::root(), "list");
if findings.is_empty() {
Ok(schema)
} else {
Err(findings)
}
}
fn check_document(schema: &Schema, path: &str, text: &str) -> Vec<String> {
let dialect = Dialect::default();
let document = Document::parse(RepoPath::parse(path).expect("a path"), text, &dialect);
let states = schema.states(document.frontmatter.as_ref());
let out = check_against(
document.view(),
schema,
&states,
&NullHost,
CheckOptions::default(),
Locator::schema("list"),
);
document
.problem
.iter()
.chain(&out.findings)
.map(|f| {
format!(
"{}:{}: [{}] {}",
f.path,
f.line.unwrap_or(0),
f.criterion,
f.message
)
})
.collect()
}
fn repair(schema: &Schema, text: &str) -> String {
let dialect = Dialect::default();
let document = Document::parse(RepoPath::root(), text, &dialect);
let states = schema.states(document.frontmatter.as_ref());
let out = check_against(
document.view(),
schema,
&states,
&NullHost,
CheckOptions::default(),
Locator::schema("list"),
);
let anchors = AnchorEdit {
body: &document.body,
placements: &out.anchors,
text: &document.text,
renames: &[],
};
let edits = line_edits(
&document.text,
&document.segments,
&out.repairs,
Some(&anchors),
);
apply(&document.text, &edits).expect("the edits of one check never clash")
}
const SCHEMA: &str = "line-limit: 50\nfrontmatter:\n title:\n example: Groceries\nsections:\n - heading: { from: title }\n level: 1\n blocks:\n - kind: bullet-list\n each: { template: \"${item:word} x${n:int}\", example: Milk x2 }\n";
#[test]
fn a_host_compiles_checks_repairs_and_derives() {
let grammar = load_grammar().expect("the grammar is built in");
let schema = compile_schema(&grammar, SCHEMA).expect("the schema compiles");
let findings = check_document(
&schema,
"/shopping.md",
"---\ntitle: Groceries\n---\n\n# Shopping\n\n- Milk x2\n- Bread\n",
);
assert_eq!(findings.len(), 2, "{findings:#?}");
assert!(findings[0].starts_with("/shopping.md:5: [TLMD_165] "));
assert!(findings[1].starts_with("/shopping.md:8: [TL_74] "));
let fixed = repair(
&schema,
"---\ntitle: Groceries\n---\n\n# Shopping\n\n- Milk x2\n",
);
assert_eq!(
fixed,
"---\ntitle: Groceries\n---\n\n# Groceries\n\n- Milk x2\n"
);
let derived = derive(&schema, &NullHost).expect("the schema derives");
let scaffold = &derived.scaffolds[0].document;
assert!(scaffold.contains("title: Groceries"), "{scaffold}");
assert!(scaffold.contains("- Milk x2"), "{scaffold}");
}
#[test]
fn a_host_extension_adds_a_key() {
let mut grammar = load_grammar().expect("the grammar is built in");
grammar
.extend(
"extension: shop\nextends: tramlines-md\nschema:\n keys:\n owner:\n type: string\n doc: The team that owns the documents of this schema.\n",
)
.expect("the extension only adds");
let with_owner = format!("owner: groceries-team\n{SCHEMA}");
assert!(compile_schema(&grammar, &with_owner).is_ok());
let plain = load_grammar().expect("the grammar is built in");
let refused =
compile_schema(&plain, &with_owner).expect_err("no `owner` without the extension");
assert!(
refused.iter().any(|f| f.criterion == "TL_12"),
"{refused:#?}"
);
}