Type: mojikumi — Line-end Punctuation Half Width for Japanese punctuation

Japanese full-width punctuation is half a mark and half a space (an
opening bracket's space before it; a closing bracket's, comma's or full
stop's after it). Set solid, 」「 or 。」 show a full em of air between
the marks, and a line ending in 。 stops half an em short of the edge.

- `ParaStyle.mojikumi` (Paragraph panel › Mojikumi): None, or Line-end
  Punctuation Half Width, which (JLREQ 3.1.4) lets consecutive
  punctuation share one half-em space — a closing mark followed by
  punctuation loses the space after it, an opening bracket after another
  the space before it — and sets a closing mark ending a line half
  width. Only full-width marks (advance an em) are touched, never inside
  a tate-chu-yoko block; vertical type the same way down the column.
- New type (`text.create`, Area/Path Type) takes Line-end Punctuation
  Half Width, the usual default of Japanese layout; documents from
  before it and imported text keep None (written only when set, so
  older files and older apps are unaffected). `text.setFormat
  {mojikumi: "none"|"lineEndHalf"}`.
- Builds on #344: on justified lines the room it frees is spread
  between the characters.
- Catalog rows in ja and zh-hant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
imachus
2026-10-08 00:46:07 +09:00
co-authored by Claude Opus 5.5
parent 5623f90ef7
commit a92905a816
13 changed files with 203 additions and 11 deletions
+2
View File
@@ -402,6 +402,8 @@ fn text_create(s: &mut Session, p: &Value) -> Result<Value> {
let text = str_param(p, "text").unwrap_or("");
let mut t = TextObject::point(Point::new(x, y), text, new_type_style(s, p));
t.vertical = p.get("vertical").and_then(Value::as_bool).unwrap_or(false);
// New type is composed the Japanese way (punctuation spacing), as the Type tools set it.
t.para.mojikumi = vectorcraft_doc::Mojikumi::LineEndHalf;
if let Some(a) = p.get("area") {
let w = f64_or(a, "width", 200.0);
let h = f64_or(a, "height", 100.0);
+13 -1
View File
@@ -24,7 +24,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, hyphenate?: bool}",
"{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, hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing)}",
has_doc,
set_format
),
@@ -101,11 +101,20 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
"spaceBefore",
"spaceAfter",
"hyphenate",
"mojikumi",
];
if !keys.iter().any(|k| p.get(*k).is_some()) {
return Err(bad(C, "nothing to change"));
}
let (position, small_caps) = super::docsetup::script_params(p, &s.doc()?.doc.setup, C)?;
let mojikumi = match p.get("mojikumi") {
None => None,
Some(v) => Some(match v.as_str() {
Some("none") => vectorcraft_doc::Mojikumi::None,
Some("lineEndHalf") => vectorcraft_doc::Mojikumi::LineEndHalf,
_ => return Err(bad(C, "`mojikumi` must be \"none\" or \"lineEndHalf\"")),
}),
};
s.edit("Character", |d, _| {
for id in &ids {
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
@@ -161,6 +170,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = flag("hyphenate") {
para.hyphenate = v;
}
if let Some(v) = mojikumi {
para.mojikumi = v;
}
super::typecmd::refresh_bounds(t);
}
Ok(())
+1
View File
@@ -378,6 +378,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
wrap: Vec::new(),
cached_bounds: None,
};
t.para.mojikumi = vectorcraft_doc::Mojikumi::LineEndHalf;
refresh_bounds(&mut t);
let id = s.edit(if on_path { "Type on a Path" } else { "Area Type" }, |d, sel| {
let (par, idx, _) = d.position(pid).ok_or(EngineError::NoNode(pid))?;
+25
View File
@@ -407,6 +407,31 @@ fn reshape_area(s: &mut Session, p: &Value) -> Result<Value> {
mod area_tests {
use super::*;
#[test]
fn new_type_is_composed_with_line_end_half_width_punctuation() {
use vectorcraft_doc::Mojikumi;
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": "雅楽。"})).unwrap()["id"].as_u64().unwrap();
let para = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.para.clone(),
_ => panic!("text"),
};
assert_eq!(para(&s).mojikumi, Mojikumi::LineEndHalf);
s.execute("select.set", &json!({"ids": [id]})).unwrap();
assert!(s.execute("text.setFormat", &json!({"mojikumi": "everything"})).is_err());
s.execute("text.setFormat", &json!({"mojikumi": "none"})).unwrap();
assert_eq!(para(&s).mojikumi, Mojikumi::None);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(para(&s).mojikumi, Mojikumi::LineEndHalf);
// Saved only when set; documents from before it read as None.
let json = serde_json::to_value(para(&s)).unwrap();
assert_eq!(json["mojikumi"], "lineEndHalf");
let old: vectorcraft_doc::ParaStyle = serde_json::from_value(json!({"justify": "Left"})).unwrap();
assert_eq!(old.mojikumi, Mojikumi::None);
assert!(serde_json::to_value(&old).unwrap().get("mojikumi").is_none());
}
#[test]
fn vertical_point_type_keeps_its_anchor_on_the_column_centre_line() {
let mut s = Session::new();