use quarb_text::{Block, Container, TextModel};
fn values(model: &TextModel, query: &str) -> Vec<String> {
match quarb::run(query, model).unwrap() {
quarb::QueryResult::Values(vs) => vs.iter().map(|v| v.to_string()).collect(),
quarb::QueryResult::Nodes(_) => panic!("expected values"),
}
}
fn nodes(model: &TextModel, query: &str) -> Vec<String> {
let mut got: Vec<String> = match quarb::run(query, model).unwrap() {
quarb::QueryResult::Nodes(ns) => ns.into_iter().map(|n| model.locator(n)).collect(),
quarb::QueryResult::Values(_) => panic!("expected nodes"),
};
got.sort();
got
}
fn outline() -> TextModel {
TextModel::build(vec![
Block::Paragraph {
text: "Preamble.".into(),
},
Block::Heading {
level: 1,
lemma: "One".into(),
},
Block::Paragraph {
text: "In one.".into(),
},
Block::Heading {
level: 3,
lemma: "Deep".into(),
},
Block::Paragraph {
text: "In deep.".into(),
},
Block::Heading {
level: 2,
lemma: "Mid".into(),
},
Block::Paragraph {
text: "In mid.".into(),
},
Block::Heading {
level: 1,
lemma: "Two".into(),
},
Block::Paragraph {
text: "In two.".into(),
},
])
}
#[test]
fn sections_derive_from_flat_headings() {
let m = outline();
assert_eq!(values(&m, "/section::lemma"), vec!["One", "Two"]);
assert_eq!(
nodes(&m, "/section/section"),
vec!["/section[1]/section[1]", "/section[1]/section[2]"]
);
assert_eq!(values(&m, "/section/section::lemma"), vec!["Deep", "Mid"]);
assert_eq!(values(&m, "/section/section::::level"), vec!["3", "2"]);
assert_eq!(values(&m, "/paragraph::"), vec!["Preamble."]);
}
#[test]
fn prose_flattens_with_lemma() {
let m = outline();
assert_eq!(
values(&m, "/section[::lemma = 'Two']::"),
vec!["Two\nIn two."]
);
}
#[test]
fn heading_inside_container_lowers_to_paragraph() {
let m = TextModel::build(vec![
Block::Open {
kind: Container::Blockquote,
lemma: None,
},
Block::Heading {
level: 2,
lemma: "Decorative".into(),
},
Block::Paragraph {
text: "Quoted.".into(),
},
Block::Close { hypograph: None },
]);
assert_eq!(nodes(&m, "//section"), Vec::<String>::new());
assert_eq!(
values(&m, "/blockquote/paragraph::"),
vec!["Decorative", "Quoted."]
);
}
#[test]
fn blockquote_hypograph() {
let m = TextModel::build(vec![
Block::Open {
kind: Container::Blockquote,
lemma: None,
},
Block::Paragraph {
text: "Quoted wisdom.".into(),
},
Block::Close {
hypograph: Some("— Sage".into()),
},
]);
assert_eq!(values(&m, "/blockquote::hypograph"), vec!["— Sage"]);
assert_eq!(values(&m, "/blockquote::"), vec!["Quoted wisdom.\n— Sage"]);
}
#[test]
fn lists_and_items() {
let m = TextModel::build(vec![
Block::Open {
kind: Container::UnorderedList,
lemma: None,
},
Block::Open {
kind: Container::Item,
lemma: None,
},
Block::Text {
text: "alpha".into(),
},
Block::Close { hypograph: None },
Block::Open {
kind: Container::Item,
lemma: None,
},
Block::Text {
text: "beta".into(),
},
Block::Open {
kind: Container::UnorderedList,
lemma: None,
},
Block::Open {
kind: Container::Item,
lemma: None,
},
Block::Text {
text: "beta-child".into(),
},
Block::Close { hypograph: None },
Block::Close { hypograph: None },
Block::Close { hypograph: None },
Block::Close { hypograph: None },
Block::Open {
kind: Container::OrderedList { start: 3 },
lemma: None,
},
Block::Open {
kind: Container::Item,
lemma: None,
},
Block::Text {
text: "third".into(),
},
Block::Close { hypograph: None },
Block::Open {
kind: Container::Item,
lemma: None,
},
Block::Text {
text: "fourth".into(),
},
Block::Close { hypograph: None },
Block::Close { hypograph: None },
]);
assert_eq!(
values(&m, "/unordered-list/unordered-item::"),
vec!["alpha", "beta\nbeta-child"]
);
assert_eq!(
nodes(&m, "//unordered-item/unordered-list/unordered-item"),
vec!["/unordered-list/unordered-item[2]/unordered-list/unordered-item"]
);
assert_eq!(values(&m, "//ordered-item::taxis"), vec!["3", "4"]);
}
#[test]
fn tables_denormalize_to_nested_lists() {
let m = TextModel::build(vec![Block::Table {
lemma: Some("Crew".into()),
headers: Some(vec!["Name".into(), "Role".into()]),
rows: vec![
vec!["Alice".into(), "captain".into()],
vec!["Bob".into(), "".into()],
],
}]);
assert_eq!(nodes(&m, "//*<table>"), vec!["/ordered-list"]);
assert_eq!(values(&m, "/ordered-list::lemma"), vec!["Crew"]);
assert_eq!(values(&m, "//ordered-item::taxis"), vec!["1", "2"]);
assert_eq!(
values(&m, "//unordered-item::"),
vec!["Name: Alice", "Role: captain", "Name: Bob"]
);
}
#[test]
fn headerless_table_keeps_bare_cells() {
let m = TextModel::build(vec![Block::Table {
lemma: None,
headers: None,
rows: vec![vec!["Alice".into(), "captain".into()]],
}]);
assert_eq!(
values(&m, "//unordered-item::"),
vec!["Alice", "captain"]
);
}
#[test]
fn plain_text_paragraphs() {
let m = TextModel::parse_plain("One line\nsame paragraph.\n\n \nSecond paragraph.\n");
assert_eq!(
values(&m, "/paragraph::"),
vec!["One line same paragraph.", "Second paragraph."]
);
assert_eq!(nodes(&m, "//section"), Vec::<String>::new());
}
#[test]
fn cells_are_addressable_by_lemma() {
let m = TextModel::build(vec![Block::Table {
lemma: Some("Crew".into()),
headers: Some(vec!["Name".into(), "Role".into()]),
rows: vec![
vec!["Alice".into(), "captain".into()],
vec!["Bob".into(), "cook".into()],
],
}]);
assert_eq!(
values(&m, "//*<cell>[::lemma = \"Role\"]::"),
vec!["Role: captain", "Role: cook"]
);
assert_eq!(
values(&m, "//*<row>[::taxis = 2]/*/*[::lemma = \"Name\"]::"),
vec!["Name: Bob"]
);
assert_eq!(
values(&m, "//*<cell>[::lemma = \"Name\"]::lemma"),
vec!["Name", "Name"]
);
}
#[test]
fn row_labels_become_cell_lemmas() {
let m = TextModel::build(vec![Block::Table {
lemma: None,
headers: None,
rows: vec![vec![
quarb_text::Cell {
label: Some("Location".into()),
text: "Campion".into(),
},
]],
}]);
assert_eq!(values(&m, "//*<cell>::lemma"), vec!["Location"]);
assert_eq!(values(&m, "//*<cell>::"), vec!["Location: Campion"]);
}
#[test]
fn item_lemma_flattens_inline() {
let m = TextModel::build(vec![
Block::Open {
kind: Container::UnorderedList,
lemma: None,
},
Block::Open {
kind: Container::Item,
lemma: Some("emu".into()),
},
Block::Paragraph {
text: "a large flightless bird".into(),
},
Block::Close { hypograph: None },
Block::Close { hypograph: None },
]);
assert_eq!(
values(&m, "//unordered-item::"),
vec!["emu: a large flightless bird"]
);
assert_eq!(values(&m, "//unordered-item::lemma"), vec!["emu"]);
}
#[test]
fn serialization_stages_render_subtrees() {
let m = TextModel::build(vec![
Block::Heading {
level: 2,
lemma: "The \"war\"".into(),
},
Block::Paragraph {
text: "Machine guns were requested.".into(),
},
Block::Heading {
level: 3,
lemma: "First attempt".into(),
},
Block::Paragraph {
text: "The birds split into small groups.".into(),
},
]);
assert_eq!(
values(&m, r#"//section[::lemma =~ /war/] | markdown"#),
vec![
"## The \"war\"\n\nMachine guns were requested.\n\n### First attempt\n\nThe birds split into small groups."
]
);
assert_eq!(
values(&m, r#"//section[::lemma = "First attempt"] | html"#),
vec!["<h3>First attempt</h3>\n\n<p>The birds split into small groups.</p>"]
);
assert_eq!(
values(&m, r#"//section[::lemma = "First attempt"] | atrep"#),
vec![
"@@@!litogramma\n\n@# First attempt\n\nThe birds split into small groups.\n\n#@"
]
);
}
#[test]
fn atrep_emits_litogramma_forms() {
let m = TextModel::build(vec![
Block::Open {
kind: Container::Blockquote,
lemma: None,
},
Block::Paragraph {
text: "Invulnerable as tanks.".into(),
},
Block::Close {
hypograph: Some("Major Meredith".into()),
},
Block::Table {
lemma: None,
headers: None,
rows: vec![vec![
quarb_text::Cell {
label: Some("Date".into()),
text: "2 November 1932".into(),
},
quarb_text::Cell {
label: Some("Outcome".into()),
text: "Minimal impact".into(),
},
]],
},
Block::Verbatim {
lang: Some("rust".into()),
text: "fn main() {}".into(),
},
]);
let out = &values(&m, "^ | atrep")[0];
assert!(out.starts_with("@@@!litogramma\n"), "{out}");
assert!(
out.contains("@\"/\nInvulnerable as tanks.\n/\"@ Major Meredith"),
"{out}"
);
assert!(
out.contains("@:: Date\n@;\n2 November 1932\n;@\n::@"),
"{out}"
);
assert!(out.contains("@@@\"\nfn main() {}\n\"@@@.rust"), "{out}");
}
#[test]
fn atrep_output_parses() {
let (Ok(bin), Ok(dia)) = (
std::env::var("ATREP_BIN"),
std::env::var("LITOGRAMMA_DIA"),
) else {
eprintln!("skip: set ATREP_BIN and LITOGRAMMA_DIA to run the parse gate");
return;
};
let m = TextModel::build(vec![
Block::Heading {
level: 2,
lemma: "Aftermath".into(),
},
Block::Paragraph {
text: "The emus prevailed.".into(),
},
Block::Open {
kind: Container::UnorderedList,
lemma: None,
},
Block::Open {
kind: Container::Item,
lemma: None,
},
Block::Text {
text: "a bounty system".into(),
},
Block::Close { hypograph: None },
Block::Close { hypograph: None },
]);
let doc = &values(&m, "^ | atrep")[0];
let dir = std::env::temp_dir().join("quarb-atrep-gate");
std::fs::create_dir_all(&dir).unwrap();
std::fs::copy(&dia, dir.join("litogramma.dia")).unwrap();
let path = dir.join("gate.atd");
std::fs::write(&path, format!("{doc}\n")).unwrap();
let out = std::process::Command::new(&bin)
.arg("check")
.arg(&path)
.output()
.expect("run atrep check");
assert!(
out.status.success(),
"atrep rejected the emission:\n{}\n---\n{doc}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn grammata_and_the_friendly_aliases() {
let m = TextModel::build(vec![
Block::Heading {
level: 2,
lemma: "Aftermath".into(),
},
Block::Paragraph {
text: "The emus prevailed.".into(),
},
Block::Table {
lemma: None,
headers: None,
rows: vec![vec![quarb_text::Cell {
label: Some("Outcome".into()),
text: "Emu victory".into(),
}]],
},
Block::Open {
kind: Container::Blockquote,
lemma: None,
},
Block::Paragraph {
text: "Invulnerable as tanks.".into(),
},
Block::Close {
hypograph: Some("Major Meredith".into()),
},
]);
assert_eq!(
values(&m, "//section[::lemma = \"Aftermath\"]::grammata @| [1] | [$_ =~ /^The emus/] @| count"),
vec!["1"]
);
assert_eq!(values(&m, "//*<cell>::grammata"), vec!["Emu victory"]);
assert_eq!(values(&m, "//*<cell>::body"), vec!["Emu victory"]);
assert_eq!(values(&m, "//*<cell>::title"), vec!["Outcome"]);
assert_eq!(
values(&m, "//section[::title = \"Aftermath\"]::lemma"),
vec!["Aftermath"]
);
assert_eq!(
values(&m, "//blockquote::attribution"),
vec!["Major Meredith"]
);
assert_eq!(
values(&m, "//blockquote::grammata"),
vec!["Invulnerable as tanks."]
);
assert_eq!(values(&m, "//*<row>[::ord = 1]::taxis"), vec!["1"]);
}
#[test]
fn closed_surface_adapters_alias_their_metadata() {
let m = TextModel::build(vec![
Block::Heading { level: 3, lemma: "Aftermath".into() },
Block::Verbatim { lang: Some("rust".into()), text: "fn main() {}".into() },
]);
assert_eq!(values(&m, "//section::level"), vec!["3"]);
assert_eq!(values(&m, "//section::::level"), vec!["3"]);
assert_eq!(values(&m, "//verbatim::lang"), vec!["rust"]);
assert_eq!(values(&m, "//section[::level = 3]::lemma"), vec!["Aftermath"]);
assert_eq!(values(&m, "//section::nonesuch"), vec![""]);
let names = values(&m, "//section | rec(\"data\", ::name, \"core\", :::name)");
assert!(names[0].contains("\"data\": null"), "{names:?}");
}