Skip to content

Implement automatic hyphenation (§17.15.1.10) - #259

Open
ikashapov wants to merge 10 commits into
nerdy-pro:mainfrom
ikashapov:issue-158-auto-hyphenation
Open

ikashapov wants to merge 10 commits into
nerdy-pro:mainfrom
ikashapov:issue-158-auto-hyphenation

Conversation

@ikashapov

Copy link
Copy Markdown
Contributor

Implement automatic hyphenation (§17.15.1.10)

Closes #158.

What Word does

A document that declares <w:autoHyphenation/> in settings.xml is hyphenated by the consumer: when the next word does not fit on the line, Word cuts it at a point its proofing dictionary allows for the run's language, paints the head with a hyphen and carries the tail to the next line. Three companion settings control it — w:hyphenationZone (§17.15.1.53, default 360 twips; "the amount of whitespace which can be left at the end of a line … before hyphenation should be attempted"), w:consecutiveHyphenLimit (§17.15.1.22, how many lines in a row may end in a hyphen; 0 or omitted is no limit) and w:doNotHyphenateCaps (§17.15.1.27) — and a paragraph exempts itself with w:suppressAutoHyphens (§17.3.1.34), a §17.7.2 cascade property that docDefaults and styles can set too. The language is the run's w:lang (§17.3.2.20), so a German quotation inside an English paragraph is cut as German.

dxpdf parsed none of the four settings and dropped w:lang before it reached a fragment, so a hyphenating document rendered with ragged or over-stretched lines and a different line count from Word.

What this does

Parse → model → resolve. w:autoHyphenation, w:hyphenationZone, w:consecutiveHyphenLimit and w:doNotHyphenateCaps land in HyphenationSettings on DocumentSettings, whose Default carries the spec's omitted values (off, 360 twips, no limit, capitals hyphenated), and are copied onto ResolvedDocument. The zone shares w:defaultTabStop's deserializer, so the universal-measure spellings parse, and 0 is a value — hyphenate on any gap — not an absence (test-files/document_outline.docx declares it).

Language to the fragment. FontProps.hyphenation: Option<HyphenLanguage> is resolved in font_props_from_run from the run's cascaded w:lang/@w:val, beside rtl and for the same reason: it is per run and every text fragment of a run shares it by Rc. An untagged run, or one in a language with no vendored patterns, is never hyphenated — falling back to English was considered and rejected, since it would cut German text at English syllables.

