diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index 36bf4648f..e53b6ed18 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -4,7 +4,7 @@
Every asset PrintCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
-## In this repository (192)
+## In this repository (193)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
@@ -21,6 +21,7 @@ Every asset PrintCraft includes, bundles or uses to build its published material
| `assets/icons/bold.svg` | Lucide icon "bold" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bold.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/book-open.svg` | Lucide icon "book-open" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/book-open.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/bookmark.svg` | Lucide icon "bookmark" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
+| `assets/icons/bookmark-plus.svg` | Lucide icon "bookmark-plus" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark-plus.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/braces.svg` | Lucide icon "braces" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/braces.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/brush-cleaning.svg` | Lucide icon "brush-cleaning" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/brush-cleaning.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
| `assets/icons/brush.svg` | Lucide icon "brush" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/brush.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml
index 7cce3b15c..affc9784f 100644
--- a/ATTRIBUTION.toml
+++ b/ATTRIBUTION.toml
@@ -151,6 +151,17 @@ kind = "icon"
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
sha256 = "864613d93fddfb9b22368882c00b7895fa2681ceb3ed45c85a1cf2c3d01a3aad"
+[[asset]]
+path = "assets/icons/bookmark-plus.svg"
+title = "Lucide icon \"bookmark-plus\""
+author = "Lucide Contributors (portions Cole Bemis, Feather)"
+source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/bookmark-plus.svg)"
+licence = "ISC"
+licence_file = "assets/icons/LICENSE-lucide.txt"
+kind = "icon"
+usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
+sha256 = "e111411ef061a708e351caefefa68bd1f8c82533f08f9e20dc6bbc0815d7fc1f"
+
[[asset]]
path = "assets/icons/braces.svg"
title = "Lucide icon \"braces\""
diff --git a/assets/icons/bookmark-plus.svg b/assets/icons/bookmark-plus.svg
new file mode 100644
index 000000000..d4b893756
--- /dev/null
+++ b/assets/icons/bookmark-plus.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs
index 6952e5981..9e36b9ae8 100644
--- a/crates/automation/src/lib.rs
+++ b/crates/automation/src/lib.rs
@@ -160,6 +160,32 @@ impl Automation {
json!({ "redone": label, "document": summary(self.doc(&a)?) })
}
"command_list" => self.command_list(&a)?,
+ "bookmark_list" => json!({ "bookmarks": bookmark_tree(&self.doc(&a)?.info.outline, &[]) }),
+ "bookmark_add" => {
+ let page = self.page(&a)?;
+ let parent = a.opt_path("parent")?.unwrap_or_default();
+ let index = a.opt_int("position")?.map_or(usize::MAX, |p| (p.max(1) - 1) as usize);
+ let title = a.str("title")?.to_string();
+ self.apply(&a, Edit::AddBookmark { parent, index, title, page })?
+ }
+ "bookmark_rename" => {
+ let (path, title) = (a.path("path")?, a.str("title")?.to_string());
+ self.apply(&a, Edit::RenameBookmark { path, title })?
+ }
+ "bookmark_delete" => {
+ let path = a.path("path")?;
+ self.apply(&a, Edit::DeleteBookmark { path })?
+ }
+ "bookmark_move" => {
+ let from = a.path("path")?;
+ let to_parent = a.opt_path("parent")?.unwrap_or_default();
+ let index = a.opt_int("position")?.map_or(usize::MAX, |p| (p.max(1) - 1) as usize);
+ self.apply(&a, Edit::MoveBookmark { from, to_parent, index })?
+ }
+ "bookmark_set_page" => {
+ let (path, page) = (a.path("path")?, self.page(&a)?);
+ self.apply(&a, Edit::SetBookmarkPage { path, page })?
+ }
other => return Err(ToolError::UnknownTool(other.into())),
};
Ok(vec![Content::Json(out)])
@@ -505,6 +531,18 @@ impl Args<'_> {
let arr = v.as_array().ok_or_else(|| Self::wrong(key, "an array of integers"))?;
arr.iter().map(|x| x.as_i64().ok_or_else(|| Self::wrong(key, "an array of integers"))).collect::>>().map(Some)
}
+ /// A 1-based bookmark path → 0-based indices.
+ fn path(&self, key: &str) -> Result> {
+ let p = self.opt_path(key)?.ok_or_else(|| Self::missing(key))?;
+ if p.is_empty() {
+ return Err(ToolError::InvalidArgs(format!("{key} must not be empty")));
+ }
+ Ok(p)
+ }
+ fn opt_path(&self, key: &str) -> Result