mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-06 18:51:26 +00:00
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:
1 parent
e847b2e5e3
commit
2d6dff96cc
3 files changed
+77
-10
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Layout and pagination parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** trivial (a missing font is drawn in an installed metric twin before any other substitute: Arial for Helvetica on Windows, Tinos before macOS Times for Times New Roman); previously trivial (layout work bounds: header-row repeats budgeted per layout, cached picture outlines charged to the decode budget, linked-box chain widths exact up to a work cap, #3867); previously trivial (the line between columns runs as tall as the section's text, so columns for selected text have it, #5178); previously trivial (a table row that would leave a cell empty on a page that already holds something, its picture or first line not fitting there, moves on whole instead of leaving the other cells' text beside the empty cell, #4272); previously trivial (a middle or bottom cell of a row split across pages lines up on the first page); previously trivial (tab leader dots spaced and placed as in Word); previously trivial (a level above that hasn't counted yet takes its first number at the first deeper item); previously trivial (paragraph side borders lie beyond their distance from the text, #5036); previously trivial (with a header taller than the top margin, Margin-anchored drawings, header-cell pins and header-table obstacles use the rendered body top; TopMargin and InsideMargin keep their declared band, #2485; previously trivial: connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (field results take the `\* DollarText`, `\* Hex` and `\* ArabicDash` switches, #4226; previously trivial: a cell spanning columns shares its extra minimum width by the columns' preferred widths, equally in automatic tables, #1883; previously trivial: continuous sections one after another go onto new pages instead of running off the page; empty marks after another empty section mark take a line each until the page is full, then stay at its bottom drawing nothing, not even a list label; an empty mark that takes no room draws no list label either; a kept-together paragraph is measured against the page it would move to, so a tall different first-page header no longer splits it, #3907; previously trivial: a justified line ending in a manual line break is stretched to the margin unless `w:doNotExpandShiftReturn` is set, #3924; previously trivial: a no-break space is as wide as a space, #4192; previously trivial: a bookmark or other zero-width marker is no line-break opportunity, #4193; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: Preserve grapheme edit/wrap boundaries across independently drawn font/style pieces; Exact-spacing table contact uses the specified line height rather than paragraph-mark metrics (#3977); previously trivial: cu20we: restrict pre-spacing float origins to paragraph anchors; document cap helpers and regressions; previously trivial: cu20we: outline caps; previously trivial: cu20we: box float paragraph origin; previously trivial: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: an empty paragraph whose mark is hidden shows nothing, not even its list number, which is still counted, #3982; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: symbol fonts keep their line height without the font; previously trivial: an equation-array row without `&` is centred, #2617; previously trivial: a superscript or subscript sizes its line at the full font size, without its raise or drop, as Word does; previously trivial: an object dragged near the bottom of a page lands where it is dropped, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: justified lines shrink their spaces by Word's measured limits; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: a click below a floating table goes to the text beside or after it, not to a cell of its last row, #2038; previously trivial: shapes drawn with their gradients and transparency, #1500; previously trivial: paragraphs above a float's anchor wrap around it, also when that pushes the anchor onto the next page, #2048; previously trivial: triple borders draw three lines, #1740; previously trivial: the caret, clicks and selection edges fall between the letters of a ligature, #1974; previously trivial: a word after a slash wraps wholLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** trivial (行頭禁則 also covers the small kana and the prolonged sound mark at ordinary break opportunities, as JIS X 4051 §4 specifies, inferred from the standard and synthetic tests rather than checked in Word); previously trivial (a missing font is drawn in an installed metric twin before any other substitute: Arial for Helvetica on Windows, Tinos before macOS Times for Times New Roman); previously trivial (layout work bounds: header-row repeats budgeted per layout, cached picture outlines charged to the decode budget, linked-box chain widths exact up to a work cap, #3867); previously trivial (the line between columns runs as tall as the section's text, so columns for selected text have it, #5178); previously trivial (a table row that would leave a cell empty on a page that already holds something, its picture or first line not fitting there, moves on whole instead of leaving the other cells' text beside the empty cell, #4272); previously trivial (a middle or bottom cell of a row split across pages lines up on the first page); previously trivial (tab leader dots spaced and placed as in Word); previously trivial (a level above that hasn't counted yet takes its first number at the first deeper item); previously trivial (paragraph side borders lie beyond their distance from the text, #5036); previously trivial (with a header taller than the top margin, Margin-anchored drawings, header-cell pins and header-table obstacles use the rendered body top; TopMargin and InsideMargin keep their declared band, #2485; previously trivial: connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (field results take the `\* DollarText`, `\* Hex` and `\* ArabicDash` switches, #4226; previously trivial: a cell spanning columns shares its extra minimum width by the columns' preferred widths, equally in automatic tables, #1883; previously trivial: continuous sections one after another go onto new pages instead of running off the page; empty marks after another empty section mark take a line each until the page is full, then stay at its bottom drawing nothing, not even a list label; an empty mark that takes no room draws no list label either; a kept-together paragraph is measured against the page it would move to, so a tall different first-page header no longer splits it, #3907; previously trivial: a justified line ending in a manual line break is stretched to the margin unless `w:doNotExpandShiftReturn` is set, #3924; previously trivial: a no-break space is as wide as a space, #4192; previously trivial: a bookmark or other zero-width marker is no line-break opportunity, #4193; previously trivial: Arabic list labels: `arabicAlpha` and `arabicAbjad` draw the 28 Arabic letters (alphabetical and abjad order), doubling past the last letter; Hebrew, Hindi, Thai and Vietnamese still show decimal (`doc::section::tests::arabic_letters_count_in_both_orders`); previously trivial: Preserve grapheme edit/wrap boundaries across independently drawn font/style pieces; Exact-spacing table contact uses the specified line height rather than paragraph-mark metrics (#3977); previously trivial: cu20we: restrict pre-spacing float origins to paragraph anchors; document cap helpers and regressions; previously trivial: cu20we: outline caps; previously trivial: cu20we: box float paragraph origin; previously trivial: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: an empty paragraph whose mark is hidden shows nothing, not even its list number, which is still counted, #3982; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: symbol fonts keep their line height without the font; previously trivial: an equation-array row without `&` is centred, #2617; previously trivial: a superscript or subscript sizes its line at the full font size, without its raise or drop, as Word does; previously trivial: an object dragged near the bottom of a page lands where it is dropped, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; previously trivial: justified lines shrink their spaces by Word's measured limits; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: a click below a floating table goes to the text beside or after it, not to a cell of its last row, #2038; previously trivial: shapes drawn with their gradients and transparency, #1500; previously trivial: paragraphs above a float's anchor wrap around it, also when that pushes the anchor onto the next pLine truncated
|
||||
|
||||
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
|
||||
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
|
||||
@@ -22,10 +22,9 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
|
||||
|
||||
| Feature | State | Evidence / gap | Hours |
|
||||
|---|---|---|---|
|
||||
| 2026-10-16 | trivial | Split-cell content retains its resolved top margin and border band on continuation pages, matching original Word Mac16.113.2 controls. Synthetic regressions cover margins, borders, line spacing and image frames. |
|
||||
| Hint-reliant CJK glyph outlines (DFKai-SB / 標楷體) | 🟡 | Required TrueType interpreter runs at units-per-em size; local Windows glyph-bounds regression and synthetic DOCX page/window comparison pass. Other platforms and hint-reliant families remain unmeasured; no native Word comparison. A font asking for a twilight zone over 1,024 points or a stack over 4,096 values, or a glyph past 256 KiB of hinting buffer, is drawn unhinted; on desktop, once at least three of a face's hinted draws have taken over 50 ms and those slow draws are at least 1/16 of its hinted draws, its later glyphs are drawn unhinted (#3692). | — |
|
||||
| Subpoint table grid columns | 🟡 | Preserve positive grid widths rather than imposing a text-box minimum; ECMA-376 §17.4.16. Native Word for Mac 16.113.2 build26092012 confirms narrow trailing grid units. `layout::tests::subpoint_grid_keeps_widths`, `tiny_grid_scales_to_width`. Tiny totals normalize without overflowing the scale factor. Zero and invalid widths retain their fallback. Border painting remains separate. | n/a |
|
||||
| Line breaking (UAX #14 with Word's exceptions: no break after `/`, a break after `-` before a digit) | ✅ | `layout::tests::no_line_break_right_after_a_slash`, `layout::tests::a_hyphen_before_digits_is_a_break_opportunity_like_word`, `layout::tests::a_bookmark_is_no_line_break_opportunity` | — |
|
||||
| Line breaking (UAX #14 with Word's exceptions: no break after `/`, a break after `-` before a digit) | ✅ | `layout::tests::no_line_break_right_after_a_slash`, `layout::tests::a_hyphen_before_digits_is_a_break_opportunity_like_word`, `layout::tests::a_bookmark_is_no_line_break_opportunity; the paragraph's kinsoku (JIS X 4051 §4 行頭禁則) lists the small kana (っゃゕヶ, the katakana phonetic extensions ㇰ…ㇿ, the halfwidth small kana ァ…ッ) and the prolonged sound mark ー/ー as non-starters — CJ/NS in UAX #14, where the break before them is only a tailoring choice — as JIS X 4051 §4 specifies, inferred from the standard and the synthetic tests named here rather than checked in Word. The rule covers ordinary break opportunities only: a line too narrow for any of them takes the emergency break in `para::break_lines`, which can still start with one, as the module doc records. `layout::tests::a_line_never_starts_with_a_small_kana_or_the_prolonged_sound_mark`, `layout::kinsoku::tests::no_start_holds_every_small_kana_and_the_prolonged_sound_mark` | — |
|
||||
| A break before a slash that starts a word (" /word" wraps whole; not after a no-break space) | 🟡 | `layout::tests::a_word_after_a_slash_wraps_whole`, `layout::tests::a_no_break_space_before_a_slash_still_holds`; behaviour from the #2963 report, not compared with Word yet | — |
|
||||
| Narrow table cells | ✅ | `layout::tests::narrow_cells_keep_their_width`: usable widths below 12 pt wrap without expansion; oversized glyphs still make progress | 0 |
|
||||
| Justification, Word 2013+ space shrinking (compat mode 15) | ✅ | #109 | — |
|
||||
@@ -304,6 +303,7 @@ layout suite are pending; this is not a new Word/native-IME acceptance claim.
|
||||
| 2026-10-22 | trivial | Layout work bounds for hostile documents: linked-box chain widths, repeated header items, `gridAfter`, picture decodes (#3692) |
|
||||
| 2026-10-22 | trivial | Hostile fonts (#3692): hint-reliant fonts with huge hinting zones are drawn unhinted, and a face draws its later glyphs unhinted once at least three of its hinted draws took over 50 ms and those are at least 1/16 of its hinted draws; a colour glyph's gradient stops and clip paths share one budget |
|
||||
| 2026-10-22 | trivial | Group connector rerouting preserves displayed free and unresolved endpoints under nonuniform scaling while resolved ends keep their connection-site path; degenerate dimensions and unrepresentable fallback display spans are guarded; synthetic model/engine coverage only, readiness estimates unchanged |
|
||||
| 2026-10-21 | trivial | 行頭禁則 (JIS X 4051 §4): the paragraph's kinsoku also forbids a line starting with a small kana (ぁ…ヶ, the katakana phonetic extensions ㇰ…ㇿ, the halfwidth small kana ァ…ッ) or the prolonged sound mark ー/ー, at ordinary break opportunities, as JIS X 4051 §4 specifies (inferred from the standard and synthetic tests, not checked in Word); the emergency break for a line too narrow for any of the prohibited characters or ordinary break opportunities is unchanged and can still start with one; kinsoku off and word wrap off are unchanged |
|
||||
| 2026-10-21 | trivial | A word with no break opportunity in it, laid out beside a floating object, no longer lays out quadratically: every word is measured once, before the line is broken (#3412) |
|
||||
| 2026-10-23 | trivial | An empty paragraph whose mark is hidden is laid out as nothing, like a deleted one: no room, no list number (still counted, as in Word) and no shading, in the body and in table cells, headers and text boxes (#3982) |
|
||||
| 2026-10-25 | trivial | A bookmark, comment anchor or other zero-width marker is no line-break opportunity: a word that doesn't fit after one no longer leaves a blank line, and one inside a word no longer splits it (#4193) |
|
||||
@@ -327,6 +327,7 @@ layout suite are pending; this is not a new Word/native-IME acceptance claim.
|
||||
| 2026-10-19 | trivial | An object dragged near the bottom of a page lands where it is dropped: its paragraph's place is measured with the object already there, not at the paragraph's top, where it could push the paragraph onto the next page (#3169) |
|
||||
| 2026-10-18 | trivial | A word after a slash (" /word") wraps whole instead of splitting between two letters (#2963) |
|
||||
| 2026-10-19 | trivial | #2964: a centred or end-aligned list label after a leading page break uses its text line's alignment. Synthetic LTR/RTL numbered/bulleted controls and the issue's DOCX export; broader pagination gaps and readiness estimates unchanged |
|
||||
| 2026-10-16 | trivial | Split-cell content retains its resolved top margin and border band on continuation pages, matching original Word Mac16.113.2 controls. Synthetic regressions cover margins, borders, line spacing and image frames. |
|
||||
| 2026-10-15 | trivial | An at-least table row with less room than its minimum on a page that holds something starts the next page whole, as checked in Word (#1742); the taller-than-a-page case (it then splits from the top of that page) and repeated header rows (they go with it) follow the issue and synthetic tests, not a Word check |
|
||||
| 2026-10-16 | trivial | Table row keep-next uses the first paragraph of the first cell, including inherited flags. Kept row chains start on a fresh page when the following row cannot start below them. Synthetic Word export covers mixed-cell flags, multiple paragraphs and kept prefixes before oversized followers. |
|
||||
| 2026-10-16 | trivial | Include table-cell headings in regenerated TOCs with full target paths |
|
||||
|
||||
Reference in new issue
Block a user