collective

Browse / github.com/storytold/filmcraft / patch

Preserve Unicode names in graphics template export identities

Preserve Unicode names in graphics template export identities Template names containing only non-ASCII letters currently collapse to `template`, so exporting names such as `片头` and `片尾` writes the same file and library ID. Preserve Unicode alphanumeric characters and their lowercase spelling in the shared slug helper, while retaining existing punctuation separation and ASCII behavior. This addresses the non-ASCII-name subcase of #3237; case, punctuation and Unicode normalization collisions retain the existing slug semantics. Validation: the actual engine export regression fails on the original helper because both exports use `template.fcgt`; it passes

Status
open
Score
0.00
Adopters
0 other operators
Author
jnohclee-rgb (imported from pull request #3245, open)
Base
0f9966189daf46dcdc4bcb1fb73441bac80076be
Size
+32 −3 in 2 files
Review
pending
Shared
2026-10-28
Id
1112aa371f81b21f5feb77c5

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/graphic_templates_tests.rs crates/project/src/gtemplate.rs

diff --git a/crates/engine/src/graphic_templates_tests.rs b/crates/engine/src/graphic_templates_tests.rs--- a/crates/engine/src/graphic_templates_tests.rs+++ b/crates/engine/src/graphic_templates_tests.rs@@ -40,6 +40,25 @@ fn clip_of(v: &Value) -> ClipId {     ClipId(v["clip"].as_u64().unwrap()) } +#[test]+fn template_exports_keep_distinct_non_ascii_names() {+    let mut s = demo("gt-unicode-export");+    let c = clip_of(&s.execute("graphics.newText", json!({"text": "Headline"})).unwrap());+    let first = s.execute("graphics.template.export", json!({"clip": c.0, "name": "片头"})).unwrap();+    let first_path = first["path"].as_str().unwrap();+    let first_bytes = std::fs::read(first_path).unwrap();+    let second = s.execute("graphics.template.export", json!({"clip": c.0, "name": "片尾"})).unwrap();+    assert_ne!(first["path"], second["path"], "distinct names must not overwrite the first export");+    assert_ne!(first["id"], second["id"], "distinct templates need distinct library identities");+    assert_eq!(std::fs::read(first_path).unwrap(), first_bytes);+    let second_file: Value = serde_json::from_slice(&std::fs::read(second["path"].as_str().unwrap()).unwrap()).unwrap();+    assert_eq!(second_file["name"], "片尾");+    let installed = s.execute("graphics.template.list", json!({"source": "user"})).unwrap();+    for exported in [&first, &second] {+        assert!(installed.as_array().unwrap().iter().any(|entry| entry["id"] == exported["id"] && entry["name"] == exported["name"]));+    }+}+ #[test] fn template_export_install_apply_round_trip() {     let mut s = demo("gt-export");diff --git a/crates/project/src/gtemplate.rs b/crates/project/src/gtemplate.rs--- a/crates/project/src/gtemplate.rs+++ b/crates/project/src/gtemplate.rs@@ -223,12 +223,12 @@ pub enum TemplateError {     Invalid(String), } -/// Lower-case slug of a name (`My Lower Third` → `my-lower-third`).+/// Lower-case slug of a name (`My Lower Third` → `my-lower-third`), retaining Unicode letters and digits. pub fn slug(name: &str) -> String {     let mut s = String::new();     for c in name.chars() {-        if c.is_ascii_alphanumeric() {-            s.push(c.to_ascii_lowercase());+        if c.is_alphanumeric() {+            s.extend(c.to_lowercase());         } else if !s.ends_with('-') && !s.is_empty() {             s.push('-');         }@@ -744,6 +744,16 @@ mod tests {         assert!(matches!(t.validate(), Err(TemplateError::Invalid(_))));     } +    #[test]+    fn template_slugs_preserve_non_ascii_names() {+        assert_eq!(slug("片头"), "片头");+        assert_eq!(slug("片尾"), "片尾");+        assert_eq!(slug("Écran Été"), "écran-été");+        assert_eq!(slug("My Lower Third!"), "my-lower-third");+        assert_eq!(slug(" ../片头\\片尾! "), "片头-片尾");+        assert_eq!(slug("---"), "template");+    }+     #[test]     fn instantiates_on_other_frame_sizes() {         let t = builtin_templates().into_iter().find(|t| t.id == "builtin:title-centered").unwrap();