Type: Kinsoku Set — Hard (today's set), Soft and None (#633)

Paragraph panel › Kinsoku Set (with the East Asian options) and `text.setFormat {kinsoku}`, as
decided in #633:

- Hard: today's set, so existing documents don't change; the default, and not saved.
- Soft: as Hard, except that 々, the prolonged sound mark ー (JLREQ cl-10) and small kana (cl-11)
  may start a line: JLREQ's level 3 line-breaking rules (Appendix C.3). Half-width forms aren't in
  those classes and stay off the line start.
- None: no kinsoku.

The setting reaches every break the composers consider (greedy, every-line and hyphenation
points). Reset Panel returns to Hard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
imachus
2026-10-09 17:26:37 +09:00
co-authored by Claude Opus 5.5
parent 24c77e9ddf
commit 806e543fd1
17 changed files with 233 additions and 31 deletions
+14 -1
View File
@@ -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), 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), 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} (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), 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} (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
),
@@ -218,6 +218,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
"spaceAfter",
"hyphenate",
"mojikumi",
"kinsoku",
"direction",
"leadingModel",
"charAlign",
@@ -245,6 +246,15 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
_ => return Err(bad(C, "`mojikumi` must be \"none\" or \"lineEndHalf\"")),
}),
};
let kinsoku = match p.get("kinsoku") {
None => None,
Some(v) => Some(match v.as_str() {
Some("none") => vectorcraft_doc::Kinsoku::None,
Some("hard") => vectorcraft_doc::Kinsoku::Hard,
Some("soft") => vectorcraft_doc::Kinsoku::Soft,
_ => return Err(bad(C, "`kinsoku` must be \"none\", \"hard\" or \"soft\"")),
}),
};
let direction = match p.get("direction") {
None => None,
Some(v) => Some(match v.as_str() {
@@ -333,6 +343,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = mojikumi {
para.mojikumi = v;
}
if let Some(v) = kinsoku {
para.kinsoku = v;
}
if let Some(v) = direction {
para.direction = v;
}
+30
View File
@@ -642,6 +642,36 @@ fn reshape_area(s: &mut Session, p: &Value) -> Result<Value> {
mod area_tests {
use super::*;
#[test]
fn kinsoku_set_is_set_per_paragraph_and_saved_only_when_not_hard() {
use vectorcraft_doc::Kinsoku;
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": 10, "text": "あカッ", "area": {"width": 40, "height": 100}})).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"),
};
// New type: Hard, today's set.
assert_eq!(para(&s).kinsoku, Kinsoku::Hard);
assert!(serde_json::to_value(para(&s)).unwrap().get("kinsoku").is_none());
s.execute("select.set", &json!({"ids": [id]})).unwrap();
assert!(s.execute("text.setFormat", &json!({"kinsoku": "strict"})).is_err());
s.execute("text.setFormat", &json!({"kinsoku": "soft"})).unwrap();
assert_eq!(para(&s).kinsoku, Kinsoku::Soft);
assert_eq!(serde_json::to_value(para(&s)).unwrap()["kinsoku"], "soft");
s.execute("text.setFormat", &json!({"kinsoku": "none"})).unwrap();
assert_eq!(para(&s).kinsoku, Kinsoku::None);
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(para(&s).kinsoku, Kinsoku::Hard);
// Documents from before it read as Hard.
let old: vectorcraft_doc::ParaStyle = serde_json::from_value(json!({"justify": "Left"})).unwrap();
assert_eq!(old.kinsoku, Kinsoku::Hard);
}
#[test]
fn burasagari_is_set_per_paragraph_and_saved_only_when_on() {
use vectorcraft_doc::Burasagari;