Merge upstream/main (#4184 craft-libs bump) into the batch

This commit is contained in:
aiongg
2026-10-24 21:58:03 +00:00
9 changed files with 1533 additions and 67 deletions
Generated
+1477 -53
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -54,7 +54,7 @@ harfrust = "0.13"
wuff = "0.2.9"
vello_cpu = "0.2"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "bmp", "tiff"] }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef", version = "=0.1.0", default-features = false, features = ["write"] }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "17744430be327d0257496a6a31c8d38fe20a6756", version = "=0.1.0", default-features = false, features = ["write"] }
quick-xml = "0.41"
zip = { version = "3", default-features = false, features = ["deflate"] }
# zlib streams (compressed metafile pictures in `.doc` drawings); already in the lock as a dependency of other crates.
@@ -110,4 +110,4 @@ debug = 0
# Required by craft-pdf: the backend rejects writers that failed during unwinding.
[patch.crates-io]
krilla = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef" }
krilla = { git = "https://github.com/storytold/craft-libs", rev = "17744430be327d0257496a6a31c8d38fe20a6756" }
+1 -1
View File
@@ -73,7 +73,7 @@ windows = { version = "0.62", features = ["UI_ViewManagement", "System_UserProfi
clipboard-win = { version = "5.4", features = ["std"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d" }
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "17744430be327d0257496a6a31c8d38fe20a6756" }
rfd = { workspace = true }
# Waits for an `rfd::AsyncFileDialog` on a background thread (the dialogs don't block the UI, #94);
# rfd itself already depends on it.
+1 -1
View File
@@ -13,7 +13,7 @@ pub fn discover() -> Discover {
}
// Keep submission workers reachable until normal app shutdown; dropping a JoinHandle would
// let process exit kill PowerShell/CUPS before it hands the job to the spooler.
// let process exit end an in-process Windows job or the CUPS hand-off before the spooler has it.
static JOBS: std::sync::Mutex<Vec<std::thread::JoinHandle<()>>> = std::sync::Mutex::new(Vec::new());
pub fn finish_pending_jobs() {
+4 -5
View File
@@ -27,7 +27,6 @@ use std::num::NonZeroU16;
use std::sync::Arc;
use craft_pdf::write::{self, Metadata, WriteError, Writer, backend as krilla};
use krilla::action::{Action, LinkAction};
use krilla::annotation::{Annotation, LinkAnnotation, Target};
use krilla::color::rgb;
use krilla::destination::XyzDestination;
@@ -1601,11 +1600,11 @@ impl Exporter<'_> {
None => continue,
}
} else {
let url = target.trim();
if url.is_empty() {
continue;
// Unsafe targets (javascript:, file:, shares, other schemes) get no link; the text stays.
match write::uri_target(target) {
Some(t) => t,
None => continue,
}
Target::Action(Action::Link(LinkAction::new(url.to_string())))
};
v.push(Annotation::new_link(LinkAnnotation::new(rect, t), Some(target.clone())));
}
+25
View File
@@ -2386,3 +2386,28 @@ fn borders_are_dashed_with_words_patterns() {
assert!(raw.contains("[6 2]0 d") || raw.contains("[6 2] 0 d"), "the dashed border");
assert!(raw.contains("[3 1 1 1]0 d") || raw.contains("[3 1 1 1] 0 d"), "the dash-dot border");
}
#[test]
fn only_safe_link_targets_become_annotations() {
use hayro_syntax::object::{Array, Dict, String as PdfString};
let mut d = Document::new();
d.body = ["https://example.com/", "mailto:ada@example.com", "javascript:alert(1)", "file:///etc/passwd"]
.into_iter()
.map(|url| para_block(Paragraph::with_text("Click here", CharProps { link: Some(url.into()), ..Default::default() })))
.collect();
let bytes = export(&d, &PdfOptions::default()).unwrap();
assert_eq!(squash(&extract_text(&bytes)[0]).matches("Click here").count(), 4, "unlinked text stays");
let pdf = hayro_syntax::Pdf::new(bytes).unwrap();
let uris: Vec<String> = pdf.pages()[0]
.raw()
.get::<Array>(b"Annots")
.into_iter()
.flat_map(|a| a.iter::<Dict>().collect::<Vec<_>>())
.map(|a| {
let uri = a.get::<Dict>(b"A").unwrap().get::<PdfString>(b"URI").unwrap();
String::from_utf8_lossy(uri.as_ref()).into_owned()
})
.collect();
assert_eq!(uris, ["https://example.com/", "mailto:ada@example.com"]);
}
+3 -2
View File
@@ -12,8 +12,9 @@ workspace = true
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "d27646a5f7c8b4fc85d9dd3213b8c88b58e0088d" }
# Interface translations: lookup, fallback, plurals and system-locale matching over the craft-i18n
# catalogs that `build.rs` embeds from `CRAFT_I18N_DIR` (storytold/craft-i18n, pinned in
# `craft-i18n.rev`). Its own rev: lazy per-locale parsing (storytold/craft-libs#247).
craft-i18n = { git = "https://github.com/storytold/craft-libs", rev = "f7cc8a7df15e824b02cf73859567a6242d3a7de4" }
# `craft-i18n.rev`). Its own rev, ahead of craft-ui's: lazy per-locale parsing
# (storytold/craft-libs#247), ICU4X plural rules and bidi isolates around inserted values.
craft-i18n = { git = "https://github.com/storytold/craft-libs", rev = "17744430be327d0257496a6a31c8d38fe20a6756" }
wordcraft-geom = { workspace = true, features = ["image"] }
wordcraft-doc = { workspace = true }
unicode-bidi = { workspace = true }
+18 -2
View File
@@ -82,16 +82,17 @@ impl ChartDataForm {
Ok(json!({"data": {"categories": self.categories, "series": series}}))
}
// Default names become document text (saved and exported), so no isolate marks.
fn add_row(&mut self) {
if self.categories.len() < MAX_POINTS {
self.categories.push(crate::i18n::fmt(tl!("Category {n}"), &[("n", &(self.categories.len() + 1).to_string())]));
self.categories.push(crate::i18n::fmt_plain(tl!("Category {n}"), &[("n", &(self.categories.len() + 1).to_string())]));
self.cells.push(vec![String::new(); self.names.len()]);
}
}
fn add_series(&mut self) {
if self.names.len() < MAX_SERIES {
self.names.push(crate::i18n::fmt(tl!("Series {n}"), &[("n", &(self.names.len() + 1).to_string())]));
self.names.push(crate::i18n::fmt_plain(tl!("Series {n}"), &[("n", &(self.names.len() + 1).to_string())]));
for r in &mut self.cells {
r.push(String::new());
}
@@ -356,4 +357,19 @@ mod tests {
f.cells[1][0] = "abc".into();
assert!(f.params().is_err());
}
/// Default names are saved into the document, so an RTL interface must not add isolate marks.
#[test]
fn default_names_in_an_rtl_interface_have_no_isolates() {
crate::i18n::set_current(crate::i18n::fixture_lang("ar", "\tCategory {n}\tالفئة {n}\n\tSeries {n}\tالسلسلة {n}\n"));
let mut f = ChartDataForm::of(&ChartSpec::sample(ChartType::Column));
f.add_row();
f.add_series();
crate::i18n::set_current(crate::i18n::Lang::EN);
let (category, series) = (f.categories.last().unwrap(), f.names.last().unwrap());
assert!(category.starts_with("الفئة") && series.starts_with("السلسلة"), "{category:?} {series:?}");
for name in [category, series] {
assert!(!name.chars().any(|c| ('\u{2066}'..='\u{2069}').contains(&c)), "{name:?}");
}
}
}
+2 -1
View File
@@ -32,13 +32,14 @@
//! - [`get`]: one context's translation only, never the plain row (`spacing`'s "Open").
//! - [`location`]: a ribbon location such as `Home › Font`, segment by segment.
//! - [`fmt`]: fill `{name}` placeholders after a lookup; translators may reorder them.
//! - [`fmt_plain`]: [`fmt`] without bidi isolate marks, for text that goes into the document.
use std::cell::Cell;
use std::sync::{LazyLock, OnceLock};
use craft_i18n::I18n;
pub use craft_i18n::fmt;
pub use craft_i18n::{fmt, fmt_plain};
include!(concat!(env!("OUT_DIR"), "/craft_i18n.rs"));