Patterns. hyphenation = "=0.8.4" with default-features = false: Knuth–Liang over the TeX hyph-utf8 pattern sets, pinned exactly because the vendored dictionaries are serialized in that version's bincode schema and the crate's last release is from 2021. No embed_all (82 dictionaries, ~2.8 MB — the #124 wheel-size risk). scripts/make_hyphenation_data.sh, in the shape of make_icu_data.sh, copies four .standard.bincode files out of the pinned crate source into src/i18n/data/hyph/ — en-us, en-gb, de-1996, ru; 385,596 bytes raw, 214,624 deflated — with ATTRIBUTION.md carrying each pattern set's licence (MIT for de-1996 and en-gb, Kuiken's permissive notice for en-us, LPPL 1.2+ for ru). src/i18n/hyphenate.rs maps a tag by primary subtag (every de-* to de-1996, because the crate's only Swiss set is the 1901 orthography; en-GB to the British patterns, every other en to American) and answers opportunities(word, lang) from one OnceLock<Standard> per language.

The fitter. Hyphenation happens in one place, layout::line::fit_lines_in, at the one moment a word is known not to fit. The fitter now works over a Cow<[Fragment]> with a FitWindow (start index and optional line limit) and an optional HyphenationCtx; fit_lines/fit_lines_with_first keep their signatures as wrappers passing a borrowed slice and no context, so a document without the setting takes exactly the old code path and never materialises a copy — pinned by a bit-for-bit test. At overflow, hyphenate_at cuts the word that did not fit only when it is a whole, unshaped, non-footnote-mark word of a run with a dictionary; only when the gap the visible line would leave exceeds the zone (strictly, so zone 0 is "any gap"); only while the consecutive count is under the limit; never an all-caps word under doNotHyphenateCaps; never a token with digits or an interrupted run of letters (don't, hard-nosed). Among the pattern points the longest head whose head + "-" re-measures to fit wins. The head is spliced in place with the new BreakAfter::Hyphenated and its text already ending in U+002D; the tail follows with the word's own break status and hanging trailing space. Emission needs no change: it paints the fragment's own text, and justification already measures from the fragments.

Threading. ParagraphStyle.hyphenation: Option<AutoHyphenation> is Some only when the document setting is on and the paragraph's cascaded w:suppressAutoHyphens is not Some(true); carried on the style so body paragraphs, table cells and split continuations all inherit it. place_paragraph builds the context when a measurer exists; the floats path now fits from an index into the whole vector with max_lines: Some(1) instead of slicing and remapping — identical for every paragraph without the setting, and the only shape that can hold a spliced word.

The continuation re-fit. The remainder of a paragraph split across a page or column is re-placed at the next page's width, and a word cut for the first width may fit whole there; re-fitting the cut fragments would paint hyphen- beside ation on one line. fragments_from_line now runs line::rejoin_hyphenated over the remainder — every Hyphenated head folded back into its tail, re-measured, the tail's break status restored — which is also the argument for a variant on the fragment rather than a flag on the line. The hole was reproduced before the fix by a paragraph flowing from a 66 pt column into a 200 pt one.

What is deliberately left out

Each is stated in a comment at the site that makes the choice.

  • The zone's compatibility-mode dependence. §17.15.1.53's definition and its own example read in opposite directions, and Microsoft's VBA documentation says Word 2013+ in native layout (compatibilityMode 15) ignores the zone altogether. This implements the normative reading with the 360-twip default; dxpdf does not parse w:compat, so Word may hyphenate more than this does, never less. test-files/auto-hyphenation-compat15.docx is kept as a probe for the Word render that settles it.
  • Word's dictionary versus TeX patterns. Cut points can differ per word, and Word's minimum word length and left/right minima are its own (the patterns' are 2/3 for English, 2/2 for German and Russian, so ei-ne is a legal cut here that Word would not make). The fixtures use compounds both agree on; the README cell says so.
  • w:softHyphen is still dropped at parse; a word already cut per cluster by split_oversized_fragments for being wider than its line is not hyphenated (Word would cut it at a syllable); a word glued across a <w:r> boundary, or to a list label's suffix, is left whole; shaped runs (Arabic, Hebrew, RTL) and every script without patterns are never hyphenated; @eastAsia/@bidi tags are not consulted; lines governed by a margin-relative position tab are skipped; the consecutive count restarts on a page continuation; the wheel delta was not measured here (no release build in this environment — the Cargo.toml comment says what to record).
  • Languages: four. Extend LANGUAGES in the regen script when a fixture and a caller justify one, the rule make_icu_data.sh already applies.

How it was verified

Every test was written from the clause first and watched fail, and each commit message records what failed; every new test was mutation-checked against the implementation it guards (the commit messages name the mutations and which tests caught them).

  • Unit: the four settings elements and their defaults (docx::parse::settings, model::types::settings); the resolve passthrough; the tag map, the crate's documented examples, a German compound's seams, Russian syllables, the minima and the boundary contract, and that every vendored dictionary loads (i18n::hyphenate); the run's cascaded w:lang reaching FontProps; nineteen fitter tests over a fixed-advance measurer and a table of cuts — the zone both ways and its strictness, the visible-width gap, longest head, no fitting head, the consecutive limit and its reset, caps, glued words, untagged/shaped/footnote fragments, punctuation and trailing-space handling, the window, the rejoin, and bit-for-bit identity with no context; the §17.3.1.34 cascade in convert.rs; the paragraph layer end to end with the real patterns, with its no-measurer and no-setting controls; and a section-level narrow→wide column paragraph that reproduced the continuation hole before the fix; and the floats path at the paragraph layer — one line per call, each at its own width, a float narrowing the first three lines — which hyphenates at both widths and never leaves an inserted hyphen mid-line, mutation-checked against max_lines: None at its call site (the fitter's own window test builds the window itself and stays green under that mutation; this one does not).
  • Integration (tests/auto_hyphenation.rs) over seven fixtures built by scripts/make_auto_hyphenation_fixtures.py (all pass scripts/verify_docx.py): every cut rejoins to a word of the paragraph and lies at a pattern point of the run's language (the en-US run inside the German paragraph is cut at understand-ing); an inserted hyphen is only ever the last piece of its line; the exempted twin, the 7200-twip zone and the off document insert nothing; no two consecutive lines end in one under limit 1, with the limitless twin as the control that they otherwise do; capitals stay whole only under doNotHyphenateCaps while Kraftfahr-zeug beside them is still cut; the unequal-column paragraph never shows a hyphen mid-line; and document_outline.docx — autoHyphenation on, zone 0, docDefaults suppressAutoHyphens — paints no hyphen its text does not contain (the §17.7.5.2 control).
  • Battery: cargo fmt --check, cargo clippy --all-targets -- -D warnings, RUSTDOCFLAGS="-D warnings" cargo doc --no-deps, cargo test --all --no-fail-fast (skipping the two committed-Go-library freshness tests, which need a release build), cargo check --no-default-features, cargo check --features capi — all green; see the branch report for counts.
  • Corpus before/after: every test-files/*.docx rendered with the upstream-main debug binary and with this branch's, comparing pdfinfo page counts and pdftotext -layout output. 56 documents (49 pre-existing + the 7 new fixtures): no page count changed anywhere, and the text of all 49 pre-existing documents is byte-identical — among them document_outline.docx (autoHyphenation on with a docDefaults exemption), sample-emoji.docx (autoHyphenation w:val="0") and font_scaling.docx (a hyphenationZone with no autoHyphenation). Text differs in exactly five documents, all new and all carrying <w:autoHyphenation/> with a zone that permits cuts: auto-hyphenation-{base,compat15,limit-1,nocaps,columns}. The two new fixtures that must not hyphenate — zone-wide (a 7200-twip zone, wider than the column) and off — render identically to the reference binary, which hyphenates nothing.

`w:autoHyphenation` (§17.15.1.10), `w:hyphenationZone` (§17.15.1.53),
`w:consecutiveHyphenLimit` (§17.15.1.22) and `w:doNotHyphenateCaps`
(§17.15.1.27) land in a new `HyphenationSettings` on `DocumentSettings`,
whose `Default` holds the spec's omitted values: off, 360 twips, no limit,
capitals hyphenated. The zone reuses `DimensionVal`, so the §22.9.2.14
universal-measure spellings parse exactly as they do for `w:defaultTabStop`,
and `0` is read as a value (test-files/document_outline.docx declares it).

Tests were written from the clauses first and watched fail — five of six
against the unchanged parser (the sixth, the universal-measure case, passed
vacuously against the 360 default and was changed to use 720-twip spellings
so it cannot). Nothing consumes the new field yet.
…pro#158)

`ResolvedDocument.hyphenation` is `DocumentSettings.hyphenation` moved
across the resolve seam, beside `default_tab_stop` and
`even_and_odd_headers`, so `paragraph_style_from_props` can read it next
to the cascaded §17.3.1.34 `w:suppressAutoHyphens` it must be combined
with. The six test literals of `ResolvedDocument` gain the field as
`Default::default()`.

Test written first: `resolve_carries_the_hyphenation_settings` sets every
field to a non-default value and was watched fail (E0609, no such field)
before the field existed. Nothing consumes it yet.
…e, ru (nerdy-pro#158)

Adds `hyphenation = "=0.8.4"` with `default-features = false` — no
`embed_all` (all 82 dictionaries, ~2.8 MB, the nerdy-pro#124 wheel-size risk) — and
vendors only four `.standard.bincode` files into src/i18n/data/hyph/ through
scripts/make_hyphenation_data.sh, whose LANGUAGES list is the single source
of truth, in the shape of scripts/make_icu_data.sh. The files are tied to
the pinned crate version (bincode 1.x schema of `Standard`), which the
script reads back from Cargo.toml rather than restating. Raw 385,596 bytes,
214,624 deflated; the wheel delta is deliberately left unmeasured here (no
release build in this environment) and the Cargo.toml comment says so.

src/i18n/hyphenate.rs: `HyphenLanguage` (own ADT, four variants) maps a
§17.3.2.20 tag by primary subtag — every `de-*` to de-1996, since the crate's
only Swiss set is the 1901 orthography; `en-GB` to the British patterns,
every other `en` to American; unknown or absent → None, never hyphenate —
and `opportunities(word, lang)` returns the byte offsets the patterns allow,
from one `OnceLock<Standard>` per language (`Standard` is Send + Sync, so no
thread_local). ATTRIBUTION.md carries the per-language pattern licences
(MIT for de-1996 and en-gb, Kuiken's notice for en-us, LPPL 1.2+ for ru).

Tests first: seven of nine were watched fail against stubs returning None /
empty (the crate's documented examples, the German compound seams, the
Russian syllables, the tag map, the minima, the boundary contract, en-gb's
exception list); the vacuous case (capitalised == lowercase on two empty
vectors) was tightened to require a non-empty answer. The dictionary
contract test was mutation-checked by loading `ru` under
`Language::Ukrainian`, which fails it.
…nary (nerdy-pro#158)

`FontProps.hyphenation: Option<HyphenLanguage>`, set in `font_props_from_run`
from the *run's* cascaded §17.3.2.20 `w:lang/@w:val` — not from the paragraph
mark `resolve_lang_tag` walks, because a German quotation inside an English
paragraph is a run-level fact — with `Locale::first_tag`'s rule that an empty
`@w:val` sets nothing. Only the Latin-script `@w:val` is read; `@eastAsia`
and `@bidi` name scripts the fitter will exclude anyway (CJK never
hyphenates, Arabic is shaped). On `FontProps` for the reason `rtl` is: per
run, shared by `Rc` across every text fragment of the run.

Every other `FontProps` literal (34, mostly test helpers) gets
`hyphenation: None`. Nothing reads the field yet.

Test first: `the_runs_cascaded_lang_picks_its_hyphenation_dictionary` was
watched fail (E0609, no such field) before the field existed; it pins the
four tags, an unknown language, an empty value, an absent `w:lang`.
)

`fit_lines_in` is the fitter over a `Cow<[Fragment]>` with a `FitWindow`
(start index + optional line limit) and an optional `HyphenationCtx`; the
old `fit_lines`/`fit_lines_with_first` keep their signatures as wrappers
passing `Cow::Borrowed`, `FitWindow::ALL` and no context, so every existing
caller and test is unchanged and a document without `w:autoHyphenation`
never materialises a copy.

At the overflow site, before falling back to the last UAX nerdy-pro#14 opportunity,
`hyphenate_at` tries to cut the word that did not fit: only a whole,
unshaped, non-footnote-mark word of a run whose cascaded `w:lang` has a
vendored dictionary, only when the gap the visible line would leave
exceeds the §17.15.1.53 zone (strictly, so zone 0 is "any gap"), only while
the §17.15.1.22 consecutive count is under the limit, and never an all-caps
word under §17.15.1.27. Among the pattern points the longest head whose
`head + "-"` re-measures to fit wins; the head is spliced in place with
`BreakAfter::Hyphenated` (new variant, the engine's own) and its text already
ending in U+002D, the tail inserted after it with the word's own
`break_after` and hanging trailing space, so emission needs no change.
Tokens with digits or interrupted letter runs are not words to the patterns
(`hyphenation_core`). The margin-span ptab case is skipped and says so.

The zone reading is the clause's normative text (gap > zone → attempt), and
the comment at `hyphenate_at` records that Word in native layout
(compatibilityMode 15, unparsed here) may hyphenate more — a Word reference
render is still owed. `FitWindow::max_lines` exists for the floats path,
which fits one line per call and must not leave hyphenated words behind in
a vector it is about to re-fit.

Tests written from the clauses first; twelve were watched fail against the
fitter without the branch while all 23 pre-existing fitter tests stayed
green through the Cow/window refactor. Mutation checks, each restored:
dropping the zone comparison fails the two zone tests; ignoring the
shaped/footnote guards fails those two; never resetting the count fails
the reset test; dropping the glued-to-previous guard fails the run-boundary
test; taking the shortest head fails nine.
nerdy-pro#158)

`ParagraphStyle.hyphenation: Option<AutoHyphenation>` (zone in points,
consecutive limit, caps flag) is `Some` only when the document turns
§17.15.1.10 on *and* the paragraph's cascaded §17.3.1.34
`w:suppressAutoHyphens` is not `Some(true)`; `paragraph_style_from_props`
takes `&HyphenationSettings` beside `default_tab_stop`, and its three
callers pass `ctx.resolved.hyphenation`. Carried on the style so every
`place_paragraph` caller — body, table cells through the stacker, split
continuations — inherits it with no per-caller plumbing.

`place_paragraph` builds the fitter's `HyphenationCtx` from the style, the
measurer and `i18n::hyphenate::opportunities`, only when a measurer exists
(registry-less layout hyphenates nothing, as `split_oversized_fragments`
degrades), and `compute_line_placements` now takes the effective vector as
`&mut Cow` and a `FitWindow`: the floats path fits from `frag_idx` in the
whole vector with `max_lines: Some(1)` instead of slicing and remapping,
which is identical for every paragraph without the setting and the only
shape that can hold a spliced word.

Tests first: the §17.3.1.34 test in convert.rs was watched fail (arity and
missing field), the paragraph-level end-to-end test (real en-US patterns,
6 pt measurer, `hyphen-` / `ation` painted) was watched fail with the
context still `None`, and its two controls (no measurer, no setting) pass
on either side. One test helper's final word had to be given
`BreakAfter::Opportunity` explicitly — `fixture_break_after` calls a bare
word glued, where the production segmenter gives a paragraph's last piece
an opportunity.
…y-pro#158)

The §4 continuation re-fit (`section/layout.rs` → `fragments_from_line` →
`place_paragraph` under `continuation_style`) re-places the rest of a split
paragraph at the next page's or column's width, around that page's floats.
A word hyphenated for the first width may fit whole there, and re-fitting
the already-cut fragments puts `hyphen-` beside `ation` on one line — a
hyphen painted mid-word. `unbreakable_prefix_lines` only guards lines
narrowed on the first page, so the hole was real.

`line::rejoin_hyphenated` folds every `BreakAfter::Hyphenated` head back
into the tail after it — hyphen dropped, text concatenated, width
re-measured through the paragraph's measurer, the tail's own `break_after`
restored — and `fragments_from_line` runs it over the remainder. A tail
whose head ended the emitted segment is left alone: its head is painted.
The `Hyphenated` variant is what makes the undo possible, which is the
argument for a variant on the fragment over a flag on `FittedLine`.

Tests first, all watched fail: `fragments_from_line_rejoins_a_hyphenated_head_with_its_tail`
(remainder had `hyphen-`/`ation`), and the end-to-end
`a_word_cut_in_a_narrow_column_is_whole_again_in_the_wide_one` in
section/mod.rs, which lays a hyphenating paragraph through a 66 pt column
into a 200 pt one and reproduced the predicted `hyphen-` `ation` pair on
the wide column's first line before the fix. Its control twin pins that a
tail whose head was emitted travels untouched, and `rejoin_hyphenated`'s
own test checks text, break status, width and hanging space round-trip.
…ro#158)

scripts/make_auto_hyphenation_fixtures.py builds one justified de-DE
document in a 2 in column under six w:settings variants — the spec defaults,
a 7200-twip zone, zone 0 with consecutiveHyphenLimit 1, zone 0 with
doNotHyphenateCaps, autoHyphenation off, and a compatibilityMode-15 twin
kept as a probe — plus an unequal-column document (§17.6.4) whose one long
paragraph flows from a 2 in column into a 4 in one, the float-free way to
reach the continuation re-fit. Four paragraphs: German prose of compounds
whose seams the de-1996 patterns and Word's dictionary agree on, the same
prose exempted by w:suppressAutoHyphens, KRAFTFAHRZEUG/Kraftfahrzeug behind
varying prefixes, and German prose carrying an en-US run. No authored hyphen
or digit anywhere, so every `-` at a rendered line end is an inserted one.
Every word is narrower than the column, so none takes the per-cluster
over-wide path (the first draft's Donaudampfschifffahrtsgesellschaft did,
and came out as gesel|lschaft — that path's pre-existing behaviour).

tests/auto_hyphenation.rs asserts properties of the DrawCommand::Text
stream grouped by baseline, never a transcript: every cut rejoins to a word
of the paragraph and lies at a pattern point of the *run's* language
(en-US inside the German paragraph), an inserted hyphen is only ever the
last piece of its line, the exempted twin and the wide zone and the off
document insert nothing, no two consecutive lines end in one under limit 1
(with the limitless twin as the control that they otherwise do), capitals
stay whole only under doNotHyphenateCaps while Kraftfahr-zeug beside them
is cut, the narrow→wide column paragraph never shows a hyphen mid-line,
and test-files/document_outline.docx — autoHyphenation on, zone 0,
docDefaults suppressAutoHyphens — paints no `-` its text does not contain
(the §17.7.5.2 control the adversarial review asked for).

Mutation-checked, each restored: a style that never carries the setting
fails five tests; dropping the rejoin before the re-fit fails the columns
and last-piece tests; ignoring doNotHyphenateCaps, the limit, and
suppressAutoHyphens each fails exactly its own test. All seven fixtures
pass scripts/verify_docx.py and are in parse_test_files.rs's ALL_FILES
and the AGENTS.md corpus table; compat15 is marked a probe there.
…on (nerdy-pro#158)

"Automatic hyphenation" flips to ✅ with its caveats stated in the cell:
the normative zone reading (Word's native layout may hyphenate more), TeX
patterns rather than Word's dictionary (cut points can differ), the four
vendored languages with every other tag and every untagged run left whole,
w:softHyphen still dropped, and no hyphenation of a word already cut per
cluster for being wider than its line. The summary line is recomputed from
the table — 83 implemented, 12 partial, 5 not yet supported — which
tests/readme_feature_matrix.rs pins; "automatic hyphenation" leaves the FAQ's
list of notable gaps; the dependency table gains the `hyphenation` row and
names the attribution file.

src/i18n/segment.rs's module doc gains the one paragraph on what it
deliberately does not report: hyphenation inserts a glyph and depends on the
line's remaining width, so it belongs to the fitter, not to UAX nerdy-pro#14.
…erdy-pro#158)

`compute_line_placements` fits one line per call when `page_floats` is
set, each at its own width, and `FitWindow::max_lines: Some(1)` is what
keeps a word cut for one line from being left spliced for a later call to
re-fit at another width. Nothing drove that path with a hyphenating
paragraph: `a_window_fits_from_its_start_and_stops_at_its_line_limit`
builds the window itself, so it stays green when the call site passes
`max_lines: None`.

`the_floats_path_hyphenates_each_line_at_its_own_width` lays six pairs of
`the hyphenation ` through a float that narrows the first three 14 pt
lines of a 126 pt paragraph to 66 pt, and asserts that the narrow lines
hyphenate, that lines at both widths do, and that no inserted hyphen is
followed by anything on its line. Watched fail under the mutation
`max_lines: None` at the floats call site, which fits every remaining
line at 66 pt on the first call and then paints `the hyphen- ation the `
as the fourth line; passes with `Some(1)`. The paragraph tests' `tagged`
closure becomes `hyphenating_word` so this test can share it.

Adversarial review: the §4 continuation rejoin was re-checked the same
way — with `rejoin_hyphenated` made a no-op, its three unit tests and
both columns integration tests fail, and this test does not, since the
floats path never re-fits.
ikashapov added a commit to ikashapov/dxpdf that referenced this pull request Oct 14, 2026
One squashed commit carrying the tree of integration/2026-10-14, which was
previously a chain of ten merges ending at ac5787e (kept locally as
archive/integration-2026-10-14-merges). Base: upstream main 3a5c7ca.

Integrated, each at the head of its open upstream pull request:

- nerdy-pro#252  fix-layout-defects-228-232 (a40d5ce) — issues nerdy-pro#228–nerdy-pro#232: bookmark-only
        paragraphs, nested-table width, list instances over one abstract
        numbering, table width resolution (pct and tcW autofit), column
        balancing and separators
- nerdy-pro#183  issue-150-wmf-svg (6e5aa99) — WMF/EMF and SVG images
- nerdy-pro#184  issue-151-pgborders-docgrid (27204b3) — page borders, document grid
- nerdy-pro#186  issue-153-shaped-clusters (65739f6) — shaped text clusters
- nerdy-pro#187  issue-154-tracked-changes (6982b34) — tracked changes and comments
- nerdy-pro#185  issue-155-smartart-charts (3683223) — SmartArt and charts
- nerdy-pro#256  issue-253-del-instr-text (0656a52) — w:delInstrText tolerated
- nerdy-pro#257  issue-250-image-hyperlinks (cb5ce2b) — a:hlinkClick on pictures
- nerdy-pro#258  issue-188-font-absent-cost (9a63bf0) — no deep catalogue tier before
        the substitution table, no theme script-font preload
- nerdy-pro#259  issue-158-auto-hyphenation (9bfbae2) — w:autoHyphenation

Beyond the union of those branches, the integration itself changed:
- README keeps one row per feature after the merges; the matrix tally is
  recomputed from the rows.
- nerdy-pro#253's tests assert that a deleted field code is never drawn and never run
  text, because nerdy-pro#154 keeps w:del runs instead of dropping them at parse.
- nerdy-pro#250's push_link_over sits after nerdy-pro#151's resolve_line_metrics; nerdy-pro#158's
  HyphenationCtx.measure and rejoin_hyphenated take &dyn SplitMeasure (nerdy-pro#153).

Verified on this exact tree: cargo fmt, clippy and doc clean; cargo test
2977 passed / 0 failed (skipping the capi library freshness guards);
--no-default-features and --features capi build; the twelve-document
harness passes 12/12.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement automatic hyphenation

1 participant