mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 13:56:54 -06:00
M6: transliterate non-ASCII layer names into code-export class names (#1235)
class_name runs layer names through deunicode (new dependency: pure Rust tables, zero deps, BSD-3-Clause, the slug crate's base) before its ASCII filter, so Korean, Cyrillic, Latin-diacritic and CJK names become readable ASCII classes in HTML/CSS/React/Leptos output, component roots and data-* variant attributes: 로그인 화면 → rogeuin-hwamyeon, кнопка → knopka, über → uber, 按钮 → an-niu (before, every one came out layer-, layer--2, ...). Symbols-only names take the node's kind (.text, .frame) instead of layer-, the 40-character cut lands on a word boundary, and a leading digit keeps the layer- prefix. SwiftUI/Compose type names are unchanged (their own sanitizer); ASCII-only names are byte-identical. html_codegen_tests updated and extended.
This commit is contained in:
@@ -594,18 +594,60 @@ fn css_gradient(g: &figcraft_color::Gradient, w: f64, h: f64) -> String {
|
||||
|
||||
// ------------------------------------------------------------------ HTML and React markup
|
||||
|
||||
/// A CSS class name from a layer name ("Card / Title" → "card-title"), unique in `used`.
|
||||
pub(crate) fn class_name(name: &str, used: &mut Vec<String>) -> String {
|
||||
/// A word for the node's kind, naming layers whose name transliterates to nothing ("###" →
|
||||
/// `.text`, `.frame`), instead of the anonymous `layer-`.
|
||||
fn kind_word(n: &Node) -> &str {
|
||||
match &n.kind {
|
||||
NodeKind::Page { .. } => "page",
|
||||
NodeKind::Frame => "frame",
|
||||
NodeKind::Group => "group",
|
||||
NodeKind::Section { .. } => "section",
|
||||
NodeKind::Rectangle => "rectangle",
|
||||
NodeKind::Ellipse { .. } => "ellipse",
|
||||
NodeKind::Polygon { .. } => "polygon",
|
||||
NodeKind::Star { .. } => "star",
|
||||
NodeKind::Line => "line",
|
||||
NodeKind::Vector { .. } => "vector",
|
||||
NodeKind::Text(_) => "text",
|
||||
NodeKind::BooleanOp { .. } => "boolean",
|
||||
NodeKind::Component { .. } | NodeKind::ComponentSet { .. } => "component",
|
||||
NodeKind::Instance { .. } => "instance",
|
||||
NodeKind::Slice => "slice",
|
||||
}
|
||||
}
|
||||
|
||||
/// A CSS class name from a layer name ("Card / Title" → "card-title"), unique in `used`. The name
|
||||
/// is transliterated to ASCII first, so non-Latin names stay readable: 로그인 화면 →
|
||||
/// `rogeuin-hwamyeon`, кнопка → `knopka`, über → `uber` (per-codepoint transliteration, the slug
|
||||
/// libraries' way); a name left with nothing (symbols only) takes `kind`, and one starting with a
|
||||
/// digit keeps the `layer-` prefix.
|
||||
pub(crate) fn class_name(name: &str, kind: &str, used: &mut Vec<String>) -> String {
|
||||
let mut base = String::new();
|
||||
for c in name.chars().flat_map(char::to_lowercase) {
|
||||
for c in deunicode::deunicode(name).chars().flat_map(char::to_lowercase) {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
base.push(c);
|
||||
} else if !base.ends_with('-') && !base.is_empty() {
|
||||
base.push('-');
|
||||
}
|
||||
}
|
||||
let base = base.trim_end_matches('-').chars().take(40).collect::<String>();
|
||||
let base = if base.is_empty() || base.starts_with(|c: char| c.is_ascii_digit()) { format!("layer-{base}") } else { base };
|
||||
// The 40-character cut lands on a word boundary when there is one past the first word.
|
||||
let base = if base.chars().count() > 40 {
|
||||
let cut: String = base.chars().take(40).collect();
|
||||
match cut.rfind('-') {
|
||||
Some(i) if i > 0 => cut[..i].to_string(),
|
||||
_ => cut,
|
||||
}
|
||||
} else {
|
||||
base
|
||||
};
|
||||
let base = base.trim_end_matches('-');
|
||||
let base = if base.is_empty() {
|
||||
kind.to_string()
|
||||
} else if base.starts_with(|c: char| c.is_ascii_digit()) {
|
||||
format!("layer-{base}")
|
||||
} else {
|
||||
base.to_string()
|
||||
};
|
||||
let name = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}-{k}") }).find(|n| !used.contains(n)).unwrap_or(base);
|
||||
used.push(name.clone());
|
||||
name
|
||||
@@ -726,7 +768,7 @@ impl<'c, 'a> Markup<'c, 'a> {
|
||||
self.used.push(c.clone());
|
||||
c
|
||||
}
|
||||
None => class_name(&n.name, &mut self.used),
|
||||
None => class_name(&n.name, kind_word(n), &mut self.used),
|
||||
};
|
||||
let pad = self.pad(depth);
|
||||
let attr = if self.lang == CodeLang::React { "className" } else { "class" };
|
||||
|
||||
Reference in New Issue
Block a user