mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Type: Digits — Default, Arabic, Hindi and Farsi numeral systems
Adds the Character panel menu's Digits option (with the Middle Eastern /
Indic options, beside Paragraph Direction in the Paragraph panel) as a
character attribute: CharStyle::digits, saved only when set, with
text.setFormat {digits} and text.setRangeStyle {id, start, end, digits}
("default" | "arabic" | "hindi" | "farsi"; a bad value is an error).
Shaping draws every digit of the Western, Arabic-Indic (U+0660–U+0669)
and Eastern Arabic-Indic (U+06F0–U+06F9) sets in the chosen system,
whatever was typed, before font fallback, so a font without the
Arabic-Indic digits gets a fallback for them. The text, its clusters,
the caret, selection and search stay as typed, as with All Caps; SVG
export writes the digits as drawn.
Catalog rows for Digits, Arabic, Hindi and Farsi in the ten translated
languages. Docs: docs/mcp.md (Digits), type-parity.md (digit types
done; Character Direction, kashidas, composers and the split caret
still missing), target-app-parity.md and gaps.md.
Behaviour from the public documentation (a character-level option with
the values Arabic, Hindi and Farsi in the Character panel menu):
https://helpx.adobe.com/indesign/desktop/language-and-proofing/arabic-and-hebrew/choose-digit-types.html
Tests: doc (the mapping and serialization), text (glyphs of "page 3" in
Hindi/Farsi equal the typed Arabic-Indic digits' on the typed clusters;
skips without a font covering the three sets), engine (setFormat,
setRangeStyle, undo, error), svg (digits written as drawn), ui-egui
(the menu shows with the Indic options).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Fable 5.1
parent
d19fd80390
commit
16af3cc3fa
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character / Paragraph",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
|
||||
has_doc,
|
||||
set_format
|
||||
),
|
||||
@@ -231,6 +231,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"leadingModel",
|
||||
"charAlign",
|
||||
"proportionalMetrics",
|
||||
"digits",
|
||||
"burasagari",
|
||||
"composer",
|
||||
];
|
||||
@@ -247,6 +248,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let (position, small_caps) = super::docsetup::script_params(p, &s.doc()?.doc.setup, C)?;
|
||||
let char_align = super::textedit::char_align_param(p, C)?;
|
||||
let digits = super::textedit::digits_param(p, C)?;
|
||||
let mojikumi = match p.get("mojikumi") {
|
||||
None => None,
|
||||
Some(v) => Some(match v.as_str() {
|
||||
@@ -333,6 +335,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = flag("proportionalMetrics") {
|
||||
st.proportional_metrics = v;
|
||||
}
|
||||
if let Some(v) = digits {
|
||||
st.digits = v;
|
||||
}
|
||||
});
|
||||
let span = super::typecmd::para_span(range, t);
|
||||
t.edit_paras(span, |para| {
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs}",
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
@@ -209,6 +209,20 @@ pub(crate) struct CharChange {
|
||||
small_caps: Option<Option<f64>>,
|
||||
char_align: Option<vectorcraft_doc::CharAlign>,
|
||||
proportional_metrics: Option<bool>,
|
||||
digits: Option<vectorcraft_doc::Digits>,
|
||||
}
|
||||
|
||||
/// `digits: "default"|"arabic"|"hindi"|"farsi"` → the numeral system the digits are drawn in.
|
||||
pub(crate) fn digits_param(p: &Value, cmd: &str) -> Result<Option<vectorcraft_doc::Digits>> {
|
||||
use vectorcraft_doc::Digits;
|
||||
let Some(v) = p.get("digits").filter(|v| !v.is_null()) else { return Ok(None) };
|
||||
Ok(Some(match v.as_str() {
|
||||
Some("default") => Digits::Default,
|
||||
Some("arabic") => Digits::Arabic,
|
||||
Some("hindi") => Digits::Hindi,
|
||||
Some("farsi") => Digits::Farsi,
|
||||
_ => return Err(bad(cmd, "`digits` must be \"default\", \"arabic\", \"hindi\" or \"farsi\"")),
|
||||
}))
|
||||
}
|
||||
|
||||
/// `features: ["dlig", "-liga", …]` → the canonical tag list (differences from the defaults).
|
||||
@@ -293,6 +307,7 @@ impl CharChange {
|
||||
small_caps,
|
||||
char_align: char_align_param(p, cmd)?,
|
||||
proportional_metrics: flag("proportionalMetrics"),
|
||||
digits: digits_param(p, cmd)?,
|
||||
};
|
||||
if c.size.is_some_and(|v| v <= 0.0) {
|
||||
return Err(bad(cmd, "size must be positive"));
|
||||
@@ -322,6 +337,7 @@ impl CharChange {
|
||||
&& self.small_caps.is_none()
|
||||
&& self.char_align.is_none()
|
||||
&& self.proportional_metrics.is_none()
|
||||
&& self.digits.is_none()
|
||||
}
|
||||
|
||||
fn localize(&mut self, t: &TextObject, cmd: &str) -> Result<()> {
|
||||
@@ -411,6 +427,9 @@ impl CharChange {
|
||||
if let Some(v) = self.proportional_metrics {
|
||||
st.proportional_metrics = v;
|
||||
}
|
||||
if let Some(v) = self.digits {
|
||||
st.digits = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -887,6 +887,40 @@ mod area_tests {
|
||||
assert!(!serde_json::from_value::<vectorcraft_doc::CharStyle>(old).unwrap().proportional_metrics);
|
||||
}
|
||||
|
||||
/// Digits (Character panel menu, Middle Eastern options): a character attribute, saved only
|
||||
/// when set; the text keeps the digits that were typed.
|
||||
#[test]
|
||||
fn digits_are_a_character_attribute_saved_only_when_set() {
|
||||
use vectorcraft_doc::Digits;
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "صفحة 12"})).unwrap()["id"].as_u64().unwrap();
|
||||
let runs = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.clone(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
assert_eq!(runs(&s)[0].style.digits, Digits::Default);
|
||||
assert!(serde_json::to_value(&runs(&s)[0].style).unwrap().get("digits").is_none());
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
for (key, want) in [("hindi", Digits::Hindi), ("farsi", Digits::Farsi), ("arabic", Digits::Arabic), ("default", Digits::Default)] {
|
||||
s.execute("text.setFormat", &json!({"digits": key})).unwrap();
|
||||
assert!(runs(&s).iter().all(|r| r.style.digits == want));
|
||||
let saved = serde_json::to_value(&runs(&s)[0].style).unwrap();
|
||||
assert_eq!(saved.get("digits").and_then(|v| v.as_str()), (want != Digits::Default).then_some(key));
|
||||
// The text is as typed: only the glyphs change.
|
||||
assert_eq!(runs(&s).iter().map(|r| r.text.as_str()).collect::<String>(), "صفحة 12");
|
||||
}
|
||||
s.execute("text.setFormat", &json!({"digits": "hindi"})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(runs(&s)[0].style.digits, Digits::Default);
|
||||
let err = s.execute("text.setFormat", &json!({"digits": "roman"})).unwrap_err().to_string();
|
||||
assert!(err.contains("`digits` must be"), "{err}");
|
||||
// Characters selected with the Type tool: only the range takes it (صفحة is 8 bytes).
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "start": 9, "end": 11, "digits": "farsi"})).unwrap();
|
||||
let set: Vec<_> = runs(&s).iter().map(|r| (r.text.clone(), r.style.digits)).collect();
|
||||
assert_eq!(set, [("صفحة ".to_string(), Digits::Default), ("12".to_string(), Digits::Farsi)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_type_is_composed_with_line_end_half_width_punctuation() {
|
||||
use vectorcraft_doc::Mojikumi;
|
||||
|
||||
Reference in New Issue
Block a user