212 lines
7.6 KiB
Rust
212 lines
7.6 KiB
Rust
//! Bases inside notes, driven headless over a fixture vault: a ```` ```base ```` block renders a
|
|
//! table, `![[x.base#Reading list]]` picks the view, a self-embed shows the cycle chip, a base too
|
|
//! deep in embeds shows the depth chip, and `this` is the embedding note.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use egui::Vec2;
|
|
use egui_kittest::kittest::Queryable;
|
|
use notescraft_engine::{Registry, Session, register_core};
|
|
use notescraft_themes::builtin;
|
|
use notescraft_ui_egui::base_view::embedded::Host;
|
|
use notescraft_ui_egui::note_view::{CalloutFolds, EmbedNotes, set_embeds, show_note};
|
|
use notescraft_ui_egui::theme::Tokens;
|
|
|
|
struct Page {
|
|
doc: notescraft_markdown::Document,
|
|
tokens: Tokens,
|
|
session: Session,
|
|
registry: Registry<Session>,
|
|
host: Host,
|
|
folds: CalloutFolds,
|
|
warmed: bool,
|
|
clicked: Vec<String>,
|
|
}
|
|
|
|
const BOOKS: &str = "views:
|
|
- type: table
|
|
name: Everything
|
|
order:
|
|
- file.name
|
|
- note.year
|
|
- type: table
|
|
name: Reading list
|
|
filters:
|
|
and:
|
|
- file.inFolder(\"Books\")
|
|
order:
|
|
- file.name
|
|
- note.year
|
|
limit: 5
|
|
";
|
|
|
|
const MINE: &str = "filters:
|
|
and:
|
|
- file.inFolder(this.file.folder)
|
|
views:
|
|
- type: list
|
|
name: Siblings
|
|
order:
|
|
- file.name
|
|
";
|
|
|
|
fn vault(name: &str, notes: &[(&str, &str)]) -> std::path::PathBuf {
|
|
let root = std::env::temp_dir().join(format!("notescraft-base-embed-{name}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
std::fs::create_dir_all(root.join("Books")).expect("books folder");
|
|
std::fs::create_dir_all(root.join("Notes")).expect("notes folder");
|
|
for i in 0..8 {
|
|
std::fs::write(root.join(format!("Books/book-{i}.md")), format!("---\nyear: {}\n---\nBook {i}\n", 1950 + i)).expect("book");
|
|
}
|
|
std::fs::write(root.join("Books.base"), BOOKS).expect("base");
|
|
for (path, text) in notes {
|
|
std::fs::write(root.join(path), text).expect("note");
|
|
}
|
|
root
|
|
}
|
|
|
|
fn page(name: &str, source: &str, extra: &[(&str, &str)]) -> egui_kittest::Harness<'static, Page> {
|
|
let mut notes = vec![("Notes/Page.md", source)];
|
|
notes.extend_from_slice(extra);
|
|
let session = Session::open(&vault(name, ¬es)).expect("the vault opens");
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("commands register");
|
|
let doc = notescraft_markdown::parse(source);
|
|
let embeds = EmbedNotes::load(&session, "Notes/Page.md", &doc);
|
|
let state = Page {
|
|
doc,
|
|
tokens: Tokens::new(builtin("catppuccin-mocha").expect("theme")),
|
|
session,
|
|
registry,
|
|
host: Host::default(),
|
|
folds: CalloutFolds::default(),
|
|
warmed: false,
|
|
clicked: Vec::new(),
|
|
};
|
|
egui_kittest::Harness::builder().with_size(Vec2::new(900.0, 900.0)).with_max_steps(300).build_ui_state(
|
|
move |ui, page: &mut Page| {
|
|
if !page.warmed {
|
|
notescraft_ui_egui::fonts::install(ui.ctx());
|
|
egui_extras::install_image_loaders(ui.ctx());
|
|
page.warmed = true;
|
|
return;
|
|
}
|
|
set_embeds(ui.ctx(), Some(Arc::new(embeds.clone())));
|
|
page.host.publish(ui.ctx());
|
|
let shown = show_note(ui, &page.tokens, &page.doc, &|_: &str| true, &mut page.folds);
|
|
page.clicked.extend(shown.clicked_link.map(|link| link.target));
|
|
let Page { session, registry, host, .. } = page;
|
|
if host.pump(&mut |id, params| registry.run(id, session, params).map_err(|error| error.to_string())) {
|
|
ui.ctx().request_repaint();
|
|
}
|
|
},
|
|
state,
|
|
)
|
|
}
|
|
|
|
/// Every text the last frame painted, joined.
|
|
fn painted_text(h: &egui_kittest::Harness<'_, Page>) -> String {
|
|
h.output()
|
|
.shapes
|
|
.iter()
|
|
.filter_map(|clipped| if let egui::epaint::Shape::Text(text) = &clipped.shape { Some(text.galley.text().to_string()) } else { None })
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
fn settle(h: &mut egui_kittest::Harness<'_, Page>, key: &str) {
|
|
for _ in 0..40 {
|
|
h.run();
|
|
if h.state().host.rows_painted(key).is_some_and(|rows| rows > 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const INLINE: &str = "# Page\n\n```base\nviews:\n - type: table\n name: Years\n order:\n - file.name\n - note.year\n filters:\n and:\n - file.inFolder(\"Books\")\n```\n\nafter\n";
|
|
|
|
#[test]
|
|
fn a_base_block_renders_a_table() {
|
|
let mut h = page("inline", INLINE, &[]);
|
|
settle(&mut h, "inline");
|
|
assert_eq!(h.state().host.len(), 1);
|
|
assert_eq!(h.state().host.rows_painted("inline"), Some(8), "the eight books");
|
|
h.get_by_label("Years");
|
|
assert!(painted_text(&h).contains("after"), "the text after the block still shows");
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_embed_picks_the_view_after_the_hash() {
|
|
let mut h = page("pick", "![[Books.base#Reading list]]\n", &[]);
|
|
settle(&mut h, "Books.base");
|
|
assert_eq!(h.state().host.shown_view("Books.base"), Some(1));
|
|
let rows = h.state().host.rows_painted("Books.base").unwrap_or(0);
|
|
assert!((5..=6).contains(&rows), "the view's limit of 5 rows (and the spare one the scroller paints): {rows}");
|
|
h.get_by_label("Open base").click();
|
|
h.run();
|
|
assert_eq!(h.state().clicked, ["Books.base"]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_embed_without_a_view_shows_tabs_for_the_first_view() {
|
|
let mut h = page("first", "![[Books.base]]\n", &[]);
|
|
settle(&mut h, "Books.base");
|
|
assert_eq!(h.state().host.shown_view("Books.base"), Some(0));
|
|
assert!(h.state().host.rows_painted("Books.base").is_some_and(|rows| rows >= 8), "every note and file shows");
|
|
}
|
|
|
|
#[test]
|
|
fn this_is_the_embedding_note() {
|
|
let source = format!("```base\n{MINE}```\n");
|
|
let mut h = page("this", &source, &[("Notes/Other.md", "o\n")]);
|
|
for _ in 0..40 {
|
|
h.run();
|
|
if h.state().host.rows_painted("inline").is_some_and(|rows| rows > 0) {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(h.state().host.rows_painted("inline"), Some(2), "the two notes next to Notes/Page.md");
|
|
}
|
|
|
|
#[test]
|
|
fn a_self_embed_shows_the_cycle_chip_not_a_second_table() {
|
|
let source = format!("{INLINE}\n![[Page]]\n");
|
|
let mut h = page("cycle", &source, &[]);
|
|
settle(&mut h, "inline");
|
|
h.run();
|
|
assert!(painted_text(&h).contains("already open above"), "{}", painted_text(&h));
|
|
assert_eq!(h.state().host.len(), 1, "one table, the embed of itself does not draw another");
|
|
}
|
|
|
|
#[test]
|
|
fn a_base_nested_too_deep_shows_the_depth_chip() {
|
|
let extra = [("Notes/B.md", "![[C]]\n"), ("Notes/C.md", "![[D]]\n"), ("Notes/D.md", "![[Books.base]]\n")];
|
|
let mut h = page("depth", "![[B]]\n", &extra);
|
|
for _ in 0..10 {
|
|
h.run();
|
|
}
|
|
assert!(painted_text(&h).contains("nested too deeply"), "{}", painted_text(&h));
|
|
assert!(h.state().host.is_empty(), "no table was started");
|
|
}
|
|
|
|
fn save(h: &mut egui_kittest::Harness<'_, Page>, dir: &str, name: &str) {
|
|
let image = h.render().expect("renders");
|
|
std::fs::create_dir_all(dir).expect("dir");
|
|
image.save(std::path::Path::new(dir).join(name)).expect("png");
|
|
}
|
|
|
|
/// Renders a note with embedded bases: `NOTESCRAFT_BASE_PNG_DIR=<dir> cargo test --test base_embed -- --ignored`.
|
|
#[test]
|
|
#[ignore = "writes PNGs for a visual check"]
|
|
fn render_pngs() {
|
|
let Ok(dir) = std::env::var("NOTESCRAFT_BASE_PNG_DIR") else { return };
|
|
let source =
|
|
format!("# Reading notes\n\nSome words before the table.\n\n{INLINE}\nAnd the file, on its second view:\n\n![[Books.base#Reading list]]\n\nThe end.\n");
|
|
let mut h = page("png", &source, &[]);
|
|
settle(&mut h, "inline");
|
|
for _ in 0..10 {
|
|
h.run();
|
|
}
|
|
save(&mut h, &dir, "note-with-bases.png");
|
|
}
|