diff --git a/NOTICE b/NOTICE index 087bb619..972ab456 100644 --- a/NOTICE +++ b/NOTICE @@ -52,6 +52,12 @@ list (author, source, license) is in ASSETS.md. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +- Norwegian hyphenation patterns, embedded through the hypher crate (feature "norwegian"): + hyph-no.tex from hyph-utf8, https://github.com/hyphenation/tex-hyphen. + Copyright (C) Rune Kleveland and Ole Michael Selberg. Copying and distribution of this file, + with or without modification, is permitted in any medium without royalty provided the copyright + notice and this notice are preserved (additionally distributed under the LaTeX Project Public + License in CTAN distributions). - Japanese fonts (craft-fonts): builds made with craft-fonts (CRAFT_FONTS_DIR; all official releases) embed the fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, BIZ UDMincho, diff --git a/crates/compose/Cargo.toml b/crates/compose/Cargo.toml index 2225367c..c1a7f9bc 100644 --- a/crates/compose/Cargo.toml +++ b/crates/compose/Cargo.toml @@ -21,7 +21,7 @@ log = { workspace = true } regex = { workspace = true } fancy-regex = { workspace = true } miniz_oxide = "0.8" -hypher = { version = "0.1.8", default-features = false, features = ["alloc", "portuguese", "spanish"] } +hypher = { version = "0.1.8", default-features = false, features = ["alloc", "portuguese", "spanish", "norwegian"] } [dev-dependencies] designcraft-fonts = { workspace = true, features = ["testing"] } diff --git a/crates/compose/src/hyphen/mod.rs b/crates/compose/src/hyphen/mod.rs index e3f4a43b..ae5997e5 100644 --- a/crates/compose/src/hyphen/mod.rs +++ b/crates/compose/src/hyphen/mod.rs @@ -1,7 +1,8 @@ //! Hyphenation. English uses the bundled dictionary and patterns below. Other languages use the //! Liang patterns compiled into `hypher`, one cargo feature per language: Spanish is Javier Bezos's //! `hyph-es.tex` 5.0 from hyph-utf8, MIT; Portuguese is `hyph-pt.tex` 1.4 from hyph-utf8 (Pedro J. -//! de Rezende, J. Joao Dias Almeida, Leonardo Araujo, Aline Benevides), BSD-3-Clause (see NOTICE). +//! de Rezende, J. Joao Dias Almeida, Leonardo Araujo, Aline Benevides), BSD-3-Clause; Norwegian is +//! `hyph-no.tex` from hyph-utf8 (Rune Kleveland, Ole Michael Selberg), permissive (see NOTICE). //! //! English lookup order for each word: //! 1. the **dictionary** ([`Dictionary::en_us`]) — the public-domain Moby Hyphenator II word list @@ -411,6 +412,26 @@ mod tests { assert_eq!(h("pós-graduação"), "pós-gra-du-a-ção", "each part of a compound"); } + #[test] + fn norwegian_uses_the_hyph_no_patterns() { + let nb = Lang::for_language("Norwegian: Bokmål").unwrap(); + assert_eq!(nb, Lang::for_language("Norwegian").unwrap()); + assert_eq!(nb, Lang::for_language("Norwegian: Nynorsk").unwrap()); + assert_eq!(nb, Lang::for_language("nb").unwrap()); + assert_eq!(nb, Lang::for_language("nn").unwrap()); + assert_eq!(nb, Lang::for_language("no").unwrap()); + assert_ne!(nb, Lang::for_language("Spanish").unwrap()); + let h = |w: &str| { + let pts = hyphen_points_in(w, &Limits::default(), nb); + w.chars().enumerate().map(|(i, c)| if pts.contains(&i) { format!("-{c}") } else { c.to_string() }).collect::() + }; + assert_eq!(h("kaller"), "kal-ler"); + assert_eq!(h("bilde"), "bil-de"); + assert_eq!(h("sjøfugl"), "sjø-fugl"); + assert_eq!(h("sirkulasjonen"), "sir-ku-la-sjo-nen"); + assert_eq!(h("beveger"), "be-ve-ger"); + } + #[test] fn heuristic_fallback() { let c: Vec = "happen".chars().collect(); diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index a91b26c8..70b733f6 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -3335,6 +3335,25 @@ fn portuguese_text_gets_portuguese_hyphenation() { } } +#[test] +fn norwegian_text_gets_norwegian_hyphenation() { + let text = "sirkulasjonen beveger nedsynkningen kaller overflaten bilde"; + let (mut d, sid, _) = doc_with(text, Rect::new(36.0, 36.0, 76.0, 700.0), ParaAttrs::default()); + let st = d.story_mut(sid).unwrap(); + let n = st.len(); + st.format_chars(0..n, |f| f.over.language = Some("Norwegian: Bokmål".into())); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let breaks: Vec<&str> = all_lines(&cs).into_iter().filter(|l| l.hyphenated).map(|l| text[l.range.clone()].trim_end()).collect(); + assert!(!breaks.is_empty(), "Norwegian text is hyphenated"); + for b in breaks { + let tail = b.rsplit(' ').next().unwrap_or(b); + let whole = text[text.find(b).unwrap() + b.len() - tail.len()..].split(|c: char| !c.is_alphabetic()).next().unwrap_or(""); + let nb = hyphen::Lang::for_language("Norwegian: Bokmål").unwrap(); + let pts = hyphen::hyphen_points_in(whole, &hyphen::Limits::default(), nb); + assert!(pts.contains(&tail.chars().count()), "break {tail}- in {whole} is a hyph-no point"); + } +} + #[test] fn cjk_aki_adds_space_without_scaling_outlines() { let (mut d, sid, _) = doc_with("AB", Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default()); diff --git a/docs/gaps.md b/docs/gaps.md index c6c25221..cab3d0d0 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -136,7 +136,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · - **Estimate:** 25–40 h. ### 13. Hyphenation and spelling languages (T, L) -- **Missing:** hyphenation patterns for every language but English and Spanish; spelling for +- **Missing:** hyphenation patterns for every language but English, Spanish, Portuguese and Norwegian; spelling for every language but English. InDesign ships dictionaries for ~40 languages. - **Estimate:** 30–60 h (permissively licensed patterns exist for most; spelling word lists need licence review). **Doc:** [typography-parity.md](typography-parity.md) diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 1bb874e1..73f3e85a 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -33,7 +33,7 @@ come as Middle East editions with an English or French interface and right-to-le | Hindi | hi | ✗ | 0 (0%) | none | Devanagari shaped by harfrust in documents but untested; no Hindi hyphenation or spelling | no | none | 20–30 | | Arabic | ar | Middle East edition (English/French UI) | 1,459 (85%) | menus, panels, most dialogs; no help | RTL UI text (`rtl.rs`: bidi + shaping); Arabic composition, kashida, digits, RTL stories and tables ✓ (arabic-typography.md); list bullets and multi-column RTL open (#100) | no | partial | 15–25 | | French | fr | ✓ | 1,438 (83%) | menus, panels, most dialogs; no help | Latin ✓; no French hyphenation | no | partial | 6–10 | -| Portuguese (Brazil) | pt-br | ✓ (pt) | 1,057 (61%) | menus, panels, some dialogs; no help | Latin ✓; no Portuguese hyphenation | no | partial | 8–12 | +| Portuguese (Brazil) | pt-br | ✓ (pt) | 1,057 (61%) | menus, panels, some dialogs; no help | Latin ✓; Portuguese hyphenation ✓ | no | partial | 8–12 | | Indonesian | id | ✗ | 0 (0%) | none | Latin ✓ | no | none | 12–18 | | Japanese | ja | ✓ | 1,644 (95%) | whole interface (#272), vertical-type commands; no help | vertical text, kinsoku, aki, ruby, warichu ✓ (partial depth, cjk.md); **no canvas IME** (#322); Japanese UI and Mincho fonts from craft-fonts | no | partial | 6–10 (+ IME, shared with zh) | | German | de | ✓ | 1,438 (83%) | menus, panels, most dialogs; no help | Latin ✓; no German hyphenation (InDesign ships 2006 reform patterns) | no | partial | 6–10 | @@ -62,7 +62,7 @@ eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, ## Document languages InDesign 2026 hyphenates and spell-checks ~40 languages (Hunspell and Duden dictionaries). -DesignCraft hyphenates **English and Spanish** and spell-checks **English** only. Text language +DesignCraft hyphenates **English, Spanish, Portuguese and Norwegian** and spell-checks **English** only. Text language can be set per paragraph and character (Properties, Character panel) and drives quotes and locale shaping. See [typography-parity.md](typography-parity.md). diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 07a45288..8f77cdd7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -59,7 +59,7 @@ they receive, setting type exactly, and basic editing that never fails. | Dimension | Weight | Ready | Remaining (Opus 5.5 h) | Evidence | Doc | |---|---|---|---|---|---| | Features (depth of each area) | 25% | 55% | 250–400 | 275 catalogue rows exist; many lack options (e.g. Books is 258 lines of engine code, Liquid Layout 329); see the area table | this file | -| Typography and composition fidelity | 15% | 45% | 120–200 | Knuth–Plass composer, keeps, grids, CJK, Arabic all exist; no comparison with InDesign's line breaks; optical kerning falls back to metrics (#181); overset rule differs (#180); hyphenation patterns for English and Spanish only | [typography-parity.md](typography-parity.md) | +| Typography and composition fidelity | 15% | 45% | 120–200 | Knuth–Plass composer, keeps, grids, CJK, Arabic all exist; no comparison with InDesign's line breaks; optical kerning falls back to metrics (#181); overset rule differs (#180); hyphenation patterns for English, Spanish, Portuguese and Norwegian only | [typography-parity.md](typography-parity.md) | | File formats | 20% | 40% | 150–250 (+20–60 for INDD integration) | INDD cannot be opened (#21); IDML both ways and verified in InDesign 2026, but real-file bugs open (#193, #338, #339, #340); PDF export strong; PDF/X-4 only | [file-format-parity.md](file-format-parity.md) | | UI/UX fidelity | 15% | 45% | 120–180 | Measured InDesign 2026 look; but users report object copy/paste (#351, #348, #185, #164), right-click menus (#238, #187, #160), out-port threading (#352, #194, #165), canvas IME (#322) failing | [ui-parity.md](ui-parity.md) | | Stability | 10% | 45% | 60–100 | Never-crash rules and last-resort guards; startup failures on some GPUs and machines (#334, #273, #220, #192, #167, #166); no CI runs the tests on pull requests | [gaps.md](gaps.md) | @@ -349,7 +349,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Edit ▸ Menus (`window.hideMenuItem`): hide menu items; menus with hidden items end in Show All Menu Items. - View ▸ Separations Preview (`view.separations`): one process plate as ink density, or areas over an ink limit in red (a GCR preview of the rendered view; spot plates and the flattener preview aren't there). - Separations Preview plates now come from each object's own colour (CMYK as authored; RGB and images through the colour settings, images via a cached 17³ table), so pure black shows only on the black plate. -- Language (Character panel ▸ Language, Properties ▸ Character, `type.char {language}`): text in other languages isn't hyphenated or spell-checked with the English dictionaries, typographer's quotes follow the language (German „…“, French and Russian «…», Swiss, Spanish, Dutch, Nordic, Japanese and Traditional Chinese 「…」, Simplified Chinese “…”), the font's localized forms (OpenType `locl`: Turkish i, Bulgarian Cyrillic, Japanese / Chinese / Korean glyph variants) follow it too, and so does the fallback for CJK characters the font lacks (with Draw Missing Glyphs from Fallback Fonts on): Japanese, Simplified Chinese, Traditional Chinese and Korean each try their own installed system fonts (Japanese starts with the craft-fonts Japanese faces, Mincho first, when built with them; Hangul, kana and Bopomofo pick their language's fonts whatever the language). Language names from IDML keep their spelling; locale codes as InDesign writes them for some languages (`de_DE_2006`, `nl_NL_2005`; any `ll`, `ll_CC`, `ll-CC`) are their language and region for all of the above, and Chinese is also recognised in its common spellings ("Simplified Chinese", "Chinese (Traditional)", `zh_CN`, `zh-Hant`). Spanish is hyphenated with Javier Bezos's Liang patterns (`hyph-es.tex` 5.0, MIT) and Portuguese with hyph-utf8's `hyph-pt.tex` 1.4 (BSD-3-Clause), both through the `hypher` crate; hyphenation for other languages isn't bundled yet. +- Language (Character panel ▸ Language, Properties ▸ Character, `type.char {language}`): text in other languages isn't hyphenated or spell-checked with the English dictionaries, typographer's quotes follow the language (German „…“, French and Russian «…», Swiss, Spanish, Dutch, Nordic, Japanese and Traditional Chinese 「…」, Simplified Chinese “…”), the font's localized forms (OpenType `locl`: Turkish i, Bulgarian Cyrillic, Japanese / Chinese / Korean glyph variants) follow it too, and so does the fallback for CJK characters the font lacks (with Draw Missing Glyphs from Fallback Fonts on): Japanese, Simplified Chinese, Traditional Chinese and Korean each try their own installed system fonts (Japanese starts with the craft-fonts Japanese faces, Mincho first, when built with them; Hangul, kana and Bopomofo pick their language's fonts whatever the language). Language names from IDML keep their spelling; locale codes as InDesign writes them for some languages (`de_DE_2006`, `nl_NL_2005`; any `ll`, `ll_CC`, `ll-CC`) are their language and region for all of the above, and Chinese is also recognised in its common spellings ("Simplified Chinese", "Chinese (Traditional)", `zh_CN`, `zh-Hant`). Spanish is hyphenated with Javier Bezos's Liang patterns (`hyph-es.tex` 5.0, MIT), Portuguese with hyph-utf8's `hyph-pt.tex` 1.4 (BSD-3-Clause) and Norwegian with hyph-utf8's `hyph-no.tex` (permissive), all through the `hypher` crate; hyphenation for other languages isn't bundled yet. - Books (`book.new/open/add/remove/list/styleSource/paginate/syncStyles/exportPdf`, Book panel): `.dcbook` files, continuous page numbering, style sync from a style source, one merged PDF. - Track Changes (`changes.track/list/acceptAll/rejectAll`, Type ▸ Track Changes): typing is marked added (highlighted on screen), deletions are kept but hidden from layout and exports until accepted or rejected. Per-change accept/reject and authors aren't there yet. - Track Changes: per-change Accept / Reject (`changes.accept`, `changes.reject`) and a Track Changes panel listing every change. diff --git a/docs/typography-parity.md b/docs/typography-parity.md index 4a244fdd..2743164b 100644 --- a/docs/typography-parity.md +++ b/docs/typography-parity.md @@ -36,7 +36,7 @@ Fidelity: **unmeasured** unless stated. | Single-line Composer | greedy | greedy with the same ranges | ✓ | | | World-Ready composers | Paragraph / Single-line World-Ready | RTL, Arabic and bidi in the main composer | ~ | Indic scripts (Devanagari etc.) shape through harfrust but are untested | | Justification | word/letter spacing, glyph scaling min/desired/max, single-word, last line | all of these | ✓ | a justified line with a tab justifies after the tab (#284); set per paragraph in Type › Justification… and the Paragraph panel menu (#531) | -| Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list) and Spanish (Bezos); zone, limits, exceptions in the user dictionary | ~ | other languages don't hyphenate (#87 reported "doesn't work") | +| Hyphenation | per-language dictionaries (~40), zone, limits, capitalized, last word, across columns | Liang patterns: English (trained from the public-domain Moby list), Spanish (Bezos), Portuguese (hyph-pt) and Norwegian (hyph-no); zone, limits, exceptions in the user dictionary | ~ | other languages don't hyphenate (#87 reported "doesn't work") | | Spelling | per-language dictionaries | English (Moby) + document/user dictionary | ~ | | | Kerning | Metrics, Optical, manual, none | Metrics, Optical (outline-based: pairs pulled towards the font's stem gap, `crates/compose/src/optical.rs`), manual, none | ~ | Optical is an approximation of Adobe's algorithm (#181), not a match: imported display text can still break differently | | Tracking, scale, skew, baseline shift | ✓ | ✓ | ✓ | |