Layout: 行頭禁則 lists small kana and the prolonged sound mark (JIS X 4051 §4), with the emergency-break gap documented (#2558)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
midasdf
2026-11-04 13:31:39 +01:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e847b2e5e3
commit 2d6dff96cc
3 changed files with 77 additions and 10 deletions
+48 -7
View File
@@ -1,9 +1,13 @@
//! Asian typography line-breaking rules (the paragraph's `kinsoku` and `word_wrap` flags), applied
//! on top of the Unicode line-break opportunities (UAX #14).
//!
//! - Kinsoku on (the default): no line starts with closing East Asian punctuation (、。,)」…)
//! and none ends with opening punctuation ((「【…). UAX #14 already forbids most of these
//! breaks; the lists make sure of it for the standard Japanese/Chinese sets.
//! - Kinsoku on (the default): no ordinary break opportunity falls before closing East Asian
//! punctuation (、。,)」…), a small kana (っゃゕヶ), the prolonged sound mark (ー ー) or an
//! iteration mark, and none falls after opening punctuation ((「【…). UAX #14 already forbids
//! most of these breaks, and classes the small kana and the prolonged sound mark CJ/NS, whose
//! break is only a tailoring choice; the lists make sure of it for the standard Japanese/Chinese
//! sets. A line too narrow for any opportunity still takes the emergency break in
//! `para::break_lines`, which can put one of these characters at the start of the line.
//! - Kinsoku off: a line may break between any two East Asian characters, punctuation included.
//! - Word wrap off: Latin words may break at any character.
//! - Automatic spacing (`autoSpaceDE`, `autoSpaceDN`): which neighbouring characters get room
@@ -14,10 +18,15 @@
use unicode_segmentation::UnicodeSegmentation;
/// East Asian characters a line may not start with: closing brackets and quotes, the
/// ideographic comma and full stop, fullwidth and halfwidth punctuation, the middle dot,
/// iteration and sound marks.
const NO_START: &str = "、。,.・:;?!゛゜ヽヾゝゞ々〻〉》」』】〕〗〙〛〞〟)]}⦆。」、・゙゚%¢";
/// 行頭禁則: East Asian characters a line may not start with, grouped as JIS X 4051 §4 and
/// Word's default 禁則処理 do:
/// - 小書きの仮名 (small kana): hiragana `ぁぃぅぇぉっゃゅょゎゕゖ`, katakana `ァィゥェォッャュョヮヵヶ`,
/// the katakana phonetic extensions (U+31F0…U+31FF) and the halfwidth small kana `ァィゥェォャュョッ`;
/// - 長音記号 (the prolonged sound mark): `ー` and its halfwidth form `ー`;
/// - 繰り返し記号 (iteration marks, with the sound marks): `゛゜ヽヾゝゞ々〻゙゚`;
/// - 閉じ括弧類 and the rest: closing brackets and quotes, the ideographic comma and full stop,
/// the fullwidth and halfwidth punctuation, the middle dot, `%` and `¢`.
const NO_START: &str = "ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿァィゥェォャュョッーー゛゜ヽヾゝゞ々〻゙゚、。,.・:;?!〉》」』】〕〗〙〛〞〟)]}⦆。」、・%¢";
/// East Asian characters a line may not end with: opening brackets and quotes, currency signs.
const NO_END: &str = "〈《「『【〔〖〘〚〝([{⦅「$£¥";
@@ -149,4 +158,36 @@ mod tests {
let any = breaks("Hello café", true, false);
assert_eq!(any, ["e", "l", "l", "o", "c", "a", "f", "é"]);
}
#[test]
fn kinsoku_no_line_starts_with_a_small_kana_or_the_prolonged_sound_mark() {
// A break opportunity sits right before each of these (after a kanji, or after a 、);
// JIS X 4051 §4 行頭禁則 drops it, as Word's default 禁則処理 does.
let ja = "漢っ、ゃ漢ー漢ッ漢ャ漢ゕ漢ヶ漢ㇺ漢ッ漢ャ漢ー";
let non_starters = ["っ", "ゃ", "ー", "ッ", "ャ", "ゕ", "ヶ", "ㇺ", "ッ", "ャ", "ー"];
let on = breaks(ja, true, true);
for c in non_starters {
assert!(!on.iter().any(|s| s == c), "kinsoku: no line starts with {c}: {on:?}");
}
// Kinsoku off: a line may break between any two East Asian characters, these included.
let off = breaks(ja, false, true);
for c in non_starters {
assert!(off.iter().any(|s| s == c), "kinsoku off: a line may start with {c}: {off:?}");
}
// Text with no East Asian characters breaks exactly as before either way.
let en = "Hello wonderful (world), 50%.";
assert_eq!(breaks(en, true, true), breaks(en, false, true));
assert_eq!(breaks(en, true, true), ["w", "(", "5"]);
}
/// 行頭禁則: the small kana and the prolonged sound mark are CJ/NS in UAX #14, where the break
/// before them is forbidden only optionally — so the list itself must hold every one of them.
#[test]
fn no_start_holds_every_small_kana_and_the_prolonged_sound_mark() {
for c in "ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿァィゥェォャュョッーー".chars()
{
assert!(super::is_east_asian(c), "U+{:04X} {c} is East Asian text", c as u32);
assert!(super::NO_START.contains(c), "NO_START (行頭禁則) must hold U+{:04X} {c}", c as u32);
}
}
}
+25
View File
@@ -7328,6 +7328,31 @@ fn kinsoku_keeps_a_manual_line_break_before_a_closing_bracket() {
assert_eq!(lines[1].0, bracket, "the second line starts at the bracket: {lines:?}");
}
/// 行頭禁則 (JIS X 4051 §4) end to end: a paragraph wrapping in a narrow column never starts a
/// line with a small kana or the prolonged sound mark, even where the break opportunity that
/// would put one there sits right after a kanji.
#[test]
fn a_line_never_starts_with_a_small_kana_or_the_prolonged_sound_mark() {
let text = "漢っ漢ゃ漢ー漢ッ漢ャ漢ゕ漢ヶ漢ㇺ漢ッ漢ャ漢ー漢";
let mut d = Document::from_text(text);
d.last_section.page_w = 72.0 + 72.0 + 72.0; // 72 pt of text: a few East Asian characters
let l = lay(&d);
let mut lines = Vec::new();
for p in &l.pages {
for it in &p.items {
if let Placed::Lines { para, l0, l1, .. } = it {
lines.extend(para.lines[*l0..*l1].iter().map(|line| (line.start, line.stop)));
}
}
}
assert!(lines.len() > 1, "the paragraph wraps: {lines:?}");
let non_starters = "ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿァィゥェォャュョッーー";
for &(start, _) in &lines {
let first = text.get(start..).and_then(|t| t.chars().next()).unwrap_or_default();
assert!(!non_starters.contains(first), "a line must not start with {first}: {lines:?}");
}
}
/// #332: a rotated floating shape keeps text clear of its rotated bounds (it reaches further at
/// the side), its frame stays put (rotation is about the centre) and it is drawn turned; hit
/// testing follows the turned shape, not its frame.
File diff suppressed because one or more lines are too long