From 3110dcf02b7d947c581fca579970644e2a8f2244 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 10:20:43 +0000 Subject: [PATCH 01/15] CP1.3: proofing comes from craft-proof (craft-libs), its data from craft-i18n - wordcraft-proof is now `package = "craft-proof"` from craft-libs (local path for now; TODO in Cargo.toml for the git+rev pin after the craft-libs PR merges). `wordcraft_proof::` paths are unchanged. - crates/proof and assets/{spelling,hyphenation} deleted with their ATTRIBUTION.md rows; one row now points at craft-i18n's Language data; NOTICE paths updated. - craft-i18n.rev (and the flatpak source) moved to the craft-i18n commit that adds language-data/ (CI sets CRAFT_I18N_REQUIRED=1, so the pin must have it). - xtask layers: proof removed. Engine tests carry their own copy of the synthetic Czech fixture (they reached into crates/proof/src). - Docs: architecture.md, CLAUDE.md/AGENTS.md layer table, README build note (CRAFT_I18N_DIR), localization-tr path, ROADMAP progress log. Before/after wordcraft-cli on a 17-language proofing document: issues, suggestions, manual hyphenation, PDF text and PDF bytes identical. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- AGENTS.md | 5 +- ATTRIBUTION.md | 10 +- CLAUDE.md | 5 +- Cargo.toml | 8 +- NOTICE | 16 +- README.md | 13 +- ROADMAP.md | 2 + assets/hyphenation/de.pat | 36797 ---------------- assets/hyphenation/en-us.dic | Bin 432905 -> 0 bytes assets/hyphenation/en-us.pat | 1633 - assets/hyphenation/it.pat | 431 - assets/hyphenation/pl.pat | 4154 -- assets/hyphenation/pt.pat | 503 - assets/spelling/en-extra.txt | 241 - assets/spelling/en-variants.txt | 305 - assets/spelling/ru-extra.txt | 214 - craft-i18n.rev | 2 +- crates/engine/src/tests.rs | 4 +- .../src/tests/czech_test_dict.rs} | 0 crates/proof/Cargo.toml | 17 - crates/proof/src/affix.rs | 1784 - crates/proof/src/bg.rs | 112 - crates/proof/src/cs/grammar.rs | 103 - crates/proof/src/cs/mod.rs | 201 - crates/proof/src/cs/suggest.rs | 78 - crates/proof/src/dict.rs | 194 - crates/proof/src/en_variants.rs | 277 - crates/proof/src/es/grammar.rs | 391 - crates/proof/src/es/mod.rs | 315 - crates/proof/src/es/suggest.rs | 97 - crates/proof/src/es/test_dict.rs | 111 - crates/proof/src/es/voseo.rs | 155 - crates/proof/src/hr/grammar.rs | 175 - crates/proof/src/hr/mod.rs | 130 - crates/proof/src/hr/suggest.rs | 63 - crates/proof/src/hr/test_dict.rs | 42 - crates/proof/src/hunspell.rs | 706 - crates/proof/src/hyphen.rs | 920 - crates/proof/src/id/grammar.rs | 105 - crates/proof/src/id/mod.rs | 210 - crates/proof/src/id/suggest.rs | 56 - crates/proof/src/id/test_dict.rs | 49 - crates/proof/src/it/grammar.rs | 472 - crates/proof/src/it/mod.rs | 213 - crates/proof/src/it/suggest.rs | 115 - crates/proof/src/it/test_dict.rs | 139 - crates/proof/src/lang.rs | 388 - crates/proof/src/lib.rs | 2000 - crates/proof/src/patterns.rs | 356 - crates/proof/src/pl/grammar.rs | 548 - crates/proof/src/pl/mod.rs | 193 - crates/proof/src/pl/suggest.rs | 132 - crates/proof/src/pl/test_dict.rs | 114 - crates/proof/src/pt/grammar.rs | 251 - crates/proof/src/pt/mod.rs | 201 - crates/proof/src/pt/suggest.rs | 122 - crates/proof/src/pt/test_dict.rs | 121 - crates/proof/src/ru/extra.rs | 217 - crates/proof/src/ru/grammar.rs | 712 - crates/proof/src/ru/hyphen.rs | 134 - crates/proof/src/ru/mod.rs | 485 - crates/proof/src/ru/suggest.rs | 258 - crates/proof/src/ru/test_dict.rs | 130 - crates/proof/src/tr/dict.rs | 144 - crates/proof/src/tr/grammar.rs | 359 - crates/proof/src/tr/hyphen.rs | 98 - crates/proof/src/tr/mod.rs | 164 - crates/proof/src/tr/suggest.rs | 328 - crates/proof/src/vi.rs | 93 - docs/architecture.md | 5 +- docs/localization-tr.md | 2 +- .../flatpak/ai.storyteller.wordcraft.yml | 5 +- xtask/src/layers.rs | 1 - 73 files changed, 48 insertions(+), 59086 deletions(-) delete mode 100644 assets/hyphenation/de.pat delete mode 100644 assets/hyphenation/en-us.dic delete mode 100644 assets/hyphenation/en-us.pat delete mode 100644 assets/hyphenation/it.pat delete mode 100644 assets/hyphenation/pl.pat delete mode 100644 assets/hyphenation/pt.pat delete mode 100644 assets/spelling/en-extra.txt delete mode 100644 assets/spelling/en-variants.txt delete mode 100644 assets/spelling/ru-extra.txt rename crates/{proof/src/cs/test_dict.rs => engine/src/tests/czech_test_dict.rs} (100%) delete mode 100644 crates/proof/Cargo.toml delete mode 100644 crates/proof/src/affix.rs delete mode 100644 crates/proof/src/bg.rs delete mode 100644 crates/proof/src/cs/grammar.rs delete mode 100644 crates/proof/src/cs/mod.rs delete mode 100644 crates/proof/src/cs/suggest.rs delete mode 100644 crates/proof/src/dict.rs delete mode 100644 crates/proof/src/en_variants.rs delete mode 100644 crates/proof/src/es/grammar.rs delete mode 100644 crates/proof/src/es/mod.rs delete mode 100644 crates/proof/src/es/suggest.rs delete mode 100644 crates/proof/src/es/test_dict.rs delete mode 100644 crates/proof/src/es/voseo.rs delete mode 100644 crates/proof/src/hr/grammar.rs delete mode 100644 crates/proof/src/hr/mod.rs delete mode 100644 crates/proof/src/hr/suggest.rs delete mode 100644 crates/proof/src/hr/test_dict.rs delete mode 100644 crates/proof/src/hunspell.rs delete mode 100644 crates/proof/src/hyphen.rs delete mode 100644 crates/proof/src/id/grammar.rs delete mode 100644 crates/proof/src/id/mod.rs delete mode 100644 crates/proof/src/id/suggest.rs delete mode 100644 crates/proof/src/id/test_dict.rs delete mode 100644 crates/proof/src/it/grammar.rs delete mode 100644 crates/proof/src/it/mod.rs delete mode 100644 crates/proof/src/it/suggest.rs delete mode 100644 crates/proof/src/it/test_dict.rs delete mode 100644 crates/proof/src/lang.rs delete mode 100644 crates/proof/src/lib.rs delete mode 100644 crates/proof/src/patterns.rs delete mode 100644 crates/proof/src/pl/grammar.rs delete mode 100644 crates/proof/src/pl/mod.rs delete mode 100644 crates/proof/src/pl/suggest.rs delete mode 100644 crates/proof/src/pl/test_dict.rs delete mode 100644 crates/proof/src/pt/grammar.rs delete mode 100644 crates/proof/src/pt/mod.rs delete mode 100644 crates/proof/src/pt/suggest.rs delete mode 100644 crates/proof/src/pt/test_dict.rs delete mode 100644 crates/proof/src/ru/extra.rs delete mode 100644 crates/proof/src/ru/grammar.rs delete mode 100644 crates/proof/src/ru/hyphen.rs delete mode 100644 crates/proof/src/ru/mod.rs delete mode 100644 crates/proof/src/ru/suggest.rs delete mode 100644 crates/proof/src/ru/test_dict.rs delete mode 100644 crates/proof/src/tr/dict.rs delete mode 100644 crates/proof/src/tr/grammar.rs delete mode 100644 crates/proof/src/tr/hyphen.rs delete mode 100644 crates/proof/src/tr/mod.rs delete mode 100644 crates/proof/src/tr/suggest.rs delete mode 100644 crates/proof/src/vi.rs diff --git a/AGENTS.md b/AGENTS.md index f52cb94d7..e1ab19f45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ People trust WordCraft with their writing; a crash loses their work. **This outr - Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily". - Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it. - **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file. -- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. +- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds but English isn't spell-checked and hyphenation is off, so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. - Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded. ## Clean room @@ -34,11 +34,12 @@ People trust WordCraft with their writing; a crash loses their work. **This outr | L0 | `geom` | units, rects, measurement parsing | | L1 | `doc`, `fonts` | document model + edit primitives; font DB, shaping, Word font substitution | | L2 | `layout`, `docx`, `formats` | line breaking, pagination, tables, headers/footers, hit testing; OOXML I/O; TXT/MD/HTML/RTF/ODT | -| L3 | `render`, `pdf`, `proof` | vello_cpu rasteriser; krilla PDF; proofing | +| L3 | `render`, `pdf` | vello_cpu rasteriser; krilla PDF | | L4 | `engine` | Session, undo, **command registry**, Word feature catalog, samples, I/O dispatch | | L5 | `mcp` | MCP server (headless or bridged to the app) | | L6 | `ui-egui` | Word-style egui front end (swappable) | | apps | `apps/wordcraft`, `apps/wordcraft-cli`, `apps/wordcraft-web` | binaries | +| external | `craft-proof` (craft-libs, used as `wordcraft-proof`, sits at L1) | proofing: spelling, grammar, hyphenation; its word lists and patterns are craft-i18n's `language-data/` (`CRAFT_I18N_DIR`) | - **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs. - **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity-checklist.md`; a test enforces a floor that only rises. Implement missing ids to raise it. diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 23d8cbd21..30ba3d511 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -10,15 +10,6 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra |---|---|---|---|---| | `egui` dependency: `epaint_default_fonts/fonts/egui-icons.ttf` | John Slegers (emoji-icon-font) | https://github.com/storytold/egui/blob/173cc7375d35dc5fbc5b5d0994fdf787f547c5d1/crates/epaint_default_fonts/fonts/egui-icons.txt; original https://github.com/jslegers/emoji-icon-font | MIT (full copyright and permission text in NOTICE) | Small egui special-icon subset; embedded by default_fonts, retained in FontDefinitions::default() | | `egui` dependency: `epaint_default_fonts/fonts/emoji-icon-font.ttf` | John Slegers | https://github.com/storytold/egui/blob/173cc7375d35dc5fbc5b5d0994fdf787f547c5d1/crates/epaint_default_fonts/fonts/emoji-icon-font-mit-license.txt; original https://github.com/jslegers/emoji-icon-font | MIT (full copyright and permission text in NOTICE) | Parent monochrome icon/emoji face embedded by monochrome_emoji_fonts; separate from the egui-icons subset | -| `assets/hyphenation/en-us.pat` | DesignCraft contributors (generated) | trained from the Moby list by DesignCraft's patgen | Public Domain (CC0-1.0) | Our own Liang hyphenation patterns (no TeX patterns) | -| `assets/hyphenation/en-us.dic` | Grady Ward (Moby Hyphenator II word list); converted by DesignCraft contributors | https://www.gutenberg.org/ebooks/3204 (file `mhyph.txt`, SHA-256 `eeb30474c86b8af3469035ec1a0913e35905325ca885290db9dfed9881e230ac`), via DesignCraft | Public Domain | ~165k words with break points; drives spelling and hyphenation | -| `assets/spelling/en-extra.txt` | WordCraft contributors | original word list written for this repository (common modern words the Moby list lacks); no other dictionary copied | MIT OR Apache-2.0 | ~240 lowercase English base forms supplementing spelling (#520), including British spellings (#40) | -| `assets/spelling/en-variants.txt` | WordCraft contributors | original list written for this repository from general knowledge of English spelling; no other dictionary or variant list copied | MIT OR Apache-2.0 | ~280 American/British spelling pairs (-or/-our, -er/-re, -ize/-ise, -yze/-yse, -ense/-ence, doubled l, ae/oe…) with a policy per English: en-US, en-CA and the British-spelling Englishes (#1281, #1045) | -| `assets/spelling/ru-extra.txt` | WordCraft contributors | original word list written for this repository (common modern Russian words older Hunspell dictionaries lack, with their declension classes); no other dictionary copied | MIT OR Apache-2.0 | ~200 Russian base forms supplementing spelling; their forms are generated by `crates/proof/src/ru/extra.rs` | -| `assets/hyphenation/pl.pat` | Hanna Kołodziejska, Bogusław Jackowski, Marek Ryćko (Polish TeX hyphenation patterns, 1987–1995); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-pl.tex`, SHA-256 `83ddbbd21bc06a44bc4f4a4c49c38782e809cff5f026b764c044daa29e818843`), bundled unmodified | MIT (the file is offered under MIT or Knuth's public-domain terms; WordCraft uses MIT, whose notice is in the file's header) | 4,053 Liang patterns and 20 exception words; Polish hyphenation | -| `assets/hyphenation/pt.pat` | Pedro J. de Rezende, J. Joao Dias Almeida, Leonardo Araujo, Aline Benevides (Portuguese TeX hyphenation patterns, 1987–2024); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-pt.tex`, SHA-256 `c110a39e501db372d1d755e1c64464321a2efa7d8e9617f66001000389b941f2`), bundled unmodified | BSD 3-clause (the file's licence header names it and carries the notice) | 426 Liang patterns and 2 exception words; Portuguese hyphenation | -| `assets/hyphenation/it.pat` | Claudio Beccari (Italian TeX hyphenation patterns, 2008–2014); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-it.tex`, SHA-256 `6ce56ed6ed2ca7c0688838e366e6595e0429f984f4fc6e96ec045cebb1e76a47`), bundled unmodified | MIT (the file is offered under LPPL 1.3+ or MIT; WordCraft uses MIT, whose notice is in the file's header) | 355 Liang patterns, no exception words; Italian hyphenation | -| `assets/hyphenation/de.pat` | Deutschsprachige Trennmustermannschaft: Stephan Hennig, Werner Lemberg, Günter Milde, Sander van Geloven, Georg Pfeiffer, Gisbert W. Selke, Tobias Wendorf, Keno Wehr (German TeX hyphenation patterns, reformed spelling, 2013–2024); hyph-utf8 package | https://github.com/hyphenation/tex-hyphen (`hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-de-1996.tex`, SHA-256 `374ad1ce3263f8a2791ec070ef9b516c532dae64456f3fb656e617e8ae53a9d3`), bundled unmodified | MIT (the notice is in the file's header) | 36,709 Liang patterns, no exception words; German hyphenation (de-DE, de-AT, de-CH) | | `assets/app-icon/wordcraft-1024.png` | WordCraft contributors | original artwork drawn in SVG (`assets/app-icon/wordcraft.svg`), rendered by `packaging/icons.sh` | MIT OR Apache-2.0 | Placeholder app icon until the WordCraft mascot drawing lands | | `packaging/macos/dmg/background.svg` | @XusBadia | original work for this repository, derived from the WordCraft app icon (`assets/app-icon/wordcraft.svg`, referenced via ``, not copied); text is outlined Inter / JetBrains Mono glyph paths | MIT OR Apache-2.0, like the code ([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE)) | WordCraft macOS DMG window background (2026-10-08); used for the Finder window of the macOS DMG, rendered to `background.tiff` by `packaging/macos/dmg/generate.py` | | `packaging/macos/dmg/background.tiff` | @XusBadia | Rendered from `packaging/macos/dmg/background.svg` by `packaging/macos/dmg/generate.py` | MIT OR Apache-2.0, like the code ([`LICENSE-MIT`](LICENSE-MIT), [`LICENSE-APACHE`](LICENSE-APACHE)) | 1x + 2x HiDPI TIFF for the macOS DMG window; includes the app icon (see its row) and LittleCMS's built-in sRGB profile ("No copyright, use freely") | @@ -70,3 +61,4 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra | `docs/brand/artcraft-mark.svg` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | | craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans CJK SC, Noto Sans Arabic | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | embedded only when built with `CRAFT_FONTS_DIR` (all official releases) | OFL-1.1 | Japanese and Chinese UI, Japanese document fallback | | craft-i18n (optional build input, not files in this repo): the interface translations | see https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md, which credits every translator of WordCraft's former in-repo catalogs (`crates/ui-egui/src/i18n/*.tsv`, migrated at `wordcraft@806a945`) | embedded only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | MIT OR Apache-2.0 | Interface languages other than English; clean-room translations from the English interface labels | +| craft-i18n Language data (optional build input, not files in this repo): the spelling word lists and hyphenation patterns (`language-data/`: the Moby Hyphenator II list and our patterns for English, `en-extra.txt`, `en-variants.txt`, `ru-extra.txt`, and the hyph-utf8 German, Polish, Portuguese and Italian patterns) | see the Language data section of https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md (authors, upstream sources with SHA-256, licences); moved there from `assets/hyphenation/` and `assets/spelling/` byte for byte | embedded by craft-proof (storytold/craft-libs) only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | Public Domain / CC0-1.0, MIT, BSD-3-Clause, MIT OR Apache-2.0 (per file; see NOTICE) | Spelling and hyphenation; without it English is not spell-checked and hyphenation is off | diff --git a/CLAUDE.md b/CLAUDE.md index 35024b74d..1a51b19b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ People trust WordCraft with their writing; a crash loses their work. **This outr - Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily". - Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it. - **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file. -- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. +- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds but English isn't spell-checked and hyphenation is off, so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. - Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded. ## Clean room @@ -34,11 +34,12 @@ People trust WordCraft with their writing; a crash loses their work. **This outr | L0 | `geom` | units, rects, measurement parsing | | L1 | `doc`, `fonts` | document model + edit primitives; font DB, shaping, Word font substitution | | L2 | `layout`, `docx`, `formats` | line breaking, pagination, tables, headers/footers, hit testing; OOXML I/O; TXT/MD/HTML/RTF/ODT | -| L3 | `render`, `pdf`, `proof` | vello_cpu rasteriser; krilla PDF; proofing | +| L3 | `render`, `pdf` | vello_cpu rasteriser; krilla PDF | | L4 | `engine` | Session, undo, **command registry**, Word feature catalog, samples, I/O dispatch | | L5 | `mcp` | MCP server (headless or bridged to the app) | | L6 | `ui-egui` | Word-style egui front end (swappable) | | apps | `apps/wordcraft`, `apps/wordcraft-cli`, `apps/wordcraft-web` | binaries | +| external | `craft-proof` (craft-libs, used as `wordcraft-proof`, sits at L1) | proofing: spelling, grammar, hyphenation; its word lists and patterns are craft-i18n's `language-data/` (`CRAFT_I18N_DIR`) | - **Everything is a command.** User-visible behaviour = a `CommandSpec` in `crates/engine/src/cmd/*.rs` (id, label, ribbon location, shortcut, params doc, `enabled`, `run`) + tests. UI-only commands (`ui.*`) live in `crates/ui-egui/src/lib.rs`. The ribbon, shortcuts, command search, CLI, control channel and MCP all dispatch by id. Programmatic calls never open dialogs. - **Parity is measured:** `crates/engine/src/catalog.rs` lists Word's ribbon/menu features with command ids. `cargo xtask parity` writes `docs/parity-checklist.md`; a test enforces a floor that only rises. Implement missing ids to raise it. diff --git a/Cargo.toml b/Cargo.toml index a1b229db3..9f8187579 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,13 @@ wordcraft-docbin = { path = "crates/docbin" } wordcraft-formats = { path = "crates/formats" } wordcraft-render = { path = "crates/render" } wordcraft-pdf = { path = "crates/pdf" } -wordcraft-proof = { path = "crates/proof" } +# Proofing is craft-libs' craft-proof (it was crates/proof); its Language data is craft-i18n's +# language-data/, embedded from CRAFT_I18N_DIR (craft-i18n.rev). The package keeps the +# `wordcraft-proof` name here so `wordcraft_proof::` paths are unchanged. +# TODO(CP1.3): once storytold/craft-libs merges craft-proof, replace the local path with its own pin +# (separate from the main craft-libs pin): +# wordcraft-proof = { package = "craft-proof", git = "https://github.com/storytold/craft-libs", rev = "" } +wordcraft-proof = { package = "craft-proof", path = "../wt-cp1-libs/crates/proof" } wordcraft-engine = { path = "crates/engine" } wordcraft-mcp = { path = "crates/mcp" } wordcraft-zotero = { path = "crates/zotero" } diff --git a/NOTICE b/NOTICE index 82e79a0a9..7d762fb8d 100644 --- a/NOTICE +++ b/NOTICE @@ -20,13 +20,17 @@ list (author, source, license) is in ATTRIBUTION.md. https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, BIZ UDMincho, Shippori Mincho), under the SIL Open Font License 1.1. Their licence texts ship with the packages as OFL-.txt. Builds without CRAFT_FONTS_DIR do not include them. -- English word list (assets/hyphenation/en-us.dic) — derived from the Moby Hyphenator II list by +- Language data (craft-i18n): the word list and hyphenation patterns below are embedded at build + time from https://github.com/storytold/craft-i18n (language-data/, CRAFT_I18N_DIR at the commit + in craft-i18n.rev; all official releases) by craft-proof (storytold/craft-libs). Builds without + CRAFT_I18N_DIR do not include them. +- English word list (craft-i18n language-data/en-us/en-us.dic) — derived from the Moby Hyphenator II list by Grady Ward, public domain (Project Gutenberg eBook #3204). The hyphenation patterns - (assets/hyphenation/en-us.pat) were trained from it and are released as CC0-1.0. -- Polish hyphenation patterns (assets/hyphenation/pl.pat) — hyph-pl.tex from the hyph-utf8 + (craft-i18n language-data/en-us/en-us.pat) were trained from it and are released as CC0-1.0. +- Polish hyphenation patterns (craft-i18n language-data/pl/pl.pat) — hyph-pl.tex from the hyph-utf8 package, Copyright (C) 1987-1995 Hanna Kołodziejska, Bogusław Jackowski, Marek Ryćko, bundled unmodified and used under the MIT License; the copyright and permission notice is in the file. -- Portuguese hyphenation patterns (assets/hyphenation/pt.pat) — hyph-pt.tex from the hyph-utf8 +- Portuguese hyphenation patterns (craft-i18n language-data/pt/pt.pat) — hyph-pt.tex from the hyph-utf8 package, bundled unmodified and used under the BSD 3-Clause License: Copyright (C) 1987, 1994, 1996, 2015 Pedro J. de Rezende, 1996, 2015 J. Joao Dias Almeida, @@ -52,7 +56,7 @@ list (author, source, license) is in ATTRIBUTION.md. 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. -- German hyphenation patterns (assets/hyphenation/de.pat) — hyph-de-1996.tex (reformed spelling) +- German hyphenation patterns (craft-i18n language-data/de/de.pat) — hyph-de-1996.tex (reformed spelling) from the hyph-utf8 package, bundled unmodified and used under the MIT License; the copyright and permission notice is in the file: @@ -74,7 +78,7 @@ list (author, source, license) is in ATTRIBUTION.md. FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- Italian hyphenation patterns (assets/hyphenation/it.pat) — hyph-it.tex from the hyph-utf8 +- Italian hyphenation patterns (craft-i18n language-data/it/it.pat) — hyph-it.tex from the hyph-utf8 package, Copyright (C) 2008-2011 Claudio Beccari, bundled unmodified and used under the MIT License (offered under LPPL 1.3+ or MIT); the copyright and permission notice is in the file: diff --git a/README.md b/README.md index 01d366510..2ddc4892f 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,17 @@ cargo run --release -p wordcraft -- --sample # the desktop app with the s cargo run --release -p wordcraft -- report.docx # open a document ``` +The interface translations and the spelling and hyphenation data come from +[craft-i18n](https://github.com/storytold/craft-i18n), an optional build input. To build with them, +check it out at the commit in `craft-i18n.rev` and point `CRAFT_I18N_DIR` at it (an absolute +path); without it WordCraft builds in English with no English spell-checking and no hyphenation: + +```sh +git clone https://github.com/storytold/craft-i18n ../craft-i18n +git -C ../craft-i18n checkout "$(cat craft-i18n.rev)" +CRAFT_I18N_DIR="$PWD/../craft-i18n" cargo run --release -p wordcraft -- --sample +``` + Command line: ```sh @@ -194,7 +205,7 @@ covers every target starting with it (`wordcraft*=debug`). The logger is | Layer | Crate | Job | |---|---|---| | L0 | `wordcraft-geom` | units and measurements | -| L1 | `wordcraft-doc`, `wordcraft-fonts`, `wordcraft-proof` | document model and editing; fonts and shaping; spelling, grammar, hyphenation | +| L1 | `wordcraft-doc`, `wordcraft-fonts`, `wordcraft-proof` ([`craft-proof`](https://github.com/storytold/craft-libs/tree/main/crates/proof) from craft-libs) | document model and editing; fonts and shaping; spelling, grammar, hyphenation (data: craft-i18n `language-data/`, `CRAFT_I18N_DIR`) | | L2 | `wordcraft-layout`, `wordcraft-docx`, `wordcraft-formats` | line breaking, pagination, tables, notes, hit testing; OOXML; ODT/RTF/HTML/Markdown/TXT | | L3 | `wordcraft-render`, `wordcraft-pdf` | rasteriser (vello_cpu); PDF ([craft-pdf](docs/shared-pdf.md), Krilla) | | L4 | `wordcraft-engine` | session, undo, 422 commands, document feature catalog | diff --git a/ROADMAP.md b/ROADMAP.md index fb6ef9ce0..80c099e58 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -231,6 +231,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target - 2026-10-15: Find opens Navigation on Results; editing the query restores Results after manual Headings/Pages navigation. Typed and selected-text searches retain document contents (#2096). | Date | What landed | +| 2026-11-01 | Proofing moved to the shared libraries, with nothing users see changing: the code is `craft-proof` in storytold/craft-libs (with its history) and the spelling and hyphenation data (the Moby list, our English patterns and word lists, the German, Polish, Portuguese and Italian patterns, `ru-extra`) is craft-i18n's `language-data/`, embedded from `CRAFT_I18N_DIR` at the commit in `craft-i18n.rev`. Before/after `wordcraft-cli` runs on a proofing document give the same issues, suggestions and hyphenated PDF (CP1). | | 2026-10-28 | **Arabic diacritic editing (#4757):** Backspace removes the last stored Arabic combining mark, so its base letter can stay while the mark is replaced. Caret movement and forward Delete keep their cluster rules. Synthetic command, history, protection and native/DOCX fresh-reopen checks; no native Word comparison or readiness change. | | 2026-10-29 | Insert › Shapes shows the basic shapes as a grid of code-drawn icon tiles, each with an accessible name and tooltip, instead of a text list (#4349) | | 2026-11-01 | Markdown export: a list inside a block quote stays in the quote (`> - item`), its continuation lines included; it was read as quoted list paragraphs but written without `>`, so the list moved out of the quote on every save | @@ -697,6 +698,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps | 2026-11-01 | trivial | Progress log: tab leader dots spaced and placed as in Word | | 2026-10-31 | trivial | Progress log: a list level above counts its first number at the first deeper item | | 2026-11-01 | trivial | Progress log: paragraph side borders lie outside the text (#5036) | +| 2026-11-01 | trivial | Progress log: proofing moved to craft-proof (craft-libs) and its data to craft-i18n `language-data/` (CP1) | | 2026-10-31 | trivial | Progress log: a paragraph naming an undefined DOCX style is formatted as Normal (#5077) | | 2026-10-31 | trivial | Progress log: TOC page numbers are PAGEREF fields (#5097) | | 2026-10-31 | trivial | Progress log: Backstage keytips, Alt, F, P prints (#4960) | diff --git a/assets/hyphenation/de.pat b/assets/hyphenation/de.pat deleted file mode 100644 index 15641e1ec..000000000 --- a/assets/hyphenation/de.pat +++ /dev/null @@ -1,36797 +0,0 @@ -% title: German Hyphenation Patterns (Reformed Orthography, 2006) -% -% notice: TeX-Trennmuster für die reformierte (2006) deutsche Rechtschreibung -% -% version: 2024-02-28 -% -% authors: -% - -% name: Deutschsprachige Trennmustermannschaft -% contact: trennmuster@dante.de -% -% copyright: Copyright (c) 2013-2024 -% Stephan Hennig, Werner Lemberg, Günter Milde, -% Sander van Geloven, Georg Pfeiffer, Gisbert W. Selke, -% Tobias Wendorf, Keno Wehr -% -% licence: -% name: MIT -% url: https://opensource.org/licenses/mit-license.php -% text: > -% Permission is hereby granted, free of charge, to any person -% obtaining a copy of this software and associated documentation -% files (the “Software”), to deal in the Software without -% restriction, including without limitation the rights to use, -% copy, modify, merge, publish, distribute, sublicense, and/or -% sell copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following -% conditions: -% -% The above copyright notice and this permission notice shall be -% included in all copies or substantial portions of the Software. -% -% THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, -% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -% OTHER DEALINGS IN THE SOFTWARE. -% -% source: https://repo.or.cz/w/wortliste.git?a=commit;h=304aaa2188a75e57afe36bad0443d90a9c715b8e -% -% language: -% name: German, reformed spelling -% tag: de-1996 -% -% hyphenmins: -% generation: -% left: 2 -% right: 2 -% typesetting: -% left: 2 -% right: 2 -% -% texlive: -% encoding: ec -% babelname: ngerman -% use_old_patterns_comment: Kept for the sake of backward compatibility, but newer and better patterns by WL are available. -% legacy_patterns: dehyphn.tex -% message: German hyphenation patterns (reformed orthography) -% package: german -% -% =========================================================================== - -\message{German Hyphenation Patterns (Reformed Orthography, 2006) `dehyphn-x' 2024-02-28 (WL)} - -% -% The used patgen parameters are -% -% 1 1 | 1 3 | 2 3 1 -% 2 2 | 2 4 | 1 5 1 -% 3 3 | 3 5 | 1 6 1 -% 4 4 | 4 6 | 1 7 1 -% 5 5 | 5 12 | 1 8 1 -% 6 6 | 6 12 | 1 9 1 -% 7 7 | 7 12 | 1 9 1 -% 8 8 | 8 12 | 1 9 1 - -\patterns{% -.ab3a -.abb2 -.ab5erk -.ab5er4r -.abi2 -.ab3it -.ab3l -.ab5le -.ab3ol -.ab3or -.ab3sa -.ab5so -.ab3u -.ach4sa -.achse4 -.ach4sp -.achts4 -.ack4e -.ae2 -.aer6schl -.ago3 -.ag4r -.ag2u -.ai2s -.ak6t5en6de. -.ak6t5en6den -.ak6t5en6des -.al4bl -.alb3r -.al4bra -.alb6rec -.ale3u4 -.ali3 -.al5l6en -.alle4r5 -.al4lin -.allo3 -.all3s -.al4mau -.al4pl -.al5s4e -.al4t5ak -.al6t5ang -.alt6an6l -.altan6s -.al4tei -.al4t5e4l -.al6t5er6f -.al6t5er6p -.alter6s5 -.al4t3h -.al4ti -.al4tor -.al4t5ra -.al4tri -.al4tro -.alt3s4 -.al4t3u -.al4u -.alu3s -.ampe4 -.am5sta -.amt4s3 -.ana3c -.an3ae -.an5alg -.ana3t -.anden6ke -.an3e -.ang4a -.an5g4li -.ang4r -.angs4 -.angst5 -.ani4s -.anis5c -.ank2 -.anla6gentec -.an3n4a -.an3o2 -.an4ora -.ans2 -.ansch4 -.an4si. -.an5sp -.an4tag -.anti5s -.anz2 -.an5zw -.apfel5 -.app6le. -.are3n -.ar4m5ac -.ar4mar -.ar4mau -.ar4mei -.ar4mun -.ar4sc -.arsch5l -.ar4tan -.ar4tau -.ar4t5ei -.arter4 -.ar6t5erh -.ar6t5erk -.ar4t3r -.arz2 -.arzt3 -.as2b -.asbe4 -.asie4 -.as4ko -.as2p -.as4ta. -.as5tar -.as4zen -.ata3 -.ate4m3 -.at4h -.aue4n3 -.au2f3 -.auf5s4 -.auge4 -.au2s3 -.aussen6dung. -.auto6ren. -.auto6rin -.ax2 -.ärm4s -.är6schl -.ät2h -.ät2s -.bahner6 -.ba4ran -.bau5chi -.bauer6f -.bau3f4 -.beige4 -.bei6ge. -.be3n4a -.be3n4e -.be3r4a -.ber4e -.ber4ga -.berg5ab -.berg5ar -.berg5ast -.ber6g5e6b -.ber6gin -.ber4g5l -.ber4g5o -.ber4g5r -.berg5s -.ber4t5r -.be3s4a -.be6t5raum -.bi2e -.bi2o -.bi4os -.bi4t3a -.bi4t3r -.blau3 -.blei5s4 -.boge4 -.bogen5 -.bogens6 -.bor4d5i -.bo4s3k -.bre4a -.brecher6 -.bre6cherk -.brid4 -.briefen6 -.brie6f5end -.bruch5s -.brut5s -.bu6cher -.buchse6 -.bue5be -.bu4ser -.buster6 -.buß3 -.bü3be -.by2t -.ce4ra -.ch2 -.ch4a -.char6me. -.char6mes -.ch4i -.chi5er -.dab2 -.da6ch5en -.da3la -.dan4c -.da2r -.dar5in -.dar4m3 -.dar3u -.da4te. -.da4tes -.daue4 -.de4al -.de3ch -.de6ck5eri -.dein4 -.de5ins -.de5int -.de6m5ents -.de3na -.den4ka -.den4kl -.den4ko -.den4kr -.denta4 -.de3od -.de5ran -.de4rei -.de5r4en -.de5skr -.des4pe -.de4spo -.des5se -.de3st -.des4ti -.de3xa -.di2a -.di5ae4r -.di3ä4r -.di2e -.die5ne -.dien4s -.dienst5a6d -.dienst5r -.di5s4ti -.do4mo -.dorf3 -.dor6fer -.do4tr -.drah6t5en -.drucker6zeu -.due3b -.du6schr -.dü3b -.dy2s3 -.ebe4r -.echo3 -.eck5erk -.ede4l -.eh4e -.ehe3i4 -.ehein4 -.ehe3n -.ehre4 -.eh4r5er -.eichs4 -.eich5ste -.eide4r5 -.ei3e2 -.eier3 -.eigener6 -.ei4lau -.ein3a -.ei4nat -.eine4 -.ei4n5eb -.ei6n5en6g -.eins4p -.eins6tem -.einzel6lig -.eisen5 -.ei4spe -.eis3s4 -.ei4s3t -.ei6ste -.ei4s3u -.ei4tr -.ek5lam -.ek3li -.ek3s -.el4a -.el4b3i -.el4bl -.elb5s -.el4fei -.elfens6 -.el4fl -.en1 -.en4d3a -.en4d5ei -.en4d5er4 -.en5der. -.en6dost -.en4d3r -.end3s -.en4due -.en4dü -.en4g3l -.eng5sta -.enk2 -.enn4st -.en4t3a -.en4tä -.en4t5ei -.en4tem -.en5thalp -.en4thi -.en4t5io -.en4toe -.en4t3r -.en5trep -.en5trop -.ent3s4 -.epi3c -.ep3o4d -.er6b5aen -.er4b5än -.er4b5ei -.er4b5er -.er6brecht -.er4bu -.er6d5achs -.er4dan -.er4dar -.er4d5ei -.er4del -.erden6k -.er4d5ep -.er4d5er -.er6d5ess -.ere5ch -.er3ei -.er3er -.erfolgsauto6 -.er3in -.erk2 -.er3lö -.ern4s -.er3o4b -.er3oe -.ers2 -.er6stab -.er6st5ein -.er6ster6 -.erst5eri -.erst5ers -.er6stim -.er6stritt. -.er6stritten. -.er4z5el -.erzen4 -.er6z5eng -.er4z5er -.es3p -.es4s3t -.es3ta4 -.es3te -.es5tra -.est5ri -.et2h -.et2s -.eu1 -.euch4 -.eu3g2 -.eur4 -.eu3t -.eve4r -.ex3em -.fahrzeugin6 -.fan4gl -.fang5s -.faus6t5r -.fe3la -.felden5 -.fel4d5r -.fel6lau -.fel4s5a -.fel4sp -.fel4s5t -.fen2 -.fer4n5o -.fe4st -.fest5r -.fi3d -.fi5est -.fi4le. -.fi4len -.film5a -.fil4s3 -.fin4s -.fi2r -.flu4g3 -.flus6s5en -.former6 -.for5tu -.fra4s -.fs2 -.fu4sc -.fus4st -.ga4me. -.ga4mes -.gan4ga -.gang5sc -.garnen6 -.gar6n5end -.ga2s -.gas3e -.gas3s -.gas5tein -.gas6tras -.ga2t -.geb2 -.gebe4a -.gege4n5 -.gel4b5r -.ge3lu -.ge3na -.gen6aug -.ge3ne -.gene5cke -.gen4ei -.ge3nu -.ger4a -.ger4e -.ge3ro -.ge3ru -.ge3sa -.ges4e -.ges4i -.ges4p -.ge5stak -.ge7stei -.ge3t4u -.gi2u -.gla4s5c -.gol6der -.gs2 -.gu3ru -.hafe4 -.haf6ter -.haft5s4 -.hal4br -.han4a -.han4f -.hang5s -.ha5sche -.hauser6w -.hau4su -.haut3 -.hau4to -.he2r -.he5rae5 -.her6b5ra -.he5rer -.he3ri -.he6r5inn -.he5ros -.he4sp -.hete3 -.he3xa -.hi2n -.hin3a -.hin3u -.hi2p -.hi4s -.hitze5 -.ho4f5en -.hof3s4 -.ho4la -.ho4met -.ho3ra -.hor4t5r -.höch4s -.hu4fer -.hu3la -.hund4 -.hu3s4a -.ia2 -.ibe3 -.ima4ge -.imm4 -.immen4 -.in1 -.ind2 -.ine2 -.in3eg -.in3i -.ink4 -.inse4l5 -.in5skr -.inu3 -.io2d3 -.ioni5e -.ire3 -.is2a -.isar3 -.it2h -.joni3 -.jor3 -.kabe4 -.ka4b5l -.ka3la -.kal4ta -.kal4t5r -.ka4t5i4o -.ke3ra -.ker4bl -.ker4na -.ke3ro -.ke2t -.ki2e -.ko5be -.ko4ter -.kraf4 -.kraft5 -.ks2 -.kue3b -.ku4ran -.kü3b -.la5ert -.lage4n5 -.la3ho -.lan4d5a -.lan6de6mi -.lan4dr -.lan4gl -.lan4g5r -.lang5s -.lan3t -.lase4 -.las4t5r -.le4ar -.leder5 -.le4gas -.lei4bl -.lei4br -.leider6 -.lei6derf -.lei4tr -.le3n4i -.len4k5a -.len4kl -.len4kr -.les3t -.le3th -.lich6ter6sc -.li2f -.li5gn -.lin4kl -.li3o -.li4ve. -.lo4g5in -.lo4sc -.losch5 -.lo4sk -.loss4 -.los3t -.lo3th -.lo4tr -.lo5ver -.lus4t5e -.luster6 -.lu5t4h -.ly2s3 -.ma5lae -.mal5e4c -.ma5len -.mal4z5a -.man3d -.mani4e -.man5t -.mas4s5u -.ma4st -.ma4ß3u -.mat4c -.mate3 -.märz3 -.md2 -.mee4ru -.mel4a -.me3no -.men6schl -.men6schw -.me3n4u -.me3nü -.me4sa -.mes4sp -.mes4st -.me4to -.met6tend -.mi2e -.mie4t5r -.mi2f -.mil4z3 -.mi4s3a -.mi6scha -.mi6schei -.mi6sch5l -.mi6schn -.mi6scho -.mi6sch5r -.mi6sch5w -.mi2t -.mi4t3a -.mit3e -.mit5in -.mm2 -.mo2i -.mo4nau -.mon6dru -.möbe4 -.musiker6fo -.nabe4 -.nae4s5c -.na4ha -.nah5ei -.namens5 -.na4mes -.nas4s5c -.na3t4h -.na3to -.nä4s3c -.nebe4n -.nebens6 -.neider6 -.nei6d5err -.nere3 -.ner4f -.ne3ro -.ne2s -.neuer6b -.nich6t5e -.ni4e -.nie5r -.ni2l -.nil3a -.no6tentr -.no4t3h -.no4top -.not3r -.nude4 -.nul6ler -.nus2 -.nu4t3r -.oa3s -.obe4n -.obe6r5ei -.ob6ers -.ob3i4t -.ob3l -.obs4k -.ob4sta -.oe2 -.oh4r5ei -.oh4rer -.oper4 -.orge4 -.orgi3 -.ort4s5a -.ort4s5e -.ort4st -.os4t3a4 -.ost5e4c -.os4t5ei -.os6t5en6de -.os6teng -.oste6r5e -.os4tes -.os4ti -.osto4 -.ost5om -.ost5ran -.ost5ri -.ot3a -.ou4te -.ozo2 -.ölk2 -.pa3b2 -.pa4nen -.par4ka -.par4k5l -.par5th -.pas6sers -.paulet6 -.pe4c -.pe3la -.pel4za -.pel4zw -.pe3na -.peri3 -.pes4t5e -.pf2 -.pf4u -.ph2 -.po4lei -.po4p3l -.postei6 -.prach4 -.pu5ert -.ra5ch6e -.ra4d5ei -.ra4der -.ra3el -.ran4d5r -.ran6g5e6b -.ran4gl -.rar4s -.rau5f4r -.rau4m3 -.rau6schl -.rau6schw -.rau4se -.re4b3l -.re5cha -.re5chr -.rei4bl -.re5insz -.reise5i -.rei4st -.reli5e -.ren4n5a -.re3sk -.rester6 -.res6t5ers -.re5stit -.rest5re -.re3un -.ri4as -.ri3so -.ro2a -.roe4s -.roes5c -.ro4h3a -.roher4 -.ro6h5erz -.rom4a -.roset6te. -.rost5e6c -.ro4tan -.ro4tau -.rot5ei -.ro4t3r -.rö2s -.rös3c -.rue3b -.rue6ck5er6 -.ruh4r3 -.ru4m3e -.rump6f5en -.runder6 -.rü3b -.rü6ck5er6 -.sa3br -.sa4fe -.sali5e -.sami3 -.san6dac -.sau3c -.sch4 -.scha4c -.schaf6t5end -.scheiner6 -.sco2 -.se2a -.se2e -.see3i -.seein4 -.see5r -.se4ha -.seh5ere -.se4hi -.se4h3o -.sen4f -.senf5l -.senf5r -.sen4kr -.se5no -.sen3s -.se3ro -.se3r4u -.se2t -.sex3p -.sha2 -.si2e -.siege6s5 -.si5err -.si3es -.silbe4 -.si4te -.sit6zer -.ski3e -.ski3s4 -.sli2 -.so3n4a -.sour4 -.spani5er. -.spiege6lei -.st2 -.st4a -.sta4ge -.stau6be6cken. -.st4e -.ste4h5a -.ste4i -.steiner6k -.ster6na -.sto4n -.sto4re -.st4r -.stro6ma -.su6cherf -.su6cher6g -.sue3b -.sumpf5l -.ta3c -.tage4s5 -.ta5lar -.tan4k5a -.tan4k5l -.tan4ko -.tan6z5er -.ta3ra -.tart4 -.ta4tab -.ta4t3h -.ta4top -.ta4tor -.ta4tri -.ta4t3u -.tau4f5o -.tau6schl -.te2e -.te2f -.tehe5 -.tei4l5a -.tei4l5ä -.teiler6s -.tei6l5ersc -.tei4l5i -.teils4 -.te5in. -.te5le -.telle4 -.te3ra -.te4st -.test5ae -.test5ä -.te6ste -.test5ei -.tester6h -.test5r -.te4te -.te3xa -.texter6 -.tex4tr -.ti2a -.tie6f5ern -.ti4me. -.ti4mes -.ti2s -.tischen6 -.tisch5end -.tisch5l -.tisch5w -.tit2 -.tit4a -.tite4 -.tode4 -.to4der -.todes5 -.to4nar -.to4nat -.tonen4 -.to6n5eng -.toner6n -.toner6z -.to4nin -.tons4 -.to4pan -.top3e -.to4pl -.to4pr -.tor5an -.to4rei -.to4th -.to4t3r -.to2w -.tow3e -.tra4de -.trag5s -.tri5es -.trost5 -.tsa3 -.ts4c -.tse3 -.ts4u3 -.tu4e -.tu5end -.tu3ri -.tur4m3 -.ub2 -.ue2 -.ue4b -.ufe2 -.ufer3 -.uh3l -.ul2b3 -.ult4 -.um1 -.ume2 -.umo2 -.ums2 -.una2 -.unab3 -.un3dr -.une2 -.unen4t5 -.un3er -.un3g2 -.unge5n4 -.uni4t -.unk2 -.un3s4t -.ur3a4d -.ur3af -.ur3a4m -.ur4anb -.uran6fa -.ur6anfo -.ur4anh -.ur4anp -.ur3at -.ur3c -.ur3e -.ur4e3t -.ur4i -.ur3o4m -.ur3o4p -.uro3s -.ur3s2 -.ut3r -.ves3t -.vi3d -.vol6lerw -.wah4l3 -.wah4n3 -.wanderer6 -.ware4 -.waren5 -.wa2s -.we4b5ei -.weg3s4 -.weh4r5u -.weine4 -.wei6neb -.weines6 -.we6ing -.wei4ta -.welter6k -.welt5s -.wer6k5er6 -.wer4k5r -.wer4tr -.wese4 -.wetterer6 -.wi2e -.win6d5er5 -.winte4 -.wo2r2 -.worten6 -.wort5end -.xan3 -.xe1 -.ya2l -.zeit5r -.zeit5s4 -.zel4l5a -.zelle4 -.zel6leb -.zel4li -.zel4th -.ze3ro -.zi4e -.zie4l5u -.zin4ka -.zin4s5c -.zin4st -.zi6schl -.zol6lers -.zuch4 -.zu5ende. -.zu4gei -.zu6g5end -.zu4glo -.zu4g5ra -.zug5st -.zurue4 -.zurü4 -.zwe2 -.zweigen6 -.zwei6g5end -a1ab -a2aba -aa2be -a3ace -a3achs -a1ae2 -aa2gr -aags2 -a3akt -aal3ac -aal3an -aal3ec -aa4lei -aale3s -aalf2 -aal5la -aa2l3o -aal3ö -aals4t -aal5ste -a1am -2a1an -a3an. -a2anä -aan3e -a1a2q -aa2r3a -aa3re -aa4r3ei -aa4r5ers -3aarg4 -aar3in -aa4rit -aa4r3oe -aar3ö -aarp2 -aar3t -aar3z -3aase -a3ass -aa2su -aata2 -aa2t3h -aa2t3r -aat2s3 -aatsche6 -a1au -a1ä2 -2abah -abais4 -ab4ak -2abal -ab3alt -ab2am -2aban -2abas -4abat -abat4t5a -ab3auf -ab1ä -2abäd -2abär -ab2äu -abbe4p -4abbine -abb5rig -1abd -abda3c -2abe. -2abec -ab3eic -abe3id -ab3eil -abein4s -abein4t -4a3b4el -abe4l5ac -abel5a4d -4aben. -abe4nin -a4berd -aber4ka -ab3erz -aber4zi -aber4zo -aber4zö -2abes -abes4p -ab5esse -abes4t -2a3bet -1abf -1abg -1abh -4abil -ab3inf -ab3inh -ab3ins -ab3iso -abi3s4t -1abk -ab1l -a2bla -ab3lag -3ablas -3ablau -a4bläu -4ab4le. -3a4bled -a4bleg -3ablei -4a3blem -4ab4ler -5ablesu -4ablet -a4bleu -a4blin -a4bloe -ab4log -a2blö -1abn -ab2o -2abod -2aboe -2abol -a2bon -abor5ang -abo5rato -abor3s -2abot -2ab2ö -ab1r -a4b3rec -ab3rie -ab4rot -2abrö -3abruf -1abs -2abs. -4absaer -4absap -4absar -4absär -ab3sc -ab3se -2absf -ab3so -4ab4sof -abs2p -4abspi -abs4pr -4abst. -ab3sta -ab3sti -ab4s5tro -3abtei -abtes3 -2abu -a4bueb -ab4ug -a2bum -a4bunt -ab3usu -a2büb -1abw -2aby -1abz -ab5zun -2ac. -2aca -1ac1c -a3cee -a3cem -a3cen -2ach. -ach3ab -ach5aen -ach3ak -ach5all -ach3an -a5chari -ach5art -ach3as -ach3au -ach3än -2achb -a3che -a4ch5e4ck -ach5e4he -ach5eic -ach5ein -ach5eis -a4ch3ep -ach5erbe. -ach5erben -ach5erbes -ach5erfu -ach5erfü -ach5erke -ach5erkl -ach5er6kr -a4ch5erl -a6cherne -a4chero -ach5eroe -a4ch5erö -acher6öf -a4ch5erw -a4ch5erz -ache5str -ach5eta -a4ch3eu -2achf -achgene6r -2achh -4achi -a3chi. -ach3i4d -a3chig -ach5ind -a3chip -ach3ir -2achk -2ach3l -ach5le -2ach3m -ach5mi -ach3n -2acho -a3cho. -ach3o4b -ach5ord -ach5org -ach5o4ri -a3chov -ach3öf -2achp -2ach3r -4achs. -4achsc -4achses -4achso -4achsp -achst4 -ach5stre -ach3su -ach5sze -ach4tak -ach6tant -acht5erfu -4achtg -ach4t5in -4achtk -ach6traeume. -ach6traeumen. -ach6träume. -ach6träumen. -ach6trit -4achtsc -achts6tu -ach4t5um -ach3u4f -ach3uh -a3chus -2achv -2ach3w -a1ci -acin3 -4ack. -a4ckeg -4a3cken -a4ckerd -4ackes -a4ck3in -2ackl -4acks -acksau6s -ack5sta -acksta6g -ack5sti -2a1cl -a3co -acon4n -ac2p -acre2 -2acu -2ad. -4ada. -ad4ab. -ad4abr -a2dad -a2dae -ad2ag -a2d3a2k -ad3ang -3adap -ada4rer -a6d5ar6ten -4adas -a4daut -1a2dä -ad3än -ad1c -2a3de. -ade4al -ad5eben -2a3dec -ade3e -adelei6n -adel6spo -3adelu -2aden -ade4nat -adenes4 -adenk4r -aden4se -ade3o2 -2adep -ade5ram -a4derg -a4derig -ader4sa -a5deru -2ades -ade5s4p -ades4s -ade5sta -ade5stel -ade5str -2adf -adgas3 -2adh -adie4s5c -adie6s5el -ad3inf -1adj -2admu -a2dob -ado3c -ado4n3a -adon3e -a2dop -ad4o3s -adot2 -2adp -2adq -adrat5s4 -a2dre -3adres -ad4ret -4adro -ad3rot -ad3rue -ad3run -ad3rü -2ads2 -3adsor -ad5stu -2ad2t1 -adte2 -ad4tel -adto2 -adt3re -a2düb -1adv -2ae. -aea2 -aeap2 -ae2bl -3aebte -aech4sa -ae3chu -2aeck -aeck5sp -ae2da -ae4ders -aede3s4 -ae4d3ia -aedi4e -ae2dr -2ae1e -aee2k3 -ae2f3l -ae2fr -aefts3 -2aeg -ae2ga -aeg3an -ae4gent -ae2gl -ae2gr -aeg3ri -aeg5sta -aeg5s4tr -3aegy -ae2ha -4aehe -ae4hei -ae4hel -ae4h3in -aeh6lein -4aehm -3aehnl -2aehr -aeh4rei -2aehs -2aeht -2aei -ae3im -a3ein -ae5isch. -ae5ischl -ae5ischs -ae5i6sch5w -ae3isk -a3eiw -ae2kr -a3el. -ael4bl -a3elh -ae4lia -aelk3s4 -a5ella. -ael4lak -a3ello -ael4pl -a3els. -a3el4sa -a3elsb -ael6schl -a3elsf -a3elsg -a3elsh -a3elsk -a3elsl -a3elso -ael4st -a5elsta -a3elsv -ael3te -aelte5i -4aemm -aemte4 -2aen. -2aene -aene4n3 -aen2f3 -4aenge -2aeni -aen3k4e -aen4kl -aenne4 -aenner5 -2aens -aen4s3a -aen4s3c -aen4sp -ae3nu -2aenz -ae2o3p2 -5aepfel -a3e2pi -1ae2q -2aer. -aer3a2 -3aera. -ae3r4af -ae4rap -ae4rat -aerauto6 -aerb3l -aer4ble -2aerd -ae4r3ei -ae4r3e4l -aere4m -ae4r5emi -ae4r5erf -ae4r5erh -aerer4k -aer5ersa -ae4rese -aeri4e -ae4r5ing -4aerk -aer4mau -aer4m5in -aer6munt -ae3ro -3aerod -ae4rop -ae4ror -aero3s -2aers -a3ersa -aer4seh -aers5er -4aert -aert6er -aert4s3 -ae4rue -2aerw -a3erwä -aer6z5en6t -aer6z5err -2aes4e -ae3se3i -aese5rei -aese5ri -ae3s4kr -ae2sp -2aess -aes4s3c -aess5erwe -aes4so -ae4st -aes3te -aestein6 -3aesth -aeß5erk -aeß5erw -ae4t3a -aeta4b -aet4eb -aete3e -aeteo4 -aet5erbl -ae2th -4aeto -ae4tob -ae4t3op -ae4t3ra -ae4t3ri -ae4t3ro -aet4sen -aet4ste -aet4sti -aet4str -aett3s4 -ae2u -aeu4eri -2aeuf -3aeugi -5aeugle -4aeul -2aeum -4aeur -aeu5re -4aeus. -aeu6scha -aeu6schr -aeu6schw -aeu4sp -ae2vi -a1e2x -2afa -a4f3aen -a2f3a2n -a2fap -a2f3au -2afä -a2f3än -2afe -a2f3ec -a2f3ef -a4f5einh -a4f5einr -afel3e -a4fentl -a2f3e2p -afe4re -afe2s -a4f3ese -a2fex -aff4a -3affär -af4fei -3affek -af4flu -aff2r -2afi -afi2e -afie3i -afi6kanz -afi4kat -afi4k3l -a4f3ins -afi2t -2afl -af3lae -af3lau -af3lä -2afo -a4fober -a2f3oc -a2fop -a4ford -a4f3ort -afo3s -a2f3o2v -2afra -af3rae -a3frag -af3rat -af3rä -af3rie -a3fris -2af2ro -af3roe -af3rö -af3run -af1s -af3sa -af2si -afs4ke -af4soc -afs2t -af4t3ak -aft5anz -af4t3au -af4tei -af4t3e4l -af4t5ent -aft5erfa -aft5int -af4toe -af4t3op -af2tö -aft5rad -af4t5res -af4t5ric -aft4sei -aft4sel -aft4spi -aft6stic -af4tur -2afu -a2f3um -a2f3ur -2afü -2aga -ag3a2d -a2g3au -ag3ärm -agd3an -ag4d3ar -ag4del -ag4det -ag4din -ag4dre -ag4dro -4age. -ageb2 -age3id -age5ind -age5inf -age5inh -age5in4s -2agel -agel5d -agelei6s -a4gelem -age4len -age4ler -4a5gen. -age4nas -agend4 -age4n5e4b -a6generg -agens4p -5a4gent. -agenta6bl -5a6genten -a4gentf -age4rad -age4r5af -age4ral -age4r5ap -age6rauf -agerei6g -ager6eign -agerer6f -age4s5am -age4s5an -age4sau -ages5ka -age4s5oe -age4sö -ages6sen -age3u -3aggr -a2g3i2d -ag3ill -a2g3im -ag3inn -ag3ins -agi3oe -agi4os -2agl -ag4lan -ag3lat -a2gli -a2glö -a2glu -ag2n2 -a2gnä -3agog -ag3opf -agori4 -ago2s3 -a4g3rad -a2gre -a2gri -ag3rie -ag3rin -a4g3rum -ag2sa -ag3s4ah -ag2se -agsen4t5 -ag4s3in -ag5span -ag5s6port. -ag5s6porta -ags5taet -ag4stät -ag5stel -ags4top -ag5stras -2agt -aguet4 -a4gund -2a1h2a -ah3ang -a1hä -ahdi3 -a1he -a4hein -ahein6se -a4h3er4h -aher4ku -a4h3er4w -ahe3s -a1hi -ahin5d -ah2l3a2 -ahl3än -ah4l3e4b -ahl3ec -ah4l3ei -ah4lel -ahlenk4 -ah5lenz -ah4l5erd -ahl5erf -ahl5erg -ah4l5er4h -ah4l5erz -ahli4n3 -ah2l3o2 -ah2lö -ahl3sa -ahl4sal -ahl5se -ahls4pa -ahl3sz -ah4m3ae -ah4m3ar -ah2m3ä -ahme3e -ahme3i -ahmen5e -ahmen7sc -ahme3s4 -ah3mu -ah2n3a2 -ah4nas -ah2n3ä -ah3nee -ah4nef -ahner6e -ahn5er4h -ahn5erk -ah6nerle -ahner4n -ah4n5erp -ah6n5ersa -ah6n5er6sc -ah4n3eu -ah4nin -ah2no -ahns2 -ahn3sa -a3hom -a3hop -3a2hor -ah3ost -a3hot -a1hö -ahr3a -ahra4b -ahra4g -ah3rai -ahr5eig -ahren6se -ahren6s5o -ahr6ere -ahrer6lau -ahre4s -ah3ri -ahr4tae -ahr4t5ei -ahr4t3h -ahr4tin -ahr4t5ri -ahr4tro -ah2ta -ah4t3e4l -aht5ero -ahte4s -ah4t5ese -ah4t3ex -ah4tis -aht3re -ah4t3ri -aht3s2 -ah4t3um -a1hu -ah3w -a1hy -ai1a2 -a3ia. -aian3 -aidi2 -ai3dr -aid2s -ai1e2 -aien3 -aien4de -ai3er3 -aif2 -ai3fr -aig2 -ai5gel -a3ik. -ai4k3er -ai4lar -ai4l5auf -ai4lei -ail5er4l -ailge3 -a3im. -a3imp -2ain -ai3nac -ai3nal -aind2 -a3indu -ai4neb -ainen4 -ai5ner -a3info -ains4p -a4inv -ai1o -3airb -3airl -airo3 -air2s -ai2sa -ais3ac -a5isch. -ai4schi -a5ischs -aiser5a -ais4o -ai3spr -aiss2 -ais4se. -ais4sem -ais4ses -ais3t -ai3s4tr -ai3ti -ait4r -a3iv. -a3ivl -a3ivs -a2jat -2ak. -2a3ka. -aka3b4r -a4kade -2akae -a3kal -2akam -2akan -3a4kanl -2akar -aka4tak -aka4tan -aka4t5er -aka4t3r -akat3s -3a2kaz -2akä -2akb -2akc -1akd -2a1ke -a2kef -ak3eis -3a2k3em -ake2n -aken3e -a2keo -2akf -2akh -2a1ki -ak3ins -aki3p -1akk -4akko. -2akl -a3klec -ak4lin -2akm -3a2kne3 -2ako -ako3c -akoni4e -3akox -2akp -2akra -2akre -2akri -ak2ro3 -a2k3ru -a2k3rü -2aks -ak3sh -ak3sta -ak3ste -ak4t3a4b -ak4tag -ak4t3ar -ak4t3au -2aktb -akter5a -2aktg -ak2th -4aktik -aktionsche6 -4aktis -2aktm -ak4t3ob -ak4toe -ak2tö -ak4t3re -ak4t3ro -ak4tru -4aktsi -4aktsp -4aktst -4aktun -2aktw -2akul -2akun -2akur -aku2s3 -2akw -1akz -2a3la. -al3abb -4alabo -a4l3abt -a3lad. -al3add -al3adm -al3aen -al3af -2alai -ala3kl -al3akr -al3ame -alami3 -al3ana -a3lanc -4al4and -a4l5ande -al5angel -al5angr -alang5s -ala3ni -al3ank -al3anm -al3ann -al3ans -al3anz -al3app -a3lar. -al3arb -al3arg -a3lari -al3arr -al3art -a3las. -alaska5 -al3asp -a3lat. -ala4tan -al5attr -alat3z -al3au -a3l4au. -alauto6r -a3lay -a1lä -a2l3än -al3ärm -al3äu -alb5aer -al4bär -3albb -4albe. -alb5eink -al4b5erh -al6b5er6sti -al4b5er4w -al4b5eta -alb5ins -al4ble -al4bon -alb5rae -al4b3rä -alb5rin -alb5ros -alb4spe -3album -al1c -alda3c -al2dä -5aldehy -ald5enkl -al4d5ere -al4d5erh -al4d5erl -al4d5ern -al4d5erw -al4d5ese -ald5inn -al4d3ot -al4drä -al4dro -ald3s4t -al4duf -2ale -al3e4be -alebran6 -a2l3ef -al3eie -a4l3eig -a4l3ein -a4l5eise -al3ele -al3elf -3a4lema -a4lemb -ale4mis -3a4l3emo -al3emp -alende6r5 -a4l5endr -a4l5ends -a4l5endu -a4l5engl -alen5in -alent6ei -al5enth -alen6z5ei -al3e4po -a4l5erfo -al4erg -alergene6 -a4l5erhe -a4l5er4hö -aler4kl -aler4kr -a4l5erlä -a4l5er4mi -aler5oe -a4l3erö -3alerr -a4l5erse -a4l5er4tr -5alerwa -5a4l5erwä -a4l5erze -a4l3e4s4k -a4less -a4l3e4th -a4l3eur -al3exi -al3ext -a1lé -al5gas -algene6r -3algor -alg2r -alig4h -al3imm -al3imp -a4l3in4q -alin6stin -al3int -al3inv -ali3o -al3ira -al3irr -ali3sp -ali5str -a4l5ital -al4k5enz -alk5erd -al4k3lä -al4klö -al4klu -al4kne -al4kof -3alkoh -al4k3rü -alk3sa -alk3se -alks4i -alk3sp -alk3st -al5lad -all5ank -all5ano -all5ans -all5anz -al4l3a4r -alla4s -al4lass -al4laus -3allee -all5eim -all5erfa -all5er6mu -all5ermü -al6lersa -al4l5er4z -al3les -al4lev -3allgä -3allia -al4l3id -alli5er. -alli5ers. -all5ink -al4l5oef -alloe6se -all5ora -al4l3öf -al4l5ö4se -all6s5amt -all4sin -all4sph -al4lum -alm5aus -almo6de. -al3n2 -2alo. -alo2b -al3obe -al3obl -al3obr -al3obs -al3oe4d -al3oer -a2lof -2a3log -alo4g3a -alo4gn -alo4gr -alo5is. -a3lop -al3ope -al3opf -a4l3orc -al3ort -2alos -al2ov -alo2w -a2lö -al3ö2d -al3ör -3alpe. -6alphas -alp3st -als5aer -alsau4g -alsch5s -al3s2e -als4pac -al5s6temp -al3sti -alt5alg -al4t5ana -al4t4an4b -al4t5ans -al4tap -alt5auf -alt5eic -al4t5eig -alt5ein -al4t5eis -alt5elt -alter6fol -al4t5eti -al4teu -al4tid -al4tir -alt5ita -al4toe -alt5ort -al2tö -al4t5rat -alt5ric -alt5rit -alt5roc -alt5ros -al4trus -a1lu -a2lum -al3umb -3alumi -a2lur -a4l3use -alu3ta -a3lüg -2aly -al4z3ap -al4z3ar -al4zet -2am. -ama3a -am3a4ba -ama3d -4amae -am3aep -ama5ers -ama5ge -3a4malg -am4an -am5ang -aman6the -aman4tr -am3apf -amas5z -a2maz -2amä -3ambiv -2amc -2amd -4ame. -am3ein -ame5len -a4memo4 -ame4n -amen3e -amen3i -amen4s5e -amen4su -amen6tarb -am2er -amer4a -amera5u -a4m3erf -ame3r4u -a4m3erz -a4mesh -ames4p -a4me3st -2a3met -a2mex -2amf -2amg -ami4kan -ami4kl -ami4kn -ami4n5an -amine4 -ami3o -amis4tr -4amix -2amk -2aml -4amm. -am4maa -amm5aba -am4m3ac -am4mal -amm5ang -am4map -am4mar -amm5ass -am4m3au -amm3ei -amme4la -amme4n -am6m5es6sen -am4m3id -4amml -am4m3oe -am2m3ö -4amms -am4munt -ammu4t3 -a2m3oc -amo2k -amotte5s -am2pf -amp4f3a4 -amp6ferr -ampf5la -ampf3o -ampf3r -amps2 -3ampul -2am2s -am3sau -am5sche -am3se -ams3en -am3so -am3spa -am3spi -am3s4tr -am3su -am2t3a -amta4b -am4tän -am4t5eig -am4t3el -4amtem -amter4 -am4t5erb -am6t5erei -am4t5erf -am4t5erg -am4t5erh -am4t5eri -am4t5erk -am4t5erl -am4t5ern -am4t5err -am4t5ers -am4t5ert -am4t5erz -am4t3ex -am2th -am5tig -amt5int -am4tis -am4tit -amto2 -amt3ob -am4toe -am4t3op -am4tou -am2tö -am4t5rat -amt5rei -amt5ric -am4tro -3amts. -3amtsd -am2tu -amt3ur -2amtv -2amu -a2m3uf -3a2mul -a1mü -2amv -2amw -a1na -a3na. -ana2a -a2nab -4anac -ana5che -anadi5er. -an3aes -an3aeu -ana4l5in -an5al4ph -anal5sk -3analy -ana3m -an4and -an3a4no -an3ant -2anar -a4narb -an4are -an4arr -a3nas -ana4se -a3nat. -ana4ta -ana4t3r -a3nats -3anäm -an3är -3a2n3äs -an3äu -1anb -4anban -4anbas -4anbib -2anbo -2anbu -2anbü -anche4n5 -an3cht -2and. -4andaa -4andab -3an3d4ac -and5achs -4andal -and5amm -4andan -and5anz -an4d5arm -an4d5ass -4andat -an4d5att -4andaw -2andd -4ande. -an4d3ei -and5elfe -an6den6ga -and5ente. -ander5ei -anderer6le -and5erhö -ande4sa -ande6spo -andes5ta -an4d3ex -2andf -2andg -andi4e -ando3s -and5ott -and5rai -an4drau -an4drec -an4d5res -2ands -and4sam -and4seh -and6s5ein -and6spar -and6spas -and6spat -and4sph -an4düb -2andw -andy3s4 -an3e2c -a3n4ei. -3aneig -a3neip -an3e2k -ane2l -an3elf -an3e4mi -3a4nemo -ane2n -anen3e -anen4ze -anen4zy -aner4nä -a4n3eru -a4n5erz. -an3eth -a2n3e2v -1anf -2anf. -4anfab -an4f3ak -an5fes -2anfi -anfi4n -anfin5i -anf5rau -2anfs -an3f2u -4anfun -4ang. -4anga. -3angab -ang5art -4angas -ang2ä -an3gäh -an4gäs -4ange. -5angebo -5angeho -5angehö -ang5eig -ang5eis -3angek -an4g5erf -an4gerg -ang5er6he -an4g5erk -an4g5er4p -ang5ertr -an6g5erwa -an4g5erz -3angez -4angie -ang4lok -angmi4 -ang3n -3angol -ang5rad -ang5ras -an4grau -5angrif -4angs. -ang5sat -ang6st. -ang5sti -ang3t2 -1anh -4anhau -4anher -4anhex -2anhu -2anhü -2ani -4ani. -4anie -aniel4l -ani5els -ani5ers. -anie3s -ani4ka -ani4ki -ani4kr -3a2nim -an3i4on -a4niso -ani5sta -4ank. -an4kag -ank5alt -ank5anz -4anken -ank5erfa -4anki -an4kid -an4k5las -an4klö -an4k3ob -4ankon -ank5ori -an4k3öf -an4k5ras -an4kro -ank5rue -an4krü -4anks -ank3s4k -ank3s4p -ankt2 -an4küb -3ankün -1anl -4anlad -4anlan -4anman -3anmel -3anmie -3anmu -4anmun -anna4ge -3annah -3annä -an4nef -annens4 -anne4s -3annex -ann4spa -2annt -3anod -an3ode -anoe4d -an3ofe -2anog -a3nol -ano4l3a -an3oma -ano4m5en -an3opf -an3opi -an3or -ano5rak -3a4nord -a4no4ri -anor3t4 -a3nos -an4ou -a3nov -a1nö -an3ö2d -1anp -4anpaa -4anpag -2anpo -1anr -4anrad -2anrö -an3s4ab -an4s5aem -an4s3al -ansa4m -an4s5ame -an4s5amt -an5s4au -3ansäs -ansch4l -an4seu -3ansic -ansicht6 -an4sim -an4spo -ans5pon -3anspr -5anstal -an4stem -an4s5ura -ans2z -an3tac -an4t5a4go -ant5ang -ant5ans -an4t3ar -anta4re -3antá -3antei -anteri4 -antes3 -ant5e4ta -5anthro -3antib -5antilo -ant5inst -3antiq -5antise -3antr -ant5räu -2ants -3antw -a1nu -anu5ell -a3nup -a3nur -a1nü -1anw -2anwi -any3l4 -an4z5aep -an4z3an -an4z5art -an4zau -an4zäp -2anzb -an4z3e4d -5anzeig -anz5elf -anze4n -4anzes -anze4t -anz5eta -2anzg -2anzh -an4z3i4d -anzi4n -an4z5inf -an4z5ins -anz5int -2anzk -2anzm -an4zol -2anzr -2anzs -2anzt -5anzuen -3anzün -an4zwä -an4zwi -2ao -a1oe2 -a1of -ao2fe -ao1i -a3okt -a2om -a1op -ao2pe -a1or -a3orm -aor5te -a1os -ao2sk -aos2p -aos3t -a3ot. -ao3t2s -2a1ö2 -2ap. -ap2a -apa3b2 -a2ped -a3pel -apen3 -ape4ra -a2pev -2apf. -ap2fa -4apfe. -3apfel -4apfen -2apfs -2aph -aph5aer -aph3är -aph5ers -a2ph3t -2a1pi -api2e -apie5l -apie3s -2apl -a3pla -a3plä -ap3li -ap2n -ap2o -2apol -apo3p2 -a5posi -apo5sta -apos5te -apo5str -a2pot -3appar -ap4pei -ap2p3f -ap4pre -ap4p3ru -a1pr -ap4san -ap4sin -ap2s3p -apst3o -apst3r -apt2 -a4que. -2ar. -a1ra -a3ra. -ar4a3a -ar3abb -ar3abt -ar3adr -arad4s -2arae -ar3aer -ar3aes -araffi4 -a5rafo -arafor4 -ar3agg -arag4l -ar4ah -ara3i -4aral -ara3le -ar3all -ar3ang -a4ranr -ar3ans -a4ranw -a4r3anz -ar5apos -a4r3app -2arat -ar3atl -ar3atm -ar3au -arauto6r -ar3ax -2a1rä -ar3äs -ar4bab -4arbe. -4arbef -3arbei -ar4b5eig -ar4b5eim -ar4b5ein -6arbeitr -4arben -arb5erl -4arbi -ar4b3le -2arbs2 -arb3sk -2arbt2 -2arbu -1ar1c -4archl -4archr -ar4d5anz -ard5arm -ar4d3ed -arde4n -ar4dob -ar4dop -ar4dro -ards2 -2are. -a2rea -are5aler -a2reb -are5ba -2arec -a2re3e2 -a2ref -ar3eff -areg2 -ar3ehr -a4r3eid -ar3eig -areim5 -a4rein -2arem -ar3emb -a4remi -4aren. -a4renah -arens4 -aren6sem -a4r5entn -aren4zy -a2reo -4a5rer. -are3r4a -arerau5 -a4r5erei -a4rer4fo -a6rergeb -a4reri -a4r3erk -a4r3erl -a4r3ert -a4r3erz -arer4zi -2ares -a4r3es4k -ares4p -a4r3es4s -are5sta -are5str -2aret -are5the -a2reu -a2rev -arfs3a -arf4s3p -ar3fu -2arg. -ar4g3e4c -arge6l5er -ar2g3n -4argo -ar5gre -a1r2i -2ari. -2aria -arie4n3 -ari5er. -ari5erbu -ari5erd -ari5erg -ari5erh -ari5erp -ari5ers. -ari5ersc -4arif -arif3t -ar3imp -2arin -arin5it -arin3s4 -a4r3int -ar3inw -2a3rio -a3rir -4arisc -ari4su -a4rith -2a3riu -3arkad -ar4k3ak -ar4kal -ark5aue -ar4käh -ar4k5erh -ar4k5er4l -arker4n -ar6k5erne -ar6k5er6sc -ar4k5erw -ar4kid -ar4k5inn -ark4lö -ar4kne -ar4koa -ark5rin -ark3sa -ark5she -ark5spe -arks4t -ark5stu -ark4ta -arkt5a4g -ark4tel -ark4t5ex -ark4top -ark4tre -arku2 -ar4k3um -ar4küb -arlang5 -ar4les -arlichter6 -arlich6ters -arl2s -arma3l -ar4m5ans -ar4m5anz -arm5art -5armatu -arm3au -ar4meb -arme4e -ar4m3e4g -arm5ein -5armeni -ar4meo -ar4mep -ar5mes -ar4m3op -2armö -arms2 -ar4mum -3armut -2arn -ar4nan -ar4nap -arn4au. -arnte4 -a1ro -ar3ob. -4aroc -aro6ckeng -ar3o4di -2aroe -ar3oem -2arof -ar3ofe -ar3off -aro2i -2arok -2arol -ar3o4no -ar3o4pe -ar3opf -2a2r3or -aros5t -2arot -arot5an -aro4to -a4roun -aro2w -a2rox -2arö -ar3öm -2arr. -arr5ach -5array -2arrä -ar4r3e4b -ar3rei -ar6r5ei6sen -arre4n -arren5e -4arri -ar4r3or -2arrs -2arrt -2ar3s2a -ar3sä -ar6schab -ar6sch5ac -ar5sche -arsch5erl -arsch5lä -arseri4 -arsex3 -ar3s4ha -ar3si -2arsk -ar4ska -ar3sp -ar3sta -ar3stä -ars5tät -ar5stel -ar5stern -ars6t5ers -ar3str -ar3su -ars4ue3 -artal4s5 -ar4t3am -ar4tang -art5att -art5auf -art5elf -arter6fo -arter5k -arter6la -3arthr -4artie -3artig -art5int -ar4t5iso -ar4t3ob -ar4t5ram -art5ren -art5rie -ar4t5rol -art5ske -a1ru -arude4 -aruder5 -a2r3uh -a3rumm -ar3ums -2arü -a3rüc -a3rüm -arwa4s -2ary -ar4z3ae -ar3zan -ar4z3au -ar2zä -ar3zäh -arze2 -arz5elf -arz5ente -arzer4 -ar4z5erl -2arzi -3arzn -3arzt -arz4th -arz4ti -arzt5ra -arz4tri -ar2zw -asa5chi -as3aeh -as3aet -4asaf -as4ai -asal4t5e -as3amp -asa5ne -a2sap -asa4ro -asa2s -as3asc -a5sau. -asau4f -as3aug -as3ät -a2sca -as5cham -a5scher. -ascher6g -a3schi -asch5mi -asch5ru -4ascht -a4schwu -a4schwü -a2sco -2ase -a2sec -a2s3ef -as3eie -as5elef -a3sen. -ase4na4 -asene4s -ase6n5esc -asenk4 -ase4n3o -a6sen6sem -asens4p -aser3a -aser5eig -ase4ren -as4erh -as4es -as3eva -a2sex -asex3p -2asf -a3s4hen -a3s4hig -3asiat -a2s3i2k -as3imp -as3ind -asing3 -as3inn -as3int -asi4s3a -a2sk -as5kla -aslan4 -a3slip -a2so2b -as3oe -aso2f -as3ofe -a3sol -a3som -a2s3o2p -as3orc -as3org -as3ori -as3pan -aspek6t5o -aspi5en -as3pik -as3pio -a4spir -as4ple -as4por -as4pul -as5s4abl -as4s5amt -as4scr -ass5eins -assens6k -assens6p -as5s4er -asser5a -asse6r5ec -asse6reis -5assess -3assim -as3ski -as4soe -as4s5ora -3assoz -as2sö -as4spo -as2st -ass4tie -ass4top -ass4tue -as4sur -a4st. -a3s4tad -ast4af -ast5ans -ast5ant -as3tap -asta3s -as5tatt -ast5auf -ast5einl -a4ste4n -ast5ents -as3ter -as6t5er6ho -as6t5er6hö -aster5oe -aste4s -as4t5ese -as4tex -as2th -3asthm -asti4e -asti4ka -a2sto -as3toc -ast5orc -as4t5rap -as4trau -ast5raum -as4tre -as5tren -as6t5ritt -ast5roll -as4tru -ast6ruk -2astu -as4tud -as2ty -asu2s3 -aswa4s -as2y -2asy. -a2syl -3a4symm -aße2 -aßen3 -2at. -2ata -a2t3aa -at5a4ben -a4t3abl -at3abr -at3aer -ata3la -at3apf -ata3ra -at3arb -at3art -ata3s2 -at3atl -at3att -a4t3auf -ata3un -a4t3aus -at3är -2atb -2ate. -ate4au -a2teb -a3teh -ateien6d -atei3f -atei3s -3a4teli -4atell -atel6l5erk -3a4temg -a4teml -a5tempe -a4tems -2aten -aten6t5ank -aten6t5ri -ate5ral -aterer5 -ate4ro -2ates -ates4sa -at4eur -ath4a -a4t3hee -at5herd -a4thin -ath5in. -at3hir -3athl -a4t3hoc -at3hof -a2th3r -2at2i -at3idi -ati4kab -ati6k5er6w -ati4kl -ati4s3a -ati4s3e -at4isl -a4tiso -ativ3a -ati4vat -ativen6d -ati6v5er6f -ati6v5er6s -4atle -4at5mus -at2o -a2t3o2b -at3oef -at3oel -a4t3oer -at3o2f -a2tom -ato4man -ato4min -3atomk -a3tomo -3atoms -at5opfe -a4tops -2ator -ato4ra -a4t3orc -ato4rei -atorgene6 -ato3s -ato2w3 -a2t3ör -at5raet -at5raeu -a4t3rau -a4t3rav -at3rän -a4t3rät -a4t3räu -at3rec -a4trep -at5ring -at3roh -at3rol -a4t3rom -4atron -a4troo -3a4trop -a4t3rot -at3rue -at3ruh -at3rut -a2t3rü -2ats -at6schma -at6schme -ats3e4h -atse4l -at4set -at4s3in -at2so -ats3pr -at4stoe -at4stop -at4stö -at4stur -3attac -at4t3ad -at4tah -att5akt -atta3s4 -at4t3au -at4t3ec -4attel -attens4 -4atter -at6t5ertr -at4thu -at3ti -at4t3o4b -att3o4f -5attrak -at4t5ras -att5rat -att5räu -att5ros -att3s4 -2atu -atu3et -atu2n -atu4rei -atu4rer -atur3z -atus3e -atwa3r -2atz -at2za -at2zä -at4z3e4c -at4z3e4d -at4z3eh -atz5eig -atz5ela -atz5elt -at4z3em -atzen5e -atz5erb -atz5erf -atz5erg -atz5erh -atz5erke -atz5erkl -atz5erkr -atz5erl -atz5ero -atz5erp -atz5ersc -atz5ert -atz5erw -at4z3e4t -at4z3in -at2zo -atz3op -at2zw -a2u -2au3a2 -au1ä -2aub -au4b3ab -aube4e -aub5ele -aube4n -auben6se -auberin6s -au4b5eul -au4b5ins -au4b5lae -aub5las -au4blä -aub5rin -aub4spo -aub4spr -au4b3um -2auc -auch5e4c -au6ch5en6t -au6ch5erke -auch5sti -2aud -au1e -4aueb -aue2d -au3eg -auene4 -auens4t -auere4 -au5erel -au5erent -aue4r3o -aue2s -aufa4c -auf5akt -au4f3ar -auf3ei -aufen4s -5aufent -auf5erf -auf5erh -au4f5er5i -auf5er6kr -auf5erla -auf5ind -3auflö -3aufn -4aufo -auf3re -5aufruf -auf3t2 -4auft. -5aufträ -au4fum -3aufzü -4augeh -augene4 -4auger -5auges. -2augl -4augr -augs4t -aug5str -3augus -au1h -auh4a -2a3uhr -2au1i -aui2n -2auj -au4kle -auk5te -au4l3at -aul3e4c -aule4s -aul5ese -aul4les -au3lü -2aum -au4mad -au4maeh -au4mäh -aume4n -aum5ents -aume4r -au4m5er5i -au4m5ern -au4m5ers -au4m5erz -aumgene6 -au4m3id -au4mind -au4m5ins -au4mis -au4m3o4r -au4m5ott -aum3p2 -au4ms2 -au4mum -au4mun -2aun -aun4ac -aun5alg -au3nas -aun4aus -aun3d -aun2e -au4n5er4d -aun5erze -a4unf -au4nio -aunk2 -au2no -au3nu -au1o -2aup2 -au3pf -aupt5er -au1r -aur4a -auran4 -aurant5 -aur2h -au3ru -au4s3ah -ausan6ne. -au4sap -au4sas -au4s3au -4ausc -au6schan -ausche6f -au6sch5lag -au6schli -au6schmi -au6sch5wi -au4scr -3ausd -4ause. -ause4e3 -aus5ehr -aus5ele -aus5elf -au4s5erb -au4s5erf -aus5er5in -au4s5erk -au4s5erl -aus5erpr -au4s5erw -3ausgl -au4s3in -au4sis -aus5kr -2auso -au4s3or -au4spic -3ausrü -auss2 -5aussag -aus4se. -aus3sz -au5stae -au5stal -au5stat -aus5tauf -4auste. -au5stein -au5stel -4austem -4austen -4austes -5austrag -au5s6tras -au4s5tür -3auswu -3auszü -auße4 -außer3 -aut5aeu -au4t3äu -aut5ein -au4t3e4l -au4t5ero -au4ters -au4tev -aut3ex -au4thy -2auti -3auto -auto6renk -auto6renp -auto6rens -auto6rent -au4torg -auto6rinn -aut5rau -aut5rin -aut5rue -au4trü -auts2 -aut3sk -aut3z -2auu -auve4 -2auw -2aux -2auz -a1ü -av2an -avari4e -a2va3t4 -a2v3ex -2avi -avie6rer -a2vl -2avo -a2vr -2avy -2aw -awi3e -a2wr -a1x -ax2am -ax2at -ax3emp -ax3erw -3a2xio -2a1ya -a2y3ak -ay4ere -ay2es -a3yeu -ayma2 -ay3of -ayou2 -ays2 -aysi3e -ay3t -ay2u -az2a -a3zad -3a2zal -aza3s2 -az2o -2a3zu -az4ur -2az2w -azz3a2 -az4z3en4 -az4z3in -az2z3w -ä1a2 -äap2 -ä2bl -äbni2 -äb2s -äbte3 -ä1ce1 -ä3che -äche3e -äche4n3 -ä3chi -ä2chr -äch4s3a -äch4s3o -äch4sp -äch4st -ä3chu -ä1ck -ä3cke. -ä3ckes -äck3sp -ä2da -ädel3a -ädel3e -ädel3i -äde3s2 -ä2d3ia -ädi2e -ä2d1r -äd2s -äd3te -ädte3e -ädte3i -ädtein4 -ädte3s -2ä1e -äe2j -äe2k3 -ä2eri -äe2x -äfermi4 -3äffc -äf2fl -äfig3s -äf1l -äf1r -äf2ro -äf3te -äft2s3 -ä2g1a -1ä2gä -ägd2 -ägdi3 -ä4g3ent -äge4ra -äger5as -äge6rend -ägeren6g -ägerer6k -äge6rers -ägerin6d -ä2g1l -ä2g3ne -äg2ni -ä2g1r -ägs2c -äg3sta -äg5stel -äg3s4tr -1ä2gy -2äh. -äh1a -2ä1he -ä2h3ei -ä2h3e2l -ähe2n -äher6gebn -ä1hi -äh3in -äh4l3a -äh4l5e4be -ähl5ein -ähl5ins -2ähm -äh3na -äh3ne -3ähnl -ähr3a -ähre2 -äh4r3ei -äh4r3el -ähr5sa -2ähs2 -äh3stu -2äht -ä1hu -äh3w -2äi -ä1im -ä3is. -ä5isch. -ä4ische -ä5i4schl -ä5ischs -ä5i4sch5w -ä3isk -ä1k -ä2kle -äk2li -ä2kr -ä1la -äl3an -äl2bl -äl1c -äl2l3a -älle3s -äl2pl -äl4schl -äl3te -älte3i -ältein4 -ä1lu -2äma -äma3t -2äml -ämon2 -ämoni5e -ämte2 -äm2th -2ämtl -2än. -ä1na -ändes4 -än2dr -2äne -äne2n3 -äne3p -äne3s -än2f3 -änge4ra -ängerin6s -än2gl -än2gr -2äni -2änk -än3k4e -änke3i -än2kl -än2kr -änne4 -änner3 -2äns -än2s3a -än2s3c -än2sp -ä1nu -2änz -ä1on -äo3s -äos4e -2äpfc -3äpfel -äp2pl -äp2pr -äp2s3c -1ä2q -ä2r1a2 -3ära. -är2af -ärana3 -ärauto4 -är1ä -är4b3le -är1c -2ärd -ärde4n -ärde4s -2äre -ä2r3ei -ä2r3e2l -äre2m -ä4r3emi -äre2n -ären3a -ä4r5ener -ä4r3erf -ä4r3erh -ä4r3er4k -ä4r3erz -ä4rese -är2gl -ä1ri -äri2e -ä4r3ind -ä4r3ing -är3ins -ä4r3int -ärm5arm -är4m3au -ärme3e4 -är4m5ent -är4m5erz -ärme5sz -är4m3in -är4mof -är3o2b -är3of -är3o2p -är3or -ä1rö -ärp2 -är4seb -är4s3eh -ärs3er -är2sz -ärt2s3 -ä1ru -ärz3au -ärzen4t -ärzer3 -ärz3te -äsche5i -äsche5s4 -2ä3se -äse3e -äse3g -äs4e3i2 -äsein4 -äse3la -äsen4s -äse3re -äse4ren -äse5r4i -äse3u2 -ä3s2kr -äs1p -2äss -äs2s3c -äss5erkr -äss5erwe -äs2so2 -äs4spr -äs4ste -äs3te -ästein4 -3ästh -äs2tr -ä4ßent -äß3erk -äß3ers -äß3erw -ä2t3a2 -ätab3o -2äte -ät4eb -äte5be -äte3e2 -ätein4n -ät3eis -ät3emp -äte2n -äteo2 -ät3e4pi -äter4bl -ätergene6 -ätes4p -ä2th -ät3id -ät3ob -ät3op -ät1r -ät2sa -ät2sä -ät4schl -äts3en -ätsi2 -äts3p -ät2st -äts3tä -äts3te -ät2su -ät2t3a -ät4tei -ätte4n3 -ät2tr -ätt3s2 -ät3v -ät2za -ät2zw -äu2b3l -äu2br -äu1c -äude3 -äuder4 -äu1e -äuel3 -äu4eri -2ä2uf -1äug -äu2gl -2äul -2äum -äu2ma -äum3p -äum2s -äum3sc -ä2un -äun2e -äu3nu -2äu3r -äu5re3i -äurein4 -äure3s -2äus. -äu4scha -äu4schä -äu6sch5ei -äu6schel -äusch5erz -äu4schi -äu4schl -äu4schm -äu4schn -äu4sch5o -äu4schr -äu4schü -äu4schw -ä3usd -ä5useff -äu3s4e3i -ä3usg -ä3usk -ä3usn -äus4s3c -ä3usst -1äuß -äu2tr -ä2vi -1äx -âte2 -á1 -1b -ba2b3a -ba4b3er4 -2babg -2b3a2br -2babs -5bach. -bach5en -5bachh -bach3s -b5achse -bach7ter -ba4ck5er4 -backs4 -bad3a -ba4dab -ba4dan -ba4dar -4b3adel -ba4dent -2b3adl -2b3adm -ba2dr -ba2du -b2ae -2b3aeh -4b3aerz -4b3aeug -bae5us. -2b1a2f -2b3ahl -bah4n5e4b -bah6nene -bah6n5ent -bai3d -ba2k3a -ba4k3er -ba2k3i -bak3l -ba2k3n -ba4k3ra -ba4kre -ba3la -ba4lab -ba4l3ak -ba4lar -ba4las -ba4lau -bale4a -ba4lei -baler4 -ba4l5erk -bal5ins -balk4a -balke4 -bal4lan -bal4l5au -bal4l5eh -bal4l5ei -bal6lerg -balo3 -bal4tr -2b1am -bama2 -bam3az -3b2amb -ba2me -ban2a -ba4n3ae -b2and -ban4dab -ban4d5al -ban4d5an -ban4dar -ban4dec -ban6delf -ban6d5en6de. -ban6d5en6den -ban6d5en6des -ban6d5erf -ban6d5erk -ban6d5erz -ban4dob -ban4dr -ban4du -ban4k3a -ban6kerf -ban6k5erw -ban4kl -ban4k3n -ban4kop -ban4kor -ban4k3r -ban4ku -2banl -b3anna -2b3ans -2banw -2b3app -ba4r3ab -ba4rad -bar5akt -ba4rar -bar5ast -ba4r3at -ba4rau -bar5ein -bar4en -barer5ei -barer4t -bari3e -bar5in4s -bark4l -bar2s -bar4ta -bar4tel -b3arti -ba4rue -bar3zw -bas2a -ba4seb -ba4seli4 -bas4i -b3asia -bas4sa -bas4s5ei -bassin4 -bas4sp -bas4st -bas5te -bas3ti -bas4tr -ba4t5ent -2b3atl -batro4 -ba4tru -bat4tak -baub2 -bauchar6m -bau6ch5el -bau6ch5er -bau3eb -bauen4s -bauer6fo -bauer4g -bauer4l -bauer4n -bauer6tr -bauer4w -bau3fa -b3aufg -bau3f4r -4b3aufs -4b3auft -4b3aufw -baug2 -bau5kl -bau3l -bau4m5ac -bau6m5ant -bau6m5ast -bau6m5ent -ba4u3n -b3ausb -4b3ausf -bau3s4k -b3ausl -bau3s4p -b3ausr -4b3auss -bau3s4t -b1a2x -ba3yo -b2ä1c -b2äd -2b1äh -b2äl -b2är -bären5 -bärk2 -2b3ärz -b2äs -2bäug -bä3us. -2bb -b3ba -bbau3 -bbauer4 -b3be -bbens4 -bbe3ra -bbin4g -bb2lo -bb2lö -bblu2 -bb2r -bbran4 -b3bru -bbru4c -b3brü -bb2s -2b1c -b3ch2 -2bd2 -b3da -bdän3 -b3de -bde3s -bdi2e -bdome4 -3be. -bea2n -bea4ten -beat4m -b3ebb -beben3 -bebens4 -beb2l -3be1c -be2ce -becher5e -4b3echt -be3cke -4b5e4cke. -bed2 -4b3e4del -bedi2 -2b3edl -be3e2h -bee2l -be3ela -be3en -be3erd -bee4r5ei -bee6r5es -bee4r3i -be5ertr -be5erzi -bee4ta -be3eti -bee4t3r -be3eu -bef2 -be3fe -befen4 -2b3eff -befi4l -be3g2 -3beh -be4he. -beh3ri -beh2u -b2ei -beib2 -bei5bl -bei3d4r -4b3eier -bei3f2 -bei3g2 -beige4o -be3imm -be3imp -bei6ner6k -be5in4fo -bein6fra -4b3eing -be5in6hal -be5in4hi -bein6spe -bei3s4a -bei5sc -beisch4 -bei3sp -bei3s4t -beit4s5e -beit4s5o -beit4s5t -beiz4w -3bek -be3ke -b2el -bel5achs -bela4n -be4l5ano -be4l5ant -be4lanz -bel5bi -4b3el4bl -bel3d4 -be5le. -be3lec -3belei -be5lei. -be5leie -be4l5ein -be4l3en -bel5er5e -bel5erh -bel5erk -bel5ern -be4l3e4t -bel3f -3belgr -beli6ef -bel5inn -belle4n5 -bel5oef -be4l3om -be4l3or -be4l3öf -bel5öse -3belp2 -bel3sk -bels4p -bel5stern -bel3sz -belti4 -2b3emp -4b3emul -b2en -3bena -be5nabe -be4nat -ben3au -be2nä -3benb -benden4 -ben6dens -ben6d5es6s -4bendl -4bendm -4bendp -be3ne. -ben3ec -benei4l -be3nen -be4n5end -be4n5ent -be3ner -be4nerf -6benergi -be5nergr -be4n5erl -be4n5ers -ben5ert -be4nerw -be4ness -ben3f -beng2 -be4nid -be4nik -benim4 -ben5inn -be4n3is -3benk2 -ben3n -be4nob -be4n3or -ben4san -ben4s5au -ben4sek -ben4s5el -bens5end -ben6sere -ben6spar -bens5trae -bens5trau -bens5trä -5bensz -4b3entb -4b3entd -4benteu -4b3entf -4b3ents -4bentw -ben3un -3benv -3benz -be1o2 -be1p2 -be4pit -4b3epoc -b2er -be3ra. -ber3aa -ber5abe -ber5abl -ber3a4c -ber5adm -be5raeu -ber3af -ber3a4k -ber5all -ber5amm -ber5anz -be4rar -bera4s -ber5asc -ber5ass -be4rato -berbeste5cke -berd4 -be4r5eck -ber5eig -ber6eini -be4r5eiw -ber3el -bere4m -ber5emi -be3ren -ber5end -be4r5en5e -be4reng -ber5ens -berer6fa -ber5ersa -ber5e4sc -be4r3e4t -berf4 -ber4g5af -berg5an. -berg5ass. -ber6g5en6t -berger4 -ber6g5erf -ber4g5li -ber4gn -bergs4t -ber4hab -ber3i4d -berin6sp -berin6st -berin4v -be4ri4o -ber5iss -berk2 -ber4ken -ber5n4a -ber3n4e -ber3ni -b3ernt -ber3o4b -ber3oe -ber3of -be4r3oh -ber5ost -ber3ö -berr2 -bers4k -ber5stau -b2e1s -be3s4an -besch4 -be4s3ep -be4s3er -bes5esk -be4s3id -be3s4la -bes4pe -bes4po -bes3sa -bess4t -b5esst. -bes3sz -be6stang -beste4 -be4ster4 -bes6t5erh -bes6t5erk -bes6t5ern -be4stol -bes4t5o4r -bes5tos -be4s5trä -be4s5tur -be4sum -be4sur -be3s4ze -be4t5ant -4b3etap -beta5sp -bet5auf -bete4s3 -bet4o -bet4tis -bett3r -be3uh -be3um -be3un -be3ur -b2ew -2b1e2x1 -3bez -2b1f2 -b3fa -bfal4li -bfang5s -b3fä -b3fe -bfin4s -b3fr -b3fu -b3fü -2bg2 -b3ga -bga6send -bge1 -bge3ne -bges4 -b3gl -2b1h -b3ha -b3hä -b2i -bi3ab -bi3ak -bi2ar -bi3as -bib2 -bi3ba -bibe4 -bibel3 -biber3 -bi2c -bid2 -bi3dj -biens4 -bie4r3a4 -bieres4 -bie4st -bie4t3a4 -4bieti -bie4tr -biet4s3 -bi3f -bi4ke. -bi4kes -bi3k2l -bi4kre -bi3ku -3bil -bi4l5ang -bi4lant -bi4lau -bil4da -bil4deb -bil4del -bil4d5in -bil4d3r -bild3s -bi4lei -4b3illu -bi2lu -2b3imb -2b3imp -bi3na -4b3indi -bing3a -bi4n3ok -4b5insas -2b3int -2b3inv -bio1 -biob2 -bi2o3d -bi3oef -bi3re -biri3e -2b3irr -bisa2 -bi4s3ei -bi3si -bi2s3p -bis5sau -bis4s3c -bi2st -bi3sta -b4i3tr -bit3s2 -bi3z -2bj -bjek4t5o4 -2b1k2 -bk4an -b3ko -b3kr -bl2 -2bl. -bla3b2 -2b3lac -blade5s -4b3laed -4b3laen -blaes5c -3b4laet -2b3lak -2b3lam -b4lanc -b3lang -bla4s3p -6blassens -blas6serk -4blassu -bl4at -b3late -3b4latt -blat4t5r -bl4au -blau4c -4b5laus -blaus4t -b3laut -2b3law -b3lay -2b3läd -2b3län -bläs3c -3b2lät -b2le2a -2b3leh -ble4i -b4lei. -b4leib -bleich6a -4b3leid -blei3f -4bleih -ble5imp -blein4 -ble5int -blei5sc -blei7sch -4bleit -blei5ta -b2lem -ble4mei -ble6m5ent -ble4mer -blem3p4 -ble2n -b4len. -b4lend -4blenk -b2leo -b4les. -4blese -4blesu -ble3sz -b2let -b3li. -4b3lich -4blief -b4lier -2blig -bl4in -b4lind -4bling -blings5 -blin4k5a -b4linz -4blis. -b4liss -b2lit -b3lite -3b2lo. -b2loc -4b3loch -bloch5s -blo6ckent -blo2g -3b2loi -blo4id -b3lok -b2lon -2blos -blues5s -2b3lun -blu4t3a4 -blu4th -blu4tin -blut3o -blut5sc -2b1m -b3me -bmen4s -2b1n2 -b3na -bnas2 -b3ne -b3nu -bo2a2 -bo3ab3 -boabi4 -bo3as -b1ob -bo2b3l -bo2br -bobs2 -bo3ch4 -bo3d2s -body3 -bo2e -boe4bl -boe3i -boei4g -bo3en -b1of -boh4r5ei -bo3is -bo4l3an -bo4l3au -bol3c -bol5erl -boli4e -bo4l3in -bo4lit -2bolu -bol3za -bombe4 -3bon. -bo3nat -bon4d3e -bo4n3er -3bons -3boo -boo2l -boo4t3i -boo4to -b1op -bo2pe -3bor. -bor3ae -bor3al -bo5rant -bo4r3as -bor4d5an -bor4dr -bo4rei -bo4rig -bo4r3in -bor2s -b3ort. -bor4ter -bor6t5rat -bose3s -bos3t -3b2ot -bot4e -bote4r3 -bo3th -bot4se -bot4so -bots3p -bot4s3t -bot3t -bo4xel -boy1 -bö2bl -2b1öf -bö2is -b1öl -2b1p2 -bpa2g -b3po -2bq -br2 -2br. -b2ra. -bra3a -2b3rad -4b3raed -b2rah -bra5i4sc -b2rak -b2ram4 -bran6dent -bran6derk -bran4d5r -4b3rat. -brater4 -bra6terd -bra6terg -4b3ratg -bra4t3h -bra4t3r -5b4raun -bra5unte -2b3räd -2b3re. -6brecherc -brech5s -6b5rechte -6brechtl -2bred -2bref -2b3reg -b3reic -4breif -brei6t5ei -brei4t5r -b3reiz -2brek -breli3 -b2rem -brem4sc -brem6sei -brem4s5t -b4ren. -4b3rent -2breo -2b3rep -b2re3r -b4res. -b3rese -4b3rest -b2ret -bret6t5en -2b3reu -b2rev -bri4da -4brieb -brie4fa -brie6f5er -4b3riem -b4rien -b4riga -b4rigi -4brigk -bri6kans -bri4ker -bri4ku -b2ril -b4rina -4b3rind -b2rio -b4risc -4briss -4b3ritt -b2roe -2b3roh -2b3rol -bro4m3a -3b2ron -b2ros -4b3rost -bro4ta -brot5ei -bro4ter -bro4t3h -bro4t3o -bro4t3r -brotz4 -2b3rou -b2rö -b2ruc -bru4chu -4b3rueb -2bruf -br4un -4b3rund -bru3n4e -brus4 -brust3 -bru4ter -bru4t3h -2b3rüb -3brüc -2b1s -b2s3ad -b4s3aer -b2sak -b4s3all -bs3amb -b4s3amt -b4sanf -b4s3ant -b4s3anz -b2sap -b2sar -b3sau -bsau4m -bsau4r -b2s3är -bsch2 -b4s5chec -b4schef -b5schu -b2sco -bs2cu -bsde3s4 -b2s3e2b -b4s3ehr -b4s3ein -bse2n -bsen3e -bs3erd -bs3er3e -b4s3erf -b4s3erg -bs5e4r5in -bs3erk -b4s3erl -bs3err -b4s3ers -bs3ert -b5serv -bs3erw -bs3erz -bse2t -bs3eth -bset6z5er6 -b2s3eu -b2sex2 -b4s3ide -b2s3im -bs3ind -bs3inf -bs5inte -b2sk -b2so2b -bs3off -b2s3op -bso2r -bs3orc -b4s3ori -bs5ort. -bs5orts -bs2öf -bs2pe -b4speri -bs2ph -b3spit -bsprun4 -bs5rei -bs3s2 -bs2t -bst5a4be -b4stade -bst3ak -b4st3al -bstands5 -bst5ank -bst5ann -bst5art -bs4t3as -b4s5tauc -bst5au4f5 -bst5aus -bs5teil -bst5einb -bst5eis -bst3e4k -bste4m -bst5ema -bst5emi -bst5en5e -bst5ent -bst5enz -bst5er5e -bst5erf -bst5erh -bst5erk -bst5ernt -bst5er5o -bst5erö -bst5erz -bs3tes4 -bs4t5ess -b3stet -b5steue -bst3h -bst3i4d -b4stin -bst3io -b4s3tip -b4st3ob -b4stoch -bs3tod -b4stor -b4strac -b4s5trae -b4strau -b4s3trä -bst5rep -bst5ret -bst5ros -bst5rue -b4st3rü -b4stun -b4stüb -b2sty -bs3umb -bs3ums -b2s3un -bs3url -b2sz -b3s4zep -2bt -btas4t5r -b3te -bt1h -bti2s -bti5schen -btisch5l -bto2n3 -btran4 -bts2t -btu3en -btü1 -bub2 -bu5chare -buch5art -buche4 -bu4ch5ec -buch5ere -buch5erf -buch5er6h -buch5er6l -bu6ch5er6s -buch5er6w -buch5er6z -bu4chet -buchs4p -bu2e -buegel5e -buer4o -2b3u2fe -bu3f2r -bu2gl -bu4g3ra -bul4l3a -2b3umf -2bumk -2bumr -bu3n2a -3bund -bun4dä -b4undd -bun6d5ent -bun4d5er -bunde4s -bun4din -b4undn -b4undz -2b3unf -b3un3gn -2b3unh -2b3unl -bun6t5er6z -bun4tr -2bunw -bur3c -bur4gan -bur4gar -bur4g5au -bur4gin -bur4gl -bur4gr -bu3ri -2b3urn -b3ursa -b2us -bu2s3a -bu4scha -busch5ar -bus5chau -bu6schei -bu6sch5el -bu6scheu -bu4schl -bu4sch5m -bu4schn -bu4sch5o -bu4schr -bu4schw -busch5wa -bus5ers -bus5erw -bu4sin -bu2so -bu2s3p -buss4pa -bus3ta -bu2su -bus3un -bu4ßer -but2a -bu2t3r -bü1c -bügel5e -bühre4 -bür2s -2bv -2bw -b3wa -bwasse4 -b3wä -b3we -b3wi -b2y1 -byauto4 -byo2 -by3p -byp4i -bys2 -byte3s -2bz2 -b3ze -b3zu -bzug2 -bz4u3n -1ca -cab2 -ca3bl -3cac -ca4ch -cae3 -caes2 -ca3g2 -cago3 -ca1h -ca3lab -ca3lam -cam4p3l -c3ang -ca2pe -car2a -car3n2 -carri3 -car2s -ca3s4a3 -cas3te -ca3t2h -ca3ur -caus2 -caust3 -ca1y2 -cä1 -cäs2 -2cc -c1ce -c1ch -cch4i -2c2d2 -c3do -2ceae -2cear -2ceä -2ceb -2cec -1ced -ce2dr -2cee -2cef -2cegr -1ceh -ce1i -2cej -2ceka -ce3l2a -cele3r -3cell -3cels -cen1 -cene2 -ceng2 -ce2n3i -ce2no -cens2 -3cent -cent3o -cen4tos -cen3un -ce1o2 -2ceop -1cer -cere3u -ce3ri -ce4ris -ce3ro -ce3s2h -2cesp -ce3sti -2cet -3cet. -ceta2 -ce4t3am -3cett -ce1u -2cez -1cé -c1f -cg2 -2ch. -4chab -ch3a4b3i4 -2cha2c -ch3ack -ch3aep -ch5aerm -ch3aes -2chaf -5chafei -ch3a4ge -ch3akt -5chalam -chal5b -chal6lein -cha4mer -ch5ampe -3chamy -3chanc -ch5andr -ch5ange -ch5angr -ch3ano -ch5ansa -ch5anst -4chanz -3chao -ch3app -4char. -3chara -ch5a4rab -3chard -4chare -5charta -cha4sc -ch3asp -ch3atm -3chato -4chatt -4chatu -4chatz -4chauk -ch5ausf -chau5spo -ch5ausr -ch5ausst -ch5austr -ch5ausw -ch5ausz -chau5t -ch3äp -ch3ärm -ch3äs -1châ -2chb -2chc -2chd -che3b -4ch5e4ben -ch5echt -che5ers -3chef. -che4fei -4cheff -5chefin -che4fr -3chefs -2cheh -4chehe -4chehr -4chei -ch3eil -ch3eim -ch5einla -ch5eintö -ch5einze -ch5eis. -ch5eise -che4las -ch5elek -4chelem -che4len -che4ler -che4me. -ch4emi -5chemie -5chemik -4chemp -che4neb -ch5enge. -che4nid -che4no -chens4p -4chentc -chen6ten -4chentf -4chents -4chentw -cher3a -che4rat -cher6bes. -4ch5erbs -cher6de. -6chergebn -4ch5ernt -6cherregu -cher5sta -ch5ertrag -cher6trage -6cherzeu -4ch3ess -4cheta -ch5e4tag -ch5e4tik -4ch3e2x -1ché -2chf -2chg -ch5gau -2chh -3chia -chilf5s -chi5li -4chind -4ch3inf -ch3inh -ch5inko -ch5inkr -ch3ins -4ch3int -4ch3inv -3chip. -3chips -3chiru -chi5sta -3chit -2chj -2chk -2chl -chl4a -ch5lade -ch5laend -ch5lanz -ch3lat -ch5lauf. -ch5läd -ch5länd -ch5lein -ch4len -chl4o -ch4lun -2chm -ch2me -2chn2 -chn4e -ch4nep -chner6ei. -chn4i -4chob -cho3ch -ch3oel -4choen -3choer -4chof -ch3off -chof4s3 -3chog -cho5l4a -3chole3 -ch3orc -4chord -cho4rei -5chores -5chors. -ch5orts -3chorw -3chosl -ch3öl -2chön -3chör -2chp -chr2 -2chra -ch3rab -ch3rad -ch3ral -chrap4 -ch3ras -ch3rat -2chrä -2ch2re -ch3reh -ch3reu -ch3rev -ch3rh -ch3rin -5christ -4chrit -4chroe -ch3rol -5chromo -3chron -ch3ros -2chrö -2chru -2chs -ch4sm -ch6s5pani -ch6spart -chstro4 -2cht -ch4tang -cht4ank -cht6en. -ch4t5ruh -4chue -4chuf -4chuh -chu2n -4ch3unf -ch3uni -4chunt -4chunv -ch3urs -ch3urt -4chut -chut4t3 -2chü -2chv -2chw -2chz -ci2a -ci1c -ci3es -cill4 -c3ind -cins2 -c3int -ci2s1 -1cit -c2k -2ck. -ck1a -3cka. -cka2b -ck3a2c -2ckae -2ckal -cka2m -cka2n -cka2r -3ckard -3ckau. -3ckaue -2ck1ä -2ckb -2ckc -2ckd -1cke -2cke2c -4ckeff -2ckeh -ck3ehe -2ck3ei -cke4la -ckel3c -4ckelek -4ckelem -ckelti4 -4ckemb -4ckemp -ckener6ze -6ckensem -cken4s5o -cken6s5tur -6ckente. -6ck5enten -4ckentf -cken6trop -4ckentw -cke4rec -6ckeremi -cker6fül -6ckergeb -6ck5erken -cker6ker -6cker6lau -6ckernie -6ck5ernst -4ck3ero -ck6ers. -4ck5ertu -4ckertü -4ckerz. -cker6zeugn -4ck3ese -2ck3eu -2ckex -2ckf -2ckg -2ckh -1cki -2ck3i2d -4ck3ind -4ck3inh -4ckink -4ck3inn -4ck3ins -4ckint -3ckis -2ckj -2ckk -2ck3l -2ckm -2ck3n -ck1o2 -ckoe2 -ck1ö2 -2ckp -2ck3r -2cks -ck5sac -ck4sar -ck4sint -ck5stae -cks5tate -cks4tec -cks4to -cksto4r -ck3str -2ckt -ck4tar -1cku2 -2ckue -2ck3uh -ck3umb -4ck3ums -4ckumw -4ck3una -ck3unf -4ckuni -4ckunt -2ck3up3 -2ck3ur -2ckw -1cky -2ckz -c2l2 -clet2 -cle4vel -clien4 -clin4g -cli2p -clip3a -1clo -clo3ch -clo5fe -1clu2 -club3 -cm2 -c2mi -1co -coca3 -co3ch -coche5 -co2de -code3s -co3g -cog4n -coi2 -co3it -co2ke -cola3s -co4leu -colle6ge. -co3l4o -com4te. -comtes4 -continuati6 -co2o -2cope -co4pe. -co3ra -2corb -cor4da -core3s -cor5ti -corvet4 -cos3t -2cotc -co2te -co2u -cou4c -cou3si -2cout -1cô2 -2c1p -2cq -c2r -c3rae -c3rah -c3rau -c3rä -1cre -2cree -cre4me -cres2 -cros2 -cross3 -1cru -cry2 -2cs -cs2a -cs2f -c2si -c3s2ti -c3s2tr -2ct -cte2a -cti2o2 -ction3 -ctur2 -1c2u -2cua -cul4u -cup1 -cu2pe -cup3p -cu2p3r -cussi4o -cus3t -c2v -c3ve -2cx -1cy -cy3an. -cybe2 -1ç -1d -da1a -2d3a2al -d1ab -d4abab -4da4bak -2dabb -2dabd -d2abe -4d3a4ben -2dabf -2dabg -2dabh -dabi2 -4da4b3it -da4blo -2dabn -2da2bo -dab4ra -4da4bre -d4ab4ru -d4ab4rü -2dabs -2dabt -2dabw -2dabz -d1ac -dach3a -dachau6t -d4ache4 -da4chec -da4ch5er -dach4in -dach3o -dach5stu -4dachtz -da5chung -3dack -d1ad -da4del. -da4delg -da4dels -da2do -dad2r -da2e -3daec -2d3aeg -2d3aeh -4d3aemt -dae4n3a -d3aend -2d3aep -2daeq -da3erf -d5aerzt -2d3aeu -dae3us -2d3aex -d1af -2daff -da3f2l -dafo4n -da3fra -d1ag -dagi4 -dag2o -da1h -dah3l -da3in -2d3air -da3ism -da3ist -da4kad -da2kn -da4kro -dal3a4d -dal5aer -da3lag -da4lär -da4lop -da4lor -da3lö -4d3alpe -4d3alte -da3lü -2d3amb -dam4e -d3a4mei -damen5s4 -d5ammae -d5amman -4d3ammä -4d3ampe -d3ampu -2d3amt -dana3e -4d3anal -dan5alg -da4nan -d3a4na3t -dan4ce. -dan3d2 -4d3anda -4d3anei -4d3a4n3eu -2danf -d4anfu -4d3anga -d5angeb -d3angs -2danh -dani5er -4danim -dan4kal -dan4kan -d5anker -dan6k5erf -dan4kl -dan5kla -dan4k3o -dan4kr -4danle -4d3anna -2d3a2no -2dans2 -d4ans. -d4anse -danta4 -4dantr -4dantw -4d3anzu -4d3anzü -dan4zw -2d1ap -d2apa -3dapol -da4por -da2pu -da2ra -dar3an -dar3as -darb2 -5darbi -2darc -dar4dar -dar4de -dard5ei -dard5er -dar4do -dar4dr -dardwa5re -4d3area -dar3ei -dare4m -dar5emi -da4r5en5e -d5a4rist -dar4kr -dar4mar -4d5arres -4d3art. -4d3artd -4d3artg -4d3ar4th -4d3arti -dar4t5in -4d3artl -da2ru -dar5um. -4darzt -da4sche -da5sei -das6ein -da3s2h -da2s2p -da3spa -4d5aspek -4d3assi -da3st -4d3aste -das4tra -das4tu -da3ta -da3tei -4d3a4tel -date4n -da2th -dati4v3 -2d3atl -2d3atm -dat4st -4d3atta -datum4 -2d3aud -dauer5e -2d3au2f3 -2d3aug -dau2i -2d3auk -da3unt -dauri3 -2d3aus -2d3au2ß -4dauto -d1ax -d1az -da2zo -3däc -2d1äf -2d1äg -2d1äh -2d3ämt -dä2n3a -2d3änd -2d1äp -2däq -därm2 -4d3ärzt -2d1ä2u -dä3us -2däx -2db2 -dbe4e -dbi2e -2dc -d3ch2 -dchengene6 -dco2r -d1cr -2dd2 -d3da -dd4ae -ddar4m3 -d3dä -dde2a -ddel5e4c -dde3ra -d3dh -ddha3 -ddi2e -ddien4 -dding5s -d3do -dea2d -de4adl -de3ae -de3a4t -deb2 -4d5e4bene -3debü -de4ca. -de5chor -de3chr -2d3ecu -ded2 -4d3e4del -dede3s -dedi4e -de4dit -dee2 -de3eb -3de3ef -de3eg -3de3em -de3ep -de3er -de3es -de3et -3de3eu -3deex -de4fa. -2d3eff -def2l -deg2 -2d3egg -deh2a -de5hop -2d3ehr -4dehyds -de4ic -dei6ch5er -de5i4den -2d3eie -2d3eif -2d3eig -de3im -4d3einb -de3in4d -4d3eine -de5in4fo -de5in4fr -d3eing -dein6gen -4deinh -4d3eink -4d3einl -4d3einm -4d3eino -4d3einr -4deins. -4d5einsa -4d5einsä -de5in4se -dein6sta -dein6str -dein6ter -5d4e3in4v -4d3einw -de3io -de3i4si -d4e3ism -dei4sp -de3ist -2dekz -del5ach -de4l3am -dela4n -del5ano -de4l5ant -dela4t -del5ate -del5auft -de4l5aug -deld2 -de5le. -de4lef -de4l5ei4g -4d3elek -4deleme -del5end -del5erk -del5ern -del5er4r -del5erz -4d3elf. -4d5elfe. -delfen6s -4d3elfm -deli4ne -del5inn -delk2 -del4l5an -del4lar -delle4 -del4l5eb -del4lei -dell5eic -del4l5er -delmi4 -del4n3e -de4l3ob -de4l3op -del6s5ach -del4san -del4sas -del6sein -del6s5int -del6s5pan -del6spar -del6spat -del4spr -de4manh -dem4ar -dement4 -de6m5entg -demi4e -4d3empf -d4en. -dena4b -den4ade -de3n4am -de4nan -de5n4arr -de5n4au. -de5naue -dende4n -4d3endh -4d3endk -den3d4r -dend4s -dend4u -den3e4b -de4n3e4d -de4n5end -de4n5eng -de4nep -den5erb -den5erei -4d5energ -de4n5ero -de4n5erö -den5ers -de4n5ert -de4nerw -den3f2 -4d5en4ge. -6d5engeln -6d5en6gels -den6gen. -de4nid -de4n3im -den4k5ak -den6kerl -den4k5ob -de4nob -d4ens -den4sal -den6stie -den6stop -dens5tor -den4sur -4d3entd -dentengene6 -4d3entf -4d3entg -den3th -4d3entk -4dentla -4d3entn -den6trop -4dentw -4dentz -den6z5erf -den4zym -2deol -de3p2f -dep2l -depo4t -de3ra. -der3ab -de5rabi -der5a4de -der5aen -de5raet -de5raeu -der3af -der3a4g -der3ak -der5alb -der5alt -de5rampe -dera4n -de5ran. -der5ane -der5ant -der5anz -de4r3ap -dera4s -der5asi -der5asp -de4r3än -4d3erbs -4derdb -de4r3e4b -de4r5eck -der5edi -6dereigni -der6eini -der5eis. -de4rele -der5ell -de4remi -der5ende -de4r5ene -der5enge -de4r5ens -der5ente -der5entg -der6er. -derer6de -derer5e -der4erf -de4r5erh -der5erre -de6rersa -dere4t -der5eta -de4r3eu -derf4 -der5fl -derg2 -6dergebn -de4r3id -de4r3i4m -der5inb -der5inner -de6r5innu -derin6teg -de4r5iss -derk2 -der4nal -der5o6ber -deroe4 -der5oef -der5onk -de4ror -de3rot -derö2 -derr2 -ders4k -der5stau -dert5ende. -der6ters -6dertragen -de4ruh -der4ump -der6weck -4d3erz. -4d3erzv -de4s3a4g -de4s3ah -de4sak -des5amt -des5ang -des5ant -des5anz -de4sap -de4s3au -de6sche6f -de4s3eb -de4s3e4h -de4s5eil -de4s5ein -des5elt -de4s5en5e -des5eng -des5er5e -des5erf -des5erg -des5erh -des5erk -des5erl -des5erm -des5ern -des5ers -des5erz -dese4t -des5eta -de3shu -desi5er. -de4s3in -de4sos -de4spa -des6park -de4spot -des4pu -dess2 -dess4e -des6sens -des6ser. -des6seri -des6sern -des6sers -de5staf -de5stall -dest5alt -de5stec -dest5eige -dest5einsa -de5stell -des6temp -des6t5erg -de5stern -des6tert -des6tina -de5stras -des4tum -de2su -des3un -de3ta -deta4c -4d3e4tat -deten4t -4d3e4thi -de4tik -det2s -2d3etw -de3u2f -de3uh -2d3eul -de3um -de3u2n3 -de3url -de3us -d4eut -4d3e4vid -dex3em -dexen4 -de4x5end -de4x3er -de4xis -4dexpe -4dexpo -2d1f2 -dfang3 -dfen2 -dfin4s -df4la -2dg2 -dga2t -d2ge. -dger4e -d4ge3sh -dge6tang -dge4tau -dge4t3e -d3gl -2d1h2 -dhau6eri -dhek2 -d3hu -dh4un -dia5l4ei -dialer4 -di3arr -dia3s2 -dib2 -di3ce -di3chl -di3cho -dich7ter -dichts4 -di4ck5el -2d3i2co -4d3i4dea -4d3idee -di4den -2di2dy -di3e2d -di2e3i -di5elek -die4n5e4b -dien5e4v -die4ni -die4no -di5ens. -dienst5art -dienst5rä -dienter6mi -die2p -die4rei -diese4 -die4th -dige4s -dig2n -dig4ta -dig4t3r -di4lar -di4l3au -dilf2 -4d3illu -dil4s3t -2d3ilt -2d3imb -2d3imp -di3na -2d3ind -2dinf -d3infe -d3infl -d3info -d3infr -3ding -din4g3a4 -2d3inh -2d3inj -4d3insc -4d3inse -4d3insp -4d3inst -4d3insu -4d3insz -2d3int -2d3inv -di4odi -dion5in -dion3s4 -di3ora -dios2 -di1p2 -di3pt -di4rak -di4ran -di3re -4d3i4re. -4d3i4ren -4d3i4rin -4d3i4ris -2d3irl -2d3irr -di3s3a2 -di4sag -di4sas -di4sch5r -diskan4 -2diso -di4som -di5s4per -2d3isr -diss2 -dist4 -di3s4ta -dis3te -di4t3ak -di4tal -di4t3an -di4t3au -dite3c -di4t5er5e -di4t5erl -di4t5erm -di4t5ero -di4t5erö -di4t5ers -di4t5erw -dite3s -dit5ins -di4tob -di2t3r -dit3s -di2t3u -di3ve -di3vi -di2vr -2dj -2d1k2 -dkab2 -dkraf4 -dkraft5 -2d1l -dl2a -dlan4dr -dlang5s -d3le -dl4er -dle4r3a -dlerin6t -dli2f -d3lo -d3lö -dlungs5 -dl2v -2d1m -2d1n -dn2a -d3n2e -dne3p -dnet4s -dn2j -dnus2 -dnuss3 -do3a2 -d1ob -3d2oba -do4bera -do4berh -do4beri -do4bern -do4berr -2dobj -2dobm -2dob2s -3doc -do3chi -doch4t5r -do3e2b -2d3oe2d -2d3oe2f -2d3oek -2d3oel -doen4 -doe5ne -doe2r -doer3i -doe4s3c -4d3o4fen -2d3off -2d3ohr3 -3dok -3doli -do4mal -do4man -do4mar -do4m5ing -2domn -do2mu -3don. -3don2a -don4au3 -donaus4 -doni3e -3donn -2dony -doo3g -do2or -2d3opf -do3ra -4d3o4rak -4d3orbi -2d3orc -2d3ord -3dorf -dor4f3a -dor4f3ä -dor4fei -dor4f3i -dor4f3l -dor4fo -dor4fr -dor4f3u -2d3org -4d3orna -4d3orni -2d3ort -dor4t3a -dor4t5ei -dor4ter -dor4t5he -dor4tr -3dosi -dos2k -2d3osm -do3sp -dos5sa -d3os2t -dosta4s -doste4c -dosten4 -do4stu -2d3ouv -dow2s -d2o1x2 -2d1ö2d -2d1ö2f -2d1ök -2d1öl1 -dör4fl -dö2s3c -2d1p2 -2dq -dr2 -3dra. -2drab -2d3rad -d3raed -4d3raet -d3raeu -3drag -d2rah -4drahm -2d3rak -3dral -d3ramm -d3ramp -dr4an -4d3rand -4d5rangl -dran5k -2d3rap -dra4pu -d3rase -d3rass -4d3rast -dra4t3a -dra4th -dra4tin -d3raub -4d3rauc -4d3raum -4d3raup -4d3raus -4d3raut -2dräd -d4räng -2d3rät -2d3räu -3dräue -2d3re. -d4rea. -4dreal -d4reas -d4reck -2dred -2dref -2dreg -3dreh -dre4h3a -dre4hi -dre4h3o -4dreic -d4rei3e -d4reik -4dreine -4dreins -d4reir -4dreise -drei5s6tr -d4reiv -d2rej -2drek -2drel -dreli3 -2drem -2dren -dre3na -d2reo -2drep -2drer -d2res -4d3res. -dre6schm -dre6schw -4d3rese -4d3resi -dres6san -dres6s5ei -dres6sel -dres6s5er6 -dres4st -4d3rest -d3resu -2dret -2drev -d2rew -2drez -d2ré -2d1rh -2d3ric -4d3rieg -drie4n3 -3drif -4d3riff -d4rift -4d3rind -4d5ring. -2d3rip -4d3risi -4d3riss -d2rit -drit6terf -4d3ritu -d2ro -2drob -4d3rock -d3roec -droe4s5c -4drogg -3droh -4drohr -5drohu -dro4id. -4d3roll -3dros -d3rose -2drot -2d3rou -2d3rov -d3row -2d3röc -drö4s3c -2d3rub -3d2ruc -2d3rud -4d3rue3b -drue7cke -d3rueh -2druf -2d3ruh -4drund -d4runt -2d3rut -2d3rü3b -drü5cke -2d3rüh -2d1s -dsa5cha -d2s3ad -d4s3aem -d2sak -d4s3alk -d4s3all -d4s3ane -d4s3ang -d4sanm -d4s3ant -d4s3anz -d2sar -ds5assi -dsau2 -d4s3auf -d4s3aus -d4saut -ds2äu -ds2ca -dsch2 -d4schin -dsch4r -5d4schun -d2scr -d2s3e2b -dse2e -dsee3i -d2s3ef -ds3ehr -ds4eign -ds5einl -d4s5eint -d4s3ele -d4s3emb -d4s5e4mis -ds5ende. -ds5enden -dsen5er -d4s3eng -dsen3s -d4s3ent -d4s3er3e -d4s3erf -d4s3erh -d4s3erk -d4s3erl -d4s3erm -ds3err -d4s3ers -d4s3ert -d4s3erz -dse4t -d4s3eta -d4s3eth -d2s3ev -dsex2 -d4sexa -d4sexe -d3s4ha4k -dshi2 -d2sid -d4s3ind -ds3ini -d4s3int -d2ske -d3s4kel -d4skir -d4skis -d5sklav -d2sko -d3slaw -d4sli -d2so2b -d2s3oe -d2s3op -dso2r -d4s3ori -d4s5ort. -d2sö -ds4ped -ds4pen -ds4perb -d4speri -d4sphi -d2spl -ds4por -d6sporto -d4sprae -d4sprä -d4sprei -d4spro -ds2pu -dss2 -ds3sa -dst2 -d6s5taeti -ds5tauf -d4s5täti -d2ste -d3stec -d3steg -d3stei -d4steil -d5stell -d3s4tep -d5s4terb -d5s4tern -d3steu -d4sth -ds2ti -ds4til -d4stoch -ds4tol -ds4top -ds2tr -ds5trad -d5strei -ds5treu. -ds2tu -d2sty -d4suml -d4s3ums -d4sumz -d2s3un -d4s3url -2dt -dta2d -dta2n -dtan3e -d3t2as -dtas3t -d3t2ax -dtä2s -d3tea -d3tec -dteer3 -d3tem -dte3na -d3tep -dte3ra -d3tes -dte5sta -dti2d -dto2b -d3töp -dt3ral -dt3rec -dtre3s -dt1s2 -dt3sa -d3tur -d3ty -du3al. -du3all -du3alm -du3alp -du3alv -du3alz -du3ar -du4b3li -du3ce -due5bel -due3br -du3el -du3en. -du3ena -du3enb -due3p -duese4 -du3ett -duf4ter -duf4to -duf4t3r -dug2 -du2ga -2d1uh -du3ist -d3u2kr -du4l5ar4r -du4l5art -du4l3au -du4l3ei -dul3s -du2lu -2d3umb -2d3umd -2d3u2m3e -2dumf -2dumg -2d3umh -2dumk -2duml -d4ump -2dumr -4d3umsc -2d3umt -2d3umv -du2n -2d3una -2d3und -dun4de -2d3une -2d3unf -d3unga -4d3ungl -dungs3 -2d3uni -dun4kl -2dunr -4d3unsc -4dunsi -dun4st -dunst5r -2dunt -2dunw -2d3unz -du3os -dur4c -durch3 -2d3urk -2d3url -2d3urn -dur2s -4d3ursa -2d3urt -dusche4 -du6sch5ec -du6sch5ei -du4schl -du4schn -du4sch5w -dü3bel -d3über -düns3 -düse2 -2dv -dv4en -2dw -dwa2 -dwa5sche -dwasse4 -dwert5a -dwesten6 -dwes6tend -2dyac -dy3an -dydi2 -dygene4 -dy3la -dy3lä -dy2le -dy1o2 -dy2sa -dy4s3l -2dz2 -2e1a -ea2be -e2abi -ea2bl -ea2br -ea2c -ea3chi -eadli4 -ea2dr -eae2 -eaga2 -ea2ge -ea2gi -ea3g2l -ea2ka -e2akd -eak3e -e2akg -e2akr -eal3ac -eal3ak -e3a4lei -e5a4l5ent -e5alen4z -e5alerb -e5a4l5er4g -e5a4l5ert -e3alex -e2ali -e3a4l3in -eal4ler -ealmo4 -ea4lob -ea4l3on -ea4lop -eals4p -ealti4 -ea2l3u2 -eam1 -e2ame2 -eam3ev -ea4mid -eamm4 -eam3s2 -eam3tu -eanden4 -e2ane -eanet4 -e3anf -eanla6gente -eannet4 -ea4n3ob -e4ant. -ean4t3a4 -ea2q -ea2ra -ear3ac -ear3ak -ear3am -ea3rat -ea4r3ei -ea4r5ene -e4arer -e2ari -ear3n4 -ea2ro -ea2r3u -e3a2sc -e3asf -easin4g -ea2sp -eass2 -e2at -eate2 -eat5ens -eater3 -e3atm -eat3s2 -e3at3t2 -e2au. -eau4fe -e3auff -eau5f4re -eaug2 -ea4u3n -eaussen6dung. -eau5ste -ea2va -e2az -e1ä2 -2eb. -eba2g -ebah6ner -eb3anz -eba2p3 -e3bas -2ebc -e3bef -ebein4s -e4b5eise -ebe4lan -ebe4lau -ebe4ler -ebel5in -e3bem -ebe4na -ebe5n6au. -ebenen5e -e5benu -e5benz -ebe5rad -eber5as -ebe4rel -eberg5s -e3bes -ebe4s5e4h -ebe4ses -ebe4sor -ebet4s3 -e4b3inb -eb3lag -e4b3le. -eb3lem -eb4len -eb5les -eb4leu -eb4lie -e3bloc -eb4loec -e3blöc -ebni2 -e2bo2b -ebo3ef -ebö2s -2ebr -eb2re -e3bru -e3brü -eb4s3ac -eb4s5ang -eb4s3as -eb4s3au -eb4s5ele -ebs5end -ebs5ene -eb4sep -eb4ser -ebsi2 -eb4sid -eb4sis -eb2sp -ebs5panne -ebs3pe -eb6s5taet -eb4s5tät -ebs5tem -ebs5ten -ebs3t4h -ebs5tic -ebs5tie -ebs5treu -eb2su -e4bueb -e4bunt -ebu2t3 -e2büb -eby2t -e1ce -ech3ab -ech3ae -ech5ang -ech5aus -ech3ä -e3che -eche4c -eche3e -ech3ei -eche6reib -echer6fah -echer5o -echer6zie -e3chi -echi3e -ech3la -e3chlo -ech3lu -ech3m -e4ch3n -e2cho -3echo. -ech5obe -echoe4f -ech5org -ech3ö4f -e2chr -ech3ra -ech3rä -ech3re -echsau4 -ech6s5en6d -ech4s3o -ech4spa -ech4spo -echst5ri -echs4tu -echt5er6ha -ech7ters -echt6s5ac -echt6s5ag -echt6s5eid -echt6sen -echt6stie -ech3uh -ech3w -e1ci -e4ck. -e3ckag -e4ckef -ecke4n -e4ckep -eck5erfo -ecker6ken -eck5ermi -e4ck5erw -eck5erze -e4cks -eck5spo -3eckzä -e1cl -e3co -e3cr -ecre2 -ec3s -e3cy -2ed. -e2da2b -ed2ac -eda5chu -ed2ad -ed2dr -ede4al -ede3e -edel5au -e3dens -eden4sa -eden4s5e -eden4sp -eden6s5tau -eden6stem -eden6s5tu -ede3o2 -edere4 -ede6r5eic -eder5er4 -ede6rinn -eder3o4 -eder3ö -ede3sk -ede5stei -edes4ti -ede5str -edia3l -edi3p -editen4 -edi4tin -ed2o -ed4oc -edost3 -edpol4n -e3d4rei -ed3rik -3edris -ed4s3al -ed4sco -ed4seh -ed4s3es -edsi2 -ed2sk -ed2s3o -ed4s3ti -ed4sto -ed4s3um -ed3te -edte3e -edte5in4 -edt2h -edue3b -edu2f -e2d3ug -e2dum -ed2ü -edü3b -edy1 -edys2 -2ee -ee3a2 -e3e4ben -eeb2l -ee3bs -ee1c -ee2ce -e3e2ck -eede3s4 -eedi4e -ee2dr -eed3s2 -ee1e2 -eees3 -e3eff -eef2l -ee3fo -eef3s -eeg2 -ee4hep -e3ehr -e1ei -ee3ig. -ee3imp -ee5in4se -ee3i4sc -eei4sp -e3e4lek -eele4n -eele4s -e3elf -eem4a -ee5man -e3emb -e3emp -ee2na -ee3n4ac -ee3n4ad -ee3nae -eena4g -ee3nas -een3er -een4es -een3e4v -e3en4ga -een4o -ee5nor -een3sh -ee1o2 -ee1p -eer3ad -eer3al -ee5ram -eer3as -e5erben -e3erbt -eer5eis. -eer5eisa -ee4r5en4g -ee5r4e4s3 -eere4t -eer5eti -e3er4fi -ee3rhi -e3erhö -e2eri -e3erlo -e3erlö -e3ermä -eero4be -ee4r5oef -ee4r3oh -e3eröf -ee3rös -eer3sh -eeru2 -ee4r3uf -eer4und -eerz2 -e5erzeu -ee1s2 -ee4seb -ee3sh -ees3ka -ee3sl -ee3sp -e3es3s -e3e4tat -eet5ein -ee2th -ee4tik -ee3tr -ee4t3ro -ee1u -eeu2f -eeun2 -eewa4r -e1e2x1 -e1f -2efa -e2f3ad -e4f3aeu -e2f3a2g -ef5akti -ef3ana -e2fap -e4farc -e2fat -ef3auf -ef3aus -e4fähn -e2fäu -2efe -ef3ebe -efecht4 -e4f3edi -e4f5eing -ef5eink -efe4l5ei -efel7ler -efe2n -efen3e -e4f5ener -e4f3enk -e4f3ent -efere4 -efer5eg -efe4rei -efer6gebn -eferin6d -e4f3eta -e2fex -ef4f3as -3effek -ef4fle -ef4fli -efig3s -efi4le. -e4f3ins -ef3int -e2fj -ef2le -ef4lue -e3f4lü -2efo -e2fob -e2f3o2f -e4f3ora -ef2r -ef3rat -ef3red -ef3reg -e5fris -ef3rol -ef3rom -efs2c -efs2p -efste4h -ef3str -eft5anz -eft5erkr -ef4top -ef4t3ri -ef4t3ro -eft2s -ef3ums -2eg -egab4r -ega4la -ega4l5in -ega2m -ega5sta -eg2äh -egd2 -ege5in4s -ege5in4t -ege4l5ac -ege4l5au -ege4lec -ege6l5ei6er -ege4ler -ege4l5in -ege4lo -egel6s5tur -egena4 -ege4nag -egen5ec -ege6nero -ege4rak -ege4ral -ege4ran -ege4r5as -ege6reck -ege6rend -egeren6g -egerer6k -ege6rers -egerin6d -ege6rin6g -egerin6t -ege6s5tro -ege3u -egg2l -eg3ins -e4g3lac -egler5e -eg3nei -egori4e -eg3rän -eg4sal -egsauto6 -egs2c -egse4r -egsgene6rat -eg3spe -eg4stä -egus3t -e2gy -e1ha -eh3aka -eha3kl -ehalt4s -e4h3ans -e4h3ant -eh3anz -e4h3app -eha4sc -eh3aug -eh5ausw -eh5ausz -e1hä -e1he -eh3eff -e4h3ein -eh3e4la -eh5elek -ehelf4 -eh3elt -3e4hema -eh3emb -e4h3e4mi -e3hen -ehen4se -ehens5ei -3ehepa -eh3epi -eher4an -eher4fa -eh5erfah -eher4fo -eher6gebn -eher4ke -eher4le -e6h5ersta -eher4zä -ehe5str -e1hi -e2h3i2d -eh3ill -eh3imb -eh3inf -e4h3inh -e4h3ins -ehi2r -ehi2t -ehla4c -eh3lae -eh3lam -ehl3ap -eh4l3as -ehl3au -eh3lä -eh4l5e4be -ehl5einh -eh4lel -ehle4n -eh3leu -ehlkop4 -ehl3o4r -eh4l3öf -ehl6s5ein -ehl4sin -2ehm -eh4mab -eh4mant -ehm6ens -ehmen6sp -ehmest5 -ehm5estr -eh3mu -eh3nad -ehn3e4c -eh3nel -eh3no -ehn6s5eid -e1ho -eho2l -eh3oly -eh3ope -eh3org -eh3osm -e1hö -eh3öff -ehö4r5er -ehr3ak -ehr3al -ehr3an -ehr3ap -ehr3än -ehre4c -ehr5eck -ehr5ei. -ehr5eic -ehr5eie -ehr5ein -eh4r5eis -eh3ren -ehre4n5e -ehrens6t -ehrer6fa -ehrer6la -ehr5er6tr -eh3reu -eh3rie -eh3roc -eh3roe -ehr3of -eh3rom -eh3rot -eh3rö -ehr4s5a4g -ehr6sein -ehr4ser -ehr6stras -ehrungsgene6 -ehs2 -eh3sh -eh3sk -ehst2 -eh3sta -eh5stei -eh3str -eh3stu -2eht -eh3t2a -eh5ther -e1hu -eh3unf -e4huni -ehu3s4a -e1hü -eh3w -e1hy -2ei1a2 -2eiä -ei4b5aer -ei4b3ak -ei4bär -eibe3e -ei4b5ele -ei6b5erla -ei4bid -ei4b5las -ei4bli -ei4b5rau -ei4brea -ei4broc -eibu4t -ei4b5ute -ei3ce -eiche4n -eich3r -ei3cl -e2id -ei4dar -ei4dau -5eidech -ei4d5eis -eid5erfue -ei6d5erfü -eides4t -eide5ste -eidi4e -4eidn -eid4str -e2i1e -eie4be -eie2d -eie3f4 -eie2l -eie2m -2eie2n -eienge4 -eie4r3a -eierei6s -ei5ereu -5eiersu -eie2t -ei3f4lo -eif5lut -e2ig -2eig. -ei4g3ak -ei4g5ans -4eige. -eige5in4 -e5igel. -5eigene. -ei4glo -ei4glö -ei2g3n -3eigne -3eignu -ei2go2 -ei4g5rad -ei4g5rat -2eigs -eigs4p -eig5str -2eigt -2eigu -4eigw -ei4kak -ei4k5ank -ei4kli -ei4klo -ei4kni -ei4l3ab -eila4n -ei4l5ane -ei4l5ang -ei4lant -ei4l5anz -ei4lar -4eil3c -eild2 -ei4lein -eiler6geb -eiler6kl -eilf2 -ei4l5inf -ei4l5ins -eil4ip -ei4lob -ei4l3op -ei4m3aa -ei4mab -ei4m3a4g -eim5all -eim5alp -ei4m5ans -ei4map -eima4ta -eima6ter -eima4to -eim5aus -e4ime -ei4m5ent -ei4m5erl -ei4m3i4d -ei4moe -eim5org -2eimp -eim3sa -ei4m3um -ei4mur -eina4b -ein3a4d -ein3ae -4einam -ei4n3an -ein3a4s -4einat -ei2n3ä -einde5ra -ein6d5erk -ein4duk -ein4dus -4eineb -eine5in6f -eine5inn -einen6zy -ein6er. -eine5ras -ein5erbe -ein5erkl -ein5erkr -ein5er6la -ei6n5er6sc -ein5ertr -ei4n5ess -ei4n5e4ti -ei4n3eu -ein6fant -ein4fek -ein4fiz -5einflus -4einfo -ein4fo. -eing2 -e3ing. -e3ingt -4einhab -ei4n3i4d -4einie -e3i4nit -eink2 -ein6karn -ein3n2 -ein4nen -ein6nere -ei2n3o2 -5einord -ein4seb -ein6seln -ein4sid -ein6stab -ein6stal -ein6star -ein4sul -ein4sz -ein4teg -ein6terf -ein6terp -5eintrag -ein6trig -5eintrit -eint4s -ein5tsc -2einu -e4inver -ein4ves -ein3z4s -ei1o -eio2b -eio2p -eio2s -2ei1p -eip2f -2eir -eir2c -e3irr -ei3ru -ei4s5aer -eis5ala -eis5alt -eis5ane -ei4s5ant -eis5anz -eisch5er6k -ei6schertr -ei6scher6z -ei6schin -eisch5war -ei6s5elas -ei6s5en6te. -eise5ra -ei4s5erf -ei4s5erg -ei4s5erh -ei4s5er4l -ei4serm -ei6s5er6sa -eis5ersp -ei6s5erst -ei4s5erw -ei4s5erz -eises4s -ei4sex -eis3id -ei3s4ky -ei4soe -ei4som -ei2sö -ei3spe -ei3spr -eis6s5chen -ei5stei -ei5stel -eisterin6f -eis4tol -eis4tri -ei5stue -ei4s3um -e2it -2eita -ei4t3ab -eit5aer -eit5alb -eit5alk -ei4t3an -ei4tap -ei4t3ar -ei4tas -ei4tat -2eitä -ei4t3är -4eite. -eit3e4h -ei4t5emi -4eiten -eitende6m -ei5t6er. -eit6err -ei4tess -ei4t5eti -ei2t3h -4eiti -ei4t3in -eito4r -ei4trau -eit5rin -4eits -eit4s5a4g -eit4sal -eitt4 -eit5tr -ei4t3um -ei4t3ur -eit3z2 -2eiv -ei4zar -ei4z5eig -ei4z5inf -ei4z5ins -ei4z5int -e2jak -2e3ju -2eka -ekaden6z5 -eka4l5oe -ek4ar -ek2ä -1ekd -e1ke -eke2n -e1ki -e1k2l -ek2n -eko4man -ek4or -ekor4da -2ek2r -ek2sa -ek4s5te -2ekt -ek4t3ae -ekt5akt -ek4tan -ekt5ank -ek5tante -ek4t3at -ek4tau -ek2t3ä -ek4t3eb -ekt5eig -ek4t3e4l -ekt5ende -ek4tep -ekt5erf -ekt5erg -ekt5erk -ek4t5erl -ekt5ero -ek4t5err -ek4t5erz -ek2th -ek4t5ins -ekt5int -ek4t3ob -ekt5ret -2ek2u -ekue3b -ek2ü -ekü3b -e3k2w -1ekz -e1la -e2l3aa -el3abb -ela4ben -e3labi -e4labk -el3abl -ela4br -el3abu -e4lada -el3add -el3aef -e2l3af -e4laho -e2l3ak -e3lake -ela2l -e4l3ama -el3amb -e4l3ame -el3amm -e4l3ana -e4lanb -e4landa -e4lanf -e4lanh -ela3n4i -e4l3ank -e4lanm -el3ann -e4lanp -el3ans -el3anz -e2l3ap -e2l3ar -ela4ri -el3a4si -el3a4sp -el3atl -e4l3atm -el3atr -e4l3att -el3au. -el5aufg -el5aufl -el3auk -e4l5ausb -el5ausl -el5ausr -e1lä -e3läd -el3äf -3el4bis -elb4lei -elb2r -elb3sl -elb6star -elb4ste -elb6stim -elb4st5o -elb4stu -el1c -elda4r -el4d5ari -el5d4eic -el4d5er4f -eld5erhe -eld5erho -eld5erhö -el6d5er6pr -el6d5er6sa -eld5ersp -eld5erst -eld5ertr -el6d5er6we -el4do4b -elds2 -eld3st -2ele. -4elea -elea4r -e6l5e6bene -el5echt -e3lecl -el3edi -ele4g5er4 -2eleh -el5ehe. -4elei -e6l5ei6ern -el3ei4f -el3ei4l -e4l5einb -e4l5einf -e4l5eing -e4l5einh -e4l5eink -e4l5einl -el5einn -e4l5einr -e4l5eins -el5einto -e4l5einz -el5eisf -el5eisz -el3eiw -e5lekti -5elektr -e2lel -el3elb -el3ele -el3eli -el3ell -el3els -el3elt -e2lem -e3lem. -e3lema -el3emb -3eleme -el3e4mi -e3lemm -el3emp -4elen. -elena4 -elen6klo -e4l5ense -e4l5entf -e4l5ents -e4lentw -el5enzi -el3epe -4eler -e5ler. -eler4e -e6l5er6fah -e6lerfin -e6l5erfol -e4l5erga -e4l5ergä -e6lergeb -elergene6 -eler5h -elerin6f -elerin6t -e4lerke -e4l5er4lä -e4l5ernä -e4l5eroe -e4l3erö -el5erre -el5ersa -el3erw -e6lerwei -e6lerzae -e4lerzä -eles4p -el3ess -ele5ste -el5e4tag -ele4tik -elet4ta -el3eur -e5leut -ele2x -e3lex. -el3ex3e -el3exk -el3ext -elf2a -3elfd -elf2l -elf3st -elgi5er. -elgi5ers -el3g2l -elg2r -e2lid -e3lie -eli5ef. -eli5eff -eli5efs -elif3 -e4l3i4ma -e4l3imb -el3imp -e4linfe -el3inj -eli5no -el5inso -e4l3inv -el3i4on -e4l3iso -eli4t3h -elk4a -el4k5anl -elke4n -elk2r -elk5sta -ell5akt -el4larb -ellar4c -el4l5arm -ellar6ma -el4l5art -ell5aufb -el6l5auft -4ellä -el3le. -ell5einf -ell5eing -ell5einh -ell5einr -ell5eins -ell5einz -el4l3el -el3len -ellens4 -ell4er -eller7ge -eller6tra -eller6zeu -elle3s -el4lid -el4l5inv -3ellip -el4l3or -ell3s4k -elm4stu -2elo -e2l3oa -e4loel -eloe6se. -e4lonk -el3opf -el3orb -el3orc -el3o4ri -e4l3orn -el3ort -e4l3osz -e3lot -e2lov -elö4se. -el4sb -els5eins -elsgene6 -el4sob -el4strü -el4sun -el4tans -elt5art -el4t3e4k -elt5eli -elt5entr -elt5er6be -elt5erd -elt5er4f -elt5erhe -elt5erk -elter6ke -elt5er6le -elt5erlo -elter4n -elt5ero -elt5er6sc -elt5er6tr -elter6zeu -el3thü -el4t3i4b -elt3se -elt5s4po -elts4tr -e1lu -e2lum -el3umb -el3umm -e2l3u2r -el3use -elu2t -el3ute -e4l3uti -el3uto -e1lü -e2lya -el4z5aep -el4z3ar -el4zäp -elz5eig -el4z5ene -el4zoe -e1m -e4mago -e4m5alar -e4m3alk -e4manb -e4manf -e4m3anm -e4manor -e4manp -e4manr -e4m3ans -e4manw -e4manz -e4m3asp -ema3to -em3aus -em2ä -3embol -3embry -em4dae -em2dä -em2dr -em4due -em4dum -em2dü -e2meb -e2mef -e2meg -eme4n3e -emen6gel -e4mengt -emen6t5ak -emen6tan -emen4t5h -e2meo -e2meö -e2mep -em3e4po -eme4r3a -emerin6t -emer4le -emes3t -e2m3ex -emi4e3i -emie3s -4emil -2emin -emin4a -e4m3inf -e4m3inh -e4m3inn -e4m3ins -e4m3int -e4m3inv -3e2mir -emi6scha -e5mitg -emi3tr -em4m3ap -emma3u -em4m3ei -e4moel -emo3ra -emo3s -5e4motio -3empfa -3emp3fä -em3pfl -3emphy -empi4re -em4p3le -emp5lin -empo3s -em4san -em4s3au -ems5ele -em4spa -ems4por -em4stal -em4s5tau -em3stu -3emuls -em3una -em3urs -e3mus -en1a -e3na. -e2naa -en3ab3i -ena2c -en5ach. -e3naep -ena5er. -e4naho -en3a4ke -e3nal. -e3nale -ena3li -4ename -e4namp -en3ams -ena2n -e4nant -ena4pa -en3ark -ena4sc -e4natl -en5atom -enat4s3 -e4natt -e4nau4f3 -e6nausta -enauto6re -ena2x -ena4zi -en1ä -e2näl -e2när -4enärm -4enbom -en4ce. -en3dac -enda4d -end3ak -en4d3al -en4d5ang -en4dark -end5eins -ende5int -ende4l5ä -end6er. -endes4p -ende5sta -ende5sti -ende5str -en4d3ex -en4do4b -3endog -en5d4ort -en4dox -en4dral -end5rom -end5s4au -end3s4l -ends4p -end2ü -2ene. -e5nece -ene4ck -e4n3eff -ene2h -en3ehe -e5nehm -e5neien -4eneigu -en3elb -en3elc -en3e4le -en3elf -e4n3eli -e4n3elt -4enem. -en3e4mi -4enen. -enens4 -e4n3epe -en3epi -en3epo -e2n3e2q -4e5ner. -ene3ra -e4n3erd -ener5ei -ener6ei. -en5ereig -e4n5erek -en3erf -ener4fi -e4nerfo -en4erfr -e4nerg -en5erge -5energi -en5ergu -e4n3erh -e4n5erla -e4n5ermi -e4n5ermo -en5ermu -4enern. -e4n5erna -ener5ob -ener5oef -ene5roes -ene5rös -e4n3err -4eners. -e4n5ersa -e4n5ersp -en3erw -e4nerwi -e4n3erz -4enes. -en3ess -e4n3eth -en3eul -4eneut -e4n3e4va -e4ne2x -enf2a -enf5einh -en5fes -3engad -3engag -5engelh -engel6st -enge6ne. -enge6nen -enge6net -engerin6s -5englis -en3gna -en3gno -eng5oel -3engp -eng3s4c -eng5stä -enhof5s -2enic -eni2d -en3ide -e3nie -eni5er. -eni5era -eni5erp -eni5ers. -eni5erv -en3i4ko -en3ill -eni2m -en3ima -en3imi -enin3a -eni2o -e4n3ion -2enis -eni4sa -eni5sta -e3niv -enizi5e -en4k5aus -en6kenla -en4k5er4f -en6k5erha -en6k5erkr -enker4l -en6k5ersa -en4k5ort -enk5spa -enk5spo -enk4su -enk2ü -4enlic -en4nef -en6n5er6fa -3enniu -enn5sa -e2n3oa -en3o4be -e4nober -en3obo -e4n3o4br -e4n3oe4d -e4n3oel -e2n3of -e2n3oh -en3oli -en3olm -en3oly -eno2n3 -en3opf -e4n3opi -eno2r -en5oran -en3orc -e4n3ord -e4n3ori -en3ork -e4n3ost -en3osz -e5noti -eno2w -e1nö -en3ö2d -enre4gu -en5rei -ensa5es -ens5amte -en4sanf -en4sang -en3sat -ensau4g -ensau4s -ensche6f -en6schlic -en4s5ehr -5ensemb -en4s5en5e -en4s5enz -ens5erei -ense4t -en4s5eta -en5s4law -en4s5off -en4s5ont -en5spen -ens4pu -4enst. -enst5alt -en6starb -4enste. -4ensten -ens7teru -4enstes -4enstl -4enstm -4enstn -en4stob -4ensts -4enstv -en4sumf -ens5umv -ens5umw -4ensym -en4tanm -ent5anst -entar6tik -en4tela -en4telä -en4tema -ent5emo -en6t5en6de. -enten6den. -en6t5en6des. -ente4n5e -5entera -ent5erben -en4t5erf -5entero -en6terre -ent5er6sc -ente4t -ent5eta -3entfl -4entfo -3entgi -5entheb -en4t3id -3entla -5entloh -3entlu -3entlü -5entnah -en4t3ob -ent5oel -en5tré -5entrieg -5entropi -5entsag -3entso -3en4tw -6entwert. -6entwerte -6entwerts -4entwet -3entz -4entzuw -en1u -e3nu. -e3nueg -enu5ett -enu4ma -enu2r -2enu2t -e4n3uto -e3nüf -e3nüg -e3nül -e3nüp -enü3st -e1ny -enz2ä -enze4ne -enz5erfo -en4z5erh -en4z5erk -enzer6ku -enz5erla -enz5ermi -en4z5ero -enz5erte -enz5erwe -en4z5erz -en4zf -en4z3i4d -enzo4l -4enzuf -4enzyl -3enzym -4enzys -2eo -e1ob1 -eo4ber -e2obo -e2ob2r -eo1c -e3ochs -eod2 -e1oe2 -eo3edi -eo3em -eo3en -eo3ers -e3off -e3okk -eo3la -e3o2ly -e1on -e3o4n3ae -e3o4n3an -e5onatom -e3o2nä -e2ono -eo1o -e3o2pe -e3opf -e3ops -e1or -eo3rau -e3orb -e3ord -eor5eis -e3org -eort2 -eo1s2 -e3os. -eo3s4e -e3oste -eoti3e -eo3ul. -eo3uls -eou2t -e1ö2 -ep2a -epa2g -epan6zerf -epas6s5er -ep3ega -ep3ent -ep3ex -2epf -eph3el -eph3em -e1pi -3e4pid -3e2pig -3e2pik -3e4pile -epin3g4 -e3pio -3episo -3epit -ep2n -e2poc -epoli3 -3e4pos. -ep2p3a -ep4pei -ep2p3f -ep2p3l -ep2pr -2e1pr -ep2s3c -ep3sh -ep4t3ae -ep4tal -ep4tau -ep2tä -epter4s -ep6t5erst -e3pu -epu2s -e1pü -2eque -3equip -er3ab. -4e3raba -e4r3abb -era4ben -er3abr -er3abt -er3ach -e3rad. -er3add -e5radi -e4radj -er3adr -erae4b -era5ebe -e3raed -er3aef -er3aeh -er3ae4m -er3aer -era5er. -er3aes -er5aeuß -era5fra -era4gen -er3a4gi -e3rago -er3ahl -era4ho -er3aic -e3rake -e3rakl -era4kro -e4r3akt -e3ral. -e3rald -e3rale -eral4eb -era6lent -e3rali -eralin6d -era6ling -eralin6sp -er5alke -er3amb -er3amn -e4ramp -e4r3a4na -erana5t -e5rand. -e6rander -er5andr -e4ranf -er3ang -e4ranga -e4rangr -e4ranh -er3ank -e4ranl -e4r3a4no -e4r3ans -e6rantei -e3ranu -e4ranw -er5anzu -er5anzü -e3rao -e3rap -er3apf -er3apo -era4rie -e4rarr -e4rars -e4rart -e3ras. -er3as4b -er3ask -er5ass. -er5assi -er3a2ß -e3rat -e4ratel -e4r3atl -e4r3atm -er3au -e3raub -e3rauc -erau4f3 -e4raufs -e3raum -e3raup -erau4se -e4rauss -e4raust -er3a4va -e3raw -e4r3ax -e1rä -er3äb -e3räd -er3äf -er3äh -er3ä2m -er3äp -e2r3äs -eräte3 -er3ätz -erbe5ers -5erbeti -3erbig -erbin4f -erb4lo -er5brec -5erbreg -er1c -er5ch4e -erche5i -er3chl -5erdaep -erd5ant -erd5anz -3erdäp -3erdb -erdens4 -erdes4t -3erdg -er4d5inn -er4dob -erd4os -3erdöl -erd3st -2e3re. -er5e4ben -er2e2c -er5echs -ere4del -er5e4dit -ere3en -er3e4he -e3rehi -e3reho -er3ehr -4e3rei. -4e3reia -4e3reib -erei6be. -erei4de -e3reie -e5reiga -e5reigeh -5ereigni -4ereih -4ereik -4ereil -er3ein -e4r5einh -e4reinl -e4reins -e4reint -er5eisar -e4r5eisb -er5eisd -er5eisf -er5eisr -erei5str -er5eisv -er5eisw -4ereit -erei6t5erk -4ereiu -e3reiv -4ereiz -er3ele -er5e4lit -4e3rem. -er3emb -er5e4mig -ere4mis -er3emp -e4r3ems -e4r3emu -4e3ren. -e3rena -er5endg -erengene6 -eren5sk -e4r5entn -e3renz -eren6z5en6d -eren6z5er -er3epi -e4repos -4e3rer. -4ererb -e4r5erde -erergene6 -e4rergo -e4r5erho -erer4hö -e4r3erk -e4r3erl -erer4lo -erer4lö -4erern. -e4r5erna -e4r5erne -e4r3ero -4er6ers. -er5ersat -e6r5ersche -e4r5ersp -e4r3ert -e4r3eru -4ererv -e4r5erwa -e4r3erz -2eres -e3res. -e4r3ess4 -er5essen -ere5star -ere4tab -e4r3eth -eretri5 -e4retu -er3eul -er3e4va -e4r5e4vid -erf2a -5erfahru -erf4e -erfen4s -5erfolg. -5erfolgr -erf4ta -erg5ant -er4g5are -5ergebn -er4g5ein -er4g5elf -6ergenau -5ergenb -6ergeni -5ergenp -3ergom -er4g3op -erg3s4o -erg3s4p -erg5sta -erg5sti -erg5sto -erg5str -erg5stu -5erhaltb -er3ho -e1ri -e4r3iat -e3rib -2eric -erich6t5er6s -e4r3ico -4erie -erie3b4 -erief4 -erie5fl -erie4ne -erien5en -eri5er. -eri5ers. -e3rif -er3ima -er3imb -er3imm -er3imp -4erin. -e4r3ind -e5rind. -e3ring -er3inh -e4r3i4ni -e4r3in5k -er3inl -6erinnen -5erinneru -5erinnr -er3ins -e5rins. -e4r3int -e4r3inz -e3rio -e4r3ion -e3ris -4erisc -er3isl -er3iso -e4r3isr -3eri3tr -4erkaj -er5kat -erk5ends -5erkranku -erk4sen -erk4ser -5erkundu -5erläut -5erlebn -5erledi -er5leg -4erlöh -5erlös. -erls2 -er3ma -er4m5alg -er5man -erme5sz -er3mo -erm2u -ernab6sa -erna5che -er4n5alg -er4n5all -er4n5alt -ern5erbe -ern5erei -ern5erst -4ernet -erne4te -er4n3ob -erno4r -er4n5ori -4ero. -e3roa -e3roba -e3robb -e3robi -er3o4bl -er3obm -e3robo -er3o4br -er3obs -er3oe4d -e3roeh -e3roen -er3oer -e5roes. -eroe4s5c -er3off -e3rofr -e3rog -e3rohl -er3ohm -e3roi -e3rol -e4r3oli -e4r3oly -e3rom -ero3ma -er3omb -e3ron -ero3n4a -ero4nan -er5opal -er3o4pe -er3opf -e3ropo -er3o2r -eror4a -eror4e -e3roro -4e3ros. -e3rosa -e3rose -5erosio -e5rosit -ero5sto -e3rotr -er5otter -e3row -er3ox -e3rozy -e3röc -er3ö2d -er4öh -e3rös. -erö4s3c -erp2 -er5pen -5erprobt -5erprobu -er3rae -er4rant -er3rä -5erregt -erri5er -5erringu -5ersatzo -5erscheinu -ersch4l -ersch4m -ersch4r -ers2e -er3si -er4sos -5ersparn -er7stati -5erstatt -er7statu -erst3h -er3sto -er4t3a4d -er6t5akts -er4tax -erte4c -ert5eck -er4t5er4f -er4t5er4h -er4t5er4k -er7tersc -ert5erst -er4t3ho -ert5spr -2e1ru -eruch4s5 -e4rueb -eruf4s3 -e4r3uhr -er3u2m -erum3a -erums4 -er3und -e4rundu -er3unf -er5unga -er5ungl -erungs5 -e4r3unk -er3up. -3e4rupt -eru2r -er3u4se -er3uto -er3uz -e1rü -ervenen6 -5erwerb. -e2rya -er4z5erk -erz3te -erz4wi -es3abl -e2s3a2d -e4s3aem -e4s5aerz -e4s3all -e4s3alt -e4s3ami -esam4t5r -es5andr -es4apa -es3art -e4s3ato -e2s3a2v -e2s3ax -es3b -esbi5er. -e3scen -e4s5cham -esche6mi -e6schent -e6sch5er6f -e6sch5er6l -esch5op -e2sco -es2cr -es3da -ese2c -ese3er -es3ehr -ese3i4m -e3s4ek -3eselc -ese4len -ese4ler -esen4ac -ese4neu -esen5th -ese5ref -e5seri -eserin6t -es5erke -es3erö -esers4 -eser3u4 -e5serv -ese5str -eseu2 -ese2x2 -es3ext -es3f -e3sham -es3han -es4har -es3he -eshi2 -es3him -es3hor -es4hut -2esi -esi5ers. -e2s3i2k -e4simp -e4s3inf -es3ini -es3int -e2sk -es3ke -es3kl -e4sky -es3lo -es3m -e2s3o2b -e2s3od -eso4de -es3ofe -eso3pa -e4s3orc -e4s3ori -es3ost -e1sö -2e1sp -es4pec -es4poe -es6portp -es6portv -es5rei -4ess. -es4s3ad -ess5alt -essau4s -3essay -4essä -4essc -es3sch -esse5er -esse3g -esse6l5ers -es3ser -es6serfah -ess5er6ho -esser6hö -esser6sae -esser6sat -es6ser6sä -4essk -4esso -es4sof -es4sph -es4spu -es4stec -es4stop -essur4k -e4st. -estab4b -e4stabg -e5stael -e3stah -est5anst -es5tant. -est5anza -esta4s -est5asc -e4s5tat. -e5statu -est5auf -e4staum -estauto6r -es4tec -e5stein. -e5steine -e6st5eing -est5eink -est5einl -est5einr -est5einsc -est5eint -estell5a -es4t5emi -est5empo -e4s3ten -esten6ga -est5engl -es6ten6se -est5entr -est5ents -ester6ken -est5ernä -ester6n5o -est5eroe -es4t5er4ö -est5er6tr -ester6weit -es4t5es4s -es4thy -es4t3id -es5tij -e3stil -est5ing -es4tins -e4s5tipp -estmo6de. -es4tob -e3s4toc -e3stol -es4top -es4t5org -es4t5ort -es2tr -es5trac -es5trag -e5strang -est5rate. -e5streb -e5streck -e5strick -e5strik -es5tros -e3strö -e4strun -e3s4tub -e3s4tue -e4stueb -est5ums -es4tu4r -est5uri -est5urk -e4stüb -es4tüm -e3sub -e3suh -e4sumg -e4s3ums -e4s3umw -es4und -es3unt -e3sup -es3urb -es3url -es3urs -es3urt -esu2s -es3w -e3sy -e2sz -es3ze -eß3ele -eße3re -e4ß3er4l -et5a4ben -et2a3c -eta3e4b -et3aeh -et3agi -etal4l5o -et3a4mi -eta3na -e4tanh -e5tanj -et3ano -e4tanp -e4t3arb -et3art -eta4t3r -et5aufs -et3auk -et3äh -2ete -et3eff -et5e4he. -e4t3eim -et5eins -e4t3eis -etel4n3 -etend4 -eteng4 -ete4n3i -e4t5ents -e4tentw -eten6z5er -e4t3e4pi -ete4r3ä -ete6r5eis -etergene6 -e4t5erhö -eterich6 -eter6loe -e4t5er4lö -eter5oe -et3erö -e5t6erta -e3tes -eth4a -3e4thik -3ethn -3ethol -et3hon -e2t3i2d -eti4kar -eti4kl -eti4kr -eti2m -eti3na -et3ind -e4t3inf -etin4g -e4t3inh -et3ini -eti4ta -eti4th -et3obs -et3o2f -eto4n5al -eto4nar -eto4ner -etons4 -e4t3orc -e4t3ord -e4t3org -et5rade -etra5e4d -et3rec -et2ro -et2sa -et3sac -ets3ae -etscher5e -etsch5wu -ets5eng -et4sim -et3sö -ets3pr -ets4toe -ets4tu -et2su -et4t3a4b -et4tad -et4tae -et4t3ak -et4t3a4m -et4t5ang -et4tanm -et4tans -et4tap -et4t3at -ett5auf -et2tä -et4temb -ette4n -et4tesp -et4t5ess -etto5en -ett5ope -et2tö -et2tr -ett5rei -ett3ru -et4t3um -2etu -3e2tui -e4t3ums -et3ups -et3use -ety2l3 -3e2tym -2etz -et2za -et2zä -et4z3eb -et4zed -et4z5eig -et4z5ene -et4z5erw -etzes3 -etze4t -et4z5eti -et4zoe -et2zw -eu1a2 -eu4b3le -eu4b3li -euch4ta -euchter6s -euch4to -euch4t5r -euder4e -eudi4e -eu1e -eue2d -eue3fa -eue2m -eue3p -eue4rei -euer5eif -euer5eig -euer5eis -euerer6r -euerer6s -euerer6t -eue4ret -eue4ro4 -euer5of -eu4esc -e3u4ess -eu3f2r -eu2ga -eug3an -eu6g5endm -eu6gense -eu4gid -eugin4f -eu4g5in4g -eu2gl -eu4gre -eugs2 -eug3sp -eug5sta -eug5sti -eug5stu -eu3hu -eui2 -eu3id -euil6le. -eu3in3 -eu3ir -3eukal -e2ul -e3um. -e4ume -eum3p -eum4s3p -eu3nas -eun2e -eune4b -eung2 -eu4ni. -eu4nio -eu4nis -eu1o2 -e3up. -eu3p2f -e3upg -3euph -eu4r3an -eurasi5e -eu4r5ast -eura3t -eur3c -e4u3re -eure5in4 -eur5eis -eurin4f -eur5in4s -e4uro -euro3s -eur4sau -eur4so -eur6s5tae -eur4stä -2eus -e3usar -eu6sch5ei -eu6schel -eu6sch5erz -eu4schl -eu4schm -eu6schue -e3usd -e5useff -eu3s4ei -e5usevan -e3usg -e3usk -e3usn -eu3spr -eus4s3c -e3usst -eu3sta -eu3sto -eu3s4t4r -eu3su -eu3tei -eute5in4 -eute5r4a -eut2h -3eu3tha -eut4o -euts4 -eut6scha -eut6schm -eut6schn -eut6schr -eut6schw -euve4 -2eux -eu1y -eu2za -eu4zec -eu2zo -eu2zw -e1ü -2ev -3e2vak -eval4s3 -evan4z3 -ev4en -eve3r4i -e2vl -ewal6t5er -e2w3au -ewebe5i -eweb4s -ewei4sc -ewer4g -ewer5te -ewi2e -e3win -ewinde5s -e3wir -e3wit -ew4st -ewuerzes6 -ewürzes6 -2ex. -3exam -2exan -ex4ans -3exek -e2x2el -ex3ele -2exes -2exf -e1xi -3e2xil -e2x3in -ex3l -ex3or -1exp -2expu -2exs -2exta -4extet -ex4tin -ext5rei -2extv -2exu -e2xum -ex3v -2e1xy -ey1 -eya2 -ey2dr -ey4ere -ey2ne -eyo2 -eys2 -2ez -e3za -e4z3enn -eze3re -ez3erk -eze3s4a -ez2o -e3zoh -ezu2e -ez2w -é1 -é2es -égi2 -élu2 -ém2 -ér2 -éu2 -è1 -ène1 -2ët -ê1 -ê2t -1fa -fab2 -2f3abb -fa4ben -2fabf -2fabg -2f3a2b3l -2fabn -f3a4bre -2fabs -2f3abt -2fabw -2fabz -fa2ce -fach3a -fa4cheb -fa4ch5ec -fa4chel -fa6cheng -fa6chent -fa6ch5en6z -fa6ch5erf -fa6cherk -fach3i -fach3o -fach5sk -fachs4p -fa5chung -fad3a -fa4dan -fa4del -f3ader -fa2di -f3adl -2f3a2dr -fad3t -fa2e -fae2c -fa3eck -f3aem -fa4er -f3aerm -4f3aerz -faesser6 -faes6s5ers -faes6serw -fae4ßer -4f3aetz -f3aeug -2f3aex -fah6l5ent -fahler6z -faib2 -fa4kad -fa2ke -fa3la -fal4k3l -fal4l3a -fal4l5ei -faller4 -fal6l5ere -fal6lerf -fal6lerg -fal6l5erk -fal6l5erm -fal6l5err -fal4lo -fal6scha -fal6schei -fal6sch5l -fal6schm -fal6sch5w -fal4t3r -fal4zw -fa4mei -2f3amp -f3amt -3famy -f4an. -fa4nar -fan4d5er -fan4dre -fan4d5ru -fan4drü -fan4gei -fan6gert -fan4gra -fani2 -fa4n3in -2f3ank2 -2fanl -2fann -f3a2no -2fanr -2fanw -2f3anz -f1ap -fa2po -fa3r4a -far4b3a -far4b3ä -far4bel -far4b5er -far4b5in -far4b3l -far4b3o -far4b3r -far4b3u -far4m5an -4f3armi -2f3a2ro -far4r3a -far4rh -2f3art -2f3arz -fa3s4a -faser5e -fa3sh -4f3assi -4f3astr -fa2ß -f1at -fa2to -2fatt -2faue -2fauf -f3aug -f4aul -f3ausb -f3ausl -3fav -1fä -fä1c -2fäf -fäh4r3u -2f1äm -2fäq -2f3ärm -2f3ärz -fässer4 -fäs6serk -fäs6s5ers -fäs6serw -fä4ßer -2f3ätz -2fäug -2fäx -2fb2 -fbau3 -fbilder6 -fbil6derh -2f1c -f3ch2 -2fd2 -fdi2e -f3ds -1fe -featu4 -2f3ebb -fe4ben -fe2c -fecht6e -fechts5e -2f3eck -fe4del -federen5 -fede4ro -fedi4e -fe4dit -fe2dr -feein5 -2feff -fef4fr -fefi2 -fef2l -2f3e2he -fehl3a -fehle4 -feh4l5ei -feh6lerinn -feh4l3o -f3eib -2f3eic -fei4l3a -fein6d5ei -fei6n5erz -4f3einl -4f3einn -4f3einq -fe5ins. -fe5inta -fe5into -fei4nu -4f3einw -4f3einz -f3eis -4feise -3fek -fek4ta -fek4t3r -3fel -fe2la2 -fel3ae -fel3at -fel3au -fe2l3ä -f4elb -fel5bi -felda4 -fel4dac -fel4d5ar -fel4d5e4m -fel6d5end -fel6d5er6k -fel4dra -fel3e4c -4f3e4lef -fe4l3eg -fel5ein -4f3e4lek -fe4l3en -fe4l3er -fe4les -feli4n3 -fel5ins -fell5ans -fel6lein -fel6l5ers -fel4lin -fe2l3o -fel6spir -fel5sti -6f5el6tern -2f3emb -fem2m -2f3emp -f4en. -fe4na4g -fe2nä -fend2 -fende4f -fen5end -fe4n5ent -4f5energ -fen5er5o -fen5erö -fene4t -fen3f2 -4fengp -fe4nid -fen3in -fen3i4t -fenk2 -fenn2 -fe2n3o2 -fen4sal -fen7schl -fen3sh -fen5skl -fens4po -5fenst -4f5ente. -4f5en4ten -4f3entf -4fentla -4f3ents -4fentw -4fentz -fe2n3u -f4er. -fer3ab -fer3a4d -fer3ae -fer3al -fe5rant -fer5anz -fer3as -fe2r3ä -4ferd. -ferde3 -4ferde. -4ferden -ferdes4 -4ferds -fe4r3e4b -fer5eck -3ferei -fe4rein -fe4r5eis -fer5eiw -fe4rek -fer5ell -fe4r5erd -fe4r5er4g -fe4r5erw -fe4r3e4v -fer6fahru -fer6fuel -4f5er4fül -6fergebn -fe4r3i4d -ferien5 -ferin4f -ferin4t -feri4on -fe4riso -fer6lebn -fernab6s -fern6au. -ferner6k -fer3oe -fer3of -fer3ö -ferr2 -fer6regt -fer3s4m -fers4p -4f3e4rup -fest5a4b -fest5ak -fest5al -fest5an -fest5ar -fest5ei -fes6t5end -fes6tero -fes4th -fes4tin -fe4s4t3o4 -fest5ru -fe4tag -fet5ero -4f3e4tik -fet4t3a -fet4t3h -fetti5s -fett3r -fett3s -2feu. -4f5eu5cha -feuer5o -2f3eul -2f3eut -f1ex -2fexp -1fé -2f1f -ffa5chu -ff3a2d -ffang5s -ff3ans -ff3ark -ff3arm -ffa2t -ff3au -f2f3e2b -ffe2e3 -f2f3ef -f4f3eig -ffei4n -ffe5in. -ff5einb -ffe5ini -ff5eink -ff5eins -ffel3a -f4felem -ffels4 -ffe2m -f4f3emi -ffe4rec -f4f3erh -f4f5erze -ffe2t -ffet3r -f2fex -fff2 -fffen4 -f2f3im -f2fj -ff3län -ff3lät -ff3lec -f2f3o2b -ffo2r -ff3ori -ff3ott -f2f3ox -ff3rak -ff4ren -ff3roa -ff3rol -ffsa4 -ffs3al -ff3sam -ffs3au -ffs5end -ff4stau -ffs5tie -ffs5tut -ffs5tür -ff3ti -f2f3um -ff3urs -f2fy -2fg2 -fga4s5er -fgas5te -fgeb2 -fge3na -fger4 -f3gh -fglim4 -2f1h -fh4au -fho4l3a -1fi -3fi. -fi2ar -fi3ask -fi3at -fici3 -fi2do -fid2s -2fi2dy -fie3b4l -fie4br -fi3er4f -fi4gec -fi2g3r -fi4kanf -fi4kanr -fi4k5ans -fi4kanw -fi4k3as -fi4k3el -fiker4s -fi6k5erst -fi4kin -fi4kle -fi4klo -fi4klö -fi2kn -fiko2 -fi4k3ob -fi4k3op -fi4kor -fi2k3r -fi4l3an -fi4lau -fi5laufb -filauf6n -filb2 -fi4l3ei -filen4 -fi4l5end -fi4lent -fi4lep -fi4les -fi4l3in -fil4ip -fil4ma -film5as -fil4mä -fil4m5ed -fil4mei -filo2 -fi4lob -fil3st -fi2mi -2f3imp -f4i3na -fi4nant -fi4nar -fin4dia -fi4nel -fi4net -finge6ro -fingst5r -2f3inh -2f3inj -4f3inse -fin4sp -fin4ste -fin5sti -fi3ol -2f3i2ri -fi4sch5a -fi4schä -fi6sch5ei -fi4schl -fi4schn -fi4sch5o -fi4schr -fi4sch5w -fi3s2h -2f3i2so -fi3s2p -fis3t -fiste4 -fite2 -fit3el -fi3ti -fi4tin -fiti3s -fito2 -fi4t3or -fi4t5rat -fi4xel -3fiz -fi2za -f2jor -2f1k2 -fkaken4 -fka4t3i -2fl. -fl2a -f3lab -3f4laec -f3laed -4flaen -f3laeu -f3lamp -flan6sch5l -flanzer6 -f3lap -f4lasc -f3lats -f3lauf -flauma4 -3f4läc -2f3läd -2flän -f3läu -fl2e -2f3le. -2f3leb -f2lee -4f3lein -flei6sch5r -4f3leit -flek5t -2f3ler -2f3li. -f5lich. -f5liche -2f3lig -2f3lil -f2lim -f3lind -fli4ne -4f3ling -4f3lini -4f3lins -f2lit -f3loef -f3lok -2f3lon -3f2lop -3f2lor -flo3ra -5floss. -3floß -3f2lot -flo2w3 -2f3löf -f2lög -f2luc -f4lueg -f3luem -f3luf -f4lug -flu4g3a -flu4ger -3flugz -f3lup -fl4ur -flur3i -3f4luss -flut3o -f2lüg -f3lüm -3flüs -f2ly1 -2f1m2 -fma2d -fmarschach6 -f3mu -2f1n2 -f3ne -1fo -foa2 -fodi2 -2f3oe2f -2f3oek -f3oele -fo3eli -foer4s5 -f3oe2s -2f3o2ly -fo4nad -fo4n3al -fo4nam -fo4n3an -fo4nap -fo4nar -fonat4 -fo4nau -foni4en -fo4n3in -fo2nu -f1op -2fo2pe -fo3rat -4fordn -ford3s -2f3org -3form -for4mae -for4m5a4g -for4mas -for4m5au -for4mä -for4m5ei -for4min -for4mor -forni5er. -for6schl -for4st -forst5ei6 -for6st5r -for4t5ei -for4t5er -for4t3r -fort3s4 -fo3r2u -fo3sta -fo3to3 -fo5toe -1fö -föde3 -2f1ö2f -2f1ök -2f1öl -för2s -förs5t -f1ö2s -2f1p2 -2fq -fr2 -f3ra. -frach6ta -frach6t5r -2f3rad -fra4dei -4f3raed -f3raet -f3raf -frage5i -2f3rah -2f3ral -fr4a2m -fram3a -f2ran -f3rand -f3rann -2f3rap -f3rase -fra3st -fras5ta -3fraß -f3rat. -f3rats -3frau. -f3raum -fraus4 -frau5se -2f3räd -f2rän -2f3rät -f3re. -f4rean -f5recht -fre4e -2f3ref -f3rege -f3regi -f3reh -f4rei. -freib4 -4f3reic -f4reie -frei3f4 -frei3k4 -frei5str -2f3rek -2f3rel -3f2rem -f3ren -2f3rep -3f4req -f4resc -4f3resi -fres4st -f3rest -f4rete -f2reu -2f3rez -f1rh -2f3ric -fri3d -2f3rig -2f3rin -frin6g5er -f3rip -3f4risc -fri6scha -f4rist -2f3roc -2frol -3f2ron -fro4n3a -fro2s -f3rose -frost5r -3f2ruc -f3rud -f5ruehr -f3rues -fru6fere -f3ruh -f3rum -2frun -f3rut -f3rühr -früh3t -f3rüs -2fs -f3sac -f2sad -fs3aer -f2sak -f3sakr -f4s3all -f4s3alt -fs3amp -f2san -fs3ana -fs3ane -fs3ang -fs3anz -f2sar -f2s3as -fs3a4te -f4s3auf -fsau4n -f4saut -f2sär -fs3äus -f2sca -f2sce -fsch2 -f5schal -f4schan -fscha4o -f5schau -f4schef -f5schra -f5schri -f2scr -f2s3e2b -fse2e -fsee3i -fs3ehr -fs3ein -f5seit -f2s3e2l -fs5endes -fs3e4n3e -f4s3ent -f3seq -f2s3er -fse3r4u -f5serv -fse4t -fs3eth -f2sex2 -f2si2d -fs5iden -fs3imp -fs3inf -fs3ing -fs3int -f2ska -f2sko -f3s2ky -fs1o -f2sob -fs3och -f3soh -f2sop -fso2r -f1sp -f5spann -f4spas -f4spel -f2sph -fsport5e -f4spot -fs5prie -fs4pul -fs3s2 -f3sta -fs5taeti -f4stag -f4s3tak -f4stale -f4s5tank -f4stas -f4s5täti -f3steg -f3stei -f4steil -f3stel -f5s4tern -fs3tes -f3steu -f3s2ti -f4s3tip -f4stit -f3s2to -f4ston -fs4tras -fs4tre -fs5tren -f3s4tri -f5strom -f3strö -f5s4tuec -f3stüc -f4s5tüte -fs3ums -f2s3un -f2s3ur -f2s3ut -f5szene -2ft -f2ta. -f4t3aeu -ft3ala -f5talg -ft3all -fta2n -f4t3ana -ft3and -ft3ang -f4t3ano -f4t3ans -ft3apf -f2t3a2r -ft3att -ft3auf -f2t3äu -fte2c -ft3eck -ft3e4he -ft3eig -ft3ein -f4t3eis -f4t5elek -f4t3eli -ft3emb -fter3a -f4t5erke -fter3o -ftersau6 -ftes4p -f4t3es4s -fte5stei -ft3eth -f4t3e4ti -fte3u -ftgu4t3 -f2t1h -f3t4hem -f2t3id -f3tik -f2t3im -f2tin -ft3ind -ft3inf -f3ting -ft3inh -ft3inn -ft3ins -ft3iso -ft3le -f2t3o2b -ft3oef -ft3of -f2tom -fto2p -ft3ope -ft3ora -ft3org -f2t3ot -ft5raed -f5t4ran -ft3rau -ft3ril -ftritt4 -ft3ruh -fts1 -ft2sa -ftsa4g -ftsau4 -ft4sän -ft3sch4 -ft4se4h -ftse4l -ft4sem -ft4send -ft4seng -ft4sep -ft4ses -ft4seu -ft2si -ft2so -ft4staf -ft3sti -fts5tick -ft6stier -ft4stor -ft6streu -ft6strop -ft4stur -ftu3en -ft3ums -ft3unb -ft3und -f4tunf -ft3urk -ft3url -ft3urt -ftwa4r -ftz2 -1fu -fuch4s5a -fuchs5t -f2ue -2f3ueb -fuen4f5a -fuen6fei -fue4r -f2uh -fuku3 -2ful -fuld4 -f3ult -2f3umb -fu4mei -fu4m5erz -fu2m3i -f2umm -2fums -fun6derg -fun6d5erh -fund3r -fund5st -2f3unf -4f3ungl -2f3u2ni -3funk -fun4ka -fun6kerk -fun4k3l -fun4k3o4 -fun4k3r -fun4ku -2f3unl -2funm -2funr -2f3unt -2funw -f2ur -fu4re. -2f3url -f2us -2fusc -fu4schw -fus4sa -fus4sei -fus4s5er4 -fus4sp -fu3s2t -fu4ß3er -3fut -1fü -2füb -fühl4s5c -fühl4s5e -fühl4st -fü2r -2fv -2fw -f1y -2fz2 -fzeiten6 -fzei6t5end -fz3t -fzu4gin -fzug4le -1g -3ga. -2g3aal -2g3abd -gabe3e -gabe3i -3gabel -gabe3r -2gabf -2gabg -gabi4t -gab5itu -2g3a2bl -gab5le -gab4ri -gab4ru -gab4rü -4gabsc -5gabst. -4gabstu -4gabstü -4g3abtr -2gabw -gaby2 -2gabz -ga1c -gade3s -g4a3di -2g3adl -gae5is. -ga3emp -4g3aend -2g3aep -ga3erh -g3aerz -gaes3 -gaeste5 -ga3eta -ga3fl -3gai -ga3il. -ga3in -ga1k -ga4kad -2g3akt -ga4kus -ga3la -gala3e -g3a4lar -4g3alau -ga4l5en4d -g3algo -4g3allt -4g3alta -4g3altd -g3a2lu -ga4m3al4 -4g3amaz -ga3me. -ga4meb -ga4mec -ga4mep -ga4m3in -gam5ma -gamo2 -gam3sc -gam3t -ga2mu -4g3anal -ga4nan -gan5ato -ga4nau -g4anbr -gan3d2 -gan5erh -gan5erk -gan5ers -2ganf -gan4gar -gan6g5e6be -gan4g5el -gan6g5er6b -gan4gr -gan4g3u -gan5inn -gank2 -4g3anke -g5ankun -2ganl -4ganmu -ga4n3ob -ga4nor -2ganp -2ganr -4g3ansi -g5ansti -2ganw -ga3ny -gan4zw -2g3app -2garc -garde3 -4g3area -ga4rena -g3arg -ga3rin -2g3ark -2g3arm -ga3r4o -gar2s -4g3arti -2g3arz -ga2s3a -ga2sä -gasche4 -ga3se. -ga4s3ei -ga4s3el -ga4s3e4m -gas5endr -ga4s5ene -ga4s5ent -ga4serz -gas3e4t -ga2si -ga2so -gass2 -6gassess -gas5tank -gas4tel -gast5elt -gas6tens -gas4tin -gas6t5rae -gas4t5rä -gas6trol -ga4sum -gat2h -ga3tha -2g3atm -gau4eri -2gau2f -ga3ufe -2g3aug -g4auk -ga3unt -4g3ausb -4gausd -4g3ausf -4gausg -4gausr -4g3auss -4g3ausw -g3ausz -2g3aut -g1äh -2g1äm -2g3änd -2g1äp -2g3ärz -gä4st -gäste3 -2g1ät -2gb2 -g3ba -gb4au -gbau3f -g3bä -gbes2 -gbi2e -g3bl -g3bo -g3bu -g3bü -gby2t -2g1c -2gd -g2dab -gd2ad -g2d3ak -gd3all -g2dan -gd3anz -gd3art -gdä1 -g2d3ei -gd3els -g4d3ent -g2d3er -gd3eth -gdi2e -gd3ins -g2do2b -g2dop -gd3roc -gd3roe -g2d3rö -gd3s2 -gea2d -gea2n -gea2p -ge3at -ge4at. -ge4ate -4g3ebbe -geb4le -geb4lin -gebot4 -ge1c -ged2 -gedi2 -4gedig -ge1e2 -ge3eg -geer3e -ge2es -geest3 -ge3ev -gef2 -ge5far -2g3eff -gegen5s4 -ge3g2l -ge2gn -geh5aben -ge4h5erl -geh5ste -ge3im -gei2n -4g3einb -gein4di -ge5in4fo -4g3eing -gein4ha -ge3ini -4g3einl -ge3in4n -4g3einr -gein6ten -gein6ter -ge3in4v -4g3einw -ge3ir -geise4 -gei3sh -ge3iso -geis4s5c -geist5r -2gek. -gela4c -gela4n -ge4l5ano -ge4l5ant -ge4lanz -g4elb -gel5bi -gelb5s -gel4del -gel4dem -gelder4 -gel6d5erh -gel6derp -gel6ders -gel6dert -gel6derw -gel6drue -gel4drü -ge3lec -ge4lef -4g3e4lek -ge4l5end -ge4l5en5e -ge4l5ent -gel5erd -gel5er5e -gel5erf -gel5erg -ge4l5erk -ge4l5erl -ge6l5erna -gel5er5o -ge4l5ers -gel5erz -ge4less -gel3f -gel4fe. -gelfen4 -gelgene6 -gel5int -gelk2 -gel3la -ge4l3om -ge5lot -gel4sal -gel6s5chor -gels4t -gel5ste -gel5sti -gel5sze -gel6tent -gel6terh -gelz2 -gem4a -ge4ma. -2g3emb -2g3emp -gem2u -gen4aes -ge5n4au. -ge5naue -ge5naust -gen6auste -4genda. -gend5in5 -ge4n3e4g -gen4eh -ge4n3el -ge4nemi -ge6n5ende -ge4nep -ge4n5er4b -ge6n5erei -ge4nerf -gen5er5in -ge4n5erk -ge4n5erl -ge4n5ern -gen5ersc -gen5erst -ge4n5ert -gener6wa -gener6wä -ge4n5esk -ge4ness -gen3f2 -geng2 -gen4ge. -gen6geln -gen6gels -genger4 -ge4nim -ge4n3in -genk2 -gen3n2 -ge4noc -gens5amt -gensch4 -gen6semb -gens5ert -gen5ska -g3entf -4gentfa -4gentfe -gen5t4he -4g5entst -4gentw -ge4nun -gen6z5art -gen6z5eig -ge3oh -ge3oo -geo4ri -ge3ou -ge3p2 -g4er. -ge3ra -ger3ab -gerabdru6 -ger5adm -ger5akr -ger5ala -ger5alb -ger5all -ger5alp -ger5alt -ger5amp -ge4r5ant -4gerdg -ger5ecke -ge4r5eid -ger5eig -6g5ereign -4g5e4remi -ger5ende -ge4r5en5e -ge4reng -ge4r5en4s -ge4r5ent -ge4rerf -ge4r5erh -gerer6ku -ger5ersa -ge4r5er4w -gerer4z -ger4hoe -4g3er4hö -gerin6de -gerin6di -gerin4f -4g5erklä -germi4 -ger4nau -ger3ne -4g3ernt -ger5obe -ge3roc -ge3rod -ge3roe -ge3ros -ge3rot -ger3o4v -ge3rö -ger6regu -4g5er4seh -gers4p -4g5er4wac -ger6wart -ger6weit -6g5erwerb -ge2ry -gerz2 -ge1s2 -g4es. -ges5anz -ges5auf -gesch4 -ge4s3eb -ge4s3ec -ge3sei -ge4s5ein -4g5e4sel. -ge4s5elt -ge3sen -ge4s5er5e -ge4s5erf -ge4s5erz -ge4sex -ge4s3ou -ge4s3ph -ge3spu -ges3sa -ges3se -ges3sp -ges3s4t -ges4tan -gest6ei -ge4s5ter -ges3th -ge4s5tur -get5ange -ge4tant -ge4thi -4getik -getin5 -ge3to -4getow -get2r -ge4t5res -get3sa -get3s4p -gets4t -get3t -4getui -2g3eul -ge3unk -ge3urt -ge3u2t -2g1e2x1 -2g1f2 -gfa5chu -g3fe -gfen2 -2gg -gga2t -g3ge -ggeb2 -gge4r5er5 -ggi3en -gg2lo -g2gn -g2gof -gg2r -ggs2 -2g1h -gh2a -gh4ae -ghafe4 -3ghale -gham3 -gh4äh -3g2het -gh1l -g3ho -3gh2r -ghs2 -ght1 -gh2ti -gh2tr -gh3w -gi1a -gib2l -gich2 -gicht3 -gi2e -gie3b -gie3d -gie3f4 -gie3g2 -gie3i -gi5enab -gie5nac -gie3n4e -gienein6 -gi3enk -gi3enr -gi3ens -gie6r5eis -gie5res -gi5ergi -gie5sp -gie3st -gif4t3r -gift3s4 -3gig -4g3i4gel -gi2gu -gik3a -gi4kan -gi4k3el -gi4me. -gi4mef -gi4mes -gi4met -4g3impf -gi3na -2g3ind -gin4di -2g3inf -gin4ga -6gingeni -gin4g3r -2g3inh -gi4nit -gin6nenb -4g5inner -gin4sa -4g3inse -4g3insf -4g3insp -gin6standh -gin4ste -2g3int -2g3inv -2giok -3gis -gi4sch5w -4g3isel -gi3sta -giste4 -3git -gi3tu -giu3s -2gj -2g1k2 -g3ka -g3kä -g3ko -gko3ef -gkraf4 -gkue3b -gkü3b -2gl. -gl2a -2g3lab -g3lach -4gladu -g5laend -glaes5c -2g3lag -2glah -4gland -glan4dr -glan4za -glan6zen -glan6z5er -2g3lap -g4las. -gla4sa -gla4s5el -4g3late -4g3lauc -g3laue -4g3lauf -4glaun -4glaus -g3laut -2g3lav -2glaw -4g3länd -g2läs -gläs3c -4gläuf -2glb -2gle. -g3leb -4glebi -g4lebo -g4lebö -gle3c -g3led -gleer4f -g2lef -2g3leg -g4leha -4glehr -4gleib -g4leic -4g3leid -4glein -g4leisi -glei6terk -glei6t5er6sc -glei4t5r -g2lek -g2lel -g2lem -g5lemmi -2glen -g2lep -2gler -glerei6g -g4les. -4glese -g2let -4gleuc -g4leur -g2lew -2glh -2gli. -3g2lia -2glib -2glic -3g2lid -3g2lie -4g3lieb -4g3lief -g3lieg -2glif -g2lik -2glil -g2lim -2glin -glings5 -g2lio -gli3on -gli3o4s -2g3lis -4glite -g4litt -g2liz -4g3lize -gl2o -g2loa -g2lob -g2loc -4g3loch -4gloes -4gloew -g2lof -4gloks -g2lom -g2lon -g2lop -4glorb -2glos -g2lot -2gloy -4glöch -2glöh -2glös -2glöw -2gls -g2luc -g3luch -g4luec -4gluef -2gluf -2glun -2g3lus -3g2lüc -2glüf -g3lüg -2glw -3g2ly -2g1m -gm4a3b2 -g3me -gm4en -gmen4tr -gmen4tu -gmi1 -gm4un -2gn. -2g3nac -g3naep -2gnag -2g3nah -gna2k -gna4l5er5 -gna4l5in -2g3nam -gn3ana -2gnaq -2gnar -2gnau -g3näp -2gnb -2gnc -2gnd -gn2e -g3neh -2gnei -2gnel -gne2n -gnen3e -4gn3ent -gner6geb -gne4ta -gne4t3r -2gneu3 -2gnf -2gng -2gnh -g2nie -g2nif -g2nin -4gninf -4gn3ing -4gnint -2g3ni2s3 -gnise4 -gniser5 -2gnk2 -2gnl -2gnm -g2no -2gnob -2gnop -gno3r -4gnori -2g3not -g3nov -2gnp -2gnq -2gnr -2gns2 -2gnt -3gnu. -3gnum. -gn4umf -4gnumg -4gn3ums -2g3nun -2gnut -2gnv -2gnw -g2ny -2gnz -go2al -goa3li -g1ob -2g3obj -g2ob4l -gob2s -go1c -2g3o2e2f -2g3oeh -4goel. -go3enz -g1of -go2fe -2gog -2g3ohr -go1i -go3k -gol2a -gol4d3a -gol6d5eng -gol4d3o -gol4d3r -gol4fa -gol4fel -gol6ferl -gol4f3l -gol4f3r -golf3s4 -g3o4liv -3gon -gon2a -go4n3an -gon5ato -2gope -go3pfl -go4pos -2gopt -go3rap -2g3ord -2g3org -g4oro -2g3ort -go2si -go3s2l -go3ste -2g3osz -go5thae -got6t5erb -got6t5erg -got6t5erl -got6t5erw -got4th -go1y -2g1ö2f -2g1öh -2g1öl -2g1p2 -gp4la -g3pu -2gq -g3qi -gr2 -gr4ab -gra4ba -gra4bi -gra4bl -gra4dei -gra6dend -4g3radl -4g3raed -graes5c -2g3rah -2g3rak -grammen6 -gram6m5end -gram6m5er -g3ramp -gra3na -g5rand. -4g3rann -2gr4ar -gra4ra -gra4r3e -gra4ru -gra4s3a -gras5st -g2rat -gr4au -4g3raub -grau5f -4g3raum -grau5sk -2gräd -gräs3c -4g3räub -2g3re. -2grea -g2reb -2g3rec -4grede -4gredu -g2ree -2g3ref -gre4fr -4grege -4gregi -4gregl -4greic -4g3reih -4greim -4grein -g2rem -g2ren -4g3renn -5grenz -gren4za -gren6z5ei -gren6zer6f -gren6zer6t -gren4z5o -g2rer -g2res -4g3rese -gres6ser6 -gres4st -2grev -2grez -2g3ric -gri2e -4g3rieg -4g3riem -g5riese -2grig -gril4l5a -4g3ring -4g3rinn -grit2 -g2ro -gro4b3a -gro4b5ei -gro4bl -gro4b3r -2groc -4g3roec -4g3roeh -groe5le -2g3roh -2g3rol -gron2 -4g3rose -gros4s5a -gros4s5c -gros6s5el -gros6senk -4grout -2g3röc -2gröh -grö3le -grue5be -4g3ruec -4gruf. -4grufe -g4ruft -2g3ruh -gr4un -grun6dere -grun4g -2g3rut -grü3be -2g3rüc -2gs1 -gsa2b -g3sack -g2sa2d -g4saeh -g2sa2k -g4sall -g4salt -g4sama -g4samt -g3sani -g4sant -g2s3a2p -g2sa2r -gs3ars -g2sas -g4sato -g3sau. -gsau4e -g4sauf -gsau4r -g4saut -g2sa2v -g2säc -g2säh -g2sän -g3sät -gsby2 -gsch4 -g5schal -g5schat -g3schä -g4sche4f -g5schei -gs4chen -g3schl -g3schm -g3schn -g3schr -g3schu -g3schü -g3schw -g2sco -g2se2b -gse2c -gse2d -g3sege -gs2eh -gs3ehr -g3seil -gse4kl -gse2l -g3sel. -gse4me -gse4n3e -g4sent -gsen5th -g2se2p -g2ser -gser3e -gser5in -gser5oe -g2ses -gse4t -g2seu3 -gs3e2v -g2se2x2 -gsgene6t -gsi2d -g3sie -g3sig -gsi2k -gsi2n -g4s3ita -g3s4ki3e -gs3li -g2so2 -gso3c -g3s2ol -g3soz -g3s4pac -gs4pate -g3spek -g3spit -g5sporn -g5s4pule -gsrue4c -gsrü4c -gs3s2 -gssau4g -gst2a -gs5tabl -g5stabs -g3stad -g5staer -g3stah -gs3tak -gs5tank -g5stark -g5start -gs3tas -gs5tat. -gs5tate -g5stati -g5statu -gs5tau. -gs5tauc -g3stäb -gs3tät -g5steig -g5stein. -g5steine -g5stell -gst5ent -gster5e -gs6t5erei -gs4terz -g3s4tew -gs5the -gs4thy -g3stic -g3stir -gs3tit -g5stock -gs4toer -g3stör -gs4tras -g5s4trat -g5streb -gst4ri -gs4t5rit -g5stroem -g5strom. -g5stroms -g5s4trop -g5ström -g3stüc -g2su2 -g3sub -gs3un -gs3ut -g5szene -2gt -gt3auf -g3te -g3ti -gti2m -gt3inh -g3to -gt2st -gt3stu -gtu3en -gtu5ers -gu3al. -gu3am -gu3ant -gu3ar. -gu3ark -gu3ars -gu2d3r -2gueb -2gued -2g1u2f -2g1uh -gu3in. -gu3ino -gu3ins -gu3is -2g3ulm -2g3umb -gum4e -2gumf -2gumg -2gumk -2guml -2gumr -2g3ums -2g3umw -2g3una -4g3unem -2g3unf -4gunge4w -4g3ungl -2g3u2ni -2g3unk -2gunr -gun4st -2gunt -2g3unz -gure2 -guren3 -2g3url -gur4t3h -gur4t3r -gurt3s -3gus1 -gu2sa -gu2sä -guschi5 -gus4s5er -gus4s3o -gus4sp -gus4st -gu4st -gust5a4b -gust5ein -gus6t5en6de -gus6terl -gus4t3h -gus3to -gus4t3r -gu2t3a2 -gutan5e -gu4ted -gut3ei -gu4t5er4h -gu2t3h -gu2t3i -gu2t3r -gut4sp -gu2tu -gutwa3 -2güb -gün2 -3gür1 -2gv -g3ve -2gw -g3wr -3gym -2gz2 -gzeu4gi -gzeugin6 -g3zh -ha2a -ha3ak -ha4b3ei -h3abla -ha4blä -h3a4blu -2habn -h3a2b3o -h3a2br -2habw -hach3a -ha4chec -ha4ch5en -hacher6be. -hacher6ben -hach5eri -ha3chi -hacks4 -ha4cku -2hada -ha4del -hade4n -haden6s5o -ha4d3in -h3adle -hado2 -h3a2dr -ha3dri -had3st -haede4 -h3aeff -h3aehr -ha3elb -ha3e4n3e -4ha3er. -h3aerg -4h3aerz -hae6s5chen -4h3aest -4h3aeug -hae5us. -haeu4s5c -hae5usp -h5affae -h3affä -haff4l -haf3r -haf4ta -haf6t5erl -haf4t3o -haf4t3r -haft4se -haft6stem -2hagi -ha2gl -h3ahl -h3ahm -2hail -2haka -ha4kad -ha4kes -ha2kr -h3akro -2hal. -hal3ab -ha3l4al -ha3lam -halan4c -hal5ang -h5alarm -ha3lat -hal4ba -hal6bein -hal4bel -hal6b5ern -hal4bin -hal4bl -hal4bo -hal4b5ra -hal4bri -hal4bro -hal4b5ru -hal4b5rü -halb5s -2hale -hale4n -ha6l5ents -2halh -2halk -h5alkal -hal4lan -hal4l5au -hal6l5ere -hal6lerf -hal6lerg -hal6l5err -haller6s -hal6l5ersc -hal6l5er6w -hal4loe -2halp -hal4sei -hal4sp -hal4sta -hal4stu -hal4tal -hal4t5an -hal4tau -hal6t5erfo -hal4th -hal4t3r -haltungscha6o -h3a4mei -h5ameri -hamot4 -h3ampf -2h3amt -h4an. -2hana -hana3e -ha4nal -ha4nan -2hanb -h4anbe -2hanc -han4d3a4 -h4ande -han4d5er -han4dob -han4dr -hand5ru -hand5rü -hand3s4 -2hane -ha4nem -han4f3i -han4f3l -han6g5end -han4gro -2hani -han6kerl -han4ko -han4kr -2h3anl -2hano -ha4nor -2hanr -4h3ansc -h5anten -h3anth -2hanz -h5anzei -h5anzug -h3apf -ha2pl -h3a2po -h3a2pr -har2a -hara3d -2h3arb -2harc -h4ard -har4da -h3area -har4fa -har4f5ei -har4fr -2harg -h3argu -ha3rh -ha3ri -h3arm. -h5armem -h5armen -h5armer -h3arms -har4ne -h3a4rom -4h3arra -2hars -har4tal -har4th -4h3arti -har4tri -har4za -har4zo -4harzt -2hasa -ha4schm -has2h3 -hasi5er -ha3spo -has4sa -hasser4 -has6s5erf -has4sp -hass3t -h1a2ß -ha2t3a -hat5ein -ha2t3h -h3atl -ha4t3ra -ha4trä -ha4t3re -ha4trou -2hats -h3attr -hat4zer -haub2 -h3audi -h3aufb -4h3aufd -h3aufk -h5aufla -hau5f6lie -hau5flu -4h3aufm -4haufn -h3aufs -4h3auft -4h3aufz -2haug2 -h5auge. -h3aukt -hau4men -hau4mer -hau4sa -hau4s3c -hau4seh -hau4sel -hau6s5en6t -hause4r -hau6seri -hau6serp -hau4so -haussen6 -haus6sen. -haus5sk -haus5tau -hau4s5ur -hau4ta -hauten4 -hau6teng -hau6terh -hau6terk -hau6tern -hau6terw -hau4tin -hau4tr -haut5s -ha4ve. -havil4 -hä2c -häde2 -h3äff -2h3ähr -2häi -hä2kl -hä2la -h3ärg -härm4s -hä3ro -2h3ärz -hä6s5chen -2h3äst -2häug -4hä3us. -häu4s3c -4häusg -4häusk -4hä3usp -2hb2 -hb4ar -hba4ras -hbar5sc -hber4e -2h1c -hchar4 -hcre2 -2hd2 -hdan2 -hdi2e -h3ds -he2ad -hebe5in -he2bl -3hebs -3hebt -he3cha -he5ch4e -h3echs -heck5sp -hed2g -he4dit -hee4la -he3e2m -hee2n -he3eng -hee4rei -hee2t -he3eti -he4fan -he4fau -he4f5erm -2heff -he4f3id -he4find -he4f5in4g -he4f3le -he4flo -hef3s2 -3heft -hef4tan -hef4tau -heft3s -he2fu -he4hef -h3ehr -hei2a -hei4da -hei6d5ert -h3eie -h3eif -h3eig -hei4kl -hei4lau -he4im -hei4mal -hei4man -hei4mar -hei4mau -hei4mei -heimer6f -hei4min -hei4m3o -heim3p -hei4mu -4hein. -hei4n3a -hein6dik -hei4n5e4b -hein5e4c -hei4neh -hei6nene -heinen6g -hei4n5er -h5eingab -h5einkä -h5einmi -hein5sk -4heio -4he3ism -heis4s5t -he3ist -h5eitei -he3i4ti -heit4s3 -h3eiw -hei4za -hei4zel -hei6zene -hei4zw -he5kar -hek2s3 -hek4t3a -he4l3an -hel5ass -he4l3au -hel3e4c -4helek -h3elem -hel5end -hel5ent -hel5ers -he4less -4heleu -4helic -he4l5ing -hel4l5au -hel3le -hel4m5an -hel4mei -3helms -he4l3oe -he4lop -he3lor -he2lö -helsau4 -2helt -helt4s -2hemb -hem4br -3hemd -he4mer -h5e4miss -2h3emp -he2na -hena4g -hen3au -he2nä -hend4r -hen5e4be -hen3ec -he4nel -he4n5end -he4n5en5e -hen5ens -hen5ent -he4nenz -he4nep -hen5erei -he4nerf -he4n5erk -he4n5ero -he4n5erö -hen5ersc -hen5erst -he4n5ert -he4ness -he4n5eta -he4n5e4ti -he4neu -hen3f2 -h3eng. -hen4gag -h5enger. -heng4le -heng4r -he4nil -he4n3im -he4n3in -henke6l5i -henk6lei -henk4lo -henk4r -hen6k5rin -henmen6s -henmi4s -hen3ne -hensch4 -hen6semb -hen5sla -h3entc -h5ente. -h3entf -4h3entg -h3entk -h3ents -4hentw -he4n3u -hen3z2 -he2o -he3on -heori4e -he3pf -2hepi -he3ra. -her3a4b -her3an -he5rane -4herap -he4r3än -her4big -herb4st -her3c -3herd. -3herds -he4r3e4b -he4r5eck -her3ef -4hereig -h5ereign -he5reigr -her6eini -he6r5eis. -her5eises -he4rel -he4r5en5e -he4r5ent -he3rer -he4rerf -he4r5erh -he6r5ersc -he4r5erw -her4fas -4herfo -h5er6folg -herg2 -6hergebn -h5erheb -h5erhit -h5erhoe -h3erhö -he4r3id -heri4e3 -4herif -he4r3il -herin4d -herin4f -he6r5in6nu -herin6st -herk2 -4herke -h5erlaub -her6lebn -her6lös. -her6löse -h3ernä -h5erneu -her5no -her3o4b -he3rod -her3of -he4r3oh -he3rok -heros3 -h5er6regu -hers4k -hert2 -her3th -her3tr -heru4t -he4r5ute -4herzae -her6zaep -her4zap -4h3erzä -her4zäp -her6z5eng -herzes4 -4herzeu -h5erzeugu -heseen4 -he3s2h -hesi4e -he3s2k -hes4ser -he3sta -he5s6temp -he5stro -he3stu -h3e4tap -heter4z -he3t2s -het3t -3hetz -he3u4fe -heu3g -he3ums -he3unf -heu3r4a -3heusc -he3xan -hex3em -2hexp -1hè -2h1f2 -hfel4la -hfen2 -hfi4le. -hfolgegene6 -2hg2 -hgeb2 -hgene6ri -hgin4s -h3glo -2h1h -hh4au -hhaustuer6 -hhaustür6 -hhoh2 -hhohl3 -2hi. -hialer4 -hi5a6lerw -hi3a2m -hi2ar -hi3a2s -h3iat -hi3ce -hich6t5er -hi4dea -hi4dio -2hido -hi3dr -hi2e -hied4s3 -3hiera -hie4rei -hier3i -hie4rin -hiesser6 -hiff4r -h3i4gel -hi2k3r -hi5la. -hi4l3a4d -hi4l3an -hil3c -hil4dau -hil4dr -hil4f3a -hil4fä -hilfe5s -hil4fl -hil4fr -hili4e -hi4lip -2hill -2hima -2hime -h3i4mit -h3imma -2himp -h3impe -h3impl -h3impo -hi3nak -hi3nal -hi3nam -hi3nap -hi3nas -hinau4 -hin4dac -hin4en3 -hing4a -hing4l -hing4r -2hi3n4i -hin3n2 -2hino -hins2 -hin4t5an -3hinw -2hi2o -hi3ob -hi3os -hip1 -hi2pa -hi4pei -hi4pen -hi4phe -hi2p3l -hi2po2 -hi2p3r -hips2 -hipt4 -hi2pu -2hire -hire3s -hir4m3a -hir4m3i -hir4ner -hirs2 -hir6scha -2his. -hi4schl -hi4schr -hi2se -h3i2so -his2p -hi3spo -hi3spr -his4st -hi5s4tie -hit2a -hi3tac -hi4t3an -hi4tel -hi4tro -hit3z4e -hi4van -hivar4t -hi2v3o -hi2vr -hi2vu -2h1k2 -hke3na -h3ku -hk4uh -2hl -h3labo -h5lache -hlach6t5r -h5laeche -h5laed -hlaf3l -hla4gä -hla4gei -hla4g3r -h3lake -hl3akt -hlammer6 -hla2n -hlan4d5a -hlan4dr -hl3ano -hl3ans -hl3arb -hl3art -h3las -hl3asp -h3late -hl3ato -h5laus. -h3laut -hl3ax -h3l4ay -h5läche -hl3är -h3läs -hlb2 -hlbe3n -hl1c -hl3d2 -hld4a -hle3e2 -h4legel -h4leic -h4leie -hlei4fa -h4leime -hl5einb -hl5eing -hle5in6ha -hl5eink -h4l5einl -h4l5einr -hl5eise -hl3ele -hl3emp -h5len. -hl5en6dun -hlenen5e -hlen5sk -hlens4t -h4l5entd -h4l5ents -h4lentw -hl4enz -h2lep -hler3a -hler5eig -h6l5er6nae -h4l5er4nä -hler6ze. -h4lesi -h4l3ess -hles4t -hle5sta -hle5sto -hle5str -h2leu -h3leuc -hlf2 -hlg2 -hlge3n -h2lie -h3lieb -h3lied -h3lieg -h2lif -h2lim -hl3imp -h4l3inf -hling4s5 -h5lings. -h4lingt -h4l5inst -hl3int -h4l3inv -h2lip -h4liss -h2lit -h3lite -hlk2 -hlo2b -hl3obe -h4loess -h2lo2f -h2loh -h2l3o2p -h4lor -hlor3a -hlo4re -hlos4s5t -h2lot -hl4samb -hl4sang -hl4sau -hlsch4 -hls2e -hl4s3el -hl4ser -hls5int -hl4sph -hl5s4tec -hl5s6tern -hls5tie -hl4sun -h3luk -h5lumpe -h3lurc -hl3urn -hlus6s5en -hluss5t -h3lüf -hlz2 -2hm -hm3abl -h4maec -h4maeh -h4mael -h4malw -h4malz -h4mand. -hmani4e -h4marc -h4maro -h4mause -h2mäc -h2mäh -h2mäl -h3me. -hmee2 -hme3eb -hmeer4s -h3mein4 -h5meise -hme5l4a -hme3le -h3men -hme5nad -hmen4sa -hmen6schl -hmen4se -hmens5em -hmen6spo -hme4rad -hme4ran -hmer5ei -hme4r5id -hmerin6t -h3mes -h3mex -h2mid -h2mie -h4milz -h4mink -h4miss -h2m3o2f -h2mol -h4morb -h4more -h4mors -h4mort -hm1p2 -hm2s3p -hm2st -hm3s4tr -hm2th -h2mud -hmuss4 -h2mut -hmut4s -2hn -hnabe4 -hna2c -hnacht6s5 -hn3amb -hn3a2n -h3nark -h3nas -hn3a4te -h3n4au. -hn3äh -h2näp -hn3är -hn3d2 -hn2e -h2nee -h4n3eff -hnei7der -hn3eig -hn3ein -h2nel -hne2n -hnen6sem -hn3epo -h5ner. -hne4r3a -h4n5er4de -h4n5er4du -hner5ei -hnergene6 -h5nerlö -h4n3ero -h4n3erö -hnet4s -h4netu -hn3ex -hn3f2 -hnflei4 -hng2 -h2nib -h2n3id -h2nie -hnik3a -hni4kan -hni4kar -hni4kl -hni4kn -h2n3im -hn3ing -hn3inn -h2nip -hni4sa -h2nit -hnit4tr -hnk2 -hn3o2b -h2nod -h2noe -h2no2r -hno3s -hns2p -hnst4 -hnsuch4 -hn4t5auf -hn4tho -h1nu -h2nuc -h2nue -h2nul -hn3ums -h4n3unf -h4nute -hnz2 -ho2ar -hobel5e -h5oberf -ho2b3l -hoch3 -ho4cha -hochen5e -hochs4 -ho6ck5ere -ho6ckerl -hoe2d -ho3edu -2hoee -hoe2s -hoes3c -h3oese -h3oest -ho2ex -ho2f3a2 -ho2fä -ho4f3ed -ho4f3ei -ho4fet -ho4feu -hof3f4a -hoffen6s6 -hof5fi -hof3fl -hof3fr -ho4f3in -ho2f3l -ho4for -ho2f3r -hogene6r -hoh4lei -h3ohm -ho3hö -h3ohr -2hoi -hok2l -hoko3 -hokos4 -h3okt -ho4lab -ho4l3ak -ho4l3an -ho4l3au -ho4lav -ho4l3ei -ho4lem -ho4l5ent -hol5gr -hol5int -hol2o -ho3lof -3holst -2holy -h3o4lym -3holz -hol6zene -hom4e -ho4me. -ho4mec -ho4med -ho4mel -ho4meo -2hon. -ho4nar -ho4nau -2hone -2hong -2honh -honi4e -2honk -2hons -ho3on -hoo2r -2ho2pe -ho4p3r -4hora -ho3rae -ho5ral. -ho5rala -ho5ralg -hor5alt -2hore -ho4rem -ho4rer -2horg -ho4rol -ho4rop -hor4ta -hor5tan -hor4ter -hor4t5rä -hor3um -hose4 -ho4s3ei -2hosm -ho2sp -ho3spr -hos4sa -host3r -h3osz -hoße2 -ho4ß5ene -2hot. -3hotl -hotli4 -hoto5p -2hot3s2 -hou1 -hou2s -2how -h1ox -2hö. -hö2c -hö3ck -h1ö2d -2höe -2höff -1höh -hö2s1 -h3öse -h3öst -2h1p2 -h3pu -2hr -hr3a2c -h3rad. -hr3adr -h5raeum -h3rago -hr4ai -hr3ala -h3ramp -h3r4ane -hran4kl -hra4no -hr3ans -hr3atl -hrau4ba -hraube4 -h3rauc -hr3auf -h3raum -hra2x -hr3ämt -h3räu -hrb2 -hr1c -h3re. -h3rea -hre4bl -h3rech -hre6ck5er6 -h3red -hr3eff -h3reg -h3reha -hr3ehr -h4rei. -hrei4ba -hrei4bl -hrei4b5r -h3reic -h4reie -h3reif -h5reihe -hr5eing -h4r5einl -h4reins -hrei5th -h3rel -hr3emb -hr3emp -hrende6s -hrenk4 -hren4zy -h3rep -h5rere4 -h4rerfa -hrer6fol -hr6erfor -hrer6gebn -hr5er4ke -h6r5er6leb -hrer5oe -hrer3ö -hrer6zae -hrer4zä -h3re2s3 -hresgene6 -hres5k -hress4 -hre2t -h4r3eta -h2reu -hr3eul -h2rev -hr3eva -h3rez -hrg2 -hrge3r -hrgu2 -hrgut3 -h3ric -h4rick -hri2e -h5riesl -hr3imk -h3ring -hr3inh -hr3ins -hr3int -h3risc -h3risi -hr3iso -hrit6tel -h5ritter -h3riu -h3robo -h3roch -h3roeh -h3roe4s3 -hro2f -hr3ofe -h3rog -h3roh -hro2l -hr3oli -hr3oly -h4romat -h4ron -hro4n5a -h3ropa -hr3o2r -hr3osm -h3rote -h3roto -hrot4t5a -h3rou -h3rö2s3 -hrp2 -hrr2 -hr4s3ac -hr4s5amp -hr4s5and -hr4s5ang -hr4s3au -hrs5eins -hr4sele -hr4sent -hrs5erf -hrs5erl -hrs5ers -hrse4t -hr4s5eta -hr4s3in -hr2sk -hr4s3of -hr4stri -hr4sum -hr4s3un -hr2sz -hrt5aen -hr4tak -hr4tall -hr4t3an -hr4t3än -hr4t3e4l -hr6t5en6de -hrten6gag -hrtengene6 -hr4t5erf -hr4t5erl -hr4t5erz -hr4t3ho -hrt5ing -hrt5ins -hrto2 -hr4top -hr4t5ram -hrt5rol -hrt6s5ein -hrt4sin -hrt4sto -hrt4stu -hr4tung -h1ru -h2rub -h4rueb -h3ruf -h2rul -h3rum. -hr3ums -hr3unf -h1rü -h2ry -hrz2 -hrzah4 -hrzahl5 -hrzeu6ger -2hs -hsa2b -h4s5achs -h4s5acht -h2s3a2d -h4s3aeb -h4s3aeh -h4s3aem -h2sak -h4s5alar -h4s3alk -h4s3all -h4s5alte -h4s3amm -h4sanb -h4sanf -hs3ang -h4sange -h4sanh -h4sanm -h4s5anna -h4sanr -h4s3ant -h4s3anz -h2sa2r -h2s3as -h4sath -h4s3ato -h4s3aud -hsau6erl -h4s3auf -h4saut -h2s3äb -h2säh -h4säug -hs2ca -hsch2 -h4schan -hsco2 -hs2cr -h2s3e2b -h2s3e2c -hse2e -hsee3i -h4s3ehr -h4s3eie -h4s5eind -h4s5einh -h4s5einl -h4seinn -h6s5einst -hsela4 -h4selad -hse4l5ag -hse4lan -hse4l5ei4 -hse4l5en -hse4ler -hse4l5in -hs5e4lit -hsels4 -hs5e4mis -h4s5endw -hsene4 -hse4n5eb -hsen5erg -h6sensem -h6s5entar -h4s3epi -h4s3erb -h4s3erf -h4s3erh -h4s5erkl -h4s3erl -h4s3erm -h4s5erna -h4s5ernä -h4s3er3o -h4s3erö -h4s5erpr -h4s3ers -hse3r4u -h4s5erwe -h4s5erwi -h4s3erz -hse2t -h4s3eth -hsex2 -h4sexe -hs4ext -hs4ha4k -hs4her -h2s3i2d -h4s3ind -h4s3int -h4s3ita -h5skala -h5skand -hs4nac -h2so2b -h4s3ofe -h4s3off -h2s3op -hs5ort. -hs6orti -hs5orts -h1sö -h1sp -hs4pach -h4sparl -h4sparz -h4spat -hs4pec -h4spel -hs4perb -h4speri -h2sph -h2spl -h4spro -hs3s2 -h4st. -h3stab -hs5tabl -h4stabs -h3stac -h3stad -hs5taeu -h3stah4 -hs5talb -hst5alt -hs5tank -h5stans -hst5anza -h3stap -h5stark -h5start -hs5tata -h5stati -h5statu -h5stau. -h4s3täu -h2ste -h3stec -h5stein -hst5einsa -h3stel -h4stele -hs3ten -hs4t5en4g -h3s4tep -hs3ter -h5s4terb -h5stere -h5s4tern -hs3tes -h5steth -hstge3 -h3stic -hs4tieg -h3stil -h3stim -hs4tin -h3s2to -h4stole -hsto4n -hs2tr -h5straen -h5straf -h5strah -h5stras -h5strat -h5strec -hs5treu. -hs5treue -h3stro -hstro6ma -h3strö -hs4tub -h3s4tue -h3stun -h2s1u2 -h3sub -h3sul -hs4ung -h3sup -hs3ut -h3s4wim -2ht -ht1a -h2ta2d -h4taer -ht5a6gent -h4taka -ht4akt. -ht4akte -h4tall -h4ta4lo -h4talp -h4talt -h2ta2m -hta2n -h4tan. -h4tanb -h4tan3e -h4tanh -h4tanna -h4tans -h4tanw -h4tapo -h2tar -hta4rie -ht5a4ris -h4tasi -h4tasy -h4tath -h4tato -h2tau -h3taub -h3taum -h2tax -ht1ä -h2tär -hte2c -hte5cha -ht5echs -ht5echt -ht3eck -h4t3ede -hte3e -ht3eff -ht3e4he -h4t3eif -h4t5eilz -h4t3eim -ht3ein -hte5in6fo -h4t3eis -h4t3e4ke -h4t5e4las -hte6l5ei. -h4t5elek -h4telem -h4t5elfe -h4t5e4lit -h4t5e4mis -h4t5e4mit -h5ten. -h4t5endo -hte6nere -hten6gag -htens4p -ht5entar -h4t5entd -ht5ente. -h4t5entf -h4t5entg -h4tentl -h4t5ents -h4tentw -hte4r3a -ht5erbe. -h6t5erben -hterer6sc -h6t5erfah -h6t5erfol -h6t5erfuel -hter6fuellt -h6t5erfül -hter6füllt -h6tergeb -hter6gri -hter6hael -h6t5er6häl -ht5erheb -hter6hiel -hter6höhu -h6t5erklae -h6t5erklä -h6terleb -h6t5erleu -h6t5erneu -h4t5ernt -h4t5er5ob -h6ter6prob -h6ter6spar -hters4t -h6ter6stat -ht5er6trä -ht5erwae -h4t5erwä -h6t5erzie -h4t3ess -hte5sta -hte5str -h4t3eta -hte4th -ht5etho -h4t3eur -h4t3eva -h4t5extr -h2t1h -h5therm -h3thr -h2t3i2d -hti6g5er6k -h2t3im -hti2n3 -h4t3ina -h4t3ind -h4t3ine -h4t3inf -h4t3ini -h4t3ins -ht3iso -h4t3isr -hti3t2 -h4t3ita -ht3l -htni2 -h4t3obs -hto2d3 -h4t3oef -ht5oele -h4t3oes -ht3o2f -h4t3oly -h4t3ont -h4t3ope -ht5opfer -h4t3opi -h4topt -h4t3ora -h4tord -h4torga -h2t3ot -h2t3ös -ht4ran -ht5rand -h4t3ras -h4t5rati -h4t5ratt -ht3rea -h5treck -ht5reic -ht5reif -h4t5reis -ht5reit -ht3rem -h4t3res -ht2ri -h4t5rieg -h5trik -ht5ring -h5trio -h5t4rit -h4t3riv -h6trocke -h4t3ros -ht3röm -ht3rus -h4t3rut -ht4s3ah -htsa4n -hts5ana -htsch4 -hts5chro -ht4scr -hts5endu -ht4s5eng -ht4sep -htse4r3 -hts5eri -hts5kr -ht5slaw -ht4soe -ht4sof -ht5s4pen -ht5spru -ht4staf -ht6s5tate -ht4stip -hts4tol -ht6streu. -ht6streue -ht6strip -ht6stuer -ht4s5tür -htu3en -ht3uh -h4t5umse -ht3und -h4tunem -h4tunf -h4t5ungl -h4t3urk -h4t3urs -htwa5re -htz2 -hu2b3a -hu4b3ei -hu4b3el -hu4b3en4 -hu4b5er4z -hu2bi -hu2b3l -hu2b3o -hu2b3r -hu2bu -hu1c -hue5ben -huef6ter -huehne4 -hue4r5ei -huer4fr -hu2f3a2 -hu2f3ä -huf5erk -hu2f3o -hu2h3a -hu4h3ei -hu2h3i -huh3l -hu2h3o2 -huh3r -h3uhr. -h3uhre -h3uhu -hu2k3a -hu2kä -hu4k3er -hu4k3in -hu2k3l -huko3s -huk3t2 -hu2l3a -hu2lä -hu4l3e4b -hu4l3ei -hu4lel -hu4lem -hu4l5eng -hu4lent -hu4l3er -huler4f -huler4w -hu4l5ess -hu4let -hu4l3in -hu2l3o -hu2l3ö -huls2 -hul5se -hul5ste -hul5ta -hu4lum -hum4a -hu5man -h3umb -2humf -h3umh -hum4pl -2h3ums -humu2 -2humz -2hun. -h3una -hunde5i -hunde5n -hunde5s4 -hund3r -hun2e -2hunf -h3ungl -hungs3 -hungsa4 -huni3 -2hunt -hup3fl -hu2p3h -hu2ra -hurg4 -h3urku -h3url -hur4ter -hur4t3h -hu4s3en -hu4so -hus4s3a -hus4s5er -hus4s3o -hus4sp -hus4sta -hus4ste -hu4t3ab -hu4tau -hut3ei -hut3ex -hu2th -hut3he -hu2t3i -hu2t3o2 -hu2t3r -hut5sc -hut4ta -hut4zeh -hut4zei -hut4z5en -hut4z5er -hü3bel -hü3ben -h3über -h2übs -hüf4t5er -hühne4 -hür4f3r -2hüri -hüs3 -2hv -2h2w2 -h3wack -hw4ar -h3weck -h3weib -h3weid -h3weih -hwei5n4e -hwein6s5a -hwen4ka -h3wick -h3wies -hwi2s -h3wisc -1hyd -hy2da -hy2de -hy3dr -1hyg -hy4lor -3hymn -h1yo -hy3os -1hyp -hy4pe. -hysteri6e -2hy2t -2hz -hzug2 -hz2w -ia1a2 -ia4ben -iab2l -i3ad. -iad2a -i3adn -ia3do -ia3ei -ia3el -i3ae2m -i3aer. -ia3erf -i3aeri -ia3err -i3aers -i3aerz -i3aet. -i3aeta -i3aetb -i5aetem -i3aetf -i3aetg -i3aeti -i3aetk -i3aetp -i3aetr -i3aet3s -i3aett -i3aetw -i3aetz -i3aev -ia3fl -i3agg -ia1h -iah4o -i1ai -ia3in -i3ak. -i3a4kei -i3akg -i3akh -i3akm -i3a2k3n -ia4kre -i3aks -i3akt -i3akv -i3akw -i1al -ial3a2 -ia4lar -ia2lä -ialb2 -ial5bl -ial3d -i3a4leb -i3alef -ia5lek -i3a4lel -i5alemp -i5alengp -i5a4l5en4s -i5a4l5ent -i5alen4z -i3a4lep -i5a4l5er4b -i5a4l5erf -i5a4l5erh -i5a4l5e4ri -i5a4l5erk -i5a4l5erl -i5a4l5erm -i5a4l5ero -i5alerö -i5a4l5erp -i5a4l5ers -ial5erwa -i5a4l5erz -i3a4l3e4t -i3alex -i2ali -i3a4lia -i3a4l3id -i3a4l3in -ialk2 -i3all -ial4ler -ia4lob -ia4l3on -ia4lop -ia4l3o4r -ialp2 -ialsch4 -ia2l3u2 -ialz2 -i3amp -iampe4 -ia3mu -i3an. -i3anab -ia4n3ae -i3a4nal -ian5alt -ia4nar -i3a4nau -i3a2nä -i3anc -ian4ce -i3and2 -i2ane -i3a4n3e4b -i3anei -i3anex -i3ank2 -i3ann -ia4nod -i3ans -ian4s3p -ian4st -i3ant -i3anz -ian4z3a4 -ia1o -ia2op -i1ar -i3ar. -ia4rab -iar5ein -i2ari -iarr2 -i3as. -i3asb -ia3sh -i2asi -ias2p -ias3pr -i3as3s -ia3sta -ia3s4tr -i3asz -i3at. -i2ate -i3atl -ia4tol -i3atom -i3ats -i1au -ia3un -i1ä -iä2m -iä2r3i -i3ä4tem -iä2ti -iä2tr -iät3s -i4b5achs -i4b3arm -i4b3art -i4barz -i4b3auf -i4b3aus -i4baut -i3bä -ib3ärm -ib4bli -ibeer4 -i4b3eig -i4b5einh -i4b3eis -ibel3i -ibend4 -i4b5ener -ibe3ra -ibe4rab -ibe4rae -ibere4 -ibe4reb -ibe4ren -i4ber4fa -ib5erfah -i6ber6geb -iber6lau -i6b5er6sche -ibe4tau -ib3ide -i4b3ins -i4b3lab -i4blad -i4bläh -ib4len -i4bleu -i4bluf -ib2o -i4boel -i2b3op -i4b3ort -i2böl -ib4rec -ib2ri -ib5roec -i4b3röc -ib4ser -ib3s4kl -i4bueb -i4b3unk -i4bunt -ibus3c -i2büb -2ic -ica2l3 -ic1c -ice1 -icein4 -i4ch. -icha2 -ich3a4c -ich3ag -ich3al -ich3an -ich3ar -ich3au -ich3ä -i3che -i4ch3e4b -ich3ei -i6chensem -i6ch5erde -icher5eig -icher6gebn -i4ch3eu -i3chi -i4chins -ich3la -ich3lä -ich3li -i5chlor -ich5lot -ich3m -ich5ma -i4ch3n -ich3oe -i3chon -ich5ort -ich3ö -i2chr -ich3ra -ich3re -ich6s5amt -ich6s5ern -ich4s5or -ich4spo -ich5stri -ich4tab -ich4tan -ich4tar -ich4tas -ich4teb -ich6t5eig -ich7ten. -ich4t5in -ich4toe -ich4t3r -i3chu2 -ich3uf -i4chur -ich3w -i1ci -i4ck5erf -i2cko -ic3k4os4 -ick3sa -ick3sk -ick3s4p -ick5sta -ick5sti -icktu3 -ic3la -ic3le -id2ac -id3aer -ida2n -id3ana -i4d3ani -i4danl -i4dante -i4danw -id3arm -id3art -id2ax -i2daz -idä1 -i2d3är -i2dee -i3deei -ide5inst -i4demul -ide5nad -idener4 -ide4n3i -ide4n3o4 -iden4zi -i4denzy -ide2o -ide5rae -ide5ran -ide4rel -ider6fül -iderin6f -iderin6nu -ide3ro -ide3rö -ide4ser -ides4p -ide5sta -ide5str -2idf -id4gea -idi3ae -idi2o -id5ion. -id5ione -i4diot -i2dol -3idol. -i3dor -id4o3s -i2d1r -id4ro -id4san -id4set -ids3pr -ids5tra -idue4 -idu3en -idwa3r -1idy -ie3a2 -ie4bab -ieb5ach -ie2bl -ie3bla -ieb4sen -ieb4s3i -ieb4so -ieb4ste -ieb4sti -ieb6stre -ie4bum -iech3o -iech3t -ie2ck -ieck4s -ie4d3an -ieden6sc -iede4r5a -iedi4e -ie2dr -ied4si -ied4spa -ie1e2 -ie3ev -i2ef -ie4f3an -ie4fau -ie4feb -iefe4m -ief5emi -ieff2 -ief5fl -ie2f3l -ie3flu -ie4fonk -ief5rea -ie4f3ro -ie4g3li -ieg2n -ie4gre -ieg4s3c -ieg4se -ieg4si -ieg4sp -ieg4s3t -ie4hap -ieh5eis -ie4h3in -ieh3la -ieh3r -i1ei -ie3im -ie3in5d -iela4r -ie4lau -ie4läs -ield2 -iel5ech -iel5e4ck -ie4l5eic -iel5eid -ie4l5eig -ie6l5eint -iel5eis. -iel5eise -ie4leng -ielen4s -ielen4z -ieler6fin -ieler6fol -ieler6ga -ieler6gä -ieler6geb -ie6l5erkl -ie6l5er6la -ieler6lebn -ieler4ö -ie6l5erru -ieler6wei -ieler6zae -ieler6zä -ieles4 -ie4less -ie4l5eta -ielgen6a -ielgene6 -iel3i4d -ie4l5inf -ie4l5ins -iel5int -ielo4b -ie4l3op -ie4lor -iel3sk -iels4p -iels4t -iel5ste -iel3sz -i5e4meti -i1en -iena4b -ie4na4g -i4enah -iende5s4 -ien3ec -ien5ekl -ie4nel -ien6erfu -iener6la -iene5sk -ien3f2 -ieng2 -ienge4z -ie4nid -ie4n3im -ie4n3in -ienk2 -i3enn -ieno2 -ien5sa -iensch4 -i5en5se -iens6er -ien3s4k -iens4p -ienst5aen -ienst5än -ienst5ein -iensten6 -iens6t5end -iens6ter -ienst5erl -ienst5erw -ienst5op -ienst5or -ienst5rän -ien6t5ero -i5enti -ien3tr -ie1o2 -ie4peg -ie3p2f -ie3ra. -ier3a4b -ier5aet -ie5raeu -ier3af -ier3al -ier3an -ier3as -i2ere -ie4r5eck -ier5eier -ier6eini -ier5eis. -ier5e4mi -ie5r4ert -i2eri -ie4r3id -ie4r3il -ie4r5ing -ierin5i -ierk4 -ier3l -iermi4s -i3ern. -iern4a -ier3ne -ier3ni -ier2o -ier3ob -ier3oe -ier3of -ie3roh -iero3n -ier3ö -ier4re. -iersau4 -ier4s5eh -ier4sko -ier5sta -ierts4 -ieru2 -ie4rund -ie4r3ut -ie4s3au -i3esd -ies5ein -iesend4 -iesen5s4 -ie3s4ka -ieso2 -ie4s3oe -ie2sö -ies2p -ies4sel -ies6s5ent -ies6s5er6l -ies4sti -iest4a -ie5stel -ie3str -ie3stu -i3esü -ie4ßel -ie4ß5ent -ie4t3ak -ie4t5ang -ie4tans -ie4t5anz -ie4t3ap -ie4tat -iet5ein -ie4t5ent -ie6t5er6ho -ieter6hö -ieterin6t -ie6t5erlo -ie4t5er4t -iet5hel -iet5her -ie4t3ho -ie4thu -i3e4thy -ie4t3ob -ietoe4s -ie4t3ö4s -iet5ram -ie4tren -ie4t3ri -iet5rud -i3ett -iet3z4a -iet3zw -ieu2e -ie3uh -i4e3um -ie3un -ieu4r3a -ieu3s -ie2w3r -i1e2x1 -iezo3 -2if -if3aen -i2f3af -if5ange -i3fari -i4f3arm -i2f3au -if3än -i2fec -if3eff -i4f5einh -i4felem -ifel4s5o -ife2n -ifen3e -ifen6te. -if3erh -ifes5tat -if4far -if2fl -iff3la -if3flu -if4f3ru -if4f3rü -iff2s -iffs5et -iff4sti -iff4stu -if3inf -i4f3ins -if3lac -if3lad -i3flae -i3flag -if3lau -i3flä -i3flec -i3f4lie -i3flue -i3flug -i3flü -if1r -i3f4rag -i3freu -i3f4rev -if3sa -if2t3a -if4t3ed -if4t3ef -if4t3ei -if4tent -if4t3ep -ift5erl -ift5erz -ifte4s -if4t5esc -if4t3ex -ift5int -if4t3op -if4t3ra -ift5rei -if4t3ri -if4t3ro -ifts4c -ifts6tem -ifts4tr -if4tum -if4tur -i2f3um -i1fy -i4ganb -i4g3ano -ig3anz -i4g3arb -ig3auf -i4g3aus -igd2 -igeb2 -i6gebrau -ig3e4ck -ige4dre -i4gefar -ige4füg -ige4hef -i4g3eig -ige5inf -ige5inh -i4g5eise -i4gel. -3i4geln -ige4mes -ige4mis -igena4b -ige4nag -ige4n5e4b -ige6nene -igener6f -ige6ners -igener6z -ige4nid -ige4pac -ige4ral -ige4rar -ige4rau -i4g5er4fa -igerin6se -iger6klä -ige3sk -i4g3ess -ige4woh -igg2 -iggas5t -igh3s -2igi -i2g3im -ig3ins -ig5laen -i4g3lau -i4g3län -ig3lec -i4gled -i6gleitu -i2gli -ig3lim -ig3lu -ig2na -i4gnae -ign3a4g -i4gnak -i4gnan -i2gnä -ign3ei -i6gnerge -i3g4neu -ig3nu -igo3p -i4grams -i4greg -i4grei -i4g3res -i4g3rin -ig3rom -igs4ae -igs4ag -ig3sar -igsau4 -ig4scha -ig4set -igs2i -ig3s4or -ig3s4pe -ig3spr -ig5stei -igs4top -ig4stö -ig4s5tür -ig2sy -ig4tin -igtu5er. -i1h -ih2a -i4h3ans -i4h3auf -ihe3e -ihe3in4 -ih3elt -ihe2n -ihenk4 -ihens4 -ihe3o -ihe3s -ihe3u -i2h3in -ih1l -ih3m -ih1n -i2h3op -ih1r -ihrau4 -ihs2 -ih3sta -ih3ste -ih3sti -ih3str -i4h3um. -ih3w -i1ia -i3i2c -i1i2d -i1ie -i1i2g -i1im -i1in -i1i2r -i1i2s -i3it. -i3ja -1i2js -i1k -ik5abla -ik5ablä -i4kada -i3kae -i4k3aeh -i4k3aer -ika4gen -ika3in -i4k3akt -ikalk4 -ikal5t -ik5alta -i4k3ang -i4kanl -i6kantei -ikanten6n -ik5anzu -ik5anzü -ika3pf -ik3arm -ika4tan -ik3att -i2k3au -i2käh -i2k3är -ikbu2 -4ike. -i2keb -ik3ebe -ike2c -i2k3e2d -i2kef -ik3eim -ikei4n3 -ike2l -4ike2n -iken3e -i4k3en4z -i2kep -i6k5er6be. -ike4ren -i6k5er6fah -iker6folgs -iker6hal -iker6ken -i6ker6leb -i4k3ero -i4keru -ik5erze -iker4zi -ike4t -i4k3eta -ik3eth -i4ketu -i2k3e2v -i3ki. -ik3imm -ik3inh -i4k3ins -ik3lab -ik3lad -i4k3lag -iklan4 -ik3lem -ik4leri -i3kles -i4klos -ik3nar -ik2o -ik3obs -i4k3oef -iko3nu -ikop2 -ik3o4ri -ik3öf -i2k3öl -i4kres -ik3rin -ik2ro -ikro3s -i2krö -ik3ru -ik1s -iks2p -iks2t -iks2z -ik4t3eb -ikt5erk -ikt5erz -iktes4 -ik4t5esk -ik4t3o4b -ik4t5res -i4kueb -i4kumf -ik3unf -i5kuns -i2kup -ik3urh -i3kus -iku2t -ik3ute -i2küb -i2ky -i1la -il3abb -il3abl -il3a4br -i4l3abt -i2l3ac -i4l3aer -ila3f -i2l3ak -il3a4ma -il3ana -i4lanb -ilan5er -i4lanf -i4lanh -i4l3anm -i4l3ano -il3ans -ilan6z5er -i4l3arb -il3art -i4l3asp -il5aufl -ilauf6ru -il5auft -il5aufw -il5aufz -il3aus -i1lä1 -i2l3är -ilch5au -ilch5ec -ilch3o -ilch3r -il4dae -ild5au. -il2dä -ild5ebe -il4d3ed -ild5ele -il4d5ene -ilden4s -il6dense -il4d5en4t -ild5erhal -il4d5ese -il4d5es4s -il4did -il4dil -il4dop -il4dor -il4drü -il5ebene -il3e2c -i2led -ile3gl -i4l3eig -il3ein -il5eise -il3ele -i4l3emb -i4l3e4mi -il3emp -i4l5entb -i4l5ente -i4l5entf -i4l5entl -i4l5ents -i4lentw -iler6be. -iler6ben -i6l5erbli -i4l5er4de -iler4ei4 -i6l5ereig -i4l5er4fo -i4l5er4fu -i4l5er4fü -i6lergeb -i4l5erhe -i4l5erkl -iler6las -i4l5er4lo -i4l5er4lö -iler6öff -iler6stat -i6l5er6wae -i4l5er4wä -iler6weit -iles2 -i4lety -i4l3eur -i4leva -il3ex -ilfe3i -ilf2s3 -ilf4sta -ili3as -i2l3i2d -ilie4n3 -ili4g5a4b -il3imp -i4l5init -i4l3inv -ili3o -il3ip. -ili3pf -il3ipp -il3ips -ilk2 -ill4ant -il4l5ein -il5lem -il3li -ills4p -3illum -il4mad -ilm5a4ge -il4mak -il4mam -ilm5ans -il4m3ap -il4meh -ilm5ein -il4m5ent -il4m5enz -il4m5erk -il4m5erl -il4mex -il4mik -il4m3in -il4mop -il4m3or -il4mou -ilms2 -iln2 -il3obe -ilob4r -il4oh -i2l3o2r -il3ox -il4san -ilsch4l -il4s5ent -il5stec -il5stemp -il5stern -il3sti -ils4to -ilsto4r -i1lu2 -i2lue -i2lum -i2l3ur -ilv2 -il4z3an -il4z3ar -il4zau -ilz5erk -ilzes4 -il4z5ess -il4zwa -im3abl -im3abr -im2ad2 -ima4get -ima3i -ima3kl -imaler4 -iman3d -i4manh -i4manw -im3apf -i4marc -im5a4ren -i4m3arm -i4m3art -ima3sz -i3mat -ima4t5in -ima4tra -ima4tur -i4maut -i2mäp -im3b -i2meg -im5eins -im5ein4w -i2mej -i2mek -i4m3ele -i4m3elf -i2m3e2p -imer4la -i4m3er4z -i4me3sh -imess4 -i4meti -i2mew -i2m3ex -imhau4 -i2m3im -i4m3inf -i4m3ins -im4mak -im4mant -im4m3au -im4mei -imme4n -im6m5enth -imm5erkr -im4m3id -im4mop -im4mor -5immuni -i2m3o2p -imo4re. -4impa -im5paa -imp4fa -imp4f5er4 -imp4f3i -impf5li -impf5lu -im4ple. -im4p5ler -imp2s -im3pse -3impu -ims2p -im2th -imt3s2 -2imu -imult4 -im2um -im3urk -i1mü -4ina. -i3nabo -ina4br -in3abu -ina2c -in5ach. -in3ack -in3aeh -in3ae4s -in3aeu -in4a3fr -ina5ge -inahe3 -4inal -i3nald -i3nale -inalen4 -inaler4 -ina6lere -ina6l5ins -in3amb -in3ana -i4nang -in3ann -ina4no -i4nanzü -4inap -in3ape -in3app -in4a4ra -i3n4are -in4arr -in4ars -in3as4b -inat3h -in3atm -in5atom -i4nau -in3aue -inauf3 -i3naz -i2n3äh -in3ä2s -inbun4 -4ind. -in4da4b -inda4c -in4d3al -in4d3an -4inde. -indei4g -ind5eis -4inden -4inder -inderei6s -inderer6t -in6der6sat -inde5spa -inde5sto -5index. -5indexe -3india -5indika -3indiv -ind2o -in4drec -in5drue -in5drü -ind4spe -ind4ue -3induk -3indus -ind2ü -2ine. -in5ebene -ine3ch -in5eck. -in5e6cker -ine3el -i4n3eff -in5e4hen -i3neim -i4n3elb -ine5nae -ine3nä -inen6gag -inen6ge. -inen3s4 -ineo2 -ine3p2 -i5ner. -i4n5erbi -i6n5er6hal -i4n5er4he -i4n5erke -iner6läu -i6ner6leb -i4n5er4lo -iner4lö -i4n5ersa -i4ner4tr -i4n5erz. -iner6ze. -iner6zen -iner6zes -iner6zeu -i4n5erzi -inesi4e -i4ne3s4k -ine5str -2in2et -ine3u4f -ine3un -ine2x -in3f2 -3infek -3infiz -3info. -4infre -in4g3af -in4g3a4g -in4gak -in4g3am -ing5ans -5ingeni -in5g4ler -in4g3n -in4gop -ing3or -in4g5rat -4ings -ing5sat -ingsauto6 -ing5s6por -3inhab -6in7haltu -4ini. -4i3nie -ini5erk -i4n3inn -ini3o2 -ini3s4e -ini3s4k -ini3sl -i5nitr -i5nitz -3injur -ink5aet -5inkarn -in4k3ät -inkels6t -ink5erd -in5kom -ink4sin -ink4ste -ink2u -in4kut -ink2ü -2inn. -4inne. -inne4n -inn5erfu -inn5erho -5innerns -inn5erwa -in4n5erz -inne4s5t -2innl -2inns -3innta -inoe4d -ino3et -in3old -i3nomi -in3opf -in3ord -i3n4ot -ino5ta -ino5te -i1nö -in3ö2d -ins5aen -in6samt. -in4s3än -insch4 -ins5ebe -in4s5eke -in4s5emb -in4s5erf -ins5erg -ins5erm -ins5ertr -3insid -in3sky -ins6port -instab4 -ins5tak -5instal -ins4tec -in6steil -in6st5einst -5instin -ins4tip -5instit -inst5rol -3insuf -3insul -in4s5umw -in4sur -4insy -int5an. -int5ans -4inte. -3integ -5interp -inter5z -int2h -inth3a -intons4 -4intö -5intrig -int3s -in1u -inue4b -i4nuh -i3nuk -3inumg -in4umt -in3unz -3inurs -inü4b -in5ver -3inves -4invo -i1ny2 -inz4el -inzel5a4 -inzel5e -inze4li -inzel6ler -inz5int -in3z2u -inzup4 -inz2w -io3a -io1c -i1od -i2oda -io2de -iodi4e -i2odu -io3e2d -ioe2f -i3oe2k -io3e2m -ioe2n3 -io3er -i3oes. -i3oest -ioe2t -iof2l -io1h -ioi2 -io3k4r -iok3s -i3ol. -i3olp -i3ols -i3om. -i3oms -i1on2 -ional5a -ion3an -i3o4n3as -i3o4n3au -i2onc -ion3d2 -ionen3 -io4nor -ions3 -ionsa4 -i2ony -io1o2 -i3opf -i3or. -ior3ak -i3oral -i3orb -i3orc -ior4e -io5re4n3 -ior6ens -i3org -io3r2h -i3orm -i3orp -i3orr -i3ors -i3ort -io1s -i3os. -io3sh -i3osm -ios2p -i2ot -i3ot. -iot4e -iote3l -io3t2o -i3ots -i3ox -i3oz. -i1ö2f -i1ö2k -i1öl -i1ön -i3ös. -i3öst -2ip -ipa2n3 -ip3ein -ipe2n -ipendi5e -i2pes -ip3fab -i3pfan -ip2hi -i1pi -ipi3el -ipie4n3 -ip2l -ip4pan -ip4pap -ip2p3f -ip2p3l -ip4pru -i1pr -ip4sei -ip2s3i -ip2sp -ips3tu -ip4s3tü -ip4ter -ipt5ers -ip4tro -i3p2ty -i1ra -i3r2ad -irae5us -ir2ak -3iraki -iral3a -iral5s4t -iral6stue -ir2an -i2rar -irat2 -i1rä -ir3äh -irä3us -irbe4l5a -irbel5i -ir4ble -ir1c -ir4ch3a -ir4ch3i -ir4ch3o -ir4en -i2rew -irgen4 -irg2s3 -irg6ste -i1ri -i4r3in4t -ir2k3l -ir4k3or -irks3c -irk2u -ir4k3ur -ir4m3ae -ir4m3a4g -ir4mak -ir4m3au -ir2m3ä -irme2 -ir4m3ed -ir4m3ei -ir4m3er -ir2m3o2 -ir4mum -ir4munt -irmwa4 -ir2na -irn3al -irn3an -irn3au -irn5erf -irn5erk -irn5ers -ir2no -i1ro -iroe4s3 -i1rö -irö2s3 -irpla4 -ir4rar -irr3au -irre4l -irre4p -3irri -irsch5ar -irsch5au -ir6schei -ir4schl -ir4schm -ir4schn -ir4sch5r -ir4sch5w -ir3s2h -ir4st3r -ir3su -ir4t3er4 -irtra4 -irt4s3e -irt4st -i1ru -ir2ue -irue3b -iru2m -iru2s -irus3e4 -iru5sta -i3r2ü -irü3b -isa2c -i4s3aen -isage4 -is3air -i2sak -i4s3alg -i3sam -i4s3amp -i4s3amt -i4sanb -i4sanf -i4s3ang -i4sanh -i4s3ann -i4sanp -i4sanr -isa2r -i4sare -is4arg -i4saro -is5assi -is3att -i2s3au -is4aue -i2s3än -i2sca -i2sce -i4schab -i4scham -ischa4o -i4schäh -ische4c -i4sch5e4h -isch5einh -isch5eint -ische4m -ische4n -i4schep -ische5re -i6scherkra -i5scheru -isches6s -i4sch5eu -i4sch5im -i6schind -i4schm -i4schna -isch5obs -i4schos -i6sch5rip -isch5ru -i6schueb -i4schum -i4schüb -i4sch5wo -i4sch5wö -isch5wun -i4schwü -is3chy -i2sco -i2scr -2ise -ise3a -i4s5eben -ise3c -ise3e4r -iseer5i -is5einsa -i4selem -isel5er -isel4l -i6s5el6ter -i4s3emb -ise4ne -ise5neb -isen5en -isen3f -isenk4 -ise6r5eic -iserin6t -is3erm -i4s5ernt -ise5r4ue5 -ise5rum -ise3r4ü3 -i4s3ess -ise5str -i4s5e4tat -is4eur -is4exi -isex5po -i2sid -isi4de -i2sim -is3imp -i4s3ind -i4s3inf -isin3g -ising4l -i4s3int -isi3st -is4kis -3islam -4isli -ismu4t -i2sob -is3oef -i2s3of -i2sol -i4somo -iso4nar -isonen4 -iso6n5end -isonet4 -iso4nor -i2sop -is5ort. -3i2sot -4i3sott -i2sou -isour4 -i2s3ov -i1sö -i3spal -is3pan -i2spe -i4s3pic -i4s3pir -is4ple -i3s4por -i3spra -i4spro -i3sprü -is3pü -iss3ac -is4sau -iss5aus -is4sed -issen6sa -issen6sp -issen6s5tu -issen6s5tü -iss5er4f -isseri6e -iss5er6ke -iss5erz -issi4l -is5s4kat -iss3l -isslich6t -iss4par -iss4por -is4stec -is6steil -iss4tu -i4st. -ist3a4c -is3tal -ist3ap -i3stat -is3täu -i2ste -ist5einh -ist5einl -is5t4e4n -isterin6t -ister6ras -iste4s -ist4hal -i3stil -i3stim -istin4f -i3stoc -i3stoe -i4stöp -is3tör -i5s4tram -istri5en -i5strik -ist5rit -istu5er. -istum4 -istungsex5po -is3tüm -isuch3 -isu6cher6 -isu2m -isum3p -i3s4up -i4s3urk -is3url -i4s3urt -i2sü -isvil4 -i2sya -isz4t3 -i4ß3erl -i4ß5erse -ißlich6t -it3ab. -it3aef -i4t3aes -itaet4s5 -ita3h -itai2 -i4t3aka -ita3kl -i6t5akte. -i6t5akten -i4takts -ital3a -italent6 -italer4 -ita6l5erh -ita6lers -ita6l5erw -ita4l5in -ita4lo -ital3t -it3a4mi -itan5at -it3ang -it3a4po -it3app -i4t3arb -it3a4re -it3art -it3asp -i4t3att -i5tauc -i4t3auf -it5aus. -i4taut -itauto6r -i2t3äf -it3änd -i2t3äs -ität2 -itäts3 -2ite -i4teben -it5echs -it3e4ck -ite3er -it3eff -it3eig -i4teige -it3ein -i4t3eis -itel5ae -itel5an -itel3ä -it5elef -itel5eh -itel5ei -i4t5elek -i4telem -i4t5e4lit -i4t3emb -ite4mis -ite2n -iteng4 -iten6gag -itens4 -i6ten6sem -i4tentl -i4t5ents -i4tentw -i6t5ereig -ite4rel -iterer4 -iteres4 -i6ter6fol -itergene6 -iterin6d -iterin6s -iterin6t -iter6klae -i6t5er6klä -i6ter6led -iter3o4 -i5terr -i4t3esk -ites4ka -ite5sta -ite4tik -i4t3eva -i3t4hag -it3hem -itho3s -i3t4hr -i2thu -i2thy -i2tia -i3tic -i2t3id -3i2tii -itik3a4 -iti4kan -iti4kl -iti4kr -it3imm -iti2n3 -i4t3ina -it3ind -it3ini -it3int -itionsex5p -i4tiso -iti4v3a -itiven4 -it3o4be -i3to3c -it3of -it3opu -itor3a -it3orc -i3tra. -it3rab -it5rade -it5raeu -it3raf -it5rand -it5rang -it3ras -it3rau -it5räts -i4t3räu -it3reb -it3rec -it5reic -it5reif -it3ren -it3res -it5rieg -it3ris -i4t3rob -it3roc -i4t3rol -it3rom -it3rö -it3ruc -i4t3run -i5trup -i4t3rut -its5alb -its3an -itsau5d -itsch5ro -it4s3e4h -it4s5eif -it4s5ein -it4s5ela -it4s5ele -it4s3es -it4set -it4staf -it4stec -its4tue -it4stur -it4t3ac -it4tän -it4t3eb -itt5eige -it4t3il -it4tl -it4t3o4b -it4t3op -itt5rad -itt5rän -itt5rei -it4tri -itt5rol -itt4s5e4h -itt6s5ein -itt4sti -itt4str -i3tud -it3uh -i4tunf -i4t3unk -i4tuns -ituran4 -itu4rer -it3urg -i4turp -itut4 -ity1 -it2za -it4z3e4c -itzen5e -itzer6fo -it6z5ergr -itz5er6ho -itzer6hö -it6z5erla -it4zoe -it4z3op -it2zö -it2zw -2i1u2 -i2u3l -ium1 -iuma2 -ium4s5en -iun2 -i3ut -i1ü -2iv -i2v3ab -i2v3ad -i2v3ae -i2v3af -i2v3a2g -i2v3ak -iva4lo -iv3ang -iva6tent -ivater4 -iva6t5erf -iva6term -iva6t5erw -i2v1ä -i2veb -i2vec -i2v3ef -i2vek -ive2l -iv3ele -iv3elt -i4vemi -i4vemu -ive4no -i2vep -iver4ha -iver4he -iver4lo -iver4lö -iver4oe -i4v3er4ö -ive3s -i4vesc -i4vese -i4vesh -i4vest -i4veti -i2veu -i2veü -i2v3e2x3 -i2v3im -i4v3ind -iv3ins -i4v3int -i2vl -i3vol -i2v3op -ivo3re -iv3ro -i2vue -i2vun -i2v3ur -i2vü -i3wo -iwur2 -i2x3a2 -ix3ele -i3xi -ixt2 -ixta2 -ix2u -i1y -2iz -iza2b -i4z3aen -i2z3ag -iz5anke -iz3anz -i2z3ap -iz3art -iz2as -i2zat -i2z3au -i2z3än -izein4t -izeit5s4 -i3ze2n -i4zentz -izen6zer -izen4zw -i4z5erle -i4z5er4sc -i4z3er4z -izi3na -iz3irr -izo2b -i2z3oe -i2zo2f -i2z3or -i2zö -i4z3una -i2zwe -i3zwec -iz3wic -i2zwo -i2zwö -iz3za -í1 -ï1 -1j -j2a -ja1a -jaal2 -jab2 -ja1c -jacht5s4 -ja3ei -ja3er -ja1f -jaf2l -jag4da -jah4r5ei -jahr4s3 -ja1i -ja3l2a -ja3le -ja3ne -jani3t4 -ja1p -ja3ru -ja3st -2jd -je2a -jean4s -jee2p -je2g -jek4ta -jek4tei -jekten4 -jek6tend -jek4ter -jek4t5in -jektor4 -jek6t5ort -jek4t3r -jek4t3u -je3na3e -je2p -jeri3 -je2s3t -je2t3a -je2t3ä -je2t3e -je2t3h -je4t3in -je4tor -je2t3r -jet3s2 -jet3t -je2t3u2 -ji2v -2j1m -joa1 -jo2b1 -joba2 -job3e -joch3a -jo3che -jo3chu -jo3ck4 -jo2d3a -jo3el -jo3gh -jo2i -jona3t -jong4 -jo2p3 -jo3ra -jor2z -jo2sc -jos3t -josta3 -jou2l -2jt -jty1 -j2u -ju2b3l -jude2 -jugen4 -jugend5 -ju2k -jun4g3a -jungens6 -jun6g5ente -jun4gl -jun4g3r -jung5s4c -jung5s4p -jung5s4t -ju3ni -jun3t -jus3t -ju3te3 -2jv -1ka -ka4ans -ka3ar -2k3abb -kab4bl -2kabd -kabel5a -kabel5i -kabel5o -4k3a4ben -2kabf -2kabg -2kabh -2kabn -2kabs -2k3abt -2kabw -2kabz -ka1c -2k3ace -4k3achs -2k3add -k2ade -kade3i -kadein4 -kade4n3 -2k3adm -ka2dr -2kadv -ka3e2d -k3ae4mi4 -4k3aend -ka3ent -k3aepf -4k3aerg -k3aerz -kae4s3c -ka3esk -4k3affi -ka3fl -ka3fr -kaft4 -k2age -4kagen -k3agg -2k3aho -kaim2 -kai3s4 -kai5se -ka3k2l -4k3akt. -4k3akti -4k3akts -4kala. -kal3ac -kal3a4d -kal3an -kala4s -ka3lat -ka4leb -ka4l3eh -kal5eis -kale4n -kalen6du -kalen6se -kal5erei -ka4l5erf -ka4l5er5i -kal5ins -kal4kan -kal4kle -kal4klo -kalk3r -kal4kri -kal4kru -4k3allt -ka4l3on -ka4lop -kalori6e -ka4l3os -4kalp. -k3altä -kal4t5ex -kal4th -kal4tre -kalts4 -ka2lu -ka4mat -kamp6f5en -kamp6f5er6f -kamp6ferp -4k3amte -ka4n3an -ka4n3a4s -ka4nau -4kanda -kandal5a -4kandä -4kandin -4k3andr -ka4nel -ka4n5ent -4kanfä -k4anfu -k4anfü -kani5er. -4kanim -kank2 -4k3a4nom -4k3a4nor -k2anp -kan3s2 -4k3ansä -4k3ansi -6k5antenn -ka3num -kanu3s4 -ka2o1 -ka4p3ho -k3appa -kap3st -4k3arbe -k4arbr -2karc -k2ard -kard4a -4k3area -ka4r5ent -k2arg -k2arh -ka3ri -kari5es -k2ark -2karm -k3arm. -k3arme -k3arms -ka3rö -k2arp -kar4pf -k2ars -k3arsc -k4arta -k3arti -4kartig -4kartik -k4arto -kart3s4 -k2aru -kar3u4f -ka3rum -ka3rü -k2arw -3karz -4karzt -ka4schm -ka5s4e3i -kasi5er. -kas3s4a -kassen5 -k5assis -kasso3 -4k3ast. -kas3ta -kas3te -4k5aster -kas5til -kas6tras -ka3sz -ka4t3a4b -kat5akt -kat5ans -k2ate -ka4tea -ka4tent -ka4tep -ka4t5erk -kat2h -ka3tha -k2ato -ka4t3ri -kat3t4 -k4atz -k2au. -k4aue -kau4eri -kau4f3a -kau4f5er -k5auffa -k5aufga -4k5aufla -k5aufli -kau4f3o -kauf6s5ag -kauf6s5en -kauf4sp -kau4fu -3k4aum -ka3umr -k4aus. -4k3ausf -4k3ausg -k3ausk -4k3auss -kaussen6 -4k3ausw -kau3t2 -4kauto -kauto4r -1kä -3käf -k1äh -2k3ä2mi2 -2k3änd -2k3äpf -2k3ärg -2k3ärz -kä2s3c -2kb2 -k3ba -k3be -kbo2n -kby2 -2k1c -k3ch2 -kcre2 -2kd2 -kdar4m -kdi2e -keb2 -ke4ben -2k1ec -2kedi -ke4dit -ke3er -2k3eff -kefi2 -kege4la -kegel5e -k3egg -ke2gl -ke4he. -ke4hen -kehl3s4 -2keho -kehr3a -keh6rert -keh6r5er6z -kehr4s5o -kehr6s5po -kehr6s5tur -2kehu -2k3eic -2k3eig -kei4l3i -keils4 -keil5ste -kei4ma -keim3p -2k3ein -keinen4 -4k3eise -keit2 -keits5c -ke4l3ab -ke4l3a4c -kel3ae -ke4laen -ke4l3am -ke4l3an -ke4l3as -ke4l3at -ke4l3au -ke2lä -kel3än -keld2 -ke4l5eim -ke4l5ein -kel5eis -4k3e4lek -ke4l3en -kelen5e -ke4l3er -kelgene6 -ke4l5inf -kel5ins -kel5int -ke4lio -ke2l3o2 -ke2lö -kel3öf -kelsch4 -kel3s4k -kel3sl -kel5s6tern -k3emb -2k3e2mi -2k3emp -ke2na -kena4b -kena4g -ken3au -ke2nä -4k3endg -kend4r -kend4s -ken5dsc -4kendst -ke4n3e4b -ke4nel -ken5end -ke4n5en5e -ken5eng -ke4n5ens -ken5ent -ke4n5enz -ke4nep -ken5erk -ken5er4n -ke4n5ero -ken5erö -ken5ers -ke4n5est -ken3e4t -ken3f2 -keng2 -4ken4gag -k5en6gel. -k5en6gels -ke4nim -ken3in -kenk2 -4kenlad -4kenläd -ken3ne -ke2no2 -ken3om -kensch4 -4k5en4sem -kens6tei -kens6temp -4k5en4te. -ken6ten. -ken6tene -ken6tenp -4k3entf -4k3entg -ken3t4h -4k3entl -4k3ents -4kentw -4kentz -ke1o2 -ke3pf -ke2pl -k3e2po -ker3ae -ker3ak -ker5all -ker5alt -ke4rap -ker3as -ke2r3ä -k5erbit -4kerde -ke4r3e4b -ker5eck -ker3ef -k5ereign -ke4rein -k5e4remi -ke4r5end -ker5ens -ke4r5ent -ke4r5erd -ke4r5ers -kere4z -ker4fas -4kerfol -ker6folg. -ker6folgen -4k5erfül -ker6gebn -kergene6 -4kergu -ker6hoehu -k5er6höhu -ke6r5in6nu -kerin6st -kerin6te -kerk2 -k5erlang -ker6langu -k5erlau -ker6lebn -ker5nab -ker4nar -kerner5 -ker6nere -ker6ners -ker6n5eur -k5er4nie -ker3o4b -ke5rode -4k5erodi -ker3oe -k4eror -2k3erö -kersau4 -ker6schü -kert2 -ker4tue -k3er4tü -ke4ruh -4k3erz. -4k5erzes -k5erzeugu -4kerzie -ker6zieh -ke4san -ke4sel -2kesh -ke3sha -ke4spa -kes4pe -4k3es4sa -kess4p -ke3sta -ke2s3u -ke4t3a4b -ke4t3an -ke4t3au -kete2 -ke4t3eb -ket3ei -ke4t3el -keten3 -ke4t3er -2ke2th -ke2to -ke4t3re -kets2 -ket4tau -ket6t5erz -ket4t3h -ket4to -2keud -2keup -2k3e2va -2k1e2x1 -2k1f2 -kfen2 -k3fi -kfi4le -kfin4s -k3fu -2kg2 -kgas5ta -kgeb2 -kgespen6 -k3gl -k3go -2k1h2 -khau6sen -k3he -ki1a -ki2ad -ki2ag -kia2k -kia2r -ki1c -2kicl -ki3dr -2ki2dy -kiefe4 -kiel3a -kie4lei -kie4l3i -kie4l3o -kierer6geb -4ki3ern -kie4sei -kie4so -kie4s3p -kiess4 -kie4ze -ki3f2l -ki3f2r -kik2 -2kila -kilan4 -2kilä -2kile -2kili -4k3illu -3kil4o3 -ki3m4o -2k3imp -ki3nad -kin3al -ki2nä -4k5index -4k3indi -4k3indo -4k3indu -2k3inf -kin4g3a -kin6g5ent -kin4g3r -2kinh -kini3k4 -kini3s -k3ink -kino3 -kinos4 -4k3inse -kin3sh -4k3insp -4k3inst -2k3int -ki1o -2kior -kio2s -kio2v -kip4pr -3kirc -kir6scha -ki3s2h -2k3i2so -2ki3s2p -kissens6 -ki3sta -ki3s4to -kit6schl -kit6sch5w -2kiv -2kiz -2kj -k3jo -2k1k2 -kkab2 -kka3la -kka3lö -kking3 -k3kn -kko3ef -kk4or -kkue3b -kku3s -kkü3b -kl2 -2kl. -2k3la. -2k3laa -2k3lac -kl4ae -4k3laed -4k3laeh -k3laeu -2k3lal -kla5me -kl4an -4kland -klan4ga -klan6ger -klan4g5r -4klat. -klaue4 -k3lauf -4k3laug -5klavie -2kläd -2k3läh -kl4är -2k3lät -2k3läu -2k3le. -kle5ar. -kle5ari -k2leb -kle4ba -kle4b3r -2kled -k2lee -4kleer -k3leg -2k3leh -4k3leib -k4leid -klei4er -kleinen6 -4k3leit -k3lek -k2lem -k3lem. -kle2o -kl4ep -4klero -kles3t -k2let -kle3us -2k3lex -4klich -k2lid -k4lien -k2lif -2k3lig -k2lim -4klin. -k4li3na -4klind -k4lino -k2lip -k2lir -k4lisc -4klist -2k3liz -k2lo. -k2lob -2kloc -4kloec -4kloef -k4loest -k4loet -2klog -2kloh -2k3lok -k2lom -k2lop -klor4 -4klose. -4klosem -4klosen -4kloser -4kloses -klos4s5t -k4lost -4klot. -4klote -4klots -2klöc -2klöf -2klöh -k4löst -k2löt -k1lu -3klu2b3 -k2lud -k2lue -4k3luec -k2lug -k2lum -klungs5 -klus5s4 -2klux -2k3lüc -2kly -2k1m -k3ma -kma4li -kmes4st -kmi2t -1kn2 -4k3nach -2k3nad -k3naep -2knah -2k3nam -k3napf -kn4ar -2knav -k3naz -2knäp -kne4bl -kne3e -knen4a -2knes -kne4ta -kne4t3r -4knetz -2kneu -2kney -k2ni -kni2e -4k3nier -knies4 -4k3nis. -4kniss -2k3niv -k2no -kno4b3l -4knorm -kno2s -2k3nov -2k3num -1ko -ko3ad -kobal4 -kobalt5 -ko4beli -kober4e -2k3obj -kob3la -kob4sk -ko3che -koch3i -ko3edu -k5oeffn -ko3eli -ko3erh -ko3erk -ko3erm -k3oert -ko3ev -ko4fen -koff6e5i -4k3offi -koh3lu -ko3hö -2k3ohr -ko1i -koka3s4 -kol4a -ko5lau -kol3c -kol2k3 -kol4l5e4b -kol4lei -kol6lerw -ko3lo -2k3o2ly -ko4m3er -2komn -ko4mp -ko4mun -ko4nau -kone2 -ko4nel -ko4nem -ko4nod -kons4 -kon5su -kon4tae -kon4tä -kon4t3h -ko2nu -koo2 -ko3on -2kop. -ko4pal -ko4p3an -2kopä -ko4pf -kopf3a4 -kop4f5en -kopf5erk -kop6f5err -kopf3l -2kopg -ko4p3he -kopi4e -2kopk -ko4pli -2kopm -3kopp -ko4p3re -2kop3s2 -ko3pte4 -2kopv -2kopz -ko3r2a -kor4an -kor4bar -kor4b5er -kor4bla -kor4b3r -2k3orc -kor4dar -kor6d5erfo -kor6der6g -kor6d5erw -kor4dr -ko4r3el -2k3org -kor4k3a -kor4ker -kor4k3r -kormo3 -kor4nac -kor4n5ae -kor4n3ä -kor4n5e4b -kor4n3o4 -ko4rob -4korpi -k3ortu -ko3r4u -k2os. -ko4scr -kos4pr -ko2st -ko5stei -kos6t5er6z -kot4a -ko4t5ent -kot5erb -ko3t4i -koti4n3 -kot3s2 -kot4tak -kot4tan -kot4th -3kow -2k1ox -1kö -kö4res -2k3ört -2k1p2 -k3pa -kpi2t3 -k3pl -2kq -k3qu -1kr2 -2k3rad -kra4da -kra4dei -4k3raed -k5raeum -2k3rah -k5raket -kr4am -kr4an -4k3rand -krang4 -5krankh -kran5s -k3rats -kr4au -k3raub -4k3rauc -4k3raum -2kräd -k3räte -k3räub -4k3räum -2k3re. -4kreak -4k3real -k4reat -k2reb -kreb4sc -kreb4s5o -2k3rec -4kred. -4krede -5kredi -4kredn -4kredu -2kref -2k3reg -4kreib -4kreic -kre3i4e4 -kreier4 -4k3reif -4k3reih -4k3reim -4krein -krei6s5ei -krei6s5in -kreis5t -k2rem -krem4l3 -k2reo -4krese -4krest -4kresu -k2reu -2k1rh -2k3ric -4k3rieb -5kriec -4k3ries -k3rind -4k5rings -4k3risi -kro3a4d -krob4 -krobe4w -5kroch -kro3e4b -4k3roeh -kro3el -4k3rohs -2k3röh -2k3ruc -krue3b -2kruf -kru4fer -krum4ma -4k3rump -2k3run -krü3b -2k3rüs -k2ry -2ks -ksa2b -ksab3a -k4s3aeh -ks3aem -k3saen -ksa2f -k2sa2k -k4s3amt -k2san -ks3ang -ks5anka -ks5ankä -k5sanke -ks3ant -k2sap -ksa2r -k2s3a2s -ksau4e -k4s3au4f3 -ksau4g -ksau4re -k4saut -k2s3a2v -k2säh -ks2än -k2sce -ks2ch2 -ksche4f -k2s3cr -k2s3e2b -k2s3e2c -k3s2ed -ks3eie -ks4eif -k4s5eind -k4s5einf -k4s3e4le -ks3elf -ks4end -k4s3eng -ks4enk -ks5ense -k4s3en4t -k4s3erf -k4s3erg -k4s3erh -k4s3erk -k4s3erl -k4s3erp -k4s3err -k4s3ers -k4s3ert -k4s3erw -k4s3erz -kse2t -ks3eta -k2s3e2v -k2sex4 -kshi2 -k2s3i2d -ksi2n -k5sinf -ks3ing -ks3int -k2s3is -kso2b -ks3oe -ks3ofe -k3son -k2s3op -kso2r -ks3org -ks3ori -k4s5ort. -k3spaz -k3s4pec -k3spek -k3s4pel -ks4pes -k4sprä -k3spri -k4spro -k3spru -k3s4pul -ks3s2 -k4st. -k3stab -k3stac -k3stad -k4stag -k5stall -ks5tanz -k3s4tar -kstat4 -ks5tat. -kstatt5 -k3stau -k3stec -k3stei -k4steil -k3stel -k3s4tep -k5stere -ks4tern -k3steu -k3s4tew -k3stic -k5s4tieg -ks4tiel -k5s4ties -k3stil -k3sto -k4stod -ks4toe -ks4tol -k4ston -ks4top -k4stot -ks2t2r -k6straen -k4strän -k4s5tres -ks5troc -kstro4m5 -k4strop -k3s4tub -k5s4tuec -k5stuer -k4s3tüt -k4s3um. -ks3umb -k4sumf -k4s3ums -k2s3un -k2s3ur -2kt -kt3abr -k2t3ad -kt3akr -k4t3alg -kt3al4l -ktal3s4 -kta2m -k4t3amo -kta2n -kt3ana -k4t3ang -kt3ano -kt3ans -kt3a4re -kt3arm -kt3ars -kt3art -kt3arw -k5tasc -kt3asp -kt3atl -k4t3auf -kt5aust -ktauto4 -k3te. -kt3eff -kt3ein -k4t3e4la -k4telem -k3te2n -kten3e -ktens4 -k4t5entf -k4tentl -k4t5ents -k4tentw -kten3z -kt3e4pi -kte5r4an -kter5ei -k4t3erh -kte4rik -kter5oe -k4t3erö -k4t3e4ti -ktgan4 -ktge3n4 -kt1h -k4thel -k2thi -k2tho -k2thy -k2t3i2d -kti4me -k4t3ind -kt3ini -k4t3ink -kt5insa -kt5inse -kti4s3e -k4t3iso -kt5i4ter -ktiven6d -k4t3oef -kt3of -kto4n3a -ktop3a -kto4rei -k4torga -ktorgene6 -kt3ott -k4t3ram -k4t3ras -k4t3rau -kt3räu -kt3rec -kt5reic -kt2ro -ktro3e -ktro3s -kt3rue -k4t3run -k2t3rü -kt1s2 -kt3s4a -kt5sae -kt3sä -kt3se -ktsen3 -kt4s5ene -kt2sk -kts5org -kttei4 -k4tunf -k4t5ungl -k4tuns -k2tup -ktu4ran -ktu4ras -ktu4rei -ktu4rer -ktur3z -ktz2 -1ku -ku4al -ku3an -ku1c -kuck2 -kue5bel -ku3en. -kuen4s -kuer4s -ku3fa -kuf3s -kuge4l5a -kugel5e -ku2gl -ku2h1 -kuhle4 -2k3uhr -ku3in -kul2a -kulan4z -ku3li -2k3ulm -2kulp -kul4t3o -kul4t3r -k3umba -2k3umd -ku3m4e -2kumg -k2umk -2kuml -kum4san -k3umsc -kum4sei -kum4si -kum4sp -k3umw -2kumz -kun5d4a -kunde5i -kundein6 -k3une -k5ungeb -k3unk -2kunm -kun4s4 -kunst3 -2kunt -2kunw -2k3up. -ku3pfl -kup3s2 -2kupu -kur5ans -kur4bl -kur5ein -2k3url -ku2r3o -kurr2 -kurs5an -kur4sau -kur4s5ei -kur4ser -kur4sin -kur6s5or6te -kur4s3p -kur4ste -kur4str -kur5tsc -kur4zel -kurzer6k -ku4sak -kusa4r -ku4schl -ku4s3el -ku4s3er3 -kus3ev -ku2sp -kus3pi -kuss4t -ku2su -2kut. -kut4a -kut3ei -kut6schw -1kü -kü3bel -2kübl -kü1c -kü2li -kür2s -2kv -k3ve -2kw -k1ya -ky3la -2kz2 -kze5le -kz4en -kzeu4g5i -k3zu -kzue4r3 -1la. -1laa -la3ar -la3at -la3ba -la4bad -3labä -2labb -lab4bri -2labd -4l3aben -la4ber4z -2labf -2labg -2labh -l3a4b3i4t -2la2bl -2labn -l3a4bo. -l3abon -l4abor -2l3abp -la3b4ra -lab4res -4labruf -2labs -3labs. -4labtei -la4bus -2labw -2labz -la3ceb -la3cel -lacher4 -la6ch5erf -lachste6 -lachter6f -lach6terp -lach4t5o -la5chun -3lack. -lacks4 -l3ada -2ladd -3ladem -la4des. -4ladj -2l3adl -l3adop -2l3a2dr -3ladu -2ladv -la2e -l4aed -2laef -3laehm -4l3aehn -3laemp -l3aemt -4l3aepf -2laeq -l3aerz -lae4sc -3laeuf -4l3aeug -lae3um -2l3aex -la4f3ae -la2f3ä -la4f3ei -la4f3er -2laff -l4afi -laf3ra -laf3s2 -2laf3t4 -la2g3a2 -lag5eis -lagen5d -lag6ers -2lagg -la4gil -la4gin -4l3a4gio -la2g3l -la2g3n -lago2 -la4gob -la4gop -lag5s6eid -lag5stan -lag5stei -lag5sto -1lah -1lai -1laj -4l3a4kad -la4k3ar -l4ake -la4kes -la4kin -3l4akka -l4ako -la4kro -l3akt. -l3akte -l3akts -2l1al -3lala. -lal3ab -3l2ali -lam2a -3l4amä -2lamb -la4mei -lam4en -lament4 -la4mer -lam4m3a -3lammf -2l3amn -2lamo -la4mor -l4ampe -4l3ampl -l3ampu -lam3s -2l3amt -lamt4s -3lamu -la4mun -la4nae -4l3anal -4l3a4na3t -la4nau -2la2nä -3land -landa4d -lan4dam -lan4dan -lan4d5au -landei6g -lan6d5en6t -lan6d5erh -lan6d5ers -lan6d5erw -lan6d5erz -lan6ding -lan6d5inn -lan4dob -lan4dra -lan4d5ru -lan4d5rü -4l3anek -lanel4 -l3anem -lan5ente -lan5erd -la4n5er4f -la6n5erst -4langa -lan4gae -lan4gan -lan4gar -lan4gat -lan4gä -4langeb -lan4gei -6l5angel. -6l5angeln -lan6gerl -lan6g5ers -lan4g3i -4langri -langs4 -lang5sa -lang5sc -lang5se -lang5si -lang5sp -lang5ste -4lanhä -4lanim -4lanke -lan4k3l -2lanl -2lann -4la4nod -la4nof -la4nor -l4anpu -l4anro -2lans -3lanse -3lansw -l4ant. -lan4tan -l5antig -4lantw -lan3um -2lanw -l4anwar -l4anwer -3lanx -lan4zel -lan6z5erf -lan6zerg -lan6z5erh -lan4zin -lan4zw -2l3apf -la2ph -2l3a2po -4lappa -lap4pl -l4ar. -la4r3ab -la4r3ac -la4r3a4g -la4r3an -la4rau -4larbei -4l3area -lar3e4b -la4r3ei -la4r3el -lar3g -lar5ini -lar5ins -l3arkt -4larm. -4larmat -lar4mau -lar4mei -l5armem -l5armer -4larms -4l3a4rom -3larp -lar6schl -lar4ter -l3arti -lar3tu -3l4aru -3l4arv -lar5za -4larzn -lar3zw -la4s3aa -la4sae -la4sam -la4san -la4sas -la2sä -lasche4 -la4s3ei -la5ser -la6s5er6ke -2lasi -la4sin -la4sis -la4sor -lass5er6kl -5lassern -5lassers -las5sto -lass4tr -3lassu -la2st -last5ae -last5an -last5ar -last3ä -last5ein -4lastiz -las3to -la4stu -3lastw -3lastz -l4at. -l4ata -lat5ang -la4tar -3l4atä -la3t4e -3late. -l3a4tel -late4n -3later -late5ra -3latè -2lath -la4ther -la3t4i -2latl -l3atla -l3atmo -la4toe -la4tol -4l3atom -la2tö -la4t3ra4 -la4tro -lat6schn -lat6schw -4latta -lat4tak -lat4t5an -lat4teb -lat6t5end -lat4tex -lat4t5in -4lattr -lat4t5ro -lat4zei -lat4zer4 -3laub. -lauben6s5 -lau4bla -lau4bra -lau4bre -3laubs -laub4se -lauch5a -l4auer -lau4f3a -lau4fel -lau4fer -lau5fer. -lauf5ere -lau4fin -lau4fo -lauf5s4t -laug4l -l4aui -2lauk -3l4aun -4laun. -l4aur -l4aus. -lau6scha -lau6schl -4lausd -4l3ausf -4l3ausg -4lausr -4l3auss -laust4 -4lausw -4l3ausz -lau4tae -4lauto -lauto6ri -lave2 -laven3 -1law -lawa2 -2lawe -1lax -la4xel -lay3s -lä1c -1läd -2läf -2lägy -3lähm -2l3ähn -3lämp -2l3ämt -lände5s -2l3äpf -2läq -2lära -lär4mar -lär4mat -lär4mei -lär4m5em -lär4mun -lär4st -2l3ärz -lä2sc -4läser -3läuf -2läug -2läx -1là -2lb -l4babw -lb3alp -lb3ana -lb3ang -l4b3ant -lb3arb -lb3a4ri -lb3ärm -lbb2 -lbby2 -lbei6ge. -l4b5eise -l4b3elf -lbengene6 -lber4ei -lbe4rem -lbe4ren -lbe4ris -lbes2 -lbe4tag -lbe4tap -l2b3i2d -lb3lag -lb3las -l4bled -lb4len -lb2lö -lb3oef -lb3oel -l2bof -l4b3ora -l2b3ov -l2böl -l4brea -lb5ries -lb4s3au -lbs4t -lbsta4 -lb4stab -lb4st5ac -lb6staende -lb4stam -lb4stan -lb4stau -lb6stände -lbst5er -lb4str -lb2u -lbuch5s -lbu2n -l4b3une -lby2t -2lc -l1ce -l4ch. -lch3al4 -lch5arm -lch3ei -lche4n -l5chen. -lch5erh -lch5ern -lch5ersa -lch5ert -lch5erz -lch3eu -l3chi -lch3la -lch3lä -lch3li -lch3n -lch3ob -lch3re -lch3sh -lch3sn -lch3s4t -l2ch3u -lch3w -l1ci -l2ck -l1cl -l1cr -2ld -l2da2b3 -l2da2d -ld3aef -ld3aer -ld3aes -ld3aet -l2daf -l2da2g -l2d3ah -l2d3ak -l2d3al -ld3ams -l2d3a2n -ld3arb -ld3arr -ld3art -l4d3ass -ldas4t -ld3asy -ld3ato -ld3att -ldau5sta -l3däm -ld3är -ld3äs -ld3ät -ld1c -lde4ben -ld5eck. -ldefen4 -l2d3e4i -ld5elef -l4d3elf -ld5e4lit -l4d3emb -ld5e4mis -l4d3emp -l4d3ems -lde4ni -l4dentl -l4d5ents -l4d3enz -lde5rae -l6d5er6fah -lder6gebn -l6d5erlas -lder3r -lde4sel -lde5skr -ldes4sa -lde5sta -lde5stel -lde5str -ld3eur -l2d3ex -ldhof5s -ldi2a -ldi3ae -l4d3ide -ldi2e -ldien4 -ld3ill -ld3i4mi -ld3inf -l4d3ink -l4d3ins -l4d3ion -ld3iso -ldo4br -l2d3oh -l4donk -ldo2p -l4d3ope -l4dopt -ldo2r -l4d3ori -ld4os -ldos3t -ld3rab -ld5raen -l4draeu -ld3rag -ld3ral -ld3ras -ld3rat -ld3rau -l4d3rän -l4drec -ld3rie -ld3rob -ld3rod -ld3roe -l4d3rol -ld3rom -l2drö -ld3run -ld3rüc -lds5amt -lds4kl -ld3sko -lds2p -ld5stel -lds4to -ld3tu -ldu2 -l4dueb -ld3ufe -ldu3i -l2d3ul -l2d3um -l2düb -ldwa3r -1le -le2ad -le4an. -le4anm -le2as -le3atl -leauto4 -le2av -leben4s -lebens5e -le5ber -2lec -lech5au -le4ch5ec -le6chens -le4chi -lech5oe -lech5o4f -leck5se -3leco -ledi4e -le4dit -le2dr -3leer -lee4ret -le3f4a -2l3eff -lef2l -lef4o -le4gabt -le4g3ae -le4gang -le4g3ar -leg5ast -le4gau -le2gä -lege3s -le4gesl -4l5egge. -4l5eggen -legi5en -le2gl -le2g3n -leg2r -legs2 -leg5sta -leg5ste -leg5str -4lehe. -3lehm -leh6mest -3lehn -3lehr -lehr3a -leh4rei -lehr3o4 -le4id -lei3dr -lei5eri -2leif -lei4fan -lei4fei -leifen6s -lei6fer6g -4l5eigen -3leih -lei4hau -leil4 -le4im -lei4mau -4leimb -4leimh -leim3p -le5impo -l4ein. -4leindr -lei5nec -lei6nerb -lei6nerk -lein5erz -6l5einfal -4leinn -4leinsa -4leinto -4leintö -4leinwe -2leir -lei6sch5a -lei6schei -lei6sch5l -lei6schn -lei6scho -lei6schw -6leisens -lei6s5ern -4leisf -4leiss -leis6s5er6 -lei5stro -4leisz -2leiß -lei4ß5er4 -lei4tae -lei4t5a4g -lei4t5ak -lei4tal -lei4tei -leit5er6kr -lei4t3o -leit5sk -leit5s4p -leits4t -2leiw -2l3ek3s -4lektr -2lekz -3lela -le4l3ab -le4l3ac -le4l3au -le4lef -lel5ein -4l3e4lek -4leleme -leler4 -lel5erd -lel5erk -lel5ers -lel5erz -le4lev -lel3ex -4l3elf. -4l3elfe -le4lim -le4l3in -le4lor -lem2a -lem5anz -le4mau -lem5ein -lem4en -lem5erf -lem5erk -3lemes -3lemet -le4m3id -le5mil -4lemmt -le4m3o4r -l3e4mot -2lemp -lemt2 -le2mu -l3emul -l4en. -lena4b -lena4d -le4nan -le4n3a4t -le2nä -lende4s -4lendl -4len4dog -l3endp -4lendun -le4n3e4b -len3ec -le4n3ed -le4nel -lenen5erg -le4n5eng -4l5energ -le4n5erk -len5erl -len5ero -len5ersc -len5ert -le4nerw -le4net -le4n5eur -4leneuv -len3f2 -4l3eng. -4len4gag -4l5enge. -len6gem. -len6ges. -4lengp -leng4r -len6k5lag -len6k5lei -len6klin -len4kob -4lenniu -le4n3on -le4n3or -le4n5ott -len6s5ein -4lensem -lens4po -6l5enteig -l5entfe -4lentla -4lentlu -4lentlü -4l3entn -6l5en6trop -4l5entrü -4lent4wa -4lent4wä -4lentwi -4lentz -lent6zug -5lentzuw -le4nz -len5z6er -len4zin -len4zun -len3zw -lenz6wei -lenz4wo -4l3enzy -le3o2f -le3o2s -le4pen -3le3p2f -2l3e2pi -4l3epoc -l4era -le3ra. -ler5abi -ler5aeu -ler3a4g -ler3al -ler3a4s -lerauto6r -ler3az -le2r3ä -4l5erbe. -4l5erben -ler6blin -4l3erbs -4l3erbt -le4r3e4b -lere3c -ler3ef -ler5eigen. -6l5ereign -le4r5ei4m -ler5eis. -le4r3el -le4remi -le4r5eng -le4r5ens -le4r5en4t -ler5er5e -le4rerf -le4r5erg -le4r5ers -le5res -le4r3e4t -7lergenau -7lergeni -l3ergi -4l5ergol -4l5er4heb -l4eri -le4r3i4d -leri4e -lerin4d -lerin4g -lerin4s -lerk2 -4l5erlas -6l5erlaub -6lerlebn -4l5er4leu -ler4mäs -ler4mäß -ler4nal -ler4nar -ler4nau -lerner4 -ler3o4b -ler3of -le3ros -4l3erot -4l5eröff -ler6regu -4l5er4ric -ler6rung -ler4set -lers4k -ler6s5tur -ler3sw -ler3t -6l5ertrag -leru2 -le4rup -4lerus -le4r3ut -l4erwec -6l5erweit -4l3erz. -4l5erzeu -4l3erzg -4l3er4zo -le4s3al -lese5in4 -lese5ras -lese5s4k -les5ess -l4e3s4h -lesi3e -le3s4ki -le3sko -les4kop -4lessi -lester6i -4l3estl -lestra4 -le3stu -2lesw -2lesy -4l3e4tap -4l3e4tat -4lethi -4lethn -lets2 -letsche6 -let4tau -let4tel -let4top -lett3r -lett3s4 -2leue -le3u2f -leu4ka -2l3eul -le3umb -le3u4m3i -le3umo -le3ums -3leunt -le3url -4l3euro -3leut -4l3e4val -4leve. -l3e4vol -2lex -3lexik -le4xis -lext2 -2l1f -lfa5chu -lf3aka -lfang3 -lfan4gr -lf2at -lf3aus -l2fec -lfe3e -lfein4f -lf5einh -l4feis -l4f5erei -lfe4rel -lferin6d -lfe5sta -lfe5str -l2f3im -l2fj -l3f2jo -lf2la -lf3län -lf2lö -lfpi2 -lf2r -lf3ram -lf3run -lf2sa -lfs3au -lf2sä -lf4spe -lf4s3ti -lf2su -2lg -l3ga -lgär4s -lge3in4 -lgene6ri -lge6ners -lgens4 -lge4rab -lge3u -lgge3 -l3g2h -lgi3en -lgin4s -l4g3lam -l3g2le -lg2lö -l3go -lg4san -lg4sau -lg2se -2l1h -lhauser6 -lhe3x -3lhi. -lhu3s2 -1li -li3abe -li3ac -li3ak -li3an4g -li3ato -liär2 -3lib2 -libi3e -li3ce -4lichet -lich3r -lich4ta -lich4to -lichts4 -licht5sp -2lick -l2id -lid4a -li4dea -4l3idee -lider4 -li4d5ers -lidi4e -l3i4dio -li3dri -3lie. -lie4ba -liebe4s5 -lie4br -li3e4f3a -5liefer -li3e4fi -li3efk -li3efl -li3efm -li3efp -lie4fr -li3em -lie4na4 -liene4b -liene4v -lie4no -lie4r5ei -li5erstu -lie4s3c -lie4s5t -li3f2l -li3f2r -3lift -lif4tu -lig5ang -liga3s -li4gatt -4l3i4gel -lige4n -li4g5er4s -li4gin -li2g3l -lign4 -lig4ne -li4g5rad -li4gre -li4gro -ligs4c -ligs4p -lig5str -li3ker -lik3l -lik4li -lik4spr -lik4tau -lik4ter -lik4th -lik4t3i -likto4 -lik4t5or -lik2u2 -li3la -li3li -2l3ill -li3m4a -lima3c -lima3e -lima3s -2limm -limma4 -3limo -2limp -lin5all -lin3a4m -lin3an -lin4d5ei -lin4del -4lindex -4l3indi -4l3indu -li4nef -li4neh -li4nep -line3s -lin4gl -2l3inh -3l4inhe -4linik -lin5iti -2linj -lin4kar -lin4k5au -link3l -lin4kla -4l5inner -li4n3ol -lins2 -4l5insel -4linspe -4l3insz -2lint -3lintu -3lintü -li3n4um -li2o -li3oe -li3ons -li4onse -lio3r -lip3he -3lipt4 -li4rak -li4ran -2lirr -lir4t3r -3lis. -li3s2a -li4sau -li4schl -li5schm -li4schn -li4sch5r -li4schu -lis2h -li3shi -2l3isl -4l3isol -4lisot -liss2 -lis3sa -li2st -lis4tra -l2it -lit2a -li4t3a4d -li4t3ak -li4tal -lit5all -li3t4e -lite3i -litein4 -lite5rar -lit2h -3lito -li4tri -li4tro -lit3s2 -li2tu -li3tus -lit4za -lit4zi -lit4zu -li4vea -live4n -2livg -livi3e -2lixi -li4z3ae -li4z3an -li2z3ä -lizei5t -li2zo -2lj -2l1k -lk2a -l4k3abl -l4k3aeh -l4k3alg -l4k3alp -lka4n5er -lka4n5in -lk3arm -l2käh -l4k3eif -lkens4 -lke3re -l3kla -l4k3lad -l3kle -lklore5 -l2knu -lko2f -lk3ofe -lkraf4 -l4k3rob -lk3roc -lks1 -lk2sa -lk3sän -lk2se -lk3s4ke -lk2su -lkue3b -lk2ü -lkü3b -2l1l -ll3abb -lla4ben -l4labk -ll3abl -l4l5abri -l4l3abt -ll3aff -ll3aft -lla3ga -l6l5a6gent -lla3gl -l4l3a4ma -ll3amb -ll3amo -ll4anb -lla6nent -lla4ner -ll3ann -lla4no -l5lans. -ll5ansä -ll5anzü -l4l5appl -l4larc -lla3rö -ll3atl -l4l3atm -ll5attr -l4l5auff -l4l5aufg -l6l5aufko -ll5aufl -ll5aufr -ll5aufsi -ll5aufst -ll3auk -llau4m -ll3aus -l4lausk -lla2x -ll3axt -llb2 -ll3c -ll3d2 -lld4e -ll5ebene -ll3ech -lle3er -lle6gest -ll3eif -l4l3eig -ll5einsa -l4l5eise -lle4la -lle4lei -lle4ler -l4l3e4mi -ll3emp -lle4na -ll5endl -ll5en6dun -lle6nens -lleng4 -llen6ge. -lle4n5in -llenk4 -llen6sem -l4l5entf -l4l5entg -l4l5entk -l4l5ents -l4lentw -llen4zy -lle4r3a -ller5eis -l6l5er6fas -ller6geb -l6l5ergen -l4lergi -llerin6f -llerin6t -ller6kla -ller6klä -ller6kun -ller6laub -ller6müd -l4l5ernt -l4l5eroe -l4l3er4ö -ll6ers. -lle4ses -lle4spo -lle4th -llet5he -llet6terf -ll3ex3e -ll3ext -llf2 -llg2 -ll3ide -ll3ido -lli4gan -lli4go -l4l3imb -ll3imp -l4l3inf -llings5 -ll3inj -l4l3inl -l4l5inst -ll3int -lli5on. -llk2 -lll4a -ll3m2 -lln2 -llnes4 -l4l3obe -ll3obs -lloe6sen -llo4ner -l4l3ope -ll3opf -l4l3orb -l4l3ord -l4l3o4ri -ll3ort -l4l3osz -l2l3ou -ll3ox -llö4sen -llrue4c -llrü4c -lls5amte -llsau4e -ll4s5en5e -ll4s3es -llsgene6ra -lls5int -ll3sla -ll3sli -ll5s6temp -ll3sti -ll4stor -ll5szen -llti4m -ll3umm -ll3urg -ll3urn -ll3urs -llus3e -llus5t4r -l2lüb -llz2 -2l1m -l2m3a2b -l4mada -l4m3aep -lm5aerz -lm5aest -lm3aka -lm3alm -lma2m -lm3ama -l4m3apf -lm3arr -l4m3art -lm5attr -lm5auss -lm5ausw -l2mäp -lm3ärz -lm3äst -lm1c -lmd2 -l2m3e2b -lmeeres6 -l2m3ef -lme2h -lm3ehe -lm5eins -l4m3emu -l4m5ends -lme4no -l6mensem -lmen4zy -l2m3e2p -lmer2 -l4m3er4b -l6m5ereig -l4m3erf -l4m3er4r -l4m3erz -l4messa -l2m3i2d -lm3iko -lm3inh -lm3ins -lm3int -l2m3of -lmo2p -lm3ope -lm3orc -lm1p2 -lm3s2k -lm3s2n -lms2t -lms5tur -lms2z -lmt2 -l2m3uf -l4m3ums -l4munt -l2m3ur -2ln -l1na -ln2ab -lna2r -l4nare -lnd2 -ln2e -l3nei -l2n3in -l1nö -l1nu -lnus2 -l1nü -l1ny -1lo -3lob. -lo3ba -4l3o4ber -lobe4s -2lobj -lo2bl -2l3obm -lo3che -lo4chel -loch3o -lochs4 -lod2g -lodi4e -loe2b -2loed -loe3du -loe2f -l3oefe -l3oehr -2loek -l3oel -2loep -lo3er. -loe4t3a -2loé -l1of -2lofe -lo4fen -2loff -lo3fj -lof2l -loga2 -lo4gar -lo4gei -lo4g3el -lo4g5ers -lo2g3l -lo4gor -lo4gre -logs2 -log3sp -loh4e -lo5her -loh4ner -lohn5erh -lohn5erw -2l3ohr -loh4rer -loi2r -2l3okk -lo2k3r -2l3okt -4l3oliv -2l3o2ly -lo4mel -lom5ing -2lomn -lo4n3al -lo4nau -lon3er -lon4gu -lon4sae -lons4p -4l3onto -lo3ob -lo2op -lo4pal -lo4per -lo3pi -lop4p3a -lop4pr -2lopt -lo4r3am -lo5rand -lo2rä -lor3do -lo3re -lorein4 -lo5r4en -2l3or3g2 -lori4di -lori6en. -lori6eng -lori6enm -2lort2 -l4orti -lo2s3a -lo4ske -2loss -los4su -lo5stei -loste4r5 -4l3osth -lo3sti -lo4stö -los3tr -lo3s4tu -l3oszi -2loß -lot5ein -lo4thr -3loti -loti4o -lotte4n -lot4tr -2lotz -lour4s -2l3ouv -l1ov -2lo2ve -l2ovi -2lox -1lö -lö2b -3löch -2löck -2löd -lö2f -2l3öfe -l3öffn -2l3öhr -2lök -2l1öl -2löp -2löss -l3östl -3lösu -2löß -lö2th -2löz -2l1p -lp2a -lpeng4 -lpete4 -lp2f -lpf4e -l5phas -l4p3hir -l4p3hoe -l4p3hor -l2p3hö -l2p3hu -lpi2e -lpi2p -l4ple. -l3p2n -lp3sta -lp3sti -lptu5r4 -l3pu -2lq -2l1r2 -l3ra -l3re -lre3st -lroe4s3 -lro2h -lrö2s3 -l3ru -lrue3b -lruecker6 -l3rü -lrü3b -lrücker6 -2ls -lsa4b3a -l2s3a2d -l4s5aeus -l4saka -ls3alg -ls3alt -l4s3amp -l4s3ana -l4sanb -l4s3ane -l4sanf -ls3ang -l5sani -l4s3ank -l4sanm -ls3ann -l4sanr -l4sant -l4s3anz -ls4apa -l4sa4ri -ls4ark -ls5assi -l4s3au. -l4s3auf -lsau6ger -lsau4m -lsau6rer -l4s3aus -l4saut -ls4äug -l4s3äus -lsby2 -ls2ca -l6sch5eid -lsch5eint -lsch4le -lsch4lo -l4schmü -l6schore -lschs4 -l4sch5wo -l4sch5wö -l2sco -l2s3e2b -l4s3eie -l4s5eind -ls5einf -l4s5eint -l4s3e4li -l4s3emb -l4s5en4ge -l4s5enzy -l4s3epi -l4s3er3e -l4s3erf -l4s3erg -l4s3erh -l4s3erk -l4s3erl -l4s3ern -ls3err -l4s3ers -lse3r4u -l4s3erw -l4s3erz -lse2t -l4s3eta -l4s3eth -lsex4t4 -l2s3id -l4s3imp -l4s3ind -l5skand -l5s4kele -ls2ky -l3s4law -l3s4nac -l4s3oef -l4s3off -l2s3op -l4s5ort. -l4s5orts -l1sö -l2söf -l3s2öl -l1sp -lspani5 -ls4pare -ls4park -l4sparl -l4späs -ls2pe -l4spiz -l4spoe -ls4pon -ls4por -l2spö -ls5prae -l4s3prä -ls4pul -l4spun -ls3s2 -l3st2a -lstab4 -ls5tabl -l6s5taeti -l4stag -lstahl5 -l4s3tak -l4stalb -l4s5tanz -l4stas -l4s5tat. -l4s5tate -ls5tauc -l4s5täti -l3stea -l5steck -l3steg -l3stei -l4steil -l3stel -l6ster. -l5s6ternb -l5s6terns -ls3tes -l3steu -l3stic -ls4tie -l3stil -l3stim -l3s4tip -l3sto -l4stoch -ls4toe -ls4toi -ls4tol -ls4topf -l3s2tr -l4s5trac -ls5trad -ls5trit -lstro6ma -l3s2tu -l4sumf -l4sumk -l4suml -l4s3ums -ls3umv -ls3unk -2lt -lt3abr -lt5ach. -l3t4a3d -lt3aeh -l4t5aerz -lta3f2 -ltag2 -lt3agr -lt3aka -lt3all -lt3alt -l4t3alu -ltampe4 -lt4anda -ltaner4 -lta6nerf -ltan6gab -ltan6ges -ltan4sa -ltan4sä -lt3a4po -lt3app -l6tarbei -ltar6beitn -ltare4a -lta5rhe -l4t5arma -ltar6me. -ltar6men. -ltar6meni -lt5art. -l5tarta -l5tartei -ltar6tik -l5tartis -l5tartr -l5tartu -lt3asp -lt3atl -l4t3ato -lt5aufb -lt5aufk -lt5auge -l4taut -lt3äh -l4t3ärz -ltbau5c -lt3e4ck -lte3e -l3tehu -lt5eink -lte5iso -l4telem -ltem4pu -l4temul -ltende6s -lten6gag -lten6gel -lten6spo -l4tentl -lten6t5ri -l4t5ents -l4tentw -lte4rat -lter6be. -lter6bel -l5terg -lter6kun -lter6loe -lt5er4lö -lt5er4nä -lter5oe -l4t3esk -lte5sta -lte5stra -lte5stre -lte5stro -lt5etho -lte4tik -lt3eur -l3thas -lt3heb -lt5heri -l2t3hi -l2t3ho -lt2hu -lt3hun -l2t3hy -lt3ide -lti6g5er6k -l4t3ind -lt3inf -l4t3ins -lt3i4ra -lt3i4ri -l4t3iso -l5tist -lti5va -l4t3o4be -l4t3obo -lt3oef -lt3oel -lt3oes -lt3o2f -l3top -l4topt -l4tord -l4t3org -l4t3o4ri -lto2w -l2töl -lt3ös -ltra3l -lt5räuc -lt3reb -lt3rec -lt5reic -lt5reif -l4trig -l4t3ris -l4t3roh -lt3ruc -lt3rum -ltse4l -lt4s5ele -lt5skal -lt4s5ort -lt3s4ph -lt4staf -lt4stie -lts4tre -lt4s5tüt -lt5szen -lt3t -ltu4el -ltu3en -lt3uh -l2tum -lt3ums -ltu4rab -ltu4r5an4 -ltu4ras -ltu4rei -ltu6r5ent -ltu4r5er -ltu4r3i -ltwa3r -ltz2a -1lu. -lu3an -2lub1 -lu2ba2 -lu2bo -lub3s2 -luch4s5a -luchs5t -luch4t5o -luch4t5r -lucht6s5o -lu4cku -lu4de. -lu2dr -2lueb -3luebd -luecke4 -luecker5 -2lueh -lu3ent -lu3enz -lue6scha -lue6schw -1luf -2l3u2fe -2luff -lu3f2r -luf2t -luf4t3a4 -luft3e -luft3r -lu4g3af -lu4gas -lu5ge. -lu4g3e4b -lu4gei -lugen5e -lug5erl -lug5erp -lu2g3i2 -lu2g3l -lu2go -lu2g3r -lug5spo -lug5sta -lug5ste -lug5str -lu2gu -2l1uh -l2u3hu -lu3id. -lu3is. -luka2 -3luke -l3ult -lumbi5e -2l3umd -lume2 -lumen3 -2lumf -2lumg -2l3umh -l3umj -2lumk -2luml -2lumm -lum4p3h -2lumr -l3ums -2l3umw -2lumz -1lun -2l3una -lu3nap -2l3unb -lund3r -2l3unf -l4ung. -5lunge. -4l3ungl -lungsgene6 -2l3u2ni -4lunkr -2l3unl -2lunr -2l3uns -2lunt -2lunv -2lunw -3lupe -lupf3 -lu4r3an -lu4r3at -lu4rau -lurch3 -lu4r3ei -2lurg -lu4ris -l3urk -2l3url -2lurn -luro2 -lu4rof -lu4ror -2lurs -2l3urt -lur3z2 -lu4ser -lu3si -lu3spr -lus4s3a -lus4s3c -lus6sein -lus4s5el -lus4s5er4 -lus4si -lus4s3o -lus4s3p -lus4st -luss5ti -lus4sur -lu2st -lust5ae -lust5ak -lust5ei -lus6t5erf -lus6t5erk -lus6terl -lus4t3o4 -lus4t3u -lu2s3u -lu4t3ae -lu4tal -lu4t3an -lu4t3au -lu2tä -lu4t3eb -lu4t3e4g -lu4t3ei -lu4t3el -lu4t5en4g -lu4t5ent -luter4 -lu4t5erb -lu4t5ere -lu4t5erg -lu6t5ersa -lu4t5erz -lut3ex -lu4thy -lut5ins -2luto -lu4t3ob -lu4toe -lu4t3o4f -lu4top -lu4t3or -lu2t3r -lut3s4a -lut5schl -lut3s4k -lu4t3ur -lut3z -2lübe -2lübu -lücke4 -lücker5 -lügel5e -2lüh -lü4h3e4l -lü4hem -lüh3l -lü2s -lü4scha -2lv -l2veo -lv2l -2lw -2lx -1ly -ly1a2 -ly3c -ly3en -lye2r -2l1yo -ly3on. -ly3onn -lyra3 -lyri3e -ly5sen -ly3sp -ly3th -ly4t3re -2lz -l4z3aer -l2z3ag -l2zak -lz3al -lza2m -l4z3ant -l4z3app -lza2r -lz3aro -l2za2s -l2zat -l4z3aus -l2z3är -l4z3eim -l4z3eis -lze2l -l4z3ele -l4z5enth -lzen6tro -l4zents -l4zentw -l4z3enz -lze2p -l4z3er4h -l4z3er4m -lze3ro -lz5ersa -lz5er4tr -l4z3er4z -lze2t -lz3eta -lzeugin6 -l2z3id -lz3iko -lzi2m -lz3imi -l4z3ind -lz3inf -lz3ing -l4z5inst -lz3int -lz3l -lzo2b -lz3oef -l2zo2f -l2z3or -l2z3oz -lz5rei -lzt2 -lz3tha -l4z3u4fe -lzug4s -lzvol4 -l4zwar -l2zwo -l2zwö -l2zwu -1ma -ma3ala -maa2n -ma3ari -ma3atl -2m3abb -2mabd -2mabf -2mabg -2mabh -2mabk -m3abla -4m3a4blä -2m3abm -2mabn -ma3bo -mab4ra -ma4bri -2mabs -2m3abt -2mabw -2mabz -ma4c3ar -ma5chan -ma3chi -ma3cho -m5achse -mach4ta -mach6terf -mach6t5erg -mach6t5erh -mach6terr -mach6t5ers -mach6t5ert -mach6t5erw -mach4t5r -ma3chu -ma2ci -2mack -mack4s -2m3act -mada4m -2m3add -ma4del -ma4dem -ma3d2j -2m3adm -2m3ad2r -mad2s -2madv -4m3aend -4m3aepf -ma3epo -2maeq -maerer4 -ma3erh -ma3erw -maer6z5en6 -maes3t -ma5estr -ma3eta -mae5us. -maeu4s5c -ma1f -2m3aff -maf2r -2m3aft -ma4ge. -ma4geb -ma4geg -ma4gek -mage4n3 -ma4gep -ma4ges. -ma4gev -ma4gew -2m3agg -magi5er. -magi5ers -m3a4git -ma3g4n -ma3gri -mah4ler -ma3ho -ma3hü -mai4ler -mai4n3e -mai4s3e -ma4kad -ma4ke. -ma4kes -ma3kla -ma3kli -mak2r -2m3akt -ma4kus -malad4a -ma5lade -mal5aer -ma3lag -mal3ak -mal3an -ma3las -ma4lär -malb2 -6mal6dehy -malen4s -ma4lent -mali5er. -ma4l5ins -ma4linv -malk2 -4mallt -ma4l3on -ma4lop -ma4l3o4r -mal5ska -malster6 -mal5s6terb -mal5s6tern -malu2 -ma4l3ut -mal4zäp -2m3amb -ma4mid -ma4moe -2m3a2mö -2m3amp -2m3amt -m3anae -5manag -4m3a4nal -man3an -ma4nar -4m3a4na3t -ma4nau -2m3anä -4manbie -man4ce. -man4cek -mand4 -man3di -man5erf -ma4n5er4n -ma4net -man3g4a -4mangab -4m5angeb -m3angs -4manhä -manin4t -5manip -4manke -4manla -m4anle -4ma4nod -man4o5m -4manpf -m4anpl -m5ansatz -4m3ansä -m5anschl -4m3ansi -4m3ansp -4m5ansti -man4tag -man4tan -man3um -manu3s -4manwen -4m3anza -manze4 -m3anzu -m3anzü -ma3o2b -ma2or -ma3pfl -ma4phr -4m3appa -4m3appl -ma3pr -4marag -mar4an -2m3arb -4m3area -mar3g4 -m3argu -ma3ri -3mark -mar4klo -mar4kr -m3armb -4m5arme. -mar4o -maro5d -ma3rö -2marr -mar6schei -mar6scher -mar6sch5l -mar6schm -mar6schn -mar6scho -mar6schr -mar6sch5w -4m3arse -mar4sp -mar4su -4mart. -4m3arti -ma3ru -ma3rü -masch4 -ma3sco -maser4n5 -ma2s2p -ma3spa -ma5spen -ma3spr -4m3as4so -mast5an -ma5stel -masten4 -mas6t5ent -mas4ti -mas4to -ma3s4tr -ma4t3ae -ma4t3an -ma4t3ar -ma4tat -ma2t3ä -mat3ei -m3a4tel -mate4n -ma4t5erd -mat5er5in -ma4t5erz -ma4tho -mati6kan -4m3atmo -ma4toa -ma5toma -ma4t5opf -ma4t5ort -mat3s4p -mats4t -mat3th -matt4ha -matt4hä5 -mat4the -mat5url -2matz -2mauc -2m3au2f3 -2m3aug -mau4les -mau4l3o -mau4lö -ma3um -m4aun -ma3una -ma3ung -ma3unt -m4aus. -mau4sa -4mausd -maus3p -maus3t -2m3auß -mau4ta -mau6t5erh -mauto4r -ma2xa -m2ay -ma3yo -1mä -mäh3r -2m3änd -m1äp -2mäq -märer4 -mär4kl -mär2z -märzen4 -mä3us. -mäu4s3c -2mb2 -mbe4er -mben5er -mbe4ren -mber5er5 -mbi4e3i -mbi3o4f -mble3s -mbo4ler -mb4re -mb3s -mby2t -2mc -m3ch2 -m1cr -2md -m2dab -md3aer -m2d3an -md3arb -md3art -m2d3a2s -md3ato -md3är -mde2a -m2d3ei -m4d3e4mi -mde2n -m4d3ent -mder2 -m4d3erf -m4d3erk -m4d3erl -m4d3err -m4d3ert -m4d3erw -mdi2a -mdi2e -mdien4 -m2dob -md3ras -m3dru -md2s3a -md2sä -mds2p -md3ums -1me -mea2n -me3at -meb2 -me4ben -me1c -4m3echo -3med -medie4 -me4dito -3mee -mee5ing -mee5ins -me5e4mis -mee2n -me3ene -meep2 -mee4rei -meer5eic -meer3u -2m3eff -3meh -meh4l5er -mehls4 -mehr3a4 -mehrer4 -meh6rert -2meic -2m3eif -2m3eig -mein4da -meinde5s -mei5nec -mei6nenz -mei6ner6k -mei6nerl -mein6hal -mein6sel -meins4p -m2eis -4m3eis. -m3eisb -me3iso -5meist -meist5a -me4l3au -3mel3d4 -melde5i -me3lec -me4l5ein -4m3e4lek -4m3elem -me4l3en -me4l3er -melet4 -4m3elf. -mel3id -4m3e4lim -meli4n -mel4k5ei -3melo -me4l3ob -me4lor -mel3p -mel4s5a4c -mel4san -mel4sph -mel4s5ta -6m5el6tern -mel4zei -mel6z5ent -mel4zi -4m5e4miss -2m3emp -m2en. -me4na4b -men4ade -men4ag -me4nan -men3au -me2nä -mende4c -4m3endl -mend4u -men3e4b -men3ed -men5end -menen5e -men5ent -men5erb -men5er5o -men5erö -men5ers -mene4s -me4n3e4v -men3f -4men4gag -me4nid -me4nil -me4n3im -menk2 -men4s5au -men4se. -men4seb -men6semb -mens5ert -men4ses -men4sex -mensex5p -men4s5i4d -mens6temp -mens5trun -men4sum -men6t5anz -mentar6ten -men6t5eig -men6t5enden -men6t5ers -men6t5erw -men4tet -men4thi -men4thu -men6t5ins -men6tres -men6t5rol -men3um -menz4w -me1o2 -2meou -me1p2 -me3ra -mera3s -merauto6 -mer3b -4m5erde. -4m5er4den -mer4dun -me4r3e4b -mer5eck -mere4f -me4r5eis -me4r5end -me4r5ens -me4r5ent -me4r5erd -me4r5erf -merer6kl -4m5erfol -merg4 -4m5ergän -4m5er4gus -meri4e -merin4d -4m5erklä -4m3erlö -merns4 -mer3of -mersau4 -mer4siv -mer3sm -mer3sn -5mer5te -merti4 -4merzä -mer4zer -4m5erzeu -mer4zwe -m4es. -mes2a -mes3ae -me2sä -mes3e4l -mes5er5o -mes3or -mes4pe -mes3pr -mes4s5ac -mes4sal -mes4s5an -mes4say -mes6sersa -mes4s5or -mes4ste -mes4sti -mes4stu -mest2 -me3sto -3me3ta -meta5st -mete4r3 -me3t2h -2metö -met5res -me4trig -metro5s -mett5en6de -met4t3i -meu1 -me3um -meute5r6e -2m3e2x3e -2mexp -1mé -2m1f2 -mfahr6ta -mfen2 -mfi4le. -2mg2 -mgeb2 -mglim4 -mgus2 -2m1h2 -mh4au -mhaut5o -1mi -mi1a -mia2b -mi3ak -mia2m -2m3i2at -mibi3e -mic3e -mi3ch -mi2ci -mi3cr -m2id. -mi4dea -mi4di. -mi3d2r -2mi2dy -mie3dr -mi4eno -mierer4 -mie4tau -mie4t3i -mie4t5rä -mie4tru -mi1f -mi4k3ar -mi4k3el -miken4 -mi4k5ens -mik6erl -mi4k5er4z -mi4k3in -mi4k3ob -mi4k3of -mi4kon -mi4kre -mi4kum -mi3l4a -mil4i -4m3illu -mi3l4u -2m3imm -2m3imp -mi3na -mi4na4b -min5eck -mi4nef -mi4n5e4r5i -4minfo -m2ing -min4gan -min4gl -ming3s4 -4m3init -min4kr -mi4n3of -mi3nop -mino3s -6minterp -mi3nu -min4z3o -mi2o -mi3o2b -mioni3 -mi3os -mi1p -mi3ra -2mirg -mi3sa -mis4au4 -mi6schef -mi6scherb -mi6sch5er6z -mi4schi -mise3r -mi3s2n -mi4som -mi3s4pa -mi3s4ph -mi3spo -mis3sa -mis4sac -mis4ser -mis4s5in -mis4st -mis3te -mit3ak -mi4tar -mit3as -mit3au -mite2 -mit3ed -mit3em -mit3er -mit3es -mi2t3h -mi3tia -mit5iss -mito2 -mit3op -mit3or -mi2t3r -mit3s4 -mit5sa -mi2tu4 -mit3ur -2mitz -mit3za -mi2zw -2mj -mjäge4 -2m1k2 -2m1l -ml1c -m3le -mlein4g -mlerin6t -ml2v -2m1m -mm3aal -mma4bar -m4m3aeh -m4m3aeu -mm3a4kr -mm3all -m4manb -m4manf -m4m3ank -m4m3ans -mmanu3 -m4m3anz -mma2p -mm3apo -m5mar. -mm3art -mma3sp -m2m3äh -m2m3äu -mmd2 -m2m3e2b -mm5e4dit -m2m3ef -m4m3eid -mm3ein -mm5eise -mmela4 -mme4l5ad -mme4l5in -mmen4er -mme4no -mmens6pa -mmen6stru -mmen5ta -mmen6te. -mmen6ten -m4m5entg -m4mentl -m4m5ents -m4mentw -mmenzu6c -mme4r3a4 -mme4rec -mmer6fol -mmerk4 -mmer6sio -mme2s -mmes3a -mme3sc -mmes3e -mmes6sens -mmes3t -m2m3eu -m2mex -mmi3e4l -mmi3k2 -m4m3inb -m4m3inf -m4m3inh -mm3inn -m4m3ins -m4m3int -mmisch4 -mmi3s4t -mmi3tu -mmi3tw -mmmi2 -mm3ope -mm3org -m2m3ou -mm1p2 -mms2k -mms4p -mms4tr -mm3te -mm2th -mm5ther -mmuel4 -m2mum -m3mun -mmut3s4 -mmül2 -mmüll3 -2m1n2 -mne4bl -m4nesi4 -mnes3t -mni3er -1mo -m4obb -2m3o2be -2mobj -mo3che -mode5st -mode5sz -mo2dr -moeb2 -2m3oe2f -moe2h -mo3ehe -2moek -mo3ela -mo3e2m -mo3ero -mo4fen -2m3off -mo4g3al -moh2a -mok3al -mo2k2l -mok3la -mo3kle -3molc -molch5a -mol3d4 -mo3lo -2molt -mom4e -mo4n3ae -mon3al -mo4n3an -mon5ato -mo2nä -mon4das -mon4d5el -mon6d5erk -mon4din -mon4dre -mo4n3er -moni4en -moni5er. -mon4s3t -mon3s4u -mont4a -2mony -mo2o -mo4per -2m3opf -2mopt -mo3rae -mo3ral -mo3rat -mor4d3a -mor6d5ere -mor6d5erl -mor6d5erm -mor4dr -mo4rei -mor5enge -morgens6 -mor5int -moro2 -mo4rob -mo4rop -mor3t4 -mosa6ische -mo4ska -mo3s2p -mo2st -mo3s4ti -mo2s3u -mo3t4h -mo2u -mous2 -2m1ox -moy1 -1mö -möb2 -mö2c -2m1ö2f -2mök -2m1öl -2mp -mpa3ne -mp2ar -mp3arb -mpat4 -mpe4la -mpe4ler -mpe4lin -mpe4ne -m4p5ener -mpens4 -mpf3au -mpf3ef -mp4f3el -mp4f5enz -mp6f5er6fo -mp6f5erfu -mp4f5erz -mpf5lae -mpf3lä -mpf3lo -mpf5lüc -mpf3ra -mp3haf -mph3au -mphe2 -m4phoe -mph5org -m2phö -m3pi -mpi2a -mpin4 -mp2l -mp3le. -m4p5lem. -m4p3len -m4p3les -m3po -mpo3l4i -mporta4 -mpor6tag -mpor6ter6 -mpp2 -m1pr -mp3sh -mp3sta -mp3s4tr -2mq -2m1r2 -m3ra -mrich7te -m3ru -2m1s -m2sak -ms3all -m4s3alt -ms3amt -ms5andr -m4s3ang -m4sanm -m4sanr -m4s3ant -m4s3anz -m2s3a2s -m4s3auf -msau4g -msau4n -ms3b -m4schan -msch4l -m3schu -m2sco -m2s3ef -ms5einsa -m3s4ek -mse5l4a -mse4le -msel5ei -mse2n -msende4 -m4s3ene -m4sentl -ms3erf -ms3erk -ms3ert -m5serv -mse2t -ms3eti -mse2x2 -mshi2 -m4s3imp -msi2n -ms3ini -ms3int -m2skl -m2s3o2d -mso2r -ms3orc -m4s3ori -m2s3ou -m4sped -m2spl -ms4pon -m4spot -m4spro -ms2pu -ms3s2 -mss4a -m3stät -ms5täti -m3stei -m4steil -m4stem -ms2ti -m4stit -ms2to -m5s4tras -mstro4 -ms2tu -m2sty -ms3ums -m2s3ur -m3sy -m2szi -2mt -mta2m -mt4asc -mt3änd -mt3c -mte2a -m2t3e2b -m2t3e2d -m2t3ef -mt3ein -m4t3eis -m4t3eiw -mt3elt -mte2m -m4t3emi -m4ten4ga -m4t5engl -mte4no -m4t5en4se -m4t5entf -m4t5entg -m4tentl -m4t5en4tr -m4t5ents -m4tentw -mte4ri -mter4n -m4t3eta -m2t3eu -m2t3e2v -mt1h -m2t3i2d -m2t3im -mt3inb -mt3ind -mt3inf -mt3inh -mt3ins -m2t3i2r -mti6schw -mt3isr -mt3ita -mtmen4s -m2tob -mt3oes -mt3ord -mt3org -m2töl -mt3ös -m4trah -mt5rang -mt5rauc -mt3rec -m4t3res -m4t3ris -mt3rob -mt3rot -mt2sa -mts2c -mt3se -mt4s3e4l -mt4sen -mt5s4kel -mts5kr -mt5spru -mts4tro -mtt2 -mtu3en -mt3um -mtz2 -1mu -mu1a -2muc -mu4ckel -mucker4 -mu6ck5erk -mu6ck5ers -mu6ck5erz -muck5sp -muck5st -mu4cku -2m3ueb -2muec -mue4her -muehl5a -mu3el. -muell5a -mu3els -muen4s -mu3ez -mu2f -2mug -2m1uh -mu3la -mul4lau -mul4l3e -2muls -mult2 -2m3umf -2muml -2mumr -mum4s3p -m3umst -2mumt -mundan4 -mun6dant -mun4dar -mun6d5erf -mund3r -muner4 -mu4nerk -4m5ungeb -4muniv -m2unm -2munw -2munz -mur3au -3mure -2m3url -murn2 -mu3r2u -mu4r3u4f -m2us -mu2s3a -mus5erf -mu4sin -mu2s3o -mus4s5e4h -muss4p -mu2st -4must. -mus3ta -mu2su -muße3 -mu4t3au -mu4t3er -2m3u2to -mut4str -2müb -1müd -1müh -mü4her -mühl3a -1mül -1mün -1müt -2mv -2mw2 -mwel2 -mwelt3 -mwelter5 -mwetterer5 -1my -my1a -my3la -my3on. -my3s2p -my4s3t -2mz2 -1na. -1naa -n1ab -3naba -na4b3ar -3nabä -4nabb -4nabd -3nabec -na4bei -nabens4 -nab5erk -nab5er4r -2nabf -nab4fr -2nabg -n3abh -n2abi -na4bi4t -na2bl -na4b3or -na4bos -3nabö -na4bra -na4bre -na4bri -nab4rue -n4ab4rü -4nabs -4nabt -na4bus -2nabz -3nac. -3nachb -na4che -nachen4 -na5chen. -nach5ent5 -nach5erb -nach5ers -3nachf -3nachh -nach3i -3nachl -3nachm -3nachr -nach3s -5nachsc -n5achse -5nach5s4p -3nacht -n4acht. -nachter6f -nach6terk -nach6t5ersc -nach4to -nach6traum -nach6t5ren -3nachw -4nadap -n3add -nade4l -nadel5a -nadel5e -na4der -n5ader. -nadet4 -n2adi -nadi5ers. -n3adl -4n3adm -4n3a2dr -n3ads -3n2aec -3naee -3naehe -3naehm -4n3aehn -3naeht -3naei -2n3ae2m -2n3aen -n2aep -2naeq -3naer. -na3erg -nae5us. -3naex -2n1af -3na3fl -na3fra -1nag -nage3i -nagein5 -nagel5e -nage6lei -na4gem -na4gent -2n3agg -na4gio -na4git -na3g2r -1n2ah -na4h5erk -3nahm -n3ahn -nah3oe -na2hö -4n3ahr. -na2hu -1n2ai -na3i2d -nai2e -na3im -2n3air -nai4re -n1ak -na4kad -3nakä -2na2kn -3nako -na4kro -4nakt -na4kus -n2al. -nala2 -nal3ab -nal3ad -nal3ae -nal3ak -nal3am -nal5ang -nal5are -nal3as -nal3at -na2lä -4nalbb -4n3albk -nalb4r -nal3da -na4leb -na4l3ed -na4l3ei -na4lel -na4l5end -nalen6se -na4lent -nal5ents -na4l3ep -na4l5erb -na6l5erei -na4l5erf -na4l5er4g -na4l5er4h -na4l5erm -na4l5er4s -na4l5erw -na4l5er4z -nales4 -nal3ex3 -nalf2 -nal3g -n2ali -na4l3i4d -nalk2 -nal6l5en6g -nal4lor -4nallt -n3almt -na4lob -na4loe -nal5oef -4nalog -na4lop -na4l3or -nal3ö -n4als. -nal5s4ka -nal5skr -n3alt. -n3altd -nal5to -n4alty -na2lu -nal3uk -2naly -n3a4mal -n3a4mat -3namä -3name. -na4mei -3n4amen -namen6s5p -4n3a4mer -n2ami -n3a4mid -na4min -2n3amm -2na3m2n -3namo -n3amp -namt4s -3namy -4nana -na4nae -na4nal -na4na3t -n3a2nä -2nanb -n3and2 -n4and. -nanden4 -n4ands -na4nem -nan3er -nan3eu -4nanf -2n3ang2 -2nanh -2nani -4n3ank -4nanl -4n3anm -3nann -4n3anna -2nanr -4n3ans2 -nanta4 -nan4tag -4nantr -3nanu -2nanw -nanzen4 -nan6zene -nan6zeng -nan6z5er6f -nan6zer6g -nan6z5er6m -nan6z5ers -nan4zin -nan4zw -1na1o -1naö -na4per -3napf. -na3pfa -4napfel -3napfs -3napl -na4pos -na2pr -n1a2q -n1ar -3n4ar. -3nara -na4rab -na4r3ak -na4ran -3narä -n4arbo -2narc -3n2ard -nar5ein -nare3s -n2arh -3n2ari -n2ark -n4arle -4narm. -4narme -na4rom -na3rö -n2arp -nar4rh -n4ars. -narse3 -nar4s5ta -nar4stä -2nart -n4arta -n2aru -na3rum -n2arv -n4arze -3n2as. -3nasa -na4san -na4sar -nas3au -3nasä -na4schm -na4schw -3nase -3nasf -n2asi -na2sp -nas4ph -4n3assi -na3sta -naster4 -nas6t5erf -na2su -na2sy -nasy5la -1nat -natal5s4 -na4tat -na4t3au -nat3ei -na2th -n3atla -2natm -n3atmu -n2ato -4natom -nats5ac -nat6scheng -nats5en -4n3atta -n5atten -n1au -n2aue -nau3eb -3nauem -3nauen -3naues -3naui -3n2aul -na3um. -4nausb -4nausd -4nauss -naussen6 -n4aut. -n4aute -1nav -2nava -nave2 -navel3 -navi5er. -navi5ers -1naw -3nax. -na2xi -3naxp -n3axt -3nazu -1nä -2näb -3n2äc -2näd -2n1äf -3näg -n4ähe -nä2hi -3n4ähm -2n3ähn -n2ähr -näh3ri -nä2hu -3näi -2n1ä2m -2n1än -n2äp -4näpfel -2näq -närm4s -n4ärr -när4s3t -n2äsc -3näss -2näst -2näu -3nä3um -nä3us. -2nb2 -n3ba -nbahner6sa -nbais4 -nbau3f -nbau3s -n3bä -n3be -nbe3n4a -nbe3ra -nber4e -nber4ga -nber6g5ei -nber4gr -nbes2 -nbi2e -n3bl -nboge4 -nbogen5 -nb4or -nbruchs5tr -n3bs -n3bu -nb4un -nby2t -2n1c -n2cab -n2cei -n2cem -n3ce4n -n2cev -ncha3l -nchar6me -n3che -nchial5 -nchi5en -n3chl -n3cu -2nd -n4dab. -n2dac -n2dad -nda3ef -nd3aet -nd2ag -n2dak -nd3akt -n4d3all -nd5ange -n4danl -nd3ans -n4danw -n4d3arb -ndar4da -ndar4t5r -n4daut -n2dax -nd3äng -n2d3ät -nd1c -nd5dre -nde4al. -nde3e -n4d3ei. -nd3eil -nd5ein. -nd5einh -n4d5einn -n4deis -nde4lat -nde5lec -nde4len -nde4ler -ndel5l -ndel6s5am -ndel6s5en -ndel6s5or -ndel5ster -nde4m5ac -nde4mau -n6d5en6gel. -nden6geln -n6dengli -nde4n5in -ndenk4r -nden5s4k -ndens6tem -n4dentl -n4d5ents -nde3o2 -nd5epoc -nde6r5eis -nde6r5eiz -nde6reng -nde6rent -nder6läs -nde4ron -n4d5eros -nder6tragen -nde4sam -nde4san -nde4sel -nde4ser -nde4set -nde6stät -nde5strae -nde5stran -nde5stri -ndi5en. -ndi4ene -ndi4ens -ndi5ensy -nd3imm -nd5inan -nd3iso -nd5kuc -ndni2 -ndo4n5au -ndoo2 -n4dopt -n4d3orb -n4d3o4ri -n4d3out -n4d3rat -n4dreih -nd3rob -n4d3rod -n4d3roe -n4drose -nd5rost -nd3rot -n2d3rö -n5druc -n4drui -nd3run -nds5amt -nds5chin -nd4s5ene -nds5erb -nd4skl -nd4spae -nd4spä -nd4szi -ndwa5re -nd5wil -1ne -nea2b -nea2n -ne2ap -ne3ar. -nebel5e -nebe6lei -nebels4 -2n3ebn -neb2r -2nec -3neca -3nece -ne3chr -n3echs -ne3ck4a -3necl -3neco -nedi4e -ne4dit -ne4e3i4g -neel4 -neen2 -nee3ra -ne3erk -nee3ro -ne3er4z -nee3t -ne3f4a -nef2l -neflei4 -2n3egg -neg2l -n3e2go -neg2r -4n3e4he. -4neheg -nehen4 -4n3e4hep -nehe4re -3nehm -neh4min -ne3ho -2n3ehr -neh4r5er -2n1ei -3neia -n3eic -nei6derf -neid5erfu -3neigt -5neigu -ne3imp -neins4 -ne5in6sel -n4einsu -nei6senho -neit4h -n4ek. -2nekd -2n3ek3s -nek3t4 -2nekz -n3e4las -4n3e4lek -4n3elem -3neli -ne3lid -ne4lim -ne4lit -3nelk -n2ell -nell5an -nel4l5au -nel4l5ei -nel6l5erf -nel6l5erk -nel4l5in -3n2eln -3nels -3nelt -n2em. -4n3e4mai -2n3emb -nem4e -n2emi -4ne4mig -n4emo -4n3emp -2n3ems -4n3e4mul -n4en. -n2ena -ne5nack -ne4nan -ne2nä -n2enb -n2enc -4n3endb -4n3endd -4n3endf -n3endg -4n3endh -nen4dig -4n3endk -n3endl -4n3endp -4n3endt -4n3endw -n2ene -ne4n3e4b -nen3ec -ne4n3ed -ne4n3el -nen5end -ne4n5en5e -nen5ens -nen5ent -nen5erb -ne4n5erk -ne4n5ers -ne4neru -ne4nerw -ne4n3eu -n2en3f2 -4n3engb -4n5enge. -nen6gen. -nen6gend -nen6gens -4n3engs -4n3engt -4n5engun -n2enh -ne2ni -nen3in -n2enj -n2enk2 -n2enm -nen4nar -nen5nec -ne2n3o2 -n2enr -n2ens -6nensemb -nen3sk -nen3sl -nen3sn -nens4p -nens6tern -4n3entb -4n3entd -4n3entf -4n3entg -4n3entl -4n3entn -4n3entp -nen6t5ric -4n3ents -4n3entt -4nentw -4nentz -ne4n3u -n2env -n5envir -n2enw -n2enz -4n5enzia -nenz4w -n3enzy -neob2 -ne4pen -ne3pfl -ne2pi -2nepo -n3e4pos -nep3t2 -n4er. -ne3ra. -ner3a4b -ner3af -ner3a4g -ner3ak -ne3ral -neral5a -ner3a4m -ner3an -ne4r3ap -ner3ar -nera4s -n4erat -4n5erbe. -4n3erbg -n5erbie -ner4big -4n3erbs -n3erbt -4nerdb -ner4dig -n2ere -ne4r3e4b -ne4r5eck -ner3ef -ne4r5eic -6n5ereign -ne4rein -ne4rek -ner5ema -ne4r5ere -ne4rerf -ne4r5erh -ner5ers -4n5erfol -n4erfüh -ner3g4 -n6erges -n5erheb -5n4erhin -n5erhöh -n2eri -neri4e -ner5inner -nerin6st -nerin4t -n2erj -nerk2 -4n5erklä -6n5erlaub -4n3erlö -ner6löses -4n5ermit -4n3ermü -n2ern -4n3ernä -4n5erneu -4n3erni -4n3ernt -4n3erod -6nerosio -ner4pre -nersau4 -ner4sk -4n5ersts -ne4rup -n2erv -nerve4 -4nerwae -4nerwä -n4erwo -4nerze. -4n5erzes -4nerzeu -n4erzim -n2es. -ne4sal -nesch4 -4n3e4sel -ne4s3ev -nesi5er. -ne4s3of -ne4spa -nes4pe -nes6sige -nes6sigs -ne3sta -nesti4e -nest5rae -nest5ran -nest5rau -nest5rä -nest5ri -4nesyn -ne4tab -ne4tad -n5e4tage -ne4t3an -4n3e4tap -4n3e4tat -ne4t3ei -ne4t3e4l -nete4n -net5erh -net5ers -ne2th -ne4t3in -ne4t3ob -net3sp -n4ett -net3tr -4netui -ne4tus -3netz -net4z3i -ne2u -neub2 -neu3c -ne3ueb -neu4ere -neuer4f -neuer4k -neuer4ö -neuer4r -neuer4s -neuer4w -n4euf -neu3g2 -4n3eule -ne3umf -ne3ums -2n3eup -neur2 -neur3a -4n3eury -neu5sc -neu3sp -neu3t4 -4neuzt -ne4van -ne4vid -ne4volu -n2ew -new2s3 -2n1ex -nex3em -1né -2n1f -nfa5chu -nfah4l -nf5akti -nfall6s5o -nfalt4 -n4fanb -nfangs5 -n4faut -n3fä -n3fe. -n4f3eim -nfen2 -n3fen. -n4f3end -n3fer -nf5erze -n2fex -n3fi -nfi4le. -nfin4s -n2fj -nf2l -nf5laen -nf3län -n3f2o -nfo3st -nf2r -nf3s2 -nf4tan -nf2t3r -nft4sen -nft4s3p -nft4st -nf1u -n2fun -2ng -n2g3a2c -ng3adr -n4g3ael -ng5aerm -nga3h -ng3aka -n4g3alm -ng3a4me -ng3amp -nga2n -ng5anda -ng5angl -ng5ansc -n4g3ant -n4g3anz -n5garn -ngas5ta -ng3ato -ng5attr -ng3auf -n2g3a2v -n2gäh -n2g3äl -ng3ärm -ng3d2 -n4g3eif -n4g3ein -n4g5eise -n4gelem -ngel6s5tu -nge4nat -ngen5ec -n6generg -ngen6erw -n6gensem -ngen5sk -n4gentl -nge4ral -ng5ereig -nge6rend -nge6rers -n4g5erho -ngerin6st -n6g5erwei -nger6zae -nger4zä -n3ges -n4g3ess -ng5e4tat -ngg2 -ngg3s -ng3hu -n2g3i2d -n3glä -ng4lea -ng4led -ng4le3e -ng3ler -ng4lev -n4glig -n4glog -ng5lot. -n4gluk -ng3m -n2gn2 -ng3nar -n3gnä -ng3ne -ng3ni -ng2ob -n2gof -ng3o4pe -ng3opf -ng3p -ng5raen -ng5raeu -n4g3rai -ng5ram. -ng5rand -n4g3rän -n4g3räu -ngres6s5o -n5grif -ng3ris -ng3rob -ng3roc -ng5rost -ng2s -ngs3e4h -ngse4m -ng4sf -ngs5tec -ngs5türm -ng3sy -2n1h2 -nhab4 -nhabe6r5e -n5halb -nh4au -n3he -nhe4re -nhe3x -nhor4s -1n2i -ni3a2b -ni3akl -ni3akr -ni5aleng -ni5alim -ni2ar -ni3as -ni3ato -nibb2 -ni3be -nib2l -nib2r -nibu2 -ni3cho -ni3chr -nich3s -nich4ta -nich7te. -nichter5 -nicht5sp -ni3cr -ni3da -n4i3de. -4nidea -4nidee -ni5den. -4nideo -nidi4e -n3i4dio -ni3d2r -2n3i2dy -nie3b -3nied -ni2e3i -ni3el. -ni3elh -ni3ell -ni3elr -ni3elw -ni3elz -nie2n -nien3e4 -niens4 -nien6z5er -niere4 -nie4r5ei -nie6rerg -nie5sti -nie4t3o -niets4 -ni3fl -ni3f2r -niga2 -ni4gab -ni4g3am -ni4g3an -4n3i4gel -nig5erz -4niget -ni4g3li -nig2n -ni4gre -nig4sal -nig4san -nig4spr -4ni4k3el -ni4k5er4f -2nikh -ni4k5ing -ni4klo -ni4k3of -ni4kor -niko3s -ni4k3ra -ni4k3rä -nikt2 -nil3l -nille3 -ni3l2o -3nimmt -2n3imp -ni3mu -ni3nac -ni3nae -ni3nap -ni3nä -2n3ind -2n3inf -3n4ing. -3nings -2n3inh -4n3i4n3it -2n3inj -2n3ink2 -4n3inno -ni4nor -2n3ins -n4ins. -2n3int -ni3n2u -2n3inv -ni3o2k -ni3ora -ni3os -ni4ron -2n3irr -ni4sai -ni4scha -ni5s4che -ni2s3e -ni3se. -nise4l -ni3seu -nisi2 -nis3id -ni4s3ik -nis3in -ni4s3oe -4n3isol -ni4som -nis3or -4nisot -ni2sö -3niss2 -nis3sk -nis3sz -nis5tha -ni2s3u -3nit. -nita4le -nit4an -4nitia -ni4top -3nitr -3nit3s2 -nit4t3a -nit6teig -nit6tele -nit6ter6g -nit6t5er6k -nit4t5ra -nitt5ri -ni3v -2nj -n3jo -2n1k -n4k3a4bl -n4k3abo -nk3a4br -n4k3adr -n4k3aeh -nk3a4ge -n4k3alg -nk3alp -nk3ama -nk3ang -n4kanh -n4kanl -n4kanna -n4kanr -n4k3ans -nk3apf -nk5art. -n4karth -n4k3atm -n4k3ato -n2käh -nke2c -n4k3ei. -nke4lei -n4kelem -nken5sk -nke6r5eis -nker6fah -nk5ergu -n4k5er4zi -nke2t -nk3eti -n4ketu -nki2d -n4k3ide -nkier4s -nkin4g3 -nk3inh -n4k3ins -n4k5lade -n4k3laf -n4k5lamp -n4klen -n4kleu -nk4link -nk4neb -n4knit -nk5ober -n4koel -n4kopt -nk5o4rie -nko2w -n2köl -nkraf4 -n4krel -n4kren -n4krez -nk3rob -n4kroh -n4k3rol -n5kron -n4k3ros -n4k3rot -nk3rüc -nk4s3ac -nk4s5alt -nk4s3el -nk2so -nk5spat -nk3s4pe -nks4por -nk3spr -nk3sta -nk3s4ti -nk3str -nk3stu -nk3s2z -nk4t3ak -nk4t3an -nk4t3au -nk4t5eig -nk4t5ent -nkte5ra -nk4t5erf -nk4t5erg -nk4t5ern -nk4t5ers -nk4t5ert -nkte5sk -nk4t3in -nk4t5ort -nk4t5rae -nk4trä -nkt5rol -n4kumf -n4k3ums -nku2n -nku3na -nk3uni -nk3urh -n4k3urk -nk3urt -nku2t -nk3ute -2n1l2 -n3la -nlage5i -nlage4n5 -nlan4dr -nlan4gl -nl4au -n3le -nle4gas -n3li -nli4ne. -nli4nem -nlite3 -n3lo -nlus4t5e -2n1m2 -n3ma -nma6l5erl -nm4en -nmen5sche -nmi6schw -nmi2t -n3mo -n3mu -2n1n -nnae4s -nna5esc -nna5le -n4n3alg -n4n3all -n4n3amb -n2n3an -nn4an. -n4n3app -nn3a4sc -nna3s4t -nn3atm -nn3ato -n2nau -nn3d2 -nndar4 -nn3eff -n3n4eh -nn4ei. -nne3lu -nnend4 -nneng4 -nne3pf -nne4r3a -nner6fol -n4n5er4fü -n6ner6geb -nner6hoe -n4n5er4hö -nner6laub -nner6mit -nner3o -nner3ö -nner6war -nner4zi -nne4s3e -nnesi5e -n3n2ex -nng2 -nngas3 -n2n3i2d -nni4sa -nnk2 -nno5be -nn3obl -n2n3of -n3nom -n2n3op -nno2r -n4n3ori -n3nos -nno4t3h -nn4s3am -nn4s3as -nn4sau -nns4po -nn4stec -nn4stoc -nn4s5tol -nnt4a -nn2th -n2n3uf -n4n3unf -nn3ur -nnvoll6 -n2nya -1no -3no. -no3at -2nob. -2n3obd -n4o5bel -nobe4la -nobe6lin -2n3obh -2nobj -no4bla -2n3obm -2n3obs -2nobt -n4oby -no1c -no2de -no3der -n3ods -2n3oe2f -2n3oek -n4oet -no2fe -2n3off -nogene6r -no3hö -n3ohr -n2ok -no4kel -2n3okk -n2ol -no4lam -nol3c -no4leu -no4liv -no4lym -4no4mal -n4omas -no3m4au -no4mel -n4o3mi -non2a -4nonke -2n3ont -2nony -no2o -no3oh -no3os -2n3o2pe -2nopf -nopf3a -no4pie -3nopp -2n3ops -2nopt -3no3p4te -n4or. -nor2a -no4rad -no3ral -4noran -no5rar -n3orbi -2norc -nor4d3a -nor4del -nor6d5en6d -nor6deng -n5ordens -nor6d5er6w -nor4des -nord3o -nor4d3r -nords4 -nor4du -nor2e -no3res -2n3org -n5o4rien -2nork -3n4o4rm -nor4m3o -2n3orn -n4oro -n4orö -2n3ort -no3ru -n2os -3n4os. -nose5l4 -no3sh -no3s2k -noste4 -nos4tei -noster5e -4n3ostl -nost5ra -4n3ostv -n2ot -no4tam -no4t3an -no4tap -no3tar -no4tau -not3ei -no4t3em -note4n -no4ter -not5erb -noter6b5r -no4tex -no3the -no4t3ho -no4t3in -not5ope -not5opf -notor4 -no4t5org -no2tr -3not3s2 -no4t3um -3notv -2n3out -no3va -2n1o2x -n2oz -2nöd -2n1ö2f -2n1ök -2n1öl -2n1p2 -npa4ge -npar4ka -npa4s -npe3l -npi2p -n3pn -n3pr -npro3 -n3ps -n3pu -2nq -2n1r2 -n3ra -nra4dei -nra4du -n3rä -n3re -nrebe4z -nre3sz -n3ri -nroe4s3 -nro2h -nrö2s3 -n3ru -nrue3b -nruecker6 -nr4un -n3rü -nrü3b -nrücker6 -2n1s -ns4aa -n3sabo -ns4ack -n2s3a2d -ns5aemt -n3saeu -n4s3a4gi -ns4ai -n2sak -nsa4kr -n4s3alg -n4s3all -n4s3alm -ns4alp -ns5alta -n4s5alte -n4sanb -n4s3ane -ns3ang -n4sanga -n4sange -n4sanh -ns5anka -n4sanm -n4s3ann -n4sanp -n4sanr -n4s3ant -n4s3anz -n4sapo -nsa4re -ns4ark -ns4atm -nsau4e -n4s3auf -nsau4n -nsau4r -n4saut -n4säugl -n5sche. -n6schefi -nsch5erf -n5sches -nsch5eul -n4schl. -nsch6mau -ns5chor. -nsch4ue -nsch5werd -n4scor -ns2cr -ns5eben -nse4ein -nseha4 -nse4hab -nse6hent -nseher4 -nseh5ere -nseh5erz -nse4hin -nse4hue -nsei4ne -n4s5einf -ns5einh -n4s5einl -n4seinn -ns5einsa -ns5einst -n4s5eint -n5seit -ns5e4kel -nse4la -n4selem -nse4len -nse4ler -nse4lim -nse4l5in -nse4lit -n3s4ell -n4sem. -n4s3epo -n3s4eq -n4s5erar -nse5ras -ns5erbi -nser5eig -n4s5erfa -n4s5erfo -ns5ergi -ns5ergu -ns5ergü -n4s3erh -n3seri -n4s5erke -n4s5erkl -n4s5erle -n4s5erne -nser3o -n4s5eroe -n4s3erö -ns5ersa -nse3r4u -n4s3erw -n4s3erz -n4s3eth -n4sety -ns3eur -nsex2 -n4se4xi -nsex4t -n3s4ha4k -ns4har -nshi2 -ns4hir -ns4ic -n4s3ima -n4s3imp -n4s3ind -n4s3ini -nsinn4s5 -n4s3int -n4siso -nsi4t3e -nsi4tr -n2sk -n5skala -n5s4kali -n3s4kel -ns3kr -n5skrip -n3s4ky. -ns4kys -n3slip -n5smara -nsmaragdei6 -n2so2b -n2s3o2d -ns2om -n2sop -nsor3a -n4s5ort. -n4s5orts -ns4pac -ns4pari -n4spark -ns6part. -n6sparti -ns4pas -ns4päs -ns2pe -n3spek -n4speri -n2sph -ns4pie -n2spl -n4s5poe -n6sporti -n5sprac -n4sprei -n4s5prie -nsrue4 -nsrü2 -ns3s2 -nst5abr -n6s5taeti -ns6tagent -ns5talb -n4stale -nsta4n -n4st5ane -n4s5tat. -n6s5taten -n4stats -n4staus -n4staut -n5stäb -n4s5täti -nst5eifer -n6steil. -n6steile -nst5einhe -nst5einsa -nstein6ste -ns4temm -ns4tent -ns4tep -nst5erbie -nster5ei -nst5ergeb -n6sterin -nst5erke -ns6terne -ns6terns -nster5o -ns4teu -n5steue -ns4tew -n3s4tic -nst3id -ns4tie -n4stilg -ns4tin -ns2to -n4stole -nst5ord -n4store -nst5org -ns2tr -n4s5trac -n4strik -ns5tros -ns2tu -ns5tung -n2sty -nsu4m3o4 -ns5umsa -n4s3urt -n2s3ut -2nt -n4t5a4ben -nta5chl -nt4ack -nt3aem -nt5aend -n4t5aerz -nta3f -nta4gn -ntak4t5a -ntal3a -ntalent6 -ntal5ins -ntalk4 -n4t5allt -nt5alter -nta3ne -n5t4anfr -n4tanna -n4tantr -nt6anwer -ntar5ak -ntar3c -ntar6chi -nt5art. -n5tarta -n5tartei -nt5arten -nt5arter -n5tartex -n5tarth -nt5arti -nt5artu -nta4ru -ntasi4 -n4tath -nt3äm -nt3änd -n4t3ärz -nte5ame -n4teben -nteg4 -n3tehe -nt3ein -n4t3eis -nt4el -nt5elek -nte4let -nte4lin -nte5ma -nte4mot -nte4nem -ntene4r5 -n4tenpä -ntens4 -nten6te. -n4t5ents -n4tentw -n4tepi -n3ter -nter3a4 -nt5erbe. -nt5erbt -nterdru6 -nter5eic -nte6r5eis -nte4rel -nteren6de -nter6loe -nter4lö -nter5n4e -nter5obe -nter5oe -nter3z4 -n4t5erz. -nter6ze. -nte3sa -n4t3ess -nte5stei -ntest5r -n6t5estri -nteu6eri -nt4hals -nthan5a -n4t3heb -n4t3hel -n3ther -nt5herd -nt5herst -nth2o -n4t3hof -n4t3hot -n3thr -n4thue -n4thum -n3ti -nti3an -nti3c -nti3f -nti6k5erh -nti4lin -nt4im -ntim3p -n4t3ind -ntin4e4 -nti5nen -ntiner6s -n4t3inf -nting4 -n4t3inh -ntini5er. -ntini5ers -n4t5inse -nt4is -nti5sta -ntlan4 -n3t4lem -ntmen4s -ntni2 -ntnis3 -n3tof -nton4st -nto5pfa -n4topt -n4t3org -n4t5o4rie -ntos2 -nto3st -nt2r -nt5raed -nt5raend -nt4ral -nt5rand -nt5rang -nt5ränd -nt5räng -n4t5räts -nt3rea -n5treco -n5t4ree -nt5reic -nt5reif -n4t5reis -n5t4rem -nt4ré -n4t5rieg -n4t5ries -nt5ring -n4t3roh -nt1s -nt2sk -nts2t -nt3sti -nt6stief -ntt4a -n3t2ue -ntu3en -n4tumsc -ntu4r3a -ntu4re. -ntu4rei -ntu4rer -n3t2ü -ntz2 -1nu1a -nu3a2r3 -nubi3 -1nu1c -1nud -nudel5e -3nue. -3nuea -2nueb -3nuec -3nue3e -3nuek -nu3el. -nu3els -3nuem -3nuen -nu3en. -nue3na -nu3end -3nueo -3nues -3nuet -3nuev -3nuew -3nuez -nu2fe -1nug -2n1uh -3nuhi -1nui -nu3in. -n2uk -n3u2kr -nul4l3a -nulle4 -nul4l5eb -nul4lei -n2um. -2n3umb -n2ume -4numf -4numg -2numl -3n2umm -4n3umma -4numr -2nums2 -2n3umv -2n3umw -2n3umz -nu2n -2n3una -2n3und -2n3une2 -4n3unfa -4nunfä -nunft3 -3n2ung -4n3ungl -nungs3 -nungscha6o -2n3uni -n3unk -2nunr -nuns2 -2nunt -nunte4 -3nunti -2nunw -3n2unz -4n3unzu -1nuo -2nup -2nur -nu2ra -nu2ri -nur2z3 -1n2us -nu2sa -nus3au -nu2sä -nu2s3i -nus5pe -nus6serl -nus3t -1nut -nu2ta -nuti2 -4n3u4top -3nutt -3nutz -nut4zei -nutzer5e -1nuu -1nu2x1 -1nuz -1nü. -1nüa -2nüb -3nüba -1nüc -1nü1e -1nük -nü3la -1nüm -1nün2 -nü3na -1nüo -nür3c -nürs2 -nür5ste -1nüs -1nüt -1nüv -1nüw -1nüz -2nv2 -n3ve -n3vi -n3vo -nvoran4 -2nw -n3wa -n3we -nweg3s -nwehr3 -nwin6d5er5 -n1x -1ny. -n1ya -n2ya. -1nyf -1nyh -1nyl -n1yo -1nyr -1ny1s2 -1nyw -2nz -nza2b -n4z3aer -n2z3a2g -n2za2k -nz3and -n4z3a4n3e -n5zani -n4z3ant -n4z3anz -nz3app -n4z3a4ri -n2zas -n2zat -n4z3aus -n2zän -n2z3är -n4z3eim -nze4la -nze4lei -n4z3emb -nze5nad -nzene4 -n6zenerg -nzen5ev -n6zensem -nz5ente. -n4z5entg -n4zentl -n4z5ents -n4zentw -n4zentz -nzer3a -nzer5eig -n4z5erlo -n4z5erlö -nzer5oe -n4z3erö -nzer6tau -nzer6trae -n6z5er6trä -nze3sk -nze5sta -nze5s4te -nze5sti -nze5str -nze3u4t -nzin3a -nz5info -n4z3inj -n4z3inn -n4z5inte -nz3l -nz4ln -nzo2b -n4z3oef -n2zof -nz3oli -n2zor -nzue4c -nzu5eck -nzu5eng -n4zwet -n5z4will -n2zwo -n2zwö -n2zwu -1ñ -ño1 -o1a -oa2be -o2abi -oa2br -o2ac -oa3chi -o2ad -oa2dr -oad3ra -oad3t -oa2g -oa3in -oak3l -oa2ku -oa2l -o2ala -oa3lab -oa3las -oa2m -o2a3mi -oampe4 -oa2n -oana3 -oan3e -o2aq -oa2r -oar4dr -o3a2sc -3oase -oa2si -o3ass -o2at -oa2tr -o3au -oauf3 -oa2v -oa2x -oa2z -o1ä -ob3ach -ob2al -oba4lin -ob2am -2ob2ar -2ob2as -2obau -ob3auf -2ob2ä -obb2l -2obe. -2obea -4obec -obe3e -2obef -ob5eins -4obek -obe6leis -obe3li -2oben -obend4 -obera4 -ober5ad -ober5al -o4beram -obe4r5ap -o4berb -obe4reb -obe6r5eis -obe4rel -obe6rend -o4berf -3o4berg -obe6rin6g -oberin6t -obe4ris -o4berk -2obet -2obev -2obez -2obi -obi2e -obi4l5in -1obj -ob2l -ob3lau -ob3leb -ob3lei -ob3li -ob4lit -3o4boe. -2oboh -ob3ort -2obot -ob2ra -2obru -2obrü -ob4s3ac -ob3sc -ob3s2h -ob3sku -ob4ster -3obsts -2obu -o4bueb -o4bunt -obu2s -2obü -o2büb -2oby -oby2t -ocas2 -ocau2 -oc1c -3occl -o1ce -och3a4b -o5chani -ocha4r -och3au -ochdru4 -oche4b -o4ch3e4c -och3eh -och3ei -oche4l -och5ele -o4ch5er4g -ocher6ke -o4ch5er4l -och3id -ochi3e -och3l -och3m -och3n -ochoe4f -ocho4f -och3ö4f -och3re -3ochse -och3sh -ochs4p -och5sta -och5stem -och5stu -ochtergene6 -ochu4f -o3chun -och3u4t -och3w -o1ci -o3ckad -ocke4l -ock5ersa -o6ck5er6sc -o4ck5er4w -ock5le -o2cko -ock3s4p -ock5sta -ock3sz -o4ckum -o1cl -o1cr -ocre2 -od2a -odab2 -od3ak -od2d3r -o3dec -ode3e -odegene6 -ode3i2 -odein4 -o5dein. -o5deins -ode4l5a4g -ode6lauf -odel6l5ae -odel6lau -odel4l5ä -ode4lo -ode2n -oden3e -oden3i -odenk4 -o3der. -o3dern -o3ders -odes5ae -ode4sä -odes4po -o6d5esser -o3dex -2odia -2odie -2odif -2odir -2odo -o3dom -3o2don -o2d3op -o4dora -odo4si -2odö -2odr -ods2 -2ody -ody2m3 -oe3ba -oebels4 -oe4ble -oe2br -oeb2s3 -oe1c -oech6ste -oe2du -oe1e -o3effi -oe2f3l -oegen4s -oe2gl -oe2gr -2oeh -oehe4n3 -oeh3ri -2o1ei -oe4is -oe5isch. -3oe2ko3 -oe2kr -oe2l -3oel. -oel3an -oel3au -oelb5ste -oel5ein -4o3elek -oel5ent -oe3ler -oeles4 -oel4fei -oelfen4 -oel6f5end -oel3in -oel4kle -oel4kli -oel3le -3oelm -oelo2 -3oelr -3oels. -oel3sz -3oelu -3oelv -3oelw -2oem -o3em. -oe4mie -2oen -oen3ec -oen5erg -o3entf -o3entl -oentu3 -oe3nu -oe3ota -2oep -oepf3l -o3e2pi -oe4r3a2 -oer4b3l -oe4r3ec -oe4r3ei -oe4rel -oe4r3e4m -oe4r5ene -oerer6fa -oerer6fol -oerer6gebn -oerer4l -oe6r5ermu -oe5r4erz -4oerf -o3erfi -o3ergä -oe4r3il -oerner6v -oer3oe -o3erö -oers4k -oert6er -o2es -oe2sa -oe4scha -oe6schef -oe6sch5ei -oe6schene -oe6sch5eri -oe4schl -oe4schm -oe4schn -oe4scho -oe4schr -oe4schw -oes2p -oes6s5che -oes4st -oest3a -oes3te -oes3tr -4oesu -oeße4n3 -o3et. -oe4tag -oet3ei -oete4n3 -oet2h -oe3t4he -oe4tho -oe4thy -oe2t3r -o1eu -oe2vo -o1e2x -o1ë -2of2a -ofa2c -of3a2d -of3a2g -of3är -o2f3e2b -o2f3ec -o4fena -o4feng -o4fenk -o4fent -o4fenw -o4fenz -o4f3erb -o4f3err -o4f5er4sc -o3fes -of3eti -of3eun -of2fa -off3al -off3an -off5eie -offe4l -offel5s -of4f3in -3offiz -of2fl -of2fr -off3ru -off3rü -off3sh -off3si -offs4o -off3t2 -of4f3uh -of4fur -ofgas3 -2ofi -of3ins -ofi3s4 -ofi5sc -2o1fl -of2la -of2lo -of2lö -2ofo -o2fop -of3orc -2o1f2r -of3rä -of4s3am -of4sei -of4s3en -of4sin -of4sof -of4spa -of4spe -of4staf -of4sto -of2s3u -of3th -of4t3ri -oft3s -2ofu -o2f3uh -o2fum -o2f3ur -2og -o2gab -o2g3ac -og3ang -og3arb -og3ari -og3aus -og3d -og3ein -ogel3i4 -ogen5ec -ogener6n -ogen4id -ogens4p -o4g3erf -ogeri4 -o4g3eth -ogfi2 -ogge4n -ogging5 -og2hu -ogin3 -o4g3ini -o4g3ins -ogle3e -ogleer4 -og2lo -og2n2 -og4nas -o2g3nu -ogo3e -o3gog -og3o4ri -o2g3ou -og3rau -ogs2c -og3spr -og2st -og3ste -og3sti -ogu2 -o1ha -oh3alk -o2h3ar -o1hä -o1he -o2h3e2d -o4h3ein -o4h3eis -ohen5s -o4h3er4t -ohe3x2 -o1hi -ohkos4 -ohkost5 -ohl3a -ohla4d -oh3lec -ohl5eint -ohl5eis -oh3lep -ohler4 -oh4l5erg -oh4l5erh -oh4l5erw -oh3les -ohl3o4r -ohl5sa -ohlste4 -oh2lu -oh3mi -oh3mu -ohn3ac -ohn3ap -oh4n3e4b -oh3nee -oh3nel -oh6nense -ohn5ersa -oh4n3e4t -oh4nin -3ohnm -oh2n3o -ohn3s4k -o1ho -oh3oel -oho4l3a -oho4l3o -o2h3o2p -oh3öl -ohr3a -ohre2 -oh4r3el -oh4rem -oh4r5er4g -ohr5erh -ohr5erk -oh4r5ero -oh6rer6sa -oh6rersä -oh3rie -ohr3im -oh4r3in -ohrs2 -ohr3sk -oh1s -ohs2p -oht2 -o1hu -o2h3um -oh3w -o1hy -o1i2d -oid3an4 -o2ik -o1i2m -oimm2 -o1in1 -oi4nen -oi2n3i -o1i2o -oi2ra -o5isch. -o4ische -o3ism -o3isr -oiss4 -ois5sa -oi4ta -oi2tu -ojek6tori -o1k -oka3b2 -okabe4 -o2kac -oka3i -okale4 -oka4lei -oka6lere -oka4lin -okal5th -oka3pf -okas3 -oker4be -oke3re -okka3s4 -ok2la -ok2li -ok2lo -ok2lu -ok2o -oko4pt -ok2r -ok2sa -ok2s3o2 -oks3p -okt4o -okto5b -oktobe4 -ok5tor -o4kula -1okw -2o1la -ol3a4br -ol3abu -o4l3ach -olade4n -ola3ei -o4l3aet -ola3fl -ol2al -ol3a4mi -ol3ana -olars4 -ol3a4ve -o1lä -o2l3ät -olb2 -olbe4n3 -ol3che -olch3r -ol5da. -ol4dae -ol2dä -ol4d3ed -old5ersa -ol4d5erz -olde4s -ol4d5ese -ol4deu -ol4dim -ol4dop -oldo4r -ol4d3re -ol4d3ri -ol4d3ro -ol4drü -old3sa -old4sam -old4sk -old3st -old6stro -ol3eie -ol3emb -ol3emp -ole2n -olen3e -ole3ra -o4l3erk -o4l3ert -o4l3erz -o4l3ess -oles4te5 -ole3um -ol3ext -ol3exz -o1lé -ol4fak -ol4fau -ol4f3ei -ol4fem -olfer4l -ol4fle -olft4 -ol2gl -ol4gre -o3li. -oli3a -olie4n3 -oli4kan -ol3imp -ol3inj -oli4tor -oli5tur -oli3ze -ol2kl -ol4kre -ol4kro -olk2s -olks3c -olks3t -oll5ach -ol4l3ak -ol4lanh -oll5anz -oll5ass -ol4l3au -oll3e4c -oll5ein -ol4l3el -ollende6f -ol4l5er4d -oll5erfo -ol4l5erh -ol4l5er4m -oll5ertr -oll5erwe -ol4l5erz -ol4l5ess -ol4l5ins -ollla6der -ol4l3op -oll5s4a -olls4p -ol4lum -2olo -oloni4e -olo5pe -olo3ra -ol3ort -ol2ov -ol3s2k -oltai4 -olta5is -ol4t3am -o1lu -olu4ta -olu4th -o1lü -olve4 -ol2y -4olys -ol2za -olz3a4c -olz3ar -ol4z5eig -ol4z5en4g -ol4zep -olzer4 -ol4z5ern -ol4z5ers -ol4z5ert -ol4zik -ol4zim -ol4z3in -ol2z3o -ol2zö -ol2zw -ol2zy -2om. -o4m3a4bl -o4m3a4br -o4mada -o4m3a4ge -oma3la -o4m3alg -omali4e -om3all -o2m3a2m -4oman -oma4n5er -om4anh -omani4e -om5anke -om4anr -o6mantei -o4m3arm -omar4s -o4m3art -omar4te -o4m3a4sy -4omat -o4m3atl -o6m5atom. -o6m5atome -o2m3au -ombi4e -ombu3c -2ome -o2meb -om3ebe -o2m3ef -om3ein -o4meis -om5eise -o4mele -ome3na -om5ener -o2mep -o5mer. -om3er4h -omer4ke -omer4sp -om5er4tr -o4meru -om3erz -o4mesh -o2m3eu -o2m3ex -omi4c3 -omi2e -omie3s -o4m3inf -o4m3inj -o4m3inn -omi3n4o -o4m3ins -o4m3int -o4m3inv -o4miso -omi3tr -2omm -omme4la -1omn -o2m3oa -om3ope -o4m3orb -o4mord -o4m3org -om3o4ri -ompani4 -om3pf -2oms2 -omt2 -o2m3um -o4m3unf -o4munt -o2m3ur -o3mus -o1mü -2o1na -o2nab -ona4be -on2ac -onaer4s5 -ona5ge -o2nak -onaler4 -ona4l5in -on3amb -on5amei -on3amt -on3ann -on3a4no -on3apo -on3app -on4are -o4narm -on4arr -on4ars -on3art -ona2s -on3asp -on5atom -onat4s -onats5c -2onä -onär4s3 -on4d3an -onda4s -ond5ast -on4d3ei -onder5ei -onde6r5ers -on4d3ex -ondi4e -ond5inn -on4dob -on4d5ril -on4d5rin -on4d3ro -ond5ruec -on4d5rüc -ond4sa -ond4sei -ond3st -ond4ue -2one -o2neb -on3e4ck -o2n3ef -on3ein -one2l -on3ela -on3ele -on3ema -3onemp -one2n -o4n5ends -on4eng -onens4 -on3ep -o4n3erd -o4n3ere -o4n3erf -oner4fa -on3erg -o4n3erh -o4n5erke -o4n3er3o4 -on3erö -on5ersa -o4n3erz -o4n3est3 -o4netu -on3f2 -onf4l -ong2 -on3ge -on3gha -on3gla -on3gle -on3go -ongru5e -ong3s2 -on3gu -2oni -oni5ake -onialer6 -oni5a6lerw -on3ido -oni4enk -oni4enm -onie3s -oni4g3a -oni4ger -oni4gl -onig3s4 -o4ni4kan -o4nikba -o4nikbe -o4nikbr -o4nikei -o4nikf -o4nikk -o4ni4kl -o4nikm -o4nikp -o4ni4kr -o4nikse -onis4o -2onj -on3k2 -3onke -onli2 -onlo4c -onloch5 -on3na -on4na4b -onn4an -on3n4e -onne3s -ono1 -on3o4be -on3ode -on3ofe -o4n3oke -on3orc -onos2 -onot4h -ons3a -onsch4 -on4s3eb -onsen5e -onser5e -onse4t -onsi4d -ons5ing -ons5kl -on4spar -onst4a -on6stapel. -onste4r5 -ont5arm -on4t5auf -ont5end. -ont5erfa -on4t5er4h -ont5erke -on4t5erl -on4tho -onto3e -on4t5rat -on3tu -o1nu -o2nue -2onuk -on3ums -on3url -2onut -1ony -on3z -onz4e -onze5in4 -onze5la -onze5r4a -onzer6t5ak -2oo -oo2bo -oo1c -oo2dr -o1oe2 -oo3en -oo3ge -oo2gl -ooki2 -oo2k3l -oo2kn -ool3c -oomgene6 -oomo2 -oo4mop -o1op -o1or -oo4r3a -oor3d4 -oor5de -oo4r3ei -ooren4 -oo4r5ent -oo4rer -oo2s3a -oo2sk -o3osm -oos2p -oo2s3u -oo2t3a -oot3ei -oo2th -oot3he -oo2t3r -oot4s3a -oot4s5en -oo4tur -o1ö2 -2op. -op3adr -op3aeh -opa3i -op3akt -opa4let -op3ant -opar4a -opa6rade -opa4rit -op3arm -op3äh -4o3ped -o2p3ef -op3eig -o3pek -4o3pel -o3pen -ope4ne -openty4 -4opep -3o4pera -op3erh -3opern -o5pers -op3ey -op2fa -opf3ac -opf3af -op3fah -opf3al -opf3ar -opf3au -op2fä -op4fem -opfe4n -opf5ent -opf5eroe -op4f5erö -op4fin -4opfl -opf3lo -op3flü -op2fo -opf3r -opf5ste -oph2 -op3him -op3hit -o4phol -oph3t -o1pi -opi3a -opia4t -op3id -opi5er. -opi5ers. -opie4r5u -op3i2k -opin2 -oping3 -op3lag -op3lic -op3lis -op2ne -opo4la -op4pan -oppe6l5in -oppin4 -op2p3l -3oppo -2o1pr -op3rec -op3roe -op3rö -3opsi -op3sta -op3sto -op3stu -op3sz -1opt2 -2opt4e -op3th -2opu -2opy -o3ra. -o3raa -ora2b -or3abb -or3abr -or3abt -or3ace -or3ach -o3rad -o4r3add -or3adr -4orae -or3aeh -or3aet -o3raeu -o3raf -o3rag -ora4ge. -4o3rak. -or5akad -3orake -o3rakr -4o3raks -or3akt -or3a4ku -o5ralba -oral6ben -oral6bum -o3rali -o4r3alm -o3rals -oral4ta -oral4tä -ora3ma -or3a4mi -oran6fra -oran4ku -oran4kü -oran4me -ora4no -or3app -or3aps -ora4r3a -ora4rer -2o3ras -o3rat. -o3rate -o3rati -or3atm -o3rats -o4r3att -4orau -o3raub -o3rauc -or3auf -o3raum -o3ra3un -or3aus -oraus6gewaehl -oraus6gewähl -oraus6sche -oraus6wa -or3auß -o3rav -o3raw -o3rax -o3ray -o3raz -o1rä -or3äh -or3änd -or3ät -or4bana -orb5art -or4b5inn -3orbit -orb2l -orb5lad -or1c -4orca -or2ce -4orda -orda4m -or4darb -or4das -or4dau -or4d3e4b -or4d3ei -ord5els -ord5engl -4ordes -or4d3eu -or4d3it -3ordn -or2do -or4dös -ord3st -ord3t2 -or4duf -or4dum -2ore -orea3s4 -ore4ast -o2reb -or3e4be -o4r3e4ck -or3e4di -or3eff -o3reg -or3eig -or3ein -or3e4la -or3emp -or3emu -o4renbo -oren4er -ore4n3i -o4renki -oren6nen. -o6rennet -o4renno -ore4no -o4renpl -o6renpor -orens4 -o6rensen -or3erd -o4r3erf -ore3ri -o4r3erk -ore3ro -or3erp -or3erw -or3erz -or3et4h -o2r3eu -o2rew -orfa4c -or4fend -or4feng -orf3s2 -or3g4a -orge3i -orgein4 -4orget -or3ghi -4orgia -orgi5en -orgi5er -or2gn -orhu2 -o1ri -ori4ena -ori6enbe -ori4enl -ori6enme -ori4enn -ori4enr -5o6rienti -ori4env -ori6enwe -o5rier -ori5er. -or3ima -ori4mi -ori2n -o4r3ind -orin3i -or3ins -o4riso -4o3rit -ork2a -ork5erz -ork2l -ork3sh -orkt4 -ork5ta -2orm -or4mad -or4mam -or4m5ans -orm5asp -or4m3eb -or6m5erfa -or6merge -orm5inh -orm5ins -orm5int -or4m3op -orm5ord -orm5org -orm5ori -or4mum -ormu4n -or4m5uni -or4munt -ormvol4 -orna4c -orn5ack -or4n3al -or4nan -or4nar -or4nau -orn5erf -orno3s -2o1ro -or3o4be -or3oef -or3oel -o2r3ok -or3oly -oro5n4a -or3o4pe -or3opf -o2r3o2r -o2r3ox -o1rö -or3öl -orp2f -orrai4 -or3r4e -or5ren -orr2h -or3s2a -orsau4 -or3sä -orsch4m -orse3s -or3sh -ors2k -orspor4 -ors4tin -orst5ob -ors2z -ort5aer -or4tan -ort5an. -or4t3a4r -or4t5aus -or4tär -or4t3eb -ort5eiw -or4t5eli -orte4n -or4tent -or4tenz -ort5erb -or4t5er5e -ort5erfi -or4t5erk -or4t5erl -orter6lo -orter6lö -ort5ersa -ort5er6sc -or4t5erw -or4t5erz -orter6ze -or4t3e4v -or4thol -2orti -orti5en -or4t3in -or4t5obs -or4t5off -or4t5ori -or4t5ort -or2tö -or4t5res -ort5rin -ort5rol -ort5spe -orts4ti -or4t3um -4ortü -or3uhr -orum4s -or3un -oru2r -o3rus -or3u2t -o3rüc -ory1 -o2rya -orzel5 -os2a -osa3ar -o3sab -osa3er -osa3i -o4s3ami -os3art -o3s4a3s2 -3o2s3ca -2osch -osch5ar -osch5aug -o3sche -osch5lei -os2co -os2e -o4s3eie -ose3in4 -o3sen -o4s3er4k -o3s2hi -o3s2hu -o1sk -os4kal -o2ski -os4kle -os2ko -os3kol -o2sky -o3s4mog -o2s3oe -os1p -os4pac -o4spae -os4pas -o4spä -os4per -o4spf -o3sphä -os2po -os4pra -o3s2pu -oss4abb -os4s3a4c -os4s3ae -os4s5ala -os4sall -os4san -oss5and -os2sä -os4s5en4z -os4s3ep -os4s5er4b -os4s5er4f -os4s5er4r -os4s5er4z -oss5esse -os4sik -os4sim -os4s3io -os4sof -os4son -os4s3or -oss3o4v -os4spar -os4spe -os4spo -os4s3ta -os4sto -os4str -osstu5er. -os4stur -o4st. -ost5abl -ost3ac -ost5aer -o3stah -ost3ak -ost5ang -ost5ans -ost5apo -os5tarr -ost5auf -ost3a4v -4ostä -ost5einb -ost5eing -ost5einl -ost5einr -ost5einw -ost5eis -os5telz -o4ste4n -ost5engl -os6t5er6weit -ost6es. -o4s3tet -ost5eur -os2th -ost5her -os4t3i4d -os5tina -ost5ink -os4tins -ost3ir -os4t3o4b -os3toc -os4tom -os2tr -ost5rac -ost5räu -ost5rie -ost5rot -o3s4tuc -os4tug -os4tums -os4tune -ost5urk -os3tüm -osu2 -osyri3 -o3s2ze -os4zin -2oß -o2ß3el -o4ß3en4z -oßer4 -o4ß3erb -o4ß3ere -o4ß3erf -o4ß3erh -o4ß3err -o4ß5ersc -oß3is -o4t3abi -ota2c -o2t3ad -o4t3aer -o4t3aeu -o4tagg -ot3a4go -o2t3ah -ot3amb -ot3ano -ot3app -otar3a -o4t3arb -o4t3arm -o4tarz -otas3t -ot5astr -o4tatem -o5tati -ot3att -ot3auf -ot5auge -ot3aus -ot3äm -o2t3är -o2teb -o4t3eib -o4t3eic -otei4n -ote5in. -ote5ina -ote5ind -ote5ine -ote5ini -ote5ink -ote5inp -ote5inu -ote5inv -ote5inw -ote5inz -o4t3eis -ote4l3a -ote4lei -ote4ler -ote4l5in -otel3s4 -ote2m -o4t3emi -o5ten. -ote4na -o4t5ends -oten3s4 -o4t5entb -o4tentl -o3ter -otergene6 -ot3erl -oter4le -o4t3erw -o4t3es4s -ot4est -otes5tant -otfall5 -ot4h3a4c -ot4har -o3t4has -o2thi -ot3hin -ot3hir -o4t3hof -ot3hol -o2til -ot3ill -o2t3i2m -otima4 -oto3c -oto3e2 -ot3off -otop3f -oto4r5in -oto3sz -o2tö -otra5c -o3tran -ot3rec -ot3ret -ot2ri -ot3ric -ot3rin -ot3rut -ot4s3at -ot4s3el -otso2 -ot4s3ol -ot3s4pe -ot4stei -ots4tra -ot4stri -ot4stru -ot4strü -ott5akt -ot4taut -ot4t3eb -ott5er6kl -ott5er6lo -otter6lö -ott5erwä -otte4s -ot3to -ott5rei -otts4t -o3tund -ot3url -ot4zae -ot2zä -otze4d -ot4z5er4k -ot4z5er4r -oub2 -ouch3e4 -ou3chi -ou3et -ouf3f4 -ou3f2l -oug2 -ou3g4a -ou2ge -o1uh -ou3is. -ou4le. -ou4lel -ou4les -ou2li -oul3s -o1um -ou2n -o4und -oun4d5ei -o5undso -o3unf -oun2g -oure2 -ou4r3et -ourie4 -ourme4 -ourmet5 -our4ne. -ou2sc -ous4i -ousi4e -ou2st -out3el -3outf -outli4 -3outp -out3s4 -outu4 -o1ü -3o2val -ove3s2 -oviso5 -2ovo -2ow -o3wec -ow3ef -o4wer. -ow3eru -o2wh -owse2 -ox2al -2oxe -ox3ele -ox4erz -o3xi. -3o2xid -ox3l -ox2od -o2x1u -ox3v -1oxy -2oxy. -o1yo -oy3s2 -o3z2a -3o2zea -oz4en -ozen4ta -ozes4s5c -ozes4s5t -o3zi -ozialer6 -ozon3a -órd2 -ö3ba -öbe4la -öbe4li -öbe4lo -öbels4 -ö2b3le -ö2br -öb2s3 -ö1c -öch3l -ö2chr -öch6ste -öchst5ei -öchst5r -öchs4tu -ödi1 -öd2st -1ödu -ö1e -öf2fl -öf1l -ögen4s3 -ö2gl -ö2gr -ö1he -öhe2n3 -öhle2 -öh4nef -öhre2 -öh3ri -ö1hu -ö1i -öi2g -ö4ische -ö1ke -1ö2ko3 -ökos2 -ö2kr -ö2l -3öl. -öl1a2 -ölas2 -ölb2r -öl3ei -öl3em -öl3ent -ö3ler -öles4 -öl3ess -öl4f3ei -ölfen4 -öl4f5end -öl4fli -ölf3s -1öli -öl3im -öl3in -öl4kle -öl4kli -öl3le -ölle4n3 -1ölm -öl4nar -ölo2 -1ölp -2ölpe -öl4ple -1öl1s -öls2z -öl3tu -1ölu -2ölz -ölz2w -ö2mie -öm2s -ön3ec -önizi5e -ön2st -ön3sta -öntu3 -ö1nu -ö3o2ta -öoti3e -2öp -öpf3l -ör1a2 -ör2b3l -ör1c -ördens4 -ö2r3ec -ö2r3ei -ö2r3e2l -ö2r3e2m -öre2n -ö4r3ene -ö4r3ent -öre4rer -ö4rer4fa -örer6fol -ör6erfor -örer6gebn -örerin6t -ö4r3er4l -ö4r5ermü -ör3ess -ör4fli -ö1ri -ö2r3il -ö2r3im -örne2 -örner4v -ör1o2 -örs2k -ö1ru2 -ö4r3une -örungs5 -ö2sa -ö4sch3a -ö4schä -ö4schef -ö4sch5ei -ö6schene -ö6schent -ösche4r -ö6sch5er6f -ö6sch5eri -ö6sch5er6l -ö4schi -ö4sch3l -ö4sch3m -ö4schn -ö4sch3o -ö4sch3r -ö4schum -ö4sch3w -ös2e -öse3e -ö4s3ein -öse5str -ös4s5che -össe4n3 -ös2st -öst3a2 -ös3te -ös2th -öße2n3 -öß2ti -2öt -ö2t1a -ö2t3ei -ötel4n3 -öte2n3 -öt3ho -ö2t1o2 -ö2t1r -öts2 -öt2sc -öt2tr -öwe2 -öze3 -özes4 -1pa -3paa -pab2ä -pab4lu -pab4ru -pab4rü -2pabw -pach6t5er6 -pa3e2b -pae6cker -pae4d5er -2paeh -pa3ell -2paen -pa3erf -p3aerm -2paeß -2paet -pae6terb -pae6t5er6k -paet3h -pae4ti -pae4tr -paet3t -pa3fl -pa3fr -pag2 -2pa3gh -pa3ho -pa3ke -paki3 -pa3kl -pak4to -3pa3la -pa4lar -pala3t4 -3palä -3pale -pa3l4i -pal4mae -pal6m5ast -pal4mä -pal4m3o -2palt -pal4tal -pal4teb -pal4tei -pal4to -pal4t3r -pa3mi -pam3s -pa4nar -pa4n3at -pa4nau -pan3d4a -pan5de -pan5enz -pa4n3eu -p2anf -3panik -pa4nisl -pank2 -2panl -pan4nar -pan4n5e4b -pan6nene -3pa2no -pans2 -pan3sl -3pan3t4h -3panto -4pantr -3pap -papieren6 -papie6r5end -pap4pr -paps2 -papst3 -para3b -3parad -pa4rae -pa4r3af -par5akt -3param -pa4rant -3paras -pa2rä -2parb -4pare. -pa3ree -4paref -par5ein -4paren. -4parer -parer6geb -pa3reu -2parg -pargel6d -pa4r3id -par5ins -par4kae -par4k5am -par4k5au -par4kop -par4kr -park5ra -par4kue -2parm -par3ne -pa4rod -pa4roe -pa4rof -3parol -pa4ron -pa4rö -2parp2 -2parr -4parsa -4parst -4parsu -4parta -par3t4e -partei5 -parter4 -3party -2paru -pa4rue -2parv -2parw -par3z2 -pa4scha -pas5chaf -3pasi -pa3spo -pa3spr -5passa -pas6sein -passer4 -pas6serg -pass5ersa -pass5ersä -pas6s5ert -pas4sp -pas3t -pa5s4ter -pa3s4tr -2paß -pat2c -pat4e2 -4patel -pa5ten -3pati -pat3sa -4patz. -2p3auf -pa3uni -4p3ausl -4p3ausr -4p3ausz -3pazif -1pä -pä2c -päck5er -pä4d3er -2päh -2päl -2pän -2p3ärm -2päß -2pät -pä4ta -pät3e4h -pä4t3ei -pä4t5ent -pä4tep -pä4t5erb -pä4t5er4k -pät3h -pä2t3i -pä2to -pä2t3r -pät3s -pät3t -2pb -2p1c -p3ch -2pd2 -pda2 -pdi2e -1pe -pe2a -pea2r -pea2s -2p1e2b -pech3e -pech3s -4pecht -2peck -ped4a -pe5dan -pe4dri -3peel -peene3 -2p3eff -2pefr -pe4gal -pege4l3 -pe2gn -pei1 -2peic -2peie -2peig -pei4l3a -pe3im -4peis -peise5i -2pek -p2el -pe4l3ab -pe4l3ac -pe4l3ae -pe4lai -pe4l3au -pe4l3ax -pe2l3ä -pelb2 -pel3d -pe4l3e4h -pe4l5ei4c -pe4l5ein -pe4l5ei4s -4p3elem -pel5end -pel3er -peler5e -peler6sa -pe4l3et -pe4leu -pel3ex -pel3i4d -peli4n -pel4inh -pe4l5ink -pel5inn -pel5int -pe4lit -pelk2 -pe4l3ob -pe4l3oe4 -pe4l3or -pe2l3ö2 -pels2 -pel3sk -pel5ster -pe4lut -pel4zap -pena4b -pena4r -pe2nä -pend4a -pende4c -pende6l5a -pend4o -pe4n3en -penen5e -pen3f -pen5inn -peni4t -pen3k2 -penn4i -pe2n3o -pens4e -3pensi -pen3sl -pen3sm -pen5s4o5 -pen5s4p -3pensu -pen3ta -4pentw -penu2 -penz2 -3pep -pept4 -pe3ra -4perad -pe4r3ae -pe4r3ak -pe4r3as -pe4rau -pe2r3ä -pe4r3e4b -pe4r3ec -per5eig -pe4re4l -peren5e -pere3s -3perga -perik4 -pe5r4io -pe3rit -per6l5a6sc -per6l5aug -3perle -pern5en -per3o4b -per3oe -pe3ros -per4r5an -perr5eig -4persio -perwa4r -per4zeu -pes2a -pese4n3 -pes3s2 -pest5op -peten6z5a -pet2r -2pe2x -2pey -1pé -2pér -2pf. -p2fab -pf3abe -p2fad -pf3aer -pf3aeu -p2faf -pf3ai -p2f3a2k -pf3ame -pf3ans -pf3ant -p2fa2r -pf3are -p2f3as -p2fau -p2fäh -p2f3är -p2f3äu -pfdru4 -2pfe. -p2f3e2b -p2fef -p2fei -pf3eig -pfeil5s4 -pf3eim -pf3ein -pfe4l3i -pfels4 -pfe2m -pf3emi -p3fens -p2f3e2p -pfe4r3a -pfe6ran -p6f5ereig -pfer6gebn -p6ferleb -pf5erpro -pfer6prob -pfer6zeu -p3fes -pfes5ta -pfe5str -p2f3e2t -p2f3e2v -p2fex -pff2 -2pfh -p2f3i2d -pf3inf -pf3inn -p4f3ins -p4f3int -2pfk -pf2l -pf3lad -pf3lag -pf3lam -3pfleg -pf3lei -pf3les -pf3lie -pf3lin -pf3lis -pf3los -pf3lös -pf3luc -4pfluem -pf3lus -4pflüm -pf3ly -2pfm -p2f3o2b -p2foe2 -pf3oed -p2f3of -p2fom -p2fo2r -pf3orc -pf3ori -pf3orn -p2fot -pf3ott -p2fö2 -pf3öd -pf3ran -pf3rän -pf3ris -pf3rob -pf3roe -pf3ros -pf3rö -2pfs2 -pfsau4 -pf3s4e -pf3sk -pf3sl -pfspor4 -pf3str -pf3stu -pf3sz -2pft -pft4r -p2fuh -pf3uhr -p2f3um -3pfusc -2pg2 -p3gl -pgra4d -1ph -2ph. -phack3 -4p3haeh -pha3el -4phaer -pha3fl -2p3hah -pha3k -phal4t5a -phal6t5es -phaltest5 -p3hand -pha3s -pha4tr -2phau -2p3häh -3phän -2phb -2phd -phebe4 -phebel5 -2p3hei -phen3d4 -phe4n3e -phens4 -2phf -2phg -phik3a -phi4kan -phi4la -phir3 -phi4st -phit3e -2phk -ph2l -2phm -2phn -p2ho. -ph3oed -phori4e -ph2r -2phs -pht2 -4ph3the -phu2s3 -phu3t -2p1hü -3phys -2phz -1pia1 -piab2 -pia5ka -pi4a5li -pi4a3n -piap2 -pi2ar -pias2 -4pi3ce -pich3o -pich3s -p2id2 -pie3b2 -pie4gl -pi2e3i -pie4l3a4 -pie6leis -pieler6ke -pieler6kl -3piep -3pier -pie5rae -pie4reb -pie4rei -pie4rem -pieren6z -pies4st -1pig -1pil -pil4zer -pil4zi -pil4zw -pi3nal -pi3nas -pi3n4at -2pind -piner4 -pi4n5erf -2p3inf -3ping -pin4ga -pingen4 -ping5er -ping3s -3pinse -pin3sk -pins4p -pi3n4um -2pioi -pi5o4n5al -pi3os -1pip -pi4pe. -pi4pel -pi3ra -5pirate -pi3ri -3piro -pir3u -1p2is -2pi3s2k -2piso -pita6l5ent -pite2 -pitel3 -pites3 -2pi3t2h -pit2s -2pitz -pit4za -pitz4e -pi4z3in -3pizz -2pj -2p1k2 -pkur3a -1pl2 -2pl. -p3lab -2p3lad -pl4an -p3lanz -pla2q -p2lat -p2lau -pla2y -p3lea -ple3c -p2leg -p2lem -ple3n2 -4p3lere -p2leu -p2li1 -2p3lig -pli3n4a -p2lo -2p3lok -plo4min -plo3r -p2lö -2pluf -2p3lun -plu3r -plu2s3 -plus5s -2p1m2 -p3ma -p3mö -2p1n -pnoe3 -1po -2pobj -po1c -3pock -pocke4 -3pod -po5e4ben -po3ems -po3erb -po3erh -po3ets -2p3off -2poil -po2in -pok2 -3pol -po6lacke -po4l3an -po4l3au -pol4d3a -pol5ein -pol5eis -po4lep -polo3p4 -polo3s -po4lum -pol3z2 -pom4ph -p2on -2pond -poni2 -pont2 -3poo -po3o2b -poo4len -popa2 -po4p3ak -po4p3ar -po4phi -po2pi -po4p3li -pop3s2 -2popt -po3ral -po3rat -po3rau -por2ä -po3rär -pori4e -4porn. -pors2 -por4tae -por4t5ak -por4t5ei -por4tem -porter6l -por4t3h -por4t5is -5porto. -5portos -5portra -por6t5raeu -por6t5rat -por6t5rau -5porträ -por6t5räu -port5ren -por6t5ric -por4tro -port5sz -3po3ru -po3s2p -po2st -po4sta -pos4t5a4g -postasi6 -post5au -post5ein -pos4tem -pos6t5er6h -post3h -pos5til -pos4t5or -post5ra -post5re -post5ri -3pota -po4t3ar -3potä -3pote -poti4e -pot3sp -pot4t3a4 -pot4t3r -po4t3um -po3un -povi2 -po2w1 -po3wa -powe2 -1pö -3pöb -pö2bl -pö2c -pöl2s -2p1p -p2p3a2b -p2p3ac -pp3alt -p4p3ank -pp3ans -pp3app -pp3arb -p2p3at -ppe3e -pp3ein -pp3eis -pp3eit -p3pek -ppe4l3a4 -ppe4len -ppe4ler -ppeli5ne -ppe4lo -ppe2n3 -ppens4 -p4p5erze -pp3fa -ppfen4 -pp3fr -p2pfu -p2p1h -pp1l -p4plac -p2plä -p4plet -p5plik -pp3lis -pp3lo -p2p3oe2 -p2p3oh -p2p3o2p -pporter6 -p2p1ö2 -ppp2 -p2p3ra -p2p3rä -pp3rec -p4p3reg -pp3ren -p2p3ri -pp3roe -pp3rol -pp3rot -pp3rö -pp3run -pp3rüc -p2ps -pps2a -pps2p -pp3sta -pp3sto -pp3str -pp3stu -pp3sy -ppt4a -ppt4e -pp3to -p3puc -p2p3um -p2r2 -3praef -5praeg -3prael -3praem -3praep -3praes -3praev -3praez -pra3f -3prak -3pral -pra3r -3prax -3präf -3präg -3präl -3präm -3präp -prä3q -3präs -3prät -3präv -3präz -2p3re. -pre6cher6l -3pred -2pre2e -pree3i -3prei -p3reih -prei4s5c -prei6s5ei -preis5s4 -prei6st -4preiz -3prem -pr4en -pren4ga -pren4gl -3pres -pres4st -p3resu -pri2e -2p3rig -pri2l3 -3prim -pri4met -4pring -prings4 -3prinz -3prio3 -3pris -pri2t -prit3a -prit5er4 -prit5t -4pritz -3priv -1pro -3prob -probe5i -probein6 -2proc -3prod -2proe -3prof -3prog -3proj -3prop -4pross -pro3st -3prot -pro4ta -pro3te -prote5i -4prott -pro3x -3proz -2prö -2p3rub -2pruc -4pruec -3pruef -3prueg -4prueh -4prung -prun4gl -prung5sc -prun4kr -2p3rut -2prüc -3prüf -3prüg -2prüh -2prün -2ps. -p2s3ad -2psai -2psan -ps3ang -p2s3au -2psb -2psc -psch4l -2pse -ps3ein -p2seu -2psf -2psg -2psh -ps2hi2 -2psi -psi2e -psie3n4 -psie3r -ps3ind -psis3 -2psk -2psm -2pso -p2s3oe -2p1sp -ps2pe -2pss2 -pss4a -ps5sau -2pst -ps3taf -pst3au -p3stea -p3stel -ps3tes -ps2t2h -ps3tha -ps2to -p2s2tu -2psv -2psw -1p2sy -2psys -ps2ze -2pt -pt3ak -pt3alb -pt3am -p4t3ana -p2t3ar -p2t3a2t -pt3au -pt3ax -pt1ä -ptbau6mes -p3te. -p2t3e2b -pt3ec -pt3e2d -pt3ef -pt3ei -pt3ele -pte2m -pt3emi -pt3emp -4pten -p3tend -pt3eng -pten4ge -p3tens -pt3ent -pt3ep -pt3erb -pter5ei -pt5ernt -pte3ro -pt3erw -pt3erz -p3tes -p3tet. -pt3eta -pt3e4ti -pt3eun -pt3e2v -ptgene4 -p2t1h -pt3ide -pti2n3 -pt3ind -pt3inh -pt3ini -pt3int -pt3ir -pto3e -pto4mo -pto4n3a -pto4p3 -ptor3a -pto2w -pt3rol -pt3ru -pt1s2 -pt4soe -pt3uh -pt3ums -p3tung -pt3urh -pt3urk -pt3url -pt3urs -pt3urt -1pty -ptz2 -1pu -pua1 -3pub -2puc -pu2dr -2pue -pueb2 -3puef -pue4lei -pue4l3o -puf4fa -puf4fr -2p1uh -2puk -pu4ker -pu2kl -pu2k3o -4pule. -pu4l3in -pul4s5ac -pul4sp -pul4s3t -pult3a -3pulv -2pulw -2pumg -pum4pa -pumpe4 -pump3f -pum4p3h -pum4p3l -pum4p3r -2pund -pun2e -3punk -punk4tr -pun2s -2punt -pu2r3a -pure2 -pu4r3ei -pur5erk -pur5erw -purg2 -pu3ri -pu2r3o2 -3purpu -pu2se -pus2h -pu3she -pus3t -puter5s -3puts -put6schm -put6schn -3putz -1püf -2pül -pü4lei -pül3l -pü2l3ö -2pv -2pw -pwa2r -1p2y1 -py3s2 -py3t -2pz -1q -2qs -qu2 -3qual -qu4ar -que4rei -que4ri -quer3n -que4te. -que4tek -2quew -2quip -3quo -ra2ab -r3aal -ra3arb -ra4b3ar -rab4ble -4rabd -ra4b3ei -rabe4l -ra4bep -rab4er -rab5erd -rabe4s5e -2rabf -4rabg -4rabh -ra4b5itu -2rabk -4rabla -rab3lo -2rabn -ra2br -4rabre -r4abro -rab4rü -4rabs -2rabt -4rabw -3raby2 -4rabz -4r3acet -ra6cher5ei -ra6ch5erf -ra6ch5erh -ra6cherk -rach5in -4rachs -rach5sk -rach6t5er6h -rachts4 -racht5sp -rach3u -3r2acr -r2ada -ra4dac -ra4d3a4m -ra4d3an -4radap -3radar -rad5ein -4r5a4del. -rad5ende -rad5enz -rad5erf -rad5erk -ra4dern -rad5ers -rad5erz -rade3s -3radf -3radh -r4adi -3radio -3radiu -3radmu -3radp -ra2dr -rad3ri -rad3s -rad4sor -3rad3t4 -3radw -ra2e -raech4s -r2aed -2raeg -2raeh -ra3elb -ra3elf -ra3elk -ra3elr -ra3els -4raem -4raenk -r3aepf -2raer -r4aer. -ra3erk -r3aerm -3raers -rae4sc -raes5sc -rae3te -raete5i -3raetl -4raeue -4raeul -4r3aex -rafa5els -raf5ahn -rafe2 -ra4f3er -raf3la -raf3lü -r4afo -raf6t5erz -raft3s4 -rag2a -4ragg -2ragm -ra2g3n -3ragou -rag4stä -3rah. -rahle4n -3r4ahm -r3ahn -ra3ho -rah4ter -rah4t3r -rai4c -rail4l -ra3ims -ra3ind -1raj -4rakad -3rakan -3rakat -2rakk -ra3k2l -ra4kre -4rakti -rak4tin -3rakue -3rakü -2rakz -r4al. -rala2 -ral3ab -ral3ae -ral3ag -ral3ak -ra5lamp -ral5ant -ral3as -ra5laue -ra2lä -ral3b2 -5ralbea -r5alben -ral5bl -5ralbuc -ra4lel -ral5ents -ra4l5erb -ra4l5erl -ra4l5erm -ra4l5ern -ra4l5erz -2ralg -ra4l3id -ralie4n -rali5er. -rali5ers -ra4l5ind -ralin6de -ra4l5inf -ralin6ge -ral5insp -ra6l5inst -ralin6str -ralin4t -ra4lin4v -ra4lir -ralk2 -r3alk. -2rall -ral3la -r5alleg -r6allin -4rallt -r3allu -r3allü -3rally -4r3alm. -ra2lo -ral3os -3ralö -r3alp. -4ralpe -3ralq -ral3s4k -ral5s6tern -rals5tuer -ral6s5turm -ral6s5tür -ral4t5ak -r3altä -ral4tem -r4al3th -2ra2lu -ral3un -3ralü -3ralz -r3a4mei -3rami -ram4mab -ram4man -ram4mas -ram4med -ram4mei -ram6mens -rammer5 -ram6m5ere -ram6m5erk -ram6m5erw -ram4mor -ram4m3u -ram4p5le -3ramsc -4r3amt -ramt4s -ran5ade -ra4nae -r3a4nal -ra4nan -ra4n3a4r -ra4nau -r3a2nä -4ranb -ran4ce. -r4anda. -ran6d5ala -r4andas -ran6dau. -r4anden -ran4dep -ran6d5ere -ran6d5erh -rand5erke -ran6d5erm -ran6d5err -ran6d5ers -ran4dob -ran4dre -rand3s -rands4t -4ranei -r3anek -ra4nenz -r4aner -ran5erk -ra4n5erz -ra4n3eu -3ranfi -ran4gan -4rangeb -ran6gerh -rang5sk -rang5s4p -rangs4t -rang5ste -rang5str -ran4i -rani3e -r3a4nil -ra3nin -ran4kr -2ranl -2r3anm -r4anmi -r3anna -ran3ne -ra4nod -ra4n3oe -4r3a4nom -2ranp -2rans -r4ans. -ran5skr -ran4spa -r5anthr -ranti4e -ran3um -4r5anzah -ran4zar -ran4z5er -ran4zin -4ranzu -4ranzü -ran4zw -3rap. -ra4par -4rapf -ra4pher -ra4pok -ra4pos -4rappa -rap4ple -rap4pr -4r3a4pri -2r1a2q -r1ar -r2ara -ra4rab -ra4r3ak -ra4r3an -4rarb -2rarc -r2ard -4rarea -3r4arei -raren5e -rar5erh -rar3e4v -r2arf -ra4rid -rari3e -ra4r3in -ra4rist -4rarm -rar4man -ra4rom -raro4t -rarot5i -r2arp -r2aru -r2arv -4rarz -rar3zw -r2as. -ra4sae -ra2sä -ra4schl -ra4s3eb -ra4s3el -4r3as4ph -4rass. -ra2st -ras5taf -rast5ei -ras4t3o -ra5stra -3rasur -4r3asyl -2raß -r4ata -ra4tae -ra4t3ar -ra2tä -ra4t3ei -4ratemg -ra6t5en6te -4rathl -ra3ti -ra4tid -rati4e -rat5inh -r3atmo -ra4toe -r3atom -rat4or -6ratorge -4ra4tro -ra4tru -ratsche6f -rat4se -rat4stä -4ratta -4r3atti -4r3attr -ratu4r5a -2rau. -r4aub -3raub. -rau4ban -rau4bel -rau4bri -r4auc -rauch5a -rau5chig -ra3ueb -raue4n -rau4ere -raue4r5i -2rauf -4raufb -4r3auff -4raufg -4rauf3l -raufs4 -4raug2 -2rauk -r4aum -5raumab -rau4m5a4g -rau4man -rau5me. -rau6ment -rau5mes -rau4m3i -r4aun -rau4nal -rauner4 -rau6nerz -r4aup -4raur -4raus. -4rausb -3rausc -rau6scher -rau6schn -rau6sch5r -4rausd -4r3ausg -4raush -4rausl -4rausr -raus5se -4rausv -4r3ausw -r4aut. -r4aute -rau6t5ent -4rauto -rau4tra -rau4tro -rau4tru -raut5s -2rax -ra2xo -r3axt -ray3o -3ra2za -ra4zub -2räb -räch4s3 -1r2äd -2räf -rä3fr -2räg -rä4g3ei -2räh -2räm -3rän. -3räni -3räns -2räp -2räq -2r1är -r2är. -rä3ra -rärm4s -rä3ro -rä2sa -rä2sc -räs3sc -rä3te -räte3i -2rätz -rä2u -4räue -2räul -räu6scher -4räuss -2räuß -2räx -2rb -rb3abb -rb3abe -rba4del -rba4lin -rba2m -r4banf -r4b3ang -r4b3ant -r4barz -rbb2 -rbby2 -rbe5erf -rbe5eri -rbei3d4 -rb5einh -rbein6te -rbe6lach -r6belaeh -rbe4lau -r4beläh -rbe4ler -rben4ac -rbengene6 -rbe5rae -rber4ei -rber6en -rberin6t -rber5k -rb5erke -rbe5rum -rbe3sl -r4beti -r2b3im -r4b3inf -r4blan -r6b5lasser -r4b5last -rb2le -r4b3le. -r4b3leg -rb5lein -r4b3ler -r4bleu -rb4lie -rb4lin -rb4log -rb2lö -rb2o -r4bonk -rboten6e -rb2ö -rb2r -rb3ras -rb3rea -r6brechts -rb4sam -rb4s3an -rb4set -rb4stau -rbs4teu -rb4sum -rbun4da -r2büb -rby2t -2rc -r1ce -rce2n -r3che. -rch3ei -rchein6s -rche4l -r3che4n -rchi3d -r3chil -rch3l -rch5mi -rch3n -rch3ra -rch3re -rch3s2 -rch5sp -rch3t4a -rch6t5er6f -rch6t5erg -rch6t5er6r -rch6t5erw -r2ch3u -rch3w -r1ci -r2ck -r3cki -rck3s -r1cl -r1cr -2rd -r2dab -rd2ac -rda5chu -r2da2d -rdaer4 -r2daf -r2d3ak -r2d3al -rd3ans -rd5anti -rd5anza -r2d3a2q -rd3ara -r4d3arg -rd3ark -r4d3art -r4darz -rd3a4si -rd3asp -rdär2 -rde4al -r4d3ech -rdein6fl -r4d3eis -r4d3elb -r4d3elf -r4d3emp -r3den -rden3d -rd5endu -rden4es -r6d5engen -rdenk4r -rden4s5a -rd5enth -r4d5ents -rde5ono -rde4r5ei -rde6rend -rde5r4er -rde6rer6g -rder6folg -rderin6du -rderin6s -r4derit -r4d5ernt -rder5oe -rder3ö -rdes4k -rde5stel -rd5estn -r5dete -rd3eti -rdeu4ri -r2d3e2x3 -rdi2e -rdie5ne -rdien4s -rdiensta6 -rdienst5ad -rdin4a -rd3inf -r4d3ins -r4d3ira -rdo4be -rdoe2 -r2d3o2f -rdo2l -rd3oly -r3dom -rd3oss -rdo4st3 -rdö2 -rd3öst -rd3rat -rd3rau -r4drea -rd6reig -rd4reis -rd3ris -rd3rob -rd3roc -rd3rol -rd3rom -r4drose -rd3rot -rd2rö -rdrucker6 -rd3rum -rd3run -rd4s5aen -rd4s3än -rds2p -rds4to -rds2z -rdt4e -rdte3l -rdt2s -r4dueb -rdu2f -rd3ufe -r2d3ug -r2d3uk -r4d3ums -rduns3 -r2düb -1re -re4ade -rea6l5erw -re2am -rea4men -rea2s -re3as3e -re3at. -re3ats -2reä -re4ban -re4bau -3rebel -reb4si -reb4sti -rech5ar -5recherc -re6cherfa -re6ch5erzi -4rechs -rech5tei -rech6t5erh -5rechtl -5rechts -re3chu -4reckg -4recki -4reckz -4recot -4recô -3recy -4redd -4r3edel -rede3r -2redi -r3e4dik -3redip -3redu -re3el -re3eng -re3er -re4es -3refe -2reff -r3effi -3refl -3refro -rege4l5a -rege4l5ä -regelei6g -2r3egg -re3gi -re2gn -2reh -re4h3ac -re4h3ar -re4h3ei -re4hel -re4h5ene -re4h5ent -re4hep -re4herf -re4h5er4l -rehgene6 -reh3l -reh5le -reh3n -reho2 -re4h3or -3rehö -reh3ra -reh3ro -re4hue -re4h3ur -re2hü -r4ei. -3reiä -r2eib -rei4bar -reich6si -rei3de -4reidec -4r3eidg -r4eie -4reier. -4reiern -4reiers -r2eif -4reifel -2r2eig -reig4a -5reigel -r5eigene -5reigeno -r5eigensc -5reiger -5reiges -5reigew -5reigru -r4eih -rei4kab -reil4a -4reils -4r3eilt -4r5eimer -rei4mor -re3im4p -rei5nalt -4reinan -4reinb -rei5necke. -4reinf -4reing -4reink -4r3einn -4reino -4reinp -4reinr -re5in6sel -r5einsp -re3in4v -4reinw -4reinz -rei4sal -6reisant -4reisar -6r5eisenb -rei6s5ent -rei6s5erk -rei6sers -reises4 -4reisf -4reish -4reisk -4reisl -rei4so -reis5or -4reisr -rei5s6temp -4reisu -4reisv -4reisw -reit3s4 -rei4zan -rei6zell -rei6zene -reizer4 -rei6z5err -rei4zwi -4rekk -rekter6z -rek4tin -rek4top -rek4tum -rekturin6 -2rekz -2r3elb -rel4e -relea4 -4re4lef -re5lei -4r3e4lek -4r3elem -re4lend -r4elev -2r3elf -re4lim -4relit -4r3elix -rel4lar -rel4l5ei -4r3elsa -4r3elst -2r3elt -relu4d -r2em -r4em. -re4m3a4b -re4mau -2remb -rem4da -re4m3ei -re5men -re6m5erei -re4m5er4f -re4m5erl -4remig -r4empe -4r3empf -4rempo -rem4sel -rem4str -re4mul -r4en. -r4ena -rena4be -re3nad -re4nan -ren5aue -4r5endab -ren4d5an -rende4b -rende4k -ren6d5erk -ren6d5erm -4rendg -3rendi -re4n3e4b -ren3ec -ren5ende -re4nep -4r5energ -ren5erk -re4n5erl -re4n5erm -4renexe -3ren3f2 -4ren4gad -4rengag -ren4goe -4rengp -4rengs -4renhis -re4nid -re4n3im -ren5inn -4renju -renk4lo -renk4r -3renl -3renm -ren4nar -ren4n5e4b -renn5e4c -ren6nene -6rennen6n -4renng -ren4n5in -renns4 -3reno -re4nos -renrue4 -renrü4 -rensch4 -4r5ensem -ren6serg -ren6sing -ren6s5pan -ren6spat -6renstip -ren6s5tri -4r3entd -6rentera -4r3entf -5rentfo -4rentgi -4r5enthä -ren4thi -4r5enthu -4r3entl -rentmo4 -ren6tric -ren4trü -4r3ents -5rentss -4rentw -4rentz -4renue -4renumg -2renü -r2enz -ren4z5ar -ren6zerg -ren6zer6l -ren6zerm -renz5ertr -ren6zer6w -ren4z5in -ren4zut -ren4zw -4r3enzy -re3ou -3repe -4r3e4pen -re3pf -2repi -re4pis -4r3epoc -2repp -3repra -3reprä -4repren -3repu -r4er. -rer3a4b -re3rad -4r5er4bie -6rerbreg -4r3erbs -4rerdb -4r3er4dr -rere4d -re4r5eid -4r5ereig -4r3e4rek -re4r5ent -re4r3ep -re4r5ers -re4r5erw -re5res -r3erfa -4rerfah -r3erfo -4rerfol -r6erform -rer4fue -4r3er4fü -4r3ergä -6rergebn -rer4gom -rer4gri -4r3erhö -r2eri -re4r3id -re4rim -rerin4f -rerin4s -rerin6ter -rerk2 -rer4kan -5r4erki -5r4erko -5r4erkre -5r6erlaen -rer4lan -5r6erland -4r5erlau -5r4erlän -5r4erli -4r5erloe -4r3erlö -4r5ermae -4r3ermä -4r5ermit -rer6mued -rer4müd -4r3ernä -rer5ne -4r3er4ni -4r3erns -4r3ernt -rero4b -4r3erod -4r3eroe -rer3o4v -2r3er2ö -rer5reg -5rers. -rer6sätz -rer4seh -rer4sto -4r3er4su -rer4tei -re2ru -rer4wac -4r3erwä -6r5erwerb -4rerzae -4rerzä -rer5ze. -5r4erzei -rer5zen -rer5zes -5r4erzu -5r4erzü -5r4erzy -r2es -r4es. -re4s3an -re4s3au -rese4h -3reser -reser5e -3rese4t -re4s3in -re4s3of -3resol -3reson -6ressen -res6s5erf -res6s5er6m -res6s5erp -res6s5er6w -ressgene6 -res4spa -3rest. -re5start -res4tas -5reste. -rest5ein -re5stel -5resten4 -res6tent -res4tex -re4stö -rest5rau -rest5ric -3rests -res4tu -4r3etab -re4t3ak -4r3e4tap -4retat -retex4 -re2th -4rethi -re4t5rol -rett3a -ret4tan -ret4th -rett3s4 -reub2 -reu4eri -reu3g2 -2reul -re3ums -re3ung -4r3eunu -2reup -2r3eur -reu4z5ei -2reü -2reva -re4val -revil4 -rewa4r -re2wi -rewin4 -2rewo -2r1e2x1 -2rey -1ré -2r1f -rfa5chu -r2fap -r5fast -rf3aue -rf4äh -r4fense -rfen6ste -r4f3erd -r4f5er4la -rfer4le -rf4es -rff2l -rff2r -rfi4le. -rf3inf -rfin4s -r2fj -rf2l -r4fland -r4f3lic -r2f3o2b -r2f3o2f -rfolg4s -rf2r -r4f3ras -r4f5rauc -rf2sa -rf4säu -rfs3id -rf4sor -rf2su -rf2u -2rg -rg2ab -r2g3a2d -r4g3aeh -r4g3aem -r2g3ah -r2g3a2k -r4g3alb -r4g3alp -rga4me. -rg5amei -rga4mes -r5gamo -r4g3amt -rga4ner -rga4nin -r2g3ap -rg5art. -r4g3ask -r5gasse. -rga4st -rgast5a -rgast5ei -rg3auf -rg3aus -r2gäh -rgd2 -rge4ant -r4g3ech -r4g3eid -r4g5eise -rge4l5ac -r4g5elef -rgel5ein -rgel5in -rgene6rals -rgen6gel -rgens4p -r4g5entr -rgen4zw -rge4ral -rge6rang -rge4rau -rgerin6t -r4g5e4tap -rgg2 -rgi5er. -rgi4sel -r4gl. -rg2le -rg3le. -r4gled -rg3leh -rg3ler -r4gleu -r4glig -rg4loec -rg4log -rgmi2 -rgn2 -rg3nar -r2g3ni -r2gno -rg3nom -r2g3ny -r2g3oa -r2go2b -rgoe2 -r4g3oed -r3gog -rg3ora -rgö2 -r2g3öd -rg2r -rg5raen -rg3rai -r4g3ral -rg5rand -r4g3rän -r4g3reg -r4g3res -r4g3ret -rg3rüs -rgs4al -rgs2c -rg4sel -rgs4en -rgs2i -rg3s4pe -rgs4pi -rg3spr -rg5stei -rgs4tol -rgs4tra -rgs4tub -rgti2 -r1h2 -2rh. -r2ha. -2rhaa -2rhae -2rhah -2rhak -2rhal -rhal4b3 -2rhan -rhang5s -2rhar -2rhau -2rhä -r2he. -2rheb -2rhef -4rheit -2rher -rhe4r3e -2rhi -r2hod -4rhoel -2rhof -rhof3s -rho2i3 -2rhol -2rhot -2rhöl -2rhs -2rhue -rh4un -2rhü -3rhyt -ri5alei -ri5a6l5erw -ri5alim -ri3am. -ri3ams -ri3ang -ria3no -ria3p -ri4ara -ria3s4t -rib4bl -rib2l -ri3ce -rich3a -ri5chlo -rich5ska -rich4sp -richtgene6 -5richtl -richt6s5e -richt6s5o -ri4cki -ri4con -ri4dar -ri4dau -4r3i4dea -4r3idee -ri4d3e4l -ri4dent -4ridol -2ri2dy -r2ie -rie4bac -rie5blo -rie4b5ru -rie4b5rü -rie4fem -4riefm -rie4fr -rie5g4r -rieg4s -ri2e3i2 -riein3 -ri3el. -rie5la -ri3ell -ri3els -rie5n6ade -riene4 -riener5i -ri5enro -rie5rad -rier5ei. -riere4n -rie5res -ri3ern -ri4ernu -rie3sa -rie5sta -ri3eur -ri2fa -rif3an -ri2fä -ri4f3ei -ri4f3er -rif6f5en6d -rif4f5er -rif4fr -ri4fin -ri2f3o -rif3s2 -rif4t5er -1r2ig -riga3e -4r3i4gel -ri4g5ene -ri2gl -rig3la -rig3lä -ri3g2o -ri2g3r -ri1i2 -rika3s4 -ri2kä -ri4kin -ri2k3l -ri2kn -ri4kone -ri4kor -ri2k3r -ri4kun -rila4b -rilf2 -rille4n5 -2rima -ri4mag -ri4m3au -ri4me. -2rimm -2rimp -rim2s3 -r2in. -ri3na -4r3inan -4rinbe -3rinde -rin4dia -4r3indu -ri3n2e -rine3i -rinein4 -rine3s4 -2r3inf -rin5fr -5rinfre -r2ing -rin4g3a -rin4gei -6ringeni -rin4gle -rin4gor -rin4gr -ring5sc -ring5s6en -ring5sti -ring5str -2rinh -5ri5ni. -4rinit -2r3inj -rin4kal -rin6k5ent -rin4kl -rin4kn -rin4ko -rin4kri -2rinl -6r5innenm -4rinner -r5innere -r3inno -4r3innr -4rinnta -rino3 -2r3in2q -2rins -r4ins. -rin4sc -r4insen -rin4sig -rin4sol -5r4inspi -r5insti -3rinsy -rin4t5an -4rinte -rin6tent -rin6ter6g -r4intr -rin4t5re -2r3inv -rio2d -2r1ir -r2is -ri6schei -ri4schl -ri4sch5o -rischoe6 -ri4sch5ö4 -ri4schw -risen3 -3risik -3risk -ri3s4ko -4risl -rismu4 -4risol -4risom -ri2sp -ri4spi -ris3pr -ris4ser -ris4sp -ris4ste -ri4st -rista4 -rist5ab -ris6t5erf -ris6t5ers -ris3to -ris4t3r -r2it -4r3i4tal -rit5ant -2ritr -rit4ra -ritre3 -rit3s2 -ritt3a -rit4tan -rit4tau -rit4t3h -rit4to -rit4tr -ritt4s5o -3ritua -r3i4tum -rix1 -rizi4s -1rí -2rj -2r1k -rka4b5l -rk3adr -r2k3a2g -rk3all -r4kanl -rk3are -r4k3att -rkauf6st -rk3edi -r4kelem -rke3lo -rke2n -rkens4t -r3ker -rker4he -rker4le -rker4we -rk3ide -r2k3im -rk3inh -r4k3i4n3i -rk2le -rk2lu -r4knut -r4k3o4be -rko3ef -r4kord -r4k3o4ri -r2k3ou -rk2ö -rk2r -rk3ren -r4k3rom -r4krou -rk4s3al -rks3au -rk4s5cho -rk4s5ein -rk4sel -rks5ern -rk4sin -rks5inn -rk2so2 -rks3or -rks4pen -rk3sta -rkstati6 -rk4stoc -rk4stor -rk3str -rk5s4tue -rk4sum -rk4t3ak -rk4t3an -rk4t5eng -rk4t5ent -rk4t5erf -rk4t5erg -rk4t5erk -rk4t5erl -rk4t5ero -rk6t5ersc -rk4t5erw -rk4t5erz -rk4t5eta -rk4t3in -rk4t3o4b -rkt5ope -rk4torg -rk4t5o4ri -rk4t5rad -rk4t5rat -rkt5rei -rk4t3ri -rkt4sor -rk4t3um -rku2n -rk3una -rk3uni -rku4ra -rku4sa -rkus3t -2r1l -rl2a -rlage4n5 -rlags5t -r3lan -r5land -rlan4d5i -r2l3ar -rlas4t5r -r4l3au. -rl1c -rle2a -r3lec -r5lei. -r3lex -r3li -rli4ne. -rli4nee -rli4nem -rli4nes -rli2s -rloes5s -rlog5in -r3lor -rlos3t -rlös3s -rl3skr -rl3ste -rl4sto -r3l2u -rlust5a -rlus4t5r -rlz2 -2r1m -rm3ade -rma3le -rma4leb -rm3ami -r4m3amo -rma5n4e -r4m3ank -r4manl -r4manp -r4mantr -r4m3aph -r4m3a4po3 -r4marc -r4marz -r4matta -rm2ä -rm3d2 -rmd4e -r4m3e4ck -rme5erh -rme5erz -r4m3ei. -r4m5einb -r4m5einf -rme5inha -rme5inse -rme5in6to -r4m5einw -rme4ner -rmens4 -r4m5entf -r4m5ents -r4mentw -r4mentz -r4m3erb -rmere4 -rmer6fah -r4m5er4fo -r4m5erfu -r4m5erfü -rmer6geb -r4m3erh -rmer4ha -rm5erkr -r4m3erl -r4m5er4na -r4m5er4nä -r4m3erp -r4m5erse -r6m5erwei -r4m5er4ze -rm4es -rme3s4a -rme5sta -rme5str -r2mex -rmid2 -r4m3i4de -rmilch5 -rmi6nanz -rmi4nar -rmi6neng -rmin5it -rmin6terp -r2m3i2r -rmi6schl -rmi6schw -r4m3iso -rmiti4 -rmm4a -rm5man -r4m3obs -rmo3el -r4m3ofe -rmo4nau -rmon5s4 -rmo6reng -rmo4ri -rmo3st -rm1p2 -rms2k -rms2p -rms4tr -rm3ums -rm3umt -r4m3una -r4m3unf -2rn -r1na -r3na. -r2nab -rna4ben -rn3abi -rnab4l -rnab4st -r3nac -r4nacc -r4n3aes -r4naho -r2nak -r4n3ala -r4n3amt -rna2n -rn3an. -rn3ana -rn4and -rn3ani -r4n3ano -r4n3anz -rn3app -rna2r -r4nare -r4narg -r4n3ari -r4narm -rn4arr -rn5arra -r4n3asp -r4n3ast -r4n3atl -rnat4s3 -r4nauf -r4naug -r4naus -r2n3äs -rnd2 -rn3da -rndi4e -rne4bl -r4n3eff -r3n4eid -r4n5elfe -r4n3e4ma -rne2n -r5nen. -r4n5ends -r4n5ener -rneng4 -r2n3e2p -r4n5erbi -r4n5er4fa -r4n5erfo -r4n3erg -r4n5erke -r4n5er4ku -r4n5er4le -r4n3ert -r4n3er4w -r4n3erz -rne4t3a -rne4ter -rne4to -rne4t3r -rne3u4f -rn3f2 -rng2 -r2n3i2d -r2n3in -rn4i3na -r3n4ing -rnk2 -rnk4a -rnn2 -r2n3oc -r2n3oe -rnoe4d -r3noet -rn3oly -r4n3ome -rn3opf -rn3orc -rn5ost. -rn5osti -r4n3ott -rn1ö -rnö2d -r3nöt -rn3sä -rnsch4 -rnse4h5a -rn3s2k -rn3s2l -rns2p -rns2z -rn3te -rnte3e -rn3tr -r1nu -rn3ums -rn3ur -rnus3 -r1nü -r1ny -rn3z2 -1ro. -ro3adr -r5oberf -ro4berl -2robj -r4obo -ro4b3o4r -2robs -ro1c -3rock. -ro4ckel -5rockes -3rock3s -ro4cku -rodi4e -rod2r -roebel5 -roe4bl -roe2f -r3oefe -r4oeh -ro4eis -4r3oek -4roel. -ro3ela -r3oelf -r3oeli -roel4la -2roem -3roemi -ro3en. -ro3end -ro3enf -ro3enk -ro3enz -ro3erf -ro3erh -ro3erz -ro3eta -3roetu -ro3ev -ro3fa -ro4fen -4roff -3rogg -ro4h3in -r4oh3l4 -roh5nau -ro4hoe -3rohr -roh4rer -ro3ir -ro3iso -rok2l -r3okt -ro3le -ro4liv -rol4lac -rol4l5a4g -rol4lan -rol4las -rolle4 -rol4l5eb -rol4lei -rol6lerg -rol6ler6w -rollor4 -rolls4 -roll5ste -r4olo -rol3s -ro4lym -5roman. -5romane -rom4anf -4romb -ro4mei -romen5e -rom5ent -ro4mer -rom5erk -rom5ers -2romn -r4on. -ron5ant -ro3nar -ron4au -rond4a -ro4n5erb -roni4e -ro4nikg -ro4niki -ro4niku -rons4 -ron4t5an -ron4tar -ron4tel -ron6tend -ron6terf -ron6terk -ron4th -ron4t3r -ron4t3u -2rony -ro3o2f -r3oog -3room -4ropf -ro3pn -ro3p4r -rops5t -4ropt -ror3ab -ro3rae -ror3al -ror3an -ro3rau -r4o3re -4r3orga -r4oro -ro3rol -ro3rom -ro4rop -rort4st -r4oru -ro3r4ue -ror2ü -rosai4 -r4ose -ro3se. -ros5elt -3rosh -ro3s4i -3rosin -ro3sp -ros4pe -ros4s5ei -ross5enke -rosseri6 -ros6sess -rossmut6 -ros4so -ros4spa -ros4st -ros4t5er -3rosto -r3oszi -ro4ß5en4k -ro2ßi -ro2ßu -ro4t3ak -ro4t3al -ro4t5ars -rot5art -ro2tä -ro4t5en4d -ro4terl -rot5ers -ro4thu -ro4toe -rot3sa -rots4te -rot6terk -rot6terl -ro4t3um -3roule -roulet4 -3rout -r3ouv -3rowd -r4ox. -ro3xa -r4oxk -röbel3 -rö2bl -rö2du -2r1ö2f -1röh -2r1ök -r3ölf -röl4l3a -3römi -r1ör -3rösl -3rötu -2r1p -r3p2a -rpe4r3a -rpe4r3e -rpe5re. -rpe4r5in -rpe2s3 -rp2fa -rp2fä -rp2fo -rp2l -r2p3li -rp2sa -rp2s3i -rps3t -rp3t -2rq -2r1r -rra3b4r -rr3abt -rr2ac -rr3a4ck -rr2ad -rrade4 -rra4deb -rra4den -rra4der -rr3aem -r4r3akt -r3ral -rrat4st -rr5aufg -rr5aufl -rr3äm -rr1c -r3reh -rr5einr -rreise6k -rrena4 -rre4nob -rren3s4 -rren6z5er -rrere4 -r4r3er4h -r4r3er4l -r4r3esk -rre4ve -r4rezi -rrg2 -rr4hen -rr4hos -r3ri -rrid4 -rri5er. -rrie5r4e -rri5erm -rrik4 -rri4tan -rrk2 -rr4nau -rr3obs -rro3el -rroe4s3 -rron3 -rro4re -r4r3osz -rrö2s3 -rr2th -r3ru -rr2ue -rrue3b -rruecker6 -r4rur -r3r2ü -rrü3b -rrücker6 -rry1 -rrz2 -2r1s -r3sabo -r3sac -rsa5chu -r2s3a2d -r4s3aem -r2sak -rsa4lin -r4s3amt -rs3ana -r4sanb -r4sanf -r4sanga -r4sanp -r4s3ant -r4s3anz -r2sap -rs4ark -r5s4au. -rsau6erl -rsau4g -rsau4m -rsau4n -rsau5se -rs2är -rscha4o -r3sche -r4sch5e4b -rsch5einh -rscher5ei -r3schi -r3schl -r3scho -rsch5ort -r3schr -rsch4ue -r4scor -r4s3ebe -rse2e -r3seg -r4s5e4mis -r3sen -rse5n4al -rse4ne -rsenk4r -rsens4 -rser5ei -r4s3erh -r4s3er3o -r4s3erö -r4s5ersc -r4s3erw -r4s3erz -r3set -rsex4t -rs4ha4k -rs4har -r3s4ha4v -rshi2 -r3shir -r3s4hor -rsi3el -rs4il -rs4kal -rs4kel -rs4kie -rs4kis -rs4kle -r3skul -r2sky -r3slip -r3s2lu -r3s4mog -r3snob -r2so2b -rs2om -r4s5ort. -r4s5orts -rs3ost -rs2p -rs5para -r4s3phi -rs3s2 -rst5abl -r3stad -rst5ala -r4stale -r4stanp -r4st5ans -r4stant -rst3as -rs4tat -r4staus -rstauto6 -rs5täti -rs4tei -r4s5teil -rst5eindr -rst5einga -rst5emi -rs4temp -rs4terb -rst5erba -rst5er4f -rs4t5er4h -r4steri -rst5er4l -rs4tern -r4st5ero -rst5er4ö -rst5er4r -rst5er6st -rst5er4t -rst5er4w -rs4t5erz -rs3tes -rs4t5ess -rs4t5eta -r3steu -r3s4tev -rs4tew -rs2th -rst5her -r3s4tie -r4stin -rst5ing -r4stit -rs2to -r4s3tot -rs2tr -r6st5rat. -rst5rät -r4strea -r4s5tren -rst5ret -rst5rin -r4strun -rs2tu -r6stuebe -rst5ums -rs5tung -r4stübe -r2sty -rsui2 -r4sumf -rs3umw -rs4win -r2sya -rs4zin -2rt -r4t5a4ben -r4t3abo -rta4ck -rt3aeh -r5taet -rt3aff -rt3aga -rt5a6gent -rt3aka -rt3akr -rt5aktes. -r4talle -rt5allt -rt3alp -rt3ama -rt3ana -rt3ang -r4tange -r4t5anku -rt3ann -rt3ans -r4tanw -rt5anza -rt3arb -r4t3arg -rt5a4rie -r4t3arm -rt3art -rt3asp -r5tat. -rt3atl -r4t3atm -rta4to -r4t5auft -r4taut -rt3äh -rt3ärm -rt3ärz -rtät2 -r3te. -r4teben -r3ted -rte3eh -rt3eff -r3teh -rtei3f -rt3ein -r5t4e5in4d -rtei3s4 -rte4lae -r4t3el4b -rte4l5ei -r4telem -rte4l5en -rte4ler -rte4lit -rtel6lau -rtels4t -rtel5ste -r4t3emb -rtem4e -rte4nem -rte4ner -rte4net -rtenge6bi -rten3s4 -r4t5entf -rt5entl -r4t5ents -r4tentw -rteo2 -rte5rae -r6t5ereig -r4t5erfo -rt5ergu -rt5ergü -r4t5erla -rter6mit -r4t5erna -r4t5ernä -r4t5ernt -rter6öff -rter6sparn -r7t6erstel -rter4wa -r3tes -rte3s4k -rte5stat -r4t5etab -r4t3e4ti -r4teuro -rtex4tr -r3t4hag -r4t3heb -r4t3hel -rt4herr -rt3heu -r2t3hi -r4thony -rt4hum -r2t3i2d -rti2e -rti4gel -rti4g3r -rti4kl -rt5illu -r4t3i4ma -r4t3ind -r4t3inf -r3tit -r3to -r4t3oef -rto4nam -rt3ope -rto5pfl -r4topt -r4t3orc -r4t3org -rto4rie -rto3s -rt2ö -r4t3ral -rt5rams -rt5rand -rt5rati -rt3rea -rt3reb -rt5reic -rt5reif -r4t5reis -rt5reit -rtreu4 -rt5ring -r4t3ris -r4t3riv -r4t3rob -r4troh -rt5roma -r4t3ron -r4trou -rtrue4c -rtrü4c -rt5sand -rtsch4 -rts5ing -rt4sis -rtski4l -rt4s3or -rt5s4pas -rt5s4por -rts4tru -rts4tue -rtt2 -rtu5end -rt3uh -rt3umb -rt3und -r4tunf -rt3ups -rtu4rei -r4t3url -r4t3urs -rtus3e -rtu2t -r4t3ute -rtwa5re -rt2y1 -rtz2 -ru1a -ru3a2r3 -rube4 -rubens6t -ruch3a -ru4ch5er5 -ruchter6 -ruch6top -rucht5s -ruck3a -ru6ck5erho -ru6ckerhö -ruck5er6kr -ru6ckerzeu -ru4cku -rude4a -rude4l5i -ru2dr -rueb3r -5rueckf -5rueckg -5rueckz -5ruempe -ru5enz. -ru5enzs -1r2uf -3ruf. -ru2f3a -ruf5erei -ruf5erk -ru2fo -ruf4se -ruf4ter -ru4fum -ru4gae -ru2gä -ru2gr -rug4sa -ruhe3i -3ruhm -3ruin -ru3in. -ru3ins -ru3ist -2r3ulk -2r3ulm -2rum -ruma2 -ru4maa -ru4m5aer -ru4mal -ru4mar -ru4mär -r3umd -rum3ei -4rumf -ru2mi -rum4mei -r4umpe -rum4ple -r4ums. -r3umt -r3umw -2r3una -2r3unb -4run4d3a -r4unde -run4d5eb -run4d5ei -run6demo -runden5e -run4dep -run6d5erf -run6d5erk -run6d5erl -run6d5ern -run6dero -run6d5ers -run6d5ert -run6d5erw -run6d5erz -run4do -rund3r -rund3s4 -run4due -4rundv -4rundz -runei4 -4r3unem -run4en -2runf -runft3 -4run4ga -4r5ungeh -run4gei -run4gel -run6gerg -4rungl -run4gla -run4gr -rungsauto6 -2r3u2ni -run4k5er4 -2r3unl -2runm -2runn -2runr -4runse -4runsi -2runt -runte4 -2runw -ru2p3a -2rupd -ru2p3h -rupt4o -2r1ur -ru2ra -ru2r3e -r2us -ru4sam -rus4s5er -3russi -3russl -rus4spa -rus4ste -rus6t5erf -rust3o -ru4ß3er -ru4t3ab -ru4tak -ru4t3an -ru4t3ap -ru4tau -rute2 -ru4tei -ru4t3el -rut5erf -2ru4to -rut3o4f -ru2t3r -rut6scha -2ruz -ru2zw -2rüb -rü4bc -rü4bk -rü4bs -rü4ckel -3rückg -rücks4 -rück5sa -rü4fet -rü2fo -rü4h3e4l -rüher4 -rüh3la -3rümpe -rün3f -rün3z2 -rüse2 -2rv -rve3a -rven3e -r4ventz -rve3s -rv2o -2rw -rwe4gel -r5weis -r2wo. -rwolf3 -2r1x -1ry -ry2ac -ry2c -ryl3e -ry3sth -2rz -rza2b -rz4aeh -r2z3af -r2zak -r2z3al -rz3am -rza2n -rz3and -rz3ano -r4z3ant -rz3anz -rz3app -r2z3ar -r2zas -r2zat -rz3aud -rzba4c -rz3ega -r3zel -r4zelem -rze4li -rzell4a -rzen3e -rzen6gen -rzen6ten -r4zentg -r4z5entl -r4z5ents -r4zentw -r4zentz -rz3epe -r4z3er4d -r4z5er5ei -r4z5erfo -r4z5erfr -rz5erfu -rz5erfü -r4z3erg -rzer4la -r6z5erreg -rz5er4sa -r4z5ersc -r5zert -r4z3erw -r4z3er4z -r3zes -r4z3ess -rzeu4g5i -rzeugin6n -r2z3id -r4z3ind -r4z3inf -rz3inh -r4z3inj -r4z3int -rz3inv -r5zirk -rzkop4 -rz5oele -rz2of -r2z3ot -rzten4g -rzt5ric -rzu4gl -rzum2 -r4zwar -r3zwec -r4zweis -r4zwet -r4z5wir -rz4wis -r2zwo -1sa -3s2aa -saat3s -2s1ab -sab2ä -5s4abbat -3sabel -sa4ben -sab5erk -3sabet -s3abh -sa4b3i4t -sa2bl -sa4bo. -sa4b3or -sa2br -sa3b4ru -sab4rü -s3abs -2sacc -sa5chal -3s4ache -sach3o4 -sachs4 -sach3t -3sachv -s2ack -sa4cki -sa3cr -2s3ada -sa4der -2s3adm -2s3a2dr -s2aeb -3saec -sa3e2d -3saeg -4s3aehn -4s3aelt -4s5aemte -4s3aend -4s5aengs -2s3aep -2saeq -s3aerm -3s2aet -4saeugi -3saeul -4s5aeuss -4s3aeuß -sa4fe. -2s3aff -sa3f2r -s2aft -saf4to -saf4t3r -saft3s4 -3sage. -sage3i -5sagen. -4s5a4gent -2s3agg -sa4gio -2s3a2gr -sah3l -sah3n4a -s2ahs -3s2ai -sai2b -sa3ik -sai4ka -sail2 -sai2r -s1ak -sa2ka -3sa3kar -3s4aki -2sakk -3sa3kl -3sako -2s3akt -sa4kus -2sakz -3s2al. -sa3la -sa4l3a4g -sa4lal -sa4lan -sa4lar -s4alat -sal4bo -3sald -3sale -sa4leb -sa4l5erb -sa4l5erf -sa4l5erk -3s2ali -sa4l3id -sali5er. -sal5ins -sal4le. -4s3alli -s3allt -sa4lor -3s2alp -sal4s3o -sal4tau -s3altä -s4alto -3s2alz -sal4zer -sal4zw -3sam. -sa4mae -sa4map -4s3a4mat -sa2mä -4s3ambu -3s2ame -4s3a4mei -samen5s -4s5a4meri -sa4m5ess -3samf -sa4min -3samk -s2amm -5samml -2s3amn -sa4m3oe -samp4 -4s3amph -3sams -sam3sc -sam4ti -sam4to -sam4tre -sam4tri -samt5s4k -samts4t -3samu -sa4nae -4s3anal -sa4nan -s4ana3t -sa2nä -4s3anbr -san2c -5s4and. -san4dab -san4dan -san4d5ar -san6d5ers -4s5andeu -san4dob -san4dot -san4dre -san4d5ri -3s4and5s4 -sa4n3er -4sanfa -4sanfä -3sang. -3sanie -4s5anker -sank4l -4s3anku -4s3ankü -2sanl -s3anm -s4anmu -2s3a2no -2s3ans2 -s4ans. -san5sp -4santr -4santw -2sanw -s1ap -sa2pe -2sapf -sapi3 -sa4pos -2sap3p -s2apr -2s1a2q -s1ar -3sar. -s2ara -2s3arb -2sarc -3s2ard -4sarea -3sarg. -s4arge -sar4gl -s4args -4sargu -4sa4rit -2sarl -2sarm -2sarr -s2ars -2sart -sarts4 -s2a2r3u2 -s2arv -s2arw -2sarz -2s3a2s2p -4s3asso -sa3s4tu -2s3a2sy -satan4 -sa4t5ant -sat3ei -4s3a4tem -2s3atl -2s3atm -sa4tol -sa2tr -sats2 -sa3t4su -4s3atta -4s3attr -3s4atz -sat4zei -sat4zel -sat4z5en -sat4zer5 -satz5ere -5satzk -s2au. -3s4auc -3s2aue -sau4ere -sau6erste -4s3aufb -sau5fra -4s3aufs -s3aufz -s5auges -sau4gla -sau4gle -sau4glo -3sau4g3r -sau2i -2s3auk -3s4aum -3s4aun -sau3n4a3 -s2aur -4s3aura -sauri5 -saurie4 -4s3ausb -4sausd -s5ausei -4s3ausf -4s3ausg -4s3ausl -4s3ausm -4s3ausn -s3ausp -4s3ausr -4s3auss -saussen6d -s5austau -4s5austr -s3ausu -s3ausü -4s3ausw -4s3ausz -2s3auß -sauto6rin -2s3a2xi -s4axo -sa2y1 -1sä -s2äb -3s2äck -3s2äg -säge3i -s1äh -2sähn -2s3ält -2s1äm -3säma -3säml -2s3änd -4s3ängs -2s1äp -2säq -s1är -s2är. -2sära -s2ärg -2särm -2särz -s2ät -s4äuge -4säugi -3säul -3s4äur -4s3äuss -2säuß -2sb2 -s3ba -sba4ne -sb4au -sbau6m5en -sbe3n4a -sbe3ra -sber4e -sbest5op -sbi2e -s3bl -sb4li -s3bo -s3br -sbue5be -sbü3be -1sc -2sc. -2scab -2scac -2scaf -2scal -2scam -s2can -2scar -2scas -2scat -s1ce -2scei -2s4ch. -4schabf -scha5cher. -s4chad -s4cha4e -4schaep -4schaeq -3schaf -s4chal -4schame -sch5ana -4schanc -4schang -4schanl -4schans -4schant -4schanw -4schao -4schap -5schar. -4schara -4scharc -sch5arme -4scharo -s4chauf -6schaufk -6schaufn -6schausf -6schauss -s2chä -4schäp -4schäq -2schb -2schc -2schd -s4che. -4schech -4schef. -4schefs -4sch5ei. -4scheim -6scheinm -schei4s -s4chel -4schemi -4schemp -6scheng. -6schenge. -6schentk -s4cher -scherin6s -scherin6t -sch5er6kl -scher6kran -6schernt -scher6trag -5scherü -4schess -4schex -2schf -2schg -2schh -s2chi -4sch3i4d -3schie -5schil -4schimi -4schinf -4schins -4schint -4schiru -2schj -2schk -4schlad -6schlaed -4schläd -4schle. -4schleb -4schleh -6schlein -4schlek -4schloc -4schlog -sch5lor -4schlöc -4schluf -6schmaes -6schmaeß -4schmas -4schmaß -4schmäs -4schmäß -4schmed -4schmes -4schmex -4schmod -4schmoh -4schmüh -4schmy -4schn. -4schnas -4schneb -4schneg -sch4nic -4schniv -4schnum -sc4ho -4schobj -scho4e -4schoer -scho4f -sch5ofe -4schoh -4schorc -sch5ord -sch5org -sch5orte -sch5ortu -4schox -4schök -4schöl -3schön -4schör -2schp -2schq -4schrad -4schrag -4schrap -4schras -4schräu -4schre. -4schrea -4schref -4schreg -4schreh -6schreif -4schrep -4schres -4schrin -4schris -4schrol -4schron -6schrote -4schrou -4schrus -4schrut -2schs -sch3s4k -schs4p -sch5sta -sch5s4ti -sch5str -sch5stu -2scht -sch3t4a -s4chu -4schuhu -4schumg -4schunf -4schunt -3schur -sch2ü -2schv -s4chw -4schwaa -4schwah -4schwas -4schwäl -4schwec -4schweg -4schwet -4schwid -2schz -2scj -2s1cl -2scoa -2scoc -2scod -2scom -2scon -4scoup -4scous -scre2 -s2cri -2scs -2scu -2scy -2sd2 -s2daq -sdaue4 -s3de -sde2a -s3di -sdi2a -sdi2e -sdien4s -s3do -s3dr -s3ds -s3du -sdue3b -sdü3b -1se -sea2n -se3at. -seauto6r -se4ben -seb2l -4s3echo -sechst5r -4s3echt -2s3eck -sed2 -sede3 -sedes4 -sedi4e -se4dik -se4dit -3see -se4e3i4g -see3na -se3end -see5n4e -4seeng -se3ent -se3enz -see3p2 -see3ra -se3erb -se3erd -se3erf -seer4i -se3erk -se3ern -see3ro -se3err -se3ers -se3ert -se3erz -see3sk -sefen4 -2s3eff -sef2l -4s3e4gal -segelei6 -3se2gl -se3gla -se3glä -se3glo -se2gn -s3e2go -seg2r -seh5abe -se4h3a4g -se4h3ak -se4h5ang -se4hap -se4heng -se6hense -sehen4t -seh5ente -se4hep -se4h5erf -se4h5er4k -se4h5erl -seh5ing -seh5int -seh3l -seh3na -seh3ra -seh3ri -seh3ro -seh3s -seh3t -se4h3ur -se4hüb -2s3ei. -2s3eic -4s3eid. -4s5eidec -4s5eides -4s3eids -4s5eifer -2s3eig -s4eih -s2eil -sei4l3a -sei4lei -4s3eime -se3imp -5s4ein. -4s3einb -4s3ein4e -seine5i -seinen4 -4s5einfl -se5in4fo -4s5einfu -4s5einfü -4s3eing -sein4ha -4s5einhe -4s3eini -4s3eink -4s3einm -s3einn -se5inne -4s3einr -s5einsat -s5einsät -4s5einsc -s5einspa -sein6stit -sein6str -s5einstu -se5in6ten -sein6ter -4s5eintr -4s3einw -4s3einz -2s3eis -3s4eism -3s2eit -2sekz -s2el. -se4l3ac -sela4n -se4l5ano -se4lanz -se4l3at -se4l3au -s2elb -4selbis -selbst5e -selbst5r -sel3d4 -sel3e4c -se4lef -4s3e4leg -se4l5eig -6selektr -4seleme -selen5e -se4l5ent -sel5erd -se4l5ere -seler5ei -se6l5erfi -se4l5erk -se4l5erl -sel5erm -4s3elf. -4s3elfm -sel5ind -sel5inf -sel5inn -se6l5inst -4s3elix -selk2 -3s2eln -se4l3ob -3s2els -sel5skl -sel5sko -sel5stem -sel5sti -sel3sz -seltex4 -se2lü -selz2 -4s3e4mig -2s3emp -s4en. -sena4b -se4na4g -se4nah -sena4r -sen3au -se5n4au. -se2nä -5sendel -senderin6f -sen5eck -se5neg -senei4l -se4nel -sen5ende -senen5e -se4nep -se6n5erei -se4nerf -4senerg -se4n5erk -se4n5ers -se4nerw -se4ness -se4n3e4v -s2enf -sen5fun -seng4le -4sengp -senhaus6 -3s2eni -se4n3im -se4n3in -6s5en6keli -sen6kero -sen4k5ni -senks4 -sen3ob -se4noe -se4n5ott -se4noz -s4ensa -sensch4 -sen4s5e4h -sen6s5ers -sen4s5i4d -sen4s5in -sen5s6lan -3s4enso -senst4 -sens6temp -4s3entd -sen6tenr -4s3entf -4s3entg -4sentla -4s3entn -sen6t5ric -4s3ents -4sentw -4sentz -se4n3un -sen4zun -sen4zym -seo2b -se3of -se3oh -seo2r -se4pen -se3p2f -s3epis -se4pit -s2er. -se3ra. -ser5adl -ser3af -ser5age -se3rai -ser3a4k -ser3al -ser3am -se5rand -ser5anz -5serat -ser5äus -serb2 -s5erbe. -se4r3e4b -ser5eck -serei4f -6s5ereign -se4rein -serei5s6t -ser5eli -sere4m -ser5emi -3s4eren -ser5end -se4r5en5e -se4r5enk -se4r5ens -ser5enth -se4r5erb -serer5e -se4r5erh -ser5ersc -se4r5erw -4s3ergä -6sergebn -ser5ha -4s3er4hi -4s3erhö -se4r3id -3serie -ser5inner -serk2 -6s5erkran -4s5erläs -4s5er4le. -4s5er4len -4s5ermah -ser3na -4s5ernte -ser3o4b -ser3oc -se3rod -4s5eroef -se5roel -4s3eros -4s3erot -4s3eröf -se3röl -s4ers. -sersau4 -4s5erseh -ser6sehn -sers4k -ser4um. -se5rumm -3s4erv -se2ry -3ses. -sesch4 -4s3e4sel -se5skan -4s5eskap -se3so -ses2p -4s3essa -s4essel -sest2 -se3sta -se5stec -se5stei -se5stel -se5stemp -se3sti -se3sto -se3stu -3s2et. -4s3e4tap -4s3e4thi -4s3e4tho -4s3e4tik -set3sp -se3tun -3setz -set4zei -seu2f -se3ufe -se3uh -2s3eul -se3um2 -seums3 -se3uni -2s3eup -seu2t -se3ute -se3uto -4s3e4van -s1ex -3s4ex. -4sex4am -se4xen -2s3exk -s2exo -2sex2p -sext4an -sex5ti -4sextr -sext6ra -3s4exu -2sex2z -1sé -2s1f2 -s3fa -sfal6l5er -s3fe -sfen2 -sfi4le. -sfin4s -s3fl -sf4la -s3fo -s3fö -2sg2 -s3ga -sgangs5 -sga2t -s3gä -s3ge -sgeha4 -sge6habe -sge3lu -sge3n4a -s3gi -s3gm -s3go -s3gö -s3gr -2s1h -3s2ha. -s3hae -s4ha4ke. -shal4li -shalt4s5 -3s4hamp -s4has. -s3hä -3shi. -sh4ia -3s2hib -3s2hik -3s4hip -3s4hirt -sh1l -sh1n -sh2o -s3hoc -s3hoe -s4hoe. -s3hof -3s4hoo -3s2ho2p3 -sho4re -s4horts -3s4ho2w3 -s3hö -sh2r -sh4re -sh2u -sh2ü -1si -si5ach. -si3am. -si3as -sia4s3e -2siat -sib2 -3sic -sichts5te -sicker5e -4s3i4dea -4s3idee -4s3ideo -si3der -2si2dy -s2ie -sie4b3a -siebens6 -sie4b3r -sie4bu -si2ed -sie4gl -sie4g3r -sie5r4e -sies2 -si1f2 -s2ig -si2g3a2 -si2gä -si4g3ei -s3i4gel -si2gl -sig2n -sig4nu -si2g3r -sigs4e -sigs4p -sig5sto -si4k3ab -si4kad -si4k3ae -si4k3a4g -si4k3ak -si4kalb -si4kana -si4kanb -si4k5ans -si6kanth -si4kanw -si4k3ar -sik5asp -sik5ass -si2k3ä -si4k3el -si4k5ens -si4k5ent -sikerbe6 -si6k5erei -sik5erfo -si6k5erke -si4k5er4z -si2k3i2 -sikin3 -si2k3l -sik5le -si2k3n -siko3n -siko3s -si2kö -si2k3r -sik3s -sikt2 -si2ku -si3kum -si2kü -s2il -3silb -silber5e -sil4br -sil3c -3sili -2s3ill -si4mag -2s3imb -2s3imm -4s3impf -4simpu -sim4st -3s4imu -si3n2a -si5nau -4s3indu -sines4 -4s3infe -4s5infor -4s3infr -sin4gae -sin4gan -sin4gar -sin5g6le. -sin4glo -sin4g3r -sings4 -sing5sa -sing5sc -sing5so -2s3inh -sini5er. -4s3i4n3it -s2ink -3sinn. -s5innenm -4s5inner4 -sin6n5erf -3sinnl -4s3inno -3sinns -2s3inq -2s3ins -3s4ins. -s4into -2s3inv -2s3inz -si4onm -si3os -3sip -s2ir -2s3irr -si4sam -3sisc -si4scha -si4schl -si4schn -si4schu -si4schw -si2s3e2 -sisen3 -sis3i -si4sin -si4sis -sismu4 -s3i2so -sis3or -siss2 -3s4i2st -sis3te -si2su -3s2it -si4tal -si4tau -si4tra -si4t3re -sit3s -sit4zel -sit6z5erm -siv3a -si4val -si4van -sive3 -si4v5er4f -si4v5er4t -si4vin -siv3o2 -si4vor -si2vr -1sí -2sj -s3jo -s1k2 -2sk. -2skab -2skae -ska3la -4skalk -2skam -5skanda -2skap -2s3kar -2s3kas -ska4s3p -4skata -ska4te. -4skateg -s4kater -ska4tes -ska4to -ska4t3r -2skau -2skä -2skb -2skei -ske4l3i -2ske2n -sken3e -3s2kep -ske3ra -s4ketc -s3kh -3s2ki. -s2kia -3s2kif -s2kig -3s2kik -2skin -s2kip -4skirc -3s4kire -3s4kis. -3skiv -3skiz -2skk -s3klam -4sklas -s4klav -5sklave -2sklu -2skn -4skoe -sko3ef -2skoh -2skol -3s4koli -4s3kom -4s3kon -3skop. -s4kopa -sko4par -s4kope -s4ko4pl -s4ko4pr -2skow -2skö -2skr -s3kra -skraf4 -s3kre -3s4krib -s4krip -s3kro -2sk3s -sk3t -skto2 -2sku -s3kue -3skulp -s3kun -s3kur -2s3kü -2skv -2s1l2 -s3lab -s3lae -s3lag -3slal -sla4ma -sland5a -sland6s5o -slap3 -4s3lar -3s4lash -sla3sp -s3l4au -sla4ve -s3lä -sl3b -s3le -sle4gas -slers4 -s3lic -s2lip -sl4ob -slo5be -s3loc -3s4loga -3s2low -s3lö -sl4um -2s1m2 -s3ma -smab2 -sma3la -s4mall -sm4an -sm4ar -smar4k5n -sma3sp -smen4t5i -s3mi -smi2e -smi2t3 -smitte4 -3s2mok -s3mu -smusiker6f -smut4ti -s3mü -2s1n -sn2a -snab2 -s4nack -snae4s3 -snä2s3 -s3n2e -s3ni -sni5er. -sni5ers -sn4ob -snus2 -1so -2s3oas -s1ob -s2oba -so4bel -4so4ber -2sobj -so2b3l -2sobm -4so4boe -so2br -3s2o1c -s4oda -sodi2 -so2do -so3e2b -2s3oe2d -soe2f -2s3oek -2s3oes -so3et -so4fen -s4off. -4s3offi -3s2oft -sog2l -s2oh -2s3o2he -3s4ohl -sohle4 -sohlen5 -4s3ohng -2s3ohr -2s3okt -3sol. -so3la -3solan -so4l3au -sol3c -3sold -sol4d5er -3sole -so4l3ei -4s3o4lig -sol4l3a -sol4l5er -3solm -so3lo -solo3p -solo3s -3sols -so3lu -2s3o2ly -2s3omb -some4n3 -3somm -s2on -son6anz -sonan6z5ei -so4nau -3sond -sone2 -so4n3eh -so4ner -son4ga -3sonn -son5ori -sons4a -son4s3o -so1o -2s3o2pe -2s3opf -3s4opr -sop3s2 -2sopt -4s3orat -2s3ord -sore2 -so4rei -so4r3el -so4rem -so4rer -4s3orga -sorgi3 -4s3o4rie -so2r3o2 -sor4t5ae -sor4tau -sor4t3ä -4s5ortes -4s3orth -5sorti -sort4st -sose4 -2s3osm -so3s2p -3s2oss -so3sta -s3osth -2s3osz -3so3ß -soun4d3 -3sour -2s3out -2s1ox -s2oz -2s3o2ze -sö2c -2s1ö2d -s1ö2f -s3öffn -1söh -2s1ök -2s1öl -2s1ös -sp2 -2sp. -4spaa -3s4pace -4spack -3spaen -4spaer -3spaet -s4paga -4spage -spa4ge. -4spago -2spak -4spala -4spalä -4spale -s4pali -4spalm -5spalt -spal4t5a -spa2m -s2p4an -5spannu -4spano -5spant. -4spanth -4s3panz -2spap -sp4ar -4spara -spa4rap -s4pard -3sparr -5s6parten -4spartn -4sparty -4spassi -3spaß -3s4pat. -5spatel -4spati -4spatr -3s4pats -2spau -s2paz -5spazie -2späd -3spän -2spär -3spät -2spc -2spe. -s2pea -3s4pedi -s2pee -2speg -3speic -4spein -3speis -s4pek -4spensi -spen6ster -4s3perf -3sperg -s3peri -4sperle -s3pern -s4perr -sper6r5ei -4s3pers -4speru -4sperü -3s4pese -2s3pet -3spex -3s2pez -2s3pf -5sphaer -s4phal -s3phan -4s3phas -s2phä -4sphän -3sphär -3s4phin -2sphy -1spi -2spia -3s2pi4e -4spiep -4spier -spier4r -2spig -spi2k -2s3pil -4sping -3s4pinn -2spip -s2pir -4spiro -2spis -2s3pla -2s3plä -4sple -sp3le. -3s2pli -2s3plu -2s3pn -2spod -2spog -3s2poh -3spoi -2s3pok -4spol -3s2pom -3s4pons -2spoo -2spop -spor6tag -spor4tu -4sporz -2s3pos -4spote -4spoti -s4pots -s2pö -4spr. -3sprac -4spraeg -4sprael -4spraem -s4praen -4spraep -4spraes -4spraev -4spraez -4sprak -3s4pran -4sprax -3spray -s3präd -4spräg -4spräl -4spräm -s4prän -4spräp -4spräs -4spräv -4spräz -3s4prec -4spred -4spreis -5spreiz -3s4pren -4spres -s4pric -5spring -5sprint -4sprinz -4spriv -4sprob -4sprod -4sprof -4sprog -4sproj -4sprom -4sprop -5spross -4sprot -4sprov -4sproz -3spruc -5spruec -4spruef -3sprüc -4sprüf -3sprüh -3sprün -2s3ps -s4ps. -sp3t -4spub -2spuf -3spuk -4spulv -3spund -4spunk -2spup -3s2pur -spu4rer -4spurpu -2sput -1spü -s2pür -2s3py -2sq -s3qu -2s1r2 -s3ra -srat4s5t -s3rä -s3re -sre5cha -s3rh -sroe4s -sroes5c -sro3tu -srö2s -srös3c -s3ru -sruecker6 -s3rü -srücker6 -2s1s -s3s4aba -ssa5ble -ssa3bo -s4sach. -s2s3aj -ss5alar -s4s3amb -s5samm -s4s3amp -s5san. -s4sanf -s4s3ang -s4sanh -s4sanp -s4s3anz -ssa4ro -ssa2s -s4s3asi -s4s3ass -ssa3s4t -ss3att -ss3aud -s4s3au4f -ssau4n -ssau4r -ssau5ri -s3sät -s3s2äu -ss2ca -s2sce -ssch2 -s6scheid. -sscheiner6 -ss4chem -s2sco -sse3a -s4s5eben -sse4ck -sse3ec -s3seh -ss3eib -sse5in4d -sse5inha -ss5einl -sse5inte -s4sekä -s4selem -sse6l5end -sse4l5oe -sse4lö -s3sen -s4s5endl -sse4n5e4b -sse6nend -sse6nene -sse6n5ent -sse6n5er5i -sseng4 -ssengene6 -sse4nid -ssenk4 -ssen6kel. -ssen6keln -ssen6kels -sse4no -s6sen6sem -ssen6te. -sser5a4d -sser5as -sse6r5att -sse6r5ent -sse4rep -sser6fahru -sser6gebn -s6s5erker -ss5ernt -sser4öf -sses4sa -sses6sen -s4s5estr -ssex2 -s4sexa -ssgene6ri -s3s4hut -ssicht6s -s3sie -ssi3es -ssi4ken -ss3i4ko -s4s3imp -s4s3ind -ssing5s -s4s3int -s4s3isr -s2sk -s5skala -ss3li -s4s3oef -ss3oel -ss3ofe -ss3off -ssoi2 -s4sonk -sso5ral -s5sorde -s4s5orgi -s2söf -ss4pec -s2spl -s4spro -ssquet4 -ss3s2 -sssau4 -ss5spr -s4st. -sst2a -s3stab -s5stad -s3stae -s3stah -ss3tak -s3stan -ss5tank -s5s4tap -s3stau -ss5tauc -s3stä -ss5täti -s5stein -ss4teng -s3s4tep -ss4tern -ss3tet -s3steu -ss5teur -ss3tho -s3stie -ss4tin -ss4toer -s3stop -s3stöp -s5stör -s3str -ss4tras -ss4trat -s4strie -sstro6ma -s3stu -s3stü -s2sty -s4sumf -s4sumg -s4sumk -s4sumr -ss5umsc -s4s5umse -s4suns -s4sunw -s3sup -s4s3urk -s2sz -2st. -s2ta -s3ta. -3staa -3stab. -4stabb -4stabd -4stabel -5stabes -4stabfl -4stabh -3stabi -4stabit -4stabl -sta4bli -st3abo -4stabr -4stabt -4stabu -4stabz -st2ac -4stada -3stadi -4stadj -4stadm -4stadr -3stadt -3s4taeb -4staebt -3staed -4staeg -4staehn -3staem -6staenz -4staep -st5aerm -5staett -4staeus -4staeuß -4stafel -5stafet -st3afr -2stag -s4tagg -s4tagl -3s4tagr -stai2 -2stak -st3aka -3staks -s3tal. -4stala -sta5lak -st3alg -stalle4 -stal6leb -stall6ta -st3alm -st3alp -st3alr -st3alv -4stame -st3a4mi -3stamm -stam4ma -3stan. -4st3ana -4stanb -3stand -3stane -4stanf -5stange. -6stangeb -6stangek -st5angel -st5anges -4stangr -4stanl -4st3anm -4stann -st3ano -3stanp -st5ansa -st5ansä -4stanw -st3a2o -3stapl -stapo3 -stapos6 -4st3app -3s4tar. -6starbei -4stari -4starn -3s4tars -star4ta -4starti -4starz -2stas -3stase -stas4tr -4statb -5statis -st3atl -st3ato -4staubh -4stauf -st5aufb -st5aufk -st5aufs -st5auft -4s3taug -3staup -5s4taur -4stausc -stau6scha -4stausg -4stauss -staussen6 -4stauto -3stauu -2stav -2s3tax -1s2tä -s4täb -3städ -2s3täf -2stäg -4stähn -4stäle -4stält -s4tänd -4stänz -2stäp -st3ärm -st3ärz -4stäte -3stätt -4stäus -4stäuß -2stb -2stc -st3ch -2std -4s3te. -3ste4ak -4steam -4stean -5s4teche -ste4chi -4stechn -ste2d -st3edi -5stedt -2stee -s2teg -3steg. -3stege -ste4gra -ste4gre -3stegs -3steh -4stehr -4s3teic -4s4t3eid -3s4teif -st5eifr -s4teig -5steiga -stei4gr -5steigu -steil5a -4steilc -4steils -4steilu -6steindr -ste5i4sc -s4t5eise -s4t3eiw -2stek -s2tel -4stel. -4stela -4stelb -4stelef -4steleg -s5telei -st5elek -4stelem -4steleo -4stelep -ste4ler4 -st5elev -s3telf -stel6laen -stel6l5än -4stelm -4steln -stel5oe -ste4lö -4stels -4stem. -4stema -ste4m5ac -ste4mei -ste6m5ent -ste4mer -4stemi -ste4min -6stemper -4stempf -ste4mu -2sten -stende6ta -s4t5ends -stenei6l -stens4 -st5entd -s4t5entf -s4t5entg -s4t5entl -s4tents -s4tentw -sten6zerf -s4tepi -4stepr -4s5ter. -4stera -ster5al -4sterba -s6terben -st5erbrin -4sterd -4stere. -ste4rec -4sterei -ster5eif -ste4rel -4sterem -4s5teren -5stereo -ste4rep -4sterer -4steres -4sterf -st5erfin -4sterg -ster6gie -4sterh -s4terit -4ster5k -4sterl -4sterm -st6ern. -4sterna -4sterö -4sterp -4sterr -4sters -4stert -4steru -4sterw -4sterz -ster4za -ster4zä -s4t5er4zo -2stes -stese4 -stes5er -stes6se. -ste4st -stes5ta -s4testn -stes5tr -2stet -ste4tab -ste4tag -3s4teti -3s4tett -s2teu -3steue -4steul -st5eura -2stev -st3eva -st3e4vi -2stex -s4texa -s4t5extr -2stf -2stg -2sth -st3heb -st3hee -st3hel -st5herd -s5therm -s4theu -st3hex -s2t3hi -s2t3ho -s2thu -2stia -2stib -s2tic -3stich -6sticket -2stid -4stie. -4stief. -4stiefl -s4tieh -sti4el -stie6lei -4stien -6stiere. -3stieß -3s2tif -2stig -st5i4gel -2s3tik -sti4kam -sti4kan -sti4kar -sti4kas -sti6k5ere -sti4kl -sti4kr -s2til -3stil. -sti4l3a -s3tild -3stile -3stils -s2tim -3stimm -4stimp -sti3na -st3inb -s4t3ind -4s4t3inf -s4t3inh -s4tink -st5inka -5stinke -st5inko -st3ins -s4tinsz -4s4t3int -2stio -4stip. -s4tipe -4stipps -4stips -s2ti2r -st3iri -3stirn -4st3iro -4stis -sti6scha -st3iso -st3isr -st3ita -4stite -4stiti -5stitui -2stiv -stiva4 -stiz3a -2stj -2stk -st5kan -2stl -2stm -2stn -4sto. -4st3o4bl -4stocht -2stod -4stoech -4s4t3oef -4stoen -4stoepf -3stoer -s4toeru -s4toes -3stoeß -4stoet -3stof -6stoffiz -s2toh -4stoil -s4tois -3stoll -4ston. -sto4ne. -4stonn -4stons -4stoo -s4t3ope -4stopf. -4s5topo -3stopp -4stor. -s4torc -4stord -4stores -4storf -4storg -4stori -s4torn -4storp -4stors -4stort -4storw -s4tory -s2tos -3stoss -stost5r -st3osz -3stoß -4stota -4s3tote -s4tott -4stou -1s2tö -4stöch -3stöck -2stöf -2s3t2öl -2stön -4stöpf -2stöt -2stp -2stq -4strad -4s5traec -4straeg -6straeum -stra4f5a -s4trafe -s4traft -4strag -4strahi -5st6rahl -4strai -4strak -4stral -stra5la -6strangi -5strangs -s5trank -4strans -st5rast -3straß -4straum -4s3träc -4sträg -3strän -3sträs -3sträß -4sträum -4stre. -4strech -4stref -4streg -4s5treib -5streif -s4t5reis -4streno -4strep -4streso -s4tress -st5rest -4stret -6streuem -6streues -4streuh -5s4treuu -4strez -4strib -4strieb -strien4 -strie5s4 -4strig -4strim -4strink -4s5trio -5s4tripp -4stris -4striu -st2ro -4stroc -4stroep -3s4troh -4stroll -s4troma -5s4trome -5stromk -4stropf -4stros -s4t5rose -4strou -4ströp -s3trös -4strua -3s4trud -6struemm -4struh -3struk -4strup -4strümm -2st3s2 -sts4a -stsau4 -st4sb -stsch4 -sts4i -2st3t2 -4s5tuch -3stud -4s5tuech -5s4tueck -3stueh -stu4el -3s4tuen -4stuer. -4stuere -5stuetz -3stuf -st3uga -3stuh -s2t3uk -4stumf -4stumo -4stums -stum4st -4stun. -st3unb -3stund -4stune -4s4tunf -4stung -4stunn -4stuns -4stunt -4s3turb -stu5re -sturi3 -s4turk -st3url -stur6mei -4s5turn -4sturs -4st3urt -3s4turz -2s3tus -3s2tut -1s2tü -4stübu -4stüch -3stüh -4stür. -4stürc -4stüre -4stürg -4stürs -4stürw -3stürz -3stüt -4stütc -2stv -2stw -stwa5re -s2tya -3s2tyl -2stz2 -1su -su3al. -su3an -su2b3 -suba2 -4subi -subs2 -3suc -such3o -such6s5ende -such6spe -such6ters -sucht4s -3sud -2sueb -3suec -sue2d3 -sueden4 -3suef -su3ell -3suen -3suep -3sues -3sueß -su3ez -su2f -2s3ufe -su3fi -sugge4s -2s1uh -3suhl -su3is -su3it. -su2k -s2ul -su3la -su3li -3sulta -su4m3an -su4mar -su4mau -su4mei -su4m3el -sument4 -su6m5ents -su6mentw -su4m3et -4sumfa -4sumfe -su4m3id -su4m3in -4sumla -3s4umm -3s2ump -4s3ums4t -su2n -2s3una -sunder4 -sun6d5erh -sun6d5ess -sunds4 -2s3une -2s3unf -6s5ungena -4s3ungl -2s3uni -2s3unl -2sunm -4sunsi -2sunt -2sunv -s4unwa -4sunwe -2supd -3sup3p -su4ran -su4rau -su4r3ei -su4r3er -3s4urf -surfs4 -sur5ins -4s3urku -su2r3o2 -s5urspr -s3urw -sus3e -su3shi -sus3i -s1ut -su2te -su2to -1sü -2süb -sü4be -3sü2d1 -süden4 -3sün -s2üs -2sv2 -s3ve -svie2 -s3vo -svoran6s -2sw -s3we -swe6gers -sweg3s -sweh2 -s3wi -4s5wink -s4wist -1sy -s1ya2 -sy3ab -syl1 -3syli -sy2lo -sy2lu -sym3 -sy2n3 -3synd -2syo -3sys -sy3sto -sz2 -2s3za -2s3zä -2sz4ei -szeits4 -2szel -3s4zena -s4zend -s4ze3n4e -s4zeni -4szent -2szer -sze3ra -2szet -2szeu -3s2zew -2szie -4szins -s4zint -3s2zis -2szo -s3zs -sz3ta -2s3zu -sz4un -szu3ni -2s3zü -2szw -2szy -1ß -2ß1a2 -ß3ac -2ß1ä -2ßb2 -ßbal4li -ßbau4c -ßbrau4 -2ß1c -2ßd2 -ße2be -2ß1ec -2ß1ef -2ß3e2g -2ß1ei -2ß1ek -ße2l3a2 -4ß3e4lek -ßels2 -ßel3sk -2ß3emp -ß4en. -ße2na2 -ßene2 -4ß5energ -ßeng2 -ße2ni -ßenk2 -ße2n3o2 -ßen3s4k -ßens4l -ßens4p -ßens4t -ßen4te. -4ß3entf -4ß3entl -4ß3ents -4ßentw -4ßentz -2ß1e2p -ßer3a -ßerei5s4 -4ß5er4geb -ßerk2 -4ß3er4ke -ßer4le -ßermen4 -4ß3ersa -ßer4seh -ßers4k -2ß3er2z -2ß3es4s -4ß3estr -2ß1ex -2ß1f2 -2ßg2 -ß3gm -2ß1h -ßi2g3a2 -ßigs4e -2ß1i2k -2ß1il -2ß1im -2ß1in -2ß1i2o -2ßj -2ß1k2 -ß3kh -2ß1l -ßlers4 -ßli1 -2ß1m -ßmau2 -ßmut2 -2ß1n -2ß1o2 -ßoe2 -ßos2 -2ß1ö2 -2ß1p2 -2ßq -2ß1r -ßroe4s3 -ßrö2s3 -ß3ru -2ß1s2 -ßsau2 -ßsch2 -ßst2 -2ßt -ß2t3in -ßt2ö -ßtu5er. -ßtu5ers -2ß1ue -2ß1um -ßums2 -2ß3unf -2ßunt -2ßunv -2ß1ü -2ßv -ßvie2 -2ßw -2ßz2 -ß3zs -1t -3taan -ta3ar -4taat -t2aba -ta4b3an -2t3abb -2tabd -3tabel -2tabf -2tabg -2tabh -4t3a4b3i4t -2t3abk -4t3abla -4t3a4blä -5tablet -4t3ablö -2tabm -t3abme -2tabn -tab5ort -2ta2br -2tabs -2t3abt -3tabu -ta4bue -tabu5s -ta2bü -2tabw -taby2 -2tabz -2t1ac -t2a3ce -t4ache -ta3chi -ta3cho -tach4r -4tachs -ta3chu -3tacl -3tacu -2t3add -4t3a4der -tadi3o4 -t3adm -t3a4dol -2t3a2dr -ta3d2s -4tadt -tad4t3a4 -2tadv -ta2e -t3aebt -tae2d -ta3ede -ta3edr -3t2aef -t3aega -3taegi -3taegl -4taegy -2taeh -t3aehn -ta3ei -3taele -4t3aelt -2tae2m -t3aemt -t5aenderu -4taeng -ta3ent -3taenz -2t3aep -2taeq -tae6rers -tae4rob -t2aet -3taete -ta3eth -taet4s5t -t3aetz -4t3aeug -5taeusc -t5aeuss -4t3aeuß -2t3aex -3taf. -3tafa -3tafel -2taff -t3afg -ta3flu -ta3fra -3t2ag -t4ag. -ta2g3a2 -ta4g3ei -6tagent. -tage4si -t3agg -ta4git -ta5gla -ta4glo -4tagnat -tagse4 -tags5ta -2tahl -tah4ler -tah4li -tahls4t -tahl5ste -tah4l3u -3t2ai -ta3i2k -tai2l3 -tai4ne. -ta3ins -ta3ir. -ta2ka -2takk -ta3kla -2t3akq -ta4kro -tak4tel -tak6t5erf -tak6t5er6z -5taktik -tak4t5in -4t5aktiv -tak4t3o4 -tak4t3r -tak4tum -2takz -t2al. -tal3a4b -tal3ac -ta3lad -ta3lag -tal3a4s -t2alb -t3alb. -4t5alben -t3albk -3talbr -ta4leb -talem4 -tal5emb -ta4l5emi -3talen -ta4l5en4d -ta4l5eng -ta4l5en4s -talent6n -3taler -ta4l5er4f -ta4l5erg -tal5erha -tal5ersa -ta4l5ert -ta4let -ta4leu -ta4lev -tal4ga -t3algo -3talia -tali6ene -3talis -3talke -tal4l5ac -tal4lan -tal6l5auf -tal6l5ein -tal6l5erh -tal6lerm -tall3s4 -t5allta -4t3alm. -talmi4 -3talo -ta4lob -ta4lop -ta4l3o4r -ta3lö -talp2 -3t2als -tal4sae -4talter -talt4s -3t4am. -t4amar -t3a4mei -3tamen -4t3a4mer -5ta5mix -tam4mei -tam6mense -tam4m5er -tam4mi -tam4m3o -2t3a2mö -t3ampl -t3ampu -3t4ams -2t3amt -3tamu -ta2na -4t3anal -ta2nä -t4anbe -4tanbl -3tanbu -3tan2c -tan4dan -tan6d5erk -tan4dre -tan4d5ri -tand4s5a -tand6sen -tand4sk -tand4st -ta5neie -tan5erfa -2tanf -4tangab -tan4gar -5tango. -5tangos -t3ang4r -tan4gra -4tanhä -t4ani -t4anj -tan4kan -tan6k5er6k -tan4klo -3tanks -4t3ankü -2tanl -3t4anne -tano2 -ta4nod -4t3a4nom -t3anon -ta4nop -ta4nor -4t5ansam -4t5ansat -t3ansc -4tanspr -4tanstr -t4ant. -tan4tan -4tantei -5tantenf -5tantenp -t5antig -5tantis -4tanwae -4tanwal -4tanwä -t4anz. -tan4zar -tan6z5erh -tan6z5erm -tan6z5erz -4t3anzu -tan4z5un -4t3anzü -tan4zw -3ta4pe. -ta4pes -2tapf -ta2pl -ta4poka -t3app. -4tappa -t3appr -ta2ra -4t5arabi -3tar3al -4t3aram -ta5rant -tar3ap -ta3ras -tarb2 -2tarc -5tarchl -5tarchr -3tard -4t3area -tar5ein -ta4r3el -ta4r5en4d -ta4rer -tar5ere -tar5ers -tar4hei -3tari -tar5ins -tar4kn -4t3arm. -t3armb -4t5arme. -4t5armee -6t5armen. -6tarmeni -4t3arms -3tarn -t3a4rom -ta4rop -4tarot -ta3rö -2tart -tar4tau -tart5e4c -tar6t5eig -tar6t5ere -t5artik -tar4t5in -tar4t3o -tar6t5rau -tar4tre -tar6t5ric -tart3s -3tartü -3tarty -ta3rum -4tarzt -3tas. -3t2asc -tas4k3l -ta3spr -ta4s3s -4t3assi -4tasso -3t2ast -ta4ste -tast5ei -tas4tin -tas4to -2t3as2z -2t1aß -t4at. -ta4t3an -ta4t5arm -3tatb -t2ate -ta4te3c -tat5ein -ta4tem -ta4t5ent -ta4t3er -ta4t3hi -4t3atom -tat5ort -tat5ric -ta4tro -3tatsa -4tatta -tatt5an -4tatti -3tatur -tat3zw -tau4b3a -tau4bei -3taubh -tau4b3r -t4auc -tauchs4 -t3audi -tau3e -tauer4l -tau4fak -t5aufar -4t5aufba -t3aufd -taufe4 -5t6aufe. -4t3auff -4t3aufg -t3aufh -4t5aufko -4t3aufl -tau5f6lie -4t3aufm -4taufn -4t3aufp -4t3aufr -taufs4 -4t5aufsi -4t3aufw -4t3aufz -3taugl -t3augm -2tauk -t3aukt -t3aul -3taume -4t3ausb -3tausc -tau6schei -tau6schm -tau6schn -tau6schr -tau6sch5w -4tausd -4t5ausei -5tausen -4t3ausf -4t3ausg -4t3aush -t3ausk -4t3ausl -t3ausn -4t3aus3p -4t3ausr -4t3auss -4t5austa -4t3ausu -4t3ausü -4t3ausw -4t3ausz -3tav. -3tavs -taxi3s -2t3axt -3taz -t3äbt -tä1c -t2äf -3tägi -3tägl -2tägy -2täh -t3ähn -3täle -2t3ält -2tä2m -t3ämt -t5änderu -2täng -t3ängs -3tänz -2t1äp -2täq -tä4reng -tä4r5ers -tärm2 -tä2ru -t2ät -3täte -5tätigk -4tätt -2t3ätz -2täug -3täusc -t3äuss -2täuß -2täx -2tb2 -tbahner5 -tbauer4 -tbau3f -tbau6m5en -tber4e -t3bh -tblocken6 -tbote4 -tb4ra -t3bs -tby2t -2t1c -tchar4m -tche4n -tch4i -tch3s -tch3w -t2ck -tcor2 -tcre2 -tcu2 -2td2 -tdar4m3 -tde2a -tde3s4t -tdi2e -t3ds -tea2 -2teak3 -3te2am -tean3 -3teba -t3e4ben -t5e4ber. -t2ec -4techd -4techk -4techm -5techn -4t3echo -4tech3s -4techt -2teck -tecke4 -te4cki -tecks4 -2t3ecu -te4del -tedi4e -te4dit -ted2r -2tedt -tee3id -te3ele -te3end -te4er. -tee3r4a -te3erl -te3erz -te4es -teesen4 -tee3t -4tefeu -2teff -te5ge. -tegen4 -te5gen. -te4g5end -teger4n -2t3egg -teg2l -3te3gr -teg5rae -teg5ran -te4grä -2teh -3teha -te4h3ac -te4hap -3tehä -te4hef -te4hep -3tehi -te4him -teh3la -3teho -3tehö -t3ehr -teh3ra -3tei. -t5eifers -tei4gab -teigerin6s -tei4gla -tei4gle -4t3eign -teik2 -t2eil -tei4lau -teilen6s -tei6l5ent -tei6l5er6h -tei6l5ero -tei6lerst -5teiln -2tein -tein5al -tein3d -tei4n5e4b -tein5e4c -tei6nen6s -tei6n5erf -tei6nerk -tein6hab -tein6sel -tein6spo -tein6tol -4t3eis. -t3eisb -4t3eish -4t3eisk -4teiss -tei5su -2teiß -tei3t -teiz2 -2tekz -3tel. -3tela -te4l3ab -te4l3ac -tel5aer -te4lam -tela4n -tel5an5e -te4lant -te4lat -te4l3au -te4l3är -3telbe -3telbr -3telbu -3telc -3tel3d4 -te3le. -tel5ech -tel5e4ck -3telef -3teleg -te4l5ei4c -te6l5eier -te4l5eig -tel5eint -5telema -tel5en5e -tel5eng -te4l5ent -3teleo -3telep -te4l5erb -te4l5erd -te4l5erf -te4l5erg -te4l5erh -te4l5erk -te4l5ern -te4l5ers -3teles -te4l5eti -te4leu -3telex -4t3elf. -4telfd -4t3elft -3telg -3telh -tel3id -te4lim -tel5ind -tel5inf -tel5inn -tel5ita -3telk2 -tel6lanf -tel6lant -tel6lein -tel5ler -tellerei6g -3tellö -3telm -3teln -3telo -telo4g3 -te4l3or -3telp2 -3telq -3telr -3tels -tel3s4k -3telt2 -3telu -te4lut -3telv -3telw -3telzy -t2em -3tem. -te4mak -3temal -te4mant -te4marc -tem5art -te4m5att -te4mau -4temäc -4t3embl -tem3eb -tem5ein -tem5er5e -tem5erf -tem5erg -tem5erh -tem5ern -tem5ers -tem5erw -te4m3i4d -te4m3i4m -tem5ing -te4miss -4temms -4temmt -te4mop -te4m3o4r -6tempelt -5temper -4t3empf -4temph -tem4pli -tempo3 -tems2 -temt2 -te4mun -te4m3ur -3t4en. -te4na4b -tena4d -ten5aeh -te4na4g -te4nan -te4n3a4r -ten3au -te2nä -ten3äh -4t5endal -4t5endap -4t3endf -4t3endh -4t3endk -4t3endl -4t3endp -tend4r -4t3endt -te3ne. -te4n3e4b -ten3ec -te4n3ed -te4nef -te5n4ei. -tenei4d -te4n3el -te4n5end -te4n5en5e -te4n5ens -te4n5ent -te4nep -ten5erei -te4nerf -4t5energ -te4n5erk -te4n5erl -ten5erm -te4n5ern -te4n5ero -te4n5erö -te4n5erp -te4n5ers -te4n5ert -te4neru -ten5e4sc -te4ness -tene4t -ten5eta -ten5eti -te4neu -te4n3ev -ten3f2 -4t3eng. -4tengag -4tenge. -6tenglis -teng4r -te4nid -te4nil -te4n3in -tenk2 -ten3n4 -4tenog -te4nol -te4nop -te4nos -te4n5ott -te4n3o4v -3tenr -3t4ens. -tensch4 -ten4seb -ten6serg -ten6stip -ten4sur -tent5art -ten6ters -ten6t5er6t -ten6t5er6w -6tentheb -t5enthu -t5enthü -ten6ting -4t3entk -4t5entrü -4tentz -te2ny -ten6z5erw -tenz6wei -ten4zym -teo2b -te3o2f -te5o4rei -te3ou -t2ep -2tep. -te3p2f -4tepit -4t3epoc -4teppe -4teppu -t4er. -t4era -te3ra. -ter5abe -ter3a4c -tera4d -4terad. -ter5ade -ter5aff -ter3a4g -ter3ak -ter5alar -te5ralk -ter5all -te6ralm -ter5alp -te5rals -ter5alt -te5ramp -te5ran. -te5rane -ter5anz -te4r3ap -ter3a4s -te4rato -ter3a4v -te4r5änd -ter6bauu -4terbe. -6terben. -6terbend -ter6blin -4t3erbs -4terbt -terb4u -4terdäp -4t5erde. -4terdg -terd4r -ter3d4s -3tere. -te4r3e4b -te4r5eck -ter5eier -ter5eige -6t5ereign -ter5eil -te4rein -ter5ell -3tere4m -te4r5ema -te4r5emi -te4r5end -te4r5en5e -te4r5ens -te4r5ent -teren5th -te4r5en4z -4tereo -3terer -te4r5erb -te4r5erd -te4r5erf -te4r5erg -ter5erh -terer6ku -terer4n -terer5o -terer5ö -te4r5erp -te4r5err -te4r5ers -te4r5erw -3teres -te4r3e4t -ter6folgr -terg2 -6tergebn -tergene6s -4t5erhab -ter7hall -ter6haltb -6t5erhöhu -te4r3id -ter5iko -4teril -6t5erinner -terin4v -teri4on -te4riso -te4rit -ter5i4ta -terk2 -t5erklär -6terkranku -ter6löse -3termi -ter4nas -4ternc -terngene6 -4ternwa -tero4be -ter5o4de -ter5oel -ter5ofe -te3roh -te3rok -4terons -ter3ö -ter6probt -terr2 -ter3ra -ter4re. -ter6regt -tersau6er -ter4sep -6tersparn -ter5stag -ter5t4a -tert4s -teru2 -te4r5ufe -terungsex5 -ter5uns -4t3erzb -terz4e -4t5erze. -ter4zes -4t3erzg -t2es -t4es. -te4sa4k -te4s3au -tes5eli -te4s3ep -tes5erb -tes5er5e -tes5erl -te4sex -tes4par -tes4pen -te4sph -tes4por -tes6senz. -tes3si -tess4t -test5ak -te5stal -test5art -tes5tät -tes4teb -te5stec -te5steig -te5s4tel -test5elt -te5step -tes6t5er5e -tes6t5er6f -tes6ter6g -tes6t5erh -tes6terk -test5erkl -tes4tin -t3estn -tes5to5s4 -te4sur -te4tabl -4t3e4tap -4t3e4tat -3tete -4t3e4thi -4tetho -2tetl -tet2s -3t4euf -teu4f3a -2t3euk -2t3eul -3te3um -te3una -te3uni -t3eunu -2t3eup -3teur. -teuren4 -t3euro -teu4r5ob -3teus -te4val -te4van -tewa4s -tex1 -4texam -2t3e2xe -2t3e2xi -2t3exk -2t3exo -2texp -tex4ta -tex4th -tex6trau -2t3exz -tè2 -2t1f2 -tf4ae -tfahr6ta -tfal4l5i -tf4äh -tfen2 -tf4la -2tg2 -tga4s5er -tgas5te -tgas4tr -tga4su -tga2t -tgeb2 -tgel4ta -tgende4 -tge4n5en5 -tge4nid -tgens4 -tger4a -t3gl -tgol6der -2th. -2t1h2a -3t4ha. -t2had -t4haei -3thal. -thal5ba -3thale -3thalh -t4hali -t4han. -3thani -2t1hä -3thäi -2thb -2thc -3the. -3t2hea -2t3hef -2t3hei -t4hein4 -the5ins -3t4hek -the4kau -3t4hema -4themd -3t4heme -t3hemi -4t3hemm -the3na -t3heng -t3henn -3t4heo -5therap -4therd -t3herl -t4herm -4t3herr -t3herz -3t4hese -t3hess -t2heu -t3heu. -2thf -2thg -3thi. -3thic -thic3k -t3hiel -thi5er. -2t3hil -2t3him -th4in -thi3nu -4thinw -2thir -4t3hist -2thk -2th1l -2th3m -thmu2 -thmus3 -2th3n -thn4o -2t3hob -tho5chr -2t3hoe -2t3hoh -4t3hole -4t3holt -4tholz -4thops -4thopt -tho3ra -4t3horn -4t3hose -tho3st -4t3hote -2t3hou -t3hov -2t1hö -2thp -4th3rau -thr4i -th3rin -2ths -2tht2 -t1hu -2thub -t3huet -2thuh -2thup -t2hur -t2hus -2thut -2t1hü -2thv -thvil4 -2thyd -ti3ac -4t3iago -ti5alei -ti3a2m -ti3a2p -ti3as -3tib2 -ti3ce -5ticket -tico2 -ti4dea -4tidee -ti4d5en4d -ti4dio -5tief. -5tiefe. -4tiefel -5tiefen -5tiefg -3tiefl -tieg2 -4tieg. -4tiegen -4tiegs -4tiegt -ti4e3in -4tiel. -tie4l3a -ti3ell -4tiels -tie2n -tien3e -5tiera -tie4r5ei -tieren6z -tie4reu -tie3s4t -2tieß -ti3eu -3tif. -ti3f2r -tifter6ke -tif4tr -3tig -ti4g3an -tige4ra -ti4g5er4z -tiges4 -ti4git -ti4g3la -ti4g3lä -ti4gle -ti4g3li -4tigmat -ti2gn -tigs4e -tigs4p -tig5s4tr -tik5age -tik5amt -ti4kanw -tik5a4sc -ti4kei -tikel3 -ti4k5ent -ti6k5erfa -ti4kim -ti4k3in -ti4k3la -ti6klini -ti4klu -ti2k3n -ti4kob -ti4k3op -ti4k3ra -ti4k3rä -ti4krei -ti4k3ro -tik3s -ti4kun -ti4kut -ti4lant -ti4lar -ti4lau -ti4l3e4b -ti4lei -ti4lel -ti4lent -ti4l3ep -3tilg -3tilie -3tilin -til5ing -til5ins -3tillo -4tillu -ti4l3oe -ti2l3ö -ti4meli -2timm -t5immat -timmer4 -tim6merg -tim6m5erk -3timo -2t3imp -ti3naf -tin5alb -tin3an -ti3nap -3tin2e -ti3nec -ti3ne3i -tinein4 -tinen4z5 -ti4n5erf -tine3t -4t3infe -4t3info -tinga4 -tin4g5ab -ting5as -tin4g3i -tin4gl -tingo4 -tings4p -ting5st -ti3n4in -t3i4n3it -2t3inj -tink3l -tin4kla -tin4kn -4t3inno -ti3no -ti4n3oe4 -ti4nor -ti2n3ö2 -t4ins. -4t5insel -tin4s5es -tin4spa -tin4sum -5tinte. -4t5inter -ti3n4um -2t3inv -3tio -ti4onv -3tip. -tip4pr -3tips -ti4que. -3tirad -t3iran -2t3ird -ti3re -ti3r2h -tirn3a -3tisc -ti5sche. -tisch5ec -tisch5ei -ti5schen. -ti5sches -ti4schl -tisch5lam -ti4schn -ti4schr -ti4schu -tisch5wa -tis5erk -tisex5p -2t3isl -tis4tal -ti3s4th -tis3ti -ti3s4tr -ti4sur -ti3ta -ti4tal -3tite2 -ti3ti -2ti3tu -3tiu -tium4s -3tiv -ti4van -tiv5ant -ti4v5atm -ti4v5att -ti4vel -ti4v5ent -ti4v5erg -ti4v5erh -ti4v5er4k -ti4v5er4l -ti4v5ero -ti4vet -ti4v3id -ti2vo -tiv3or -ti2v3r -ti2za -ti4z3in -ti4zir -ti2z3o -ti2zw -2tj -2t1k2 -tkege4 -tkraf4 -tkue3b -tkü3b -2t1l -tlang5s -tlei4bl -tlei6d5er6 -t2lep -tl4er -tle4r3a -tlergene6 -tle6ring -t3li -tlings5 -t3lö -tlungs5 -2t1m -tm4en -tmen6schl -tmen4t3 -tments4 -tmetaller5 -tmin4s -tmi2t -tmo4des -tm4or -2t1n2 -t3na -tn4am -tn4an -t3ne -tne5reig -tne4rer -tnergene6 -tnis3t -to3al -to2as -to3a2t -4tobj -to4boe -tob4re -to3cha -to3che -4t3ochs -3tocht -2tock -to6ck5ent -to6ckers -to6ck5er6z -t2od -3tod. -toder4 -tod5ern -todi4e -to4d3un -2t3o2dy -3toech -4toeck -2t3oe2d -toe2f -2t3oe2k -4to4el. -4t3oels -4toelu -to3e2m -3t4oen -3toepf -4toeps -to3erh -to3ero -t3oest -2toeß -t3oett -2tof -3tof. -toff3a -tof4f5ei -t5offens -tof6f5ent -tof6f5ertr -4toffi -toff3l -tof4fo -toff3r -toff3s4 -tof4f3u -3tofi -3tofo -3tofr -to3fu -togas3 -2t3ohr3 -2toi. -3toil -toi4ne. -toi2r -2toiz -3toj -tok2l -2t3okt -3tole -to4lym -2tolz -4toma. -tom5ans -tomar4b -to4m5ene -3tomie -3tomis -2tomn -to2mu -tom3un -3ton. -to4nad -tona5er -to4n3an -to4nap -to4nau -ton3d2 -ton3e4b -to4neh -tonend4 -toner6ke -to4nid -ton5ing -3tonn -tonne4 -to4nob -to4nof -3tons -ton4sin -2tony -3too -to4pad -to4pak -4t3o4pal -top5ang -to4pat -to4p3el -to4per -t2opf -3topf. -to3pfä -topf5erd -5topfes -top5flu -to4ph3a -to4p3hi -topi4e -to4ple -to4pli -2topp -tops2 -5topsy -3tor. -tor5aen -to3ral -to3ram -to5rand -tor5ans -tor5ant -tor5anz -to3rat -to4rän -tor6ch5el -4tordn -3tore -tor5eic -to4rein -to4r3el -to4rem -to6renna -tor4fan -t3orga -4t5orgel -to4r3i4d -torin4s -tor5int -tor3n -to4rob -to4roe -to4rop -to2rö -3tors -4t3ort. -tor5tan -4t3orth -4t3ortn -4t3orts -tort4st -tor3um -3torw -torz2 -3tos. -3tosa -tos2h -to4ska -to3s2m -3to3s2p -tosser4 -tos6s5ersc -to5stat -tos4to -to3s4tu -2toß -to4tae -to2tä -to5t4em3 -totemp4 -tote4n -to4tho -to4t3ri -tot3s4 -3t2ou -touil4 -to3un -to1x -4toxid -3töch -2t1ö2d -2t3ö2f -2t1ö2k -t1öl -2töl. -2töle -2töls -2tölu -3tön -3töpf -3törtc -2t3öst -3töt -4t3ött -2t1p2 -tpar4k -tpho2 -2tq -tr2 -2tr. -t4ra. -tra3a -tr4ab -tra4bu -tra5cha -tra5chl -3tract -4t3rad. -tra4dem -tra4der -4t5rades -t5radie -4tradio -5tradit -4tradiu -4t3radl -4tradp -4t3rads -3traeg -4traeh -4traend -5traene -4traeng -4traes -4traeß -tra4fah -4traff -4trafg -traf3l -4trafr -tra4g3l -3tragö -3trahi -4trahl -4trahm -3trai -tra5inh -3trak -4t3rake -3tral. -3trale -tra4leb -3tralg -tra6lins -4tramm -5t4ran. -4trand -tran4d5r -4trang -t5rangi -trang5s -t5ranki -tran4k5o -t3rann -5t4rans -tra5po -tras6terf -tras4t5r -4traß -4trateg -tra4t3h -t5ratin -tra4tr -3trau. -4traub. -4t5raubv -4trauc -4t3raup -4trauß -2t3räd -3träg -2träh -4tränd -3träne -4träng -2träs -2träß -4träuc -4träus -4träuß -t2re -2t3re. -2trea -t3reak -t3real -2treb -tre4bl -tre4b3r -2trec -t3rech -5treck. -5trecô -2t3red -4trefe -3t4reff -4trefl -4t3refo -2t3reg -2t3reh -trei4bl -4treic -4treif -4t3reig -4t3reih -4treik -4t3reim -4t3rein -trei4sc -t3reiß -4treit -trei6terp -t3reiz -2t3rek -2t3rel -t3rena -3trend -4trendi -5trennu -4t3rent -t3repa -4trepe -4t3repo -5trepp -4trepr -t5resek -5tresen -4t3resi -t5resso -tres4st -4trest. -tre5str -4trests -t3resu -tre4ta -3t4rete -tret3h -4t3retr -tre4tra -tre4t5ri -4trett -tr4e2u -treu3c -5treuem -5treues -3treuh -4treun -2t3rev -tre3x -2t3rez -t2ré -2t1rh -3tri. -3trib -4trich. -4triche -trichs4 -trid2 -tri2e -3trieb -trie5fr -tri5ens -tri5ent -3trier -tri3gl -3trigo -t4rik -tri4ke. -tri4kes -5triko -4trikt -3trim -4t3rind -4tring -4t3rinn -3t4rio -t2rip -3trisc -t3riss -tris5ta -4t3ritu -tri2x -trizi3 -3tro. -4trock. -4t3roec -4t3roeh -4troem -3troep -4t5roess -3troj -tro4kes -4trom. -tro6manp -4tromb -tro4men -tromer4 -4tromg -tro4min -4tromk -4troml -5trompe -4trompu -4tromr -4troms -4tromt -4tromv -4t3rond -tro6niks -t3ronn -tro3ny -t4r4op -3tropf -5trophä -3tross -tro5sta -4t3rot. -4trout -3trow -3troy -2t3röc -2t3röh -2tröm -2t3rön -3tröp -4t3röss -3trua -3truc -true5be -true5bu -4t3ruec -truecker6 -5truemm -t3rues -2truf -2truk -trum2 -3trum. -t3rumä -4t3rumm -trums3 -4t3run. -4t3rund -t3rung -5trunke -3t4rup -tru4th -2truw -trü3be -trü3bu -2t3rüc -trücker6 -3trümm -2t3rüs -t2ry1 -2ts -tsa2b -t3sack -t2s3a2d -t4s3aem -ts3aer -t3saeu -ts5aeus -t2sak -t3s4akk -t3sakr -t4s3all -t4s3alt -t4s3amb -t5samm -ts3amp -t4s3amt -t2san -ts3ang -ts3ank -ts3ant -ts3anz -t2sa2p -t2sa2r -t3s4ark -t2s3as -t2s3au -t3s4au. -tsau4d -ts4au4e -tsau4m -tsau4n -tsauto6r -ts2äc -t2sär -t2säs -t4s3äus -ts3b -ts4cap -t2sce -t6schart -t4s5chef -t6s5chine -t3schl -tsch6lic -tsch4lo -t6schmue -t6schmun -t4schmü -t6schor. -t6schore -t6schors -t4schro -t4sch5wo -t4sch5wö -tsco2 -t2s3e2b -tse2c -t2s3e2d -tse2e -tsee3i -t2sef -ts6ein. -t4s5eind -ts5einf -t4s5einh -t4s5einl -t4s5eint -t5seit -ts3e4kl -tse4la -t4s5e4mis -tsen5er -t4senq -t4s3ent -t4s3e4pi -ts3epo -t2ser -ts3er3e -ts3erf -ts3erg -ts3erh -ts5e4r5in -ts3erk -ts3erl -ts3erm -ts3ern -ts3er3o -ts3erö -ts3err -ts3ers -ts3ert -tse3r4u -ts3erw -ts3erz -t6s5essen -tse2t -t4s3e4ta -t4s3eth -t4s3eti -t2s3e2v -t2sex2 -t3s4exi -ts3ha -t2s3i2d -t3sig -t2si2k -ts3iko -ts3ima -t4s3imp -tsi2n -t4s3ind -tsing4 -tsin5gl -t4s3ini -t4s3int -t5skala -ts4kele -t5sklav -ts3ko -t3slip -t3s4mog -t2so2b -ts3oef -t4s3off -t3son -t2s3op -tso2r -t4s3ora -t4s3orc -t4s3ori -t4s3orn -t4s5ort. -t4s5orts -t3sos -t2s3o2v -t2sö -ts3öf -ts2öl -t3span -t5s4pare -t3spek -t4speri -t5sperr -t4s3pic -t4spins -t2spl -t5s6port. -t5s6portl -ts6ports -ts4prac -t4sprei -t5sprun -t3spue -ts4pul -ts5rei -ts3s2 -tsse5ru -tss4t -t1st2 -t6s5taeti -t4stafe -ts3tak -t4stale -ts5tank -ts5tant -t4s5tanz -t4s5tat. -t4s5tauc -ts3tav -t4s5täti -t4steil -t5stein -t4step -ts4terb -ts4tern -ts4tero -ts3tex -ts3th -ts4tig -t4stit -t4stoch -t4ston -t4stört -t4strac -ts4trap -ts4tras -t4stren -t4strie -ts4trom -tstro6ma -t4strun -t5s4tub -ts4tuem -tstu5er. -t2sty -t2su. -3tsubi -t2s3ul -t4s3um. -t4s3umb -t4s3umd -t4sumf -t4sumg -t4sumk -t4suml -t4sumr -t4s3ums -t4s3umv -t4s3umw -t4sumz -t2s3un -t2s3ur -t2s3ut -tswa4s -t5szene -2tt -tt3abm -t3t2ac -tt5achs -tt3aeh -tt3aer -ttafe4 -tta6g5ess -tt3ahl -tta4ke -t4t3alb -t4t3ana -t4tanb -t4t3and -tt5anka -tt5ankä -ttan4k5l -tt5anku -t4tanr -t4tanw -t4t3app -t4t3arb -tt3arm -tt3art -tt3äh -tt3änd -tt3är -tte2c -tt3eck -t2t3ef -t4t3eib -t4t3eif -tteil5a -tteile4 -ttei6leb -t4t3eim -tt3ein -t4t3eis -tte4l3a4 -tte4l5e4b -ttel5ein -tte4l5en -tte4l5in -tte4lit -tte4l3o -tte4lö -ttels4t -ttel5ste -ttem4bo -t4temo -t4temö -t4t3emu -t3ten -tte4na -tte4net -tte4nob -t6ten6sem -ttens4p -t4t5entb -t4t5entf -t4tents -t2teo -tte4rat -tte4r5ec -tte6r5ei6s -tteri4e -tte4rik -tte4ris -tter6klä -t6terneu -t4t5ernt -tter5oe -tte4r5oh -ttesa4 -ttes5ac -ttes5ae4 -tte4s3ä4 -tte4s3o -ttes5pr -ttes6ser -tte5stri -t4teti -tt5extr -t4t3heb -tt3hel -tt3hen -t4t3hin -t2t3ho -t2ti2d -tti3es -t4t3igi -tti5le -tt3imm -t4t3ind -t4t3inf -t4t3ins -tt5isol -tti3z -t4t3la -t4tli -tto3ef -tt3oel -tto5ent -tto3er -tt3ord -t4t3org -t4t3orn -tto3s -tt5rand -tt3rea -tt3rec -tt2ro -tt5roch -t3troe -tt5rose -t4trou -t3trö -tt3rud -tts5and -ttsau4 -ttschi4 -tt4sele -tt4s5end -tts5erb -tts3pr -tts5taet -tt4s5tät -tt4stem -tts4tu -tt4sum -ttu3en -ttu2f -tt3ufe -tt3uh -ttur4m3 -ttü1 -ttwa3r -ttz2 -tu3al. -tu3alm -tu3alp -tu3alv -tu3ant -tu3ar. -tu3ars -tuba3b4 -3tuch -tu4chel -tu3cho -tuch3s4 -tu3cka -2tud -tudie4n5 -3tudo -2tueb -tue5ber. -3tuech -2tueh -tu3ell -tu3en. -tu3enb -tu3eng -tu3enk -tu3ens -tu3ent -3tuer. -3tuerc -5tuere -tu3ern -tue4r3o -tu5ers. -4tuerz -tu3es -tue4th -4tuetz -2tuf -tuf4e -tu5fen -t3u4fer -3tuff -tu4gan -2tuh -tuhl3a -tuh4lei -tu3ist -tu2kr -2t3ulm -tul4pa -tul4pf -tul4ph -3tum. -tum4b3l -tume4 -2tumf -2tumg -2t3umh -2tumk -2tuml -3t4umo -2tump -2tumr -4t5umsat -t3umsc -tum4si -tum4s3o -tum4spe -2t3umt -2t3umw -2tumz -2t3una -3tuna. -3tunas -2tund -tund4e -tun4d5ei -tun3dr -3tune -tune4b -tun4en -t3unf -3tung. -4t3unga -4t5ungeh -4t5unger -4tunget -tungs5 -4tunif -4t3u4nio -4tuniv -2t3unl -2tunm -3t4unn -2t3u2no -2tunr -t3unsc -2tunt -3tunt. -2tunv -t3unz -2t3up. -2tupd -2t3upg -2tups -t2ur -tur5abe -tu4r3ac -tu4rad -tu4rae -tu4r3a4g -tur5aka -tu4r5alm -tu4ralt -tu4ram -tu4r5ang -tur5ank -tur5ans -tu4r5anz -tur5asp -tura5st -tu4rau -tur3c -tu4r3e4b -tur5eic -tur5ein -tur5eis -tu4rep -tur5erb -tur5erg -tur5erh -tur5erk -tur5erl -tur5ern -tur5ers -tur5ert -tur5erw -tu4res -tur5es5t -tu4r3e4t -tu4r3e4v -tu4rid -tu4ril -tur5ini -tur5ins -tur5int -tu2r3o -turo4p -tur5sa -tur3sk -tur3sn -tu2ru2 -tur3uf -tur3un -tu2s3a -tu4schl -tu4schm -tu2se -tu2s3i -tu2so -tu3spr -tu3t4a -tuts3c -tuts5p -2tüb -tü5ber. -3tüch -tück4se -3tüf -3tüll -3tüm. -3tüma -3tümb -3tümc -3tümf -3tümi -3tüml -3tüms -3tümt -3tümu -3tümv -3tümw -3tür. -3tür3c -3türe -3türg -3türk -3tür3s -3türw -3tütc -2tv -tvie2 -tvoran4 -2tw2 -twa3i2 -twa2r -twa2s -twegs4 -twe4n3e -twetterer5 -twi2e -3t4wist -2t1x -tya2 -3typ -ty2p3a -ty2pf -ty1s2 -2tz -tz1a2 -t2zat -t2z3än -t2zäp -t2z3är -tz3e4di -t4z3eie -t4z3eim -tze5ina -tze5ind -t4z3eis -tzei6t5end -t4zelek -tze2n -tzene4 -tzen5eb -t6zensem -t4z5entg -t4zentl -t4z5ents -t4zentw -t4zentz -t5zer. -tzer6bre -tze4reb -tze4rer -tz6erfe -tz6erfr -tzer6gre -t4z5erhö -tzerin6s -tz6erl. -tz5ermi -t5zern -tzero4 -t4z3erö -tzer4öf -t6z5ertra -t6z5erträ -tzer6werb -tz5erzi -tze2s -t2z3i2d -tz3imi -t4z3ind -tz3inf -t4z3inh -tz5insa -tz3int -tz3inv -tz3iso -tzo2b -tz3oef -t2z3ok -tz3om -t2z3or -tz3ti -tz4tin -tzu4gu -t4z3unf -t4z3uni -tzwan4d5 -t3zwie -2ua -u1a2b -u1a2c -ua4ch -uad3a -ua4dan -uad2r -uae2s -ua2g -u3al3a2 -u3a2l3ä -u3alb -u3ald -uale2 -u3a4leb -u3a4lei -u5alemp -u5a4l5ent -ualer4 -u5a4l5erf -u5a4l5erg -u5a4l5erk -u5a4l5erl -ua4l5err -u5a4l5erz -u3a4let -u3alf -u3alg -u3alh -u2ali -u3a4l3id -u3a4l3in -u3alk -u3aln -ua2l3o2 -ualp2 -u3alr -u3als -u3alt -ual5to -u3a2lu -u3alw -ualz2 -u1am -u3an. -u3anar -u2ane -u3ans -uante4 -uanten5 -uara4b -uarer3 -uar4k3a -uar4k3e -uark3s -uar4t5a4n5 -uart3s -u3arw -u3as. -uas2a -uasi3 -uas2p -ua2th -uati6ons. -u1au -uauf3 -u1ay -u1äm -uä2s -u1äu -2ub -ub5anke -ub3ant -u4b3arb -uba2s -ub3asc -ubb2l -ube4be -u4b3ehe -u4b3eig -u4b5eins -ub5eint -ub5ein4w -u4b3e4mi -uben3o -u4b5erde -uberin6t -ube4woe -ube4wö -u3b4is -u3b4it -ub2l -ub3läu -ub3los -u4b3lus -ubmi2 -u2bo2b -u2b3ok -u2bop -u2b3oz -ub5rank -ub2ri -ub2ru -ub2rü -ub4s5ang -ubsau4 -ub4s5auf -ub4s5aus -ub4sin -ub4s3or -ubst4 -ubs5trau -ub4s5tri -ubt2h -u4bueb -u2büb -2uc -uc1c -u3cha. -uch3ab -uch3ad -uch3ae -uch3al -uch3am -uch5ank -uch5ans -uch5ant -uch5anz -uch5arte -uch3as -uch3au -uch3ä -u3che -u4ch3e4b -u4ch3ed -uch3ei -u4ch5elf -uch5ell -uchens4 -u4ch3ep -ucherin6s -ucherin6t -ucher6ken -u6cher6lau -uch5erlö -uch4erz -uch5er6zeu -u5ches -uche4t -uch5eti -uch3i4d -u5chig. -uch3il -uch3im -uch3in -uch3l -uch3m -uch5ma -uch5mi -uch3n -u3cho. -uch3o4b -uch5oef -uch3op -uch5org -uch5ort -u3chos -uch3öf -uch3r -uch4san -uch6s5ein -uch4s5el -uch4sem -uchser6z -uch4s5in -uch4s5or -uch5stran -uch5stub -uch4tak -uch6t5eig -uch6t5erf -ucht5erl -uch6t5ersc -uch6t5er6t -ucht5erw -uch6t5erz -uch4t5in -ucht5sk -u3chu2 -uch3uh -u4chum -u4chur -uch3ut -uch3w -u1ci -ucin3 -u4ckar -uck5elf -u4ck5ene -u4ckent -u4ck5en4z -u5ckero -u2cki -uck3in -uck5spe -uck5stu -u1cl -u1cr -2ud -u3d2a -uda3d -ude3e -ude3i2 -udein4 -ude4n3e -udens4 -ude6rent -ude6rerr -uderna5 -uder3o -uder3ö -ude4str -udiener6 -udi2o3 -uditi4 -ud2ob -udo3c -u3d4o3e -u2don -ud4o3s -u4d3rat -2ue. -2uea -1u2e2b -2ueba -ueb3ae -2ue4bc -2uebd -uebe2 -ueber3 -ue3bi -ue4bk -ueb5le -ue3blo -ue3bo -ue4bs -2u2ec -uech4s5c -ueck5erb -ue6ckerf -ueck5erh -ue6ck5ers -ue6ckerz -ue2d3a2 -uede4c -ue5den. -ue2d3o4 -ue2d3r -ued3s2 -ued3t2 -2uee2 -ue3er -2uef -ue2fa -uef3ae -ue4f3ei -ue4f3in -ue2f3l -ue2f3r -uefs2 -ue4fum -2ueg -uege4l5a4 -uege6lei6s -ue2gl -ue2gn -u2eh -ue2h3a -uehan5 -ue4h3ei -ue4h5eng -ue4hent -ue4h5erf -ueh5erk -ue4h5er4z -ue2h3i -ueh4lei -ueh4l3o -uehn4s -ueho2 -ue4h3ob -ue4h3of -u1ei -2uek -2uel -ue3lak -ue4l3am -ue4lau -uel5eim -uel5ein -ue4lek -uel4er -ueler5ei -ueli2 -ue4lio -uella4 -uel4l5ad -uel4lar -uel4l5au -uell5e4c -uel4l5ei -uelle4n -uel4lo -ue4lor -2uem -ue4ment -ue4m5erz -ue4m3id -ue4m3in -uemm4s -uem2s -ue2mu -2uen -ue2na -uena4g -uen3ah -uen3ar -ue2nä -uen4d5ei -uendergene6 -uen4due -uendungsgene6 -uen5ema -uen5ens -uen5erk -ue4n5er4l -uen5esk -uen4fak -uen4gag -uenge6bu -uengene6 -uenge6schl -uenge6wan -u3enh -ueni2 -ue4n3in -uenk4l -uenk4r -ue2no -uen4seb -uen4ser -uen5ska -uens6por -u3env -uen6z5eig -uen4z5in -uen4zw -2ue1o -2uep -uepfel5 -ue2po -uer3a -uera4de -ue4ra4t -ue2r3ä -uer3d2 -ue5re. -ue4r3ec -uer3ed -ue3rei -ue5rei. -uerei5s4 -uer5ela -uere4m -uer5emi -ue4r5eng -ue4r5ent5 -ue4rerf -ue4r5er4g -ue4r5er4h -uerer4k -uerer4l -ue4r5er4m -ue4r5err -ue6r5er6sa -ue6r5ersc -uerer6sp -ue6r5er6st -uerer6za -uerer6zä -u5er5esk -u5eress -ueres6si -uere4t -uer5eta -u5ereu -uer6fahru -uer4fas -uer6g5eng -ue4r3i4d -ue4r3i4m -uerin6nu -uerin4t -uerk4l -uer4le. -uerme4c -uer4nan -uer4nau -uer4neu -uerns4t -ue3ro -uer5ode -uero3s -uer4rh -uerr4s -uers4k -uers4ta -uer5stag -uer5stau -uer6trag -u3e4rue -u3e4ruh -u3erum -u5erunf -u5erunt -u3erur -uer4zoe -2ues -uesch5af -ue4schl -uesen5e -uese3s -ue4sin -ues3l -uesli3 -ues2p -ues4sau -ues4s3c -ues4so -ues4st -ues5tel -ues4th -2ueß -2uet -ue4t3al -ue4tap -ue4tau -ue3te -ue4tik -ue4t3ro -uet6schw -uet4st -uety2 -2uev -2uew -u1e2x1 -2uez -u2fab -uf3abe -u2f3ad -uf3aen -ufae4ß -ufall4 -ufa2n -uf3ana -u4f3an3e -ufa2r -uf3are -uf3arm -u2f3aß -u2fa2t -uf3au -u2f3än -u2f3ä2ß -u2fei -uf3eig -uf3ein -ufel4s5a -ufel4s5i -u4f3ent -ufen4te -ufer4fo -uferin6s -uf5erke -ufer6lau -u4f5erne -ufe2t -u4f3eta -u4f3eth -u4f3eur -u2fex -uffer5e -ufferin6h -uff2l -uf4fot -uff2r -uf4fro -u2f3id -u2f3im -u4f3ins -u4f3int -u4f3inv -uf1l -uf3lim -uf3lit -uflos3 -u3fluc -u3flug -u3flüg -u2fob -u2f3of -u2fop -ufo2r -uf3ori -uf5orte -uf1r -uf4rass -u3frän -ufs3an -uf4s5eta -uf4s3in -uf5skie -uf2so -ufs4por -uf4spre -uf4spro -uf5stras -ufta4g -uft3eb -ufte4m -uft5erd -uft5er4g -uft5er4l -uft3in -uft3s4 -ufuen4 -uf3ums -uf3urk -ufus2 -2ug -ug3a4br -u4gabs -u4gabte -u2g3a2d -ug3aeh -ug3aku -u4g3ana -u4ganb -u6gangeb -u4gani -u4g3ank -u4g3ant -u4g3anz -u2g3ap -ug3ars -uga2s -ug3asc -ug3au -u4gaus -ug3d2 -ugd4e -ugeb2 -u2g3ec -u4g3eig -u4g3ein -ug5eise -u4gelem -ugena4b -ugen6dre -ugen6d5ri -uge4n5en -ugens4 -u4gentf -ugenz4 -u4g5erke -u4g5ersa -uges4 -u5ges. -u4g3esk -u4g3ess -ugge4l5a -ug3i4de -ugi3en -u2g3im -ugin4h -ugin6ner -u4g3ins -ugin4sp -ug1l -u4gläu -u6gleist -u6gleitb -u6gleits -u6gleitu -u2gli -ug3liz -u4glok -u2g3n -ugo1 -u2go2b -u2g3oc -u2g3oe -u2gof -u2gom -ug3o2p -u2g3o2r -u2gö -u4g3ram -u4g3rat -u4g3reg -u4g5reis -u4g3res -u4g5rind -u4grou -ug3rum -ug3rüs -ugs4alb -ugs4and -ugs2c -ug4sein -ug4sel -ug4sin -ug5span -ugs4por -ug5stan -ug5s4tie -ugs4tol -ug5stre -ug4s5tür -u1ha -u1hä -2u1he -uhe3a2 -uhe3b -uhe3e2 -uhein4t -uhe5rae -uhe3s2 -2u1hi -2uhl -uh4l3as -uh3lä -uhl5ein -uh4l5ent -uh4l5erb -uh4lob -uhmes3 -u1ho -u1hö -uhr3an -uhr3au -uh3rei -uhrei4s -uh4r3er -uhr3o -uh2ru -uh1s2 -uht2 -2u1hü -uh3w -2ui -ui2a -ui4che -ui3cho -ui4cker -u3idd -u3idm -u3ids -u3idt -u1ie -ui3em -u1ig -ui2ge -uik2 -uil4les -u1im -uin3it -u3inl -u3inw -u4ische -uisi4n -u3ism -uis3ta -u3isv -u2it -ui4te. -ui2t3r -ui1x -uji3 -u1ke -uke2n -uken3e -uk3er3e -u1ki -u1k2l -ukle3i -uk3ler -uk2n -u2k3o2b -uko3c -uko4ma -uk2r -u4krai -uk2t3a -uk2t3ä -uk4t3eb -uk4t3el -uk4t5ent -uk4t3er -uk2th -uk4t3i4m -uk4t3in -uk4top -uk4t5o4ri -uk2t3r -uk4t3um -uk2u -ukue3b -uku2s -uk2ü -ukü3b -u1la -u3la. -u2l3af -u3l4ag -ulan4di -u4lanf -ulap3 -ula4ra -ularen4 -ula6r5end -ularz4 -ula2s -ulasch5 -ul3aus -u1lä -ul3äm -ulb2 -ul1c -ulda3e -uldan3 -ul4dar -ulde4n -ul4d5erf -ul4d5erk -ul4d5erl -ul4d5ers -ul4d5rue -ul4drü -ul3eig -u2lek -ul3elf -ul3elt -ul3emb -ule2n -ulen3e -ulen3i -ulenk4 -u2lep -ul3epe -ul3e4po -ul3er4h -ul3er4r -u4leru -ule4s3a -ules3t -ule2t -ul3eta -ul3ext -ulf2 -ulg2 -uli2k -ulik5an -ulik3o -uli3pf -ulk4a -ulk2l -ulk2r -3ulkt -ul3le -ull5eic -ull5ein -ul4lem -ull5endu -ull5erkl -ul4lid -ul4l3o4d -ul4lop -ull3s2 -ulni2 -ulo2i -ulom2 -ulomb3 -u2l3o2p -u2l3or -ulps2 -ul4sal -ul4sam -ul4s3ec -ul4ser -ul4s3in -uls6t5erk -uls4t3h -ul4sum -4ulta -ult3ar -ul4tau -ultauto6 -ult5ein -ulter6min -ulti5t -ult3s2 -u1lu -ul3umb -u2l3ur -ul2vr -ulz2w -u4m5a4ben -uma2c -u4m5aest -u4m3aku -um3alb -u4m3all -u4m3alt -u5mann -u4m3app -u4marc -u4m3arg -u4m3arm -um3aro -u4m3art -u4m5asts -u4m3atl -u4m3atm -u4m3aus -u4maut -um3ärg -u4m3äst -umd2 -u2m3e2b -u4m3e4ck -u2m3ef -um3eic -u4m3ei4n3 -u4m3elf -umens4 -u2m3e2p -umer4a -u4merd -u4m3erf -u4m3erg -u4m3erh -u4m5erku -u4m3erl -u4m3ero -u4m3erw -1umf -1umg -2umgü -um3ill -um3inh -um3ir -um3iso -umi2t -um3ite -1umk -1uml -um3me -4ummel -ummi5erz -um3n -u2m3ob -u4moel -3u2m3o2f -u2m3o2p -3umöf -ump4fa -ump4fin -4umpfs -ump5ler -1umr -umsch4 -umsche6f -um4s3ed -ums5ein -um4ser -ums5erw -um2sk -um3s4kl -ums4ter -um4sum -umt2 -um3to -umu2r -u1mü -u2m1y -1umz -2umzw -un1 -4una. -u2nab -un4abe -4u3n4abr -u3nac -u3nae -un2ag -u3n2al -un4apa -un4ara -4un2as -una3st -u3n4au. -u3naue -u3naus -u3naz -un2är -2und. -un4da4b -und3ak -und5art -un4d3as -un4dän -3undd -2unde -un4d3ec -undein6f -und5eis -undemi4 -unde5ras -under5ei -underer6 -und5eroe -un4d5erö -underten6 -under6tend -un4d3ex -4undg -4undi -un4d3id -un4dim -und5in5i -3undn -2undo -un4do4b -un4d3op -4un2dr -und3rä -und3re -und3ri -und3rü -4undsc -undue6b -un4d3um -undü4 -un4düb -3undz -un3ec -une2d -une2h -un4ei. -un3eid -un3ein -un3eis -une2l -u4n3elf -3unemp -une2n -unen3e -uner5o4b -u4n5erz. -un2es2 -uneu3 -u4n3eul -unf2 -un3fa -3unfä -4unfe -3unfr -un4g3ab -un4gad -un4g3a4g -un4gak -un4gam -ung5art -ung5ass -un4gat -un4g3au -ung5eig -un3gen -ungen5erg -ungen6te. -ungen6ten -unger4e -3unget -3ungew -3ungez -un4g3ig -un4g3in -ung5lat -3unglu -3unglü -un2go -un4g3rä -un4g3ri -un4gro -ungs5pa -ungs5tae -ung5stang -un3i4de -3u2nif -unik2 -un3iro -u3nisc -un3isl -3u2niv -2unk -un4k3ak -un4k3an -un4k3au -unker6ke -un4ket -unkgene6 -un4kne -un4kno -un4knu -un4k3ro -un4k3ru -unk3s2 -unk4tit -unk4top -unk4t5ri -unk4tro -un4kum -un4kur -un4kut -un4küb -unloe4 -unlö2 -1unm -unmen4s -unmut4 -un4n3ad -unne2 -unnel3 -unn6en5e -u2no2b -uno2r -1unr -uns2a -unsch5el -un5schen -unsch5wi -3un3si -uns2k -un3ska -uns2p -unsta4g -unste4c -uns6t5erw -unstu4 -un2sz -1unt -2unt. -unt2a -unt3ar -5unterd -unteren6 -unte4ri -unt6ers -2unth -2unti -unti4m -2unto -unt2ö -unt3s2 -un3t4u -unuss4 -1unv -2unvo -unvol4 -unvoll5 -1unw -2unwä -u3ny -unz2w -2uo -u1o2b -u1oc -uoe2d -u1of -u3or. -u3or3a -u3or3c -u3o4ret -u3org -u3orh -u3o4ris -u3ork -u3orm -u3ors -u3ort -uo3ru -u3orv -u3orw -uos2 -u3os. -uote2 -u1ox -u1ö -uö2d -u2ös -1upd -upe2r -upe4r3a4 -uper3e -uperer4 -upf3au -u4pfen -upf6erp -u2pfi -u4pf5lau -u2p2fo -u1pi -up2l -2u3po -2upp -up2pf -up2pl -uppor4 -upport5 -u1pr -up3sl -up4t3a2 -upt5en5e -upt5erf -upt5erg -upt5erk -upt5err -upt5ers -upti4d -upt3il -upti4m -upt3in -upt3o -upt3r -2ur. -u1ra -u4r3abb -ura4ben -ur3abt -ur3adr -ur3ael -ur3ae4m -ur3aen -u4r3akt -ural6leg -u5rallei -u5rallo -ur3alt -ural4ta -ural4te -ur3alu -ur3amp -3u4ran. -u4ra4na -uran5at -uran4ba -uran4bi -u4ranf -uran6fän -ur5angs -u4rank -u4rann -ur4anr -uran5s -u4r3apo -u2rar -ura4ren -ura4ri -u4r3atl -u4r3att -ur3auf -ur3auk -ur3aus -u2r3a2v -ur3äl -ur3ä2m -ur3än -ur3b4a -urchi4 -ure3am -ure4be. -ure3e -ur3eff -ur3ehr -ur3eig -u4r3ele -ure4ne -u4r5ener -uren6gag -urens4 -uren6sem -u4r5entg -uren6thu -u4r5entn -uren4zy -ur3epo -u2r3e2q -urer5eig -ure3un -u4rex -urfar4m5 -urf5eig -urf5erg -urf5erk -ur4f5erl -ur4f3li -ur4f5rec -ur4fro -urf5str -ur4f3uh -ur6gense -urg5inn -ur4g3ri -urgros4 -urg3s2 -u1ri -uri4c -ur3ide -ur3ill -ur3imp -u4r3ind -u4rini -urin5it -ur6ins. -ur5in6sek -urin6stin -u4rint -u4r3i4so -urk2l -urk2o -urk3sp -ur4m3ab -ur4maeh -ur4m5ang -ur4m5art -ur4matt -ur4mäh -ur5me. -urm5eie -urm5einh -ur4m5ere -ur5mes -urms2 -ur4mum -ur4m3ur -ur4n5ere -ur4nol -u1ro -ur3ofe -ur3off -u4r3orc -ur3org -u1rö -urp2 -urr2a -3ursac -ur4san -urs5ang -ursau4e -urs5auf -ur4seb -urs5ere -urs5erf -urs5erk -urs5ert -ur4sex -urs5inf -urs5int -ur2sk -ur4staf -urs5tak -urs5tele -urster4 -ur4s3um -ur4s3un -ur4tai -urt5erz -urt2s -urts4c -u1ru -urue5cken -urueck5er5 -uruf2 -uru3fl -ur3uhr -ur3umb -ur3umh -ur3ums -uru3s -u1rü -urück5er5 -urve4n -ur2za2 -urz3ac -ur4z3ec -ur4zeg -ur4z3ep -urzer3 -ur4z5ere -ur4z5erk -ur4z5erl -ur4zin -ur4z3op -ur2zw -us3aar -usa2b -us5achs -us3aeh -us3aer -us3aes -usa4gi -usa2k -usa4la -us5alar -u5sali -us5alta -u5salz -us3amb -us5amma -us3amt -u4s3ang -us3ant -us3anz -u2sar -usa4ro -us3ast -usauto4 -u2säh -u2sär -u2s3äs -us5bli -usby2 -u2sce -us2ch -u4schab -u6schaer -u4schak -u4schar -u4schas -u4schär -u4schef -usch5eic -u6sch5eind -usch5einh -u6schene -u6schent -usch5eul -us4chi -u6schimp -u6schlan -usch5lap -u6schlauf -u6schlem -usch5mis -u4schmo -u4schmö -u6schmus -u4schof -u4schok -u4schor -u4schur -u2sco -usde3s4 -use5be -u2s3e2c -use3en -u2s3ei -u3seid -us5e4kel -us2en -usen6dac -usen6dere -usenk4 -u6sen6sem -u4sentl -us3erd -use4rec -u4s5er4pe -u2se2x -us3g -u3shid -u2sid -usi5er. -usi5ers. -usi4kas -usi4kat -usiken4 -us3ind -us3inf -us3int -u2sk -us5lob -us3oc -u2s3oh -u2s3op -us3orb -us3orc -us3ost -us3ot -us3ou -us4pate -us4patz -u3s4pec -u3spek -us3pic -u3s4piz -us6port. -us6ports -u4sprae -u4s5prä -us4s3ad -us4s3eb -us4s3ec -usse4g -uss5einf -uss5eins -us4seka -usse4kr -us4sema -us4semä -usse4mi -usse4n -ussen5e -us4sep -us5s6er. -usser6fa -usser5kr -us6s5erla -uss5er6le -usser5w -uss5erz -us4sesp -us4sete -us4sez -us4sph -uss5tal -us4stor -ussu2 -us4s3um -uss5urt -u4st. -u3stad -ust5arm -usta4v -ust5ave -u6stele -ustell5a -ust6er. -uster6fü -uster6tr -us4t5ess -us5thal -u3stir -u3s4tol -u4stöp -u5strah -u5streck -u5s4tuec -u2sue4 -u2s3uf -u4s3umb -u4s3umd -u4sumg -u4sumz -u2sü -u2sya -u4s3zi -2uß -u2ß3eb -uße2n -ußen3e -u2ß1u -2ut -ut5ablo -ut3abr -ut3adl -ut3aeh -ut3aer -u5tafe -ut3aff -u2tah -ut3akt -ut3alg -ut3alk -ut3al4l -u4t3alt -u2t3a2m -ut3ana -ut3and -u2t3ar -ut3ass -ut3auf -u4taut -ut3äh -u2t3är -u3tea -ut3ede -ute3e -uteg2 -u3teh -ut3ei. -ut3eie -u4t3eig -ut3eiw -ute4la -uten4ac -ute4n3e -utens4 -u4t5entf -uter3a4 -utergene6 -u4t5erhö -uteri4e5 -ute6ring -u4t5ernt -u4terwä -ut4es -ut4et -ut4ev -u2tex -utfi2 -ut3hel -u2t3hi -u2t3ho -u2t3hu -u2t3i2d -u4t5igel -3u4tili -ut3ind -u4t3inf -u2t3ir -u4tiso -uti4vi -uto3c -ut3oef -uto5erf -uto5ers -uto5erz -u3tof -u3tom -u4tops -u4tord -uto6rena -uto6renb -uto6renc -uto6rend -uto6rene -uto6renf -uto6reng -uto6renh -uto6reni -uto6renl -uto6renm -uto6renr -uto6renv -uto6renw -uto6renz -ut3ort -uto3ru -uto3s2 -u2töl -ut5raeu -u4t3räu -ut3rea -ut3res -ut3ril -u4t3rou -ut3röt -u5trup -ut3rü -utsau4 -ut4schl -ut4scho -ut4schö -ut3ser -ut4sin -u4tsk -ut3sky -ut3sp -uts4pe -uts4to -uts4tu -ut3t2l -ut3to -utto3e -ut4t3ro -utt3s2 -utu3en -u2t3um -utu2n -u4t3une -utu4re -utu3ro -utu3ru -ut2za -ut4zed -utz5ehr -utz5eig -ut4z5ene -utzer6tra -utzer6trä -ut4z3e4t -ut4z3in -ut4zis -ut2zo -ut2zö -ut2zw -2u1u2 -uum1 -uume2 -uumen5e -uungsex5 -u1ü -u2ve. -uve3na -uve3ra -uve3rä -uvoran4 -uv2r -2u1x -ux2a -ux1o -ux2or -ux3t2 -u1ya -2uz -uz3al -uz3ar -uz3eck -u4z3ela -uze2n -u4z3ene -u6z5en6tro -u4z3en4z -u6z5er6hoe -u4z5er4hö -uzer4l -u4z5erla -u2z3i2k -u4z3inf -uzi3o -uzo2f -uz3or -uz3ot -uzu4pf -uz3z2 -uz4zb -ü1a -1ü2b -2ü3ba -üb1ä -2übc -2übd -übe2 -2ü3bef -übe3le -ü3bem -übe5n4au -über3 -ü3bes -ü3bi -üb3l -üb5len -üb5les -üb1r -üb4s3te -2üc -ü3che -ü3chi -üch4s3c -ücke4n -ü5cken. -ück5erb -ü4ck5erf -ück5erh -ück5er6ke -ück5erla -ü4ck5ers -ück5erw -ü4ck5erz -ü4ckin -ück4sor -ü4ckum -ü2d1a2 -ü2dä -üde2c -üde2l2 -ü3den. -üden4g -ü3dens -üdi2r -üdi2t -ü2d1o2 -üdo4s -üdost3 -ü2d1ö -üdpol4n -ü2d3r -üd3s2 -üd3t2 -üdu2 -üd3ug -üe2 -üeb3l -ü1ei -2üf -ü2f1a -ü2f1ä -ü2f3ei -ü4f3ent -ü6fer6geb -üfe2t -üf3eti -ü2f1i -üf1l -üf1s2 -üf4tan -üf2t3o -üf4t3ri -üf4t3ro -üft3s -ü2fum -üge4l3a4 -üge4l3ä -ügel3d4 -üge4lec -üge6lei6s -üge4len -üge4ler -üge4lo -ügen3s -ü2g1l -ü2gn -ügs2 -ügs3t -üh1a2 -ühan3 -ühan5d -ü1he -ü2h3ei -ühe2m2 -üh3emb -üh3emi -ü4h3eng -ü4h3ent -ü4h3erf -ü4h3er4k -ü4h3er4z -ü2hex -üh1i2 -ühla4c -üh3lam -ühl3au -üh3lä -üh4l3ei -üh4lel -üh4l5ene -üh2lo -üh2l3ö -ühl4sin -üh3ne -ühn2s -üh1o2 -üh3rad -üh3reh -ühr5ei. -ühr5eie -üh4r5eis -ühren5e -ührens4 -üh3ro -ührungsgene6 -üh1s -ühs2p -üh5stei -üh3t2a -ü1hu -üh3w -ü1k -ül1a -ül2ad -ü2l3ef -ül3eim -ül3ein -üler3a4 -üler5ei -ülergene6 -ülerin6f -ülerin6t -ül3io -ül2la2 -üll3ad -üll3an -üll3au -üll3e4c -ül4l3ei -ül4lent -üller4s -üller6zeu -ül4lid -ül2l3o2 -ül2lö -ülls2 -üll3sk -ü2l1o -ü1lu -ü2m1a -ü4ment -ü2m3e2p -üme4r3a -ümerin6t -ü4m3erz -ümi2 -ü2m3id -ü2m3in -ümm2s -ü2m1u -2ün -ün1a -üna2c -ün2da -ün4d3ei -ünde4l5a -ündergene6 -ün2dr -ünd3s -ündungsgene6 -ün2dü -ünen3 -ü4n3erd -ü4n3er4l -ün2f -ünf3ac -ünf3ak -ünf3ei -ünf3le -ünf3li -ün2gl -ün3gla -ünkoh4 -ünkohl5 -ünn4st -ü2n1o -ün2st -ün3sta -ün5stei -ünste4r -ün3str -ün3t -ü1nu -ün2za -ünze4n -ün2z3i -ünzu2 -ün4z3un -ün2zw -ü1o -2üp -üpfel3 -üpf3l -ü1pi -üp2p3a -üp2p3l -ür1a -ü2r3ei -ü2r3e2l -ü4r3en4t3 -ür4f3er4 -ür4g5en4g -ürge4ra -ü1ri -ürk2l -ürme4c -ü1ro3 -üroe4 -üro5fe -ürom4 -üror4 -ürp2 -ür2rh -ürr2s -ür2st -ürte4l3 -ü1ru -ür4z3au -ür4z3in -ür2zo -ür2zö -ürzt4 -ür2zw -üsch5af -ü3schi -ü4schl -üse3e -üsen3e -üse3r4 -üse3s -üs3l -üsli3 -ü1s2p -üs2s3a -üs2s3c -üsse6l5ers -üsse4l5i -üs2s3o -üs2st -üst3a -üs4tei -üste4n -üs2t3h -üs4tl -2üß -2üt -ü2t3al -ü2t3au -ü3te -üte2n -ütens4 -üter3a -üte6r5eis -üte4rer -üterich6 -ü2t1h -ü2t1r -üt2sa -üt2se -üt2st -ütte4n3 -üt2tr -üt2za -üt2zw -1v -vab2r -2vabs -va1c -va3e2m -va3er -va3fl -v2ak -va3lam -val4en -val5ero -val5erp -val4lan -val4lo -vall3s4 -val4sa -val4sp -val4su -2v3amt -v3ansa -v3ansc -2v3arb -var4d3r -2v3arm -2v3art -va1s -va4s3ek -2v3ass -vas2t -va4s3ta -va2t3a2 -va4tag -va2tä -va4t3ei -vatem4 -va4t5emp -va4ten4g -va4t5enz -vat5ermi -va4tess -va2t3h -va4t3id -vatik4 -va4t3in -vati6ons. -va4t3op -va4t5ora -va4t5ord -va4torg -va2t3r -vat3sa -vat3s4p -vats4t -va2t3u -va2ty -vat3z -2v1au -2v1ax -2vb2 -2v1c -2vd2 -vdi2e -ve2a -ve3ak -2ve3al -ve3an -ve3ar -veb2 -ve1c -ve1e2 -ve4ff -veg2 -2v3eig -2v3ein -veit2 -veits3 -ve3l4ar -ve4l3au -velo5p -velo3s -vembe4 -2v3emp -vendi4 -4v3endu -vene4n -4v5energ -vener6sa -ve4n3in -venk2 -ven4t5a4g -4v3entd -vent4st -venu2 -venzer5 -ven6z5ere -ve3of -ve1p -ve3pn -ve2r1 -ve3ra. -ve5rada -ve5rade -ve5rand -ve5rane -vera4s -ver6b5art -ver5blei -ver5b4li -ver3d4 -vere2 -ve3rec -ve3reg -ve5reif -ve5reig -vere6in -verein6s5tr -ve3rem -ve3res -ver5est -ve4rf4 -ver5fl -ve4r3g2 -ve3rit -verk2 -vermen6s -vern4a -ver3n4e -ver4sep -ver5sta -ver3sw -ve4rt -vert6er -verue4 -ve3rus -verü4 -verz2 -ve2sp -ves3ti -vesti4e -vete3r -veto3 -ve1u2 -ve2ur -2v1f -vf4gh -2vg2 -2v1h -vi3ana -vi3a2r3 -via3t2 -vi2c -vid3s4t -vie4h3a -vieh3s -vie4l3a4 -viele4 -vie6l5er6z -vi4ene -vier5ad -vie4rec -vie4r5ei -vie5r4er -viers4 -vie2w3 -vig2 -vi2l3a -vi2lä -vi4l3e4h -vi4lei -viler4 -vi4l5ers -vi4l3in -vil4le. -vi2l3o -vi2ma2 -vi2n3a -2v3inv -vin4zin -vire4n3 -vi3s2a3 -vise2 -visen3 -vi3si -vis4o -vi5sor -vis2u -vit2 -vi4tam -vi3z -viz4e -vize3i -vize3s -2vj -2v1k2 -v1l2 -v3le -2vlic -v2lie -2v1m -vmwa2 -2v1n -v1ob -vo2be -vob4l -voge2 -vogel3 -vo2gu -vol4l3a -vol4lei -vollen4 -vol6l5end -voller4 -vol6lerf -vol6lert -vol4li -von3s4 -vo2pe -vor1 -vo4ra -voran6schl -voran6ze -vor5de -vo2re2 -vor3es -vo4rie -vo4rim -vo4rin -vo2ro2 -vo2rö -vors2 -vorsch4 -vor5ta -voy1 -vö2c -2v1p2 -v3ps -2vq -2v1ra -v1rä -v2ree -v3ris -2vs -vsau2 -vs2c -vson2 -v1s2p -v3sta -v3stel -v3steu -v3s2tr -vsu1 -v1s2z -2vt2 -vue1 -vu3en. -2vumf -2vumg -2vumk -2vv -2vw -2vz -1w -3waa -wab4bl -w2ac -wa3cho -3wacht -wach6tra -wach6trä -wa3chu -3waeld -4waeng -3waes -w3aeu -waffe4 -waffel5 -wa3ge -3wage4n3 -wa2g3n -3wah -wahlen4 -wah6l5ent -wah4ler -wah4l3i -wahl3s4 -wai2b -wak3l -wa3la5c -3wald -wal4d3a4 -wal4dec -wal4del -wal6denk -wal4din -wal4d3r -wa3le. -wa4les -walf2 -wa3li -wal2m3 -wa2lo -wal4t3a -walter5e -wal6t5erf -wal6terl -wal4t3h -wal4t5in -wal4t3o -wal4tri -walt6sta -wal4tur -3walz -walze4 -walzen5 -walzens6 -wal4zw -wam4mei -wa3na -wa4nau -wan4dab -wan4dan -wan4d5ar -wan4d5as -wan4d5eb -wan4dec -wandels6 -5wander -wand5in -wan4dob -wan4dr -wan4due -wan5ge -wangs3 -3wann -wan4z3a -wanze4 -wan6z5en6d -wan4z5er -wa2p -3ware -wa4ref -ware3i -warein4 -wa3ren -war4ma -war4m3i -3warn -war4th -wart4st -war3u -war4za -war4z5ei -war4zel -war6zent -3was -wa3s2a -wa4scha -wa5sche. -wasch5ei -wa5schen -wa4sch5l -wa4schm -wa5schun -wa4schw -wa3se -wa3s2h -wasser5e -wa3su -3watt -watte5s -3wäh -wäh4lei -wäh3ri -3wäld -wände5s -2wäng -wärme5i -3wäs -2w1äu -2wb2 -wbu2 -2w1c -2wd -we2a -we2b3a -webeat5 -webe3e -we2b3i -we2b3l -we2bo -we2b3r -web3sp -w2ec -we3cha -we5cke. -we5cken. -we5ckes -wecks4 -wee2 -weed3 -2wef -we2fl -3weg -we2g3a -we4g3ei -we4g5en4g -we4g5erk -weg5ersc -we4gim -we4gin -we2g3l -we2g3n -we2g3o2 -we2g3r -weg5s4a -wegs4t -3weh -wehr3a4 -weh4rei -weh4r5er4 -wehr3o4 -wei5be -wei4ble -weib4s -wei3de -weide5r -wei3dr -2weie -2weif -weifel6d -weifs4 -wei4gra -weigs4 -2weik2 -3weil -wei4na -wei6nenz -wein5erz -weinsau6 -wei4san -wei4sas -weiser6g -weiser6h -weisex5 -3weish -weis6sel -weis6spi -wei5s4tr -wei6takt. -wei4tal -wei4t5ra -weit3s -wele4n3 -wel4kl -welle4 -wellen5 -wellende6f -wel6schl -wel6schr -wel4t3a4 -welte4 -wel4teb -welt5ei -wel4tel -wel6t5en6de -wel6tent -wel4ter -welt5ere -welt5ern -welt5ers -welt5erw -welt5erz -wel6t5esc -wel4th -wel4t3i -wel4to -wel4t3r -wel4t3u -wena2 -wendegene6 -wendes4 -wen3f -wen4kau -wen4k5la -wen4k5ri -we2no -wens2 -we4r3a2 -werbe5e -werbe5i -werbes4 -werb4s3 -3werbu -2werc -werde5i -5werdens -3werdu -werer4 -werga4 -wer4g5am -wer4gan -wergel6 -wer6g5els5 -wer6g5en6t -wer4g3l -wer4gn -wer4g3o -wer4g3r -wergs4 -w4e3ri -werin4d -we4ri4o -3werk. -wer4k3a -wer4kä -3werke -werk5erz -wer4k3i -wer4k3l -wer4kne -wer4kop -wer4k5or -wer4k5re -3werks -wer4ku -wer4kü -wer3o -wer3ö -wer3s4h -wer6star -wer4ta -wert5ab -wert5ak -wert5an -wer4t5än -wer6t5eig -wer6ter6g -wer6t5erm -wer6t5ero -wer6t5erö -wer4t3h -3werti -wer4t5in -wer4t3o4 -wer4tre -wer4t5ri -wert5sk -werts4t -wer4t5um -wer4t5ur -3wese -wesen4s -we2sp -we4st -west3a -weste4 -west5ec -west5ei -wes6temp -wes6t5end. -west5ende -wes6teng -wes6t5erwei -wes4tex -wes4th -wes4ti -west3o4 -wes4tor -west3r -wes4tu -2wets -3wett -wet4ta -wett3r -wett3s -we1u -2w1ey -2w1f -2wg -whi2 -whi4t -w3ho -wicht6se -wi3cka -3wid -2wieb -wi3ec3 -wiede4 -wie5la -wien4e -wi2gl -wigs4c -wi3k -wik4i -3wild -wil4d3a4 -wil4del -wil4dr -wim4ma -wim6m5ent -wim4m3u -wi3na -win4d3a -win4d5e4c -win4d5ei -win6d5erz -3win4dr -wind5ri -wine2 -winen3 -win4g3l -win4g3r -wings4 -wing5se -win4kl -win6nerh -win6n5er6sc -win6nerw -win4no -win3sk -wi2r -3wirk -wir6kene -2wirn -3wirt -wi6schef -wi4sch5l -wi4scho -wi4schr -wis4e -wi5sen -3wiss -wiss4z -wis4t3r -wi3th -3witzi -wit4zin -3witzl -w2j -2w1k -2w1l -2w1m -2wn -wn2s3p -3wo1c -wo4cha -woche4 -3woh -woh4lei -woh4l5e4t -woh4na -woh3r -3wolf -wol4fr -wol4l3a -wol4lei -wolls4 -wor3a -wo2r3i -work3a -wort3a -wort3ä -3worte -wort5ei -wor4tel -wor6t5erf -wor6t5erh -wort3h -wor4to -wor4t3r -3wort3s4 -wor3u -wor3ü -wöl4fa -wöl4fo -4wölft -wör2g -2w1p -2wq -2w1ro -2w1s -wser3e -w3s2h -w2ska -ws2ki -ws2ky -ws2t -w2s1u -2wt2 -wti2 -3wuc -wuch4sa -wuch4sc -wuch4sp -3wuec -wue3le -3wuerf -3wuerg -wuer4s -5wuerst -2w1u2f -wun4d3a -wun4dä -wund3r -2wung -wun4g3r -wung5sc -wun2s -wunsch5ei -wunsch5l -wur4f3a -wur4f5ei -wur4fel -wur4fer -wur4f3l -wur4f3o -3wurm -wur4m3a -wur4mei -wur4mer -wurmes4 -wur6m5ess -wur4m3i -wur2s -3wurst -wurst5a -wus2 -wus3te -wu2t3 -wutz2 -3wüc -3wüh -3würf -3würg -3würst -2ww -2wz -x1a -1xa. -2xab -xa2be -xa2br -xaby2 -1x2ac -1x2ad -1x2a1e2 -xa3fl -1x2ag2 -xal3a -xal2l3 -xa2m -xa3mer -2xan -x2an. -xa2na -x3anb -3x2ane -xant4h -3x4anti -3x4anzi -xa2r -1x2as -2xau -xa2z -2xb2 -2xc -xcal2 -x1ce -x1ch -x1cl -2xd -xda2 -xdi2e -1xe -2x1ef -2x3e2g -2x1ei -2xek -xe2l -3xel. -xel3a -xe3lei -2xemp -x2en -xenk2 -xen3s2 -2x1e2p -2x3erl -xer4la -xer3s2 -x3erz -2x1eu -2x1ex -2x1f2 -x3fi -2xg2 -2x1h -x2i -2xia -xib2 -xi1c -xich4 -xi3dan -xide2 -xi4d3ei -xi4d3em -xidi2 -x3i2do -3xi2e -3xik2 -xi4lar -xi4l3au -xi4l3er -xi2lo -xi2l3u -1xim -x3imp -xin3s2 -1xip -xi2s3a -3xisc -xi2s3e -xi4s3or -xiss2 -xi2su -x3i2tu -2x1k2 -xkal2 -2x2l2 -x3la -x3lä -x3le -x3lo -2x1m -xmon2 -2x1n -x1od -xo2du -2xo1e2 -x1o2l -xonen4 -xo4n5end -x1o2r -xos2 -x2ot -2x1ö2 -2x1p2 -x3par -x3pau -x3ph -x3plor -xpor4ta -xpor6t5er6 -xpor4t5r -x3pu -2x1r -2x1s2 -x3sh -xst2 -2xt -xt1a -x2ta2b -x2tak -xt4ane -x2tau -xt1ä -x2tän -x2täs -xtblo4 -xtblock5 -x2t3e2b -x2t3e2d -x4t3eig -xt3ein -x2t3el -x4t3ent -x4t3er4f -x4t3er4g -x4t3er4k -x4t3er4l -xter4s -x4t5ersc -x4t5erse -x4t3erw -x4t3er4z -x2t3e2v -x2t3ex -xtfi4l -xt1h -x2thi -x2t3id -xti4la -xti4lin -xt5il4lu -x2t3im -xt3ind -xt3inf -xt3inh -xt3ins -xt1o2 -x2tob -x2tor -xtra3b4 -xtra3c -xtra3l -xt5rand -xt5raum -xt3rec -x4t3rep -x4t3res -x4t3rol -xt3s2 -xtu3e -x2t3um -x2t3un -x2tup -xtu5ral -1xu -xu1a -xu1e -x1um -xu2n -2xunt -xu2s1 -xusa2 -xuss2 -xus3t -2x2v -x3ve -xvil2 -x3vo -2xw -1xy. -1yac -y2ach -yach4t5r -yacht5s4 -ya1h -ya1i2 -y3al. -ya3la -y3alb -yan2g -y3ank -y3anz -ya2ro -ya3s2p -y1au -y1ät -yb2r -y1c -ycer4 -y4chia -ych3n -yd3al -ydri2 -ydrid3 -ydro3 -y1e -y2ec -ye2d -y2ef -y2el -ye2ne -yen2n -3y2eno -yer3a2 -y2eri -yer2n3 -y2es. -yes2p -ye2th -y1f2 -yfen2 -yg2l -ygro3s -y1h -yhr2 -2y1i2 -y1k2 -yke3n -yk3s -yk3t -yl3ac -yl3aet -y3lak -y2l3a2m -yla2n -ylan3e -y5lant. -y5l4ante -y5l4anti -yl3ar -y3lat -yl3ät -yl3c -y2le. -yle3is -y2l3es -y2l3e2t -ylinde4 -yli4ne. -yli4nes -ylk2 -yl4l3ak -yloid3 -yloni5e -yl3ora -yl1s2 -yltri3 -y1lu -2ym -ym4ae -ym4an -ym4är -ym4el -ym1p2 -ym4pha -ym4phor -ympi3e -2yn -y1na -ynae4r -y2n3an -ynä2r -yndro6ma -y3ne. -yne4c -ynk2 -yn1o -yno2d -yno2t -yno2v -yn3t2 -ynta2 -y1nu -y2od -yof2 -yoff3 -1yo3g -yog4a3 -yonal5s4 -yon4ne -y3ont -y3o2pe -yo3ru -yo1s -y3os. -y3ot. -y1ou -yout3a -2y1p -yp3a2b -ypa2n -yp3än -y2pec -ype4r -y4pesc -y2p3i2d -ypi2e -y2p3in -y2p3lo -ypo3t2 -y2p1s -yp3th -y2p3um -y2pü -y1r -yrae3i -yras4t -yrä3i -y2rh -yri5er. -yri5ers -yro1 -yro4s5te -yrr2 -2ys -ysau2 -ys2c -yse1 -ysein4 -yse2n3 -yse3s2 -y3sho -y3s2n -ys2po -yst4e -ys5tem -yste4n -ys4the -ysti5er -ys4tol -ysto4le -ys4tri -y3s2ty -ysu2 -y2s3ur -y1s2z -yte2n3 -y2tes -yt2h -yt4he -ythi3e -y3to -yto3s2 -y1u -yu2r -yure3 -yvet2 -yvon4 -ywurs4 -y1x -y1y -yze5r4i -1z -2z1aa -2z1ab -2zacc -2z3ach -za3cha -za3chä -2z1a2d -za2e -z3ae2c -z2aeh -2z3aem -4z3aend -4z5aengs -z3aerm -za3ess -2z3aet -4z3aeus -4z3aeuß -za3f4a -2z3aff -za3fl -za2ga -za3gr -3z2ah -zah4n5er4 -za3hol -z2ai -za3im -z1ak -2z3akk -za3kl -2z3alb -2z3ald -zal4e -2z3all -2z3alp -2z3alt -2z1am -2z3a2na3 -2zanb -zan3d2 -2zanf -4z3anga -4z5angeb -z5angel -z3angl -4z3angs -2zanh -zan4ka -zan4kl -2zanl -2z3anm -2zano2 -2zanp -2zanr -2z3ans2 -z4ans. -zanti3 -2zanw -zao2 -zapf3l -2z3a2po -zap4ple -2z1a2q -3zar. -2z3arb -2zarc -zare3s -2z3arg -2z3arm -4z3arti -zar4t3r -zarts4 -2z3arz -z1as -zas2b -zas2p -z1at -3z4aub -2z3au2f3 -2z3aug -2z3auk -3zaun -z3aur -z3ausb -z3ausf -4z3ausg -4z3ausl -4z3auss -4z3ausw -2z3auß -2z3aut -2z1ä2c -zäh4lei -zäh4lin -4zähnl -zäh3r -2z1äm -2z3änd -z3ängs -z1äp -z3ärg -2z3ärm -2z1ät -2zäug -2z3äus -2zäuß -2zäx -2zb2 -z3be -zber4e -z3bo -zbue3b -zbü3b -zby2 -2z1c -2zd2 -zdan2 -zdä1 -zdi2e -zdi5str -z3ds -zdue3b -zdü3b -4z3e4ben -zebra3 -4z3echo -zech3s -zed2 -ze4dit -ze1e2 -2z3eff -zef2l -zeg2 -ze4gal -ze4hem -zehe4n3 -zeh2l -zehn3a -zeh4n5en -zehns4 -zei5cho -zei3f2 -zeik2 -zei5le4 -zeilen5 -2z3ein -ze3in. -z4ein4a -zein4de -zei4n3e -zeinen4 -z4eino -ze5in4se -zeis2 -zei3sk -zei3sp -zei3st -zei4t3a -zei4tä -zeite4 -zei4teb -zei4tei -zei4tel -zei4t5er5 -zei4t3o -zei4tr -zeit5re -zeit5ri -zeit6sto -ze2la -zel5abr -zel3a4d -zel3ae -zel3am -zela4n -zel5an5e -zel5ang -zel3au -ze2l3ä -zel3d4 -ze4led -zelei6er -ze4l5eig -ze5leis -z3e4lek -4zelekt -4zeleme -ze4l3en -ze4l3er -zeler5e -ze4les -zel3ex -zel3id -zel3in -ze4linf -4z3e4lit -zel4lab -zel4l5ac -zellar6t -zel6lein -zeller6z -zell3s4 -ze2lo -zel3ob -zelp2 -zels4t -zel5ste -zel3sz -zel4tau -zel4t5in -zelu2 -zel5unk -ze4lus -zembe4 -zem4e -ze4mis -2z3emp -zen4ade -zena4g -ze5natt -zen3au -ze2nä -zende4c -zende4k -zende4s -ze3nec -zen5en5e -zen5erke -zen5ern -zen5ertr -ze4nerw -zen3f2 -4z5en4ge. -zenge6ne -zengene6r -4zengp -zeng4r -ze4nid -zen5inn -zenk2 -zen3n -ze2n3o -zen4san -zensch4 -zen4sem -zen4ser -zens4po -4z5entna -zent5rae -zent3s -zen4z5ar -zen6z5ers -zen6zert -zen6zerw -zen4zin -zen4zy -zeo2r -ze4pen -ze3p2f -zeppe3 -zer3ad -zer3ae -zer3a4g -zer3an -ze4r3at -ze2r3ä -6z5ereign -ze4rein -ze4remi -zer5end -zer5erw -ze4r3e4t -4z5erfol -4z5erfül -4z5ergae -4z3ergä -zer4gon -4z3er4gu -ze4r3i4d -zerin6di -zerin4f -zerin4h -zerin6te -5zerl. -zer6lang -zer6laub -zer4läu -4z5erleb -zer4lei -zerlo4 -zer6maec -4z5er4mäc -5zerme -zer4nan -zer4n5e4b -zer6nerb -zer5obe -ze3ros -ze4rou -ze3rös -2z3erq -zers2 -z5ersae -4z5ersat -zersau4 -4z3ersä -zer3sk -6z5er6strec -zer4t5a4g -zer4tar -zerte4 -zer6t5emp -zerten4 -zerter5 -zer6t5ere -zer6terl -zer4t5in -zer4ton -zer6t5rau -zert3s4 -6z5erwerb -zer4zä -z4es. -zes2a -zes3au -ze3sch -zes3er -zes4ph -zes4sa4 -zess5ag -zess5an -zes4seb -zessen4 -zes6s5end -zes4ser4 -zess5erf -zess5erz -zes6s5ort. -zes6s5or6te. -zes6s5or6ten -zes6s5orts -zes4spa -zes4spo -zes4sum -zes4s5ur4 -ze5stau -zes4tie -ze5stra -4z3e4tap -4zetat -2z3e2th -4zetts -zeug3a -zeu4gei -zeu4gel -zeu4g5e4m -zeuge4n -zeu6g5ersc -zeu4g3r -2z3eul -ze3um -ze3ur -2z1e2x1 -2z1f2 -zfen2 -2zg2 -zge3n4a -zger4a -zge6r5in6nu -zger4s5e -2z1h2 -zhau4t3 -zi3af -zi5alei -zi5aleng -zi5alim -zi3ang -zi2ar -zib2 -zi3cho -zich6t5er6 -zi4d3ak -zi4d3ei -3zie -zie4hei -zie4l3a -ziele4 -zie4l5eb -zie4lei -zie4l5er -zie4let -zie4l3i -zi3ell -zie2n -zierde4 -zie4r5ei -zi3es. -zi3ess -zi3est -zi3esu -zi3esz -zif4fr -zi3fl -zi4gec -4z3i4gel -zig3sp -zi4kon -zil4e -zi4mit -2z3imp -zim2t3 -4z3inde -zi4n5er4z -zi4n3et -zi4n3in -4z3i4n3it -zin4kae -zin6k5erz -zin4k3l -zinks4 -zin4na -zin4sa -zin5sas -4z5insel -zin4ser -4zinsuf -zi2o -zio3d -zi3on. -zi3ons -zip4a -zirk2 -zirks3 -4zirri -zi3s2z -3zit -zi4t3an -zite2 -zithe4 -zi2t3o2 -2zj -2z1k2 -z3ko -zko3ef -z3kr -zkue3b -zkü3b -z3ky -2z1l -zlan6d5er -zlei5ti -zle3s -z3lo -2z1m2 -zmau4s -2z1n2 -zna5che -z3ne -znebe4 -znei3 -2z3o4as -2z1ob -2z3oe2d -zoe2f -3z4oeg -2z3oek -4zoel. -4z3oels -z1of -zo2gl -2z1oh -zo4lig -zol4l3a4 -zol4l5ei -zol6l5en6t5 -zoller4 -zol6lerf -zol6l5erk -zol6l5erl -zol6lert -zol4lin -zol4lo -zolls4 -zoll5ste -z3o2ly -3zon -zon3s2 -zon4teb -zon4t5er -3zo2o -zoo3s -2z3o2pe -zop4f5en -2zopt -2z3ora -z3orc -z3ord -z3org -zo2ri -zor4n3a -zor4ner -2z3ort -2z3osz -zo2t3a -2z1ou -zo2w -2z1ox -zo2ze -2z1ö2d -2z1ö2f -3zög -2z1ök -z1öl -2zöl. -2zöle -z2öli -z2öll -2zöls -2z1p2 -z3pn -2zq -2z1r2 -z3ra -z3ru -2z1s2 -z3sa -zsau2 -zsch2 -zse3r4u -z3sh -zsi1 -z3sk -z3sl -z3sp -zst2 -zsu1 -zsui2 -2zt -zt3abr -zta2n -z4t3an3e -z4t5anze -z2t3ar -ztee2 -zte3eh -zt3ehe -zt5eins -z4t3ent -z4t3erz -z3t4es -zte3s4k -zte5str -zt3hel -z2t3hi -z2t3ho -z2thu -zt3ind -zt3inf -zt3ins -zt4or -zt4ram -zt3rec -zt3s2 -z4t3urs -ztz2 -zu1 -zub2 -zucht5o -zucht5r -zuchts4 -zud2 -zudi4e -2zueb -zu3ebb -zu3ebn -zue2k -zu2el -zu2en -zu3ent -zu4erc -zu4ern -zue4r3o4 -zu4ers. -zue2t -zu3f2 -zu4g5ans -zu4g3ar -zu4g3at -zug5eis -zu4g5ent -zugg2 -zug5inn -zu4gla -zu4glä -zu4glö -zug2r -zug5rau -zug4san -2z3uhr -zui2 -zu3le -2z3ult -zu3lu -2z3um. -2z3umb -zumen4s -2zumf -2zumg -2z3umh -2zumk -2zuml -zu3mo -2zumr -2z3ums -2z3um4t -zu3m2u -2z3umw -4z3unab -zun4am -zu3n2e -4z3unem -4z3unfa -4zunfä -zunge4n5 -4zunget -4z3ungl -zungs3 -4z3u4nio -2zunr -2z3uns -2zunt -2zunw -zuo2 -zup4fi -zu3r4a -2z3urk -2z3url -2z3urn -2z3urs -2z3urt -zu3s2 -zu5sch4 -zus4e -zus4p -zu3t4a -zu3te -zut4r -zuve4 -zuz2 -2züb -zür3c -2zv -4zwaer -4zwaes -2zwaf -2zwag -2zwah -2zwal -4zwand -z4wang -2z3wap -2zwa2s -zwasse4 -2zwäc -2zwär -2zwäs -2zweb -z2wec -4z3wech -2zwed -2zweg -zweg3s -2zweh -5z4weig -4zweil -4zwein -zwei5sc -zwei6t5er6k -2zwel -2zwen -z4werg -4zwerk -4zwert -2zwes -z2wic -2zwid -zwi2e -4z5wied -4zwild -5z4wing -4z5wint -4zwirr -4zwirt -z4wisc -4zwiss -z4wist -2zwur -zwur4m3 -2zwü -zy3an. -5zyklu -zy2le -zy2m3i -2zz -zz2a -zza3t -z2z3id -z2z3i2k -zzi2n -zz3ini -zzi3s2 -zz2o -zzol2 -z2z3or -zz2ö -zzug4s -zz2w -} - -\endinput - -%%% Local Variables: -%%% mode: tex -%%% coding: utf-8 -%%% End: diff --git a/assets/hyphenation/en-us.dic b/assets/hyphenation/en-us.dic deleted file mode 100644 index a09009145a91ed230ba7739bd6170cad6a041a45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 432905 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¨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 ZDFvhl4GvE78S%`V!0Z9poR!@7?)R>d_ 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�PgV@1NofK-u6 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 z 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 -% Permission is hereby granted, free of charge, to any person -% obtaining a copy of this software and associated documentation -% files (the "Software"), to deal in the Software without -% restriction, including without limitation the rights to use, -% copy, modify, merge, publish, distribute, sublicense, and/or sell -% copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following -% conditions: -% -% The above copyright notice and this permission notice shall be -% included in all copies or substantial portions of the Software. -% -% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -% OTHER DEALINGS IN THE SOFTWARE. -% hyphenmins: -% typesetting: -% left: 2 -% right: 2 -% changes: -% - 2014-04-22 - Add few patterns involving `h' -% - 2011-08-16 - Change the licence from GNU LGPL into LPPL v1.3. -% - 2010-05-24 - Fix for Italian patterns for proper hyphenation of -ich and Ljubljana. -% - 2008-06-09 - Import of original ithyph.tex into hyph-utf8 package. -% - 2008-03-08 - (last change in ithyph.tex) -% texlive: -% encoding: ascii -% babelname: italian -% legacy_patterns: ithyph.tex -% message: Italian hyphenation patterns -% description: |- -% Hyphenation patterns for Italian in ASCII encoding. -% Compliant with the Recommendation UNI 6461 on hyphenation -% issued by the Italian Standards Institution -% (Ente Nazionale di Unificazione UNI). -% ========================================== -% -% These hyphenation patterns for the Italian language are supposed to comply -% with the Recommendation UNI 6461 on hyphenation issued by the Italian -% Standards Institution (Ente Nazionale di Unificazione UNI). No guarantee -% or declaration of fitness to any particular purpose is given and any -% liability is disclaimed. -% -\patterns{ -.a3p2n % After the Garzanti dictionary: a-pnea, a-pnoi-co,... -.anti1 -.anti3m2n -.bio1 -.ca4p3s -.circu2m1 -.contro1 -.di2s3cine -.e2x1eu -.fran2k3 -.free3 -.li3p2sa -.narco1 -.opto1 -.orto3p2 -.para1 -.ph2l -.ph2r -.poli3p2 -.pre1 -.p2s -.re1i2scr -.sha2re3 -.tran2s3c -.tran2s3d -.tran2s3l -.tran2s3n -.tran2s3p -.tran2s3r -.tran2s3t -.su2b3lu -.su2b3r -.wa2g3n -.wel2t1 -2'2 -a1ia -a1ie -a1io -a1iu -a1uo -a1ya -2at. -e1iu -e2w -o1ia -o1ie -o1io -o1iu -1b -2bb -2bc -2bd -2bf -2bm -2bn -2bp -2bs -2bt -2bv -b2l -b2r -2b. -2b' -1c -2cb -2cc -2cd -2cf -2ck -2cm -2cn -2cq -2cs -2ct -2cz -2chh -c2h -2ch. -2ch'. -2ch''. -2chb -ch2r -2chn -c2l -c2r -2c. -2c' -.c2 -1d -2db -2dd -2dg -2dl -2dm -2dn -2dp -d2r -2ds -2dt -2dv -2dw -2d. -2d' -.d2 -1f -2fb -2fg -2ff -2fn -f2l -f2r -2fs -2ft -2f. -2f' -1g -2gb -2gd -2gf -2gg -g2h -g2l -2gm -g2n -2gp -g2r -2gs -2gt -2gv -2gw -2gz -2gh2t -2g. -2g' -.h2 -1h -2hb -2hd -2hh -hi3p2n -h2l -2hm -2hn -2hr -2hv -2h. -2h' -.j2 -1j -2j. -2j' -.k2 -1k -2kg -2kf -k2h -2kk -k2l -2km -k2r -2ks -2kt -2k. -2k' -1l -2lb -2lc -2ld -2l3f2 -2lg -l2h -l2j -2lk -2ll -2lm -2ln -2lp -2lq -2lr -2ls -2lt -2lv -2lw -2lz -2l. -2l'. -2l'' -1m -2mb -2mc -2mf -2ml -2mm -2mn -2mp -2mq -2mr -2ms -2mt -2mv -2mw -2m. -2m' -1n -2nb -2nc -2nd -2nf -2ng -2nk -2nl -2nm -2nn -2np -2nq -2nr -2ns -n2s3fer -2nt -2nv -2nz -n2g3n -2nheit -2n. -2n' -1p -2pd -p2h -p2l -2pn -3p2ne -2pp -p2r -2ps -3p2sic -2pt -2pz -2p. -2p' -1q -2qq -2q. -2q' -1r -2rb -2rc -2rd -2rf -r2h -2rg -2rk -2rl -2rm -2rn -2rp -2rq -2rr -2rs -2rt -r2t2s3 -2rv -2rx -2rw -2rz -2r. -2r' -1s2 -2shm -2sh. -2sh' -2s3s -s4s3m -2s3p2n -2stb -2stc -2std -2stf -2stg -2stm -2stn -2stp -2sts -2stt -2stv -2sz -4s. -4s'. -4s'' -.t2 -1t -2tb -2tc -2td -2tf -2tg -t2h -2th. -t2l -2tm -2tn -2tp -t2r -t2s -3t2sch -2tt -t2t3s -2tv -2tw -t2z -2tzk -tz2s -2t. -2t'. -2t'' -1v -2vc -v2l -v2r -2vv -2v. -2v'. -2v'' -1w -w2h -wa2r -2w1y -2w. -2w' -1x -2xb -2xc -2xf -2xh -2xm -2xp -2xt -2xw -2x. -2x' -y1ou -y1i -1z -2zb -2zd -2zl -2zn -2zp -2zt -2zs -2zv -2zz -2z. -2z'. -2z'' -.z2 -} % Pattern end - diff --git a/assets/hyphenation/pl.pat b/assets/hyphenation/pl.pat deleted file mode 100644 index 1a3c6c1d3..000000000 --- a/assets/hyphenation/pl.pat +++ /dev/null @@ -1,4154 +0,0 @@ -% title: Hyphenation patterns for Polish -% copyright: Copyright (C) 1987-1995 Hanna Kołodziejska, Bogusław Jackowski, Marek Ryćko -% notice: This file is part of the hyph-utf8 package. -% See http://www.hyphenation.org/tex for more information. -% language: -% name: Polish -% tag: pl -% version: 3.0a, Wednesday, May 17th, 1995 -% authors: -% - -% name: Hanna Kołodziejska -% - -% name: Bogusław Jackowski -% contact: b_jackowski (at) gust.org.pl -% - -% name: Marek Ryćko -% contact: marek (at) do.com.pl -% licence: -% - This file is made available under any of the following licences: -% - -% name: MIT -% url: https://opensource.org/licenses/MIT -% text: > -% Permission is hereby granted, free of charge, to any person -% obtaining a copy of this software and associated documentation -% files (the “Software”), to deal in the Software without -% restriction, including without limitation the rights to use, -% copy, modify, merge, publish, distribute, sublicense, and/or sell -% copies of the Software, and to permit persons to whom the -% Software is furnished to do so, subject to the following -% conditions: -% -% The above copyright notice and this permission notice shall be -% included in all copies or substantial portions of the Software. -% -% THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, -% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -% OTHER DEALINGS IN THE SOFTWARE. -% - text: > -% This macro file belongs to the public domain -% under the conditions specified by the author of TeX: -% -% “Macro files like PLAIN.TEX should not be changed in any way, -% except with respect to preloaded fonts, -% unless the changes are authorized by the authors of the macros.” -% -% Donald E. Knuth -% hyphenmins: -% typesetting: -% left: 2 -% right: 2 -% changes: -% - The first version of the patterns was developed -% by Hanna Kołodziejska (1987). -% - The adaptation to the LeX format (see below) and extensive modification -% were done by Bogusław Jackowski & Marek Ryćko (1987--1989). -% - The hyphenation rules were further improved and adapted to the -% TeX 3.x requirements by Hanna Kołodziejska (1991). -% - Lone-standing version (3.0a) of patterns was prepared (under pressure -% from LaTeX users) by Bogusław Jackowski and Marek Ryćko, following -% Mariusz Olko's suggestions, 1995. -% - The LeX format mentioned above was the first version of the adaptation -% of TeX to the Polish language. The next version is called MeX. -% texlive: -% encoding: qx -% babelname: polish -% legacy_patterns: plhyph.tex -% message: Polish hyphenation patterns -% description: |- -% Hyphenation patterns for Polish in QX and UTF-8 encodings. -% These patterns are also used by Polish TeX formats MeX and LaMeX. -% -\patterns{ -.ćć8 -.ćł8 -.ćń8 -.ćś8 -.ćź8 -.ćż8 -.ć8 -.ćb8 -.ćc8 -.ćd8 -.ćf8 -.ćg8 -.ćh8 -.ćj8 -.ćk8 -.ćl8 -.ćm8 -.ćn8 -.ćp8 -.ćr8 -.ćs8 -.ćt8 -.ćv8 -.ćw8 -.ćwier2ć3 -.ćx8 -.ćz8 -.łć8 -.łł8 -.łń8 -.łś8 -.łź8 -.łż8 -.ł8 -.łb8 -.łc8 -.łd8 -.łf8 -.łg8 -.łh8 -.łj8 -.łk8 -.łl8 -.łm8 -.łn8 -.łp8 -.łr8 -.łs8 -.łt8 -.łv8 -.łw8 -.łx8 -.łz8 -.ńć8 -.ńł8 -.ńń8 -.ńś8 -.ńź8 -.ńż8 -.ń8 -.ńb8 -.ńc8 -.ńd8 -.ńf8 -.ńg8 -.ńh8 -.ńj8 -.ńk8 -.ńl8 -.ńm8 -.ńn8 -.ńp8 -.ńr8 -.ńs8 -.ńt8 -.ńv8 -.ńw8 -.ńx8 -.ńz8 -.ść8 -.śł8 -.śń8 -.śś8 -.śź8 -.śż8 -.ś8 -.śb8 -.śc8 -.śd8 -.śf8 -.śg8 -.śh8 -.śj8 -.śk8 -.śl8 -.śm8 -.śn8 -.śp8 -.śró2d5 -.śródr2 -.śr8 -.śs8 -.śt8 -.śv8 -.św8 -.światło3w2 -.śx8 -.śz8 -.źć8 -.źł8 -.źń8 -.źś8 -.źź8 -.źż8 -.ź8 -.źb8 -.źc8 -.źdź8 -.źd8 -.źf8 -.źg8 -.źh8 -.źj8 -.źk8 -.źl8 -.źm8 -.źn8 -.źp8 -.źr8 -.źs8 -.źt8 -.źv8 -.źw8 -.źx8 -.źz8 -.żć8 -.żł8 -.żń8 -.żś8 -.żź8 -.żż8 -.ż8 -.żb8 -.żc8 -.żd8 -.żf8 -.żg8 -.żh8 -.żj8 -.żk8 -.żl8 -.żm8 -.żn8 -.żp8 -.żr8 -.żs8 -.żt8 -.żv8 -.żw8 -.żx8 -.żz8 -.a2b2s3t -.a2d3 -.ad4a -.ad4e -.ad4i -.ad4o -.ad4u -.ad4y -.ad5apt -.ad5iu -.ad5op -.ad5or -.ae3ro -.aeroa2 -.aeroe2 -.aeroi2 -.aeroo2 -.aerou2 -.antya2 -.antye2 -.antyi2 -.antyo2 -.antyu2 -.arcy3ł2 -.arcy3b2 -.arcy3bz2 -.arcy3k2 -.arcy3m2 -.arcya2 -.arcye2 -.arcyi2 -.arcyo2 -.arcyu2 -.au3g2 -.au3k2 -.au3t2 -.auto3ch2 -.autoa2 -.autoe2 -.autoi2 -.autoo2 -.autotran2s3 -.autou2 -.bć8 -.bł8 -.bń8 -.bś8 -.bź8 -.bż8 -.b8 -.bb8 -.bc8 -.bd8 -.be2z3 -.be3z4an -.be3z4ec -.be3z4ik -.bezch2 -.bezm2 -.bezo2 -.bezo2b1j -.bezw2 -.bezzw2 -.bf8 -.bg8 -.bh8 -.bj8 -.bk8 -.bl8 -.bm8 -.bn8 -.bp8 -.br8 -.brz8 -.bs8 -.bt8 -.bv8 -.bw8 -.bx8 -.bz8 -.cć8 -.cł8 -.cń8 -.cś8 -.cź8 -.cż8 -.c8 -.cało3ś2 -.cało3k2 -.cb8 -.cc8 -.cd8 -.cf8 -.cg8 -.ch8 -.chrz8 -.cienko3w2 -.ciepło3kr2 -.cj8 -.ck8 -.cl8 -.cm8 -.cn8 -.cp8 -.cr8 -.cs8 -.ct8 -.cv8 -.cw8 -.cx8 -.cz8 -.czarno3k2 -.czk8 -.cztere2ch3 -.czterechse2t3 -.cztero3ś2 -.czwó2r3 -.czwó3r4ą -.czwó3r4ę -.czwó3r4a -.czwó3r4e -.czwó3r4o -.dć8 -.dł8 -.długo3tr2 -.długo3w2 -.dń8 -.dś8 -.dź8 -.dż8 -.d8 -.daleko3w2 -.db8 -.dc8 -.dd8 -.de2z3 -.de3z4a3bil -.de3z4a3wu -.de3z4el -.de3z4er -.de3z4y -.deza2 -.dezo2 -.df8 -.dg8 -.dh8 -.dj8 -.dk8 -.dl8 -.dm8 -.dn8 -.do3ć2 -.do3ł2 -.do3ś2 -.do3ź2 -.do3ż2 -.do3b2 -.do3c2 -.do3d2 -.do3f2 -.do3g2 -.do3h2 -.do3k2 -.do3l2 -.do3m2 -.do3p2 -.do3r2 -.do3s2 -.do3t2 -.do3w2 -.do3z2 -.do4ł3k -.do4k3t -.do4l3n -.do4m3k -.do4r3s -.do4w3c -.do5m4k2n -.dobr2 -.dobrz2 -.doch2 -.docz2 -.dodź2 -.dodż2 -.dodz2 -.dogrz2 -.dopch2 -.doprz2 -.dorż2 -.dorz2 -.dosch2 -.dosm2 -.dosz2 -.dotk2 -.dotr2 -.dp8 -.dr8 -.drogo3w2 -.drz8 -.ds8 -.dt8 -.dv8 -.dwó2j3 -.dwó3j4ą -.dwó3j4ę -.dwó3j4a -.dwó3j4e -.dwó3j4o -.dw8 -.dx8 -.dy2s3 -.dy2z3 -.dy3s4e -.dy3s4o -.dy3s4ta -.dy3s4y -.dy3sz -.dy3z4e -.dyzu2 -.dz8 -.dziesięcio3ś2 -.dziewięćse2t3 -.dziewię2ć3 -.dziewięcio3ś2 -.e2k2s3 -.e2m3e2s5ze2t -.e2s1e2s1ma -.e2s1ha -.e2s1t -.egoa2 -.egoe2 -.egoi2 -.egoo2 -.egou2 -.eks4y -.elektroa2 -.elektroe2 -.elektroi2 -.elektroo2 -.elektrou2 -.fć8 -.fł8 -.fń8 -.fś8 -.fź8 -.fż8 -.f8 -.fb8 -.fc8 -.fd8 -.ff8 -.fg8 -.fh8 -.fj8 -.fk8 -.fl8 -.fm8 -.fn8 -.fp8 -.fr8 -.fs8 -.ft8 -.fv8 -.fw8 -.fx8 -.fz8 -.gć8 -.gł8 -.gń8 -.gś8 -.gź8 -.gż8 -.g8 -.gb8 -.gc8 -.gd8 -.ge2o3 -.gf8 -.gg8 -.gh8 -.gj8 -.gk8 -.gl8 -.gm8 -.gn8 -.go2u3 -.gp8 -.gr8 -.grubo3w2 -.grz8 -.gs8 -.gt8 -.gv8 -.gw8 -.gx8 -.gz8 -.hć8 -.hł8 -.hń8 -.hś8 -.hź8 -.hż8 -.h8 -.hb8 -.hc8 -.hd8 -.hf8 -.hg8 -.hh8 -.hipe2r3 -.hipe3r4o -.hipera2 -.hipere2 -.hj8 -.hk8 -.hl8 -.hm8 -.hn8 -.hp8 -.hr8 -.hs8 -.ht8 -.hv8 -.hw8 -.hx8 -.hz8 -.i2n3 -.i2s3l -.i3n4ic -.i3n4o -.i3n4u -.i4n5o2k -.in4f3lan -.ino3w2 -.izoa2 -.izoe2 -.izoi2 -.izoo2 -.izou2 -.jć8 -.jł8 -.jń8 -.jś8 -.jź8 -.jż8 -.j8 -.jadło3w2 -.jb8 -.jc8 -.jd8 -.jf8 -.jg8 -.jh8 -.jj8 -.jk8 -.jl8 -.jm8 -.jn8 -.jp8 -.jr8 -.js8 -.jt8 -.jv8 -.jw8 -.jx8 -.jz8 -.kć8 -.kł8 -.kń8 -.kś8 -.kź8 -.kż8 -.k8 -.kb8 -.kc8 -.kd8 -.kf8 -.kg8 -.kh8 -.kilkuse2t3 -.kilkuseto2 -.kj8 -.kk8 -.kl8 -.km8 -.kn8 -.koło3w2 -.kon2t2r3 -.kon3tr4a -.kon3tr4e -.kon3tr4o3l -.kon3tr4o3w -.kon3tr4y -.kon4tr5a2gi -.kon4tr5a2se -.kon4tr5a2sy -.kon4tr5a2ta -.kon4tr5adm -.kon4tr5akc -.kon4tr5alt -.kon4tr5arg -.kontro2 -.kontru2 -.kp8 -.krótko3tr2 -.krótko3w2 -.kr8 -.kro2ć3 -.krz8 -.ks8 -.kt8 -.kv8 -.kw8 -.kx8 -.kz8 -.lć8 -.lł8 -.lń8 -.lś8 -.lź8 -.lż8 -.l8 -.lb8 -.lc8 -.ld8 -.lf8 -.lg8 -.lh8 -.lj8 -.lk8 -.ll8 -.lm8 -.ln8 -.lp8 -.lr8 -.ls8 -.lt8 -.ludo3w2 -.lv8 -.lw8 -.lx8 -.lz8 -.mć8 -.mł8 -.mń8 -.mś8 -.mź8 -.mż8 -.m8 -.mb8 -.mc8 -.md8 -.mf8 -.mg8 -.mh8 -.mili3amp -.mj8 -.mk8 -.ml8 -.mm8 -.mn8 -.możno3w2 -.mp8 -.mr8 -.ms8 -.mt8 -.mv8 -.mw8 -.mx8 -.mz8 -.nć8 -.nł8 -.nń8 -.nś8 -.nź8 -.nż8 -.n8 -.na2d2 -.na2j -.na3ć2 -.na3ł2 -.na3ś2 -.na3ź2 -.na3ż2 -.na3b2 -.na3c2 -.na3dą -.na3dę -.na3dź2 -.na3d4łub -.na3d4ir -.na3d4much -.na3d4ręcz -.na3d4r2w -.na3d4repcz -.na3d4rept -.na3d4ruk -.na3d4rz -.na3d4worn -.na3daj -.na3de -.na3do -.na3dy -.na3dzi -.na3f2 -.na3g2 -.na3h2 -.na3ją -.na3ję -.na3jazd -.na3je -.na3k2 -.na3l2 -.na3m2 -.na3p2 -.na3r2 -.na3s2 -.na3t2 -.na3u2 -.na3w2 -.na3z2 -.na4d3o2b2ł -.na4d3o2bojcz -.na4d3o2bowi -.na4d3o2brot -.na4d3o2drz -.na4d3o2kien -.na4d3olbrz -.na4d5rzą -.na4d5rzę -.na4d5rzecz -.na4d5rzy -.na4d5ziem -.na4f3c -.na4f3t -.na4j3e2f -.na4j3e2g -.na4j3e2k2s -.na4j3e2ko -.na4j3e2n -.na4j3e2r -.na4j3e2s -.na4j3e2w -.na4j3emf -.na4j3eu -.na4r3c -.na4r3d -.na4r3k -.na4r3r -.na4r3t -.nabrz2 -.nach2 -.nacz2 -.nadśrod5ziem -.nad3ć2 -.nad3ł2 -.nad3ś2 -.nad3b2 -.nad3c2 -.nad3d2 -.nad3e2tat -.nad3f2 -.nad3g2 -.nad3h2 -.nad3i2 -.nad3j2 -.nad3k2 -.nad3l2 -.nad3m2 -.nad3n2 -.nad3p2 -.nad3r2 -.nad3s2 -.nad3t2 -.nad3u2 -.nad3w2 -.nad5ż2 -.nad5zó -.nad5z2mys -.nad5zo -.nad5zwycz -.nadch2 -.nadcz2 -.naddź2 -.nade3ć2 -.nade3ł2 -.nade3ś2 -.nade3ź2 -.nade3ż2 -.nade3b2 -.nade3c2 -.nade3d2 -.nade3f2 -.nade3g2 -.nade3h2 -.nade3k2 -.nade3l2 -.nade3m2 -.nade3p2 -.nade3r2 -.nade3s2 -.nade3t2 -.nade3w2 -.nade3z2 -.nade4p3c -.nade4p3n -.nade4p3t -.nadech2 -.nadecz2 -.nadedź2 -.nadedż2 -.nadedz2 -.naderż2 -.naderz2 -.nadesz2 -.nadsz2 -.nadtr2 -.nadz2 -.nagrz2 -.naj3ć2 -.naj3ł2 -.naj3ś2 -.naj3ź2 -.naj3ż2 -.naj3akt -.naj3au -.naj3b2 -.naj3c2 -.naj3d2 -.naj3f2 -.naj3g2 -.naj3h2 -.naj3i2 -.naj3k2 -.naj3l2 -.naj3m2 -.naj3o2 -.naj3o2ć2 -.naj3o2ł2 -.naj3o2ś2 -.naj3o2ź2 -.naj3o2ż2 -.naj3o2b2 -.naj3o2c2 -.naj3o2d2 -.naj3o2f2 -.naj3o2g2 -.naj3o2h2 -.naj3o2k2 -.naj3o2l2 -.naj3o2m2 -.naj3o2p2 -.naj3o2r2 -.naj3o2s2 -.naj3o2t2 -.naj3o2w2 -.naj3o2z2 -.naj3p2 -.naj3r2 -.naj3ro2z3 -.naj3s2 -.naj3t2 -.naj3u2 -.naj3w2 -.naj3z2 -.najbe2z3 -.najbezw2 -.najch2 -.najcz2 -.najdź2 -.najdż2 -.najdo3ć2 -.najdo3ł2 -.najdo3ś2 -.najdo3ź2 -.najdo3ż2 -.najdo3b2 -.najdo3c2 -.najdo3d2 -.najdo3f2 -.najdo3g2 -.najdo3h2 -.najdo3k2 -.najdo3l2 -.najdo3m2 -.najdo3p2 -.najdo3r2 -.najdo3s2 -.najdo3t2 -.najdo3w2 -.najdo3z2 -.najdoch2 -.najdocz2 -.najdodź2 -.najdodż2 -.najdodz2 -.najdorz2 -.najdosz2 -.najdotk2 -.najdz2 -.najkr2 -.najob3ć2 -.najob3ł2 -.najob3ś2 -.najob3ź2 -.najob3ż2 -.najob3c2 -.najob3d2 -.najob3f2 -.najob3g2 -.najob3h2 -.najob3j2 -.najob3k2 -.najob3l2 -.najob3m2 -.najob3n2 -.najob3p2 -.najob3s2 -.najob3t2 -.najob3w2 -.najobch2 -.najobcz2 -.najobdź2 -.najobdż2 -.najobdz2 -.najobrz2 -.najobsz2 -.najoch2 -.najocz2 -.najodź2 -.najod3ć2 -.najod3ś2 -.najod3c2 -.najod3d2 -.najod3f2 -.najod3g2 -.najod3h2 -.najod3j2 -.najod3k2 -.najod3l2 -.najod3m2 -.najod3n2 -.najod3p2 -.najod3s2 -.najod3t2 -.najod3w2 -.najod5ż2 -.najodch2 -.najodcz2 -.najoddź2 -.najoddż2 -.najoddz2 -.najodsz2 -.najodz2 -.najorz2 -.najosz2 -.najro3z4u -.najrz2 -.najsm2 -.najsz2 -.najtk2 -.najtr2 -.najucz2 -.najzw2 -.nakr2 -.napo2d2 -.napo3ć2 -.napo3ł2 -.napo3ś2 -.napo3ź2 -.napo3ż2 -.napo3b2 -.napo3c2 -.napo3f2 -.napo3g2 -.napo3h2 -.napo3k2 -.napo3l2 -.napo3m2 -.napo3p2 -.napo3r2 -.napo3s2 -.napo3t2 -.napo3w2 -.napo3z2 -.napo4m3p -.napoch2 -.napocz2 -.napodź2 -.napodż2 -.napod3d -.napomk2 -.naporz2 -.naposz2 -.naprz2 -.narż2 -.naro2z3 -.narz2 -.nasm2 -.nasz2 -.natch2 -.natk2 -.naz3m2 -.nazw2 -.nb8 -.nc8 -.nd8 -.ne2o3 -.nf8 -.ng8 -.nh8 -.nie3ć2 -.nie3ł2 -.nie3ś2 -.nie3ź2 -.nie3ż2 -.nie3b2 -.nie3c2 -.nie3d2 -.nie3f2 -.nie3g2 -.nie3h2 -.nie3k2 -.nie3l2 -.nie3m2 -.nie3p2 -.nie3r2 -.nie3s2 -.nie3t2 -.nie3u2 -.nie3w2 -.nie3z2 -.nie4c3c -.nie4c3k -.nie4dź3 -.nie4m3c -.nie4m3k -.niech2 -.niecz2 -.niedż2 -.niedo3ć2 -.niedo3ł2 -.niedo3ś2 -.niedo3ź2 -.niedo3ż2 -.niedo3b2 -.niedo3c2 -.niedo3d2 -.niedo3f2 -.niedo3g2 -.niedo3h2 -.niedo3k2 -.niedo3l2 -.niedo3m2 -.niedo3p2 -.niedo3r2 -.niedo3s2 -.niedo3t2 -.niedo3w2 -.niedo3z2 -.niedobrz2 -.niedoch2 -.niedocz2 -.niedodź2 -.niedodż2 -.niedodz2 -.niedokr2 -.niedomk2 -.niedopch2 -.niedorz2 -.niedosz2 -.niedotk2 -.niedz2 -.nieoć2 -.nieoł2 -.nieoś2 -.nieoź2 -.nieoż2 -.nieo2 -.nieob2 -.nieob3ć2 -.nieob3ś2 -.nieob3ź2 -.nieob3ż2 -.nieob3c2 -.nieob3d2 -.nieob3f2 -.nieob3g2 -.nieob3h2 -.nieob3j2 -.nieob3k2 -.nieob3m2 -.nieob3p2 -.nieob3s2 -.nieob3w2 -.nieobch2 -.nieobcz2 -.nieobdź2 -.nieobdż2 -.nieobdz2 -.nieobsz2 -.nieoc2 -.nieoch2 -.nieocz2 -.nieodź2 -.nieod2 -.nieod3ć2 -.nieod3ł2 -.nieod3ś2 -.nieod3c2 -.nieod3d2 -.nieod3f2 -.nieod3g2 -.nieod3h2 -.nieod3j2 -.nieod3k2 -.nieod3l2 -.nieod3n2 -.nieod3p2 -.nieod3s2 -.nieod3t2 -.nieod3wr -.nieod5ż2 -.nieodch2 -.nieodcz2 -.nieoddź2 -.nieoddż2 -.nieoddz2 -.nieodsz2 -.nieodw2 -.nieodz2 -.nieof2 -.nieog2 -.nieoh2 -.nieok2 -.nieol2 -.nieom2 -.nieop2 -.nieor2 -.nieorz2 -.nieos2 -.nieosz2 -.nieot2 -.nieow2 -.nieoz2 -.niepo2d2 -.niepo3ć2 -.niepo3ł2 -.niepo3ś2 -.niepo3ź2 -.niepo3ż2 -.niepo3b2 -.niepo3c2 -.niepo3dź2 -.niepo3d4łu -.niepo3d4much -.niepo3d4ręcz -.niepo3d4raż -.niepo3d4rap -.niepo3d4repcz -.niepo3d4rept -.niepo3d4waj -.niepo3d4woj -.niepo3do -.niepo3du -.niepo3dz2 -.niepo3f2 -.niepo3g2 -.niepo3h2 -.niepo3k2 -.niepo3l2 -.niepo3m2 -.niepo3p2 -.niepo3r2 -.niepo3s2 -.niepo3t2 -.niepo3w2 -.niepo3z2 -.niepo4d3o2choc -.niepo4d3o2strz -.niepoch2 -.niepocz2 -.niepod3ć2 -.niepod3ł2 -.niepod3ś2 -.niepod3b2 -.niepod3c2 -.niepod3d2 -.niepod3f2 -.niepod3g2 -.niepod3h2 -.niepod3j2 -.niepod3k2 -.niepod3l2 -.niepod3m2 -.niepod3n2 -.niepod3p2 -.niepod3r2 -.niepod3s2 -.niepod3t2 -.niepod3w2 -.niepod5ż -.niepodch2 -.niepodcz2 -.niepoddź2 -.niepoddż2 -.niepodsm2 -.niepodsz2 -.nieporz2 -.nieposm2 -.nieposz2 -.nieprzełk2 -.nieprze2d2 -.nieprze3ć2 -.nieprze3ł2 -.nieprze3ś2 -.nieprze3ź2 -.nieprze3ż2 -.nieprze3b2 -.nieprze3brz2 -.nieprze3c2 -.nieprze3dź2 -.nieprze3d4łuż -.nieprze3d4much -.nieprze3d4ramat -.nieprze3d4ruk -.nieprze3d4ryl -.nieprze3d4rz2 -.nieprze3d4um -.nieprze3dy -.nieprze3dz2 -.nieprze3e2k2s3 -.nieprze3f2 -.nieprze3g2 -.nieprze3h2 -.nieprze3k2 -.nieprze3l2 -.nieprze3m2 -.nieprze3n2 -.nieprze3p2 -.nieprze3r2 -.nieprze3s2 -.nieprze3t2 -.nieprze3w2 -.nieprze3z2 -.nieprze4d5łużyc -.nieprze4d5ż2 -.nieprze4d5z2a -.nieprze4d5zg2 -.nieprze4d5zim -.nieprze4d5zj -.nieprze4d5zl -.nieprze4d5zw2r -.nieprze4d5zwoj -.nieprzech2 -.nieprzecz2 -.nieprzed3ć2 -.nieprzed3ł2 -.nieprzed3ś2 -.nieprzed3c2 -.nieprzed3d2 -.nieprzed3f2 -.nieprzed3g2 -.nieprzed3h2 -.nieprzed3i2 -.nieprzed3j2 -.nieprzed3k2 -.nieprzed3l2 -.nieprzed3m2 -.nieprzed3n2 -.nieprzed3p2 -.nieprzed3r2 -.nieprzed3s2 -.nieprzed3sz2 -.nieprzed3t2 -.nieprzed3u2 -.nieprzed3w2 -.nieprzedch2 -.nieprzedcz2 -.nieprzeddź2 -.nieprzeddż2 -.nieprzeddz2 -.nieprzegrz2 -.nieprzekl2 -.nieprzekr2 -.nieprzepch2 -.nieprzerż2 -.nieprzerz2 -.nieprzesch2 -.nieprzesm2 -.nieprzesz2 -.nieprzetk2 -.nieprzetr2 -.niero2z3 -.niero3z4e -.niero3z4u -.nierozś2 -.nierozbrz2 -.nieroze3r2 -.nierozm2 -.nieroztr2 -.nierz2 -.niesu2b3 -.niesu3b4ie -.niesz2 -.nietk2 -.nietr2 -.nieucz2 -.nieuw2 -.niewy3ć2 -.niewy3ł2 -.niewy3ś2 -.niewy3ź2 -.niewy3ż2 -.niewy3b2 -.niewy3c2 -.niewy3d2 -.niewy3f2 -.niewy3g2 -.niewy3h2 -.niewy3k2 -.niewy3l2 -.niewy3m2 -.niewy3p2 -.niewy3r2 -.niewy3s2 -.niewy3t2 -.niewy3w2 -.niewy3z2 -.niewybrz2 -.niewych2 -.niewycz2 -.niewydź2 -.niewydż2 -.niewydz2 -.niewyrz2 -.niewysz2 -.niewytk2 -.niewytr2 -.niezw2 -.nj8 -.nk8 -.nl8 -.nm8 -.nn8 -.np8 -.nr8 -.ns8 -.nt8 -.nv8 -.nw8 -.nx8 -.nz8 -.oć2 -.oś2 -.ośmio3ś2 -.oź2 -.oż2 -.o2b2 -.o2d2 -.o2t3chł -.o3b4łą -.o3b4łę -.o3b4łoc -.o3b4luzg -.o3b4rać -.o3b4raso -.o3b4roń -.o3b4ron -.o3b4ryź -.o3b4ryz -.o3b4rz2 -.o3be -.o3bi -.o3d4iu -.o3d4ręt -.o3d4rap -.o3d4robin -.o3d4rut -.o3d4rwi -.o3d4rzeć -.o3d4rzw -.o3d6zia -.o3d6zie -.o3de -.o3l2śn -.o4b5łocz -.o4b5rzą -.o4b5rzęd -.o4b5rzez -.o4b5rzuc -.o4b5rzut -.o4b5rzyn -.o4d7ziar -.o4d7ziem -.oa3z -.ob3ć2 -.ob3ł2 -.ob3ś2 -.ob3ź2 -.ob3ż2 -.ob3c2 -.ob3d2 -.ob3f2 -.ob3g2 -.ob3h2 -.ob3j2 -.ob3k2 -.ob3l2 -.ob3m2 -.ob3n2 -.ob3o2strz -.ob3p2 -.ob3r -.ob3s2 -.ob3t2 -.ob3u2m2 -.ob3w2 -.obch2 -.obcz2 -.obdź2 -.obdż2 -.obdz2 -.obe3ć2 -.obe3ł2 -.obe3ś2 -.obe3ź2 -.obe3ż2 -.obe3b2 -.obe3c2 -.obe3d2 -.obe3f2 -.obe3g2 -.obe3h2 -.obe3k2 -.obe3l2 -.obe3m2 -.obe3p2 -.obe3r2 -.obe3r3t -.obe3s2 -.obe3t2 -.obe3w2 -.obe3z2 -.obe4c3n -.obe4z3w -.obech2 -.obecz2 -.obedź2 -.obedż2 -.obedz2 -.oberż2 -.ober3m -.oberz2 -.obesch2 -.obesz2 -.obetk2 -.obi3b2 -.obsz2 -.oc2 -.och2 -.ochrz2 -.ocz2 -.odź2 -.od3ć2 -.od3ś2 -.od3au -.od3b2 -.od3c2 -.od3d2 -.od3f2 -.od3g2 -.od3h2 -.od3i2 -.od3i2zo -.od3j2 -.od3k2 -.od3l2 -.od3m2 -.od3n2 -.od3o2s -.od3p2 -.od3r2 -.od3s2 -.od3t2 -.od3u2cz -.od3u2m2 -.od3w2 -.od5ż2 -.od5z2 -.odbe2z3 -.odch2 -.odcz2 -.oddź2 -.oddż2 -.oddz2 -.ode3ć2 -.ode3ł2 -.ode3ś2 -.ode3ź2 -.ode3ż2 -.ode3b2 -.ode3c2 -.ode3d2 -.ode3f2 -.ode3g2 -.ode3h2 -.ode3k2 -.ode3l2 -.ode3m2 -.ode3mk2 -.ode3p2 -.ode3r2 -.ode3s2 -.ode3t2 -.ode3w2 -.ode3z2 -.odech2 -.odecz2 -.odedź2 -.odedż2 -.odedz2 -.odepch2 -.oderż2 -.oderz2 -.odesz2 -.odetch2 -.odetk2 -.odkrz2 -.odrz2 -.odsz2 -.of2 -.ogólno3k2 -.og2 -.ognio3tr2 -.oh2 -.ok2 -.oka3m2 -.okr2 -.ole2o3 -.om2 -.op2 -.opch2 -.or2ż2 -.or2tę -.or2z2 -.os2 -.osie2m3 -.osiemse2t3 -.osz2 -.ot2 -.ow2 -.oz2 -.pć8 -.pł8 -.płasko3w2 -.pń8 -.półk2 -.półkr2 -.półm2 -.póło2 -.półob3r -.półom2d -.półprzy3m2k -.pó2ł3 -.pó3ł4ą -.pó3ł4ę -.pó3ł4ecz -.pó3ł4y -.pś8 -.pź8 -.pż8 -.p8 -.pb8 -.pc8 -.pch8 -.pd8 -.pełno3kr2 -.pe2r3 -.pe3c2k -.pe3r4e -.pe3r4i -.pe3r4o -.pe3r4u -.pe3r4y -.pe4r5i2n -.pee2se2l -.pepee2r -.pepee2s -.peze2t1pee2r -.pf8 -.pg8 -.ph8 -.pięćse2t3 -.pię2ć3 -.pięcio3ś2 -.pierwo3w2 -.piono3w2 -.pj8 -.pk8 -.pl8 -.pm8 -.pn8 -.połk2 -.po2d2 -.po3ć2 -.po3ł2 -.po3ś2 -.po3ź2 -.po3ż2 -.po3b2 -.po3c2 -.po3dą -.po3dę -.po3dź2 -.po3d4łu -.po3d4much -.po3d4naw -.po3d4ręcz -.po3d4rętw -.po3d4róż -.po3d4r2wi -.po3d4raż -.po3d4rap -.po3d4repcz -.po3d4rept -.po3d4roż -.po3d4robó -.po3d4roba -.po3d4robo -.po3d4roby -.po3d4rocz -.po3d4ruzg -.po3d4ryg -.po3d4rze -.po3d4wójn -.po3d4wór -.po3d4waj -.po3d4woi -.po3d4woj -.po3d4worz -.po3da -.po3de -.po3dej -.po3diu -.po3do -.po3du -.po3dy -.po3dz2 -.po3e2k2s3 -.po3f2 -.po3g2 -.po3h2 -.po3k2 -.po3l2 -.po3m2 -.po3p2 -.po3rż -.po3r2 -.po3s2 -.po3t2 -.po3w2 -.po3z2 -.po4ń3c -.po4cz3d -.po4cz3t -.po4d3ów -.po4d3e4k2s3 -.po4d3o2bóz -.po4d3o2biad -.po4d3o2bojcz -.po4d3o2braz -.po4d3o2choc -.po4d3o2dm -.po4d3o2f -.po4d3o2g -.po4d3o2kien -.po4d3o2kn -.po4d3o2kręg -.po4d3o2kres -.po4d3o2piecz -.po4d3o2ryw -.po4d3o2siniak -.po4d3o2strz -.po4d3obsz -.po4d3odd -.po4d3olbrz -.po4d3u2cz -.po4d3u2dz -.po4d3u2pa -.po4d3u2ral -.po4d3u2sta -.po4d3u2szcz -.po4d5ręczn -.po4d5zakr -.po4d5zam -.po4d5zast -.po4d5zbi -.po4d5ze -.po4d5zielenią -.po4d5zielenić -.po4d5zielenię -.po4d5zielenił -.po4d5zielenic -.po4d5zielenien -.po4d5zielenil -.po4d5zielenim -.po4d5zielenio -.po4d5zielenis -.po4d5ziem -.po4d5ziom -.po4d5zw2r -.po4l3s -.po4m3p -.po4r3c -.po4r3f -.po4r3n -.po4r3t -.po4st3d -.po4st3f -.po4st3g -.po4st3h -.po4st3i2 -.po4st3k -.po4st3l -.po4st3m -.po4st3p -.po4st3rom -.po4st3s -.po5d4uszczyn -.po5r4tę -.pobr2 -.pobrz2 -.poch2 -.pochrz2 -.pocz2 -.pod3ć2 -.pod3ł2 -.pod3ś2 -.pod3śró2d5 -.pod3alp -.pod3b2 -.pod3c2 -.pod3d2 -.pod3f2 -.pod3g2 -.pod3h2 -.pod3i2n -.pod3j2 -.pod3k2 -.pod3l2 -.pod3m2 -.pod3n2 -.pod3p2 -.pod3r2 -.pod3s2 -.pod3t2 -.pod3w2 -.pod5ż2 -.podch2 -.podcz2 -.poddź2 -.poddż2 -.pode3ć2 -.pode3ł2 -.pode3ś2 -.pode3ź2 -.pode3ż2 -.pode3b2 -.pode3c2 -.pode3d2 -.pode3f2 -.pode3g2 -.pode3h2 -.pode3k2 -.pode3l2 -.pode3m2 -.pode3p2 -.pode3r2 -.pode3s2 -.pode3t2 -.pode3tk2 -.pode3w2 -.pode3z2 -.podech2 -.podecz2 -.podedź2 -.podedż2 -.podedz2 -.podepch2 -.poderż2 -.poderz2 -.podesch2 -.podesz2 -.podro2z3 -.podsm2 -.podsz2 -.pogrz2 -.pokl2 -.pokr2 -.pom4pk -.pomk2 -.pona2d2 -.pona3ć2 -.pona3ł2 -.pona3ś2 -.pona3ź2 -.pona3ż2 -.pona3b2 -.pona3c2 -.pona3cz2 -.pona3dź2 -.pona3do -.pona3f2 -.pona3g2 -.pona3h2 -.pona3k2 -.pona3l2 -.pona3m2 -.pona3p2 -.pona3r2 -.pona3s2 -.pona3t2 -.pona3w2 -.pona3z2 -.pona4f3t -.ponabrz2 -.ponach2 -.ponad3ć2 -.ponad3ś2 -.ponad3c2 -.ponad3ch2 -.ponad3cz2 -.ponad3dź2 -.ponad3f2 -.ponad3g2 -.ponad3h2 -.ponad3j2 -.ponad3k2 -.ponad3l2 -.ponad3p2 -.ponad3s2 -.ponad3t2 -.ponadz2 -.ponarz2 -.ponasm2 -.ponasz2 -.ponaz3m2 -.ponazw2 -.ponie3k2 -.ponie3w2 -.popch2 -.popo3w2 -.poprz2 -.por4t1w -.por4tf -.por4tm -.poro2z3 -.poro3z4u -.porz2 -.posch2 -.posm2 -.posz2 -.potk2 -.potr2 -.poz4m2 -.poza3u2 -.pozw2 -.pp8 -.pr8 -.pra3s2 -.pra3w2nu -.pra3w2z -.prapra3w2nu -.predy2s3po -.prz8 -.przełk2 -.prze2d2 -.prze3ć2 -.prze3ł2 -.prze3ś2 -.prze3ź2 -.prze3ż2 -.prze3b2 -.prze3c2 -.prze3dą -.prze3dę -.prze3dź2 -.prze3d4łuż -.prze3d4much -.prze3d4o3br -.prze3d4o3st -.prze3d4o3zo -.prze3d4ramat -.prze3d4ruk -.prze3d4ryl -.prze3d4rz2 -.prze3d4um -.prze3dy -.prze3dz2 -.prze3e2k2s3 -.prze3f2 -.prze3g2 -.prze3h2 -.prze3k2 -.prze3l2 -.prze3m2 -.prze3n2 -.prze3p2 -.prze3r2 -.prze3s2 -.prze3t2 -.prze3u2 -.prze3w2 -.prze3z2 -.prze4d5łużyc -.prze4d5ż2 -.prze4d5o4stat -.prze4d5za -.prze4d5zg2 -.prze4d5zim -.prze4d5zj -.prze4d5zl -.prze4d5zw2r -.prze4d5zwoj -.przebr2 -.przebrz2 -.przech2 -.przechrz2 -.przeci2w3 -.przeci3w4ie -.przeciwa2 -.przeciww2 -.przecz2 -.przed3ć2 -.przed3ł2 -.przed3ś2 -.przed3a2gon -.przed3a2kc -.przed3alp -.przed3b2 -.przed3c2 -.przed3d2 -.przed3e2gz -.przed3e2mer -.przed3f2 -.przed3g2 -.przed3h2 -.przed3i2 -.przed3j2 -.przed3k2 -.przed3l2 -.przed3m2 -.przed3n2 -.przed3o2 -.przed3p2 -.przed3r2 -.przed3s2 -.przed3się3w2 -.przed3sz2 -.przed3t2 -.przed3u2 -.przed3w2 -.przedch2 -.przedcz2 -.przeddź2 -.przeddż2 -.przeddz2 -.przedgrz2 -.przedy2s3ku -.przegrz2 -.przekl2 -.przekr2 -.przemk2 -.przepch2 -.przerż2 -.przerz2 -.przesch2 -.przesm2 -.przesz2 -.przetk2 -.przetr2 -.przetran2s3 -.przy3ć2 -.przy3ł2 -.przy3ś2 -.przy3ź2 -.przy3ż2 -.przy3b2 -.przy3c2 -.przy3d2 -.przy3f2 -.przy3g2 -.przy3h2 -.przy3k2 -.przy3l2 -.przy3m2 -.przy3p2 -.przy3r2 -.przy3s2 -.przy3t2 -.przy3w2 -.przy3z2 -.przybr2 -.przych2 -.przycz2 -.przydź2 -.przydż2 -.przydz2 -.przygrz2 -.przymk2 -.przyoz2 -.przypch2 -.przyrż2 -.przyrz2 -.przysch2 -.przysz2 -.przytk2 -.ps8 -.pt8 -.pv8 -.pw8 -.px8 -.pz8 -.rć8 -.rł8 -.rń8 -.rś8 -.rź8 -.rż8 -.r8 -.rb8 -.rc8 -.rd8 -.retran2s3 -.rf8 -.rg8 -.rh8 -.rj8 -.rk8 -.rl8 -.rm8 -.rn8 -.ro2z3 -.ro3z4a -.ro3z4e -.ro3z4e3ć2 -.ro3z4e3ł2 -.ro3z4e3ś2 -.ro3z4e3ź2 -.ro3z4e3ż2 -.ro3z4e3b2 -.ro3z4e3c2 -.ro3z4e3d2 -.ro3z4e3f2 -.ro3z4e3g2 -.ro3z4e3h2 -.ro3z4e3k2 -.ro3z4e3l2 -.ro3z4e3m2 -.ro3z4e3p2 -.ro3z4e3r2 -.ro3z4e3s2 -.ro3z4e3t2 -.ro3z4e3w2 -.ro3z4e3z2 -.ro3z4ej -.ro3z4u -.ro4z5a2gi -.ro4z5a2nie -.ro4z5e2mo -.ro4z5e4g3z -.ro4z5e4n3t -.rozś2 -.rozbrz2 -.rozd2 -.rozech2 -.rozecz2 -.rozedź2 -.rozedż2 -.rozedz2 -.rozepch2 -.rozerż2 -.rozerz2 -.rozesch2 -.rozesz2 -.rozi2 -.rozm2 -.rozo2 -.rozpo3w2 -.rozt2 -.roztr2 -.rozw2 -.rp8 -.rr8 -.rs8 -.rt8 -.rv8 -.rw8 -.rx8 -.rz8 -.sć8 -.sł8 -.sń8 -.sś8 -.sź8 -.sż8 -.s8 -.samo3ch2 -.samo3k2 -.samo3p2 -.samo3w2 -.samoro2z3 -.sb8 -.sc8 -.sch8 -.sd8 -.sf8 -.sg8 -.sh8 -.siede2m3 -.siedemse2t3 -.siedmio3ś2 -.sj8 -.ską2d5że -.sk8 -.skl8 -.skr8 -.sl8 -.sm8 -.sn8 -.sobo3w2 -.spó2ł3 -.sp8 -.spo2d2 -.spo3ć2 -.spo3ł2 -.spo3ś2 -.spo3ź2 -.spo3ż2 -.spo3b2 -.spo3c2 -.spo3dz2 -.spo3f2 -.spo3g2 -.spo3h2 -.spo3k2 -.spo3l2 -.spo3m2 -.spo3p2 -.spo3r2 -.spo3s2 -.spo3t2 -.spo3w2 -.spo3z2 -.spo4r3n -.spo4r3t -.spoch2 -.spocz2 -.spodź2 -.spodż2 -.spod3d -.sporz2 -.sposz2 -.sr8 -.ss8 -.st8 -.stere2o3 -.stereoa2 -.stereoe2 -.stereoi2 -.stereoo2 -.stereou2 -.su2b3 -.su3b4ie -.su3b4otn -.supe2r3 -.supe3r4at -.supe3r4io -.supe4r5a2tr -.super5z2b -.supere2 -.supero2d1rzut -.sv8 -.sw8 -.sx8 -.sz8 -.sześćse2t3 -.sześcio3ś2 -.sze2ś2ć3 -.sze2s3 -.tć8 -.tł8 -.tń8 -.tś8 -.tź8 -.tż8 -.t8 -.ta2o3 -.ta2r7zan -.tb8 -.tc8 -.tch8 -.td8 -.te2o3 -.tf8 -.tg8 -.th8 -.tj8 -.tk8 -.tl8 -.tm8 -.tn8 -.toa3 -.tp8 -.tró2j3 -.tró3j4ą -.tró3j4ę -.tró3j4ecz -.tr8 -.tran2s3 -.tran3s4e -.tran3s4ie -.tran3s4y -.tran3sz -.tran4s5eu -.transa2 -.transo2 -.trz8 -.trze2ch3 -.trzechse2t3 -.ts8 -.tt8 -.tv8 -.tw8 -.tx8 -.tysią2c3 -.tysią3c4a -.tysią3c4e -.tysią3cz -.tysią4c5zł -.tz8 -.uć2 -.uś2 -.u3ł2 -.u3ź2 -.u3ż2 -.u3b2 -.u3c2 -.u3d2 -.u3f2 -.u3g2 -.u3h2 -.u3k2 -.u3l2 -.u3m2 -.u3n2 -.u3p2 -.u3r2 -.u3s2 -.u3t2 -.u3w2 -.u3z2 -.u4d3k -.u4f3n -.u4k3lej -.u4l3s -.u4l3t -.u4m3br -.u4n3c -.u4n3d -.u4p3p2s -.u4r3s -.u4st3n -.u4stc -.u4stk -.u4z3be -.ube2z3 -.ubezw2 -.ubr2 -.uch2 -.ucz2 -.udź2 -.udż2 -.udz2 -.ukr2 -.umk2 -.upch2 -.upo2d2 -.upo3ć2 -.upo3ł2 -.upo3ś2 -.upo3ź2 -.upo3ż2 -.upo3b2 -.upo3c2 -.upo3da -.upo3f2 -.upo3g2 -.upo3h2 -.upo3k2 -.upo3l2 -.upo3m2 -.upo3p2 -.upo3r2 -.upo3s2 -.upo3t2 -.upo3w2 -.upo3z2 -.upoch2 -.upocz2 -.upodź2 -.upodż2 -.upod3d -.uporz2 -.uposz2 -.urż2 -.uro2z3 -.urz2 -.usch2 -.usz2 -.utk2 -.utr2 -.uze3w2 -.vć8 -.vł8 -.vń8 -.vś8 -.vź8 -.vż8 -.v8 -.vb8 -.vc8 -.vd8 -.vf8 -.vg8 -.vh8 -.vj8 -.vk8 -.vl8 -.vm8 -.vn8 -.vp8 -.vr8 -.vs8 -.vt8 -.vv8 -.vw8 -.vx8 -.vz8 -.wć8 -.wł8 -.wń8 -.wś8 -.wź8 -.wż8 -.w8 -.wb8 -.wc8 -.wd8 -.we3ć2 -.we3ł2 -.we3ś2 -.we3ż2 -.we3b2 -.we3c2 -.we3d2 -.we3f2 -.we3g2 -.we3h2 -.we3k2 -.we3l2 -.we3m2 -.we3n2 -.we3p2 -.we3r2 -.we3s2 -.we3t2 -.we3w2 -.we3z2 -.we4ł3n -.we4k3t -.we4l3w -.we4n3d -.we4n3t -.we4r3b -.we4r3d -.we4r3n -.we4r3s -.we4r3t -.we4s3prz -.we4s3tch2 -.we4z3br -.we4z3gł -.wech2 -.wecz2 -.wedź2 -.wedż2 -.wedz2 -.wemk2 -.wepch2 -.werz2 -.wesz2 -.wetk2 -.wewną2trz3 -.wf8 -.wg8 -.wh8 -.wielo3ś2 -.wielo3d2 -.wielo3k2 -.wieluse2t3 -.wilczo3m2 -.wj8 -.wk8 -.wl8 -.wm8 -.wn8 -.wniebo3w2 -.wodo3w2 -.wp8 -.wr8 -.ws8 -.współi2 -.współo2b3w -.współu2 -.współw2 -.wspó2ł3 -.wsze2ch3 -.wszecho2 -.wszechw2 -.wt8 -.wv8 -.ww8 -.wx8 -.wy3ć2 -.wy3ł2 -.wy3ś2 -.wy3ź2 -.wy3ż2 -.wy3b2 -.wy3c2 -.wy3d2 -.wy3f2 -.wy3g2 -.wy3h2 -.wy3k2 -.wy3l2 -.wy3m2 -.wy3o2d3r -.wy3p2 -.wy3r2 -.wy3s2 -.wy3t2 -.wy3w2 -.wy3z2 -.wy4ż3sz -.wy4cz3ha -.wybr2 -.wybrz2 -.wych2 -.wycz2 -.wydź2 -.wydż2 -.wydr2 -.wydz2 -.wye2k2s3 -.wygrz2 -.wyi2zo -.wykl2 -.wykr2 -.wykrz2 -.wymk2 -.wypch2 -.wyprz2 -.wyrż2 -.wyrz2 -.wysch2 -.wysm2 -.wysz2 -.wytch2 -.wytk2 -.wytr2 -.wz8 -.xć8 -.xł8 -.xń8 -.xś8 -.xź8 -.xż8 -.x8 -.xb8 -.xc8 -.xd8 -.xf8 -.xg8 -.xh8 -.xj8 -.xk8 -.xl8 -.xm8 -.xn8 -.xp8 -.xr8 -.xs8 -.xt8 -.xv8 -.xw8 -.xx8 -.xz8 -.zć8 -.zł8 -.zło3w2 -.zń8 -.zś8 -.zź8 -.zż8 -.z8 -.za3ć2 -.za3ł2 -.za3ś2 -.za3ź2 -.za3ż2 -.za3b2 -.za3c2 -.za3d2 -.za3f2 -.za3g2 -.za3h2 -.za3k2 -.za3l2 -.za3m2 -.za3o2b3r -.za3o2b3s -.za3p2 -.za3r2 -.za3s2 -.za3t2 -.za3u2 -.za3w2 -.za3z2 -.za4k3t -.za4l3g -.za4l3k -.za4l3t -.za4m3k -.za4r3ch -.za4uto -.za5m4k2n -.zabr2 -.zabrz2 -.zach2 -.zacz2 -.zadź2 -.zadż2 -.zadośću4 -.zado2ść3 -.zadr2 -.zady2s3po -.zadz2 -.zagrz2 -.zai2n3 -.zai2zo -.zain4ic -.zakl2 -.zakr2 -.zakrz2 -.zanie3d2 -.zarż2 -.zarz2 -.zasch2 -.zasm2 -.zasz2 -.zatk2 -.zatr2 -.zb8 -.zc8 -.zd8 -.zde2z3 -.zde3z4awu -.zde3z4el -.zde3z4er -.zde3z4y -.zdy2s3kont -.zdy2s3kred -.zdy2s3kwal -.ze3ć2 -.ze3ł2 -.ze3ś2 -.ze3ź2 -.ze3ż2 -.ze3b2 -.ze3c2 -.ze3d2 -.ze3f2 -.ze3g2 -.ze3h2 -.ze3k2 -.ze3l2 -.ze3m2 -.ze3p2 -.ze3r2 -.ze3s2 -.ze3t2 -.ze3tk2 -.ze3w2 -.ze3z2 -.ze4r3k -.ze4t3e2m1e2s -.ze4t3e2s1e2l -.ze4t3emp -.ze4t3hap -.zech2 -.zecz2 -.zedź2 -.zedż2 -.zedz2 -.zekl2 -.zepch2 -.zerż2 -.zerz2 -.zesch2 -.zesm4 -.zesz2 -.zf8 -.zg8 -.zh8 -.zimno3kr2 -.zj8 -.zk8 -.zl8 -.zm8 -.zmartwy2ch3 -.zmartwychw2 -.zn8 -.znie3ć2 -.znie3ł2 -.znie3ń2 -.znie3ś2 -.znie3ź2 -.znie3ż2 -.znie3b2 -.znie3c2 -.znie3d2 -.znie3f2 -.znie3g2 -.znie3h2 -.znie3k2 -.znie3l2 -.znie3m2 -.znie3n2 -.znie3p2 -.znie3r2 -.znie3s2 -.znie3t2 -.znie3w2 -.znie3z2 -.znie4dź3 -.znie4m3c -.zniech2 -.zniecz2 -.zniedż2 -.zniedz2 -.znierz2 -.zniesz2 -.zo2o3 -.zp8 -.zr8 -.zro2z3 -.zro3z4u -.zs8 -.zt8 -.zv8 -.zw8 -.zx8 -.zz8 -ą1 -ę1 -ó1 -ó4w3cz -ś1c -ź2dź -1ś2ci -2ć1ń -2ć1ś -2ć1ź -2ć1ż -2ć1b -2ć1c -2ć1d -2ć1f -2ć1g -2ć1k -2ć1m -2ć1n -2ć1p -2ć1s -2ć1t -2ć1z -2ł1ć -2ł1ń -2ł1ś -2ł1ź -2ł1ż -2ł1b -2ł1c -2ł1d -2ł1f -2ł1g -2ł1h -2ł1j -2ł1k -2ł1l -2ł1m -2ł1n -2ł1p -2ł1r -2ł1s -2ł1t -2ł1w -2ł1z -2ń1ć -2ń1ł -2ń1ń -2ń1ś -2ń1ź -2ń1ż -2ń1b -2ń1c -2ń1d -2ń1f -2ń1g -2ń1h -2ń1j -2ń1k -2ń1l -2ń1m -2ń1n -2ń1p -2ń1r -2ń1s -2ń1t -2ń1w -2ń1z -2śćc -2ś1ś -2ś1ź -2ś1ż -2ś1b -2ś1d -2ś1f -2ś1g -2ś1k -2ś1p -2ś1s -2ś1t -2ś1z -2ślm -2śln -2ź1ć -2ź1ś -2ź1ż -2ź1b -2ź1c -2ź1d -2ź1f -2ź1g -2ź1k -2ź1l -2ź1m -2ź1n -2ź1p -2ź1s -2ź1t -2ź1w -2ź1z -2ż1ć -2ż1ł -2ż1ń -2ż1ś -2ż1ź -2ż1b -2ż1c -2ż1d -2ż1f -2ż1g -2ż1j -2ż1k -2ż1l -2ż1m -2ż1n -2ż1p -2ż1r -2ż1s -2ż1t -2ż1w -2ż1z -2błk -2b1ć -2b1ń -2b1ś -2b1ź -2b1ż -2b1c -2b1d -2b1f -2b1g -2b1k -2b1m -2b1n -2b1p -2b1s -2b1t -2b1z -2brn -2c1ć -2c1ń -2c1ś -2c1ź -2c1ż -2c1b -2c1d -2c1f -2c1g -2c1k -2c1l -2c1m -2c1n -2c1p -2c1s -2c1t -2ch1ć -2ch1ń -2ch1ś -2ch1ź -2ch1ż -2ch1b -2ch1c -2ch1d -2ch1f -2ch1g -2ch1k -2ch1m -2ch1n -2ch1p -2ch1s -2ch1t -2ch1z -2cz1ć -2cz1ń -2cz1ś -2cz1ź -2cz1ż -2cz1b -2cz1c -2cz1d -2cz1f -2cz1g -2cz1k -2cz1l -2cz1m -2cz1n -2cz1p -2cz1s -2cz1t -2cz1z -2dłb -2dłsz -2dź1ć -2dź1ń -2dź1ś -2dź1ź -2dź1ż -2dź1b -2dź1c -2dź1d -2dź1f -2dź1g -2dź1k -2dź1m -2dź1n -2dź1p -2dź1s -2dź1t -2dź1z -2dż1ć -2dż1ń -2dż1ś -2dż1ź -2dż1ż -2dż1b -2dż1c -2dż1d -2dż1f -2dż1g -2dż1k -2dż1m -2dż1n -2dż1p -2dż1s -2dż1t -2dż1z -2d1ć -2d1ń -2d1ś -2d1b -2d1c -2d1f -2d1g -2d1k -2d1m -2d1n -2d1p -2d1s -2d1t -2drn -2dz1ć -2dz1ń -2dz1ś -2dz1ź -2dz1ż -2dz1b -2dz1c -2dz1d -2dz1f -2dz1g -2dz1k -2dz1l -2dz1m -2dz1n -2dz1p -2dz1s -2dz1t -2dz1z -2f1c -2f1k -2f1m -2f1n -2głb -2g1ć -2g1ń -2g1ś -2g1ź -2g1ż -2g1b -2g1c -2g1d -2g1f -2g1k -2g1m -2g1p -2g1s -2g1t -2g1z -2h1ć -2h1ł -2h1ń -2h1ś -2h1ź -2h1ż -2h1b -2h1c -2h1d -2h1f -2h1g -2h1j -2h1k -2h1l -2h1m -2h1n -2h1p -2h1r -2h1s -2h1t -2h1w -2h1z -2j1ć -2j1ł -2j1ń -2j1ś -2j1ź -2j1ż -2j1b -2j1c -2j1d -2j1f -2j1g -2j1h -2j1k -2j1l -2j1m -2j1n -2j1p -2j1r -2j1s -2j1t -2j1w -2j1z -2kłb -2k1ć -2k1ń -2k1ś -2k1ź -2k1ż -2k1b -2k1c -2k1d -2k1f -2k1g -2k1m -2k1n -2k1p -2k1s -2k1sz -2k1t -2k1z -2l1ć -2l1ł -2l1ń -2l1ś -2l1ź -2l1ż -2l1b -2l1c -2l1d -2l1f -2l1g -2l1h -2l1j -2l1k -2l1m -2l1n -2l1p -2l1r -2l1s -2l1t -2l1w -2l1z -2m1ć -2m1ł -2m1ń -2m1ś -2m1ź -2m1ż -2m1b -2m1c -2m1d -2m1f -2m1g -2m1h -2m1j -2m1k -2m1l -2m1n -2m1p -2m1r -2m1s -2m1t -2m1w -2m1z -2n1ć -2n1ł -2n1ń -2n1ś -2n1ź -2n1ż -2n1b -2n1c -2n1d -2n1f -2n1g -2n1h -2n1j -2n1k -2n1l -2n1m -2n1p -2n1r -2n1s -2n1t -2n1w -2n1z -2ntn -2p1ć -2p1ń -2p1ś -2p1ź -2p1ż -2p1b -2p1c -2p1d -2p1f -2p1g -2p1k -2p1m -2p1n -2p1s -2p1sz -2p1t -2p1z -2pln -2r1ć -2r1ł -2r1ń -2r1ś -2r1ź -2r1ż -2r1b -2r1c -2r1d -2r1f -2r1g -2r1h -2r1j -2r1k -2r1l -2r1m -2r1n -2r1p -2r1s -2r1t -2r1w -2rz1ć -2rz1ł -2rz1ń -2rz1ś -2rz1ź -2rz1ż -2rz1b -2rz1c -2rz1d -2rz1f -2rz1g -2rz1h -2rz1j -2rz1k -2rz1l -2rz1m -2rz1n -2rz1p -2rz1r -2rz1s -2rz1t -2rz1w -2słb -2s1ź -2s1ż -2s1b -2s1d -2s1f -2s1g -2s1s -2snk -2stk -2stn -2stsz -2sz1ć -2sz1ś -2sz1c -2sz1f -2sz1k -2sz1l -2sz1m -2sz1n -2sz1p -2sz1s -2sz1t -2sz1w -2sz1z -2szln -2t1ć -2t1ń -2t1ś -2t1ź -2t1ż -2t1b -2t1c -2t1d -2t1f -2t1g -2t1k -2t1m -2t1n -2t1p -2t1s -2t1z -2tln -2trk -2trzn -2w1ć -2w1ł -2w1ń -2w1ś -2w1ź -2w1ż -2w1b -2w1c -2w1d -2w1f -2w1g -2w1j -2w1k -2w1l -2w1m -2w1n -2w1p -2w1r -2w1s -2w1t -2w1z -2z1ć -2z1ś -2z1c -2z1d -2z1f -2z1k -2z1p -2z1s -2z1t -2zdk -2zdn -3d2niow -3k2sz2t -3m2k2n -3m2nest -3m2nezj -3m2sk2n -3p2neu -3w2ład -3w2łos -3w2czas -4ć3ć -4ł3ł -4ź3ź -4ż3ż -4b3b -4c3c -4d3d -4f3f -4g3g -4h3h -4j3j -4k3k -4l3l -4m3m -4n3n -4p3p -4r3r -4t3t -4w3w -4z3z -8ć. -8ćć. -8ćł. -8ćń. -8ćś. -8ćź. -8ćż. -8ćb. -8ćc. -8ćd. -8ćf. -8ćg. -8ćh. -8ćj. -8ćk. -8ćl. -8ćm. -8ćn. -8ćp. -8ćr. -8ćs. -8ćt. -8ćv. -8ćw. -8ćx. -8ćz. -8ł. -8łć. -8łł. -8łń. -8łś. -8łź. -8łż. -8łb. -8łc. -8łd. -8łf. -8łg. -8łh. -8łj. -8łk. -8łl. -8łm. -8łn. -8łp. -8łr. -8łs. -8łt. -8łv. -8łw. -8łx. -8łz. -8ń. -8ńć. -8ńł. -8ńń. -8ńś. -8ńź. -8ńż. -8ńb. -8ńc. -8ńd. -8ńf. -8ńg. -8ńh. -8ńj. -8ńk. -8ńl. -8ńm. -8ńn. -8ńp. -8ńr. -8ńs. -8ńt. -8ńv. -8ńw. -8ńx. -8ńz. -8ś. -8ść. -8śł. -8śń. -8śś. -8śź. -8śż. -8śb. -8śc. -8śd. -8śf. -8śg. -8śh. -8śj. -8śk. -8śl. -8śm. -8śn. -8śp. -8śr. -8śs. -8śt. -8śv. -8św. -8śx. -8śz. -8ź. -8źć. -8źł. -8źń. -8źś. -8źź. -8źż. -8źb. -8źc. -8źd. -8źf. -8źg. -8źh. -8źj. -8źk. -8źl. -8źm. -8źn. -8źp. -8źr. -8źs. -8źt. -8źv. -8źw. -8źx. -8źz. -8ż. -8żć. -8żł. -8żń. -8żś. -8żź. -8żż. -8żb. -8żc. -8żd. -8żf. -8żg. -8żh. -8żj. -8żk. -8żl. -8żm. -8żn. -8żp. -8żr. -8żs. -8żt. -8żv. -8żw. -8żx. -8żz. -8b. -8bć. -8bł. -8bń. -8bś. -8bź. -8bż. -8bb. -8bc. -8bd. -8bf. -8bg. -8bh. -8bj. -8bk. -8bl. -8bm. -8bn. -8bp. -8br. -8brz. -8bs. -8bt. -8bv. -8bw. -8bx. -8bz. -8c. -8cć. -8cł. -8cń. -8cś. -8cź. -8cż. -8cb. -8cc. -8cd. -8cf. -8cg. -8ch. -8chł. -8chrz. -8chw. -8cj. -8ck. -8cl. -8cm. -8cn. -8cp. -8cr. -8cs. -8ct. -8cv. -8cw. -8cx. -8cz. -8czt. -8d. -8dć. -8dł. -8dń. -8dś. -8dź. -8dż. -8db. -8dc. -8dd. -8df. -8dg. -8dh. -8dj. -8dk. -8dl. -8dm. -8dn. -8dp. -8dr. -8drz. -8ds. -8dt. -8dv. -8dw. -8dx. -8dz. -8f. -8fć. -8fł. -8fń. -8fś. -8fź. -8fż. -8fb. -8fc. -8fd. -8ff. -8fg. -8fh. -8fj. -8fk. -8fl. -8fm. -8fn. -8fp. -8fr. -8fs. -8ft. -8fv. -8fw. -8fx. -8fz. -8g. -8gć. -8gł. -8gń. -8gś. -8gź. -8gż. -8gb. -8gc. -8gd. -8gf. -8gg. -8gh. -8gj. -8gk. -8gl. -8gm. -8gn. -8gp. -8gr. -8gs. -8gt. -8gv. -8gw. -8gx. -8gz. -8h. -8hć. -8hł. -8hń. -8hś. -8hź. -8hż. -8hb. -8hc. -8hd. -8hf. -8hg. -8hh. -8hj. -8hk. -8hl. -8hm. -8hn. -8hp. -8hr. -8hs. -8ht. -8hv. -8hw. -8hx. -8hz. -8j. -8jć. -8jł. -8jń. -8jś. -8jź. -8jż. -8jb. -8jc. -8jd. -8jf. -8jg. -8jh. -8jj. -8jk. -8jl. -8jm. -8jn. -8jp. -8jr. -8js. -8jt. -8jv. -8jw. -8jx. -8jz. -8k. -8kć. -8kł. -8kń. -8kś. -8kź. -8kż. -8kb. -8kc. -8kd. -8kf. -8kg. -8kh. -8kj. -8kk. -8kl. -8km. -8kn. -8kp. -8kr. -8ks. -8kst. -8kt. -8kv. -8kw. -8kx. -8kz. -8l. -8lć. -8lł. -8lń. -8lś. -8lź. -8lż. -8lb. -8lc. -8ld. -8lf. -8lg. -8lh. -8lj. -8lk. -8ll. -8lm. -8ln. -8lp. -8lr. -8ls. -8lt. -8lv. -8lw. -8lx. -8lz. -8m. -8mć. -8mł. -8mń. -8mś. -8mź. -8mż. -8mb. -8mc. -8md. -8mf. -8mg. -8mh. -8mj. -8mk. -8ml. -8mm. -8mn. -8mp. -8mr. -8ms. -8mst. -8mt. -8mv. -8mw. -8mx. -8mz. -8n. -8nć. -8nł. -8nń. -8nś. -8nź. -8nż. -8nb. -8nc. -8nd. -8nf. -8ng. -8nh. -8nj. -8nk. -8nl. -8nm. -8nn. -8np. -8nr. -8ns. -8nt. -8nv. -8nw. -8nx. -8nz. -8p. -8pć. -8pł. -8pń. -8pś. -8pź. -8pż. -8pb. -8pc. -8pd. -8pf. -8pg. -8ph. -8pj. -8pk. -8pl. -8pm. -8pn. -8pp. -8pr. -8prz. -8ps. -8pt. -8pv. -8pw. -8px. -8pz. -8r. -8rć. -8rł. -8rń. -8rś. -8rź. -8rż. -8rb. -8rc. -8rd. -8rf. -8rg. -8rh. -8rj. -8rk. -8rl. -8rm. -8rn. -8rp. -8rr. -8rs. -8rsz. -8rt. -8rv. -8rw. -8rx. -8rz. -8rzł. -8s. -8sć. -8sł. -8sń. -8sś. -8sź. -8sż. -8sb. -8sc. -8sch. -8sd. -8sf. -8sg. -8sh. -8sj. -8sk. -8skrz. -8sl. -8sm. -8sn. -8sp. -8sr. -8ss. -8st. -8str. -8strz. -8stw. -8sv. -8sw. -8sx. -8sz. -8szcz. -8szczb. -8szk. -8szn. -8szt. -8sztr. -8t. -8tć. -8tł. -8tń. -8tś. -8tź. -8tż. -8tb. -8tc. -8td. -8tf. -8tg. -8th. -8tj. -8tk. -8tl. -8tm. -8tn. -8tp. -8tr. -8trz. -8ts. -8tt. -8tv. -8tw. -8tx. -8tz. -8v. -8vć. -8vł. -8vń. -8vś. -8vź. -8vż. -8vb. -8vc. -8vd. -8vf. -8vg. -8vh. -8vj. -8vk. -8vl. -8vm. -8vn. -8vp. -8vr. -8vs. -8vt. -8vv. -8vw. -8vx. -8vz. -8w. -8wć. -8wł. -8wń. -8wś. -8wź. -8wż. -8wb. -8wc. -8wd. -8wf. -8wg. -8wh. -8wj. -8wk. -8wl. -8wm. -8wn. -8wp. -8wr. -8ws. -8wt. -8wv. -8ww. -8wx. -8wz. -8x. -8xć. -8xł. -8xń. -8xś. -8xź. -8xż. -8xb. -8xc. -8xd. -8xf. -8xg. -8xh. -8xj. -8xk. -8xl. -8xm. -8xn. -8xp. -8xr. -8xs. -8xt. -8xv. -8xw. -8xx. -8xz. -8z. -8zć. -8zł. -8zń. -8zś. -8zź. -8zż. -8zb. -8zc. -8zd. -8zdr. -8zdrz. -8zf. -8zg. -8zh. -8zj. -8zk. -8zl. -8zm. -8zn. -8zp. -8zr. -8zs. -8zt. -8zv. -8zw. -8zx. -8zz. -a1 -a2u -a2y -aa2 -ae2 -ai2 -ao2 -be2eth -be2f3sz2 -be2k1hend -bi2n3o2ku -bi2sz3kop -bi2z3ne2s3m -bi2z3nes -birmin2g1ham -blo2k1hauz -bo2s3ma -br2d -bro2a2d3way -bu2sz3me -buk2sz3pan -busine2ss3m -busines2s -c4h -c4z -cal2d1well -ch2ł -ch2j -ch2l -ch2r -ch2w -chus1t -cu2r7zon -dż2ł -dż2j -dż2l -dż2r -dż2w -dże4z3b -dże4z3m -d4ź -d4ż -d4z -deut4sch3land -drz2w -du2sz3past -e1 -e2r5zac -e2u -e2y -e3u2sz -ea2 -ee2 -ei2 -eo2 -fi2s3harm -fi2sz3bin -fo2k2s3t -fo2r5zac -fol2k1lor -fos2f1a2zot -ga3d2get -gado3p2ta -gol2f3s -golfsz2 -gran2d1ilo -gro4t3r -hi2sz3p -hu2cz1w -hu2x3ley -i1 -i2ą -i2ę -i2ó -i2a -i2e -i2i -i2o -i2u -i2y -in4nsbruck -in4sbruc -j2t1ł -j2t1r -ja4z4z3b -ja4z4z3m -karl2s1kron -karl2s1ruhe -kir2chhoff -kongre2s3m -led1w -lu2ft3waffe -lu2ks1fer -ly2o -ma2r5zł -ma2r5zl -ma2r5zn -mi2sz1masz -mie2r5zł -mie2r5zi -mon2t3real -moza2i3k -mu2r7zasich3l -na4ł3kows -na4r3v -o1 -o2y -oa2 -och3mistrz -oe2 -of2f3set -oi2 -oo2 -ou2 -pa2n3a2mer -pa2s3cal -pa2s3ch -połu3d2ni -po3d4nieprz -po3m2ną -po3m2nę -po3m2ni -po4rt2s3mo2uth -po4rt3land -poli3e2t -poli3u2re -powsze3d2ni -pr2chal -pre2sz3pa -r4z -ro2e3nt2gen -ro2k3rocz -ro2s3to3c2k -s4z -se2t3le -sko2r5zoner -sm2r -sowi3z2 -sy2n3opt -sy2s1tem -sza2sz1ły -sze2z1long -sze4ść -szto2k1holm -szyn2k1was -to3y2o3t -turboo2d3rzut -tygo3d2ni -u1 -u2y -ua2 -ue2 -ui2 -uo2 -uu2 -vo2lk2s3 -we2e2k1end -we4st3f -we4st3m -y1 -ya2 -ye2 -yi2 -yo2 -yu2 -ze4p3p -} -\hyphenation{ -be-zach -be-zami -by-naj-mniej -gdzie-nie-gdzie -ina-czej -na-dal -ni-gdy -ni-gdzie -niech-że -niech-by -ow-szem -pó-łach -pó-łami -pó-łek -pod-ów-czas -przy-naj-mniej -skąd-inąd -tró-jach -tró-jami -tró-jek -} diff --git a/assets/hyphenation/pt.pat b/assets/hyphenation/pt.pat deleted file mode 100644 index cb947c43b..000000000 --- a/assets/hyphenation/pt.pat +++ /dev/null @@ -1,503 +0,0 @@ -% title: Hyphenation patterns for Portuguese -% copyright: Copyright (C) 1987, 1994, 1996, 2015 Pedro J. de Rezende, 1996, 2015 J. Joao Dias Almeida, 2024 Leonardo Araujo and Aline Benevides -% notice: This file is part of the hyph-utf8 package. -% See http://www.hyphenation.org/tex for more information. -% language: -% name: Portuguese -% tag: pt -% version: 1.4 2024-07-13 -% authors: -% - -% name: Pedro J. de Rezende -% contact: rezende (at) ic.unicamp.br -% - -% name: J. Joao Dias Almeida -% contact: jj (at) di.uminho.pt -% - -% name: Leonardo Araujo -% contact: leolca (at) gmail.com -% - -% name: Aline Benevides -% contact: benevides.aline12 (at) gmail.com -% licence: -% name: BSD 3-clause licence -% url: https://opensource.org/licenses/BSD-3-Clause -% text: > -% Redistribution and use in source and binary forms, with or without -% modification, are permitted provided that the following conditions -% are met: -% * Redistributions of source code must retain the above copyright -% notice, this list of conditions and the following disclaimer. -% * Redistributions in binary form must reproduce the above copyright -% notice, this list of conditions and the following disclaimer in the -% documentation and/or other materials provided with the -% distribution. -% * Neither the name of the University of Campinas, of the University -% of Minho nor the names of its contributors may be used to endorse -% or promote products derived from this software without specific -% prior written permission. -% -% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -% A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PEDRO J. DE -% REZENDE OR J.JOAO DIAS ALMEIDA BE LIABLE FOR ANY DIRECT, INDIRECT, -% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -% BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS -% OF USE, DATA, OR PROFITS; OR BUSINESS 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. -% hyphenmins: -% typesetting: -% left: 2 -% right: 3 -% changes: -% - Version 1.4 Release date: 13/07/2024 Leonardo Araujo and Aline Benevides -% - Version 1.3 Release date: 12/08/2015 Pedro J. de Rezende and J. Joao Dias Almeida -% - Version 1.2 Release date: 07/21/1996 Pedro J. de Rezende and J. Joao Dias Almeida -% - Version 1.1 Release date: 04/12/1994 Pedro J. de Rezende -% - Version 1.0 Release date: 02/13/1987 Pedro J. de Rezende -% texlive: -% synonyms: -% - portuges -% encoding: ec -% babelname: portuguese -% legacy_patterns: pthyph.tex -% message: Portuguese hyphenation patterns -% description: Hyphenation patterns for Portuguese in T1/EC and UTF-8 encodings. -% -\patterns{ -1b2l -1b2r -1ba -1be -1bi -1bo -1bu -1bá -1bâ -1bã -1bé -1bí -1bó -1bú -1bê -1bõ -1c2h -1c2l -1c2r -1ca -1ce -1ci -1co -1cu -1cá -1câ -1cã -1cé -1cí -1có -1cú -1cê -1cõ -1ça -1çe -1çi -1ço -1çu -1çá -1çâ -1çã -1çé -1çí -1çó -1çú -1çê -1çõ -1d2l -1d2r -1da -1de -1di -1do -1du -1dá -1dâ -1dã -1dé -1dí -1dó -1dú -1dê -1dõ -1f2l -1f2r -1fa -1fe -1fi -1fo -1fu -1fá -1fâ -1fã -1fé -1fí -1fó -1fú -1fê -1fõ -1g2l -1g2r -1ga -1ge -1gi -1go -1gu -1gu4a -1gu4e -1gu4i -1gu4o -1gá -1gâ -1gã -1gé -1gí -1gó -1gú -1gê -1gõ -1ja -1je -1ji -1jo -1ju -1já -1jâ -1jã -1jé -1jí -1jó -1jú -1jê -1jõ -1k2l -1k2r -1ka -1ke -1ki -1ko -1ku -1ká -1kâ -1kã -1ké -1kí -1kó -1kú -1kê -1kõ -1l2h -1la -1le -1li -1lo -1lu -1lá -1lâ -1lã -1lé -1lí -1ló -1lú -1lê -1lõ -1ma -1me -1mi -1mo -1mu -1má -1mâ -1mã -1mé -1mí -1mó -1mú -1mê -1mõ -1n2h -1na -1ne -1ni -1no -1nu -1ná -1nâ -1nã -1né -1ní -1nó -1nú -1nê -1nõ -1p2l -1p2r -1pa -1pe -1pi -1po -1pu -1pá -1pâ -1pã -1pé -1pí -1pó -1pú -1pê -1põ -1qu4a -1qu4e -1qu4i -1qu4o -1ra -1re -1ri -1ro -1ru -1rá -1râ -1rã -1ré -1rí -1ró -1rú -1rê -1rõ -1sa -1se -1si -1so -1su -1sá -1sâ -1sã -1sé -1sí -1só -1sú -1sê -1sõ -1t2l -1t2r -1ta -1te -1ti -1to -1tu -1tá -1tâ -1tã -1té -1tí -1tó -1tú -1tê -1tõ -1v2l -1v2r -1va -1ve -1vi -1vo -1vu -1vá -1vâ -1vã -1vé -1ví -1vó -1vú -1vê -1võ -1w2l -1w2r -1xa -1xe -1xi -1xo -1xu -1xá -1xâ -1xã -1xé -1xí -1xó -1xú -1xê -1xõ -1za -1ze -1zi -1zo -1zu -1zá -1zâ -1zã -1zé -1zí -1zó -1zú -1zê -1zõ -a3a -a3e -a3o -c3c -e3a -e3e -e3o -i3a -i3e -i3i -i3o -i3â -i3ê -i3ô -o3a -o3e -o3o -r3r -s3s -u3a -u3e -u3o -u3u -.p2si -.p2sí -.g2no -.g2nó -.g2nô -t2c -1p2neu -.t2m -.p2t -su2b3r -su2b3l -.su3b4li -a1ir. -u1ir. -1qu -1vô -1lô -1cô -1gô -1bô -1tô -1rô -1pô -a1é -a1í -a1ó -a1ú -e1á -e1â -e1ã -e1é -e1ê -e1í -e1ó -é1o -e1ú -i1á -i1ã -í1a -i1é -i1í -i1ó -í1o -i1u -i1ú -o1á -o1é -o1í -o1ó -u1á -u1ã -u1â -u1í -ú1o -1qu2á -1qu2â -1qu2í -1gu2í -a1ind -a1i1nh -e1imp -e1inc -e1inf -e1ing -e1ins -e1int -e1inv -u1iz. -a1iz. -4a. -4e. -4o. -1gu2á -1gu2ã -1qu2ã -.m2n -c2za -.s2 -1p2seu1d -1dô -1fô -1mô -1nô -1sô -1zô -tu1i -tu2it -tu2id -bu1i -nu1i -o1in -u1in -su1i -í1e -ju1i -fu1i -du1i -do1im -au1i -u1i1ç -u1ê -1gu2ê -1qu2ê -1çô -u1é -1gu2é -1qu2é -1xô -a1â -a1ã -a1ô -e1ô -.ne4o -o1ã -o1ê -o1im -o2i1na -pro1i1b -co2ima -.t2 -1- -} -\hyphenation{% Do NOT make any alterations to this list! --- PdR -hard-ware -soft-ware -} diff --git a/assets/spelling/en-extra.txt b/assets/spelling/en-extra.txt deleted file mode 100644 index 7d04908e3..000000000 --- a/assets/spelling/en-extra.txt +++ /dev/null @@ -1,241 +0,0 @@ -# English spelling supplement: common words the public-domain Moby list (1990s) lacks. -# Written by WordCraft contributors (MIT OR Apache-2.0); see ATTRIBUTION.md. -# One lowercase base form per line, sorted. Plurals, -ed, -ing, -er, -ly… come from the -# inflection rules in crates/proof/src/lib.rs, so list "desktop", not "desktops". -airbag -airfare -artcraft -autocomplete -autocorrect -autofill -autoformat -autosave -backend -bitcoin -bitmap -blockchain -blog -blogger -bluetooth -captcha -caregiver -carpool -cellphone -chatbot -chatroom -checkbox -checklist -childcare -clamour -clickable -colourful -colourisation -colourise -colourless -configurable -copyedit -coronavirus -covid -crowdfunding -crowdsource -crypto -cryptocurrency -customizable -customization -cyber -cybersecurity -cyberspace -database -dataset -daycare -desktop -download -downloadable -downsize -dropdown -earbud -ebook -ecommerce -email -emoji -emoticon -endeavour -esports -facebook -favicon -fintech -firewall -flowchart -foodie -freeware -frontend -fundraiser -gameplay -gamification -geeky -geolocation -gigabit -gigabyte -github -glamour -globalization -google -groupthink -hackathon -hardcode -hashtag -healthcare -homepage -homeschool -honour -honourable -honourably -hotspot -hyperlink -hypertext -inbox -incentivize -infographic -instagram -internet -interoperability -intranet -ipad -iphone -javascript -kickstart -kilobyte -laptop -lifestyle -linkedin -linux -livestream -lockdown -login -logout -lowercase -makeover -malware -manoeuvrable -megabyte -megapixel -meme -metadata -microchip -microsoft -middleware -mindset -multicultural -multiplayer -multitask -nerdy -netbook -netflix -newsfeed -offboard -offline -onboard -online -onscreen -outsource -paediatric -paralegal -passcode -petabyte -phishing -photobomb -playdate -playlist -plugin -podcast -popup -prequel -printout -prioritise -prioritization -prioritize -privatise -ransomware -recyclable -rigour -ringtone -roadmap -rollout -runtime -scalability -screencast -screensaver -screenshot -scrollbar -selfie -signup -sitemap -skillset -smartphone -smartwatch -smiley -snowboard -spellcheck -spellchecker -spreadsheet -spyware -stakeholder -standup -startup -subfolder -sunscreen -superfood -superuser -synthesise -tagline -takeout -techie -telecommute -teleconference -telehealth -telemedicine -terabyte -textbox -timeline -timestamp -toolbar -toolkit -touchpad -touchscreen -trackball -trackpad -travelogue -upcycle -upload -uppercase -upsell -upskill -usability -username -videoconference -videogame -vlog -vlogger -voicemail -walkthrough -webcam -webcast -webinar -weblog -webmaster -webpage -website -whatsapp -whiteboard -wifi -wiki -wikipedia -wireframe -wordcraft -workaround -workflow -workforce -workplace -workspace -workstation -youtube diff --git a/assets/spelling/en-variants.txt b/assets/spelling/en-variants.txt deleted file mode 100644 index 789ad5387..000000000 --- a/assets/spelling/en-variants.txt +++ /dev/null @@ -1,305 +0,0 @@ -# English spelling variants: American and British forms of the same word, for the English -# dialects (#1281, #1045). Written by WordCraft contributors from general knowledge of English -# spelling (MIT OR Apache-2.0); no other word list copied. See ATTRIBUTION.md. -# -# Each line: POLICY AMERICAN BRITISH -# POLICY is three letters, for American English (en-US), Canadian English (en-CA) and the -# British-spelling Englishes (en-GB, en-AU, en-NZ, en-IE, en-ZA, en-IN…), in that order: -# u only the American form is right there (the British one is flagged, American suggested) -# b only the British form is right there -# * both are right -# Inflections (colours, centred, organising, organisation…) come from the rules in -# crates/proof/src/en_variants.rs for -or/-our, -er/-re and -ize/-ise style pairs; other pairs -# get only a plural, so list their other forms on lines of their own. When in doubt a form is -# accepted: a false alarm is worse than a missed variant. - -# -or / -our -ubb arbor arbour -ubb ardor ardour -ubb armor armour -ubb armory armoury -ubb behavior behaviour -ubb behavioral behavioural -ubb candor candour -ubb clamor clamour -ubb color colour -ubb demeanor demeanour -ubb discolor discolour -ubb dishonor dishonour -ubb endeavor endeavour -ubb favor favour -ubb favorite favourite -ubb favoritism favouritism -ubb fervor fervour -ubb flavor flavour -ubb harbor harbour -ubb honor honour -ubb humor humour -ubb labor labour -ubb misbehavior misbehaviour -ubb neighbor neighbour -ubb neighborhood neighbourhood -ubb neighborly neighbourly -ubb odor odour -ubb parlor parlour -ubb rancor rancour -ubb rigor rigour -ubb rumor rumour -ubb savior saviour -ubb savor savour -ubb savory savoury -ubb splendor splendour -ubb succor succour -ubb tumor tumour -ubb unfavorable unfavourable -ubb valor valour -ubb vapor vapour -ubb vigor vigour - -# -er / -re -ubb caliber calibre -ubb center centre -ubb centimeter centimetre -ubb epicenter epicentre -ubb fiber fibre -ubb kilometer kilometre -ubb liter litre -ubb luster lustre -ubb maneuver manoeuvre -ubb meager meagre -ubb millimeter millimetre -ubb miter mitre -ubb ocher ochre -ubb saber sabre -ubb sepulcher sepulchre -ubb somber sombre -ubb specter spectre -*bb theater theatre -# A meter is also a measuring device in British English. -u** meter metre - -# -ize / -ise (British English accepts both: Oxford spelling uses -ize) -uu* agonize agonise -uu* apologize apologise -uu* authorize authorise -uu* capitalize capitalise -uu* categorize categorise -uu* centralize centralise -uu* characterize characterise -uu* civilize civilise -uu* colonize colonise -uu* commercialize commercialise -uu* computerize computerise -uu* criticize criticise -uu* crystallize crystallise -uu* customize customise -uu* decentralize decentralise -uu* demonize demonise -uu* deodorize deodorise -uu* destabilize destabilise -uu* digitize digitise -uu* dramatize dramatise -uu* economize economise -uu* emphasize emphasise -uu* energize energise -uu* equalize equalise -uu* familiarize familiarise -uu* fantasize fantasise -uu* fertilize fertilise -uu* finalize finalise -uu* formalize formalise -uu* fossilize fossilise -uu* galvanize galvanise -uu* generalize generalise -uu* globalize globalise -uu* harmonize harmonise -uu* hospitalize hospitalise -uu* humanize humanise -uu* hypnotize hypnotise -uu* hypothesize hypothesise -uu* idealize idealise -uu* idolize idolise -uu* immobilize immobilise -uu* immortalize immortalise -uu* immunize immunise -uu* industrialize industrialise -uu* initialize initialise -uu* italicize italicise -uu* itemize itemise -uu* jeopardize jeopardise -uu* legalize legalise -uu* legitimize legitimise -uu* liberalize liberalise -uu* localize localise -uu* magnetize magnetise -uu* marginalize marginalise -uu* maximize maximise -uu* mechanize mechanise -uu* memorize memorise -uu* mesmerize mesmerise -uu* minimize minimise -uu* mobilize mobilise -uu* modernize modernise -uu* monopolize monopolise -uu* moralize moralise -uu* nationalize nationalise -uu* naturalize naturalise -uu* neutralize neutralise -uu* normalize normalise -uu* optimize optimise -uu* organize organise -uu* ostracize ostracise -uu* oxidize oxidise -uu* pasteurize pasteurise -uu* patronize patronise -uu* penalize penalise -uu* personalize personalise -uu* plagiarize plagiarise -uu* polarize polarise -uu* popularize popularise -uu* pressurize pressurise -uu* prioritize prioritise -uu* privatize privatise -uu* publicize publicise -uu* pulverize pulverise -uu* randomize randomise -uu* rationalize rationalise -uu* realize realise -uu* recognize recognise -uu* reorganize reorganise -uu* revolutionize revolutionise -uu* satirize satirise -uu* scrutinize scrutinise -uu* sensitize sensitise -uu* socialize socialise -uu* specialize specialise -uu* stabilize stabilise -uu* standardize standardise -uu* sterilize sterilise -uu* stigmatize stigmatise -uu* subsidize subsidise -uu* summarize summarise -uu* symbolize symbolise -uu* sympathize sympathise -uu* synchronize synchronise -uu* synthesize synthesise -uu* systematize systematise -uu* tantalize tantalise -uu* terrorize terrorise -uu* theorize theorise -uu* trivialize trivialise -uu* urbanize urbanise -uu* utilize utilise -uu* vandalize vandalise -uu* verbalize verbalise -uu* victimize victimise -uu* visualize visualise -uu* vocalize vocalise -uu* westernize westernise - -# -yze / -yse -u*b analyze analyse -u*b catalyze catalyse -u*b dialyze dialyse -u*b electrolyze electrolyse -u*b hydrolyze hydrolyse -u*b paralyze paralyse -u*b psychoanalyze psychoanalyse - -# -ense / -ence -ubb defense defence -ubb offense offence -ubb pretense pretence -ubb defenseless defenceless -# In British English "license" and "practise" are the verbs, so only American English flags. -u** license licence -u** practice practise - -# Doubled l (American English accepts both) -*bb canceled cancelled -*bb canceling cancelling -*bb channeled channelled -*bb channeling channelling -*bb counseled counselled -*bb counseling counselling -*bb counselor counsellor -*bb counselors counsellors -*bb dueling duelling -*bb equaled equalled -*bb equaling equalling -*bb fueled fuelled -*bb fueling fuelling -*bb jeweler jeweller -*bb jewelers jewellers -*bb jewelry jewellery -*bb labeled labelled -*bb labeling labelling -*bb leveled levelled -*bb leveling levelling -*bb marveled marvelled -*bb marveling marvelling -*bb marvelous marvellous -*bb modeled modelled -*bb modeling modelling -*bb quarreled quarrelled -*bb quarreling quarrelling -*bb signaled signalled -*bb signaling signalling -*bb totaled totalled -*bb totaling totalling -*bb traveled travelled -*bb traveler traveller -*bb travelers travellers -*bb traveling travelling -*bb tunneled tunnelled -*bb tunneling tunnelling -*bb woolen woollen - -# -og / -ogue -*bb catalog catalogue -*bb cataloged catalogued -*bb cataloging cataloguing - -# ae / oe -u*b anemia anaemia -u*b anemic anaemic -u*b anesthesia anaesthesia -u*b anesthetic anaesthetic -**b archeology archaeology -**b archeologist archaeologist -u*b diarrhea diarrhoea -u*b edema oedema -u*b esophagus oesophagus -u*b estrogen oestrogen -u*b gynecologist gynaecologist -u*b gynecology gynaecology -u*b hematology haematology -u*b hemoglobin haemoglobin -u*b hemorrhage haemorrhage -u*b leukemia leukaemia -u*b orthopedic orthopaedic -u*b pediatric paediatric -u*b pediatrician paediatrician -u*b pediatrics paediatrics - -# Other words -u*b aluminum aluminium -u*b cozy cosy -ubb mold mould -ubb molded moulded -ubb molding moulding -ubb moldy mouldy -ubb molt moult -ubb pajamas pyjamas -ubb plow plough -ubb plowed ploughed -ubb plowing ploughing -u** program programme -u*b skeptic sceptic -u*b skeptical sceptical -u*b skepticism scepticism -ubb smolder smoulder -ubb smoldering smouldering -# "Tire" and "curb" are also verbs everywhere, so only American English flags. -uu* tire tyre -uu* curb kerb diff --git a/assets/spelling/ru-extra.txt b/assets/spelling/ru-extra.txt deleted file mode 100644 index 7741135ed..000000000 --- a/assets/spelling/ru-extra.txt +++ /dev/null @@ -1,214 +0,0 @@ -# Russian spelling supplement: common words older Russian Hunspell dictionaries lack. -# Written by WordCraft contributors (MIT OR Apache-2.0); see ATTRIBUTION.md. -# One word per line: the base form, a TAB, then its declension, from which -# crates/proof/src/ru/extra.rs makes every form: -# m masculine noun ending in a consonant (маркетплейс, -а, -у, -ом, -е, -ы/-и, -ов/-ей…) -# f feminine noun in -а (инфографика, -и, -е, -у, -ой…; genitive plural without ending) -# fia feminine noun in -ия (интеграция, -ии, -ию, -ией, -ий…) -# fs feminine noun in -ь (соцсеть, -и, -ью, -ей, -ям…) -# nie neuter noun in -ие (согласие, -ия, -ию, -ием, -ий…) -# no neuter noun in -о (юрлицо, -а, -у, -ом, -е; genitive plural without ending) -# adj adjective or participle in -ый, -ий, -ой (бонусный, газоиспользующий) -# ovat verb in -овать with its conjugation and participles (колеровать, администрировать) -# = the forms themselves, space-separated (verbs, nouns with a fleeting vowel) -# A word alone on its line doesn't decline (онлайн, медиа). Capitalized words are names and match -# only when written with a capital. - -# Commerce, marketing and the web -маркетплейс m -селлер m -фулфилмент m -ритейл m -ритейлер m -дистрибьютор m -дропшиппинг m -мерчандайзер m -эквайринг m -промокод m -кешбэк m -кэшбэк m -лендинг m -лид m -тренд m -бренд m -брендбук m -ребрендинг m -таргетинг m -геотаргетинг m -ретаргетинг m -таргетолог m -маркетолог m -копирайтер m -рерайтер m -контент m -блогер m -инфлюенсер m -хештег m -хэштег m -репост m -лайк m -подкаст m -стриминг m -вебинар m -чат m -бот m -чат-бот m -стартап m -франчайзинг m -аутсорсинг m -аутстаффинг m -коворкинг m -дедлайн m -фидбэк m -фидбек m -кейс m -скриншот m -скринкаст m -баннер m -виджет m -дашборд m -сервис m -сайт m -скан m -токен m -аккаунт m -логин m -пароль m -смартфон m -гаджет m -планшет m -ноутбук m -апгрейд m -апдейт m -бэкап m -бэкенд m -фронтенд m -датасет m -промпт m -интерфейс m -хостинг m -домен m -сниппет m -трафик m -юзер m -стикер m -чекбокс m -инпут m -плейсхолдер m -тултип m -гипсокартон m -пеноплекс m -праймер m -колер m -колорант m -Яндекс m -Битрикс m -Вебвизор m -Дзен m -инфографика f -аналитика f -оплата f -предоплата f -постоплата f -доплата f -переплата f -недоплата f -маркировка = маркировка маркировки маркировке маркировку маркировкой маркировкою маркировок маркировкам маркировками маркировках -отгрузка = отгрузка отгрузки отгрузке отгрузку отгрузкой отгрузкою отгрузок отгрузкам отгрузками отгрузках -колеровка = колеровка колеровки колеровке колеровку колеровкой колеровкою колеровок колеровкам колеровками колеровках -рассылка = рассылка рассылки рассылке рассылку рассылкой рассылкою рассылок рассылкам рассылками рассылках -выгрузка = выгрузка выгрузки выгрузке выгрузку выгрузкой выгрузкою выгрузок выгрузкам выгрузками выгрузках -интеграция fia -пагинация fia -госрегистрация fia -геолокация fia -монетизация fia -кастомизация fia -персонализация fia -сегментация fia -верификация fia -валидация fia -авторизация fia -аутентификация fia -оптимизация fia -визуализация fia -соцсеть fs -согласие nie -домовладение nie -рождение nie -паллетирование nie -неисполнение nie -непредоставление nie -бонусный adj -маркетинговый adj -мотивационный adj -гипоаллергенный adj -тиксотропный adj -сополимерный adj -покупочный adj -таргетированный adj -брендированный adj -кастомный adj -онлайновый adj -офлайновый adj -колеруемый adj -колерованный adj -заколерованный adj -газоиспользующий adj -колеровать ovat -заколеровать ovat -администрировать ovat -модерировать ovat -отмодерировать ovat -таргетировать ovat -монетизировать ovat -брендировать ovat -кастомизировать ovat -детектировать ovat -верифицировать ovat -валидировать ovat -генерировать ovat -сгенерировать ovat -мониторить = мониторить мониторю мониторишь мониторит мониторим мониторите мониторят мониторил мониторила мониторили мониторя мониторинг -гуглить = гуглить гуглю гуглишь гуглит гуглим гуглите гуглят гуглил гуглила гуглили -скачивать = скачивать скачиваю скачиваешь скачивает скачиваем скачиваете скачивают скачивал скачивала скачивали скачивается скачиваются -оценивание nie -медучреждение nie -юрлицо no -учётка = учётка учётки учётке учётку учёткой учёток учёткам учётками учётках учетка учетки учетке учетку учеткой учеток учеткам учетками учетках -перелинковка = перелинковка перелинковки перелинковке перелинковку перелинковкой перелинковок -поклейка = поклейка поклейки поклейке поклейку поклейкой поклеек -детекция fia -досудебный adj -категорийный adj -оркестратор m -оффер m -трекер m -бриф m -гайд m -монтажёр m -тенант m -мерчант m -амбассадор m -Роскомнадзор m -Директ m -Маркет m -вовлечённость fs -вовлеченность fs -укрывистость fs -# Words that don't decline -онлайн -офлайн -оффлайн -медиа -демо -франчайзи -киноа -смс -пн -вт -ср -чт -пт -сб -вс diff --git a/craft-i18n.rev b/craft-i18n.rev index fd264c675..7179c76bd 100644 --- a/craft-i18n.rev +++ b/craft-i18n.rev @@ -1 +1 @@ -9205108cfdced5177706264038848b68b2422d2b +f84aaa56adf40a595e0d279a6014cf1d07eb0289 diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 506033cb1..3d60ff070 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1,6 +1,6 @@ -// Reuse our hand-written Czech proof fixture; its install helper resolves this parent import. +// A copy of craft-proof's hand-written Czech fixture (`crates/proof/src/cs/test_dict.rs` in +// craft-libs, which a dependency's tests can't share); its install helper resolves this parent import. use wordcraft_proof::cs::install_dictionary; -#[path = "../../proof/src/cs/test_dict.rs"] mod czech_test_dict; use serde_json::json; diff --git a/crates/proof/src/cs/test_dict.rs b/crates/engine/src/tests/czech_test_dict.rs similarity index 100% rename from crates/proof/src/cs/test_dict.rs rename to crates/engine/src/tests/czech_test_dict.rs diff --git a/crates/proof/Cargo.toml b/crates/proof/Cargo.toml deleted file mode 100644 index 4c1a5b112..000000000 --- a/crates/proof/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "wordcraft-proof" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "WordCraft proofing per language: spelling, suggestions, grammar checks and hyphenation for English (public-domain word list) and Polish (installed Hunspell dictionary, TeX patterns)" - -[lints] -workspace = true - -[dependencies] -log = { workspace = true } -miniz_oxide = "0.9" -unicode-normalization = "0.1" - -[dev-dependencies] -proptest = { workspace = true } diff --git a/crates/proof/src/affix.rs b/crates/proof/src/affix.rs deleted file mode 100644 index cb4621210..000000000 --- a/crates/proof/src/affix.rs +++ /dev/null @@ -1,1784 +0,0 @@ -//! Hunspell-style affix dictionaries — the subset the SJP.PL Polish dictionary (`pl_PL`) uses. -//! -//! A word is correct when it is a stem, a stem with one suffix, a stem with one prefix, or a stem -//! with a prefix and a suffix that both allow cross products; each rule applies only to stems -//! carrying its flag and ending (suffixes) or starting (prefixes) as its condition says. Highly -//! inflected languages store millions of forms this way in a few hundred thousand stems: the -//! Polish dictionary's ~350k stems and ~7,500 rules accept ~4.5 million forms. -//! -//! The rules and stems come from a Hunspell `.aff`/`.dic` pair ([`AffixDict::from_hunspell`]), -//! read at runtime from the dictionaries installed on the system ([`crate::hunspell`]). They can -//! also be stored in WordCraft's compact format ([`AffixDict::to_bytes`]): the rules as text -//! lines and the stems front-coded, raw-deflated together (the Polish dictionary is under 1 MB). -//! -//! Supported: one-character flags (at most 256 distinct, any Unicode character — `FLAG UTF-8`), -//! flag aliases (`AF`: a number standing for a set of flags), `PFX`/`SFX` with strip, append and -//! condition (`.`, letters, `[abc]`, `[^abc]`), cross products, continuation classes on suffixes -//! (at most two suffixes in a row, as in Hunspell), `FORBIDDENWORD`, `NOSUGGEST`, `KEEPCASE` (the -//! stem is a word only in its own letter case), input conversion (`ICONV`: ligatures such as `fi` -//! read as `fi`) and ignored characters (`IGNORE`), and the suggestion tables `TRY`, `KEY`, `MAP` -//! and `REP`. -//! Morphological data (`AM` aliases, the fields after a stem) is skipped. Rejected: compounding, -//! long or numeric flags, `NEEDAFFIX` and the other directives that change which words are correct. -//! -//! Lookups never panic: stems sit in one string, found through an open-addressing hash table, and -//! a probe allocates only small scratch strings for the candidate roots it tries. - -use std::collections::HashMap; -use std::hash::Hasher; - -use crate::patterns::{FxHasher, FxMap}; - -const MAGIC: &str = "WCAFF1"; -/// Longest word (in bytes) looked up; longer input is never a dictionary word. -pub const MAX_WORD_BYTES: usize = 120; -/// Empty slot in the stem hash table. -const EMPTY: u32 = u32::MAX; - -/// Directives that change which words are correct in ways this engine doesn't implement. -const UNSUPPORTED: &[&str] = &[ - "CHECKSHARPS", - "CIRCUMFIX", - "COMPLEXPREFIXES", - "COMPOUNDBEGIN", - "COMPOUNDEND", - "COMPOUNDFIRST", - "COMPOUNDFLAG", - "COMPOUNDLAST", - "COMPOUNDMIDDLE", - "COMPOUNDMIN", - "COMPOUNDPERMITFLAG", - "COMPOUNDRULE", - "FULLSTRIP", - "NEEDAFFIX", - "OCONV", - "ONLYINCOMPOUND", - "PSEUDOROOT", -]; - -/// Most distinct flags a dictionary may use: every single-byte flag (the Hungarian dictionary -/// uses 158). -const MAX_FLAGS: usize = 256; - -/// A set of flags, one bit per distinct flag character of the dictionary. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -struct Mask([u64; MAX_FLAGS / 64]); - -impl Mask { - const EMPTY: Mask = Mask([0; MAX_FLAGS / 64]); - - /// The set holding flag number `i` alone (empty when `i` is out of range). - fn bit(i: usize) -> Mask { - let mut m = Mask::EMPTY; - if let Some(w) = m.0.get_mut(i / 64) { - *w = 1 << (i % 64); - } - m - } - - /// The number of the lowest flag in the set, if any. - fn first(self) -> Option { - self.0.iter().enumerate().find(|(_, w)| **w != 0).map(|(i, w)| i * 64 + w.trailing_zeros() as usize) - } - - fn is_empty(self) -> bool { - self.0.iter().all(|&w| w == 0) - } - - /// Do the two sets share a flag? - fn meets(self, other: Mask) -> bool { - !(self & other).is_empty() - } - - /// Does this set hold every flag of `other`? - fn covers(self, other: Mask) -> bool { - (self & other) == other - } -} - -impl std::ops::BitAnd for Mask { - type Output = Mask; - fn bitand(mut self, o: Mask) -> Mask { - for (a, b) in self.0.iter_mut().zip(o.0) { - *a &= b; - } - self - } -} - -impl std::ops::BitOr for Mask { - type Output = Mask; - fn bitor(mut self, o: Mask) -> Mask { - self |= o; - self - } -} - -impl std::ops::BitOrAssign for Mask { - fn bitor_assign(&mut self, o: Mask) { - for (a, b) in self.0.iter_mut().zip(o.0) { - *a |= b; - } - } -} - -/// One character class of a rule's condition. -#[derive(Clone, Debug, PartialEq, Eq)] -enum Cond { - Any, - Char(char), - Set { negated: bool, chars: Box<[char]> }, -} - -impl Cond { - fn matches(&self, c: char) -> bool { - match self { - Cond::Any => true, - Cond::Char(x) => *x == c, - Cond::Set { negated, chars } => chars.contains(&c) != *negated, - } - } -} - -/// Parse a Hunspell condition (`.`, `[^i]e`, `[km]anie`…). -fn parse_cond(text: &str) -> Result, String> { - if text == "." { - return Ok(Box::new([])); - } - let mut out = Vec::new(); - let mut chars = text.chars(); - while let Some(c) = chars.next() { - match c { - '.' => out.push(Cond::Any), - '[' => { - let mut set = Vec::new(); - let mut negated = false; - let mut closed = false; - let mut first = true; - for d in chars.by_ref() { - if d == ']' { - closed = true; - break; - } - if first && d == '^' { - negated = true; - } else { - set.push(d); - } - first = false; - } - if !closed { - return Err(format!("unclosed `[` in condition `{text}`")); - } - out.push(Cond::Set { negated, chars: set.into_boxed_slice() }); - } - ']' => return Err(format!("stray `]` in condition `{text}`")), - c => out.push(Cond::Char(c)), - } - } - Ok(out.into_boxed_slice()) -} - -/// How many suffixes a word may carry in a row through continuation classes (`work` + -/// `able/B` + `s` → `workables` is two). Hunspell strips at most two suffixes, so a third -/// chained suffix is not a word; the bound also keeps hostile input from walking deep recursions. -const MAX_CHAIN: u8 = 2; -/// How many suffixes may sit under the outermost one: the depth `form_has_flag` walks. -const INNER_CHAIN: u8 = MAX_CHAIN - 1; -/// Most forms [`AffixDict::forms`] lists for one stem (real stems have a few hundred at most). -pub const MAX_FORMS: usize = 4096; -/// Most rules [`AffixDict::forms`] tries along continuation chains and prefix–suffix cross -/// products for one stem: real dictionaries need a few thousand, hostile ones are cut off. -const MAX_FORMS_WORK: usize = 1 << 18; - -/// One prefix or suffix rule. -#[derive(Clone, Debug)] -struct Rule { - /// Bit of the rule's flag in the stems' flag masks. - bit: Mask, - flag: char, - cross: bool, - strip: Box, - add: Box, - /// Continuation classes (`SFX A 0 able/B`): flags the derived form carries, licensing - /// further affixes. Chained application is bounded by [`MAX_CHAIN`]. - cont: Mask, - cond: Box<[Cond]>, - cond_text: Box, -} - -impl Rule { - /// Does `root` end (suffix rule) or start (prefix rule) as the condition says? - fn cond_matches(&self, root: &str, suffix: bool) -> bool { - if self.cond.is_empty() { - return true; - } - if suffix { - let mut it = root.chars().rev(); - self.cond.iter().rev().all(|c| it.next().is_some_and(|x| c.matches(x))) - } else { - let mut it = root.chars(); - self.cond.iter().all(|c| it.next().is_some_and(|x| c.matches(x))) - } - } -} - -/// Suffix rules sharing an appended string and a stripped one: one stem lookup serves them all. -#[derive(Clone, Debug, Default)] -struct SfxGroup { - strip: Box, - rules: Vec, -} - -/// A stem list with affix rules (see the module docs). -#[derive(Clone, Debug, Default)] -pub struct AffixDict { - /// All stems, concatenated, sorted by bytes. - words: String, - offs: Vec, - /// Per stem: index into `sets`. - flags: Vec, - /// Distinct flag masks. - sets: Vec, - /// The `FORBIDDENWORD` flag's bit; stems carrying it are not words (0: none declared). - forbidden: Mask, - /// Stems with NOSUGGEST are correct, but none of their forms are suggested. - no_suggest: Mask, - /// Stems with SUBSTANDARD are correct too, and likewise never suggested. - substandard: Mask, - /// Open-addressing hash table of stem indices (`EMPTY` = free). - table: Vec, - /// Flag char of each bit. - flag_chars: Vec, - prefixes: Vec, - suffixes: Vec, - /// Appended string → groups of suffix rules by stripped string. - sfx_index: FxMap, Vec>, - /// Per flag bit: the suffix rules of that flag, in rule order. - sfx_by_flag: Vec>, - /// Longest appended suffix, in chars. - max_sfx_chars: usize, - try_chars: String, - key: String, - map: Vec>, - rep: Vec<(String, String)>, - /// The `KEEPCASE` flag's bit: such stems are words only in their own letter case (0: none). - keepcase: Mask, - /// `ICONV`: input conversions applied before a lookup, `(from, to)`. - iconv: Vec<(String, String)>, - /// `IGNORE`: characters removed from input words (and from the stems and appended texts). - ignore: Box<[char]>, - /// Provenance and licence lines, kept through [`AffixDict::to_bytes`]. - comments: Vec, -} - -fn hash_str(s: &str) -> u64 { - let mut h = FxHasher::default(); - h.write(s.as_bytes()); - h.finish() -} - -/// Letter case of a word. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Casing { - /// No capitals (`kot`). - Lower, - /// First letter capital, no other (`Kot`). - Initial, - /// Every cased letter a capital, at least two (`KOT`). - Upper, - /// Anything else (`McDonald`, `kOT`). - Mixed, -} - -/// The casing of `w`. -pub fn casing(w: &str) -> Casing { - let (mut upper, mut lower, mut first_upper) = (0usize, 0usize, false); - for (i, c) in w.chars().enumerate() { - if c.is_uppercase() { - upper += 1; - if i == 0 { - first_upper = true; - } - } else if c.is_lowercase() { - lower += 1; - } - } - if upper == 0 { - Casing::Lower - } else if upper == 1 && first_upper { - Casing::Initial - } else if lower == 0 { - Casing::Upper - } else { - Casing::Mixed - } -} - -/// `w` with its first letter a capital. -pub fn capitalize(w: &str) -> String { - let mut c = w.chars(); - match c.next() { - Some(f) => f.to_uppercase().chain(c).collect(), - None => String::new(), - } -} - -/// Parsed rules and tables, shared by the Hunspell reader and the compact format. -#[derive(Default)] -struct Rules { - prefixes: Vec, - suffixes: Vec, - try_chars: String, - key: String, - map: Vec>, - rep: Vec<(String, String)>, - /// The `FORBIDDENWORD` flag: stems carrying it are listed but never correct. - forbidden: Option, - no_suggest: Option, - substandard: Option, - /// The `KEEPCASE` flag. - keepcase: Option, - iconv: Vec<(String, String)>, - ignore: Vec, - /// `AF` flag aliases: alias `n` (1-based) stands for `aliases[n - 1]`. - aliases: Vec, - /// The `AF` table opened with its count line, so flag fields are alias numbers. A table - /// without one is rejected, as in Hunspell: its lines are ignored. - aliases_on: bool, -} - -impl Rules { - /// Flags as written in a `.dic` entry or a continuation class: the flag characters - /// themselves, or with an `AF` table the alias its leading digits number, as Hunspell reads - /// them (`2x` is alias 2). A field without digits or an undefined alias reads as no flags: - /// the stem is still a word, the rule still applies, without the flags (Hunspell warns). - fn flags<'a>(&'a self, written: &'a str) -> &'a str { - if !self.aliases_on { - return written; - } - let digits = written.bytes().take_while(u8::is_ascii_digit).count(); - written - .get(..digits) - .and_then(|d| d.parse::().ok()) - .and_then(|n| n.checked_sub(1)) - .and_then(|i| self.aliases.get(i)) - .map_or("", String::as_str) - } - - /// `w` without the `IGNORE` characters. - fn strip_ignored(&self, w: &str) -> String { - if self.ignore.is_empty() { w.to_string() } else { w.chars().filter(|c| !self.ignore.contains(c)).collect() } - } -} - -/// The fields of an `.aff`/`.dic` line: Hunspell separates them with spaces and tabs only, so a -/// flag byte read as U+0085 or U+00A0 (see [`crate::hunspell::decode`]) stays a flag. -fn split_fields(line: &str) -> impl Iterator { - line.split([' ', '\t']).filter(|f| !f.is_empty()) -} - -/// The one flag character a directive (`FORBIDDENWORD w`) names. -fn one_flag(arg: &str, dir: &str, n: usize) -> Result { - let mut it = arg.chars().filter(|&c| c != EMOJI_SELECTOR); - match (it.next(), it.next()) { - // Only the field separators of `split_fields` can't be a flag: a flag byte read as - // U+0085 or U+00A0 is one. - (Some(c), None) if !matches!(c, ' ' | '\t') => Ok(c), - _ => Err(format!("line {}: {dir} needs one flag character", n + 1)), - } -} - -/// A rule as parsed from text, before flags become bits: `(flag, cross, strip, add, cont, cond)`. -type RawRule = (char, bool, String, String, String, String); - -/// Most distinct `TRY` characters kept; a hostile `.aff` could otherwise make every suggestion -/// round cost time and memory proportional to the file size. -const MAX_TRY_CHARS: usize = 128; - -/// Keeps the first [`MAX_TRY_CHARS`] distinct characters of a `TRY` value, in order. -fn cap_try(s: &str) -> String { - let mut out = String::new(); - let mut n = 0; - for c in s.chars() { - if n >= MAX_TRY_CHARS { - break; - } - if !out.contains(c) { - out.push(c); - n += 1; - } - } - out -} - -/// `0` means empty in Hunspell's strip and append fields. -fn zero_empty(s: &str) -> String { - if s == "0" { String::new() } else { s.to_string() } -} - -/// U+FE0F VARIATION SELECTOR-16 asks for the emoji form of the character before it. A dictionary -/// that uses an emoji as a flag may write it after the flag (RLA-ES Spanish: `PFX ☎️`, `banco/S☎️`), -/// but it is not a flag of its own: it is dropped wherever flags are read. -const EMOJI_SELECTOR: char = '\u{fe0f}'; - -/// Read Hunspell `.aff` text (already decoded to UTF-8). -fn parse_aff(aff: &str) -> Result { - let mut r = Rules::default(); - let mut cross: HashMap<(bool, char), bool> = HashMap::new(); - let mut map_header = false; - let mut rep_header = false; - let mut af_header = false; - let mut iconv_header = false; - for (n, line) in aff.lines().enumerate() { - let f: Vec<&str> = split_fields(line).collect(); - let Some(&dir) = f.first() else { continue }; - if dir.starts_with('#') { - continue; - } - let at = |k: usize| f.get(k).copied().unwrap_or(""); - match dir { - "SET" | "LANG" | "NAME" | "VERSION" | "HOME" | "WORDCHARS" | "MAXNGRAMSUGS" | "MAXDIFF" | "ONLYMAXDIFF" | "NOSPLITSUGS" - | "SUGSWITHDOTS" | "WARN" | "FORBIDWARN" | "MAXCPDSUGS" => {} - // Morphological aliases, and a flag that only matters for morphological generation: - // the words it marks are correct. - "AM" | "LEMMA_PRESENT" => {} - "SUBSTANDARD" => r.substandard = Some(one_flag(at(1), dir, n)?), - // The first `AF` or `ICONV` line is the table's count, as in Hunspell, even with a - // trailing comment (`AF 1559 # aliases`). - "AF" => { - if !af_header { - af_header = true; - // A count below 1 rejects the table too: flags stay literal (`AF 0`). - r.aliases_on = at(1).parse::().is_ok_and(|n| n > 0); - } else if r.aliases_on { - r.aliases.push(at(1).to_string()); - } - } - "ICONV" => { - if !iconv_header && at(1).parse::().is_ok() { - iconv_header = true; - } else if f.len() >= 3 { - r.iconv.push((at(1).to_string(), at(2).to_string())); - } - } - "IGNORE" => r.ignore = at(1).chars().collect(), - "KEEPCASE" => r.keepcase = Some(one_flag(at(1), dir, n)?), - // Per-character flag modes (`FLAG UTF-8`, as VERO's pt_BR dictionary uses) work - // with the one-character parsing here; only the multi-character encodings differ. - "FLAG" if !matches!(at(1), "UTF-8" | "DEFAULT" | "utf-8" | "default" | "UTF8" | "") => { - return Err(format!("line {}: FLAG {} is not supported (one-character flags only)", n + 1, at(1))); - } - "FLAG" => {} - "FORBIDDENWORD" => r.forbidden = Some(one_flag(at(1), dir, n)?), - "NOSUGGEST" => r.no_suggest = Some(one_flag(at(1), dir, n)?), - "TRY" => r.try_chars = cap_try(at(1)), - "KEY" => r.key = at(1).to_string(), - "MAP" => { - if !map_header && at(1).parse::().is_ok() { - map_header = true; - } else if !at(1).contains('(') { - r.map.push(at(1).chars().collect()); - } - } - "REP" => { - if !rep_header && at(1).parse::().is_ok() { - rep_header = true; - } else if f.len() >= 3 { - r.rep.push((at(1).replace('_', " "), at(2).replace('_', " "))); - } - } - "PFX" | "SFX" => { - let suffix = dir == "SFX"; - let mut flag_chars = at(1).chars().filter(|&c| c != EMOJI_SELECTOR); - let (Some(flag), None) = (flag_chars.next(), flag_chars.next()) else { - return Err(format!("line {}: flag `{}` is not one character", n + 1, at(1))); - }; - // Only a header ignores trailing fields; a rule condition beginning with # is literal. - let is_header = f.len() >= 4 && matches!(at(2), "Y" | "N") && at(3).parse::().is_ok() && !cross.contains_key(&(suffix, flag)); - if is_header { - cross.insert((suffix, flag), at(2) == "Y"); - continue; - } - if f.len() < 4 { - return Err(format!("line {}: short affix rule", n + 1)); - } - // `able/B`: the appended text and its continuation classes. - let (add, cont) = match at(3).split_once('/') { - Some((a, c)) => (a, c), - None => (at(3), ""), - }; - let cond = if f.len() >= 5 { at(4) } else { "." }; - parse_cond(cond).map_err(|e| format!("line {}: {e}", n + 1))?; - let c = cross.get(&(suffix, flag)).copied().unwrap_or(false); - let rule: RawRule = (flag, c, zero_empty(at(2)), zero_empty(add), cont.to_string(), cond.to_string()); - if suffix { - r.suffixes.push(rule); - } else { - r.prefixes.push(rule); - } - } - d if UNSUPPORTED.contains(&d) => return Err(format!("line {}: {d} is not supported", n + 1)), - _ => {} - } - } - // Aliases may be declared after the rules that use them; resolve once all are read. - // Hunspell removes `IGNORE` characters from the appended text only: a stripped text that - // holds one never matches a stem (stems lose them too), so the rule never applies. - let resolve = |rules: &mut Vec, r: &Rules| { - for rule in rules.iter_mut() { - rule.4 = r.flags(&rule.4).to_string(); - rule.3 = r.strip_ignored(&rule.3); - } - }; - let (mut pfx, mut sfx) = (std::mem::take(&mut r.prefixes), std::mem::take(&mut r.suffixes)); - resolve(&mut pfx, &r); - resolve(&mut sfx, &r); - (r.prefixes, r.suffixes) = (pfx, sfx); - Ok(r) -} - -impl AffixDict { - /// Build from Hunspell `.aff` and `.dic` text (decoded to UTF-8; the `.dic` count line and - /// morphological fields are ignored). `comments` (provenance, licence) are kept with the data. - pub fn from_hunspell(aff: &str, dic: &str, comments: &[&str]) -> Result { - // Some dictionaries open with a UTF-8 BOM; it must not glue itself to the first - // directive or turn the `.dic` count line into a stem. - let aff = aff.strip_prefix('\u{feff}').unwrap_or(aff); - let dic = dic.strip_prefix('\u{feff}').unwrap_or(dic); - let rules = parse_aff(aff)?; - let mut stems: Vec<(String, String)> = Vec::new(); - for (n, line) in dic.lines().enumerate() { - // The entry may hold spaces (`ad hoc/B\tpo:adv`); it ends where Hunspell ends it. - // Leading blanks belong to it, as in Hunspell (` lab` is no `lab`). - let tok = line.get(..crate::hunspell::dic_entry_end(line.as_bytes())).unwrap_or_default(); - if tok.is_empty() { - continue; - } - if n == 0 && tok.parse::().is_ok() { - continue; - } - // `\/` is a literal slash in a word; the first plain `/` starts the flags. - let mut word = String::new(); - let mut flags = String::new(); - let mut chars = tok.chars().peekable(); - let mut in_flags = false; - while let Some(c) = chars.next() { - if in_flags { - if c != EMOJI_SELECTOR { - flags.push(c); - } - } else if c == '\\' && chars.peek() == Some(&'/') { - word.push('/'); - chars.next(); - } else if c == '/' { - in_flags = true; - } else { - word.push(c); - } - } - let word = rules.strip_ignored(&word); - if word.is_empty() || word.len() > MAX_WORD_BYTES || word.contains(['\n', '/']) { - continue; - } - let flags = rules.flags(&flags).to_string(); - stems.push((word, flags)); - } - AffixDict::build(rules, stems, comments.iter().map(ToString::to_string).collect()) - } - - fn build(rules: Rules, mut stems: Vec<(String, String)>, comments: Vec) -> Result { - // Flags: every one a rule or a stem uses gets a bit. - let mut flag_chars: Vec = rules.prefixes.iter().chain(&rules.suffixes).map(|r| r.0).collect(); - flag_chars.extend(stems.iter().flat_map(|s| s.1.chars())); - flag_chars.sort_unstable(); - flag_chars.dedup(); - if flag_chars.len() > MAX_FLAGS { - return Err(format!("{} distinct flags; at most {MAX_FLAGS} are supported", flag_chars.len())); - } - let bit_of = |c: char| flag_chars.iter().position(|&x| x == c).map_or(Mask::EMPTY, Mask::bit); - let forbidden = rules.forbidden.map_or(Mask::EMPTY, bit_of); - let keepcase = rules.keepcase.map_or(Mask::EMPTY, bit_of); - // Stems sorted by bytes; homonyms (the same word twice) are merged. - stems.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); - let mut d = AffixDict { - words: String::with_capacity(stems.iter().map(|s| s.0.len()).sum()), - offs: Vec::with_capacity(stems.len() + 1), - flag_chars: flag_chars.clone(), - forbidden, - no_suggest: rules.no_suggest.map_or(Mask::EMPTY, bit_of), - substandard: rules.substandard.map_or(Mask::EMPTY, bit_of), - keepcase, - iconv: rules.iconv, - ignore: rules.ignore.into_boxed_slice(), - try_chars: rules.try_chars, - key: rules.key, - map: rules.map, - rep: rules.rep, - comments, - ..AffixDict::default() - }; - d.offs.push(0); - let mut set_ids: HashMap = HashMap::new(); - let mut i = 0; - while i < stems.len() { - let Some((word, _)) = stems.get(i) else { break }; - let mut mask = Mask::EMPTY; - let mut j = i; - while let Some((w, f)) = stems.get(j) - && w == word - { - mask |= f.chars().map(bit_of).fold(Mask::EMPTY, |a, b| a | b); - j += 1; - } - let next_id = u16::try_from(d.sets.len()).map_err(|_| "too many distinct flag sets".to_string())?; - let id = *set_ids.entry(mask).or_insert(next_id); - if usize::from(id) == d.sets.len() { - d.sets.push(mask); - } - d.words.push_str(word); - d.offs.push(u32::try_from(d.words.len()).map_err(|_| "stems too large".to_string())?); - d.flags.push(id); - i = j; - } - let mk = |(flag, cross, strip, add, cont, cond): RawRule| -> Result { - let cont = cont.chars().fold(Mask::EMPTY, |m, c| m | bit_of(c)); - Ok(Rule { bit: bit_of(flag), flag, cross, strip: strip.into(), add: add.into(), cont, cond: parse_cond(&cond)?, cond_text: cond.into() }) - }; - d.prefixes = rules.prefixes.into_iter().map(mk).collect::>()?; - d.suffixes = rules.suffixes.into_iter().map(mk).collect::>()?; - d.index(); - Ok(d) - } - - /// Build the suffix index and the stem hash table. - fn index(&mut self) { - let mut idx: FxMap, Vec> = FxMap::default(); - // Each (appended, stripped) pair's group, found by hash rather than by scanning the - // appended string's groups: a dictionary with many strips per suffix stays linear. - let mut group_of: FxMap<(&str, &str), usize> = FxMap::default(); - let mut by_flag: Vec> = vec![Vec::new(); MAX_FLAGS]; - for (i, r) in self.suffixes.iter().enumerate() { - let groups = idx.entry(r.add.clone()).or_default(); - let Ok(i) = u32::try_from(i) else { break }; - match group_of.get(&(&*r.add, &*r.strip)).and_then(|&g| groups.get_mut(g)) { - Some(g) => g.rules.push(i), - None => { - group_of.insert((&r.add, &r.strip), groups.len()); - groups.push(SfxGroup { strip: r.strip.clone(), rules: vec![i] }); - } - } - if let Some(list) = r.bit.first().and_then(|b| by_flag.get_mut(b)) { - list.push(i); - } - } - self.sfx_by_flag = by_flag; - self.max_sfx_chars = self.suffixes.iter().map(|r| r.add.chars().count()).max().unwrap_or(0); - self.sfx_index = idx; - let n = self.len(); - let size = (n.max(1) * 2).next_power_of_two(); - let mut table = vec![EMPTY; size]; - let mask = size - 1; - for i in 0..n { - let Some(w) = self.word(i) else { continue }; - let mut slot = hash_str(w) as usize & mask; - while table.get(slot).is_some_and(|&t| t != EMPTY) { - slot = (slot + 1) & mask; - } - if let (Some(t), Ok(i)) = (table.get_mut(slot), u32::try_from(i)) { - *t = i; - } - } - self.table = table; - } - - /// Number of stems. - pub fn len(&self) -> usize { - self.flags.len() - } - - pub fn is_empty(&self) -> bool { - self.flags.is_empty() - } - - /// Number of prefix and suffix rules. - pub fn rule_count(&self) -> usize { - self.prefixes.len() + self.suffixes.len() - } - - fn word(&self, i: usize) -> Option<&str> { - let a = *self.offs.get(i)? as usize; - let b = *self.offs.get(i + 1)? as usize; - self.words.get(a..b) - } - - /// The flag characters of `mask`, in flag order (how the compact format stores flags). - fn flag_chars_for(&self, mask: Mask) -> String { - self.flag_chars.iter().enumerate().filter(|&(i, _)| mask.meets(Mask::bit(i))).map(|(_, c)| c).collect() - } - - /// The character of `mask` when it holds exactly one flag. - fn flag_char(&self, mask: Mask) -> Option { - let s = self.flag_chars_for(mask); - let mut it = s.chars(); - match (it.next(), it.next()) { - (Some(c), None) => Some(c), - _ => None, - } - } - - /// The stems, in byte order. - pub fn stems(&self) -> impl Iterator + '_ { - (0..self.len()).filter_map(|i| self.word(i)) - } - - /// The stems starting with `prefix`, in byte order (a binary search, then a scan). - pub fn stems_with_prefix<'a>(&'a self, prefix: &str) -> impl Iterator + use<'a> { - let prefix = prefix.to_string(); - let first = { - let (mut lo, mut hi) = (0usize, self.len()); - while lo < hi { - let mid = lo + (hi - lo) / 2; - if self.word(mid).is_some_and(|w| w.as_bytes() < prefix.as_bytes()) { - lo = mid + 1; - } else { - hi = mid; - } - } - lo - }; - (first..self.len()).map_while(move |i| self.word(i).filter(|w| w.starts_with(prefix.as_str()))) - } - - /// Flag mask of a stem, if `w` is one. - fn stem_flags(&self, w: &str) -> Option { - self.stem_flags_except(w, Mask::EMPTY) - } - - /// Flag mask of a stem, if `w` is one and carries none of the flags in `deny`. - fn stem_flags_except(&self, w: &str, deny: Mask) -> Option { - if self.table.is_empty() { - return None; - } - let mask = self.table.len() - 1; - let mut slot = hash_str(w) as usize & mask; - for _ in 0..self.table.len() { - let t = *self.table.get(slot)?; - if t == EMPTY { - return None; - } - if self.word(t as usize) == Some(w) { - let id = *self.flags.get(t as usize)?; - let mask = self.sets.get(usize::from(id)).copied(); - // A stem carrying FORBIDDENWORD is listed but is not a word: no direct hit, - // and (this is the only stem lookup) no affixed form is built on it either. - return mask.filter(|m| !m.meets(self.forbidden | deny)); - } - slot = (slot + 1) & mask; - } - None - } - - /// Does `w` carry any flag of `flags` — as a stem, or as the form of a suffix rule whose - /// continuation classes include one (chained suffixation, depth-bounded)? - /// - /// The rules of a group that could lead to `w` are merged into one mask of their flags and - /// followed once, not once per rule: the answer is the same, but thousands of rules of one - /// flag cost one walk, not one each (#3613). - fn form_has_flag(&self, w: &str, flags: Mask, depth: u8, deny: Mask) -> bool { - if self.stem_flags_except(w, deny).is_some_and(|f| f.meets(flags)) { - return true; - } - if depth == 0 { - return false; - } - let mut root = String::with_capacity(w.len() + 8); - let cuts = std::iter::once(w.len()).chain(w.char_indices().rev().map(|(i, _)| i).filter(|&i| i > 0)); - for b in cuts.take(self.max_sfx_chars + 1) { - let (Some(base), Some(add)) = (w.get(..b), w.get(b..)) else { continue }; - let Some(groups) = self.sfx_index.get(add) else { continue }; - for g in groups { - root.clear(); - root.push_str(base); - root.push_str(&g.strip); - let inner = g - .rules - .iter() - .filter_map(|&i| self.suffixes.get(i as usize)) - .filter(|r| r.cont.meets(flags) && r.cond_matches(&root, true)) - .fold(Mask::EMPTY, |m, r| m | r.bit); - if !inner.is_empty() && self.form_has_flag(&root, inner, depth - 1, deny) { - return true; - } - } - } - false - } - - /// Is `w` exactly (case and all) a stem or an affixed form of one? `ICONV` and `IGNORE` - /// apply first. - pub fn contains(&self, w: &str) -> bool { - self.contains_except(&self.normalize(w), Mask::EMPTY) - } - - /// Exactly spelled dictionary words eligible for suggestions (NOSUGGEST and SUBSTANDARD - /// stems excluded). - pub fn contains_suggestible(&self, w: &str) -> bool { - self.contains_except(&self.normalize(w), self.no_suggest | self.substandard) - } - - /// `w` after the `ICONV` conversions (at each position the longest match wins) and without - /// the `IGNORE` characters. - fn normalize<'a>(&self, w: &'a str) -> std::borrow::Cow<'a, str> { - if self.iconv.is_empty() && self.ignore.is_empty() { - return std::borrow::Cow::Borrowed(w); - } - // One pass: the input is copied only from the first change on. - let mut out: Option = None; - let mut rest = w; - while let Some(c) = rest.chars().next() { - let done = w.get(..w.len() - rest.len()).unwrap_or_default(); - let hit = self.iconv.iter().filter(|(from, _)| !from.is_empty() && rest.starts_with(from.as_str())).max_by_key(|(from, _)| from.len()); - match hit { - Some((from, to)) => { - out.get_or_insert_with(|| done.to_string()).extend(to.chars().filter(|c| !self.ignore.contains(c))); - rest = rest.get(from.len()..).unwrap_or_default(); - } - None => { - if self.ignore.contains(&c) { - out.get_or_insert_with(|| done.to_string()); - } else if let Some(o) = out.as_mut() { - o.push(c); - } - rest = rest.get(c.len_utf8()..).unwrap_or_default(); - } - } - } - out.map_or(std::borrow::Cow::Borrowed(w), std::borrow::Cow::Owned) - } - - /// [`AffixDict::contains`] on normalized input, where stems carrying a flag in `deny` don't - /// count (case variants of `KEEPCASE` stems). - fn contains_except(&self, w: &str, deny: Mask) -> bool { - if w.is_empty() || w.len() > MAX_WORD_BYTES { - return false; - } - if self.stem_flags_except(w, deny).is_some() || self.suffixed(w, Mask::EMPTY, deny) { - return true; - } - let mut root = String::with_capacity(w.len() + 8); - for r in &self.prefixes { - let Some(rest) = w.strip_prefix(&*r.add) else { continue }; - if rest.is_empty() { - continue; - } - root.clear(); - root.push_str(&r.strip); - root.push_str(rest); - if !r.cond_matches(&root, false) { - continue; - } - if self.stem_flags_except(&root, deny).is_some_and(|f| f.meets(r.bit)) { - return true; - } - if self.form_has_flag(&root, r.bit, INNER_CHAIN, deny) { - return true; - } - if r.cross && self.suffixed(&root, r.bit, deny) { - return true; - } - } - false - } - - /// Is `w` a stem with one suffix? With `need` set (a prefix was taken off), the stem must - /// carry those flags too and the suffix must allow cross products. - fn suffixed(&self, w: &str, need: Mask, deny: Mask) -> bool { - let mut root = String::with_capacity(w.len() + 8); - // Each split point from the end: the appended string is `w[b..]`, at most the longest - // suffix, and something must stay before it. - let cuts = std::iter::once(w.len()).chain(w.char_indices().rev().map(|(i, _)| i).filter(|&i| i > 0)); - for b in cuts.take(self.max_sfx_chars + 1) { - let (Some(base), Some(add)) = (w.get(..b), w.get(b..)) else { continue }; - let Some(groups) = self.sfx_index.get(add) else { continue }; - for g in groups { - root.clear(); - root.push_str(base); - root.push_str(&g.strip); - let rules = || g.rules.iter().filter_map(|&i| self.suffixes.get(i as usize)); - // The stem path: `root` is a stem carrying the rule's flag (and the prefix's, - // when a cross product is being checked). - if let Some(f) = self.stem_flags_except(&root, deny) - && f.covers(need) - && rules().any(|r| f.meets(r.bit) && (need.is_empty() || r.cross) && r.cond_matches(&root, true)) - { - return true; - } - // Continuation classes: `root` need not be a stem carrying this rule's flag — - // it may be a form that gained the flag from another rule's continuation. The - // matching rules' flags are looked for together, in one walk. - if need.is_empty() { - let flags = rules().filter(|r| r.cond_matches(&root, true)).fold(Mask::EMPTY, |m, r| m | r.bit); - if !flags.is_empty() && self.form_has_flag(&root, flags, INNER_CHAIN, deny) { - return true; - } - } - } - } - false - } - - /// Is `w` correct, Hunspell style: as written, or — for a capitalized word or one in capitals - /// — in lower case (and capitalized), since a sentence may start with any word and headings - /// may be in capitals. Lower-case input never matches a capitalized stem (`warszawa`), and a - /// `KEEPCASE` stem matches only as written. - pub fn check(&self, w: &str) -> bool { - self.check_except(w, Mask::EMPTY) - } - - /// Whether a suggestion is correct in its displayed case and has an eligible stem. - pub fn can_suggest(&self, w: &str) -> bool { - self.check_except(w, self.no_suggest | self.substandard) - } - - /// Whether `w` is a word only through a `KEEPCASE` stem, so it is correct in this letter case - /// alone (`km` from `km/K`, which `Km` and `KM` are not). - pub(crate) fn case_locked(&self, w: &str) -> bool { - self.keepcase != Mask::EMPTY && self.contains(w) && !self.contains_except(&self.normalize(w), self.keepcase) - } - - fn check_except(&self, w: &str, deny: Mask) -> bool { - let w = self.normalize(w); - if self.contains_except(&w, deny) { - return true; - } - let deny = deny | self.keepcase; - match casing(&w) { - Casing::Lower | Casing::Mixed => false, - Casing::Initial => self.contains_except(&w.to_lowercase(), deny), - Casing::Upper => { - let lower = w.to_lowercase(); - self.contains_except(&capitalize(&lower), deny) || self.contains_except(&lower, deny) - } - } - } - - /// Every form of the stem `stem` (itself included), unsorted, possibly with repeats; at most - /// [`MAX_FORMS`], and at most [`MAX_FORMS_WORK`] rules tried along the continuation chains and - /// cross products, so a dictionary with thousands of self-continuing rules can't make one stem - /// cost millions of strings (#3613). - pub fn forms(&self, stem: &str) -> Vec { - let Some(f) = self.stem_flags(stem) else { return Vec::new() }; - let mut out = vec![stem.to_string()]; - let mut cross_sfx = Vec::new(); - // Suffix forms queued with the continuation classes they carry, for chained expansion; - // each (form, classes) pair is expanded once. - let mut chain: Vec<(String, Mask)> = Vec::new(); - let mut queued: std::collections::HashSet<(String, Mask)> = std::collections::HashSet::new(); - let mut work = 0usize; - for r in &self.suffixes { - if !f.meets(r.bit) || !r.cond_matches(stem, true) { - continue; - } - let Some(base) = stem.strip_suffix(&*r.strip) else { continue }; - let form = format!("{base}{}", r.add); - if r.cross { - cross_sfx.push(form.clone()); - } - if !r.cont.is_empty() && queued.insert((form.clone(), r.cont)) { - chain.push((form.clone(), r.cont)); - } - out.push(form); - if out.len() >= MAX_FORMS { - return out; - } - } - // Continuation classes chain suffixes (`workable/B` → `workables`), up to [`MAX_CHAIN`] - // suffixes in all — the same bound the checker walks. - 'chain: for _ in 0..INNER_CHAIN { - let level = std::mem::take(&mut chain); - if level.is_empty() { - break; - } - for (form, flags) in level { - for r in self.suffixes_with(flags).into_iter().filter_map(|i| self.suffixes.get(i as usize)) { - work += 1; - if work > MAX_FORMS_WORK { - break 'chain; - } - if !r.cond_matches(&form, true) { - continue; - } - let Some(base) = form.strip_suffix(&*r.strip) else { continue }; - let next = format!("{base}{}", r.add); - if !r.cont.is_empty() && queued.insert((next.clone(), r.cont)) { - chain.push((next.clone(), r.cont)); - } - out.push(next); - if out.len() >= MAX_FORMS { - return out; - } - } - } - } - for r in &self.prefixes { - if !f.meets(r.bit) || !r.cond_matches(stem, false) { - continue; - } - let Some(rest) = stem.strip_prefix(&*r.strip) else { continue }; - out.push(format!("{}{rest}", r.add)); - if r.cross { - for s in &cross_sfx { - work += 1; - if out.len() >= MAX_FORMS || work > MAX_FORMS_WORK { - return out; - } - if let Some(rest) = s.strip_prefix(&*r.strip) { - out.push(format!("{}{rest}", r.add)); - } - } - } - if out.len() >= MAX_FORMS { - return out; - } - } - out - } - - /// The suffix rules carrying any flag of `mask`, in rule order. - fn suffixes_with(&self, mask: Mask) -> Vec { - let mut rules: Vec = Vec::new(); - let mut lists = 0; - for (bit, list) in self.sfx_by_flag.iter().enumerate() { - if mask.meets(Mask::bit(bit)) && !list.is_empty() { - rules.extend_from_slice(list); - lists += 1; - } - } - if lists > 1 { - rules.sort_unstable(); - } - rules - } - - /// The `TRY` letters, most frequent first (suggestions try these). - pub fn try_chars(&self) -> &str { - &self.try_chars - } - - /// The `KEY` keyboard rows, separated by `|` (adjacent letters are likely mistypes). - pub fn key(&self) -> &str { - &self.key - } - - /// The `MAP` groups: letters often confused with each other (`aą`, `zżź`, `oóu`…). - pub fn map(&self) -> &[Vec] { - &self.map - } - - /// The `REP` table: common misspellings, `(wrong, right)`. - pub fn rep(&self) -> &[(String, String)] { - &self.rep - } - - /// The provenance and licence lines. - pub fn comments(&self) -> &[String] { - &self.comments - } - - /// Serialize to WordCraft's compact format: raw-deflate of `WCAFF1`, the comments, the - /// tables and rules as text lines, a `WORDS` line, then one line per stem, front-coded - /// against the previous one (a byte with the shared prefix length in bytes, the rest of the - /// word, `/` and its flags if any). - pub fn to_bytes(&self) -> Vec { - let mut out = String::new(); - out.push_str(MAGIC); - out.push('\n'); - for c in &self.comments { - out.push_str("# "); - out.push_str(c.trim()); - out.push('\n'); - } - if !self.try_chars.is_empty() { - out.push_str(&format!("TRY {}\n", self.try_chars)); - } - if !self.key.is_empty() { - out.push_str(&format!("KEY {}\n", self.key)); - } - for m in &self.map { - out.push_str(&format!("MAP {}\n", m.iter().collect::())); - } - for (a, b) in &self.rep { - out.push_str(&format!("REP {} {}\n", a.replace(' ', "_"), b.replace(' ', "_"))); - } - for (directive, mask) in - [("FORBIDDENWORD", self.forbidden), ("NOSUGGEST", self.no_suggest), ("SUBSTANDARD", self.substandard), ("KEEPCASE", self.keepcase)] - { - if !mask.is_empty() - && let Some(c) = self.flag_char(mask) - { - out.push_str(&format!("{directive} {c}\n")); - } - } - for (a, b) in &self.iconv { - out.push_str(&format!("ICONV {a} {b}\n")); - } - if !self.ignore.is_empty() { - out.push_str(&format!("IGNORE {}\n", self.ignore.iter().collect::())); - } - let z = |s: &str| if s.is_empty() { "0".to_string() } else { s.to_string() }; - for (kind, rules) in [("PFX", &self.prefixes), ("SFX", &self.suffixes)] { - for r in rules { - let y = if r.cross { "Y" } else { "N" }; - // Continuation classes ride on the appended field, as in the `.aff` source. - let mut add = z(&r.add); - let cont = self.flag_chars_for(r.cont); - if !cont.is_empty() { - add.push('/'); - add.push_str(&cont); - } - out.push_str(&format!("{kind} {} {y} {} {} {}\n", r.flag, z(&r.strip), add, r.cond_text)); - } - } - out.push_str("WORDS\n"); - let mut raw = out.into_bytes(); - let mut prev: &[u8] = &[]; - for i in 0..self.len() { - let Some(w) = self.word(i) else { continue }; - let b = w.as_bytes(); - let p = prev.iter().zip(b).take_while(|(x, y)| x == y).count().min(255); - raw.push(p as u8); - raw.extend_from_slice(b.get(p..).unwrap_or_default()); - let mask = self.flags.get(i).and_then(|&id| self.sets.get(usize::from(id))).copied().unwrap_or_default(); - if !mask.is_empty() { - raw.push(b'/'); - raw.extend_from_slice(self.flag_chars_for(mask).as_bytes()); - } - raw.push(b'\n'); - prev = b; - } - miniz_oxide::deflate::compress_to_vec(&raw, 10) - } - - /// Read data written by [`AffixDict::to_bytes`]. - pub fn from_bytes(data: &[u8]) -> Result { - let raw = miniz_oxide::inflate::decompress_to_vec_with_limit(data, 64 << 20) - .map_err(|e| format!("affix dictionary: inflate failed: {:?}", e.status))?; - let words_at = raw.windows(7).position(|w| w == b"\nWORDS\n").ok_or("affix dictionary: no WORDS section")?; - let head = std::str::from_utf8(raw.get(..words_at).unwrap_or_default()).map_err(|_| "affix dictionary: bad utf-8 in the rules")?; - let mut lines = head.lines(); - if lines.next() != Some(MAGIC) { - return Err("affix dictionary: bad header".into()); - } - let mut rules = Rules::default(); - let mut comments = Vec::new(); - for line in lines { - if let Some(c) = line.strip_prefix("# ") { - comments.push(c.to_string()); - continue; - } - let f: Vec<&str> = split_fields(line).collect(); - let at = |k: usize| f.get(k).copied().unwrap_or(""); - match at(0) { - "TRY" => rules.try_chars = cap_try(at(1)), - "KEY" => rules.key = at(1).to_string(), - directive @ ("FORBIDDENWORD" | "NOSUGGEST" | "SUBSTANDARD" | "KEEPCASE") => { - let c = one_flag(at(1), directive, 0).map_err(|_| format!("affix dictionary: bad {directive} in `{line}`"))?; - match directive { - "FORBIDDENWORD" => rules.forbidden = Some(c), - "NOSUGGEST" => rules.no_suggest = Some(c), - "SUBSTANDARD" => rules.substandard = Some(c), - _ => rules.keepcase = Some(c), - } - } - "ICONV" if f.len() == 3 => rules.iconv.push((at(1).to_string(), at(2).to_string())), - "IGNORE" => rules.ignore = at(1).chars().collect(), - "MAP" => rules.map.push(at(1).chars().collect()), - "REP" => rules.rep.push((at(1).replace('_', " "), at(2).replace('_', " "))), - kind @ ("PFX" | "SFX") => { - let mut fc = at(1).chars(); - let (Some(flag), None) = (fc.next(), fc.next()) else { return Err(format!("affix dictionary: bad flag in `{line}`")) }; - if f.len() != 6 { - return Err(format!("affix dictionary: bad rule `{line}`")); - } - // Continuation classes ride on the appended field, as in the `.aff` source. - let (add, cont) = match at(4).split_once('/') { - Some((a, c)) => (zero_empty(a), c.to_string()), - None => (zero_empty(at(4)), String::new()), - }; - let rule: RawRule = (flag, at(2) == "Y", zero_empty(at(3)), add, cont, at(5).to_string()); - if kind == "SFX" { - rules.suffixes.push(rule); - } else { - rules.prefixes.push(rule); - } - } - "" => {} - other => return Err(format!("affix dictionary: unknown line `{other}`")), - } - } - let body = raw.get(words_at + 7..).unwrap_or_default(); - let mut stems: Vec<(String, String)> = Vec::new(); - let mut cur: Vec = Vec::new(); - let mut i = 0; - while let Some(&p) = body.get(i) { - let rest = body.get(i + 1..).unwrap_or_default(); - let len = rest.iter().position(|&b| b == b'\n').ok_or("affix dictionary: truncated")?; - let line = rest.get(..len).unwrap_or_default(); - i += len + 2; - let p = p as usize; - if p > cur.len() { - return Err("affix dictionary: bad prefix".into()); - } - let (tail, flags) = match line.iter().position(|&b| b == b'/') { - Some(k) => (line.get(..k).unwrap_or_default(), line.get(k + 1..).unwrap_or_default()), - None => (line, &[][..]), - }; - cur.truncate(p); - cur.extend_from_slice(tail); - let word = std::str::from_utf8(&cur).map_err(|_| "affix dictionary: bad utf-8 in a stem")?; - let flags = std::str::from_utf8(flags).map_err(|_| "affix dictionary: bad utf-8 in flags")?; - if word.is_empty() || word.len() > MAX_WORD_BYTES { - return Err("affix dictionary: bad stem".into()); - } - stems.push((word.to_string(), flags.to_string())); - } - AffixDict::build(rules, stems, comments) - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - - const AFF: &str = "SET UTF-8\nTRY aeoiułkr\n\nPFX b Y 1\nPFX b 0 nie .\n\nSFX a Y 3\nSFX a 0 y [^y]\nSFX a ek ka ek\nSFX a 0 em .\n\nSFX c N 1\nSFX c a ą a\n\nMAP 2\nMAP aą\nMAP oóu\nREP 2\nREP ż rz\nREP u ó\n"; - const DIC: &str = "4\nkot/ab\npiesek/a\nkrowa/c\nWarszawa/c\n"; - - fn dict() -> AffixDict { - AffixDict::from_hunspell(AFF, DIC, &["test data"]).unwrap() - } - - #[test] - fn affix_headers_allow_comments_but_rule_conditions_stay_literal() { - let aff = "SET UTF-8\nPFX X Y 1 # prefix comment\nPFX X 0 ne . # rule comment\nSFX y Y 1 # suffix comment\nSFX y 0 y . # rule comment\n"; - let d = AffixDict::from_hunspell(aff, "1\nstrom/Xy\n", &[]).unwrap(); - assert_eq!(d.rule_count(), 2, "headers must not become affix rules"); - for w in ["strom", "stromy", "nestrom", "nestromy"] { - assert!(d.contains(w), "{w}"); - } - for w in ["stromY1", "stromN1", "X", "y"] { - assert!(!d.contains(w), "{w}"); - } - let hash = AffixDict::from_hunspell("SFX H Y 1\nSFX H 0 # .\nTRY a#b\nKEY a#b\n", "1\nstrom/H\n", &[]).unwrap(); - assert!(hash.contains("strom#"), "a hash in an affix field is literal"); - let literal = AffixDict::from_hunspell( - "PFX P Y 1 # header\nPFX P 0 pre #\nSFX S Y 1 # header\nSFX S 0 post #\n", - "3\n#stem/P\nstem#/S\nstem/PS\n", - &[], - ) - .unwrap(); - assert!(literal.contains("pre#stem") && literal.contains("stem#post")); - assert!(!literal.contains("prestem") && !literal.contains("stempost"), "# must remain a literal condition"); - - assert_eq!(hash.try_chars(), "a#b"); - assert_eq!(hash.key(), "a#b"); - assert_eq!(AffixDict::from_bytes(&hash.to_bytes()).unwrap().key(), "a#b"); - } - - #[test] - fn try_alphabet_is_capped_and_deduplicated() { - let aff = format!("TRY {}\n", "abcdefghijklmnopqrstuvwxyz".repeat(10_000)); - let d = AffixDict::from_hunspell(&aff, "1\nkot\n", &[]).unwrap(); - assert_eq!(d.try_chars(), "abcdefghijklmnopqrstuvwxyz"); - let compact = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert_eq!(compact.try_chars(), "abcdefghijklmnopqrstuvwxyz"); - - // More distinct characters than the cap, each repeated: keep the first 128 in order. - let many: String = (0x100u32..0x100 + 300).filter_map(char::from_u32).collect(); - let aff = format!("TRY {many}{many}\n"); - let want: String = many.chars().take(128).collect(); - let d = AffixDict::from_hunspell(&aff, "1\nkot\n", &[]).unwrap(); - assert_eq!(d.try_chars(), want); - let compact = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert_eq!(compact.try_chars(), want); - - // An oversized TRY line fed straight to the compact reader is capped as well. - let raw = format!("{MAGIC}\nTRY {many}{many}\nWORDS\n\0kot\n"); - let bytes = miniz_oxide::deflate::compress_to_vec(raw.as_bytes(), 6); - assert_eq!(AffixDict::from_bytes(&bytes).unwrap().try_chars(), want); - } - - /// Shared hand-written fixture: collectors must reject every path to NOSUGGEST forms. - pub(crate) fn assert_nosuggest_is_honoured(suggest: impl Fn(&AffixDict, &str, usize) -> Vec) { - let aff = "SET UTF-8\nNOSUGGEST N\nFORBIDDENWORD F\nTRY abeklmnostxy\nMAP 1\nMAP ae\nKEY ak\nREP 2\nREP xy toka\nREP zz normal\nPFX X Y 1\nPFX X 0 pre .\nSFX A Y 1\nSFX A 0 ble/B .\nSFX B Y 1\nSFX B 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "4\ntoka/NXA\nnormal/XA\nTkname/N\nbanned/F\n", &[]).unwrap(); - for word in d.forms("toka").into_iter().chain(["TOKA".into(), "Toka".into(), "Tkname".into(), "TKNAME".into()]) { - assert!(d.check(&word), "NOSUGGEST is still correct: {word}"); - assert!(!d.can_suggest(&word), "NOSUGGEST form: {word}"); - } - for input in ["tokax", "tokablesx", "pretokax", "pretokablesx", "toka", "TOKA", "tkname", "teka", "xy", "normaltoka", "toak", "taak"] { - let results = suggest(&d, input, 32); - assert!(results.iter().all(|word| word.split(' ').all(|part| d.can_suggest(part))), "{input}: {results:?}"); - assert!(results.iter().all(|word| !word.to_lowercase().contains("toka") && word.to_lowercase() != "tkname"), "{input}: {results:?}"); - } - assert!(suggest(&d, "norrmal", 8).contains(&"normal".into())); - assert!(suggest(&d, "zz", 8).contains(&"normal".into())); - // Filtering happens before display casing: ordinary mixed-case stems retain the - // existing all-caps suggestion behaviour, with and without a NOSUGGEST declaration. - for aff in ["", "NOSUGGEST N\n"] { - let ordinary = AffixDict::from_hunspell(aff, "2\nmixCase\nhidden/N\n", &[]).unwrap(); - assert!(suggest(&ordinary, "MIXCASX", 8).contains(&"MIXCASE".into())); - } - } - - #[test] - fn nosuggest_is_correct_but_not_suggestible_and_survives_compact_format() { - let d = AffixDict::from_hunspell("NOSUGGEST N\nSFX S Y 1\nSFX S 0 s .\n", "2\nsecret/NS\nnormal/S\n", &[]).unwrap(); - for dict in [&d, &AffixDict::from_bytes(&d.to_bytes()).unwrap()] { - for word in ["secret", "secrets", "Secret", "SECRETS"] { - assert!(dict.check(word), "{word}"); - assert!(!dict.can_suggest(word), "{word}"); - } - assert!(dict.contains_suggestible("normal") && dict.can_suggest("NORMALS")); - } - assert!(AffixDict::from_hunspell("NOSUGGEST NN\n", "1\nword\n", &[]).is_err()); - } - - #[test] - fn stems_suffixes_prefixes_and_cross_products() { - let d = dict(); - assert_eq!(d.len(), 4); - for w in ["kot", "koty", "kotem", "piesek", "pieska", "piesekem", "krowa", "krową", "niekot", "niekoty", "niekotem", "Warszawa", "Warszawą"] - { - assert!(d.contains(w), "{w}"); - } - for w in ["kotą", "pieseka", "pieskem", "niepiesek", "niekrową", "krowy", "warszawa", "", "nie", "y", "ka"] { - assert!(!d.contains(w), "{w}"); - } - } - - #[test] - fn case_variants() { - let d = dict(); - assert!(d.check("Kot") && d.check("KOT") && d.check("Koty") && d.check("WARSZAWĄ") && d.check("Warszawą")); - assert!(!d.check("warszawa") && !d.check("kOT") && !d.check("KoT")); - assert_eq!(casing("Kot"), Casing::Initial); - assert_eq!(casing("KOT"), Casing::Upper); - assert_eq!(casing("K"), Casing::Initial); - assert_eq!(casing("kot"), Casing::Lower); - assert_eq!(casing("McDonald"), Casing::Mixed); - assert_eq!(capitalize("żółw"), "Żółw"); - } - - #[test] - fn forms_are_all_accepted() { - let d = dict(); - let mut forms = d.forms("kot"); - forms.sort(); - forms.dedup(); - assert_eq!(forms, ["kot", "kotem", "koty", "niekot", "niekotem", "niekoty"]); - for stem in d.stems().map(str::to_string).collect::>() { - for f in d.forms(&stem) { - assert!(d.contains(&f), "{stem} → {f}"); - } - } - } - - #[test] - fn compact_format_round_trips() { - let d = dict(); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert_eq!(back.len(), d.len()); - assert_eq!(back.rule_count(), d.rule_count()); - assert_eq!(back.comments(), ["test data"]); - assert_eq!(back.rep(), d.rep()); - assert_eq!(back.map(), d.map()); - assert_eq!(back.try_chars(), "aeoiułkr"); - for w in ["kotem", "niekoty", "pieska", "krową", "Warszawą"] { - assert!(back.contains(w), "{w}"); - } - assert!(!back.contains("kotą")); - } - - #[test] - fn flag_utf8_and_default_modes_are_accepted() { - // `FLAG UTF-8` (VERO pt_BR) and `FLAG DEFAULT` are per-character flag modes: the same - // mode the reader already assumes. Only `long` and `num` stay unsupported. - for mode in ["UTF-8", "DEFAULT"] { - let aff = format!("SET UTF-8\nFLAG {mode}\nSFX a Y 1\nSFX a 0 y [^y]\n"); - let d = AffixDict::from_hunspell(&aff, "1\nkot/a\n", &[]).unwrap(); - assert!(d.contains("kot") && d.contains("koty"), "{mode}"); - } - assert!(AffixDict::from_hunspell("FLAG UTF-8\n", "0\n", &[]).is_ok(), "FLAG alone is harmless"); - } - - #[test] - fn emoji_flags_with_a_variation_selector_load() { - // RLA-ES (the Spanish dictionaries in LibreOffice and most Linux distributions) writes its - // telephone flag as ☎ followed by U+FE0F, in the rule and on the stems. - let aff = "SET UTF-8\nFLAG UTF-8\nPFX \u{260e}\u{fe0f} Y 1\nPFX \u{260e}\u{fe0f} 0 tele .\nSFX S Y 1\nSFX S 0 s [aeiou]\n"; - let d = AffixDict::from_hunspell(aff, "2\nbanco/S\u{260e}\u{fe0f}\nbasura/S\n", &[]).unwrap(); - assert!(d.contains("banco") && d.contains("bancos")); - assert!(d.contains("telebanco") && d.contains("telebancos"), "the ☎ prefix applies"); - assert!(d.contains("basuras") && !d.contains("telebasura"), "only to stems that carry it"); - assert!(!d.flag_chars.contains(&EMOJI_SELECTOR), "the selector is not a flag"); - } - - #[test] - fn emoji_forbiddenword_and_nosuggest_flags_load() { - // The same selector after the flag of the directives that name one flag. - let aff = "SET UTF-8\nFLAG UTF-8\nFORBIDDENWORD \u{260e}\u{fe0f}\nNOSUGGEST \u{1f6ab}\u{fe0f}\n"; - let d = AffixDict::from_hunspell(aff, "3\nbanco\nbanko/\u{260e}\u{fe0f}\nbancu/\u{1f6ab}\u{fe0f}\n", &[]).unwrap(); - assert!(d.contains("banco") && d.contains("bancu")); - assert!(!d.contains("banko"), "a stem with the FORBIDDENWORD flag is not a word"); - } - - #[test] - fn more_than_64_distinct_flags_load() { - // VERO pt_BR uses 106 distinct flags (FLAG UTF-8). Build one with every basic Latin - // letter, the digits and eight accented letters: 70 flags, more than 64. - let mut flags = String::new(); - let mut aff = String::from("SET UTF-8\nFLAG UTF-8\n"); - let chars: Vec = ('a'..='z').chain('A'..='Z').chain('0'..='9').chain(['á', 'é', 'í', 'ó', 'ú', 'ç', 'ã', 'õ']).collect(); - assert_eq!(chars.len(), 70); - for c in chars { - aff.push_str(&format!("SFX {c} N 1\nSFX {c} 0 {c} .\n")); - flags.push(c); - } - let d = AffixDict::from_hunspell(&aff, &format!("1\nverbo/{flags}\n"), &[]).unwrap(); - assert!(d.contains("verbo")); - assert!(d.contains("verboa") && d.contains("verboz") && d.contains("verbo9")); - assert!(d.contains("verboç") && d.contains("verboã"), "multi-byte UTF-8 flags"); - assert!(!d.contains("verboß"), "ß is not a flag and not a stem"); - } - - #[test] - fn more_than_128_distinct_flags_load() { - // The Hungarian dictionary uses 158 single-byte flags: 200 flags here, U+0080 up. - let flags: String = (0x80u32..0x80 + 200).filter_map(char::from_u32).collect(); - let mut aff = String::from("SET UTF-8\n"); - for (i, c) in flags.chars().enumerate() { - aff.push_str(&format!("SFX {c} N 1\nSFX {c} 0 x{i} .\n")); - } - let half: String = flags.chars().step_by(2).collect(); - let d = AffixDict::from_hunspell(&aff, &format!("1\nszó/{half}\n"), &[]).unwrap(); - assert!(d.contains("szóx0") && d.contains("szóx198") && d.contains("szóx130")); - assert!(!d.contains("szóx1") && !d.contains("szóx199") && !d.contains("szóx131")); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert!(back.contains("szóx198") && !back.contains("szóx199")); - let too_many: String = (0x100u32..0x100 + 257).filter_map(char::from_u32).collect(); - assert!(AffixDict::from_hunspell("SET UTF-8\n", &format!("1\nszó/{too_many}\n"), &[]).is_err()); - } - - #[test] - fn forbidden_words_are_rejected() { - // VERO pt_BR marks spellings it must reject (flag ý under FLAG UTF-8) as stems carrying - // FORBIDDENWORD: listed, but never correct and never inflected. - let aff = "SET UTF-8\nFLAG UTF-8\nFORBIDDENWORD ý\nSFX a Y 1\nSFX a 0 y [^y]\n"; - let d = AffixDict::from_hunspell(aff, "2\nkot/a\nimundo/ý\n", &[]).unwrap(); - assert!(d.check("kot") && d.check("koty") && d.check("Kot") && d.check("KOT")); - assert!(!d.check("imundo") && !d.check("Imundo") && !d.check("IMUNDO")); - assert!(!d.contains("imundoy"), "a forbidden stem must not inflect"); - assert!(d.forms("imundo").is_empty()); - } - - #[test] - fn continuation_classes_chain_suffixes() { - // `SFX A 0 able/B`: the form derived by A carries flag B, so a suffix of class B may - // follow (workable → workables). The bare stem has A only, so `works` stays wrong. - let aff = "SET UTF-8\nFLAG UTF-8\nSFX A N 1\nSFX A 0 able/B .\nSFX B N 1\nSFX B 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "1\nwork/A\n", &[]).unwrap(); - assert!(d.contains("work") && d.contains("workable") && d.contains("workables")); - assert!(!d.contains("works"), "the stem has A, not B"); - assert!(!d.contains("workabless"), "B declares no continuation"); - assert!(d.check("Workable") && d.check("WORKABLES")); - } - - #[test] - fn continuation_chains_stop_at_two_suffixes() { - // Hunspell strips at most two suffixes: work + able/B + s/C is a word, a third (C) is not. - let aff = "SET UTF-8\nFLAG UTF-8\nSFX A N 1\nSFX A 0 able/B .\nSFX B N 1\nSFX B 0 s/C .\nSFX C N 1\nSFX C 0 x .\n"; - let d = AffixDict::from_hunspell(aff, "1\nwork/A\n", &[]).unwrap(); - assert!(d.contains("workable") && d.contains("workables")); - assert!(!d.contains("workablesx"), "three chained suffixes exceed the limit"); - assert!(!d.forms("work").iter().any(|f| f == "workablesx")); - } - - #[test] - fn forms_include_continuation_chains() { - let aff = "SET UTF-8\nFLAG UTF-8\nSFX A N 1\nSFX A 0 able/B .\nSFX B N 1\nSFX B 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "1\nwork/A\n", &[]).unwrap(); - let mut f = d.forms("work"); - f.sort(); - f.dedup(); - assert_eq!(f, ["work", "workable", "workables"]); - for form in d.forms("work") { - assert!(d.contains(&form), "every listed form is accepted: {form}"); - } - } - - #[test] - fn continuation_classes_merge_the_flags_of_a_group() { - // `s` under B and under C share one group (same append, same strip): a form carrying - // either flag takes it, and the stem carrying neither doesn't. - let aff = "SET UTF-8\nSFX A N 1\nSFX A 0 able/C .\nSFX D N 1\nSFX D 0 er/B .\nSFX B N 1\nSFX B 0 s .\nSFX C N 1\nSFX C 0 s [e]\n"; - let d = AffixDict::from_hunspell(aff, "2\nwork/A\nplay/D\n", &[]).unwrap(); - assert!(d.contains("workables") && d.contains("players")); - assert!(!d.contains("works") && !d.contains("plays") && !d.contains("workabless")); - assert!(d.forms("work").iter().any(|f| f == "workables") && d.forms("play").iter().any(|f| f == "players")); - } - - /// `n` suffix rules of flag A on one stem, `rule(i)` giving each rule's strip, append and condition. - fn many_rules(n: usize, rule: impl Fn(usize) -> String) -> AffixDict { - let mut aff = format!("SET UTF-8\nSFX A Y {n}\n"); - for i in 0..n { - aff.push_str(&format!("SFX A {}\n", rule(i))); - } - AffixDict::from_hunspell(&aff, "1\nkot/A\n", &[]).unwrap() - } - - #[test] - fn self_continuing_rules_check_in_linear_time() { - // Thousands of empty-append rules that continue with their own flag: each check used to - // follow every rule from every rule, millions of steps a word (#3613). The checks take - // about a millisecond; the old checker took 14 s for two of them at 16,000 rules, so at - // 40,000 (quadratic: minutes) the 10 s bound leaves a busy machine a wide margin. - let d = many_rules(40_000, |_| "0 0/A .".into()); - let t = std::time::Instant::now(); - assert!(d.check("kot") && !d.check("pies") && !d.check("kotx")); - assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed()); - } - - #[test] - fn forms_of_self_continuing_rules_are_bounded() { - // Every rule's form continues with every rule: millions of strings for one stem (#3613). - let same = many_rules(3000, |_| "0 0/A .".into()); - let distinct = many_rules(3000, |i| format!("0 x{i}/A .")); - let t = std::time::Instant::now(); - let (a, b) = (same.forms("kot"), distinct.forms("kot")); - // The cap on the count is what tells the old code (9 million forms each) apart; the - // generous time bound (tens of ms here) only backs up the rule-visit cap. - assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed()); - assert!(a.len() <= MAX_FORMS && b.len() <= MAX_FORMS, "{} {}", a.len(), b.len()); - assert!(b.iter().any(|f| f == "kotx0x1"), "chained forms are still listed"); - } - - #[test] - fn many_strips_per_suffix_index_in_linear_time() { - // One append with thousands of strips: the index used to scan the append's groups for - // each rule. Building 200,000 rules takes well under a second; the old index took 3 s at - // 30,000 and over 10 minutes at 300,000. - let n = 200_000; - let t = std::time::Instant::now(); - let d = many_rules(n, |i| format!("k{i} y .")); - assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed()); - assert_eq!(d.sfx_index.get("y").map(Vec::len), Some(n)); - assert!(!d.contains("koty")); - } - - #[test] - fn compact_format_round_trips_continuations_and_forbidden() { - let aff = "SET UTF-8\nFLAG UTF-8\nFORBIDDENWORD ý\nSFX A N 1\nSFX A 0 able/B .\nSFX B N 1\nSFX B 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "2\nwork/A\nimundo/ý\n", &[]).unwrap(); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert!(back.contains("work") && back.contains("workable") && back.contains("workables")); - assert!(!back.contains("works"), "the stem has A, not B, after a round trip"); - assert!(!back.check("imundo") && !back.contains("imundoy"), "forbidden survives a round trip"); - } - - #[test] - fn dic_bom_glued_to_the_count_line_is_skipped() { - // VERO's .dic opens with a UTF-8 BOM glued to its count line; it must not become a stem. - let aff = "SET UTF-8\nSFX a Y 1\nSFX a 0 y [^y]\n"; - let d = AffixDict::from_hunspell(aff, "\u{feff}1\nkot/a\n", &[]).unwrap(); - assert_eq!(d.len(), 1); - assert!(d.contains("kot") && d.contains("koty")); - } - - #[test] - fn flag_aliases_stand_for_flag_sets() { - // `AF`: `/1` in the `.dic` and in a continuation class means the first alias's flags. - // Morphological aliases (`AM`) and the fields after a stem are skipped. - let aff = "SET UTF-8\nAF 2\nAF ab # 1\nAF c # 2\nAM 1\nAM po:noun\nSFX a Y 1\nSFX a 0 ok/2 . 1\nSFX b Y 1\nSFX b 0 at . 1\nSFX c Y 1\nSFX c 0 ban . 1\n"; - let d = AffixDict::from_hunspell(aff, "1\nház/1\t1\n", &[]).unwrap(); - assert!(d.contains("ház") && d.contains("házok") && d.contains("házat") && d.contains("házokban")); - assert!(!d.contains("házban"), "the stem has a and b, not c"); - // An undefined alias reads as no flags, as in Hunspell: the stem loads, without affixes. - let undefined = AffixDict::from_hunspell(aff, "2\nház/3\nkert/1\n", &[]).unwrap(); - assert!(undefined.check("ház") && !undefined.check("házok"), "alias 3 is not defined"); - assert!(undefined.check("kertok"), "the other entries keep their aliases"); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert!(back.contains("házokban") && !back.contains("házban"), "aliases are expanded in the compact format"); - } - - #[test] - fn iconv_and_ignore_normalize_input() { - // Ligatures read as their letters; `IGNORE` characters vanish from input and stems. - let aff = "SET UTF-8\nICONV 2\nICONV fi fi\nICONV ffl ffl\nIGNORE ()\nSFX a Y 1\nSFX a 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "2\nfine/a\nwaf(fle)/a\n", &[]).unwrap(); - assert!(d.check("fine") && d.check("fines") && d.check("Fine") && d.contains("fine")); - assert!(d.check("waffle") && d.check("waffles") && d.check("wa(ffle)")); - assert!(!d.check("finer")); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert!(back.check("fines") && back.check("waffle") && back.check("waf(fle)")); - } - - #[test] - fn alias_numbers_are_read_as_hunspell_reads_them() { - // Checked with the Hunspell 1.7 command line. With an `AF` table a flag field is the alias - // its leading digits number: `fal/2x` is alias 2, `lab/b` has no flags. - let aff = "SET UTF-8\nAF 2\nAF B\nAF C\nSFX B Y 1\nSFX B 0 s .\nSFX C Y 1\nSFX C 0 x .\nSFX b Y 1\nSFX b 0 q .\n"; - let d = AffixDict::from_hunspell(aff, "3\nlab/b\nfal/2x\nkut/1\n", &[]).unwrap(); - assert!(d.check("lab") && !d.check("labq") && !d.check("labs")); - assert!(d.check("falx") && !d.check("fals") && d.check("kuts")); - // A table without its count line is rejected: no alias applies. - let broken = AffixDict::from_hunspell("SET UTF-8\nAF B\nAF C\nSFX B Y 1\nSFX B 0 s .\n", "2\nlab/1\nfal/2\n", &[]).unwrap(); - assert!(broken.check("lab") && !broken.check("labs") && !broken.check("fals")); - // `AF 0` is rejected the same way: `lab/B` keeps its literal flag. - let zero = AffixDict::from_hunspell("SET UTF-8\nAF 0\nSFX B Y 1\nSFX B 0 s .\n", "1\nlab/B\n", &[]).unwrap(); - assert!(zero.check("lab") && zero.check("labs")); - // An undefined alias in a continuation class leaves the rule in force, without it. - let cont = AffixDict::from_hunspell("SET UTF-8\nAF 1\nAF a\nSFX a Y 1\nSFX a 0 ok/7 .\n", "1\nhaz/1\n", &[]).unwrap(); - assert!(cont.check("haz") && cont.check("hazok")); - } - - #[test] - fn a_rep_count_line_may_carry_a_comment() { - let d = AffixDict::from_hunspell("SET UTF-8\nREP 1 # confusions\nREP x y\n", "1\nay\n", &[]).unwrap(); - assert_eq!(d.rep(), [("x".to_string(), "y".to_string())]); - } - - #[test] - fn count_lines_may_carry_a_comment() { - // Hunspell reads the first `AF` and `ICONV` line as the count, comment and all: `AF 2 #` - // is no alias, and `ICONV 1 # …` doesn't turn `1` into `#`. - let aff = "SET UTF-8\nAF 2 # aliases\nAF a # 1\nAF b # 2\nICONV 1 # ligatures\nICONV q y\nSFX a Y 1\nSFX a 0 ok .\nSFX b Y 1\nSFX b 0 at .\n"; - let d = AffixDict::from_hunspell(aff, "3\nhaz/1\nkert/2\na1\n", &[]).unwrap(); - assert!(d.check("hazok") && !d.check("hazat") && d.check("kertat") && !d.check("kertok"), "alias 1 is `a`"); - assert!(d.check("a1") && !d.check("a#")); - // The UTF-8 reader keeps such a line as it is, so both readers agree on it. - let (a, dic) = crate::hunspell::decode(aff.as_bytes(), "1\nhaz/1\n".as_bytes()).unwrap(); - assert!(a.contains("AF 2 # aliases") && a.contains("ICONV 1 # ligatures"), "{a}"); - assert!(AffixDict::from_hunspell(&a, &dic, &[]).unwrap().check("hazok")); - } - - #[test] - fn dic_entries_may_hold_spaces() { - // `ad hoc/B` is one entry, as in Hunspell; `ad` alone is no word. - let aff = "SET UTF-8\nSFX B Y 1\nSFX B 0 s .\n"; - // Checked with the Hunspell 1.7 command line: without a tab or a morphological field the - // whole line is the entry (`kot/B 12` has the flag `B`), and leading blanks belong to it - // (` lab` is no `lab`). - let d = AffixDict::from_hunspell(aff, "5\nad hoc/B\tpo:adv\nword/B po:noun\nkot/B 12\n lab/B\nés így/B\n", &[]).unwrap(); - assert!(d.contains("ad hoc") && d.contains("ad hocs") && d.contains("és így") && d.contains("és ígys")); - assert!(!d.contains("ad") && !d.contains("hoc") && !d.contains("ads") && !d.contains("így")); - assert!(d.contains("words") && d.contains("kots") && !d.contains("kot/B")); - assert!(!d.contains("lab") && !d.contains("labs")); - } - - #[test] - fn ignored_characters_leave_the_stripped_text_alone() { - // Hunspell removes `IGNORE` characters from the appended text, not from the stripped - // one: `SFX a bx c` never matches the stem `abx` (stored as `ab`), so `ac` is wrong, - // while `SFX a b cx` appends `c` (checked with the Hunspell 1.7 command line). - let strip = AffixDict::from_hunspell("SET UTF-8\nIGNORE x\nSFX a Y 1\nSFX a bx c .\n", "1\nabx/a\n", &[]).unwrap(); - assert!(strip.check("ab") && strip.check("abx") && !strip.check("ac")); - let add = AffixDict::from_hunspell("SET UTF-8\nIGNORE x\nSFX a Y 1\nSFX a b cx .\n", "1\nab/a\n", &[]).unwrap(); - assert!(add.check("ac") && add.check("acx")); - for d in [&strip, &add] { - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert_eq!(back.check("ac"), d.check("ac")); - } - } - - #[test] - fn normalizing_borrows_when_nothing_changes() { - let d = AffixDict::from_hunspell("SET UTF-8\nICONV 1\nICONV fi fi\nIGNORE ()\n", "1\nfine\n", &[]).unwrap(); - assert!(matches!(d.normalize("fine"), std::borrow::Cow::Borrowed("fine"))); - assert_eq!(d.normalize("(fi)ne"), "fine"); - assert!(matches!(dict().normalize("kot"), std::borrow::Cow::Borrowed("kot"))); - } - - #[test] - fn keepcase_stems_match_only_as_written() { - let aff = "SET UTF-8\nKEEPCASE k\nSFX a Y 1\nSFX a 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "3\npH/k\nkm/ka\nkot/a\n", &[]).unwrap(); - assert!(d.check("pH") && d.check("km") && d.check("kms")); - assert!(!d.check("PH") && !d.check("Km") && !d.check("KM") && !d.check("Kms")); - assert!(d.check("Kot") && d.check("KOTS"), "other stems still follow the sentence and heading case"); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert!(back.check("km") && !back.check("Km")); - } - - #[test] - fn substandard_and_lemma_present_words_are_correct() { - // `SUBSTANDARD` and `LEMMA_PRESENT` steer morphological generation, and a SUBSTANDARD - // stem is never suggested; the stems and rules they mark are still correct. - let aff = "SET UTF-8\nSUBSTANDARD x\nLEMMA_PRESENT l\nSFX a Y 2\nSFX a 0 y .\nSFX a 0 em/x .\n"; - let d = AffixDict::from_hunspell(aff, "2\nkot/a\npies/axl\n", &[]).unwrap(); - for dict in [&d, &AffixDict::from_bytes(&d.to_bytes()).unwrap()] { - assert!(dict.check("kot") && dict.check("koty") && dict.check("kotem")); - assert!(dict.check("pies") && dict.check("piesy") && dict.check("Piesem")); - assert!(dict.can_suggest("koty") && dict.can_suggest("kotem") && dict.contains_suggestible("kot")); - assert!(!dict.can_suggest("piesy") && !dict.can_suggest("Pies") && !dict.contains_suggestible("pies")); - } - } - - #[test] - fn unsupported_directives_are_refused_and_the_rest_load() { - // A dictionary that needs a directive outside this engine fails to load rather than - // accepting the wrong words. - for dir in UNSUPPORTED { - assert!(AffixDict::from_hunspell(&format!("SET UTF-8\n{dir} X\n"), DIC, &[]).is_err(), "{dir} should be refused"); - } - // The directives this engine reads, with no other change to the dictionary (`AF` would - // turn the flags of `DIC` into alias numbers; it has tests of its own). - for line in ["AM 1\nAM po:noun", "ICONV 1\nICONV fi fi", "IGNORE ()", "KEEPCASE k", "SUBSTANDARD x", "LEMMA_PRESENT l"] { - let d = AffixDict::from_hunspell(&format!("{AFF}{line}\n"), DIC, &[]).unwrap_or_else(|e| panic!("{line}: {e}")); - assert!(d.check("koty") && d.check("niekoty") && !d.check("kotx"), "{line}"); - } - } - - #[test] - fn fields_split_on_spaces_and_tabs_only() { - // A flag byte read as U+00A0 (no-break space) or U+0085 is a flag, not a separator. - let aff = "SET UTF-8\nSFX \u{a0} Y 1\nSFX \u{a0} 0 y .\nSFX \u{85} Y 1\nSFX \u{85} 0 em .\n"; - let d = AffixDict::from_hunspell(aff, "1\nkot/\u{a0}\u{85}\n", &[]).unwrap(); - assert!(d.contains("koty") && d.contains("kotem")); - let back = AffixDict::from_bytes(&d.to_bytes()).unwrap(); - assert!(back.contains("koty") && back.contains("kotem")); - // The same bytes as one-flag directives, as Hunspell reads them. - let aff = "SET UTF-8\nFORBIDDENWORD \u{a0}\nKEEPCASE \u{85}\nSFX a Y 1\nSFX a 0 s .\n"; - let d = AffixDict::from_hunspell(aff, "3\nkot/a\nrossz/\u{a0}\npH/\u{85}\n", &[]).unwrap(); - for dict in [&d, &AffixDict::from_bytes(&d.to_bytes()).unwrap()] { - assert!(dict.check("kots") && !dict.check("rossz")); - assert!(dict.check("pH") && !dict.check("PH")); - } - } - - #[test] - fn unsupported_and_broken_input_is_an_error() { - assert!(AffixDict::from_hunspell("FLAG long\n", DIC, &[]).is_err()); - assert!(AffixDict::from_hunspell("FLAG num\n", DIC, &[]).is_err()); - assert!(AffixDict::from_hunspell("COMPOUNDFLAG X\n", DIC, &[]).is_err()); - assert!(AffixDict::from_hunspell("SFX a Y 1\nSFX a 0 y [ab\n", DIC, &[]).is_err()); - assert!(AffixDict::from_bytes(b"not deflate").is_err()); - let ok = dict().to_bytes(); - assert!(AffixDict::from_bytes(&ok[..ok.len() / 2]).is_err()); - let garbage = miniz_oxide::deflate::compress_to_vec(b"WCAFF1\nWORDS\n\x09abc\n", 6); - assert!(AffixDict::from_bytes(&garbage).is_err(), "prefix longer than the previous word"); - let empty = AffixDict::default(); - assert!(!empty.contains("kot") && !empty.check("Kot") && empty.forms("kot").is_empty()); - } - - proptest::proptest! { - #[test] - fn lookups_never_panic(s in "\\PC{0,40}", bytes in proptest::collection::vec(proptest::num::u8::ANY, 0..200)) { - let d = dict(); - let _ = d.check(&s); - let _ = d.forms(&s); - let _ = AffixDict::from_bytes(&bytes); - let _ = AffixDict::from_bytes(&miniz_oxide::deflate::compress_to_vec(&bytes, 1)); - } - } -} diff --git a/crates/proof/src/bg.rs b/crates/proof/src/bg.rs deleted file mode 100644 index 6b456d8c9..000000000 --- a/crates/proof/src/bg.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Bulgarian hyphenation by rule ([`ProofLang::Bg`](crate::ProofLang::Bg)). -//! -//! Bulgarian spelling splits words at syllables, and its rules are regular enough to apply -//! directly, so no pattern file or word list is needed (and none is borrowed): -//! - two vowels next to each other may be split (`на-у-ка`); -//! - one consonant between vowels goes with the next syllable (`ма-ма`, `ро-за`); -//! - of two or more consonants between vowels, the first stays with the syllable before and the -//! rest go with the next (`сес-тра`, `въз-дух`, `ком-пю-тър`) — the spelling rules allow any -//! split of a consonant group, and this is the usual one — except that a consonant followed by -//! `р` or `л` starts the next syllable together with it (`до-бре`, `ко-пре`, `та-бли-ца`); -//! - `й` before a consonant stays with the vowel before it (`строй-ка`, `май-ка`); -//! - `ьо` is never split, and the consonant before `ь` goes with it (`шо-фьор`); -//! - `дж` and `дз`, one sound each, are never split (`хо-джа`). -//! -//! A single letter left at the end or start of a line is ruled out by the paragraph's -//! [`Limits`](crate::hyphen::Limits) (`Бъл-га-рия`, not `Бъл-га-ри-я`). - -fn is_vowel(c: char) -> bool { - matches!(c, 'а' | 'е' | 'и' | 'о' | 'у' | 'ъ' | 'ю' | 'я' | 'ѝ') -} - -/// Consonants that may start a syllable before `р` or `л` (`бр`, `пл`, `тр`, `гл` …). -fn is_obstruent(c: char) -> bool { - matches!(c, 'б' | 'в' | 'г' | 'д' | 'з' | 'к' | 'п' | 'с' | 'т' | 'ф' | 'х') -} - -/// A lowercase Bulgarian letter (the words [`points`] splits). -pub(crate) fn is_letter(c: char) -> bool { - matches!(c, 'а'..='я' | 'ѝ') -} - -/// Break points of one lowercase Bulgarian word (char indices; a hyphen goes before that char). -/// Empty for anything that is not entirely Bulgarian letters. -pub fn points(word: &[char]) -> Vec { - if word.len() < 2 || !word.iter().all(|&c| is_letter(c)) { - return Vec::new(); - } - let vowels: Vec = (0..word.len()).filter(|&i| is_vowel(word[i])).collect(); - let mut out = Vec::new(); - for pair in vowels.windows(2) { - let (v1, v2) = (pair[0], pair[1]); - let mut pt = match v2 - v1 - 1 { - 0 | 1 => v1 + 1, - _ => v1 + 2, - }; - // A consonant with `р` or `л` after it starts a syllable together with it. - if v2 - v1 > 2 && matches!(word[v2 - 1], 'р' | 'л') && is_obstruent(word[v2 - 2]) { - pt = v2 - 2; - } - // `дж`, `дз`: one sound, so both go to the next syllable. - if pt > v1 + 1 && word[pt - 1] == 'д' && matches!(word[pt], 'ж' | 'з') { - pt -= 1; - } - // `ь` keeps the consonant before it (`фьо`). - if pt < v2 && word[pt] == 'ь' && pt > v1 + 1 { - pt -= 1; - } - if pt > v1 && pt <= v2 && word[pt - 1] != 'ь' { - out.push(pt); - } - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn split(w: &str) -> String { - let chars: Vec = w.chars().collect(); - let pts = points(&chars); - let mut s = String::new(); - for (i, c) in chars.iter().enumerate() { - if pts.contains(&i) { - s.push('-'); - } - s.push(*c); - } - s - } - - #[test] - fn syllables_follow_the_bulgarian_rules() { - for (word, expected) in [ - ("мама", "ма-ма"), - ("наука", "на-у-ка"), - ("сестра", "сес-тра"), - ("въздух", "въз-дух"), - ("компютър", "ком-пю-тър"), - ("стройка", "строй-ка"), - ("майка", "май-ка"), - ("шофьор", "шо-фьор"), - ("ходжа", "хо-джа"), - ("здравей", "здра-вей"), - ("плевен", "пле-вен"), - ("българия", "бъл-га-ри-я"), - ("сричкопренасяне", "срич-ко-пре-на-ся-не"), - ("добре", "до-бре"), - ("таблица", "та-бли-ца"), - ("отговор", "от-го-вор"), - ] { - assert_eq!(split(word), expected, "{word}"); - } - } - - #[test] - fn non_bulgarian_letters_are_left_alone() { - assert!(points(&"word".chars().collect::>()).is_empty()); - assert!(points(&"съобщение1".chars().collect::>()).is_empty()); - assert!(points(&"объём".chars().collect::>()).is_empty(), "Russian ё is not a Bulgarian letter"); - } -} diff --git a/crates/proof/src/cs/grammar.rs b/crates/proof/src/cs/grammar.rs deleted file mode 100644 index 26a14fcdf..000000000 --- a/crates/proof/src/cs/grammar.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Generic repeated-word and spacing checks with Czech messages. -//! No Czech-specific grammar, comma or sentence-capital rules are inferred. - -use crate::{Issue, IssueKind, ProofLang, words}; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::Cs } -} - -pub fn check(text: &str) -> Vec { - let mut issues = Vec::new(); - repeated_words(text, &words(text), &mut issues); - spaces(text, &mut issues); - issues.sort_by_key(|i| (i.start, i.end)); - issues -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() && gap.chars().all(char::is_whitespace) && w0.chars().all(char::is_alphabetic) && w0.to_lowercase() == w1.to_lowercase() { - v.push(issue(*a0, *b1, format!("Opakované slovo: „{w1}“"), vec![w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - // A ratio/score keeps the spaces in digit-space-colon-space-digit. - let ratio = p == ':' - && text.get(..i).and_then(|before| before.chars().next_back()).is_some_and(|c| c.is_ascii_digit()) - && after == Some(' ') - && it.next().is_some_and(|c| c.is_ascii_digit()); - let flag = !ratio - && match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Mezera před interpunkcí".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Dvojitá mezera".into(), vec![" ".into()])); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn repeated_words_and_spaces_have_czech_messages() { - let text = "Město město , škola."; - let issues = check(text); - assert_eq!(issues.len(), 3, "{issues:?}"); - assert_eq!(issues[0].message, "Opakované slovo: „město“"); - assert_eq!(issues[0].suggestions, ["Město"]); - assert!(issues.iter().any(|i| i.message == "Mezera před interpunkcí" && &text[i.start..i.end] == " ,")); - assert!(issues.iter().any(|i| i.message == "Dvojitá mezera")); - assert!(issues.iter().all(|i| i.lang == ProofLang::Cs && i.kind == IssueKind::Grammar)); - for text in ["město. škola", "Dr. Novák", "A apple", "Město, město!", "Úsměv :) Číslo .5 a tečky ..."] { - assert!(check(text).is_empty(), "{text}"); - } - } - - #[test] - fn ratios_and_scores_keep_their_spaces() { - for text in ["3 : 1", "1 : 2", "Skóre 12 : 10.", "Poměr 1 : 100."] { - assert!(check(text).is_empty(), "{text}: {:?}", check(text)); - } - for text in ["Slovo : další", "3 : slovo", "slovo : 1", "3 :"] { - assert!(check(text).iter().any(|i| i.message == "Mezera před interpunkcí"), "{text}"); - } - } - - proptest::proptest! { - #[test] - fn never_panics_and_ranges_are_valid(s in "\\PC{0,80}") { - for issue in check(&s) { - proptest::prop_assert!(issue.start <= issue.end && issue.end <= s.len()); - proptest::prop_assert!(s.is_char_boundary(issue.start) && s.is_char_boundary(issue.end)); - } - } - } -} diff --git a/crates/proof/src/cs/mod.rs b/crates/proof/src/cs/mod.rs deleted file mode 100644 index 9f8bbd0a7..000000000 --- a/crates/proof/src/cs/mod.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Czech proofing using the installed Hunspell `cs_CZ` dictionary (never bundled). -//! Suggestions use its runtime MAP/REP/TRY/KEY tables. Generic grammar checks have Czech -//! messages; hyphenation keeps the existing fallback, without Czech pattern data. - -pub mod grammar; -pub(crate) mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of a Czech Hunspell dictionary. -const NAMES: &[&str] = &["cs_CZ", "cs-CZ", "cs"]; - -/// The Czech dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Czech"); - -/// The Czech dictionary: the installed Hunspell `cs_CZ` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Czech spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Czech dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is a Czech dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Is `word` spelled correctly in Czech? Numbers, single letters, short words in capitals -/// (acronyms: `ČR`, `EU`), words with digits and words in other scripts are accepted, and so -/// is every word when no Czech dictionary is installed. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - if w.contains('-') { - return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); - } - known(d, w) -} - -/// Is one word (no hyphens) in the dictionary or the user dictionary? -fn known(d: &AffixDict, w: &str) -> bool { - d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn dictionary_lookup_accepts_czech_file_names() { - let dir = std::env::temp_dir().join(format!("wordcraft-cs-aliases-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - for name in ["cs_CZ", "cs-CZ", "cs"] { - std::fs::write(dir.join(format!("{name}.aff")), test_dict::AFF).unwrap(); - std::fs::write(dir.join(format!("{name}.dic")), test_dict::DIC).unwrap(); - let (aff, dic) = crate::hunspell::find_in(&dir, NAMES).unwrap(); - let d = crate::hunspell::load(&aff, &dic).unwrap(); - assert!(is_correct_with(&d, "městě")); - std::fs::remove_file(aff).unwrap(); - std::fs::remove_file(dic).unwrap(); - } - std::fs::remove_dir(dir).unwrap(); - } - - #[test] - fn czech_words_affixes_chains_case_and_diacritics() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert_eq!(d.len(), 20); - assert_eq!(d.rule_count(), 8); - for w in [ - "město", - "města", - "městě", - "neměsta", - "rychle", - "rychlejší", - "učitel", - "učitelé", - "Praha", - "Praze", - "Město", - "POČÍTAČ", - "příliš", - "žluťoučký", - "kůň", - "úpěl", - "ďábelské", - "ódy", - "děkujeme", - "zpráva", - "zprava", - "škola-počítač", - ] { - assert!(is_correct(w), "{w}"); - } - for w in ["městu", "městoě", "rychleejší", "učiteléé", "praha", "mĚsto", "pocitac", "dekujem", "škola-pocitac"] { - assert!(!is_correct(w), "{w}"); - } - for c in "ěščřžýáíéůú".chars() { - assert!(test_dict::DIC.contains(c), "UTF-8 fixture covers {c}"); - } - } - - #[test] - fn forbidden_words_are_rejected_in_every_case() { - test_dict::install(); - for w in ["zqzakázané", "Zqzakázané", "ZQZAKÁZANÉ"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn missing_dictionary_and_ignored_inputs_are_quiet() { - let empty = AffixDict::default(); - assert!(is_correct_with(&empty, "pocitac")); - assert!(install_dictionary("FLAG long\n", "1\nstrom\n").is_err()); - let d = AffixDict::from_hunspell(test_dict::AFF, test_dict::DIC, &[]).unwrap(); - for w in ["", "a", "2026", "ČR", "článek2", "日本語"] { - assert!(is_correct_with(&d, w), "{w}"); - } - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("zqčeskévlastníslovo")); - crate::add_word("Zqčeskévlastníslovo"); - assert!(is_correct("zqčeskévlastníslovo")); - } - - #[test] - fn czech_dispatch_messages_options_and_mixed_spans() { - test_dict::install(); - assert!(crate::has_dictionary(Some("cs-CZ"))); - assert!(crate::is_correct_for("městě", Some("cze"))); - let text = "Město pocitac"; - let issues = crate::check_text(text, &[(0..text.len(), crate::ProofLang::Cs)]); - assert_eq!(issues.len(), 1, "{issues:?}"); - assert_eq!(&text[issues[0].start..issues[0].end], "pocitac"); - assert_eq!(issues[0].message, "Možná pravopisná chyba"); - assert_eq!(issues[0].lang, crate::ProofLang::Cs); - assert!(crate::suggest_in("pocitac", 8, crate::ProofLang::Cs).contains(&"počítač".into())); - let text = "Město this is is wrong"; - let issues = crate::check_text(text, &[(0..6, crate::ProofLang::Cs), (7..text.len(), crate::ProofLang::En)]); - assert!(issues.iter().any(|i| i.lang == crate::ProofLang::En && i.message.starts_with("Repeated word"))); - assert!(issues.iter().all(|i| i.start >= 7)); - let opts = crate::Options { ignore_uppercase: false, ..Default::default() }; - assert!(!crate::is_correct_in_with("ZQZAKÁZANÉ", crate::ProofLang::Cs, &opts)); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Czech checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_czech_check() { - test_dict::install(); - let lang = crate::ProofLang::Cs; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("ŠKULA", lang, &defaults), "short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("ŠKULA", lang, &check_caps)); - assert!(crate::is_correct_in_with("ŠKOLA", lang, &check_caps)); - assert!(crate::is_correct_in_with("škula2", lang, &defaults)); - assert!(!crate::is_correct_in_with("škula2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("ŠKULA"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/cs/suggest.rs b/crates/proof/src/cs/suggest.rs deleted file mode 100644 index 05eba74a8..000000000 --- a/crates/proof/src/cs/suggest.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Czech suggestions: the shared ranking ([`crate::affix_suggest`]) with the installed -//! dictionary's own MAP/REP/TRY/KEY tables and nothing Czech hard-coded. - -use crate::affix::AffixDict; -use crate::affix_suggest::Tuning; - -/// Spelling suggestions for a Czech word, best first, at most `max`, in the word's letter case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &Tuning::DEFAULT) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn suggestions_are_fast_enough() { - crate::cs::test_dict::install(); - let _ = suggest("město", 1); - let t = std::time::Instant::now(); - for word in ["zqčeskévelmidlouhéslovo", "pocitacz", "nejrychllejší"] { - let _ = suggest(word, 6); - } - let ms = t.elapsed().as_secs_f64() * 1000.0; - eprintln!("3 Czech suggestion calls: {ms:.0} ms"); - assert!(ms < 20_000.0, "{ms} ms"); - } - - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(suggest_in); - } - - #[test] - fn czech_suggestions_use_runtime_tables_and_case() { - crate::cs::test_dict::install(); - for (word, want) in [ - ("mesto", "město"), - ("mňe", "mě"), - ("xůž", "nůž"), - ("škloa", "škola"), - ("pocitac", "počítač"), - ("Mesto", "Město"), - ("praha", "Praha"), - ("toškola", "to škola"), - ] { - assert!(suggest(word, 8).contains(&want.into()), "{word}: {:?}", suggest(word, 8)); - } - // Each table changes the answer without a hard-coded Czech alphabet or confusion list. - for (table, wrong, right) in - [("MAP 1\nMAP cč\n", "caj", "čaj"), ("REP 1\nREP xy město\n", "xy", "město"), ("TRY š\n", "kola", "škola"), ("KEY xn\n", "xůž", "nůž")] - { - let dic = format!("1\n{right}\n"); - let d = AffixDict::from_hunspell(table, &dic, &[]).unwrap(); - assert!(suggest_in(&d, wrong, 4).contains(&right.into()), "{table}"); - let without = AffixDict::from_hunspell("", &dic, &[]).unwrap(); - assert!(!suggest_in(&without, wrong, 4).contains(&right.into()), "must depend on {table}"); - } - } - - #[test] - fn suggestions_stay_bounded_and_skip_forbidden_words() { - crate::cs::test_dict::install(); - for w in ["", "dvě slova", &"ě".repeat(41)] { - assert!(suggest(w, 4).is_empty()); - } - assert!(suggest("mesto", 0).is_empty()); - assert!(suggest_in(&AffixDict::default(), "mesto", 4).is_empty()); - assert!(suggest("město", 4).len() <= 4); - assert!(!suggest("město", 4).contains(&"město".into())); - assert!(!suggest("zqzakázane", 8).contains(&"zqzakázané".into())); - } -} diff --git a/crates/proof/src/dict.rs b/crates/proof/src/dict.rs deleted file mode 100644 index 81eb0de1b..000000000 --- a/crates/proof/src/dict.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Hyphenation exception dictionary: lowercase words with their break points. -//! -//! On-disk format (`assets/hyphenation/en-us.dic`): raw-deflate of -//! `"DCHYPH1\n"` followed by one line per word, sorted by the plain word's bytes. Each line is -//! front-coded against the previous one: a byte with the shared prefix length (in bytes of the -//! hyphenated form), then the rest of the hyphenated form (`-` at every break), then `\n`. - -/// Sorted word list with a break bitmask per word (bit `i` = hyphen before char `i`). -#[derive(Clone, Debug, Default)] -pub struct Dictionary { - words: String, - offs: Vec, - masks: Vec, -} - -const MAGIC: &[u8] = b"DCHYPH1\n"; -/// Largest inflated dictionary, and most bytes of words kept from one (the bundled US English one -/// is about 1 MB). -const MAX_BYTES: usize = 64 << 20; -/// Most words read from one dictionary (the bundled one has about 160k). -const MAX_ENTRIES: usize = 4_000_000; - -impl Dictionary { - /// Build from `(lowercase word, break mask)` pairs (sorted and de-duplicated here; the first - /// entry of a duplicate wins). - pub fn from_entries(mut entries: Vec<(String, u64)>) -> Dictionary { - entries.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); - entries.dedup_by(|b, a| a.0 == b.0); - let mut d = Dictionary { words: String::new(), offs: vec![0], masks: Vec::with_capacity(entries.len()) }; - for (w, m) in entries { - d.words.push_str(&w); - d.offs.push(d.words.len() as u32); - d.masks.push(m); - } - d - } - - pub fn len(&self) -> usize { - self.masks.len() - } - - pub fn is_empty(&self) -> bool { - self.masks.is_empty() - } - - fn word(&self, i: usize) -> &str { - &self.words[self.offs[i] as usize..self.offs[i + 1] as usize] - } - - /// Break mask of an exact (already lowercased) word. - pub fn get(&self, word: &str) -> Option { - let (mut lo, mut hi) = (0usize, self.len()); - while lo < hi { - let mid = (lo + hi) / 2; - match self.word(mid).as_bytes().cmp(word.as_bytes()) { - std::cmp::Ordering::Less => lo = mid + 1, - std::cmp::Ordering::Greater => hi = mid, - std::cmp::Ordering::Equal => return Some(self.masks[mid]), - } - } - None - } - - /// Iterate `(word, mask)`. - pub fn iter(&self) -> impl Iterator + '_ { - (0..self.len()).map(|i| (self.word(i), self.masks[i])) - } - - /// Serialize (deflate-compressed). - pub fn to_bytes(&self) -> Vec { - let mut raw = MAGIC.to_vec(); - let mut prev: Vec = Vec::new(); - for (w, m) in self.iter() { - let h = hyphenated(w, m).into_bytes(); - let p = prev.iter().zip(&h).take_while(|(a, b)| a == b).count().min(255); - raw.push(p as u8); - raw.extend_from_slice(&h[p..]); - raw.push(b'\n'); - prev = h; - } - miniz_oxide::deflate::compress_to_vec(&raw, 10) - } - - /// Deserialize data written by [`Dictionary::to_bytes`]. Data that inflates past - /// [`MAX_BYTES`], or holds more than [`MAX_ENTRIES`] words or [`MAX_BYTES`] of them, is refused. - pub fn from_bytes(data: &[u8]) -> Result { - Dictionary::from_bytes_within(data, MAX_BYTES, MAX_ENTRIES) - } - - /// [`Dictionary::from_bytes`] with the limits given. - fn from_bytes_within(data: &[u8], max_bytes: usize, max_entries: usize) -> Result { - let raw = miniz_oxide::inflate::decompress_to_vec_with_limit(data, max_bytes) - .map_err(|e| format!("hyphenation dictionary: inflate failed: {:?}", e.status))?; - let body = raw.strip_prefix(MAGIC).ok_or("hyphenation dictionary: bad header")?; - let mut d = Dictionary { words: String::with_capacity(body.len()), offs: vec![0], masks: Vec::new() }; - let mut cur: Vec = Vec::new(); - let mut i = 0; - while i < body.len() { - let p = body[i] as usize; - let end = body[i + 1..].iter().position(|&b| b == b'\n').map(|e| i + 1 + e).ok_or("hyphenation dictionary: truncated")?; - if p > cur.len() { - return Err("hyphenation dictionary: bad prefix".into()); - } - cur.truncate(p); - cur.extend_from_slice(&body[i + 1..end]); - i = end + 1; - let s = std::str::from_utf8(&cur).map_err(|_| "hyphenation dictionary: bad utf-8")?; - let (w, m) = parse_hyphenated(s); - // Front coding lets a short line repeat a long prefix: bound what is kept, not only - // what is read. - if d.masks.len() >= max_entries || d.words.len() + w.len() > max_bytes { - return Err("hyphenation dictionary: too large".into()); - } - d.words.push_str(&w); - d.offs.push(d.words.len() as u32); - d.masks.push(m); - } - Ok(d) - } -} - -/// `word` with `-` before every char whose bit is set in `mask`. -pub fn hyphenated(word: &str, mask: u64) -> String { - let mut s = String::with_capacity(word.len() + 8); - for (i, c) in word.chars().enumerate() { - if i > 0 && i < 64 && mask & (1 << i) != 0 { - s.push('-'); - } - s.push(c); - } - s -} - -/// Inverse of [`hyphenated`]. -pub fn parse_hyphenated(s: &str) -> (String, u64) { - let mut w = String::with_capacity(s.len()); - let mut m = 0u64; - let mut n = 0; - for c in s.chars() { - if c == '-' { - if n > 0 && n < 64 { - m |= 1 << n; - } - } else { - w.push(c); - n += 1; - } - } - (w, m) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn roundtrip() { - let e: Vec<(String, u64)> = ["ty-pog-ra-phy", "cat", "hap-pen", "o'clock", "naïve-té", "naïve"].iter().map(|s| parse_hyphenated(s)).collect(); - let d = Dictionary::from_entries(e); - let back = Dictionary::from_bytes(&d.to_bytes()).unwrap(); - assert_eq!(back.len(), 6); - assert_eq!(back.get("typography").map(|m| hyphenated("typography", m)), Some("ty-pog-ra-phy".to_string())); - assert_eq!(back.get("cat"), Some(0)); - assert_eq!(back.get("naïveté").map(|m| hyphenated("naïveté", m)), Some("naïve-té".to_string())); - assert!(back.get("dog").is_none()); - } - - #[test] - fn oversized_data_is_refused() { - const LIMIT: usize = 1 << 16; - let within = |raw: &[u8]| Dictionary::from_bytes_within(&miniz_oxide::deflate::compress_to_vec(raw, 6), LIMIT, 1000); - // Data that inflates past the limit is refused before anything is decoded. - let mut raw = MAGIC.to_vec(); - raw.resize(LIMIT + 1, b'a'); - assert!(within(&raw).unwrap_err().contains("inflate failed")); - // Short lines that each repeat a long prefix: small to inflate, but the words they spell - // out would be ~100x larger. Kept storage is bounded too. - let mut raw = MAGIC.to_vec(); - raw.push(0); - raw.extend_from_slice(&[b'a'; 255]); - raw.push(b'\n'); - for _ in 0..(LIMIT / 255 + 2) { - raw.extend_from_slice(&[255, b'\n']); - } - assert!(raw.len() < LIMIT); - assert!(within(&raw).unwrap_err().contains("too large")); - // So is the number of words. - let words: Vec<(String, u64)> = (0..1001).map(|i| (format!("w{i}"), 0)).collect(); - let raw = miniz_oxide::inflate::decompress_to_vec(&Dictionary::from_entries(words).to_bytes()).unwrap(); - assert!(within(&raw).unwrap_err().contains("too large")); - // The real limits leave the bundled dictionary whole. - assert!(Dictionary::en_us().len() > 100_000); - } -} diff --git a/crates/proof/src/en_variants.rs b/crates/proof/src/en_variants.rs deleted file mode 100644 index 56b9f26f5..000000000 --- a/crates/proof/src/en_variants.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! American and British spellings of the same English word (#1281, #1045): which forms each -//! English ([`ProofLang::En`], [`ProofLang::EnCa`], [`ProofLang::EnGb`]) accepts, which it flags, -//! and what it suggests instead (`colour` → `color` in American English). -//! -//! The pairs come from `assets/spelling/en-variants.txt`, our own list of base forms with a -//! policy per English. Inflections are generated here by rule for the regular patterns (-or/-our, -//! -er/-re, -ize/-ise, -ense/-ence…), so `colours`, `centred` and `organisation` follow their -//! base forms. A form is right when the shared English word list knows it or knows its other -//! spelling (`realisation` is right in British English because `realization` is a word), so the -//! rules never make a misspelling right. - -use std::collections::HashMap; -use std::sync::OnceLock; - -use crate::ProofLang; - -/// Our list of American/British pairs: `POLICY AMERICAN BRITISH` per line, `#` comments. -const VARIANTS: &str = include_str!("../../../assets/spelling/en-variants.txt"); - -/// What one English does with a pair's two spellings. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Policy { - /// Only the American spelling is right. - American, - /// Only the British spelling is right. - British, - /// Both are right. - Both, -} - -impl Policy { - fn from_char(c: char) -> Option { - match c { - 'u' => Some(Policy::American), - 'b' => Some(Policy::British), - '*' => Some(Policy::Both), - _ => None, - } - } -} - -/// One line of the list: the policies for American, Canadian and British-spelling English, and -/// the two base forms. -struct Pair { - policy: [Policy; 3], - american: &'static str, - british: &'static str, -} - -/// The lines of [`VARIANTS`]; malformed lines are skipped (a test keeps the file clean). -fn pairs() -> &'static [Pair] { - static P: OnceLock> = OnceLock::new(); - P.get_or_init(|| VARIANTS.lines().filter_map(parse_line).collect()) -} - -fn parse_line(line: &'static str) -> Option { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - return None; - } - let mut parts = line.split_whitespace(); - let (policy, american, british) = (parts.next()?, parts.next()?, parts.next()?); - if parts.next().is_some() || american == british { - return None; - } - let mut p = policy.chars().map(Policy::from_char); - let policy = [p.next()??, p.next()??, p.next()??]; - if p.next().is_some() { - return None; - } - let word = |w: &str| !w.is_empty() && w.chars().all(|c| c.is_ascii_lowercase()); - (word(american) && word(british)).then_some(Pair { policy, american, british }) -} - -/// The column of a pair's policy for `lang`; `None` for a language that isn't English. -fn column(lang: ProofLang) -> Option { - match lang { - ProofLang::En => Some(0), - ProofLang::EnCa => Some(1), - ProofLang::EnGb => Some(2), - _ => None, - } -} - -/// `word` with `suffix`: a final `e` drops before a vowel (`centre` + `ing` → `centring`), a -/// final consonant + `y` becomes `i` (`cosy` + `er` → `cosier`), `s` is the plural (`es` after a -/// sibilant, `ies` after a consonant + `y`). -fn add(word: &str, suffix: &str) -> String { - let b = word.as_bytes(); - let last = b.last().copied().unwrap_or(b'_'); - let before = b.len().checked_sub(2).and_then(|i| b.get(i)).copied().unwrap_or(b'_'); - let vowel = |c: u8| b"aeiou".contains(&c); - let head = word.get(..word.len().saturating_sub(1)).unwrap_or(word); - if suffix == "s" { - if last == b'y' && !vowel(before) { - return format!("{head}ies"); - } - if matches!(last, b's' | b'x' | b'z') || word.ends_with("ch") || word.ends_with("sh") { - return format!("{word}es"); - } - return format!("{word}s"); - } - let suffix_vowel = suffix.as_bytes().first().is_some_and(|&c| vowel(c)); - if last == b'e' && suffix_vowel { - return format!("{head}{suffix}"); - } - if last == b'y' && !vowel(before) && !suffix.starts_with('i') { - return format!("{head}i{suffix}"); - } - format!("{word}{suffix}") -} - -/// The suffixes both spellings of a pair take, by the pattern that tells them apart. -fn suffixes(american: &str, british: &str) -> &'static [&'static str] { - // Not -al, -ist or -ite: `humoral` and `humorist` are spelled so in British English too, and - // `behavioural` and `favourite` have lines of their own. - const OUR: &[&str] = &["s", "ed", "ing", "er", "ers", "ful", "fully", "less", "able", "ably"]; - const RE: &[&str] = &["s", "ed", "ing"]; - const IZE: &[&str] = &["s", "ed", "ing", "er", "ers", "ation", "ations", "ational", "able"]; - const E: &[&str] = &["s", "ed", "ing", "er", "ers"]; - const Y: &[&str] = &["s", "er", "est", "ly", "ness"]; - const PLURAL: &[&str] = &["s"]; - let stem = |w: &str, n: usize| w.get(..w.len().saturating_sub(n)).map(str::to_string); - if american.ends_with("or") && british.ends_with("our") && stem(american, 2) == stem(british, 3) { - return OUR; - } - if american.ends_with("er") && british.ends_with("re") && stem(american, 2) == stem(british, 2) { - return RE; - } - // One letter apart (`organize`/`organise`, `defense`/`defence`, `cozy`/`cosy`). - let differ = american.len() == british.len() && american.bytes().zip(british.bytes()).filter(|(a, b)| a != b).count() == 1; - if differ && american.ends_with('e') && british.ends_with('e') { - let ize = american.ends_with("ize") || american.ends_with("yze"); - return if ize { IZE } else { E }; - } - if differ && american.ends_with('y') && british.ends_with('y') { - return Y; - } - PLURAL -} - -/// One English's forms: those it flags (each with the form to suggest), and those it accepts -/// when the shared list knows them or their other spelling (each with that other spelling). -#[derive(Default)] -struct Forms { - foreign: HashMap, - accepted: HashMap, -} - -fn forms(lang: ProofLang) -> Option<&'static Forms> { - static F: [OnceLock; 3] = [OnceLock::new(), OnceLock::new(), OnceLock::new()]; - let col = column(lang)?; - Some(F.get(col)?.get_or_init(|| build(col))) -} - -fn build(col: usize) -> Forms { - let mut f = Forms::default(); - for pair in pairs() { - let Some(&policy) = pair.policy.get(col) else { continue }; - let mut both = vec![(pair.american.to_string(), pair.british.to_string())]; - both.extend(suffixes(pair.american, pair.british).iter().map(|s| (add(pair.american, s), add(pair.british, s)))); - for (am, br) in both { - if am == br { - continue; - } - match policy { - Policy::American => { - f.foreign.insert(br.clone(), am.clone()); - f.accepted.insert(am, br); - } - Policy::British => { - f.foreign.insert(am.clone(), br.clone()); - f.accepted.insert(br, am); - } - Policy::Both => { - f.accepted.insert(am.clone(), br.clone()); - f.accepted.insert(br, am); - } - } - } - } - // A form one pair accepts is never flagged by another, and `analyses`, `paralyses`… are also - // the plurals of `analysis`, `paralysis`… everywhere. - f.foreign.retain(|w, _| !f.accepted.contains_key(w) && !w.ends_with("yses")); - f -} - -/// `lower` without a possessive `'s`. -fn without_possessive(lower: &str) -> &str { - lower.strip_suffix("'s").filter(|s| !s.is_empty()).unwrap_or(lower) -} - -/// The spelling `lang` wants instead of `lower` (a lower-case word, `'` apostrophes), when -/// `lower` is the other dialect's spelling of a word (`colour` in American English: `color`). -/// A possessive keeps its `'s` (`colour's` → `color's`). -pub fn preferred(lower: &str, lang: ProofLang) -> Option { - let f = forms(lang)?; - let base = without_possessive(lower); - let native = f.foreign.get(base)?; - Some(if base.len() < lower.len() { format!("{native}'s") } else { native.clone() }) -} - -/// Is `lower` right in `lang` because the shared word list knows its other spelling (`known` -/// says what the list knows)? `realisation` in British English, `organization` in all three. -pub fn known_variant(lower: &str, lang: ProofLang, known: impl Fn(&str) -> bool) -> bool { - let Some(f) = forms(lang) else { return false }; - let base = without_possessive(lower); - f.accepted.get(base).is_some_and(|other| known(other)) -} - -/// Is `lower` the other dialect's spelling in `lang` (flagged even though it is a word)? -pub fn is_foreign(lower: &str, lang: ProofLang) -> bool { - forms(lang).is_some_and(|f| f.foreign.contains_key(without_possessive(lower))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn every_line_parses() { - let lines = VARIANTS.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')).count(); - assert_eq!(pairs().len(), lines, "a malformed line in en-variants.txt"); - assert!(pairs().len() > 250); - } - - #[test] - fn suffixes_follow_the_pattern() { - assert_eq!(add("centre", "ing"), "centring"); - assert_eq!(add("centre", "ed"), "centred"); - assert_eq!(add("center", "ed"), "centered"); - assert_eq!(add("organise", "ation"), "organisation"); - assert_eq!(add("cosy", "er"), "cosier"); - assert_eq!(add("cosy", "s"), "cosies"); - assert_eq!(add("armoury", "s"), "armouries"); - assert_eq!(add("analyse", "s"), "analyses"); - assert_eq!(add("colour", "ful"), "colourful"); - assert_eq!(add("", "s"), "s"); - assert_eq!(suffixes("color", "colour").len(), 10); - assert_eq!(suffixes("center", "centre"), ["s", "ed", "ing"]); - assert!(suffixes("organize", "organise").contains(&"ation")); - assert_eq!(suffixes("program", "programme"), ["s"], "no -ed: programmed is American too"); - } - - #[test] - fn no_form_is_both_flagged_and_accepted() { - for lang in [ProofLang::En, ProofLang::EnCa, ProofLang::EnGb] { - let Some(f) = forms(lang) else { panic!("{lang:?}") }; - for (w, native) in &f.foreign { - assert!(!f.accepted.contains_key(w), "{lang:?}: {w}"); - assert!(!f.foreign.contains_key(native), "{lang:?}: suggests {native}, itself flagged"); - } - } - // Words both dialects use alike are never flagged. - for w in [ - "programmed", - "tired", - "tires", - "curbs", - "humorous", - "honorary", - "humorist", - "humoral", - "meter", - "license", - "for", - "tour", - "analyses", - "paralyses", - ] { - for lang in [ProofLang::En, ProofLang::EnCa, ProofLang::EnGb] { - assert!(!is_foreign(w, lang), "{lang:?}: {w}"); - } - } - assert!(forms(ProofLang::Pl).is_none() && !is_foreign("color", ProofLang::Other)); - } -} diff --git a/crates/proof/src/es/grammar.rs b/crates/proof/src/es/grammar.rs deleted file mode 100644 index 614a1f07b..000000000 --- a/crates/proof/src/es/grammar.rs +++ /dev/null @@ -1,391 +0,0 @@ -//! Spanish grammar checks with Spanish messages: the generic repeated-word and spacing checks, -//! and the opening question and exclamation marks Spanish writes before a question or an -//! exclamation (`¿Vienes?`, `¡Qué bien!`): a closing `?` or `!` whose sentence has no opening mark -//! is flagged, with the opening mark offered at the sentence's start, and so is a space after an -//! opening mark (`¿ Vienes?`). Either opening mark may pair with either closing one (`¡Qué -//! dices?`), and `(?)` and `(!)`, which mark doubt or surprise, stand alone. No other -//! Spanish-specific grammar, comma or sentence-capital rules are inferred. - -use unicode_normalization::UnicodeNormalization; - -use crate::{Issue, IssueKind, ProofLang, words}; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec, lang: ProofLang) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang } -} - -/// Grammar issues in a paragraph of Spanish text; `lang` is the variety (they explain the same in -/// every one). -pub fn check(text: &str, lang: ProofLang) -> Vec { - let mut issues = Vec::new(); - repeated_words(text, &words(text), lang, &mut issues); - spaces(text, lang, &mut issues); - opening_marks(text, lang, &mut issues); - issues.sort_by_key(|i| (i.start, i.end)); - issues -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], lang: ProofLang, v: &mut Vec) { - // Letters and the combining marks of a decomposed word (`tambie\u{301}n`); a composed and a - // decomposed spelling of one word are the same word. - let letters = |w: &str| w.chars().all(|c| c.is_alphabetic() || unicode_normalization::char::is_combining_mark(c)); - let folded = |w: &str| w.nfc().collect::().to_lowercase(); - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() && gap.chars().all(char::is_whitespace) && letters(w0) && folded(w0) == folded(w1) { - v.push(issue(*a0, *b1, format!("Palabra repetida: «{w1}»"), vec![w0.to_string()], lang)); - } - } -} - -fn spaces(text: &str, lang: ProofLang, v: &mut Vec) { - for (i, c) in text.char_indices() { - // A space after an opening mark: `¿ Vienes?`. - if matches!(c, '¿' | '¡') - && text.get(i + c.len_utf8()..).is_some_and(|r| r.starts_with(' ') && r.trim_start().chars().next().is_some_and(char::is_alphanumeric)) - { - v.push(issue(i, i + c.len_utf8() + 1, "Espacio después de un signo de apertura".into(), vec![c.to_string()], lang)); - continue; - } - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - // A ratio/score keeps the spaces in digit-space-colon-space-digit. - let ratio = p == ':' - && text.get(..i).and_then(|before| before.chars().next_back()).is_some_and(|c| c.is_ascii_digit()) - && after == Some(' ') - && it.next().is_some_and(|c| c.is_ascii_digit()); - let flag = !ratio - && match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '»' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Espacio antes de un signo de puntuación".into(), vec![p.to_string()], lang)); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Espacio doble".into(), vec![" ".into()], lang)); - } -} - -/// Does the character `b` at byte `j` end what comes before it, so a question or exclamation after -/// it starts there? Sentence ends, clause breaks, line breaks and the start of a quotation or -/// parenthesis do, but not punctuation inside a word (`3.5`, `10:30`) or inside an address -/// (`example.com`, `https:`), except a mark ending an address (`…/? Vienes mañana?`). -fn ends_clause(text: &str, addresses: &[(usize, usize)], j: usize, b: char) -> bool { - let after = j + b.len_utf8(); - if let Some((_, end)) = address_at(addresses, j) { - return matches!(b, '?' | '!' | '.') && end == after; - } - let inside_word = matches!(b, '.' | ':' | ';') && text.get(after..).and_then(|r| r.chars().next()).is_some_and(char::is_alphanumeric); - !inside_word && matches!(b, '.' | '?' | '!' | '…' | ':' | ';' | '\n' | '\r' | '\t' | '«' | '“' | '"' | '(' | '[' | '—') -} - -/// The one of the `addresses` ([`address_ranges`]) that byte `j` is inside, if any. The ranges come -/// in text order and never overlap, so this is a binary search, O(log m) for m addresses: a paragraph -/// of thousands of addresses costs O(n log m) when every character before a question mark asks -/// (#3562 review), not the O(n m) of a scan per character. -fn address_at(addresses: &[(usize, usize)], j: usize) -> Option<(usize, usize)> { - let after_start = addresses.partition_point(|&(s, _)| s <= j); - let (s, e) = *addresses.get(after_start.checked_sub(1)?)?; - (s <= j && j < e).then_some((s, e)) -} - -/// The byte ranges of the whitespace-delimited tokens of `text` that are web or e-mail addresses -/// (`https://example.com/?`, `www.example.com`, `ana@example.com`), as spelling skips them. -fn address_ranges(text: &str) -> Vec<(usize, usize)> { - let mut out = Vec::new(); - let mut start = None; - // A final space ends the last token. - for (i, c) in text.char_indices().chain(std::iter::once((text.len(), ' '))) { - if !c.is_whitespace() { - start = start.or(Some(i)); - } else if let Some(s) = start.take() - && text.get(s..i).is_some_and(crate::is_address) - { - out.push((s, i)); - } - } - out -} - -/// A closing `?` or `!` with no opening `¿` or `¡` before it. Openers are counted left to right, like -/// brackets, and each closing mark takes one back (either opener pairs with either closer: -/// `¡Qué dices?`), so what sits between them doesn't matter: quotes, parentheses, dashes, an -/// ellipsis or an abbreviation's full stop (`¿Viene el Sr. García?`, `¿Leíste «Rayuela»?`) all belong -/// to the question. Only a closing mark with nothing open is flagged, and then the sentence it -/// closes starts after the last clause break ([`ends_clause`]). Marks in web and e-mail addresses -/// aren't sentence punctuation, and neither is punctuation inside an address or a word -/// (`example.com`, `3.5`, `10:30`) a sentence's start. -fn opening_marks(text: &str, lang: ProofLang, v: &mut Vec) { - let addresses = address_ranges(text); - // Openers not closed yet: a run of them (`¡¡`) counts once, like a run of closers (`!!`). - let mut open = 0usize; - for (i, c) in text.char_indices() { - let before = text.get(..i).unwrap_or(""); - let prev = before.chars().next_back(); - let (opener, message) = match c { - '¿' | '¡' => { - if !matches!(prev, Some('¿' | '¡')) { - open += 1; - } - continue; - } - '?' => ('¿', "Falta el signo de interrogación de apertura (¿)"), - '!' => ('¡', "Falta el signo de exclamación de apertura (¡)"), - _ => continue, - }; - let next = text.get(i + 1..).and_then(|r| r.chars().next()); - // `?!` and `!!!` are handled at their first mark; `(?)` stands alone; `?q=`, `a!b` and a - // mark ending an address (`https://example.com/?`) aren't sentence ends. - if matches!(prev, Some('?' | '!')) - || (prev == Some('(') && next == Some(')')) - || next.is_some_and(char::is_alphanumeric) - || address_at(&addresses, i).is_some() - { - continue; - } - if open > 0 { - open -= 1; - continue; - } - let from = before.char_indices().rev().find(|&(j, b)| matches!(b, '¿' | '¡') || ends_clause(text, &addresses, j, b)); - let start = from.map_or(0, |(j, b)| j + b.len_utf8()); - let Some(sentence) = text.get(start..i) else { continue }; - let lead = sentence.len() - sentence.trim_start().len(); - let start = start + lead; - let Some(sentence) = text.get(start..i + 1) else { continue }; - if !sentence.chars().any(char::is_alphabetic) { - continue; - } - v.push(issue(start, i + 1, message.into(), vec![format!("{opener}{sentence}")], lang)); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn messages(text: &str) -> Vec<(String, String)> { - check(text, ProofLang::Es).into_iter().map(|i| (text[i.start..i.end].to_string(), i.message)).collect() - } - - /// The missing-opening-mark issues of `text`: the flagged text and the suggestions offered. - fn missing(text: &str) -> Vec<(String, Vec)> { - check(text, ProofLang::Es) - .into_iter() - .filter(|i| i.message.starts_with("Falta")) - .map(|i| (text[i.start..i.end].to_string(), i.suggestions)) - .collect() - } - - #[test] - fn repeated_words_and_spaces_have_spanish_messages() { - let text = "Es es la casa , dijo."; - let issues = check(text, ProofLang::EsMx); - assert_eq!(issues.len(), 3, "{issues:?}"); - assert_eq!(issues[0].message, "Palabra repetida: «es»"); - assert_eq!(issues[0].suggestions, ["Es"]); - assert!(issues.iter().any(|i| i.message == "Espacio antes de un signo de puntuación" && &text[i.start..i.end] == " ,")); - assert!(issues.iter().any(|i| i.message == "Espacio doble")); - assert!(issues.iter().all(|i| i.lang == ProofLang::EsMx && i.kind == IssueKind::Grammar)); - for text in ["La casa. Es grande", "Sr. García", "No, no lo sé", "Hola :) número .5 y puntos ...", "3 : 1", "teórico-práctico"] { - assert!(check(text, ProofLang::Es).is_empty(), "{text}: {:?}", check(text, ProofLang::Es)); - } - } - - #[test] - fn questions_and_exclamations_need_their_opening_mark() { - assert_eq!(messages("Vienes mañana?"), [("Vienes mañana?".into(), "Falta el signo de interrogación de apertura (¿)".into())]); - assert_eq!(check("Vienes mañana?", ProofLang::Es)[0].suggestions, ["¿Vienes mañana?"]); - assert_eq!(messages("Llegó. Qué bien!"), [("Qué bien!".into(), "Falta el signo de exclamación de apertura (¡)".into())]); - assert_eq!(messages("Le preguntó: vienes?")[0].0, "vienes?"); - assert_eq!(messages("Dijo «vienes?»")[0].0, "vienes?"); - assert_eq!(messages("Hola!!!").len(), 1); - assert_eq!(messages("Qué?!").len(), 1); - for text in [ - "¿Vienes mañana?", - "¡Qué bien!", - "Si vienes, ¿me llamas?", - "¡Qué dices?", - "¿Qué?! ¡No!", - "Nació en 1950 (?) en Montevideo.", - "Lo dijo (!) en público.", - "Ver https://example.com/?q=1 y a!b", - "¿Qué? ¿Cómo?", - "2 + 2 = ?", - "", - "?", - ] { - assert!(messages(text).iter().all(|(_, m)| !m.starts_with("Falta")), "{text}: {:?}", messages(text)); - } - } - - /// Maintainer review on #3562: a URL ending in `/?` isn't a question, and the dots and colons - /// of an address or a word (`example.com`, `https:`, `10:30`) don't start the sentence the - /// missing `¿` is offered for; ordinary questions are still checked. - #[test] - fn addresses_are_not_questions_or_sentence_starts() { - for text in [ - "Ver https://example.com/?", - "Ver https://example.com/? ¿Vienes?", - "Ver http://example.com/buscar/? y www.example.com/?", - "Escribí a ana@example.com!", - "¿Viste https://example.com/?", - ] { - assert!(missing(text).is_empty(), "{text}: {:?}", missing(text)); - } - assert_eq!( - missing("Mirá https://example.com/ y decime si vienes?"), - [("Mirá https://example.com/ y decime si vienes?".to_string(), vec!["¿Mirá https://example.com/ y decime si vienes?".to_string()])] - ); - assert_eq!(missing("Mirá example.com y decime si vienes?")[0].0, "Mirá example.com y decime si vienes?"); - assert_eq!(missing("Son las 10:30, vienes?")[0].0, "Son las 10:30, vienes?"); - assert_eq!(missing("Ver https://example.com/? Vienes mañana?"), [("Vienes mañana?".to_string(), vec!["¿Vienes mañana?".to_string()])]); - assert_eq!(missing("Llegó. Vienes?")[0].0, "Vienes?", "a full stop before a space still ends a sentence"); - } - - /// Maintainer review on #3562: a correctly opened question or exclamation is never flagged, - /// whatever sits between its marks: quotes, an abbreviation's full stop, an ellipsis, a dash - /// aside, parentheses, nested questions, a line break. - #[test] - fn correctly_opened_questions_are_not_flagged() { - for text in [ - "¿Leíste «Rayuela»?", - "¿Viene el Sr. García?", - "¿Y si... no viene?", - "¿Vendrás —preguntó Juan— mañana?", - "¿Qué significa (aquí) la palabra?", - "¿Has leído \"Rayuela\" de Cortázar?", - "¿Leíste “Rayuela”?", - "¿Es 'Rayuela' o 'Los premios'?", - "¿Y tú, qué opinas… vienes?", - "¿Dijo «¿qué?» o no?", - "¿Viene la Dra. López, es decir, la jefa (la de 2.5 años)?", - "¿Dónde: aquí o allí?", - "¿Cuánto cuesta: 3,50 o a las 10:30?", - "¿Vienes a las 10:30; o mejor mañana?", - "¡Qué dices, Dr. Pérez!", - "¡Qué dices, Dr. Pérez? ¡Basta!", - "¡¡Hola!! ¿Qué tal?", - "¡¡¡Basta!!!", - "¿Vienes?!", - "¿Qué?! ¡No!", - "¿Vienes?\n¿O no?", - "¿Vienes\nmañana?", - "Ana dijo: «¿Vienes?» y yo: «Sí».", - "—¿Vienes? —preguntó Juan.", - "¿Viste https://example.com/? Dime.", - "¿Es verdad (!) o no?", - "Nació en 1950 (?) en Montevideo.", - ] { - assert!(missing(text).is_empty(), "{text:?}: {:?}", missing(text)); - } - } - - /// A closing mark with nothing open is still flagged, after a sentence or a clause that closed - /// an earlier question, and with the sentence it closes offered with its opening mark. - #[test] - fn closing_marks_with_nothing_open_are_still_flagged() { - for (text, flagged, offered) in [ - ("Vienes mañana?", "Vienes mañana?", "¿Vienes mañana?"), - ("Llegó. Vienes?", "Vienes?", "¿Vienes?"), - ("Le preguntó: vienes?", "vienes?", "¿vienes?"), - ("Dijo «vienes?»", "vienes?", "¿vienes?"), - ("Qué bien!", "Qué bien!", "¡Qué bien!"), - ("¿Vienes? No sé qué hacer?", "No sé qué hacer?", "¿No sé qué hacer?"), - ("¿Viene el Sr. García? Llegó tarde!", "Llegó tarde!", "¡Llegó tarde!"), - ("¡¡Hola!! Qué bien!", "Qué bien!", "¡Qué bien!"), - ("¿Qué?! No sé qué decir!", "No sé qué decir!", "¡No sé qué decir!"), - ] { - let found = missing(text); - assert_eq!(found.len(), 1, "{text:?}: {found:?}"); - assert_eq!((found[0].0.as_str(), found[0].1.first().map(String::as_str)), (flagged, Some(offered)), "{text:?}"); - } - } - - #[test] - fn address_lookup_finds_the_range_a_byte_is_in() { - let ranges = [(2, 5), (7, 8), (10, 20)]; - let inside = |j| address_at(&ranges, j); - assert_eq!([0, 1].map(inside), [None, None], "before the first"); - assert_eq!([2, 3, 4].map(inside), [Some((2, 5)); 3], "the start is inside, the end is not"); - assert_eq!([5, 6].map(inside), [None, None], "between ranges"); - assert_eq!([7, 10, 19].map(inside), [Some((7, 8)), Some((10, 20)), Some((10, 20))]); - assert_eq!([8, 9, 20, 99].map(inside), [None; 4], "after a range, and after the last"); - assert_eq!(address_at(&[], 3), None); - } - - /// Maintainer review on #3562: a paragraph of thousands of addresses followed by a question was - /// checked in quadratic time (every character before the question scanned every address). - /// About 99 KB (under the 100,000-byte paragraph cap) took hundreds of milliseconds; it is now - /// O(n log m): about 1 to 5 ms in a debug build, against about 300 ms for the scan, so the - /// bound below leaves room on a loaded machine and still catches a return to the quadratic - /// lookup. - #[test] - fn many_addresses_before_a_question_are_checked_quickly() { - for (unit, ending, whole_paragraph) in - [("https://x/ ", "Vienes?", true), ("https://x/? ", "Vienes?", false), ("www.x.com ", "Qué bien!", true)] - { - // As many as fit in about 99 KB: 9,000 of the first, as in the review. - let text = format!("{}{ending}", unit.repeat(99_000 / unit.len())); - assert!(text.len() < 100_000, "{}", text.len()); - let started = std::time::Instant::now(); - let issues = check(&text, ProofLang::Es); - let took = started.elapsed(); - assert!(took < std::time::Duration::from_millis(75), "{unit:?} took {took:?}"); - let missing: Vec<&Issue> = issues.iter().filter(|i| i.message.starts_with("Falta")).collect(); - assert_eq!(missing.len(), 1, "{unit:?}: {missing:?}"); - // The mark's sentence is the whole paragraph when the addresses don't end with a mark - // of their own, and only the last question when they do. - let want_start = if whole_paragraph { 0 } else { text.len() - ending.len() }; - assert_eq!((missing[0].start, missing[0].end), (want_start, text.len()), "{unit:?}"); - } - } - - /// A word repeated once composed and once decomposed is still repeated. - #[test] - fn repeated_words_match_across_normalization_forms() { - let text = "también tambie\u{301}n"; - let issues = check(text, ProofLang::Es); - assert_eq!(issues.len(), 1, "{issues:?}"); - assert_eq!((issues[0].start, issues[0].end), (0, text.len())); - assert!(issues[0].message.starts_with("Palabra repetida")); - } - - #[test] - fn a_space_after_an_opening_mark_is_flagged() { - let text = "¿ Vienes? ¡ Hola!"; - let issues = check(text, ProofLang::EsUy); - let spaced: Vec<&str> = - issues.iter().filter(|i| i.message == "Espacio después de un signo de apertura").map(|i| &text[i.start..i.end]).collect(); - assert_eq!(spaced, ["¿ ", "¡ "], "{issues:?}"); - assert!(issues.iter().all(|i| !i.message.starts_with("Falta")), "{issues:?}"); - assert!(check("¿ ", ProofLang::Es).is_empty()); - } - - proptest::proptest! { - #[test] - fn never_panics_and_ranges_are_valid(s in "\\PC{0,80}|[¿¡?!a-z .,(«»):]{0,40}") { - for issue in check(&s, ProofLang::Es) { - proptest::prop_assert!(issue.start <= issue.end && issue.end <= s.len()); - proptest::prop_assert!(s.is_char_boundary(issue.start) && s.is_char_boundary(issue.end)); - } - } - } -} diff --git a/crates/proof/src/es/mod.rs b/crates/proof/src/es/mod.rs deleted file mode 100644 index 5263bd1d9..000000000 --- a/crates/proof/src/es/mod.rs +++ /dev/null @@ -1,315 +0,0 @@ -//! Spanish proofing (#3360, #3553) using the installed Hunspell dictionaries (never bundled; -//! LibreOffice ships them as its `dict-es` extension, Debian and Ubuntu as `hunspell-es`). Each -//! variety reads its own dictionary: Spain ([`ProofLang::Es`]) `es_ES`, Mexico -//! ([`ProofLang::EsMx`]) `es_MX`, Latin America ([`ProofLang::Es419`]) `es_419` and Uruguay -//! ([`ProofLang::EsUy`]) `es_UY`, each falling back to a nearby or generic one. Uruguayan Spanish -//! also accepts the `vos` forms of every verb in the dictionary ([`voseo`]: `tenés`, `vení`, -//! `decime`). Suggestions use the dictionary's own MAP/REP/TRY/KEY tables (the regional -//! dictionaries' REP tables carry their differences, such as `vosotros` forms in Mexico). -//! Grammar is limited to generic repeated-word and spacing checks and the Spanish opening -//! question and exclamation marks, with Spanish messages; hyphenation keeps the English fallback -//! (no Spanish patterns are bundled). - -pub mod grammar; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; -pub mod voseo; - -use std::sync::Arc; - -use crate::ProofLang; -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// The dictionary of each variety, found on first use (see [`DictSlot`]). -pub(crate) static SPAIN_DICT: DictSlot = DictSlot::new(SPAIN, "Spanish (Spain)"); -pub(crate) static MEXICO_DICT: DictSlot = DictSlot::new(MEXICO, "Spanish (Mexico)"); -pub(crate) static LATIN_AMERICA_DICT: DictSlot = DictSlot::new(LATIN_AMERICA, "Spanish (Latin America)"); -pub(crate) static URUGUAY_DICT: DictSlot = DictSlot::new(URUGUAY, "Spanish (Uruguay)"); - -/// File names of a Spanish Hunspell dictionary for Spain, most specific first (`es_ANY` is the -/// generic one RLA-ES publishes). -const SPAIN: &[&str] = &["es_ES", "es-ES", "es_ANY", "es"]; -/// For Mexico: its own, then a Latin American or generic one. -const MEXICO: &[&str] = &["es_MX", "es-MX", "es_419", "es-419", "es_ANY", "es"]; -/// For Latin America: its own or a generic one, then the United States' and Mexico's, which -/// LibreOffice ships (it has no `es_419`) and which use Latin American spelling, then -/// Argentina's, so `es-AR` text is checked when that is the only one installed. -const LATIN_AMERICA: &[&str] = &["es_419", "es-419", "es_ANY", "es", "es_US", "es_MX", "es_AR", "es-AR"]; -/// For Uruguay: its own, then Argentina's (the same Rioplatense Spanish), then a Latin American -/// or generic one. -const URUGUAY: &[&str] = &["es_UY", "es-UY", "es_AR", "es-AR", "es_419", "es-419", "es_ANY", "es"]; - -/// `lang`'s variety's dictionary; Spain's for any other language. -fn slot(lang: ProofLang) -> &'static DictSlot { - match lang { - ProofLang::EsMx => &MEXICO_DICT, - ProofLang::Es419 => &LATIN_AMERICA_DICT, - ProofLang::EsUy => &URUGUAY_DICT, - _ => &SPAIN_DICT, - } -} - -/// The Hunspell name each variety's dictionary is known by (`es_ES`, `es_MX`, `es_419`, -/// `es_UY`): the first of its file names, used for the status-bar hint and load errors. -pub fn dictionary_name(lang: ProofLang) -> &'static str { - slot(lang).name() -} - -/// The Spanish dictionary of `lang`'s variety: the installed one (read on first use), one given -/// with [`install_dictionary`], or an empty one when none is available (spelling is off then; -/// see [`has_dictionary`]). -pub fn dictionary(lang: ProofLang) -> Arc { - slot(lang).get() -} - -/// Use this dictionary for `lang`'s variety from now on (Hunspell `.aff` and `.dic` text, -/// decoded to UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(lang: ProofLang, aff: &str, dic: &str) -> Result<(), String> { - slot(lang).install(aff, dic) -} - -/// Is a dictionary available for `lang`'s variety (spelling is checked)? -pub fn has_dictionary(lang: ProofLang) -> bool { - slot(lang).has_dictionary() -} - -/// Is `word` spelled correctly in `lang`'s Spanish? Numbers, single letters, short words in -/// capitals (acronyms: `ONU`, `CDMX`), words with digits and words in other scripts are accepted, -/// and so is every word when no dictionary is installed for the variety. Hyphenated compounds -/// are checked part by part (`teórico-práctico`, `franco-mexicano`). In Uruguayan Spanish the -/// `vos` forms of the dictionary's verbs are correct too ([`voseo`]). -pub fn is_correct(word: &str, lang: ProofLang) -> bool { - is_correct_with(&dictionary(lang), word, lang) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, lang: ProofLang, opts: &crate::Options) -> bool { - check(&dictionary(lang), word, lang, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str, lang: ProofLang) -> bool { - check(d, word, lang, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, lang: ProofLang, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - if w.contains('-') { - return known(d, w, lang) || w.split('-').all(|p| p.chars().count() <= 1 || known(d, p, lang)); - } - known(d, w, lang) -} - -/// Is one word in the dictionary or the user dictionary, or (in Uruguayan Spanish) the `vos` -/// form of one of the dictionary's verbs? -fn known(d: &AffixDict, w: &str, lang: ProofLang) -> bool { - d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) || (lang == ProofLang::EsUy && voseo::is_vos_form(w, |inf| d.check(inf))) -} - -#[cfg(test)] -mod tests { - use super::*; - - const VARIETIES: [ProofLang; 4] = [ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy]; - - #[test] - fn each_variety_looks_for_its_own_dictionary_first() { - let dir = std::env::temp_dir().join(format!("wordcraft-es-aliases-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let (es, mx, la, uy) = (ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy); - // Alone in the folder, each file name is found by the varieties that list it. - for (name, found_by) in [ - ("es_ES", &[es][..]), - ("es-ES", &[es]), - ("es_MX", &[mx, la]), - ("es-MX", &[mx]), - ("es_419", &[mx, la, uy]), - ("es-419", &[mx, la, uy]), - ("es_US", &[la]), - ("es_UY", &[uy]), - ("es-UY", &[uy]), - ("es_AR", &[la, uy]), - ("es-AR", &[la, uy]), - ("es_ANY", &VARIETIES), - ("es", &VARIETIES), - ] { - std::fs::write(dir.join(format!("{name}.aff")), test_dict::AFF).unwrap(); - std::fs::write(dir.join(format!("{name}.dic")), test_dict::DIC).unwrap(); - for lang in VARIETIES { - let found = crate::hunspell::find_in(&dir, slot(lang).names()); - assert_eq!(found.is_some(), found_by.contains(&lang), "{name}: {lang:?}"); - if let Some((aff, dic)) = found { - assert!(is_correct_with(&crate::hunspell::load(&aff, &dic).unwrap(), "canción", lang)); - } - } - std::fs::remove_file(dir.join(format!("{name}.aff"))).unwrap(); - std::fs::remove_file(dir.join(format!("{name}.dic"))).unwrap(); - } - // With them all, each variety reads its own. - for name in ["es_ES", "es_MX", "es_419", "es_UY", "es_AR", "es_ANY"] { - std::fs::write(dir.join(format!("{name}.aff")), test_dict::AFF).unwrap(); - std::fs::write(dir.join(format!("{name}.dic")), test_dict::DIC).unwrap(); - } - for (lang, name) in [(es, "es_ES"), (mx, "es_MX"), (la, "es_419"), (uy, "es_UY")] { - assert_eq!(crate::hunspell::find_in(&dir, slot(lang).names()).map(|(_, dic)| dic), Some(dir.join(format!("{name}.dic")))); - assert_eq!(dictionary_name(lang), name); - } - std::fs::remove_dir_all(dir).unwrap(); - } - - #[test] - fn spanish_words_affixes_case_and_hyphens() { - test_dict::install(); - let d = dictionary(ProofLang::Es); - assert!(has_dictionary(ProofLang::Es)); - assert!(d.rule_count() > 0); - for w in [ - "canción", - "canciones", - "prueba", - "pruebas", - "corrector", - "correctores", - "también", - "México", - "Canción", - "CANCIÓN", - "teórico-práctico", - "franco-mexicano", - "ONU", - "Año", - "niño", - "pingüino", - ] { - assert!(is_correct(w, ProofLang::Es), "{w}"); - } - for w in ["prueva", "corector", "tambien", "mexico", "cancion", "teórico-prático", "cAnción", "nino"] { - assert!(!is_correct(w, ProofLang::Es), "{w}"); - } - } - - #[test] - fn varieties_keep_their_own_dictionaries() { - test_dict::install(); - // The Americas' fixture lacks `vosotros` forms and has American words; Spain's is the other way round. - assert!(is_correct("habéis", ProofLang::Es) && !is_correct("habéis", ProofLang::EsMx)); - assert!(is_correct("platicar", ProofLang::EsMx) && !is_correct("platicar", ProofLang::Es)); - assert!(is_correct("platicar", ProofLang::Es419) && is_correct("computadora", ProofLang::Es419)); - assert!(!is_correct("habéis", ProofLang::Es419) && !is_correct("habéis", ProofLang::EsUy)); - assert!(is_correct("ordenador", ProofLang::Es) && !is_correct("ordenador", ProofLang::EsMx)); - } - - /// Uruguayan Spanish accepts the `vos` forms of the dictionary's verbs, which the other - /// varieties flag. - #[test] - fn uruguayan_spanish_accepts_vos_forms() { - test_dict::install(); - for w in ["tenés", "vení", "Vení", "decime", "decímelo", "andate", "mirá", "hablás", "querés", "venís", "fijate"] { - assert!(is_correct(w, ProofLang::EsUy), "{w}"); - assert!(!is_correct(w, ProofLang::Es419) && !is_correct(w, ProofLang::Es), "{w}"); - } - for w in ["tenéz", "venì", "decimelo", "decíme", "zqbailá"] { - assert!(!is_correct(w, ProofLang::EsUy), "{w}"); - } - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Spanish checks - /// those words against the dictionary instead of accepting them (CodeRabbit review on #3562). - #[test] - fn proofing_options_reach_the_spanish_check() { - test_dict::install(); - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - for lang in VARIETIES { - assert!(crate::is_correct_in_with("PRUEVA", lang, &defaults), "{lang:?}: short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("PRUEVA", lang, &check_caps), "{lang:?}"); - assert!(crate::is_correct_in_with("PRUEBA", lang, &check_caps), "{lang:?}"); - assert!(crate::is_correct_in_with("prueva2", lang, &defaults), "{lang:?}"); - assert!(!crate::is_correct_in_with("prueva2", lang, &check_digits), "{lang:?}"); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "{lang:?}: numbers alone stay correct"); - } - assert!(is_correct("PRUEVA", ProofLang::Es), "direct checks keep the defaults"); - } - - /// Maintainer review on #3562: a word typed decomposed (NFD: `e` + U+0301) is checked like its - /// composed form (NFC) and keeps its own byte range, accents and all. - #[test] - fn decomposed_accents_are_checked_like_composed_ones() { - test_dict::install(); - for lang in VARIETIES { - for text in ["esto también", "esto tambie\u{301}n", "una cancio\u{301}n y pingu\u{308}ino", "CANCIO\u{301}N", "nin\u{303}o"] { - let issues = crate::check_text(text, &[(0..text.len(), lang)]); - assert!(issues.is_empty(), "{lang:?} {text:?}: {issues:?}"); - } - // A misspelling that ends in a decomposed accent is flagged whole, at its own bytes. - let text = "esto es una prueva tambie\u{301}nn"; - let issues = crate::check_text(text, &[(0..text.len(), lang)]); - let flagged: Vec<(&str, usize, usize)> = issues.iter().map(|i| (&text[i.start..i.end], i.start, i.end)).collect(); - assert_eq!(flagged, [("prueva", 12, 18), ("tambie\u{301}nn", 19, text.len())], "{lang:?}"); - assert!(crate::suggest_in("tambie\u{301}nn", 8, lang).contains(&"también".into()), "{lang:?}"); - } - } - - #[test] - fn forbidden_words_are_rejected_in_every_case() { - test_dict::install(); - for w in ["zqprohibida", "Zqprohibida", "ZQPROHIBIDA"] { - assert!(!is_correct(w, ProofLang::Es), "{w}"); - } - } - - #[test] - fn missing_dictionary_and_ignored_inputs_are_quiet() { - let empty = AffixDict::default(); - assert!(is_correct_with(&empty, "prueva", ProofLang::Es)); - assert!(install_dictionary(ProofLang::Es, "FLAG long\n", "1\ncasa\n").is_err()); - let d = AffixDict::from_hunspell(test_dict::AFF, test_dict::DIC, &[]).unwrap(); - for w in ["", "a", "2026", "EE", "1.º", "日本語", "-", "--"] { - assert!(is_correct_with(&d, w, ProofLang::EsUy), "{w}"); - } - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("zqpalabrapropia", ProofLang::EsMx)); - crate::add_word("Zqpalabrapropia"); - assert!(is_correct("zqpalabrapropia", ProofLang::EsMx)); - } - - #[test] - fn spanish_dispatch_messages_options_and_mixed_spans() { - test_dict::install(); - for tag in ["es-ES", "es-MX", "es-419", "es-AR", "es-UY", "spa"] { - assert!(crate::has_dictionary(Some(tag)), "{tag}"); - } - assert!(crate::is_correct_for("canción", Some("es-MX"))); - assert!(crate::is_correct_for("tenés", Some("es-UY")) && !crate::is_correct_for("tenés", Some("es-MX"))); - // The sentence from #3553. - let text = "esto es una prueva del corector"; - for lang in VARIETIES { - let issues = crate::check_text(text, &[(0..text.len(), lang)]); - let flagged: Vec<&str> = issues.iter().map(|i| &text[i.start..i.end]).collect(); - assert_eq!(flagged, ["prueva", "corector"], "{lang:?}: {issues:?}"); - assert!(issues.iter().all(|i| i.message == "Posible error ortográfico" && i.lang == lang), "{issues:?}"); - assert!(crate::suggest_in("prueva", 8, lang).contains(&"prueba".into()), "{lang:?}"); - assert!(crate::suggest_in("corector", 8, lang).contains(&"corrector".into()), "{lang:?}"); - } - let text = "Canción this is is wrong"; - let issues = crate::check_text(text, &[(0..8, ProofLang::Es419), (9..text.len(), ProofLang::En)]); - assert!(issues.iter().any(|i| i.lang == ProofLang::En && i.message.starts_with("Repeated word"))); - assert!(issues.iter().all(|i| i.start >= 9)); - let opts = crate::Options { ignore_uppercase: false, ..Default::default() }; - assert!(!crate::is_correct_in_with("ZQPROHIBIDA", ProofLang::Es, &opts)); - } -} diff --git a/crates/proof/src/es/suggest.rs b/crates/proof/src/es/suggest.rs deleted file mode 100644 index 0568ae870..000000000 --- a/crates/proof/src/es/suggest.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Spanish suggestions. The ranking is the shared dictionary-driven one -//! ([`crate::affix_suggest`]: case, MAP — the Spanish dictionaries map each vowel to -//! its accented forms and `n` to `ñ`, so `tambien` → `también` comes first —, REP, missing spaces, -//! KEY, TRY edits and a stem scan); every table comes from the variety's installed dictionary. -//! In Uruguayan Spanish a `vos` form typed without its accent is offered with it first -//! (`veni` → `vení`, `tenes` → `tenés`; [`super::voseo`]). - -use crate::ProofLang; -use crate::affix::{AffixDict, Casing, capitalize, casing}; -use crate::affix_suggest::{MAX_LETTERS, Tuning}; - -/// Spelling suggestions for a word in `lang`'s Spanish, best first, at most `max`, in the word's -/// case. -pub fn suggest(word: &str, max: usize, lang: ProofLang) -> Vec { - suggest_in(&super::dictionary(lang), word, max, lang) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize, lang: ProofLang) -> Vec { - let mut out = Vec::new(); - let word = word.trim(); - if lang == ProofLang::EsUy - && !d.is_empty() - && word.chars().count() <= MAX_LETTERS - && let Some(v) = super::voseo::accented(&word.to_lowercase(), |inf| d.check(inf)) - { - out.push(match casing(word) { - Casing::Upper => v.to_uppercase(), - Casing::Initial => capitalize(&v), - _ => v, - }); - } - for s in crate::affix_suggest::suggest_in(d, word, max, &Tuning::DEFAULT) { - if !out.contains(&s) { - out.push(s); - } - } - out.truncate(max); - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn spanish_suggestions_use_runtime_tables_and_case() { - crate::es::test_dict::install(); - for lang in [ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy] { - for (word, want) in [ - ("tambien", "también"), - ("cancion", "canción"), - ("Cancion", "Canción"), - ("mexico", "México"), - ("nino", "niño"), - ("pinguino", "pingüino"), - ("prueva", "prueba"), - ("corector", "corrector"), - ("estoes", "esto es"), - ] { - assert!(suggest(word, 8, lang).contains(&want.into()), "{lang:?} {word}: {:?}", suggest(word, 8, lang)); - } - } - // The first suggestion for a missing accent is the accented word. - assert_eq!(suggest("tambien", 4, ProofLang::Es).first().map(String::as_str), Some("también")); - } - - #[test] - fn uruguayan_vos_forms_get_their_accent_back_first() { - crate::es::test_dict::install(); - for (word, want) in [("veni", "vení"), ("tenes", "tenés"), ("Veni", "Vení"), ("TENES", "TENÉS"), ("queres", "querés")] { - assert_eq!(suggest(word, 4, ProofLang::EsUy).first().map(String::as_str), Some(want), "{word}"); - assert!(!suggest(word, 8, ProofLang::Es419).contains(&want.into()), "{word}: no vos forms outside Uruguay"); - } - } - - #[test] - fn suggestions_stay_bounded_and_skip_forbidden_words() { - crate::es::test_dict::install(); - for lang in [ProofLang::Es, ProofLang::EsUy] { - for w in ["", "dos palabras", &"a".repeat(41)] { - assert!(suggest(w, 4, lang).is_empty(), "{w}"); - } - assert!(suggest("prueva", 0, lang).is_empty()); - assert!(suggest("veni", 0, lang).is_empty()); - assert!(suggest_in(&AffixDict::default(), "veni", 4, lang).is_empty()); - assert!(suggest("prueba", 4, lang).len() <= 4); - assert!(!suggest("prueba", 4, lang).contains(&"prueba".into())); - assert!(!suggest("zqprohibda", 8, lang).contains(&"zqprohibida".into())); - } - } - - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(|d, w, max| suggest_in(d, w, max, ProofLang::Es)); - } -} diff --git a/crates/proof/src/es/test_dict.rs b/crates/proof/src/es/test_dict.rs deleted file mode 100644 index 6b3e6b1f5..000000000 --- a/crates/proof/src/es/test_dict.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Hand-written synthetic Spanish test data. No words, rules or suggestion tables were copied -//! from a system dictionary. This is a small fixture, not a redistributable Spanish dictionary. -//! Spain's word list has `vosotros` forms and Spain's words; the Americas' list (Mexico, Latin -//! America and Uruguay) has theirs instead, so the tests can tell the varieties' dictionaries apart. - -use crate::ProofLang; - -pub const AFF: &str = "SET UTF-8 -FLAG UTF-8 -FORBIDDENWORD F -TRY aeosrnidlctumpbgvyqhfzjñáéíóúüxkw -KEY qwertyuiop|asdfghjklñ|zxcvbnm -MAP 6 -MAP aá -MAP eé -MAP ií -MAP oó -MAP uúü -MAP nñ -REP 2 -REP v b -REP b v -SFX S Y 2 # synthetic plurals -SFX S 0 s [aeiou] -SFX S ón ones ón -SFX E Y 1 -SFX E 0 es [^aeiou] -"; - -/// Spain. -pub const DIC: &str = "32 -hablar -tener -venir -decir -andar -mirar -querer -fijar -poner -canción/S -prueba/S -corrector/E -también -México -esto -es -una -del -de -la -el -y -niño/S -pingüino/S -año/S -teórico -práctico -mexicano -franco -habéis -ordenador/E -zqprohibida/F -"; - -/// Mexico, Latin America and Uruguay. -pub const DIC_AMERICAS: &str = "33 -hablar -tener -venir -decir -andar -mirar -querer -fijar -poner -canción/S -prueba/S -corrector/E -también -México -esto -es -una -del -de -la -el -y -niño/S -pingüino/S -año/S -teórico -práctico -mexicano -franco -platicar -computadora/S -han -zqprohibida/F -"; - -/// Install the fixture for every variety: [`DIC`] for Spain, [`DIC_AMERICAS`] for Mexico, Latin -/// America and Uruguay. -pub fn install() { - for (lang, dic) in [(ProofLang::Es, DIC), (ProofLang::EsMx, DIC_AMERICAS), (ProofLang::Es419, DIC_AMERICAS), (ProofLang::EsUy, DIC_AMERICAS)] { - if let Err(e) = super::install_dictionary(lang, AFF, dic) { - panic!("test dictionary: {e}"); - } - } -} diff --git a/crates/proof/src/es/voseo.rs b/crates/proof/src/es/voseo.rs deleted file mode 100644 index d9753e129..000000000 --- a/crates/proof/src/es/voseo.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! The `vos` forms of verbs in Uruguayan Spanish ([`crate::ProofLang::EsUy`]), which dictionaries -//! built around `tú` may lack (#3360). Rioplatense `voseo` builds them regularly from the -//! infinitive, stress on the ending, without the stem changes `tú` has: -//! -//! - the imperative drops the infinitive's `r` and accents the vowel: `hablar` → `hablá`, -//! `tener` → `tené`, `venir` → `vení`, `decir` → `decí` (`ir` uses `andá`, from `andar`); -//! - with one pronoun after it the accent goes (`decime`, `fijate`, `andate`, `poneme`), with two -//! it comes back (`decímelo`, `mandámela`); `-ír` verbs keep theirs (`sonreíte`); -//! - the present indicative ends in `-ás`, `-és`, `-ís`: `hablás`, `tenés`, `venís`. -//! -//! A form is accepted only when the dictionary has its verb's infinitive, so nothing but these -//! endings is hard-coded. Other `vos` forms (`sos`) are ordinary dictionary words. - -/// Pronouns that come first when two follow an imperative (`decímelo`, `dáselo`). -const FIRST_PRONOUNS: [&str; 4] = ["me", "te", "se", "nos"]; -/// Pronouns that come second (or alone). -const SECOND_PRONOUNS: [&str; 6] = ["lo", "la", "le", "los", "las", "les"]; - -/// Shortest infinitive considered (`dar` and `ir` have no `vos` imperative of their own here, and -/// short stems would let nouns such as `bar` pass for verbs). -const MIN_INFINITIVE: usize = 4; - -/// Is `word` (one word, any case) a `vos` form of a verb whose infinitive `is_word` accepts? -pub fn is_vos_form(word: &str, is_word: impl Fn(&str) -> bool) -> bool { - let w = word.to_lowercase(); - if w.chars().count() > crate::affix::MAX_WORD_BYTES { - return false; - } - infinitives(&w).iter().any(|inf| inf.chars().count() >= MIN_INFINITIVE && is_word(inf)) -} - -/// The infinitives `w` (lower case) could be a `vos` form of, by its ending. -fn infinitives(w: &str) -> Vec { - let mut out = Vec::new(); - // Present indicative: `hablás`, `tenés`, `venís`. - if let Some(stem) = w.strip_suffix('s') { - imperative(stem, &mut out); - } - // Imperative: `hablá`, `tené`, `vení`. - imperative(w, &mut out); - // With one pronoun the accent goes: `decime`, `fijate`; `-ír` verbs keep it: `sonreíte`. - for p in FIRST_PRONOUNS.iter().chain(SECOND_PRONOUNS.iter()) { - if let Some(base) = w.strip_suffix(p) - && base.ends_with(['a', 'e', 'i', 'í']) - { - out.push(format!("{base}r")); - } - } - // With two the accent comes back: `decímelo`, `mandámela`. - for second in SECOND_PRONOUNS { - let Some(rest) = w.strip_suffix(second) else { continue }; - for first in FIRST_PRONOUNS { - if let Some(base) = rest.strip_suffix(first) { - imperative(base, &mut out); - } - } - } - out -} - -/// The infinitives of an imperative ending in an accented vowel: `hablá` → `hablar`, and for `í` -/// both `vení` → `venir` and `sonreí` → `sonreír`. -fn imperative(form: &str, out: &mut Vec) { - let mut chars = form.chars(); - let Some(last) = chars.next_back() else { return }; - let stem = chars.as_str(); - match last { - 'á' => out.push(format!("{stem}ar")), - 'é' => out.push(format!("{stem}er")), - 'í' => { - out.push(format!("{stem}ir")); - out.push(format!("{stem}ír")); - } - _ => {} - } -} - -/// The `vos` form `word` (lower case, without its accent) was most likely meant as, when the -/// writer left the accent out: `veni` → `vení`, `tenes` → `tenés`. `None` when it isn't one. -pub fn accented(word: &str, is_word: impl Fn(&str) -> bool) -> Option { - let (body, s) = match word.strip_suffix('s') { - Some(b) => (b, "s"), - None => (word, ""), - }; - let mut chars = body.chars(); - let accent = match chars.next_back()? { - 'a' => 'á', - 'e' => 'é', - 'i' => 'í', - _ => return None, - }; - let form = format!("{}{accent}{s}", chars.as_str()); - is_vos_form(&form, is_word).then_some(form) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn verbs(w: &str) -> bool { - ["hablar", "tener", "venir", "decir", "andar", "mirar", "sonreír", "fijar", "mandar", "poner", "bar", "querer"].contains(&w) - } - - #[test] - fn imperatives_present_and_pronouns_come_from_the_infinitive() { - for w in [ - "hablá", - "tené", - "vení", - "decí", - "andá", - "mirá", - "poné", - "hablás", - "tenés", - "venís", - "querés", - "decime", - "fijate", - "andate", - "poneme", - "sonreíte", - "sonreí", - "decímelo", - "mandámela", - "decinos", - "Vení", - "TENÉS", - ] { - assert!(is_vos_form(w, verbs), "{w}"); - } - // Wrong accents, verbs the dictionary lacks, and short stems (`bame` isn't `bar`). - for w in ["habla", "decíme", "decínos", "decimelo", "tenéz", "cantá", "bá", "bame", "", "s", "á", "més", "lo", "melo"] { - assert!(!is_vos_form(w, verbs), "{w}"); - } - } - - #[test] - fn missing_accents_are_restored() { - assert_eq!(accented("veni", verbs).as_deref(), Some("vení")); - assert_eq!(accented("tenes", verbs).as_deref(), Some("tenés")); - assert_eq!(accented("habla", verbs).as_deref(), Some("hablá")); - for w in ["canta", "casa", "", "s", "pan", "tenés"] { - assert_eq!(accented(w, verbs), None, "{w}"); - } - } - - proptest::proptest! { - #[test] - fn never_panics(s in "\\PC{0,40}") { - let _ = is_vos_form(&s, |_| true); - let _ = accented(&s, |_| true); - } - } -} diff --git a/crates/proof/src/hr/grammar.rs b/crates/proof/src/hr/grammar.rs deleted file mode 100644 index 517c56235..000000000 --- a/crates/proof/src/hr/grammar.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! Conservative Croatian grammar and punctuation checks. -//! Handles repeated words, whitespace, capitals and duplicated punctuation; -//! intentionally avoids speculative agreement and comma rules. - -use crate::{Issue, IssueKind, ProofLang, words}; - -/// Words Croatian repeats on purpose: interjections, onomatopoeia and emphatic doubling. -const ALLOWED_REPEATS: &[&str] = - &["da", "ne", "no", "ha", "hej", "joj", "aj", "oh", "uh", "hop", "kuc", "tik", "tak", "bum", "malo", "brzo", "polako", "više", "dobro"]; -const ABBREVIATIONS: &[&str] = &[ - "npr", "itd", "itp", "tj", "tzv", "odn", "sl", "str", "br", "god", "st", "dr", "mr", "prof", "doc", "ing", "dipl", "mag", "univ", "g", "god", - "mj", "min", "sek", "čl", "st", "č", "toč", "tab", "sl", "sv", "ul", "trg", "pr", "izd", "ur", "svez", "vol", "vid", "usp", "prim", "engl", - "njem", "hrv", "lat", "tel", "mob", "e", "p", "o", "d", "m", "v", "z", "r", "pon", "uto", "sri", "čet", "pet", "sub", "ned", "sij", "velj", - "ožu", "tra", "svi", "lip", "srp", "kol", "ruj", "lis", "stu", "pro", -]; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::Hr } -} - -fn capitalize(w: &str) -> String { - crate::affix::capitalize(w) -} - -/// Is `prev`, before a period, an abbreviation, an initial, a number or a Roman numeral (after -/// which a lower-case word doesn't start a sentence)? -fn abbreviation_or_number(prev: &str) -> bool { - prev.chars().count() <= 1 - || prev.chars().any(|c| c.is_ascii_digit()) - || (prev.chars().all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))) - || ABBREVIATIONS.contains(&prev.to_lowercase().as_str()) -} - -const CLOSING: [char; 8] = ['"', '”', '’', '\'', ')', '»', ']', '\u{1}']; -const OPENING: [char; 8] = ['"', '“', '«', '(', '[', '\'', '‚', '—']; - -/// Does the gap between two words end a sentence: a `.`, `!` or `?` (not an ellipsis, not after -/// an abbreviation), then space, with nothing but quotes and brackets around? -fn ends_sentence(gap: &str, prev: &str) -> bool { - let t = gap.trim_start_matches(CLOSING); - let Some(end) = t.chars().next() else { return false }; - if !matches!(end, '.' | '!' | '?') || t.starts_with("..") { - return false; - } - if end == '.' && abbreviation_or_number(prev) { - return false; - } - let rest = t.trim_start_matches(['.', '!', '?']).trim_start_matches(CLOSING); - rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| OPENING.contains(&c)) -} - -/// Grammar issues in a paragraph of Croatian text, in order. -pub fn check(text: &str) -> Vec { - check_with(text, true) -} - -/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital -/// (false for a list item, which may be a fragment). -pub fn check_with(text: &str, capital_first: bool) -> Vec { - let ws: Vec<(usize, usize)> = words(text); - let mut v = Vec::new(); - repeated_words(text, &ws, &mut v); - spaces(text, &mut v); - sentence_capitals(text, &ws, capital_first, &mut v); - doubled_punctuation(text, &mut v); - v.sort_by_key(|i| (i.start, i.end)); - v -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() - && gap.chars().all(char::is_whitespace) - && w0.chars().all(char::is_alphabetic) - && w0.to_lowercase() == w1.to_lowercase() - && !ALLOWED_REPEATS.contains(&w0.to_lowercase().as_str()) - { - v.push(issue(*a0, *b1, format!("Ponovljena riječ: \"{w1}\""), vec![w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - let flag = match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Razmak ispred interpunkcijskog znaka".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Dvostruki razmak".into(), vec![" ".into()])); - } -} - -fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec) { - // List items (`– maçãs,`) may start in lower case. - let list_item = text.trim_end().ends_with([',', ';']); - for (k, &(a, b)) in ws.iter().enumerate() { - let Some(w) = text.get(a..b) else { continue }; - let starts = if k == 0 { - let before = text.get(..a).unwrap_or(""); - capital_first && !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c)) - } else { - let Some(&(pa, pb)) = ws.get(k - 1) else { continue }; - ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or("")) - }; - if starts && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) { - v.push(issue(a, b, "Rečenica treba započeti velikim slovom".into(), vec![capitalize(w)])); - } - } -} - -fn doubled_punctuation(text: &str, v: &mut Vec) { - let bytes = text.as_bytes(); - let mut i = 0; - while let Some(&b) = bytes.get(i) { - if !matches!(b, b',' | b'.' | b';') { - i += 1; - continue; - } - let run = bytes.get(i..).map_or(0, |r| r.iter().take_while(|&&x| x == b).count()); - let flag = match b { - b'.' => run == 2, - _ => run >= 2, - }; - if flag { - let end = i + run; - let suggestions: Vec = match b { - b'.' => vec![".".into(), "…".into()], - _ => vec![char::from(b).to_string()], - }; - v.push(issue(i, end, "Ponovljen interpunkcijski znak".into(), suggestions)); - } - i += run.max(1); - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn checks_croatian_punctuation() { - let t = "Ovo ovo je primjer , s dva razmaka.."; - let issues = check(t); - assert!(issues.iter().any(|i| i.message.starts_with("Ponovljena riječ"))); - assert!(issues.iter().any(|i| i.message == "Dvostruki razmak")); - assert!(issues.iter().all(|i| i.lang == ProofLang::Hr)); - } - #[test] - fn abbreviations_do_not_trigger_capitals() { - let issues = check("To je npr. sasvim dobar primjer."); - assert!(!issues.iter().any(|i| i.message == "Rečenica treba započeti velikim slovom")); - } -} diff --git a/crates/proof/src/hr/mod.rs b/crates/proof/src/hr/mod.rs deleted file mode 100644 index 53edf0674..000000000 --- a/crates/proof/src/hr/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Croatian spelling and conservative grammar support. -//! Uses an installed hr_HR Hunspell .aff/.dic pair (not bundled). -//! The engine decodes ISO-8859-2 dictionaries at load time. -//! Suggestions prioritize č/ć, đ, š, ž and common one-character mistakes. -//! Hyphenation remains the host engine's fallback until Croatian patterns are supplied. - -pub mod grammar; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of a Croatian Hunspell dictionary. -const NAMES: &[&str] = &["hr_HR", "hr-HR", "hr"]; - -/// The Croatian dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Croatian"); - -/// The Croatian dictionary: the installed Hunspell `hr_HR` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Croatian spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Croatian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is a Croatian dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Is `word` spelled correctly in Croatian? Numbers, single letters, short words in capitals -/// (acronyms: `HRT`, `RH`), words with digits and words in other scripts are accepted, and so -/// is every word when no Croatian dictionary is installed. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - if w.contains('-') { - return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); - } - known(d, w) -} - -/// Is one word (no hyphens) in the dictionary or the user dictionary? -fn known(d: &AffixDict, w: &str) -> bool { - d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dictionary_loads() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert!(d.len() > 15); - assert!(d.rule_count() > 0); - assert!(!d.map().is_empty() && !d.rep().is_empty() && !d.try_chars().is_empty()); - } - - #[test] - fn correct_words_and_affixes() { - test_dict::install(); - for w in ["kuća", "kuće", "računalo", "čovjek", "riječ", "dijete", "Hrvatska", "čokolada", "škola", "škole", "HRT", "2026"] { - assert!(is_correct(w), "{w}"); - } - } - - #[test] - fn misspellings_are_flagged() { - test_dict::install(); - for w in ["kuca", "racunalo", "covjek", "rijec", "dijeteh", "skolla"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn missing_dictionary_is_silent() { - assert!(is_correct_with(&AffixDict::default(), "nepostojećariječ")); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Croatian checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_croatian_check() { - test_dict::install(); - let lang = crate::ProofLang::Hr; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("ŠKOLLA", lang, &defaults), "short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("ŠKOLLA", lang, &check_caps)); - assert!(crate::is_correct_in_with("ŠKOLA", lang, &check_caps)); - assert!(crate::is_correct_in_with("školla2", lang, &defaults)); - assert!(!crate::is_correct_in_with("školla2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("ŠKOLLA"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/hr/suggest.rs b/crates/proof/src/hr/suggest.rs deleted file mode 100644 index d8c19b99b..000000000 --- a/crates/proof/src/hr/suggest.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Croatian spelling suggestions, best first: the shared ranking ([`crate::affix_suggest`]) with -//! the words Croatian runs into their neighbours (`neznam` → `ne znam`), the letter groups of -//! Croatian diacritics and letters to try, for a dictionary without MAP or TRY lines. - -use crate::affix::AffixDict; -use crate::affix_suggest::{Split, Tuning}; - -/// Short words often run into the next one (`ne znam`, `na primjer`, `da li`). -const LEADING_WORDS: &[&str] = &[ - "a", "ali", "i", "ili", "da", "do", "od", "na", "ne", "ni", "o", "po", "pod", "pri", "sa", "s", "se", "su", "u", "uz", "za", "iz", "je", "će", - "bi", "bih", "što", "koji", "koja", "koje", "kao", "kad", "dok", "bez", "pred", "nad", -]; -const TRAILING_WORDS: &[&str] = - &["je", "su", "se", "ga", "mi", "ti", "mu", "joj", "ih", "im", "li", "će", "ću", "bi", "bih", "i", "a", "u", "na", "za", "od", "do"]; - -/// What Croatian adds to its dictionary's tables. -const TUNING: Tuning = Tuning { - split: Split::Near { leading: LEADING_WORDS, trailing: TRAILING_WORDS }, - alphabet: "aeiourntsjkvlmdpbgczčćšžđfh", - map: &["cčć", "dđ", "sš", "zž"], - ..Tuning::DEFAULT -}; - -/// Spelling suggestions for a Croatian word, best first, at most `max`, in the word's letter case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &TUNING) -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(suggest_in); - } - /// A dictionary without MAP lines still gets the Croatian diacritic groups: `kuca` → `kuća` - /// first, before a typo that costs more (`kuha`). - #[test] - fn diacritics_without_map_lines() { - let d = AffixDict::from_hunspell("", "2\nkuća\nkuha\n", &[]).unwrap(); - assert_eq!(suggest_in(&d, "kuca", 1), ["kuća"]); - } - #[test] - fn croatian_suggestions() { - crate::hr::test_dict::install(); - for (wrong, right) in [("kuca", "kuća"), ("racunalo", "računalo"), ("covjek", "čovjek"), ("rijec", "riječ"), ("skolla", "škola")] { - let result = suggest(wrong, 10); - assert!(result.iter().any(|s| s == right), "{wrong} -> {result:?}, expected {right}"); - } - } - #[test] - fn bounded() { - crate::hr::test_dict::install(); - assert!(suggest("", 5).is_empty()); - assert!(suggest("kuca", 0).is_empty()); - assert!(suggest(&"a".repeat(41), 5).is_empty()); - } -} diff --git a/crates/proof/src/hr/test_dict.rs b/crates/proof/src/hr/test_dict.rs deleted file mode 100644 index 038069e83..000000000 --- a/crates/proof/src/hr/test_dict.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Small synthetic UTF-8 Croatian Hunspell dictionary for deterministic tests. -pub const AFF: &str = "SET UTF-8 -TRY abcčćdđefghijklmnoprsštuvzž -MAP 5 -MAP cčć -MAP dđ -MAP sš -MAP zž -MAP iije -REP 3 -REP c č -REP dj đ -REP je ije -SFX A Y 2 -SFX A a e a -SFX A a i a -"; -pub const DIC: &str = "18 -kuća/A -računalo -čovjek -riječ -dijete -Hrvatska -čokolada -škola/A -more -sunce -dobar -hrvatski -jezik -tekst -pogreška -pravopis -provjera -računala -"; -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} diff --git a/crates/proof/src/hunspell.rs b/crates/proof/src/hunspell.rs deleted file mode 100644 index 1afd903cd..000000000 --- a/crates/proof/src/hunspell.rs +++ /dev/null @@ -1,706 +0,0 @@ -//! Hunspell dictionaries installed on the system, read at runtime. -//! -//! WordCraft doesn't bundle large spelling dictionaries. For languages with an affix dictionary -//! (Polish, Russian), it reads the standard Hunspell `.aff`/`.dic` pair installed by the operating system, -//! a package manager or LibreOffice, with [`crate::affix::AffixDict::from_hunspell`]. When none is -//! installed the language has no spelling check (no marks, no errors), like Word without the -//! language's proofing tools. -//! -//! Search order: the directory in `WORDCRAFT_DICTIONARIES` (or one set with -//! [`set_dictionary_dir`]), then the usual system locations. On the web there are none. - -use std::path::{Path, PathBuf}; -use std::sync::RwLock; - -use crate::affix::AffixDict; - -/// Largest `.aff` or `.dic` file read (real ones are a few MB). -const MAX_FILE: u64 = 64 << 20; - -/// An extra directory searched first (a setting), set by the front end. -static EXTRA_DIR: RwLock> = RwLock::new(None); - -/// Why an installed dictionary failed to load, by `.dic` name (`pt_BR`): the file and the reason. -/// Without it a file that is there but unreadable looks the same as no file (#2830). -static LOAD_ERRORS: RwLock> = RwLock::new(Vec::new()); - -/// Remember (or, for `Ok`, forget) why the dictionary `key` found at `dic` failed to load, and -/// hand the result back unchanged. -pub fn note_load(key: &str, dic: &Path, result: Result) -> Result { - let mut errors = LOAD_ERRORS.write().unwrap_or_else(std::sync::PoisonError::into_inner); - errors.retain(|(k, _, _)| k != key); - if let Err(e) = &result { - errors.push((key.to_string(), dic.to_path_buf(), e.clone())); - } - result -} - -/// Forget the load error of the dictionary `key` (none found, or it is about to be searched for again). -pub fn clear_load_error(key: &str) { - LOAD_ERRORS.write().unwrap_or_else(std::sync::PoisonError::into_inner).retain(|(k, _, _)| k != key); -} - -/// The `.dic` file and the reason of the last failed load of the dictionary `key`, if any. -pub fn load_error(key: &str) -> Option<(PathBuf, String)> { - LOAD_ERRORS.read().unwrap_or_else(std::sync::PoisonError::into_inner).iter().find(|(k, _, _)| k == key).map(|(_, p, e)| (p.clone(), e.clone())) -} - -/// Search `dir` before the system locations (a "dictionaries folder" setting); `None` clears it. -/// Takes effect for dictionaries not loaded yet. -pub fn set_dictionary_dir(dir: Option) { - *EXTRA_DIR.write().unwrap_or_else(std::sync::PoisonError::into_inner) = dir; - LOAD_ERRORS.write().unwrap_or_else(std::sync::PoisonError::into_inner).clear(); // they belong to the old search path -} - -/// The LibreOffice extension directories a Hunspell dictionary name may live in: LibreOffice's -/// IDs are per dictionary — Polish is `dict-pl`, Brazilian Portuguese `dict-pt-BR` — so both -/// the primary-language folder and the dashed full name are tried. -fn lo_ext_dirs(name: &str) -> Vec { - let primary = name.split(['-', '_']).next().unwrap_or(name); - let dashed = name.replace('_', "-"); - let first = format!("dict-{primary}"); - let full = format!("dict-{dashed}"); - if full == first { vec![first] } else { vec![first, full] } -} - -/// Directories that may hold Hunspell dictionaries, most specific first. `lo_exts`, when a -/// language is being searched for, adds LibreOffice's extension folders for it (`dict-pt-BR`). -pub fn search_dirs(lo_exts: Option<&[String]>) -> Vec { - let mut dirs: Vec = Vec::new(); - if let Some(d) = EXTRA_DIR.read().unwrap_or_else(std::sync::PoisonError::into_inner).clone() { - dirs.push(d); - } - if cfg!(target_arch = "wasm32") { - return dirs; - } - if let Some(d) = std::env::var_os("WORDCRAFT_DICTIONARIES").filter(|v| !v.is_empty()) { - dirs.push(PathBuf::from(d)); - } - // LibreOffice keeps each language under `…/share/extensions/`. - let lo_dirs = - |prefix: &str| -> Vec { lo_exts.map_or_else(Vec::new, |exts| exts.iter().map(|e| PathBuf::from(prefix).join(e)).collect()) }; - let home = std::env::var_os("HOME").filter(|v| !v.is_empty()).map(PathBuf::from); - if cfg!(windows) { - for var in ["LOCALAPPDATA", "APPDATA"] { - if let Some(base) = std::env::var_os(var).filter(|v| !v.is_empty()).map(PathBuf::from) { - dirs.push(base.join("WordCraft").join("dictionaries")); - dirs.push(base.join("hunspell")); - } - } - for var in ["ProgramFiles", "ProgramFiles(x86)"] { - if let Some(base) = std::env::var_os(var).filter(|v| !v.is_empty()).map(PathBuf::from) { - dirs.extend(lo_dirs(&base.join("LibreOffice").join("share").join("extensions").to_string_lossy())); - } - } - // If LibreOffice is installed on another drive (e.g. D:\Program Files\LibreOffice), check drives C..Z - for drive in b'C'..=b'Z' { - let drive_char = drive as char; - for pf in ["Program Files", "Program Files (x86)"] { - let lo_base = PathBuf::from(format!(r"{drive_char}:\{pf}\LibreOffice\share\extensions")); - if lo_base.is_dir() { - dirs.extend(lo_dirs(&lo_base.to_string_lossy())); - } - } - } - } else if cfg!(target_os = "macos") { - if let Some(h) = &home { - dirs.push(h.join("Library/Spelling")); - } - dirs.push(PathBuf::from("/Library/Spelling")); - dirs.push(PathBuf::from("/opt/homebrew/share/hunspell")); - dirs.push(PathBuf::from("/usr/local/share/hunspell")); - dirs.extend(lo_dirs("/Applications/LibreOffice.app/Contents/Resources/extensions")); - } else { - if let Some(h) = &home { - dirs.push(h.join(".local/share/hunspell")); - } - for d in ["/usr/share/hunspell", "/usr/share/myspell", "/usr/share/myspell/dicts", "/usr/local/share/hunspell", "/usr/local/share/myspell"] { - dirs.push(PathBuf::from(d)); - } - for d in ["/usr/lib/libreoffice/share/extensions", "/opt/libreoffice/share/extensions"] { - dirs.extend(lo_dirs(d)); - } - } - dirs -} - -/// The first `.aff`/`.dic` pair found for any of `names` (`pl_PL`, `pl`), searching -/// [`search_dirs`] (including LibreOffice's extension folder for the first name). -pub fn find(names: &[&str]) -> Option<(PathBuf, PathBuf)> { - let lo = names.first().copied().map(lo_ext_dirs); - search_dirs(lo.as_deref()).into_iter().find_map(|dir| find_in(&dir, names)) -} - -/// The first `.aff`/`.dic` pair for any of `names` in `dir`. -pub fn find_in(dir: &Path, names: &[&str]) -> Option<(PathBuf, PathBuf)> { - names.iter().find_map(|n| { - let aff = dir.join(format!("{n}.aff")); - let dic = dir.join(format!("{n}.dic")); - (aff.is_file() && dic.is_file()).then_some((aff, dic)) - }) -} - -/// Read a dictionary file: a regular file of at most [`MAX_FILE`] bytes. The read itself is -/// bounded, so a file that grows after its size is checked (or a filesystem that reports a wrong -/// size) can't make it read more (#3661). -pub(crate) fn read_capped(p: &Path) -> Result, String> { - use std::io::Read; - let err = |e: std::io::Error| format!("{}: {e}", p.display()); - let f = std::fs::File::open(p).map_err(err)?; - let meta = f.metadata().map_err(err)?; - if !meta.is_file() { - return Err(format!("{}: not a regular file", p.display())); - } - if meta.len() > MAX_FILE { - return Err(format!("{}: too large ({} bytes)", p.display(), meta.len())); - } - let mut buf = Vec::new(); - f.take(MAX_FILE + 1).read_to_end(&mut buf).map_err(err)?; - if buf.len() as u64 > MAX_FILE { - return Err(format!("{}: too large", p.display())); - } - Ok(buf) -} - -/// Read a Hunspell pair from disk (size-capped), decoded per the `.aff`'s `SET` line. -pub fn load(aff: &Path, dic: &Path) -> Result { - let (aff_text, dic_text) = decode(&read_capped(aff)?, &read_capped(dic)?)?; - let source = format!("Hunspell dictionary {}", dic.display()); - AffixDict::from_hunspell(&aff_text, &dic_text, &[source.as_str()]) -} - -/// ISO 8859-1: every byte is the code point of the same value (the original VERO pt_BR uses it). -fn decode_latin1(bytes: &[u8]) -> String { - bytes.iter().map(|&b| char::from(b)).collect() -} - -/// ISO 8859-9 (Latin-5, Turkish): ISO 8859-1 with Ğ İ Ş ğ ı ş in place of Ð Ý Þ ð ý þ. The -/// encoding of older Turkish dictionaries (`SET ISO8859-9`). -fn decode_latin5(bytes: &[u8]) -> String { - bytes - .iter() - .map(|&b| match b { - 0xd0 => 'Ğ', - 0xdd => 'İ', - 0xde => 'Ş', - 0xf0 => 'ğ', - 0xfd => 'ı', - 0xfe => 'ş', - _ => char::from(b), - }) - .collect() -} - -/// ISO 8859-2 code points 0xA0..=0xFF (0x00..=0x9F map to themselves). -const ISO_8859_2_HIGH: &str = "\u{a0}Ą˘Ł¤ĽŚ§¨ŠŞŤŹ\u{ad}ŽŻ°ą˛ł´ľśˇ¸šşťź˝žżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙"; - -fn decode_latin2(bytes: &[u8]) -> String { - let high: Vec = ISO_8859_2_HIGH.chars().collect(); - bytes.iter().map(|&b| if b < 0xa0 { char::from(b) } else { high.get(usize::from(b - 0xa0)).copied().unwrap_or('\u{fffd}') }).collect() -} - -/// KOI8-R code points 0x80..=0xFF (0x00..=0x7F are ASCII): the encoding of older Russian -/// dictionaries. -const KOI8_R_HIGH: &str = - "─│┌┐└┘├┤┬┴┼▀▄█▌▐░▒▓⌠■∙√≈≤≥\u{a0}⌡°²·÷═║╒ё╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡Ё╢╣╤╥╦╧╨╩╪╫╬©юабцдефгхийклмнопярстужвьызшэщчъЮАБЦДЕФГХИЙКЛМНОПЯРСТУЖВЬЫЗШЭЩЧЪ"; - -/// Windows-1251 code points 0x80..=0xFF (0x98 is unassigned): the other one Russian dictionaries -/// use. -const CP1251_HIGH: &str = "ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏђ‘’“”•–—\u{fffd}™љ›њќћџ\u{a0}ЎўЈ¤Ґ¦§Ё©Є«¬\u{ad}®Ї°±Ііґµ¶·ё№є»јЅѕїАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя"; - -/// Decode a single-byte encoding whose bytes 0x80..=0xFF are `high` (ASCII below). -fn decode_high(bytes: &[u8], high: &str) -> String { - let high: Vec = high.chars().collect(); - bytes.iter().map(|&b| if b < 0x80 { char::from(b) } else { high.get(usize::from(b - 0x80)).copied().unwrap_or('\u{fffd}') }).collect() -} - -/// Decode an `.aff`/`.dic` pair to UTF-8 from the encoding the `.aff`'s `SET` line names -/// (UTF-8 when there is none; ISO 8859-2 is the other one Polish dictionaries use, ISO 8859-1 the -/// one the original Portuguese VERO uses, ISO 8859-9 the one older Turkish dictionaries use, KOI8-R -/// and Windows-1251 the ones older Russian dictionaries use). -/// -/// Under a UTF-8 `SET` with single-byte flags (no `FLAG` line, or `FLAG DEFAULT`), flags are bytes, -/// as in Hunspell: the Hungarian dictionary writes its affix flags and flag aliases as bytes above -/// 0x7F that aren't UTF-8. Those fields are read byte by byte (each byte becomes U+0000..U+00FF, -/// one flag per byte); the words, affixes and conditions around them are UTF-8. With any other -/// `FLAG` (`UTF-8`, `num`, `long`) flags are text and the whole file is UTF-8: `SFX 1234` keeps -/// its number (the Turkish dictionary's `FLAG num`). -pub fn decode(aff: &[u8], dic: &[u8]) -> Result<(String, String), String> { - let (set, flag) = directives(aff); - let set = set.unwrap_or_else(|| "UTF-8".into()); - match set.as_str() { - "UTF-8" | "UTF8" => { - if flag.as_deref().is_some_and(|f| f != "DEFAULT") { - let text = |b: &[u8], what: &str| String::from_utf8(b.to_vec()).map_err(|e| format!("{what}: not UTF-8: {e}")); - return Ok((text(aff, ".aff")?, text(dic, ".dic")?)); - } - Ok((decode_utf8_aff(aff)?, decode_utf8_dic(dic)?)) - } - "ISO8859-1" | "ISO-8859-1" => Ok((decode_latin1(aff), decode_latin1(dic))), - "ISO8859-2" | "ISO-8859-2" => Ok((decode_latin2(aff), decode_latin2(dic))), - "ISO8859-9" | "ISO-8859-9" => Ok((decode_latin5(aff), decode_latin5(dic))), - "KOI8-R" | "KOI8R" => Ok((decode_high(aff, KOI8_R_HIGH), decode_high(dic, KOI8_R_HIGH))), - "CP1251" | "WINDOWS-1251" | "MICROSOFT-CP1251" => Ok((decode_high(aff, CP1251_HIGH), decode_high(dic, CP1251_HIGH))), - other => Err(format!("unsupported encoding {other}")), - } -} - -/// The first arguments of the `.aff`'s first `SET` and `FLAG` lines, in capitals, found in one pass -/// over the whole file: a `FLAG UTF-8` after the rules still decides how the `.dic` flags are read, -/// as in Hunspell. Only the first argument counts: `FLAG UTF-8 # Unicode flags` is UTF-8. -fn directives(aff: &[u8]) -> (Option, Option) { - let (mut set, mut flag) = (None, None); - for l in aff.split(|&b| b == b'\n') { - if set.is_some() && flag.is_some() { - break; - } - let l = l.strip_prefix("\u{feff}".as_bytes()).unwrap_or(l); - let slot = match l.first() { - Some(b'S') if set.is_none() => (&mut set, &b"SET"[..]), - Some(b'F') if flag.is_none() => (&mut flag, &b"FLAG"[..]), - _ => continue, - }; - let arg = l - .strip_prefix(slot.1) - .filter(|rest| rest.first().is_some_and(|b| matches!(b, b' ' | b'\t'))) - .and_then(|rest| rest.split(|b| matches!(b, b' ' | b'\t' | b'\r')).find(|f| !f.is_empty())); - if let Some(a) = arg { - *slot.0 = Some(String::from_utf8_lossy(a).to_ascii_uppercase()); - } - } - (set, flag) -} - -/// Directives whose one argument is a flag. -const FLAG_DIRECTIVES: &[&str] = &[ - "CIRCUMFIX", - "COMPOUNDBEGIN", - "COMPOUNDEND", - "COMPOUNDFLAG", - "COMPOUNDFORBIDFLAG", - "COMPOUNDLAST", - "COMPOUNDFIRST", - "COMPOUNDMIDDLE", - "COMPOUNDPERMITFLAG", - "COMPOUNDROOT", - "FORBIDDENWORD", - "FORCEUCASE", - "GENERATE", - "HU_KOTOHANGZO", - "KEEPCASE", - "LEMMA_PRESENT", - "NEEDAFFIX", - "NOSUGGEST", - "ONLYINCOMPOUND", - "ONLYROOT", - "PSEUDOROOT", - "SUBSTANDARD", - "WARN", -]; - -/// Directives that only describe the dictionary; a non-UTF-8 byte in them (the Hungarian -/// dictionary's `NAME` is ISO 8859-2) is replaced instead of failing the load. -const METADATA_DIRECTIVES: &[&str] = &["NAME", "HOME", "VERSION", "LANG", "AM"]; - -/// `text/flags`: the text as UTF-8 and the flags byte by byte (`\/` is a literal slash). `what` -/// names the line in an error. -fn text_and_flags(field: &[u8], what: impl Fn() -> String) -> Result { - let mut slash = None; - let mut i = 0; - while let Some(&b) = field.get(i) { - if b == b'\\' && field.get(i + 1) == Some(&b'/') { - i += 2; - continue; - } - if b == b'/' { - slash = Some(i); - break; - } - i += 1; - } - let (text, flags) = match slash { - Some(k) => (field.get(..k).unwrap_or_default(), field.get(k..).unwrap_or_default()), - None => (field, &[][..]), - }; - let mut out = std::str::from_utf8(text).map_err(|e| format!("{}: not UTF-8: {e}", what()))?.to_string(); - out.push_str(&decode_latin1(flags)); - Ok(out) -} - -/// Split a line into fields on spaces and tabs, keeping the separators (so a line rebuilt from -/// its parts is the original). -fn fields(line: &[u8]) -> Vec<&[u8]> { - let mut out = Vec::new(); - let mut start = 0; - let mut in_sep = line.first().is_some_and(|b| matches!(b, b' ' | b'\t')); - for (i, b) in line.iter().enumerate() { - let sep = matches!(b, b' ' | b'\t'); - if sep != in_sep { - out.push(line.get(start..i).unwrap_or_default()); - start = i; - in_sep = sep; - } - } - out.push(line.get(start..).unwrap_or_default()); - out -} - -/// A UTF-8 `.aff` with single-byte flags (see [`decode`]). -fn decode_utf8_aff(aff: &[u8]) -> Result { - let mut out = String::with_capacity(aff.len()); - for (n, line) in aff.split(|&b| b == b'\n').enumerate() { - if n > 0 { - out.push('\n'); - } - if let Ok(s) = std::str::from_utf8(line) - && s.is_ascii() - { - out.push_str(s); - continue; - } - let parts = fields(line); - // Field index among the non-separator parts. - let words: Vec = - parts.iter().enumerate().filter(|(_, p)| !p.first().is_some_and(|b| matches!(b, b' ' | b'\t'))).map(|(i, _)| i).collect(); - let dir = words - .first() - .and_then(|&i| parts.get(i)) - .map(|p| String::from_utf8_lossy(p).trim_start_matches('\u{feff}').to_string()) - .unwrap_or_default(); - let what = || format!(".aff line {}", n + 1); - if dir.starts_with('#') || METADATA_DIRECTIVES.contains(&dir.as_str()) { - out.push_str(&String::from_utf8_lossy(line)); - continue; - } - // A trailing `# comment` (the Hungarian `AF` lines number themselves) is never a word or - // a flag; from there on the line is kept as text. It can only start after the directive's - // own fields, since a field such as an affix condition may itself begin with `#`. - let field_text = |k: usize| words.get(k).and_then(|&w| parts.get(w)).map(|p| String::from_utf8_lossy(p).into_owned()); - let first_free = comment_field(&dir, field_text); - let comment_at = words.iter().skip(first_free).position(|&w| parts.get(w).is_some_and(|f| f.first() == Some(&b'#'))).map(|k| k + first_free); - // The field index of the next non-separator part. - let mut next_field = 0; - for (i, p) in parts.iter().enumerate() { - let field = (!p.first().is_some_and(|b| matches!(b, b' ' | b'\t'))).then(|| { - next_field += 1; - next_field - 1 - }); - let in_comment = comment_at.is_some_and(|c| words.get(c).is_some_and(|&w| i >= w)); - let decoded = match (dir.as_str(), field) { - _ if in_comment => String::from_utf8_lossy(p).into_owned(), - ("AF" | "COMPOUNDRULE" | "SYLLABLENUM", Some(1)) => decode_latin1(p), - ("PFX" | "SFX", Some(3)) | ("CHECKCOMPOUNDPATTERN", Some(1 | 2)) => text_and_flags(p, what)?, - // A field that names one flag keeps its first byte, as Hunspell's `decode_flag` - // does: `SFX é` is the flag 0xC3, the first of the two flags `kert/é` carries. - ("PFX" | "SFX", Some(1)) => decode_latin1(p.get(..1).unwrap_or_default()), - (d, Some(1)) if FLAG_DIRECTIVES.contains(&d) => decode_latin1(p.get(..1).unwrap_or_default()), - _ => std::str::from_utf8(p).map_err(|e| format!("{}: not UTF-8: {e}", what()))?.to_string(), - }; - out.push_str(&decoded); - } - } - Ok(out) -} - -/// The first field of an `.aff` line where a trailing `# comment` may start: after the -/// directive's own fields. A table's count line (`REP 12`, `PFX A Y 3`) has fewer fields than its -/// rows (`REP a b`, `PFX A 0 x .`). `field(k)` is the line's `k`-th field. -fn comment_field(dir: &str, field: impl Fn(usize) -> Option) -> usize { - let number = |k: usize| field(k).is_some_and(|f| f.parse::().is_ok()); - match dir { - "PFX" | "SFX" if field(2).is_some_and(|f| f == "Y" || f == "N") && number(3) => 4, - "PFX" | "SFX" => 5, - "REP" | "ICONV" | "OCONV" | "PHONE" | "MAP" | "BREAK" | "AF" | "AM" | "COMPOUNDRULE" | "CHECKCOMPOUNDPATTERN" - if number(1) && field(2).is_none_or(|f| f.starts_with('#')) => - { - 2 - } - "REP" | "ICONV" | "OCONV" | "PHONE" => 3, - "CHECKCOMPOUNDPATTERN" => 4, - _ => 2, - } -} - -/// Where the `word/flags` entry of a `.dic` line ends, as Hunspell reads it: at a tab or at the -/// first morphological field (a space or tab, two characters and `:`, as in ` po:noun`), whichever -/// comes first, so an entry may hold spaces (`ad hoc`); with neither, at the end of the line -/// (`kot/B 12` has the flags `B 12`, as in Hunspell). -pub(crate) fn dic_entry_end(line: &[u8]) -> usize { - let blank = |b: &u8| matches!(b, b' ' | b'\t'); - let morph = line.windows(4).position(|w| matches!(w, [a, b, c, b':'] if blank(a) && !blank(b) && !blank(c))); - let tab = line.iter().position(|&b| b == b'\t'); - let end = match (morph, tab) { - (Some(m), Some(t)) => m.min(t), - (m, t) => match m.or(t) { - Some(e) => e, - None => line.len(), - }, - }; - // Blanks before the morphological field or the line end aren't part of the entry. - line.get(..end).map_or(end, |e| e.iter().rposition(|b| !blank(b)).map_or(0, |k| k + 1)) -} - -/// A UTF-8 `.dic` with single-byte flags: the word is UTF-8, the flags after `/` are bytes. -fn decode_utf8_dic(dic: &[u8]) -> Result { - let mut out = String::with_capacity(dic.len()); - for (n, line) in dic.split(|&b| b == b'\n').enumerate() { - if n > 0 { - out.push('\n'); - } - if let Ok(s) = std::str::from_utf8(line) - && s.split_once('/').is_none_or(|(_, flags)| flags.is_ascii()) - { - out.push_str(s); - continue; - } - let what = || format!(".dic line {}", n + 1); - // The first field is `word/flags` (it may hold spaces, see [`dic_entry_end`]); - // morphological fields after it are UTF-8 text. - let end = dic_entry_end(line); - out.push_str(&text_and_flags(line.get(..end).unwrap_or_default(), what)?); - let rest = line.get(end..).unwrap_or_default(); - out.push_str(std::str::from_utf8(rest).map_err(|e| format!("{}: not UTF-8: {e}", what()))?); - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn decodes_latin2_and_utf8() { - let aff = b"SET ISO8859-2\nTRY a\n"; - let (a, d) = decode(aff, b"2\n\xbf\xf3\xb3w\n").unwrap(); - assert!(a.contains("TRY a")); - assert_eq!(d, "2\nżółw\n"); - let (_, d) = decode("SET UTF-8\n".as_bytes(), "żółw".as_bytes()).unwrap(); - assert_eq!(d, "żółw"); - let (_, d) = decode(b"SET ISO8859-9\n", b"\xd0\xdd\xde\xf0\xfd\xfe\xd6\xe7").unwrap(); - assert_eq!(d, "ĞİŞğışÖç"); - assert!(decode(b"SET SHIFT_JIS\n", b"").is_err()); - assert!(decode(b"SET UTF-8\n", b"\xff\xfe").is_err()); - } - - /// #2830: a dictionary that is there but can't be read leaves its reason behind; a good - /// load or no file clears it. - #[test] - fn load_error_is_remembered_until_the_next_good_load() { - let key = "test_unreadable_2830"; - assert_eq!(load_error(key), None); - let dic = Path::new("/dicts/test.dic"); - assert!(note_load(key, dic, decode(b"SET SHIFT_JIS\n", b"")).is_err()); - let (file, reason) = load_error(key).expect("error kept"); - assert_eq!(file, dic); - assert_eq!(reason, "unsupported encoding SHIFT_JIS"); - assert!(note_load(key, dic, decode(b"SET UTF-8\n", b"")).is_ok()); - assert_eq!(load_error(key), None); - let _ = note_load::<()>(key, dic, Err("too large".into())); - clear_load_error(key); - assert_eq!(load_error(key), None); - } - - #[test] - fn decodes_latin1() { - // `orçamento esperávamos` in ISO 8859-1: ç = 0xE7, á = 0xE1. - let (a, d) = decode(b"SET ISO8859-1\nTRY \xe7\n", b"1\nor\xe7amento/A\nesper\xe1vamos\n").unwrap(); - assert!(a.contains("TRY ç"), "{a}"); - assert_eq!(d, "1\norçamento/A\nesperávamos\n"); - assert!(decode(b"SET ISO-8859-1\n", b"\xe7").is_ok()); - } - - #[test] - fn utf8_dictionaries_keep_single_byte_flags() { - // The Hungarian dictionary is `SET UTF-8` but writes flags as single bytes above 0x7F, - // which aren't UTF-8; each byte is one flag. Its `NAME` line is ISO 8859-2. - let aff = b"SET UTF-8\nNAME Magyar helyes\xedr\xe1si\nAF 2\nAF \xd5a # 1\nAF \xe9 # 2\nSFX \xd5 Y 1\nSFX \xd5 0 ok/2 . 7\nSFX \xe9 Y 1\nSFX \xe9 0 ban .\nSFX a Y 1\nSFX a 0 at .\n"; - let dic = "2\nház/1\t1\nkert/\u{e9}\n".as_bytes(); - let (a, d) = decode(aff, dic).unwrap(); - assert!(a.contains("AF \u{d5}a # 1") && a.contains("AF \u{e9} # 2") && a.contains("SFX \u{d5} 0 ok/2 . 7")); - assert!(d.contains("kert/\u{c3}\u{a9}"), "a UTF-8 é in the flags is two one-byte flags"); - let dict = crate::affix::AffixDict::from_hunspell(&a, &d, &[]).unwrap(); - assert!(dict.check("ház") && dict.check("házok") && dict.check("házokban") && dict.check("házat")); - assert!(!dict.check("házban")); - // `FLAG UTF-8` keeps the whole file UTF-8, and words must still be UTF-8. - let (_, d) = decode(b"SET UTF-8\nFLAG UTF-8\n", "kert/\u{e9}\n".as_bytes()).unwrap(); - assert_eq!(d, "kert/\u{e9}\n"); - assert!(decode(b"SET UTF-8\nSFX a Y 1\nSFX a 0 \xe9 .\n", b"").is_err(), "an affix must be UTF-8"); - assert!(decode(b"SET UTF-8\n", b"1\nh\xe1z/a\n").is_err(), "a stem must be UTF-8"); - } - - #[test] - fn single_flag_fields_keep_their_first_byte() { - // Without `FLAG UTF-8`, a UTF-8 `é` (0xC3 0xA9) in a field that names one flag is the - // flag 0xC3; in a `.dic` flag list it is the two flags 0xC3 and 0xA9. - let aff = "SET UTF-8\nKEEPCASE é\nSFX é Y 1\nSFX é 0 ok .\nSYLLABLENUM é\n".as_bytes(); - let (a, d) = decode(aff, "2\nkert/é\nNATO/é\n".as_bytes()).unwrap(); - assert!(a.contains("KEEPCASE \u{c3}\n") && a.contains("SFX \u{c3} Y 1") && a.contains("SFX \u{c3} 0 ok ."), "{a}"); - assert!(a.contains("SYLLABLENUM \u{c3}\u{a9}"), "SYLLABLENUM lists flags: {a}"); - assert!(d.contains("kert/\u{c3}\u{a9}")); - let dict = crate::affix::AffixDict::from_hunspell(&a, &d, &[]).unwrap(); - assert!(dict.check("kert") && dict.check("kertok")); - assert!(dict.check("NATO") && !dict.check("Nato"), "KEEPCASE é reaches NATO/é"); - } - - #[test] - fn numeric_and_long_flags_stay_text() { - // `FLAG num` (the Turkish dictionary) and `FLAG long`: no byte-wise flag reading, so a - // number with a non-ASCII rule or comment keeps every digit. - let aff = "SET UTF-8\nFLAG num\nFORBIDDENWORD 6000 # tiltott ő\nSFX 1234 Y 1\nSFX 1234 0 ları .\n"; - let (a, d) = decode(aff.as_bytes(), "1\nkitap/1234\n".as_bytes()).unwrap(); - assert_eq!(a, aff); - assert_eq!(d, "1\nkitap/1234\n"); - let tr = crate::tr::dict::TrDict::from_hunspell(&a, &d).unwrap(); - assert!(tr.contains("kitapları") && tr.contains("kitap")); - let long = "SET UTF-8\nFLAG long\nSFX Aa Y 1\nSFX Aa 0 ős .\n"; - assert_eq!(decode(long.as_bytes(), "1\nkő/Aa\n".as_bytes()).unwrap().0, long); - assert!(decode(b"SET UTF-8\nFLAG num\n", b"1\nh\xe1z/1\n").is_err(), "the .dic must be UTF-8"); - // `FLAG DEFAULT` is the single-byte default. - let (_, d) = decode(b"SET UTF-8\nFLAG default\n", b"1\nkert/\xe9\n").unwrap(); - assert_eq!(d, "1\nkert/\u{e9}\n"); - } - - #[test] - fn flag_utf8_counts_anywhere_in_the_aff() { - // A `FLAG UTF-8` after a long rule table still makes the `.dic` flags Unicode. - let mut aff = b"SET UTF-8\nSFX a Y 400\n".to_vec(); - for _ in 0..400 { - aff.extend_from_slice(b"SFX a 0 aaaaaaaaaaaaaaaaaaaaaaaa .\n"); - } - aff.extend_from_slice(b"FLAG UTF-8\r\n"); - assert!(aff.len() > 8192); - let (_, d) = decode(&aff, "kert/\u{e9}\n".as_bytes()).unwrap(); - assert_eq!(d, "kert/\u{e9}\n"); - } - - #[test] - fn a_comment_after_flag_utf8_keeps_unicode_flags() { - let (_, d) = decode(b"SET UTF-8\nFLAG UTF-8 # Unicode flags\n", "kert/\u{e9}\n".as_bytes()).unwrap(); - assert_eq!(d, "kert/\u{e9}\n"); - let (_, d) = decode(b"SET\tUTF-8 # comment\nFLAG\t\tUTF-8\n", "kert/\u{e9}\n".as_bytes()).unwrap(); - assert_eq!(d, "kert/\u{e9}\n", "repeated spaces and tabs separate the argument"); - } - - #[test] - fn decodes_russian_single_byte_encodings() { - // `ёлка` in KOI8-R (0xA3 0xCC 0xCB 0xC1) and in Windows-1251 (0xB8 0xEB 0xEA 0xE0). - let (a, d) = decode(b"SET KOI8-R\nTRY \xcf\xc5\n", b"1\n\xa3\xcc\xcb\xc1/A\n").unwrap(); - assert!(a.contains("TRY ое"), "{a}"); - assert_eq!(d, "1\nёлка/A\n"); - let (_, d) = decode(b"SET microsoft-cp1251\n", b"1\n\xb8\xeb\xea\xe0 \xc4\xee\xec\n").unwrap(); - assert_eq!(d, "1\nёлка Дом\n"); - assert_eq!(decode_high(&[0x98], CP1251_HIGH), "\u{fffd}"); - } - - #[test] - fn comments_start_after_the_directive_fields() { - let f = |line: &'static str| move |k: usize| line.split_whitespace().nth(k).map(str::to_string); - assert_eq!(comment_field("PFX", f("PFX A Y 3 # count")), 4); - assert_eq!(comment_field("SFX", f("SFX A 0 # .")), 5, "a rule's condition may be `#`"); - assert_eq!(comment_field("AF", f("AF 1234 # aliases")), 2); - assert_eq!(comment_field("AF", f("AF Aa # 1")), 2); - assert_eq!(comment_field("REP", f("REP 12")), 2); - assert_eq!(comment_field("REP", f("REP # x")), 3); - assert_eq!(comment_field("CHECKCOMPOUNDPATTERN", f("CHECKCOMPOUNDPATTERN a b c")), 4); - assert_eq!(comment_field("KEEPCASE", f("KEEPCASE k")), 2); - // A byte flag after an append text that begins with `#` stays a flag, not a comment. - let (a, _) = decode(b"SET UTF-8\nSFX \xd5 Y 1 # header\nSFX \xd5 0 #/\xe9 . # rule\n", b"").unwrap(); - assert!(a.contains("SFX \u{d5} 0 #/\u{e9} . # rule"), "{a}"); - } - - #[test] - fn dic_entries_end_where_hunspell_ends_them() { - let end = |l: &str| l.get(..dic_entry_end(l.as_bytes())).unwrap().to_string(); - assert_eq!(end("word/A po:noun\tts:x"), "word/A", "the morphological field comes before the tab"); - assert_eq!(end("ad hoc/B\tpo:adv"), "ad hoc/B", "a tab ends a phrase"); - assert_eq!(end("ad hoc/B po:adv"), "ad hoc/B"); - assert_eq!(end("word/A 123"), "word/A 123", "neither: the whole line, as in Hunspell"); - assert_eq!(end("ad hoc/B "), "ad hoc/B"); - assert_eq!(end("word"), "word"); - assert_eq!(end("a:b/A"), "a:b/A", "a colon inside the word isn't a field"); - // Single-byte flags of a phrase entry are read byte by byte, then the UTF-8 morphology. - let (_, d) = decode(b"SET UTF-8\n", b"1\nad hoc/\xe9\tpo:adv\n").unwrap(); - assert_eq!(d, "1\nad hoc/\u{e9}\tpo:adv\n"); - // Without a tab or a morphological field the whole line is the entry: its byte flags - // don't fail the load (the Hungarian dictionary has such lines: `tök jó/396`). - let (_, d) = decode(b"SET UTF-8\n", b"1\nad hoc/\xe9\n").unwrap(); - assert_eq!(d, "1\nad hoc/\u{e9}\n"); - } - - #[test] - fn libreoffice_extension_dirs_follow_the_language() { - // LibreOffice's IDs are per dictionary: Polish is `dict-pl`, Brazilian Portuguese - // `dict-pt-BR`, so both the primary-language folder and the dashed full name are tried. - assert_eq!(super::lo_ext_dirs("cs_CZ"), ["dict-cs", "dict-cs-CZ"]); - assert_eq!(super::lo_ext_dirs("id_ID"), ["dict-id", "dict-id-ID"]); - assert_eq!(super::lo_ext_dirs("pl_PL"), ["dict-pl", "dict-pl-PL"]); - assert_eq!(super::lo_ext_dirs("pt_BR"), ["dict-pt", "dict-pt-BR"]); - assert_eq!(super::lo_ext_dirs("de-DE"), ["dict-de", "dict-de-DE"]); - let dirs = search_dirs(Some(&super::lo_ext_dirs("pt_BR"))); - assert!(dirs.iter().any(|d| d.ends_with("dict-pt-BR")), "{dirs:?}"); - assert!(search_dirs(None).iter().all(|d| !d.to_string_lossy().contains("dict-")), "no language: no LibreOffice guess"); - } - - #[test] - fn finds_and_loads_a_pair_in_a_directory() { - let dir = std::env::temp_dir().join(format!("wordcraft-hunspell-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - assert!(find_in(&dir, &["xx_XX"]).is_none(), "no files: nothing found"); - std::fs::write(dir.join("xx_XX.aff"), "SET UTF-8\nSFX A Y 1\nSFX A 0 y .\n").unwrap(); - assert!(find_in(&dir, &["xx_XX"]).is_none(), "an .aff alone isn't a dictionary"); - std::fs::write(dir.join("xx_XX.dic"), "1\nkot/A\n").unwrap(); - let (aff, dic) = find_in(&dir, &["yy", "xx_XX"]).unwrap(); - let d = load(&aff, &dic).unwrap(); - assert!(d.check("kot") && d.check("koty") && !d.check("kotx")); - assert!(load(&dir.join("missing.aff"), &dic).is_err()); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn reads_are_bounded_and_regular_files_only() { - let dir = std::env::temp_dir().join(format!("wordcraft-hunspell-cap-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let aff = dir.join("xx.aff"); - std::fs::write(&aff, "SET UTF-8\n").unwrap(); - // One byte over the cap (sparse: nothing is written) is refused without reading it. - let big = dir.join("big.dic"); - std::fs::File::create(&big).unwrap().set_len(MAX_FILE + 1).unwrap(); - assert!(load(&aff, &big).unwrap_err().contains("too large")); - // A directory, and on Unix an endless device, are not dictionary files. (No clock: a - // reader that read `/dev/zero` would never return, so the error itself is the proof.) - assert!(load(&aff, &dir).unwrap_err().contains("not a regular file")); - #[cfg(unix)] - assert!(load(&aff, Path::new("/dev/zero")).unwrap_err().contains("not a regular file")); - // Turkish reads through the same reader. - let (tr_aff, tr_dic) = (dir.join("tr_TR.aff"), dir.join("tr_TR.dic")); - std::fs::write(&tr_aff, "SET UTF-8\nSFX 1 Y 1\nSFX 1 0 lar .\n").unwrap(); - std::fs::write(&tr_dic, "1\nkitap/1\n").unwrap(); - assert!(crate::tr::load(&tr_aff, &tr_dic).unwrap().contains("kitaplar")); - assert!(crate::tr::load(&tr_aff, &big).unwrap_err().contains("too large")); - let _ = std::fs::remove_dir_all(&dir); - } - - /// A Turkish dictionary in ISO 8859-9, the encoding older tr_TR dictionaries use, read from - /// disk through the capped reader and decoded per its `SET` line: its words with Turkish - /// letters (ğ ı ş İ, and ç ö ü from Latin-1) are found, as stems and with a suffix. Before the - /// Turkish dictionary went through this reader it was read as UTF-8, which these bytes are not. - #[test] - fn a_turkish_dictionary_in_iso_8859_9_loads_from_disk() { - let dir = std::env::temp_dir().join(format!("wordcraft-hunspell-latin5-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let (aff, dic) = (dir.join("tr_TR.aff"), dir.join("tr_TR.dic")); - std::fs::write(&aff, b"SET ISO8859-9\nSFX 1 Y 2\nSFX 1 0 lar .\nSFX 1 0 \xfd .\nSFX 2 Y 1\nSFX 2 0 ler .\n").unwrap(); - // ağaç/1 (-lar, -ı), ışık, şişe/2 (-ler), İzmir, öğrenci/2, güneş. - let words: &[u8] = b"6\na\xf0a\xe7/1\n\xfd\xfe\xfdk\n\xfei\xfee/2\n\xddzmir\n\xf6\xf0renci/2\ng\xfcne\xfe\n"; - std::fs::write(&dic, words).unwrap(); - assert!(String::from_utf8(words.to_vec()).is_err(), "the .dic is not UTF-8"); - let d = crate::tr::load(&aff, &dic).unwrap(); - for w in ["ağaç", "ağaçlar", "ağaçı", "ışık", "şişe", "şişeler", "İzmir", "öğrenci", "öğrenciler", "güneş"] { - assert!(d.contains(w), "{w}"); - } - // The six letters ISO 8859-9 puts in place of ISO 8859-1's aren't read as those. - assert!(!d.contains("aðaç") && !d.contains("ýþýk") && !d.contains("Ýzmir")); - assert!(!d.contains("ağaçs") && !d.contains("ışıklar") && !d.contains("şişelar"), "only the rules' suffixes, on the stems that take them"); - let _ = std::fs::remove_dir_all(&dir); - } -} diff --git a/crates/proof/src/hyphen.rs b/crates/proof/src/hyphen.rs deleted file mode 100644 index b203ff1a2..000000000 --- a/crates/proof/src/hyphen.rs +++ /dev/null @@ -1,920 +0,0 @@ -//! Hyphenation, per proofing language ([`ProofLang`]). -//! -//! **English** (and languages without patterns of their own, as before languages were told -//! apart), lookup order for each word: -//! 1. the **dictionary** ([`Dictionary::en_us`]) — the public-domain Moby Hyphenator II word list -//! by Grady Ward (~160k words), stored compactly in `assets/hyphenation/en-us.dic`; simple -//! inflections (`'s`, `-s`, `-es`, `-ed`, `-d`) inherit the breaks of their stem; -//! 2. **Liang patterns** ([`Patterns::en_us`]) for words not in the dictionary — our own pattern -//! set, trained from the same list by DesignCraft's patgen (generated by DesignCraft's `hyphgen` example); -//! 3. a small syllable **heuristic** for words outside the pattern alphabet. -//! -//! **Polish**: the exception words, then the TeX Polish patterns ([`Patterns::pl`], by Hanna -//! Kołodziejska, Bogusław Jackowski and Marek Ryćko, MIT licence option), then a Polish syllable -//! heuristic for words with letters outside the Polish alphabet (`é`, `ü`…). -//! -//! **Bulgarian**: the Bulgarian syllable rules ([`crate::bg`]); no pattern file or word list. -//! -//! **German**: the TeX German patterns for the reformed spelling ([`Patterns::de`]), also for -//! Swiss text, whose `ss` the patterns split like any double consonant (`Stras-se`). -//! -//! Then the paragraph's limits ([`Limits`]) are applied. - -pub use crate::dict; - -pub use crate::patterns; - -use std::sync::OnceLock; - -pub use crate::lang::ProofLang; -pub use dict::Dictionary; -pub use patterns::Patterns; - -static EN_US_DIC: &[u8] = include_bytes!("../../../assets/hyphenation/en-us.dic"); -static EN_US_PAT: &str = include_str!("../../../assets/hyphenation/en-us.pat"); -/// hyph-utf8's `hyph-pl.tex`, unmodified (its licence header is in the file). -static PL_PAT: &str = include_str!("../../../assets/hyphenation/pl.pat"); -/// hyph-utf8's `hyph-pt.tex`, unmodified (its BSD 3-clause header is in the file). -static PT_PAT: &str = include_str!("../../../assets/hyphenation/pt.pat"); -/// hyph-utf8's `hyph-it.tex`, unmodified (offered under MIT or LPPL; WordCraft uses MIT, whose -/// notice is in the file's header). -static IT_PAT: &str = include_str!("../../../assets/hyphenation/it.pat"); -/// hyph-utf8's `hyph-de-1996.tex` (reformed spelling), unmodified (MIT; the notice is in the -/// file's header). -static DE_PAT: &str = include_str!("../../../assets/hyphenation/de.pat"); - -impl Dictionary { - /// The bundled US English dictionary (decoded on first use). - pub fn en_us() -> &'static Dictionary { - static D: OnceLock = OnceLock::new(); - D.get_or_init(|| { - if EN_US_DIC.is_empty() { - return Dictionary::default(); - } - Dictionary::from_bytes(EN_US_DIC).unwrap_or_else(|e| { - log::error!("{e}"); - Dictionary::default() - }) - }) - } -} - -impl Patterns { - /// The bundled US English patterns (parsed on first use). - pub fn en_us() -> &'static Patterns { - static P: OnceLock = OnceLock::new(); - P.get_or_init(|| Patterns::parse(EN_US_PAT)) - } - - /// The bundled Polish patterns and exception words (parsed on first use). - pub fn pl() -> &'static Patterns { - static P: OnceLock = OnceLock::new(); - P.get_or_init(|| Patterns::parse(PL_PAT)) - } - - /// The bundled Portuguese patterns and exception words (parsed on first use). - pub fn pt() -> &'static Patterns { - static P: OnceLock = OnceLock::new(); - P.get_or_init(|| Patterns::parse(PT_PAT)) - } - - /// The bundled Italian patterns (parsed on first use; the file has no exception words). - pub fn it() -> &'static Patterns { - static P: OnceLock = OnceLock::new(); - P.get_or_init(|| Patterns::parse(IT_PAT)) - } - - /// The bundled German patterns (parsed on first use; the file has no exception words). - pub fn de() -> &'static Patterns { - static P: OnceLock = OnceLock::new(); - P.get_or_init(|| Patterns::parse(DE_PAT)) - } -} - -/// Hyphenation limits from the paragraph's Hyphenation settings. -#[derive(Clone, Copy, Debug)] -pub struct Limits { - pub min_word: usize, - pub after_first: usize, - pub before_last: usize, - pub capitalized: bool, -} - -impl Default for Limits { - fn default() -> Self { - Limits { min_word: 5, after_first: 2, before_last: 2, capitalized: true } - } -} - -/// Where a word's break points came from. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Source { - Dictionary, - Patterns, - Heuristic, - /// A language's syllable rules (Bulgarian, [`crate::bg`]). - Rules, - None, -} - -fn is_apostrophe(c: char) -> bool { - matches!(c, '\'' | '’' | 'ʼ') -} - -fn is_word_char(c: char) -> bool { - c.is_alphabetic() || is_apostrophe(c) -} - -/// Unrestricted break points of one lowercase core (letters and `'` only, no leading/trailing `'`). -fn core_breaks(core: &[char], lang: ProofLang) -> (Vec, Source) { - let n = core.len(); - if n < 2 { - return (vec![], Source::None); - } - if lang == ProofLang::Pl { - return core_breaks_pl(core); - } - // Bulgarian words follow the Bulgarian syllable rules; Latin-script words in a Bulgarian run - // (`WordCraft`, `email`) are hyphenated as English, as in any other language. - if lang == ProofLang::Bg && core.iter().all(|&c| crate::bg::is_letter(c)) { - return (crate::bg::points(core), Source::Rules); - } - if lang == ProofLang::Tr && core.iter().all(|&c| crate::tr::hyphen::is_letter(c)) { - return (crate::tr::hyphen::points(core), Source::Rules); - } - match lang { - ProofLang::Pt => return core_breaks_patterns(Patterns::pt(), core), - ProofLang::It => return core_breaks_patterns(Patterns::it(), core), - ProofLang::De => return core_breaks_patterns(Patterns::de(), core), - _ => {} - } - if lang == ProofLang::Ru && core.iter().all(|&c| crate::ru::is_cyrillic(c)) { - return (crate::ru::hyphen::points(core), Source::Heuristic); - } - let dict = Dictionary::en_us(); - let s: String = core.iter().collect(); - let from_mask = |m: u64, len: usize| (1..len.min(64)).filter(|&i| m & (1 << i) != 0).collect::>(); - if let Some(m) = dict.get(&s) { - return (from_mask(m, n), Source::Dictionary); - } - // Inflections inherit the stem's breaks (the suffix adds no break of its own). - for suf in ["'s", "s", "es", "ed", "d"] { - if let Some(stem) = s.strip_suffix(suf) - && stem.chars().count() >= 2 - && let Some(m) = dict.get(stem) - { - return (from_mask(m, stem.chars().count()), Source::Dictionary); - } - } - let pats = Patterns::en_us(); - if !pats.is_empty() - && let Some(p) = pats.points(core) - { - return (p, Source::Patterns); - } - (heuristic_points(core), Source::Heuristic) -} - -/// Polish: exception words, then the patterns, then the Polish syllable heuristic. -fn core_breaks_pl(core: &[char]) -> (Vec, Source) { - let pats = Patterns::pl(); - let s: String = core.iter().collect(); - if let Some(p) = pats.exception(&s) { - return (p.to_vec(), Source::Dictionary); - } - if !pats.is_empty() - && let Some(p) = pats.points(core) - { - return (p, Source::Patterns); - } - (heuristic_points_pl(core), Source::Heuristic) -} - -/// [`core_breaks_pl`] for a language with bundled patterns (Portuguese, Italian, German): its -/// exception words, the patterns, then the generic heuristic. -fn core_breaks_patterns(pats: &Patterns, core: &[char]) -> (Vec, Source) { - let s: String = core.iter().collect(); - if let Some(p) = pats.exception(&s) { - return (p.to_vec(), Source::Dictionary); - } - if !pats.is_empty() - && let Some(p) = pats.points(core) - { - return (p, Source::Patterns); - } - (heuristic_points(core), Source::Heuristic) -} - -/// Unrestricted break points of an English `word` ([`word_breaks_in`] with [`ProofLang::En`]). -pub fn word_breaks(word: &str) -> (Vec, Source) { - word_breaks_in(word, ProofLang::En) -} - -/// Unrestricted break points of `word` in `lang` (char indices; a hyphen goes before that char), -/// handling case, apostrophes (`don't`, `’s`), surrounding punctuation and compounds (each letter -/// run is hyphenated separately). No break is ever placed next to an apostrophe or non-letter. -pub fn word_breaks_in(word: &str, lang: ProofLang) -> (Vec, Source) { - let chars: Vec = word.chars().collect(); - let mut out = Vec::new(); - let mut src = Source::None; - let mut i = 0; - while i < chars.len() { - if !is_word_char(chars[i]) { - i += 1; - continue; - } - let mut j = i; - while j < chars.len() && is_word_char(chars[j]) { - j += 1; - } - let (mut a, mut b) = (i, j); - while a < b && is_apostrophe(chars[a]) { - a += 1; - } - while b > a && is_apostrophe(chars[b - 1]) { - b -= 1; - } - let core: Vec = chars[a..b].iter().map(|&c| if is_apostrophe(c) { '\'' } else { c.to_lowercase().next().unwrap_or(c) }).collect(); - let (pts, s) = core_breaks(&core, lang); - if src == Source::None { - src = s; - } - out.extend(pts.into_iter().filter(|&p| p > 0 && p < core.len() && core[p - 1] != '\'' && core[p] != '\'').map(|p| a + p)); - i = j; - } - (out, src) -} - -/// Allowed hyphenation points in an English `word` ([`hyphen_points_in`] with [`ProofLang::En`]). -pub fn hyphen_points(word: &str, lim: &Limits) -> Vec { - hyphen_points_in(word, lim, ProofLang::En) -} - -/// Allowed hyphenation points in `word`, a word in `lang`, as char indices (a hyphen goes before -/// that char), with the paragraph limits applied to each letter run. -pub fn hyphen_points_in(word: &str, lim: &Limits, lang: ProofLang) -> Vec { - let chars: Vec = word.chars().collect(); - let (pts, _) = word_breaks_in(word, lang); - if pts.is_empty() { - return pts; - } - let min_before = lim.after_first.max(1); - let min_after = lim.before_last.max(1); - // Counted once, so each point's checks take constant time however long the word: letters and - // uppercase letters before each char, and the letter run (trimmed of apostrophes) of each char. - let (mut letters, mut upper) = (Vec::with_capacity(chars.len() + 1), Vec::with_capacity(chars.len() + 1)); - let (mut l, mut u) = (0usize, 0usize); - for c in &chars { - letters.push(l); - upper.push(u); - if c.is_alphabetic() { - l += 1; - u += usize::from(c.is_uppercase()); - } - } - letters.push(l); - upper.push(u); - let mut run_of: Vec<(usize, usize)> = vec![(0, 0); chars.len()]; - let mut i = 0; - while i < chars.len() { - let mut j = i; - while chars.get(j).is_some_and(|c| is_word_char(*c)) { - j += 1; - } - let (mut a, mut b) = (i, j); - while a < b && chars.get(a).is_some_and(|c| is_apostrophe(*c)) { - a += 1; - } - while b > a && chars.get(b - 1).is_some_and(|c| is_apostrophe(*c)) { - b -= 1; - } - for r in run_of.get_mut(i..j).into_iter().flatten() { - *r = (a, b); - } - i = j.max(i + 1); - } - let count = |v: &[usize], a: usize, b: usize| v.get(b).zip(v.get(a)).map_or(0, |(y, x)| y.saturating_sub(*x)); - pts.into_iter() - .filter(|&p| { - // A point lies between two letters of one run; the char after it gives the run. - let Some(&(a, b)) = run_of.get(p) else { return false }; - let n = count(&letters, a, b); - if n < lim.min_word.max(2) { - return false; - } - if n > 1 && count(&upper, a, b) == n { - return false; - } - if !lim.capitalized && chars.get(a).is_some_and(|c| c.is_uppercase()) { - return false; - } - count(&letters, a, p) >= min_before && count(&letters, p, b) >= min_after - }) - .collect() -} - -/// `word` with `-` at every hyphenation point (tests and diagnostics). -pub fn hyphenate_word(word: &str, lim: &Limits) -> String { - hyphenate_word_in(word, lim, ProofLang::En) -} - -/// `word`, a word in `lang`, with `-` at every hyphenation point (tests and diagnostics). -pub fn hyphenate_word_in(word: &str, lim: &Limits, lang: ProofLang) -> String { - let mut pts = hyphen_points_in(word, lim, lang); - pts.sort_unstable(); - // Walk the points alongside the chars rather than searching them for each char. - let mut next = pts.into_iter().peekable(); - let mut s = String::with_capacity(word.len() + 8); - for (i, ch) in word.chars().enumerate() { - if next.next_if_eq(&i).is_some() { - while next.next_if_eq(&i).is_some() {} - s.push('-'); - } - s.push(ch); - } - s -} - -// ---------- heuristic fallback (words outside the pattern alphabet) ---------- - -/// Letters with a diacritic that are consonants (`ł`, `ż`, `ç`, `ñ`…); any other non-ASCII letter -/// counts as a vowel (`é`, `ø`, `ą`…), and so do letters of other scripts, which keeps the -/// heuristic from breaking them. -const NON_ASCII_CONSONANTS: &str = "łńśćźżçčďđğģķļľĺňñņřŕšşșťţțžßŋðþ"; - -fn is_vowel(c: char) -> bool { - let l = c.to_lowercase().next().unwrap_or(c); - if l.is_ascii() { matches!(l, 'a' | 'e' | 'i' | 'o' | 'u' | 'y') } else { l.is_alphabetic() && !NON_ASCII_CONSONANTS.contains(l) } -} - -fn onset_pair(a: char, b: char) -> bool { - let (a, b) = (a.to_ascii_lowercase(), b.to_ascii_lowercase()); - matches!( - (a, b), - ('c' | 's' | 't' | 'p' | 'w' | 'g', 'h') - | ('b' | 'c' | 'd' | 'f' | 'g' | 'k' | 'p' | 't', 'r') - | ('b' | 'c' | 'f' | 'g' | 'k' | 'p' | 's', 'l') - | ('q', 'u') - | ('s', 't' | 'p' | 'c' | 'k') - ) -} - -/// Syllable heuristic: V-CV ("ty-po"), VC-CV ("hap-pen"), keeping onset clusters ("gra-phy"). -fn heuristic_points(c: &[char]) -> Vec { - let n = c.len(); - if n < 4 || !c.iter().all(|c| c.is_alphabetic()) { - return vec![]; - } - let v: Vec = c.iter().map(|&x| is_vowel(x)).collect(); - // Both parts need a sounded vowel: a trailing silent `e`, `ed` or `es` doesn't count. The part - // after `i` is `c[i..end]`, `end` before that ending; computed once, so each point is O(1). - let end = |i: usize| { - if n - i >= 2 && (c.ends_with(&['e', 'd']) || c.ends_with(&['e', 's'])) { - n - 2 - } else if c.ends_with(&['e']) { - n - 1 - } else { - n - } - }; - // UTF-8 bytes before each char, and the first vowel at or after each char. - let mut bytes = Vec::with_capacity(n + 1); - let mut acc = 0usize; - for ch in c { - bytes.push(acc); - acc += ch.len_utf8(); - } - bytes.push(acc); - let mut next_vowel = vec![n; n + 1]; - for i in (0..n).rev() { - next_vowel[i] = if v[i] { i } else { next_vowel[i + 1] }; - } - let mut out = vec![]; - let mut vowel_before = false; - for i in 1..n { - vowel_before |= v[i - 1]; - let ok = if v[i - 1] && !v[i] { - (i + 1 < n && v[i + 1]) || (i + 2 < n && onset_pair(c[i], c[i + 1]) && v[i + 2]) - } else if !v[i - 1] && !v[i] { - i >= 2 && v[i - 2] && i + 1 < n && v[i + 1] && !onset_pair(c[i - 1], c[i]) && c[i - 1] != c[i] || (c[i - 1] == c[i] && i >= 2 && v[i - 2]) - } else { - false - }; - let e = end(i); - if ok && next_vowel[i] < e && bytes[e] - bytes[i] > 1 && vowel_before { - out.push(i); - } - } - out -} - -/// Polish digraphs, one sound each: never split. -fn is_digraph_pl(a: char, b: char) -> bool { - matches!((a, b), ('c', 'h' | 'z') | ('d', 'z' | 'ź' | 'ż') | ('r' | 's', 'z')) -} - -/// Polish syllable heuristic, for words with letters outside the Polish pattern alphabet (`é`, -/// `ü`…): one break between each pair of syllable nuclei — after a leading sonorant (`kon-ty`, -/// `Pol-ska`) or inside a doubled consonant (`Pöp-pel`), else before the consonant cluster -/// (`sio-stra`). Digraphs (`ch`, `cz`, `dz`, `dź`, `dż`, `rz`, `sz`) stay whole, and an `i` that -/// only softens the consonant before a vowel (`nia`, `sio`) stays with that consonant. -fn heuristic_points_pl(c: &[char]) -> Vec { - let n = c.len(); - if n < 4 || !c.iter().all(|c| c.is_alphabetic()) { - return vec![]; - } - let v: Vec = c.iter().map(|&x| is_vowel(x)).collect(); - let at = |i: usize| v.get(i).copied().unwrap_or(false); - // A syllable nucleus: a vowel, except a softening `i` (after a consonant, before a vowel). - let nucleus: Vec = (0..n).map(|i| at(i) && !(c.get(i) == Some(&'i') && i > 0 && !at(i - 1) && at(i + 1))).collect(); - let nuclei: Vec = (0..n).filter(|&i| nucleus.get(i).copied().unwrap_or(false)).collect(); - let mut out = vec![]; - for pair in nuclei.windows(2) { - let [a, b] = pair else { continue }; - let (start, end) = (a + 1, *b); - if start >= end { - continue; // adjacent vowels (na-u-ka): no break inside a vowel pair here - } - // Consonant units of the cluster: digraphs and a softening `i` join their consonant. - let mut units: Vec = Vec::new(); - let mut i = start; - while i < end { - units.push(i); - i += if i + 1 < end && c.get(i).zip(c.get(i + 1)).is_some_and(|(x, y)| is_digraph_pl(*x, *y)) { 2 } else { 1 }; - if i < end && c.get(i) == Some(&'i') { - i += 1; - } - } - let sonorant = |i: usize| matches!(c.get(i), Some('l' | 'ł' | 'm' | 'n' | 'ń' | 'r' | 'j')); - let first_is_single = units.get(1).is_some_and(|&second| second == start + 1); - // A doubled consonant splits (`lek-ki`, `Pöp-pel`), as does a cluster after a sonorant. - let doubled = c.get(start).is_some_and(|x| c.get(start + 1) == Some(x)); - out.push(if units.len() >= 2 && first_is_single && (sonorant(start) || doubled) { start + 1 } else { start }); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn h(w: &str) -> String { - hyphenate_word(w, &Limits::default()) - } - - #[test] - fn bundled_data_loads() { - assert!(Dictionary::en_us().len() > 150_000, "{}", Dictionary::en_us().len()); - assert!(Patterns::en_us().len() > 1000, "{}", Patterns::en_us().len()); - } - - #[test] - fn dictionary_words() { - assert_eq!(h("happen"), "hap-pen"); - assert_eq!(h("typography"), "ty-pog-ra-phy"); - assert_eq!(h("composition"), "com-po-si-tion"); - assert_eq!(h("hyphenation"), "hy-phen-a-tion"); - assert_eq!(h("international"), "in-ter-na-tion-al"); - assert_eq!(h("cat"), "cat"); - assert!(!h("changed").contains('-')); - assert_eq!(h("NASA"), "NASA"); - assert_eq!(word_breaks("publication").1, Source::Dictionary); - } - - #[test] - fn case_apostrophes_and_punctuation() { - assert_eq!(h("Typography"), "Ty-pog-ra-phy"); - assert_eq!(h("TYPOGRAPHY"), "TYPOGRAPHY"); - assert_eq!(h("typography,"), "ty-pog-ra-phy,"); - assert_eq!(h("“typography”"), "“ty-pog-ra-phy”"); - assert_eq!(h("typography's"), "ty-pog-ra-phy's"); - assert_eq!(h("typography’s"), "ty-pog-ra-phy’s"); - assert_eq!(h("'typography'"), "'ty-pog-ra-phy'"); - // Compounds: each part separately, never next to the hyphen. - assert_eq!(h("typography-composition"), "ty-pog-ra-phy-com-po-si-tion"); - } - - #[test] - fn inflections_inherit_the_stem() { - assert_eq!(h("ripples"), "rip-ples"); - assert_eq!(h("gradients"), "gra-di-ents"); - assert_eq!(h("restrained"), "re-strained"); - } - - #[test] - fn patterns_cover_unknown_words() { - // Not in the word list: the trained patterns decide. - let (pts, src) = word_breaks("hyperlocalization"); - assert_eq!(src, Source::Patterns); - assert!(pts.len() >= 4, "{}", h("hyperlocalization")); - let (_, src) = word_breaks("zzyzx"); - assert_ne!(src, Source::Dictionary); - } - - #[test] - fn limits_are_respected() { - let lim = Limits { min_word: 5, after_first: 3, before_last: 3, capitalized: false }; - for w in ["typography", "hyphenation", "justification", "Paragraph", "beautiful"] { - for p in hyphen_points(w, &lim) { - assert!(p >= 3 && p <= w.chars().count() - 3, "{w} {p}"); - } - } - assert!(hyphen_points("Paragraph", &lim).is_empty()); - let lim = Limits { min_word: 12, ..Limits::default() }; - assert!(hyphen_points("typography", &lim).is_empty()); - } - - /// The CPU time `f` takes on this thread, in seconds. On Linux it is the scheduler's count of the - /// time the thread actually ran, so the time it waits while other work on a busy machine has the - /// CPU is left out; elsewhere, wall-clock time. - fn cpu_cost(f: &mut impl FnMut()) -> f64 { - let ran = || { - let s = std::fs::read_to_string("/proc/thread-self/schedstat").ok()?; - s.split_whitespace().next()?.parse::().ok() - }; - let (t, before) = (std::time::Instant::now(), ran()); - f(); - match (before, ran()) { - (Some(a), Some(b)) => b.saturating_sub(a) as f64 * 1e-9, - _ => t.elapsed().as_secs_f64(), - } - } - - /// How much more `big` costs than `small`, for `big` an input `scale` times the size of `small`'s: - /// (small, big, big / small), ~`scale` when linear and ~`scale`² when quadratic. Each is the - /// cheapest of several runs after a warm-up, the runs alternating so a busy spell slows both sizes - /// alike; `small` is timed `scale` times over per run, so both timings are long against the clock's - /// resolution. - fn cost_ratio(scale: usize, mut small: impl FnMut(), mut big: impl FnMut()) -> (f64, f64, f64) { - small(); - big(); - let mut small_scaled = || (0..scale).for_each(|_| small()); - let (mut t1, mut tk) = (f64::INFINITY, f64::INFINITY); - for _ in 0..5 { - t1 = t1.min(cpu_cost(&mut small_scaled) / scale as f64); - tk = tk.min(cpu_cost(&mut big)); - } - (t1, tk, tk / t1.max(1e-9)) - } - - /// `hyphen_points`' limit checks as first written (a rescan of the letter run per point): the - /// reference for the linear version. - fn hyphen_points_ref(word: &str, lim: &Limits) -> Vec { - let chars: Vec = word.chars().collect(); - let (pts, _) = word_breaks(word); - let min_before = lim.after_first.max(1); - let min_after = lim.before_last.max(1); - let run_of = |p: usize| { - let mut a = p; - while a > 0 && is_word_char(chars[a - 1]) { - a -= 1; - } - let mut b = p; - while b < chars.len() && is_word_char(chars[b]) { - b += 1; - } - while a < b && is_apostrophe(chars[a]) { - a += 1; - } - while b > a && is_apostrophe(chars[b - 1]) { - b -= 1; - } - (a, b) - }; - pts.into_iter() - .filter(|&p| { - let (a, b) = run_of(p); - let run = &chars[a..b]; - let letters = run.iter().filter(|c| c.is_alphabetic()).count(); - if letters < lim.min_word.max(2) { - return false; - } - if letters > 1 && run.iter().filter(|c| c.is_alphabetic()).all(|c| c.is_uppercase()) { - return false; - } - if !lim.capitalized && run[0].is_uppercase() { - return false; - } - let before = chars[a..p].iter().filter(|c| c.is_alphabetic()).count(); - let after = chars[p..b].iter().filter(|c| c.is_alphabetic()).count(); - before >= min_before && after >= min_after - }) - .collect() - } - - /// `heuristic_points` as first written (the right part rebuilt per point): the reference. - fn heuristic_ref(c: &[char]) -> Vec { - let n = c.len(); - if n < 4 || !c.iter().all(|c| c.is_alphabetic()) { - return vec![]; - } - let v: Vec = c.iter().map(|&x| is_vowel(x)).collect(); - let mut out = vec![]; - for i in 1..n { - let ok = if v[i - 1] && !v[i] { - (i + 1 < n && v[i + 1]) || (i + 2 < n && onset_pair(c[i], c[i + 1]) && v[i + 2]) - } else if !v[i - 1] && !v[i] { - i >= 2 && v[i - 2] && i + 1 < n && v[i + 1] && !onset_pair(c[i - 1], c[i]) && c[i - 1] != c[i] - || (c[i - 1] == c[i] && i >= 2 && v[i - 2]) - } else { - false - }; - let right: String = c[i..].iter().collect(); - let core = right.strip_suffix("ed").or_else(|| right.strip_suffix("es")).or_else(|| right.strip_suffix('e')).unwrap_or(&right); - if ok && core.chars().any(is_vowel) && core.len() > 1 && v[..i].iter().any(|x| *x) { - out.push(i); - } - } - out - } - - #[test] - fn linear_limits_and_heuristic_match_the_reference() { - let long = "hyphenation".repeat(40); - let words = [ - "", - "x", - "''", - "a'b'c", - "cat", - "happen", - "hyphenation", - "Typography's", - "TYPOGRAPHY", - "Typography", - "typography’s", - "'typography'", - "“typography”,", - "typography-composition", - "don't", - "rock'n'roll", - "naïveté", - "crème-brûlée", - "Ünïcödé", - "ÉTUDE", - "strengths", - "ee", - "bed", - "hopped", - "rhythmed", - "aeiouy", - "xxyyzz", - "bookkeeper", - "ʼtwas", - "mid-1990s", - "twenty-one", - "école", - "Ålesund", - "smørrebrød", - "hopes", - "hoped", - "taed", - "teeed", - &long, - ]; - for w in words { - for min_word in [2, 5, 7] { - for (after_first, before_last) in [(1, 1), (2, 2), (3, 2), (2, 4)] { - for capitalized in [true, false] { - let lim = Limits { min_word, after_first, before_last, capitalized }; - assert_eq!(hyphen_points(w, &lim), hyphen_points_ref(w, &lim), "{w:?} {lim:?}"); - } - } - } - let c: Vec = w.to_lowercase().chars().collect(); - assert_eq!(heuristic_points(&c), heuristic_ref(&c), "{w:?}"); - } - } - - #[test] - fn hyphenating_a_long_word_is_linear() { - // A word of n letters against one of 8n: linear ~8x the cost, the old rescans ~64x. One in - // the pattern alphabet, one outside it (the syllable heuristic). - let pattern = |k: usize| "hyphenation".repeat(k); - let heuristic = |k: usize| "smørrebrød".repeat(k); - assert_eq!(word_breaks(&heuristic(2)).1, Source::Heuristic); - // Each timing repeats the call so the shorter one is milliseconds, well above timer noise. - let ratios: Vec<(String, f64)> = [(pattern as fn(usize) -> String, 20), (heuristic, 40)] - .into_iter() - .map(|(word, repeat)| { - let (small, big) = (word(400), word(8 * 400)); - let time = |w: &str| (0..repeat).for_each(|_| drop(hyphen_points(w, &Limits::default()))); - let (t1, t8, ratio) = cost_ratio(8, || time(&small), || time(&big)); - let what = format!("{}…: t(n) {t1:.4} s, t(8n) {t8:.4} s, ratio {ratio:.1}", word(1).chars().take(10).collect::()); - eprintln!("{what}"); - (what, ratio) - }) - .collect(); - for (what, ratio) in ratios { - assert!(ratio < 24.0, "8x the letters, not linear: {what}"); - } - } - - #[test] - fn russian_hyphenation_and_hyphenated_output_are_linear() { - // A Russian word breaks at every syllable: each break used to be checked against all the - // earlier ones (#3657), and the hyphenated output searched every break for each char. - let word = |k: usize| "мама".repeat(k); - assert_eq!(word_breaks_in(&word(2), ProofLang::Ru).0, [2, 4, 6]); - assert_eq!(hyphenate_word_in(&word(2), &Limits::default(), ProofLang::Ru), "ма-ма-ма-ма"); - // The breaks' rescan is a cheap one per break: it takes longer words to stand out. - for (what, k, repeat, f) in [ - ("points", 1000, 3, (|w: &str| drop(hyphen_points_in(w, &Limits::default(), ProofLang::Ru))) as fn(&str)), - ("hyphenated", 400, 20, |w: &str| drop(hyphenate_word_in(w, &Limits::default(), ProofLang::Ru))), - ] { - let (small, big) = (word(k), word(8 * k)); - let time = |w: &str| (0..repeat).for_each(|_| f(w)); - let (t1, t8, ratio) = cost_ratio(8, || time(&small), || time(&big)); - eprintln!("{what}: t(n) {t1:.4} s, t(8n) {t8:.4} s, ratio {ratio:.1}"); - assert!(ratio < 24.0, "8x the letters, not linear: {what} {ratio:.1}"); - } - } - - #[test] - fn heuristic_fallback() { - let c: Vec = "happen".chars().collect(); - assert_eq!(heuristic_points(&c), vec![3]); - } - - #[test] - fn heuristic_knows_consonants_with_diacritics() { - // `ł`, `ż`, `ç` are consonants (every non-ASCII letter used to count as a vowel). - assert!(!is_vowel('ł') && !is_vowel('Ż') && !is_vowel('ç') && is_vowel('é') && is_vowel('ą') && is_vowel('ø')); - let c: Vec = "façade".chars().collect(); - assert_eq!(heuristic_points(&c), vec![2]); - } - - fn pl(w: &str) -> String { - hyphenate_word_in(w, &Limits::default(), ProofLang::Pl) - } - - #[test] - fn polish_patterns_load() { - let p = Patterns::pl(); - assert!(p.len() > 4000, "{}", p.len()); - assert_eq!(p.exception_count(), 20); - assert_eq!(p.exception("nigdy"), Some(&[2][..])); - } - - fn pt(w: &str) -> String { - hyphenate_word_in(w, &Limits::default(), ProofLang::Pt) - } - - fn it(w: &str) -> String { - hyphenate_word_in(w, &Limits::default(), ProofLang::It) - } - - #[test] - fn italian_patterns_load() { - let p = Patterns::it(); - assert!(p.len() > 300, "{}", p.len()); - // The file has no exception words. - assert_eq!(p.exception_count(), 0); - } - - #[test] - fn italian_words_hyphenate_by_italian_rules() { - for (w, want) in [ - ("famiglia", "fa-mi-glia"), - ("finestra", "fi-ne-stra"), - ("bicicletta", "bi-ci-clet-ta"), - ("passeggiata", "pas-seg-gia-ta"), - ("caffè", "caf-fè"), - ("tavolo", "ta-vo-lo"), - ("bambino", "bam-bi-no"), - ("scuola", "scuo-la"), - ("notte", "not-te"), - ("giorno", "gior-no"), - ("acqua", "ac-qua"), - // Under five letters (`Limits::min_word`): no break. - ("casa", "casa"), - ("pane", "pane"), - ("libro", "li-bro"), - ("re", "re"), - ("più", "più"), - ] { - assert_eq!(it(w), want, "{w}"); - } - assert_eq!(word_breaks_in("famiglia", ProofLang::It).1, Source::Patterns); - } - - #[test] - fn portuguese_patterns_load() { - let p = Patterns::pt(); - assert!(p.len() > 300, "{}", p.len()); - // `hard-ware`, `soft-ware` — and the file's last pattern line, a bare `1-`, which the - // reader counts as a (never-matching) exception because it contains a hyphen. - assert_eq!(p.exception_count(), 3); - assert_eq!(p.exception("hardware"), Some(&[4][..])); - assert_eq!(p.exception("software"), Some(&[4][..])); - } - - #[test] - fn portuguese_words_hyphenate_by_portuguese_rules() { - for (w, want) in [ - ("problema", "pro-ble-ma"), - ("coração", "co-ra-ção"), - ("paralelepípedo", "pa-ra-le-le-pí-pe-do"), - ("computador", "com-pu-ta-dor"), - ("escrever", "es-cre-ver"), - ("saudade", "sau-da-de"), - ("extraordinário", "ex-tra-or-di-ná-rio"), - ("automático", "au-to-má-ti-co"), - // One syllable: no break. - ("flor", "flor"), - ("bem", "bem"), - ] { - assert_eq!(pt(w), want, "{w}"); - } - } - - #[test] - fn polish_words_hyphenate_by_polish_rules() { - for (w, want) in [ - ("konstytucja", "kon-sty-tu-cja"), - ("konstytucyjnego", "kon-sty-tu-cyj-ne-go"), - ("Rzeczpospolita", "Rzecz-po-spo-li-ta"), - ("przedsiębiorstwo", "przed-się-bior-stwo"), - ("pięćdziesięciu", "pięć-dzie-się-ciu"), - ("dziewczyna", "dziew-czy-na"), - ("książka", "książ-ka"), - ("Warszawa", "War-sza-wa"), - ("szczęście", "szczę-ście"), - ("rzeczywistość", "rze-czy-wi-stość"), - ("najpiękniejszy", "naj-pięk-niej-szy"), - ("wyobraźnia", "wy-obraź-nia"), - ("gżegżółka", "gżeg-żół-ka"), - // One syllable, or a single letter left over: no break. - ("chrząszcz", "chrząszcz"), - ("źdźbło", "źdźbło"), - ("obiad", "obiad"), - // The pattern file's exception words. - ("nigdy", "ni-gdy"), - ("przynajmniej", "przy-naj-mniej"), - ("skądinąd", "skąd-inąd"), - // Compounds: each part, never next to the hyphen. - ("biało-czerwony", "bia-ło-czer-wo-ny"), - ] { - assert_eq!(pl(w), want, "{w}"); - } - assert_eq!(word_breaks_in("nigdy", ProofLang::Pl).1, Source::Dictionary); - assert_eq!(word_breaks_in("konstytucja", ProofLang::Pl).1, Source::Patterns); - } - - #[test] - fn polish_heuristic_for_foreign_letters() { - // Letters outside the Polish alphabet: the Polish syllable heuristic, digraphs kept whole. - assert_eq!(word_breaks_in("Pöppelmann", ProofLang::Pl).1, Source::Heuristic); - assert_eq!(pl("Pöppelmann"), "Pöp-pel-mann"); - assert_eq!(pl("Müllerowi"), "Mül-le-ro-wi"); - let c: Vec = "kawiarnié".chars().collect(); - assert_eq!(heuristic_points_pl(&c), vec![2, 6], "ka-wiar-nié: `wi` and `ni` soften, `rn` splits after the sonorant"); - let c: Vec = "szczérze".chars().collect(); - assert_eq!(heuristic_points_pl(&c), vec![5], "szczé-rze: `rz` stays whole"); - } - - #[test] - fn languages_without_patterns_keep_the_english_ones() { - for w in ["hyphenation", "typography", "internationalization"] { - for lang in [ProofLang::Other, ProofLang::Cs, ProofLang::Id, ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy] { - assert_eq!(hyphen_points_in(w, &Limits::default(), lang), hyphen_points(w, &Limits::default()), "{w}"); - } - } - } - - #[test] - fn german_patterns_load() { - let p = Patterns::de(); - assert_eq!(p.len(), 36_709); - // The file has no exception words, and its `\message{…}` line adds none. - assert_eq!(p.exception_count(), 0); - } - - #[test] - fn german_words_follow_the_german_patterns() { - let de = |w: &str| hyphenate_word_in(w, &Limits::default(), ProofLang::De); - assert_eq!(de("Silbentrennung"), "Sil-ben-tren-nung"); - assert_eq!(de("unterliegende"), "un-ter-lie-gen-de"); - assert_eq!(de("öffentlich-rechtlicher"), "öf-fent-lich-recht-li-cher"); - assert_eq!(de("Begründung"), "Be-grün-dung"); - assert_eq!(de("Zucker"), "Zu-cker"); - // Swiss spelling: `ss` splits like any double consonant. - assert_eq!(de("Strasse"), "Stras-se"); - assert_eq!(word_breaks_in("Unterlagen", ProofLang::De).1, Source::Patterns); - } - - #[test] - fn bulgarian_words_follow_the_bulgarian_rules() { - let bg = |w: &str| hyphenate_word_in(w, &Limits::default(), ProofLang::Bg); - assert_eq!(bg("сричкопренасяне"), "срич-ко-пре-на-ся-не"); - assert_eq!(bg("Таблица"), "Та-бли-ца"); - // A single letter is never left on a line (the paragraph limits): България, not Бъл-га-ри-я. - assert_eq!(bg("България"), "Бъл-га-рия"); - assert_eq!(bg("„шофьор“,"), "„шо-фьор“,"); - assert_eq!(word_breaks_in("таблица", ProofLang::Bg).1, Source::Rules); - // Latin words in a Bulgarian run are hyphenated as English. - for w in ["publication", "WordCraft"] { - assert_eq!(hyphen_points_in(w, &Limits::default(), ProofLang::Bg), hyphen_points(w, &Limits::default()), "{w}"); - } - } -} diff --git a/crates/proof/src/id/grammar.rs b/crates/proof/src/id/grammar.rs deleted file mode 100644 index 72ca10c91..000000000 --- a/crates/proof/src/id/grammar.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Generic repeated-word and spacing checks with Indonesian messages. No Indonesian-specific -//! grammar rules are inferred. A word written twice with a space is flagged; since standard -//! Indonesian spelling (PUEBI) writes reduplication with a hyphen (`anak-anak`), the hyphenated -//! form is offered first, then the single word. - -use crate::{Issue, IssueKind, ProofLang, words}; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::Id } -} - -pub fn check(text: &str) -> Vec { - let mut issues = Vec::new(); - repeated_words(text, &words(text), &mut issues); - spaces(text, &mut issues); - issues.sort_by_key(|i| (i.start, i.end)); - issues -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() && gap.chars().all(char::is_whitespace) && w0.chars().all(char::is_alphabetic) && w0.to_lowercase() == w1.to_lowercase() { - v.push(issue(*a0, *b1, format!("Kata berulang: “{w1}”"), vec![format!("{w0}-{w1}"), w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - // A ratio/score keeps the spaces in digit-space-colon-space-digit. - let ratio = p == ':' - && text.get(..i).and_then(|before| before.chars().next_back()).is_some_and(|c| c.is_ascii_digit()) - && after == Some(' ') - && it.next().is_some_and(|c| c.is_ascii_digit()); - let flag = !ratio - && match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Spasi sebelum tanda baca".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Spasi ganda".into(), vec![" ".into()])); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn repeated_words_and_spaces_have_indonesian_messages() { - let text = "Anak anak , sekolah."; - let issues = check(text); - assert_eq!(issues.len(), 3, "{issues:?}"); - assert_eq!(issues[0].message, "Kata berulang: “anak”"); - assert_eq!(issues[0].suggestions, ["Anak-anak", "Anak"]); - assert!(issues.iter().any(|i| i.message == "Spasi sebelum tanda baca" && &text[i.start..i.end] == " ,")); - assert!(issues.iter().any(|i| i.message == "Spasi ganda")); - assert!(issues.iter().all(|i| i.lang == ProofLang::Id && i.kind == IssueKind::Grammar)); - for text in ["anak-anak bermain", "rumah. Sekolah", "Dr. Budi", "Anak, anak!", "Senyum :) Angka .5 dan titik ...", "kupu-kupu"] { - assert!(check(text).is_empty(), "{text}: {:?}", check(text)); - } - } - - #[test] - fn ratios_and_scores_keep_their_spaces() { - for text in ["3 : 1", "Skor 2 : 1.", "Rasio 1 : 100."] { - assert!(check(text).is_empty(), "{text}: {:?}", check(text)); - } - for text in ["Kata : lain", "3 : kata", "3 :"] { - assert!(check(text).iter().any(|i| i.message == "Spasi sebelum tanda baca"), "{text}"); - } - } - - proptest::proptest! { - #[test] - fn never_panics_and_ranges_are_valid(s in "\\PC{0,80}") { - for issue in check(&s) { - proptest::prop_assert!(issue.start <= issue.end && issue.end <= s.len()); - proptest::prop_assert!(s.is_char_boundary(issue.start) && s.is_char_boundary(issue.end)); - } - } - } -} diff --git a/crates/proof/src/id/mod.rs b/crates/proof/src/id/mod.rs deleted file mode 100644 index 04248d638..000000000 --- a/crates/proof/src/id/mod.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! Indonesian proofing using the installed Hunspell `id_ID` dictionary (never bundled; Debian and -//! Ubuntu ship it as `hunspell-id`, LibreOffice as its `dict-id` extension). Suggestions use the -//! dictionary's own MAP/REP/TRY/KEY tables. Grammar is limited to generic repeated-word and -//! spacing checks with Indonesian messages; hyphenation keeps the English fallback (no Indonesian -//! patterns are bundled). - -pub mod grammar; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of an Indonesian Hunspell dictionary (`in` is the old ISO 639 code, still used by -/// Java locales). -const NAMES: &[&str] = &["id_ID", "id-ID", "id", "in_ID"]; - -/// The Indonesian dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Indonesian"); - -/// The Indonesian dictionary: the installed Hunspell `id_ID` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Indonesian spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Indonesian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is an Indonesian dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Clitics PUEBI joins with a hyphen to a capitalized word, an abbreviation or a number -/// (`se-Indonesia`, `KTP-nya`, `Jakarta-lah`); they aren't dictionary words on their own. -const CLITICS: &[&str] = &["se", "nya", "ku", "mu", "lah", "kah", "pun"]; - -/// Is `word` spelled correctly in Indonesian? Numbers, single letters, short words in capitals -/// (acronyms: `DPR`, `KTP`), words with digits and words in other scripts are accepted, and so -/// is every word when no Indonesian dictionary is installed. A hyphenated word is correct when -/// the dictionary has it whole (`kupu-kupu`, `sayur-mayur`) or has each part (`anak-anak`, -/// `rumah-rumah`); a hyphenated clitic (`KTP-nya`, `se-Indonesia`) counts as known. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - if w.contains('-') { - return known(d, w) - || w.split('-').all(|p| { - p.chars().count() <= 1 - || CLITICS.iter().any(|c| p.eq_ignore_ascii_case(c)) - || (opts.ignore_uppercase && p.chars().all(|c| !c.is_lowercase()) && p.chars().count() <= 6) - || known(d, p) - }); - } - known(d, w) -} - -/// Is one word in the dictionary or the user dictionary? -fn known(d: &AffixDict, w: &str) -> bool { - d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn dictionary_lookup_accepts_indonesian_file_names() { - let dir = std::env::temp_dir().join(format!("wordcraft-id-aliases-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - for name in ["id_ID", "id-ID", "id", "in_ID"] { - std::fs::write(dir.join(format!("{name}.aff")), test_dict::AFF).unwrap(); - std::fs::write(dir.join(format!("{name}.dic")), test_dict::DIC).unwrap(); - let (aff, dic) = crate::hunspell::find_in(&dir, NAMES).unwrap(); - let d = crate::hunspell::load(&aff, &dic).unwrap(); - assert!(is_correct_with(&d, "rumah")); - std::fs::remove_file(aff).unwrap(); - std::fs::remove_file(dic).unwrap(); - } - std::fs::remove_dir(dir).unwrap(); - } - - #[test] - fn indonesian_words_affixes_case_and_hyphens() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert_eq!(d.len(), 20); - assert!(d.rule_count() > 0); - for w in [ - "rumah", - "makan", - "makanan", - "dimakan", - "membaca", - "bacaan", - "dibaca", - "Jakarta", - "Rumah", - "SEKOLAH", - "anak-anak", - "rumah-rumah", - "kupu-kupu", - "KTP-nya", - "se-Indonesia", - "Jakarta-lah", - "DPR", - ] { - assert!(is_correct(w), "{w}"); - } - for w in ["rumha", "makanann", "skolah", "jakarta", "kupu", "anak-anka", "mAkan", "bukunya-xyz"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn forbidden_words_are_rejected_in_every_case() { - test_dict::install(); - for w in ["zqterlarang", "Zqterlarang", "ZQTERLARANG"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn missing_dictionary_and_ignored_inputs_are_quiet() { - let empty = AffixDict::default(); - assert!(is_correct_with(&empty, "rumha")); - assert!(install_dictionary("FLAG long\n", "1\nrumah\n").is_err()); - let d = AffixDict::from_hunspell(test_dict::AFF, test_dict::DIC, &[]).unwrap(); - for w in ["", "a", "2026", "RI", "ke-2", "日本語", "-", "--"] { - assert!(is_correct_with(&d, w), "{w}"); - } - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("zqkatasendiri")); - crate::add_word("Zqkatasendiri"); - assert!(is_correct("zqkatasendiri")); - } - - #[test] - fn indonesian_dispatch_messages_options_and_mixed_spans() { - test_dict::install(); - assert!(crate::has_dictionary(Some("id-ID"))); - assert!(crate::is_correct_for("rumah", Some("ind"))); - let text = "Rumah sekolha"; - let issues = crate::check_text(text, &[(0..text.len(), crate::ProofLang::Id)]); - assert_eq!(issues.len(), 1, "{issues:?}"); - assert_eq!(&text[issues[0].start..issues[0].end], "sekolha"); - assert_eq!(issues[0].message, "Kemungkinan salah eja"); - assert_eq!(issues[0].lang, crate::ProofLang::Id); - assert!(crate::suggest_in("sekolha", 8, crate::ProofLang::Id).contains(&"sekolah".into())); - let text = "Rumah this is is wrong"; - let issues = crate::check_text(text, &[(0..5, crate::ProofLang::Id), (6..text.len(), crate::ProofLang::En)]); - assert!(issues.iter().any(|i| i.lang == crate::ProofLang::En && i.message.starts_with("Repeated word"))); - assert!(issues.iter().all(|i| i.start >= 6)); - let opts = crate::Options { ignore_uppercase: false, ..Default::default() }; - assert!(!crate::is_correct_in_with("ZQTERLARANG", crate::ProofLang::Id, &opts)); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Indonesian checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_indonesian_check() { - test_dict::install(); - let lang = crate::ProofLang::Id; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("RUMHA", lang, &defaults), "short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("RUMHA", lang, &check_caps)); - assert!(crate::is_correct_in_with("RUMAH", lang, &check_caps)); - assert!(crate::is_correct_in_with("RUMHA-nya", lang, &defaults)); - assert!(!crate::is_correct_in_with("RUMHA-nya", lang, &check_caps), "a part in capitals is checked too"); - assert!(crate::is_correct_in_with("rumha2", lang, &defaults)); - assert!(!crate::is_correct_in_with("rumha2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("RUMHA"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/id/suggest.rs b/crates/proof/src/id/suggest.rs deleted file mode 100644 index b42efde98..000000000 --- a/crates/proof/src/id/suggest.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Indonesian suggestions: the shared ranking ([`crate::affix_suggest`]: case, MAP, REP, missing -//! spaces, KEY, TRY edits and a stem scan); every table comes from the installed `id_ID` -//! dictionary, so nothing Indonesian is hard-coded here. - -use crate::affix::AffixDict; - -/// Spelling suggestions for an Indonesian word, best first, at most `max`, in the word's case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &crate::affix_suggest::Tuning::DEFAULT) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn indonesian_suggestions_use_runtime_tables_and_case() { - crate::id::test_dict::install(); - for (word, want) in [ - ("rumha", "rumah"), - ("sekolha", "sekolah"), - ("Sekola", "Sekolah"), - ("jakarta", "Jakarta"), - ("makanna", "makanan"), - ("djalan", "jalan"), - ("dsn", "dan"), - ("sayadan", "saya dan"), - ("membcaa", "membaca"), - ] { - assert!(suggest(word, 8).contains(&want.into()), "{word}: {:?}", suggest(word, 8)); - } - } - - #[test] - fn suggestions_stay_bounded_and_skip_forbidden_words() { - crate::id::test_dict::install(); - for w in ["", "dua kata", &"a".repeat(41)] { - assert!(suggest(w, 4).is_empty()); - } - assert!(suggest("rumha", 0).is_empty()); - assert!(suggest_in(&AffixDict::default(), "rumha", 4).is_empty()); - assert!(suggest("rumah", 4).len() <= 4); - assert!(!suggest("rumah", 4).contains(&"rumah".into())); - assert!(!suggest("zqterlarag", 8).contains(&"zqterlarang".into())); - } - - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(suggest_in); - } -} diff --git a/crates/proof/src/id/test_dict.rs b/crates/proof/src/id/test_dict.rs deleted file mode 100644 index 23c8b97ef..000000000 --- a/crates/proof/src/id/test_dict.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Hand-written synthetic Indonesian test data. No words, rules or suggestion tables were copied -//! from a system dictionary. This is a small fixture, not a redistributable Indonesian dictionary. - -pub const AFF: &str = "SET UTF-8 -FLAG UTF-8 -FORBIDDENWORD F -TRY aniekrumgtslhbdopy -KEY qw|as|zx -MAP 1 -MAP eé -REP 2 -REP oe u -REP dj j -SFX A Y 1 # synthetic -an -SFX A 0 an . -PFX D Y 1 -PFX D 0 di . -PFX M Y 1 -PFX M 0 mem b -"; - -pub const DIC: &str = "20 -rumah -makan/AD -baca/ADM -sekolah -anak -buku -kupu-kupu -Jakarta -Indonesia -saya -kamu -dan -yang -ini -itu -pergi -jalan/A -besar -kecil -zqterlarang/F -"; - -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} diff --git a/crates/proof/src/it/grammar.rs b/crates/proof/src/it/grammar.rs deleted file mode 100644 index 463029369..000000000 --- a/crates/proof/src/it/grammar.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Italian grammar and punctuation checks: cheap, rule-based and tuned to stay quiet when -//! unsure (a missed mistake costs less than a wrong mark on correct text). Messages are in -//! Italian. -//! -//! - **Repeated word** (`la la`), except words Italian doubles on purpose (`piano piano`, -//! `sì sì`, interjections). -//! - **Space before punctuation** (`bene ,`) and **double spaces**; emoticons (` :)`) and -//! ellipses are left alone. -//! - **Capital letter at the start of a sentence**, knowing Italian abbreviations (`ecc.`, -//! `dott.`, `prof.`, `sig.`), initials, ordinal numbers (`1. posto`) and ellipses. -//! - **Doubled punctuation** (`,,`, `..`, `;;`). -//! - **`po'`**: `un pò` and `un po` → `un po'` (a truncation of `poco`: apostrophe, no accent). -//! - **`qual è`**: `qual'è`, `qual'era` → `qual è`, `qual era` (a truncation, never elided). -//! - **`È`**: `E'` written for the verb → `È`. -//! - **`un` / `un'`**: the apostrophe only before a feminine word. `un'altro`, `un'anno` → -//! `un altro`, `un anno` (a masculine word in `-o`; `auto` and `eco` are feminine), and -//! `un altra`, `un amica` → `un'altra`, `un'amica` (a word in `-a`, except the masculine and -//! common-gender ones: `-ista`, `-ita`, `-ota`, `-cida`, `-ma`, `-eta`, `atleta`, `acrobata`…). -//! The same for `nessun`, `ciascun`, `qualcun` and `alcun`. -//! -//! No comma rule and no English rule (such as `a`/`an`) applies to Italian text. - -use crate::{Issue, IssueKind, ProofLang, words}; - -/// Words Italian repeats on purpose: emphatic doubling, interjections, onomatopoeia. -const ALLOWED_REPEATS: &[&str] = &[ - "sì", "no", "piano", "bene", "male", "ecco", "oh", "ah", "eh", "mh", "bah", "ehi", "urrà", "evviva", "ciao", "addio", "adagio", "forte", "lento", - "chiaro", "pian", "poco", "quasi", "presto", "subito", "zitto", "via", "così", "tanto", "molto", "pure", "dai", "su", "avanti", "basta", "aiuto", -]; - -/// Indefinite articles (and the like) that elide to `un'` before a feminine word only. -const UN_FORMS: &[&str] = &["un", "nessun", "ciascun", "qualcun", "alcun"]; - -/// Feminine words in `-o` that start with a vowel (`un'auto`, `un'eco`). -const FEMININE_IN_O: &[&str] = &["auto", "autoradio", "eco", "ecografo"]; - -/// Endings of masculine or common-gender words in `-a` (`un artista`, `un idiota`, `un enigma`). -const MASCULINE_A_ENDINGS: &[&str] = &["ista", "ita", "ota", "cida", "ma", "eta", "nauta", "iatra", "arca"]; - -/// Words in `-a` after `un` that aren't feminine nouns: masculine or common-gender ones that fit -/// no ending above, and verbs and pronouns (`un ha`, `un una`). -const NOT_FEMININE_IN_A: &[&str] = &["acrobata", "autodidatta", "alfa", "omega", "ha", "una"]; - -/// Abbreviations written with a final period after which a sentence goes on in lower case -/// (lower case, without the period). -const ABBREVIATIONS: &[&str] = &[ - "ecc", - "es", - "dott", - "prof", - "profssa", - "ing", - "avv", - "sig", - "sigg", - "sigra", - "sigr", - "geom", - "rag", - "dottssa", - "avvoc", - "pag", - "pagg", - "tel", - "art", - "artt", - "fig", - "figg", - "vol", - "voll", - "ed", - "sez", - "cap", - "capp", - "par", - "n", - "num", - "vs", - "cfr", - "cfrt", - "op", - "cit", - "ibid", - "sez", - "all", - "tav", - "pag", - "ditta", - "soc", - "spa", - "srl", - "on", - "sen", - "spett", - "eg", - "ca", - "v", - "vv", - "sec", - "min", - "max", - "lun", - "mar", - "mer", - "gio", - "ven", - "sab", - "dom", - "gen", - "feb", - "mar", - "apr", - "mag", - "giu", - "lug", - "ago", - "set", - "ott", - "nov", - "dic", - "genn", - "febb", - "sett", - "dottor", - "professor", - "cav", - "commend", - "mons", - "fr", - "suor", - "s", - "ss", - "b", - "beato", -]; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::It } -} - -/// Is `prev`, before a period, an abbreviation, an initial, a number or a Roman numeral (after -/// which a lower-case word doesn't start a sentence)? -fn abbreviation_or_number(prev: &str) -> bool { - prev.chars().count() <= 1 - || prev.chars().any(|c| c.is_ascii_digit()) - || (prev.chars().all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))) - || ABBREVIATIONS.contains(&prev.to_lowercase().as_str()) -} - -/// Does the gap between two words end a sentence: a `.`, `!` or `?` (not an ellipsis, not after -/// an abbreviation), then space, with nothing but quotes and brackets around? -fn ends_sentence(gap: &str, prev: &str) -> bool { - let t = gap.trim_start_matches(['"', '”', '’', '\'', ')', '»', ']', '\u{1}']); - let Some(end) = t.chars().next() else { return false }; - if !matches!(end, '.' | '!' | '?') || t.starts_with("..") { - return false; - } - if end == '.' && abbreviation_or_number(prev) { - return false; - } - let rest = t.trim_start_matches(['.', '!', '?']).trim_start_matches(['"', '”', '’', '\'', ')', '»', ']']); - rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| matches!(c, '"' | '“' | '«' | '(' | '[' | '\'' | '—')) -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() - && gap.chars().all(char::is_whitespace) - && w0.chars().all(char::is_alphabetic) - && w0.to_lowercase() == w1.to_lowercase() - && !ALLOWED_REPEATS.contains(&w0.to_lowercase().as_str()) - { - v.push(issue(*a0, *b1, format!("Parola ripetuta: \"{w1}\""), vec![w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - let flag = match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Spazio prima della punteggiatura".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Spazio doppio".into(), vec![" ".into()])); - } -} - -fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec) { - let mut after_end = capital_first; - for (a, b) in ws { - let Some(w) = text.get(*a..*b) else { continue }; - if after_end && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) { - let fixed: String = w.chars().next().map(|f| f.to_uppercase().chain(w.chars().skip(1)).collect()).unwrap_or_default(); - v.push(issue(*a, *b, "Maiuscola a inizio frase".into(), vec![fixed])); - } - after_end = ends_sentence(text.get(*b..).unwrap_or(""), w); - } -} - -fn doubled_punctuation(text: &str, v: &mut Vec) { - for pat in [",,", "..", ";;", "::", "??", "!!"] { - for (i, _) in text.match_indices(pat) { - if pat == ".." && text.get(i + 2..).is_some_and(|r| r.starts_with('.')) { - continue; // ellipsis - } - v.push(issue(i, i + pat.len(), "Punteggiatura doppia".into(), vec![pat[..1].into()])); - } - } -} - -/// Is `c` an apostrophe (straight or typographic)? -fn is_apostrophe(c: char) -> bool { - matches!(c, '\'' | '’') -} - -/// `pò` (never accented) and `po` without its apostrophe → `po'`. `Po` (the river) is left alone. -fn po(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for (a, b) in ws { - let Some(w) = text.get(*a..*b) else { continue }; - let apostrophe = text.get(*b..).and_then(|r| r.chars().next()).is_some_and(is_apostrophe); - let fix = match w { - "pò" => "po'", - "Pò" => "Po'", - "po" if !apostrophe => "po'", - _ => continue, - }; - v.push(issue(*a, *b, "Si scrive «po'», con l'apostrofo e senza accento".into(), vec![fix.into()])); - } -} - -/// `qual'è` → `qual è` (any case): the fix when `word` is `qual` + apostrophe + `è`/`era`. -pub(crate) fn qual_elided(word: &str) -> Option { - let head_end = word.char_indices().take(4).last().map(|(i, c)| i + c.len_utf8())?; - let (head, rest) = (word.get(..head_end)?, word.get(head_end..)?); - let tail = rest.strip_prefix(is_apostrophe)?; - let tail_ok = matches!(tail.to_lowercase().as_str(), "è" | "era"); - (head.eq_ignore_ascii_case("qual") && tail_ok).then(|| format!("{head} {tail}")) -} - -fn qual_e(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for (a, b) in ws { - if let Some(fix) = text.get(*a..*b).and_then(qual_elided) { - v.push(issue(*a, *b, "«Qual è» si scrive senza apostrofo".into(), vec![fix])); - } - } -} - -/// `E'` for the verb → `È`. A quoted letter (`la lettera 'E'`) is left alone. -fn e_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for (a, b) in ws { - if text.get(*a..*b) != Some("E") { - continue; - } - let mut after = text.get(*b..).unwrap_or("").chars(); - let Some(ap) = after.next().filter(|&c| is_apostrophe(c)) else { continue }; - let ends = after.next().is_none_or(char::is_whitespace); - let quoted = text.get(..*a).and_then(|t| t.chars().next_back()).is_some_and(|c| is_apostrophe(c) || c == '‘'); - if ends && !quoted { - v.push(issue(*a, *b + ap.len_utf8(), "«È» si scrive con l'accento, non con l'apostrofo".into(), vec!["È".into()])); - } - } -} - -/// Does `w` (lower case) start with a vowel or `h`, as the words `un'` elides before? -fn starts_with_vowel(w: &str) -> bool { - w.chars().next().is_some_and(|c| "aeiouhàèéìòóù".contains(c)) -} - -/// Is `w` (lower case) a feminine word in `-a` (`altra`, `amica`, `ora`), as far as the ending -/// tells? -fn feminine_in_a(w: &str) -> bool { - w.chars().count() >= 2 && w.ends_with('a') && !MASCULINE_A_ENDINGS.iter().any(|e| w.ends_with(e)) && !NOT_FEMININE_IN_A.contains(&w) -} - -/// `un'` before a masculine word (`un'altro` → `un altro`), and `un` without the apostrophe -/// before a feminine one (`un altra` → `un'altra`). Only lower-case words: a name's gender isn't -/// in its ending. -fn un_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for (a, b) in ws { - let Some(w) = text.get(*a..*b) else { continue }; - let Some((head, tail)) = w.split_once(is_apostrophe) else { continue }; - if UN_FORMS.contains(&head.to_lowercase().as_str()) - && tail.chars().all(|c| c.is_alphabetic() && c.is_lowercase()) - && starts_with_vowel(tail) - && tail.ends_with('o') - && !FEMININE_IN_O.contains(&tail) - { - v.push(issue(*a, *b, format!("Davanti a un nome maschile «{head}» non vuole l'apostrofo"), vec![format!("{head} {tail}")])); - } - } - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if gap == " " - && UN_FORMS.contains(&w0.to_lowercase().as_str()) - && w1.chars().all(|c| c.is_alphabetic() && c.is_lowercase()) - && starts_with_vowel(w1) - && feminine_in_a(w1) - { - v.push(issue(*a0, *b1, format!("Davanti a un nome femminile si scrive «{w0}'», con l'apostrofo"), vec![format!("{w0}'{w1}")])); - } - } -} - -/// Grammar issues in a paragraph of Italian text, in order. -pub fn check(text: &str) -> Vec { - check_with(text, true) -} - -/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital -/// (false for a list item, which may be a fragment). -pub fn check_with(text: &str, capital_first: bool) -> Vec { - let ws: Vec<(usize, usize)> = words(text); - let mut v = Vec::new(); - repeated_words(text, &ws, &mut v); - spaces(text, &mut v); - sentence_capitals(text, &ws, capital_first, &mut v); - doubled_punctuation(text, &mut v); - po(text, &ws, &mut v); - qual_e(text, &ws, &mut v); - e_apostrophe(text, &ws, &mut v); - un_apostrophe(text, &ws, &mut v); - v.sort_by_key(|i| (i.start, i.end)); - v -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn italian_grammar_flags() { - test_dict_install(); - let t = "questo questo è un test , fatto male.. Vedi dott. rossi"; - let v = check(t); - let msgs: Vec<&str> = v.iter().map(|i| i.message.as_str()).collect(); - assert!(msgs.iter().any(|m| m.contains("ripetuta")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("prima della punteggiatura")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("doppio")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("doppia")), "{msgs:?}"); - assert!(v.iter().all(|i| i.lang == ProofLang::It), "{v:?}"); - } - - #[test] - fn italian_grammar_stays_quiet() { - test_dict_install(); - assert!( - check("Vado a Roma. Il dott. Rossi è già lì... piano piano!").is_empty(), - "{:?}", - check("Vado a Roma. Il dott. Rossi è già lì... piano piano!") - ); - assert!(check("Sì sì, bene bene :)").is_empty()); - } - - /// The fixes the rules offer for `t`, in order. - fn fixes(t: &str) -> Vec<(String, String)> { - check(t).into_iter().map(|i| (t.get(i.start..i.end).unwrap_or("").to_string(), i.suggestions.join("|"))).collect() - } - - fn pair(a: &str, b: &str) -> (String, String) { - (a.to_string(), b.to_string()) - } - - #[test] - fn po_takes_an_apostrophe() { - assert_eq!(fixes("Dammi un pò di pane"), [pair("pò", "po'")]); - assert_eq!(fixes("Dammi un po di pane"), [pair("po", "po'")]); - assert_eq!(fixes("Pò di più"), [pair("Pò", "Po'")]); - assert!(fixes("Dammi un po' di pane, un po’ d'acqua").is_empty()); - assert!(fixes("Il Po scorre a Torino").is_empty(), "the river"); - } - - #[test] - fn qual_e_has_no_apostrophe() { - assert_eq!(fixes("Dimmi qual'è il problema"), [pair("qual'è", "qual è")]); - assert_eq!(fixes("Qual’era la strada?"), [pair("Qual’era", "Qual era")]); - assert!(fixes("Qual è il problema? Quale era?").is_empty()); - assert!(fixes("Quale'è").is_empty(), "not the truncation"); - assert_eq!(qual_elided("qual'è").as_deref(), Some("qual è")); - assert_eq!(qual_elided("qua"), None); - assert_eq!(qual_elided(""), None); - } - - #[test] - fn e_apostrophe_becomes_e_grave() { - assert_eq!(fixes("E' vero"), [pair("E'", "È")]); - assert_eq!(fixes("Lo so. E’ tardi"), [pair("E’", "È")]); - assert!(fixes("È vero. E poi?").is_empty()); - assert!(fixes("La lettera 'E' è una vocale").is_empty(), "a quoted letter"); - } - - #[test] - fn un_apostrophe_follows_the_gender() { - assert_eq!(fixes("Ho visto un'altro amico"), [pair("un'altro", "un altro")]); - assert_eq!(fixes("È passato un’anno"), [pair("un’anno", "un anno")]); - assert_eq!(fixes("Prendo un altra strada"), [pair("un altra", "un'altra")]); - assert_eq!(fixes("Ho un amica a Roma"), [pair("un amica", "un'amica")]); - assert_eq!(fixes("Non c'è nessun altra via"), [pair("nessun altra", "nessun'altra")]); - for ok in [ - "Un altro amico", - "Un'altra amica", - "Un'auto nuova", - "Un'eco lontana", - "Un artista", - "Un atleta", - "Un idiota", - "Un ipocrita", - "Un enigma", - "Un omicida", - "Un astronauta", - "Un acrobata", - "Un'ora", - "Un'idea", - "Un uomo", - "Un'Alfa", - "Un Andrea", - "Un ha", - ] { - assert!(fixes(ok).is_empty(), "{ok}: {:?}", fixes(ok)); - } - } - - #[test] - fn more_emphatic_doubles_are_allowed() { - for ok in ["Pian piano", "Poco poco", "Zitto zitto", "Via via", "Così così", "Dai dai"] { - assert!(fixes(ok).is_empty(), "{ok}: {:?}", fixes(ok)); - } - } - - proptest::proptest! { - #[test] - fn never_panics(s in "\\PC{0,80}") { - for i in check(&s) { - proptest::prop_assert!(i.start <= i.end && i.end <= s.len()); - } - } - - #[test] - fn never_panics_on_italian_like_text(s in "[a-zàèéìòóùA-ZÈ ,.;:!?'’‘\"«»…\\-]{0,60}") { - for i in check(&s) { - proptest::prop_assert!(i.start <= i.end && i.end <= s.len() && s.is_char_boundary(i.start) && s.is_char_boundary(i.end)); - } - } - } - - fn test_dict_install() { - crate::it::test_dict::install(); - } -} diff --git a/crates/proof/src/it/mod.rs b/crates/proof/src/it/mod.rs deleted file mode 100644 index ee5aaaee3..000000000 --- a/crates/proof/src/it/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! Italian proofing. -//! -//! - **Spelling** against the Hunspell `it_IT` dictionary installed on the system (found by -//! [`crate::hunspell`]: `it_IT.aff`/`it_IT.dic` from the OS, a package manager or -//! LibreOffice; it isn't bundled — the upstream file is GPL-licensed). Without it Italian -//! text gets no spelling marks. Elided forms check the word after the apostrophe -//! (`l'amico`, `un'idea`, `c'è`, `dov'è`, `senz'altro`); truncations keep their apostrophe -//! (`po'`, `mo'`); hyphenated compounds are checked part by part; the user dictionary applies -//! as in English. Forms a grammar rule explains better (`pò`, `qual'è`) are left to it. -//! - **Suggestions** tuned for Italian ([`suggest`]): missing accents first (`caffe` → `caffè`, -//! `perche` → `perché`), then the dictionary's `REP` confusions, a missing space, one-letter -//! typos, and a dictionary scan for anything further off. -//! - **Grammar** ([`grammar`]): cheap, reliable rules with Italian messages, which work with -//! no dictionary installed. - -pub mod grammar; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of an Italian Hunspell dictionary (Italy first, then Switzerland). -const NAMES: &[&str] = &["it_IT", "it_CH", "it-IT", "it"]; - -/// The Italian dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Italian"); - -/// The Italian dictionary: the installed Hunspell `it_IT` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Italian spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Italian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is an Italian dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Proclitics (and verb clippings) that elide before a vowel or `h`: the word after the -/// apostrophe is what gets checked (`l'amico`, `un'idea`, `c'è`, `m'hai`, `dov'è`, `anch'io`). -const PROCLITICS: &[&str] = &[ - "l", "un", "una", "uno", "all", "dall", "dell", "nell", "sull", "coll", "pell", "c", "m", "t", "s", "v", "d", "j", "n", "ch", "gl", "quest", - "quell", "bell", "buon", "sant", "grand", "poc", "nessun", "ciascun", "qualcun", "alcun", "dov", "cos", "com", "quand", "anch", "senz", "tutt", - "nient", "mezz", "sott", "tant", "quant", "dev", -]; - -/// Truncated words written with an apostrophe (`un po'`, `a mo' di`, `be'`): [`crate::words`] -/// trims the apostrophe, so spelling sees the bare word; [`grammar`] checks the apostrophe. -const TRUNCATIONS: &[&str] = &["po", "mo", "be"]; - -/// Misspellings a grammar rule reports with a clearer message and fix (`pò` → `po'`, `qual'è` -/// → `qual è`), so spelling leaves them alone rather than marking the word twice. -fn left_to_grammar(lower: &str) -> bool { - lower == "pò" || grammar::qual_elided(lower).is_some() -} - -/// Is `word` spelled correctly in Italian? Numbers, single letters, short words in capitals -/// (acronyms: `RAI`, `TG`), words with digits and words in other scripts are accepted, and so -/// is every word when no Italian dictionary is installed. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - let lower = w.to_lowercase(); - if TRUNCATIONS.contains(&lower.as_str()) || left_to_grammar(&lower) { - return true; - } - if w.contains('-') { - return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); - } - if let Some((head, tail)) = w.split_once(['\'', '’']) - && PROCLITICS.contains(&head.to_lowercase().as_str()) - && !tail.is_empty() - { - return known(d, tail); - } - known(d, w) -} - -/// Is one word (no hyphens, no apostrophe) in the dictionary or the user dictionary? -fn known(d: &AffixDict, w: &str) -> bool { - d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dictionary_loads() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert!(d.len() > 50, "{}", d.len()); - assert!(d.rule_count() >= 4, "the test dictionary's affix rules"); - assert!(!d.rep().is_empty() && !d.map().is_empty() && !d.try_chars().is_empty()); - } - - #[test] - fn swiss_dictionary_files_are_found() { - // it_CH is searched too, so Swiss users with only it_CH installed get Italian spelling. - let dir = std::env::temp_dir().join(format!("wc-it-ch-{}", std::process::id())); - let _ = std::fs::create_dir_all(&dir); - std::fs::write(dir.join("it_CH.aff"), test_dict::AFF).expect("aff"); - std::fs::write(dir.join("it_CH.dic"), test_dict::DIC).expect("dic"); - let found = crate::hunspell::find_in(&dir, NAMES); - let ok = found.as_ref().is_some_and(|(a, d)| a.ends_with("it_CH.aff") && d.ends_with("it_CH.dic")); - assert!(ok, "{found:?}"); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn without_a_dictionary_nothing_is_flagged() { - let none = AffixDict::default(); - for w in ["caffè", "perché", "xqzv", ""] { - assert!(is_correct_with(&none, w), "{w}"); - } - assert!(install_dictionary("FLAG long\n", "1\nkot\n").is_err(), "unsupported files are refused"); - } - - #[test] - fn italian_words_are_correct() { - test_dict::install(); - for w in [ - "casa", "case", "libro", "libri", "città", "caffè", "perché", "più", "può", "parlare", "parla", "parlano", "rifare", "bello", "belli", - "nuovo", "bianco", "Roma", "Milano", "l'amico", "un'idea", "L'amico", "RAI", "2026", "e", "non", - ] { - assert!(is_correct(w), "{w}"); - } - } - - #[test] - fn elisions_and_truncations_are_correct() { - test_dict::install(); - for w in ["dov'è", "Com'è", "cos'è", "anch'io", "senz'acqua", "tutt'uno", "quand'è", "nessun'amica", "n'ha", "po'", "po", "mo'"] { - assert!(is_correct(w), "{w}"); - } - } - - #[test] - fn misspellings_are_flagged() { - test_dict::install(); - for w in ["caffé", "perche", "lbiro", "parlre", "amicaa", "citta", "quelo", "dov'lbiro", "xyz'casa"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn grammar_owned_forms_are_not_marked_twice() { - test_dict::install(); - // `pò` and `qual'è` are reported by the grammar rules, with the fix; spelling stays quiet. - for w in ["pò", "Pò", "qual'è", "Qual’era"] { - assert!(is_correct(w), "{w}"); - assert!(!grammar::check(w).is_empty(), "{w}: grammar reports it"); - } - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("zxqitalianaparola")); - crate::add_word("Zxqitalianaparola"); - assert!(is_correct("zxqitalianaparola")); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Italian checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_italian_check() { - test_dict::install(); - let lang = crate::ProofLang::It; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("LIBRRO", lang, &defaults), "short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("LIBRRO", lang, &check_caps)); - assert!(crate::is_correct_in_with("LIBRO", lang, &check_caps)); - assert!(crate::is_correct_in_with("librro2", lang, &defaults)); - assert!(!crate::is_correct_in_with("librro2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("LIBRRO"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/it/suggest.rs b/crates/proof/src/it/suggest.rs deleted file mode 100644 index 55f869091..000000000 --- a/crates/proof/src/it/suggest.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Italian spelling suggestions, best first: the shared ranking ([`crate::affix_suggest`]) with the -//! words Italian runs into their neighbours (`perme` → `per me`, `amilano` → `a milano`) and -//! letters to try for a dictionary without a TRY line. - -use crate::affix::AffixDict; -use crate::affix_suggest::{Split, Tuning}; - -/// Short words often run into the next one (`per me`, `a Milano`, `di Roma`). -const LEADING_WORDS: &[&str] = &[ - "a", "e", "è", "di", "da", "in", "con", "su", "per", "tra", "fra", "che", "chi", "cui", "non", "si", "ci", "vi", "ne", "più", "meno", "molto", - "tanto", "come", "dove", "quando", "mai", "qui", "qua", "lì", "là", "ogni", "altro", "tutto", "questo", "quello", "bene", "male", "anche", - "ancora", "sempre", "già", "poi", "ed", "od", "ad", "un", "uno", "una", "il", "lo", "la", "i", "gli", "le", "del", "al", "dal", "nel", "sul", - "col", "pel", "allo", "dello", "nello", "sullo", "della", "alla", "dalla", "nella", "sulla", "degli", "agli", "dagli", "negli", "sugli", "delle", - "alle", "dalle", "nelle", "sulle", "dei", "ai", "dai", "nei", "sui", "coi", "dei", -]; -/// Short words often run into the one before (enclitic pronouns: `dimmelo` in writing without -/// the doubling, or a dropped space: `perme` → `per me`). -const TRAILING_WORDS: &[&str] = &[ - "mi", "ti", "si", "ci", "vi", "ne", "lo", "la", "li", "le", "gli", "me", "te", "lui", "lei", "noi", "voi", "loro", "che", "cosa", "come", "dove", - "è", -]; - -/// What Italian adds to its dictionary's tables. -const TUNING: Tuning = - Tuning { - split: Split::Near { leading: LEADING_WORDS, trailing: TRAILING_WORDS }, alphabet: "aeiortnslcdupmghfbqvàèéìòóùz", ..Tuning::DEFAULT - }; - -/// Spelling suggestions for an Italian word, best first, at most `max`, in the word's letter case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &TUNING) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::affix_suggest::{fold, weighted_distance}; - - fn has(word: &str, want: &str) { - let s = suggest(word, 8); - assert!(s.iter().any(|x| x == want), "{word} → {s:?}, want {want}"); - } - - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(suggest_in); - } - - #[test] - fn italian_misspellings_get_the_right_suggestion() { - crate::it::test_dict::install(); - has("caffe", "caffè"); - has("perche", "perché"); - has("cioe", "cioè"); - has("citta", "città"); - has("lbiro", "libro"); - has("parlre", "parlare"); - has("quelo", "quello"); - has("perme", "per me"); - has("amilano", "a Milano"); - has("roma", "Roma"); - has("Caffe", "Caffè"); - } - - #[test] - fn the_likeliest_fix_comes_first() { - crate::it::test_dict::install(); - assert_eq!(suggest("caffe", 3).first().map(String::as_str), Some("caffè")); - assert_eq!(suggest("perche", 3).first().map(String::as_str), Some("perché")); - } - - #[test] - fn edge_cases_stay_empty_or_bounded() { - crate::it::test_dict::install(); - assert!(suggest("", 5).is_empty()); - assert!(suggest("bene", 0).is_empty()); - assert!(suggest(&"a".repeat(41), 5).is_empty()); - assert!(suggest("due parole", 5).is_empty()); - assert!(suggest_in(&crate::affix::AffixDict::default(), "caffe", 5).is_empty()); - assert!(suggest("caffè", 5).len() <= 5); - assert!(!suggest("caffè", 5).contains(&"caffè".to_string())); - } - - #[test] - fn distance_weighs_accents_half() { - let map = vec![vec!['a', 'à'], vec!['e', 'é', 'è']]; - let related = |a: char, b: char| fold(a, &map) == fold(b, &map); - let c = |s: &str| s.chars().collect::>(); - assert_eq!(weighted_distance(&c("caffe"), &c("caffè"), &related, 9), 1); - assert_eq!(weighted_distance(&c("citta"), &c("città"), &related, 9), 1); - assert_eq!(weighted_distance(&c("bene"), &c("bene"), &related, 9), 0); - assert_eq!(weighted_distance(&c("bene"), &c("male"), &related, 2), 3); - assert_eq!(weighted_distance(&c("bene"), &c("benissimo"), &related, 2), 3); - assert_eq!(weighted_distance(&c(""), &c("ab"), &related, 9), 4); - } - - #[test] - fn suggestions_are_fast_enough() { - crate::it::test_dict::install(); - // Warm the dictionary, then time a worst case (a long word nothing near): the scan round. - let _ = suggest("bene", 1); - let t = std::time::Instant::now(); - for w in ["xqzvweitalianaparola", "prsidenti", "parallellepipedo"] { - let _ = suggest(w, 6); - } - let ms = t.elapsed().as_secs_f64() * 1000.0; - eprintln!("3 suggestion calls: {ms:.0} ms"); - assert!(ms < 20_000.0, "{ms} ms"); - } -} diff --git a/crates/proof/src/it/test_dict.rs b/crates/proof/src/it/test_dict.rs deleted file mode 100644 index 5d594d2fd..000000000 --- a/crates/proof/src/it/test_dict.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! A tiny synthetic Italian Hunspell dictionary for tests (written for these tests; not taken -//! from any real dictionary). Real dictionaries are installed on the system, never bundled. -//! -//! It exercises what the Italian dictionaries use: the default single-character `FLAG` type, -//! suffixes (verb endings `-are`, noun plurals `-o` → `-i`), a prefix (`ri-`), the suggestion -//! tables (`TRY`, `MAP` for accents, `REP` for common confusions) and elided forms with an -//! apostrophe (`l'amico`). - -pub const AFF: &str = "SET UTF-8 -TRY aeiortnslcdupmghfbqvàèéìòóùz -MAP 5 -MAP aà -MAP eéè -MAP iì -MAP oòó -MAP uù -REP 3 -REP caffé caffè -REP perche perché -REP cioe cioè -SFX A Y 3 -SFX A 0 are . -SFX A 0 a . -SFX A 0 ano . -SFX N Y 2 -SFX N o i o -SFX N a e a -PFX R Y 1 -PFX R 0 ri . -"; - -/// Stems (the count line first, as in Hunspell files). -pub const DIC: &str = "97 -città -caffè -perché -più -può -è -già -così -virtù -tribù -menù -tè -Mosca -Roma -Milano -Torino -casa/N -libro/N -cane -gatto -sole -luna -mare -amico -amica -idea -parl/A -mangiare -dormire -fare/R -scuola -bambino/N -albero -fiore -pane -acqua -terra -cielo -notte -giorno -vecchia -vado -con -ha -ho -bel -cioè -la -un -in -piazza -lavoro -tempo -uomo -donna -bello/N -buono -grande -piccolo -nuovo/N -vecchio -bianco -nero -rosso -verde -giallo -uno -due -tre -tutto -come -quando -dove -senza -sopra -sotto -tra -molto -sempre -mai -qui -lì -sì -no -non -che -chi -cui -mio -tuo -suo -nostro -a -per -me -quello -io -"; - -/// Make [`AFF`]/[`DIC`] the Italian dictionary (every test that needs one calls this; they all -/// install the same one, so tests running in parallel agree). -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} diff --git a/crates/proof/src/lang.rs b/crates/proof/src/lang.rs deleted file mode 100644 index 8f681408c..000000000 --- a/crates/proof/src/lang.rs +++ /dev/null @@ -1,388 +0,0 @@ -//! Proofing languages: which dictionary, grammar rules and hyphenation patterns apply to a run of -//! text. A run's language is its BCP 47 tag (`w:lang` in .docx, `CharProps::lang` in the model), -//! resolved with [`ProofLang::from_tag`]. - -/// A language WordCraft can proof, or [`ProofLang::Other`] for one it can't. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum ProofLang { - /// American English (`en`, `en-US`, and every region not listed for [`ProofLang::EnGb`] or - /// [`ProofLang::EnCa`]): the public-domain Moby word list, the English grammar rules and the - /// English hyphenation dictionary and patterns. British spellings (`colour`, `centre`, - /// `organise`) are flagged, with the American form suggested ([`crate::en_variants`]). Also - /// text with no language at all (WordCraft's default editing language is US English, like - /// Word's). - #[default] - En, - /// English with British spelling (`en-GB`, `en-AU`, `en-NZ`, `en-IE`, `en-ZA`, `en-IN`…): - /// [`ProofLang::En`]'s tools, but American spellings (`color`, `center`, `analyze`) are - /// flagged and `-ize` and `-ise` are both right, as in Oxford spelling. - EnGb, - /// Canadian English (`en-CA`): British `-our`, `-re` and `-ence` (`colour`, `centre`, - /// `defence`) with American `-ize` (`organize`), `tire` and `program`. - EnCa, - /// Polish (`pl`, `pl-PL`): the installed Hunspell dictionary (when there is one), the Polish grammar rules and the Polish - /// hyphenation patterns. - Pl, - /// Bulgarian (`bg`, `bg-BG`): hyphenation by the Bulgarian syllable rules ([`crate::bg`]). - /// There is no Bulgarian dictionary or grammar yet, so, as for [`ProofLang::Other`], no - /// spelling or grammar marks. - Bg, - /// Portuguese (`pt`, `pt-BR`, `pt-PT`): the installed Hunspell dictionary (VERO and - /// friends, when there is one), the Portuguese grammar rules and the Portuguese hyphenation - /// patterns. - Pt, - /// Italian (`it`, `it-IT`, `it-CH`): the installed Hunspell dictionary (`it_IT`, when there - /// is one), the Italian grammar rules and the Italian hyphenation patterns. - It, - /// Croatian (`hr`, `hr-HR`) - Hr, - /// Turkish (`tr`, `tr-TR`, `tr-CY`): the installed Hunspell dictionary (when there is one), - /// Turkish grammar rules, spelling suggestions and Turkish syllable hyphenation rules. - Tr, - /// Russian (`ru`, `ru-RU`): the installed Hunspell dictionary (when there is one), the Russian - /// grammar rules and the Russian word-division rules. - Ru, - /// Czech (`cs`, `cs-CZ`): the installed Hunspell dictionary, dictionary-driven suggestions - /// and generic punctuation checks. Hyphenation keeps the fallback English patterns. - Cs, - /// Indonesian (`id`, `id-ID`; also the old `in` and ISO 639-2 `ind`): the installed Hunspell - /// `id_ID` dictionary, dictionary-driven suggestions and generic checks with Indonesian - /// messages. Hyphenation keeps the fallback English patterns. - Id, - /// Spanish of Spain (`es`, `es-ES`, and every region not listed for [`ProofLang::EsMx`], - /// [`ProofLang::Es419`] or [`ProofLang::EsUy`]; also ISO 639-2 `spa`): the installed - /// Hunspell `es_ES` dictionary (or a generic Spanish one), dictionary-driven suggestions and - /// generic checks with Spanish messages ([`crate::es`]). Hyphenation keeps the fallback - /// English patterns. - Es, - /// Mexican Spanish (`es-MX`): [`ProofLang::Es`]'s tools with the installed `es_MX` - /// dictionary, else a Latin American or generic Spanish one. - EsMx, - /// Latin American Spanish (`es-419`, and the Spanish of the Americas other than Mexico and - /// Uruguay: `es-AR`, `es-CO`, `es-US`…): [`ProofLang::Es`]'s tools with the installed `es_419` - /// dictionary, else a generic or another Latin American Spanish one. - Es419, - /// Uruguayan Spanish (`es-UY`): [`ProofLang::Es`]'s tools with the installed `es_UY` - /// dictionary (else the neighbouring Rioplatense `es_AR`, a Latin American or a generic one), - /// and the `vos` forms of verbs (`tenés`, `vení`, `decime`) accepted ([`crate::es::voseo`]). - EsUy, - /// German (`de`, `de-DE`, `de-AT`, `de-CH`…): hyphenation by the German patterns (reformed - /// spelling). There is no German dictionary or grammar yet, so, as for [`ProofLang::Other`], - /// no spelling or grammar marks. - De, - /// A language without proofing tools here (`fr-FR`, `ja-JP`, `x-none`…). Like Word without - /// the language's proofing tools: no spelling or grammar marks, so its words are never - /// flagged as English misspellings. Hyphenation keeps using the English patterns, as before - /// languages were told apart. - Other, -} - -impl ProofLang { - /// Every language with a spelling dictionary, in display order. - pub const PROOFED: [ProofLang; 15] = [ - ProofLang::En, - ProofLang::EnGb, - ProofLang::EnCa, - ProofLang::Pl, - ProofLang::Pt, - ProofLang::It, - ProofLang::Hr, - ProofLang::Tr, - ProofLang::Ru, - ProofLang::Cs, - ProofLang::Id, - ProofLang::Es, - ProofLang::EsMx, - ProofLang::Es419, - ProofLang::EsUy, - ]; - - /// The proofing language of a BCP 47 tag (`pl-PL`, `en_GB`, `PL`…). The primary subtag - /// counts, and for English and Spanish the region too ([`ProofLang::english`], - /// [`ProofLang::spanish`]). No tag (or an empty one) is English, WordCraft's default editing - /// language. - pub fn from_tag(tag: Option<&str>) -> ProofLang { - let Some(tag) = tag.map(str::trim).filter(|t| !t.is_empty()) else { return ProofLang::En }; - let primary = tag.split(['-', '_']).next().unwrap_or(""); - if primary.eq_ignore_ascii_case("en") || primary.eq_ignore_ascii_case("eng") { - ProofLang::english(tag) - } else if primary.eq_ignore_ascii_case("pl") || primary.eq_ignore_ascii_case("pol") { - ProofLang::Pl - } else if primary.eq_ignore_ascii_case("bg") || primary.eq_ignore_ascii_case("bul") { - ProofLang::Bg - } else if primary.eq_ignore_ascii_case("pt") || primary.eq_ignore_ascii_case("por") { - ProofLang::Pt - } else if primary.eq_ignore_ascii_case("it") || primary.eq_ignore_ascii_case("ita") { - ProofLang::It - } else if primary.eq_ignore_ascii_case("hr") || primary.eq_ignore_ascii_case("hrv") { - ProofLang::Hr - } else if primary.eq_ignore_ascii_case("tr") || primary.eq_ignore_ascii_case("tur") { - ProofLang::Tr - } else if primary.eq_ignore_ascii_case("ru") || primary.eq_ignore_ascii_case("rus") { - ProofLang::Ru - } else if primary.eq_ignore_ascii_case("cs") || primary.eq_ignore_ascii_case("ces") || primary.eq_ignore_ascii_case("cze") { - ProofLang::Cs - } else if primary.eq_ignore_ascii_case("id") || primary.eq_ignore_ascii_case("ind") || primary.eq_ignore_ascii_case("in") { - ProofLang::Id - } else if primary.eq_ignore_ascii_case("es") || primary.eq_ignore_ascii_case("spa") { - ProofLang::spanish(tag) - } else if ["de", "deu", "ger"].iter().any(|c| primary.eq_ignore_ascii_case(c)) { - ProofLang::De - } else { - ProofLang::Other - } - } - - /// The English of an English tag, by its [`region`]: Canada is [`ProofLang::EnCa`], regions - /// that spell the British way are [`ProofLang::EnGb`], and the United States, any other - /// region and no region are [`ProofLang::En`]. - fn english(tag: &str) -> ProofLang { - match region(tag) { - Some(r) if r.eq_ignore_ascii_case("CA") => ProofLang::EnCa, - Some(r) if BRITISH_SPELLING.iter().any(|b| b.eq_ignore_ascii_case(r)) => ProofLang::EnGb, - _ => ProofLang::En, - } - } - - /// The Spanish of a Spanish tag, by its [`region`]: Mexico is [`ProofLang::EsMx`], Uruguay - /// [`ProofLang::EsUy`], Latin America (`419`) and the other countries of the Americas are - /// [`ProofLang::Es419`], and Spain, any other region and no region are [`ProofLang::Es`]. - fn spanish(tag: &str) -> ProofLang { - match region(tag) { - Some(r) if r.eq_ignore_ascii_case("MX") => ProofLang::EsMx, - Some(r) if r.eq_ignore_ascii_case("UY") => ProofLang::EsUy, - Some(r) if LATIN_AMERICA.iter().any(|l| l.eq_ignore_ascii_case(r)) => ProofLang::Es419, - _ => ProofLang::Es, - } - } - - /// Is this one of the Englishes ([`ProofLang::En`], [`ProofLang::EnGb`], [`ProofLang::EnCa`])? - pub fn is_english(self) -> bool { - matches!(self, ProofLang::En | ProofLang::EnGb | ProofLang::EnCa) - } - - /// Is this one of the Spanishes ([`ProofLang::Es`], [`ProofLang::EsMx`], [`ProofLang::Es419`], - /// [`ProofLang::EsUy`])? - pub fn is_spanish(self) -> bool { - matches!(self, ProofLang::Es | ProofLang::EsMx | ProofLang::Es419 | ProofLang::EsUy) - } - - /// The primary language subtag (`en`, `pl`, `bg`, `tr`, `ru`, `cs`, `id`, `de`, `es`); empty for [`ProofLang::Other`]. - pub fn code(self) -> &'static str { - match self { - ProofLang::En | ProofLang::EnGb | ProofLang::EnCa => "en", - ProofLang::Pl => "pl", - ProofLang::Bg => "bg", - ProofLang::Pt => "pt", - ProofLang::It => "it", - ProofLang::Hr => "hr", - ProofLang::Tr => "tr", - ProofLang::Ru => "ru", - ProofLang::Cs => "cs", - ProofLang::Id => "id", - ProofLang::Es | ProofLang::EsMx | ProofLang::Es419 | ProofLang::EsUy => "es", - ProofLang::De => "de", - ProofLang::Other => "", - } - } - - /// Does WordCraft check spelling and grammar in this language? - pub fn is_proofed(self) -> bool { - Self::PROOFED.contains(&self) - } -} - -/// The region of a tag: the first two-letter or three-digit subtag after the language and -/// script, before any singleton; a POSIX `.UTF-8` or `@…` tail is ignored. -fn region(tag: &str) -> Option<&str> { - let tag = tag.split(['.', '@']).next().unwrap_or(""); - // Subtags after a singleton (`x-…` private use, `u-…` extensions) aren't regions. - tag.split(['-', '_']) - .skip(1) - .take_while(|p| p.len() != 1) - .find(|p| (p.len() == 2 && p.chars().all(|c| c.is_ascii_alphabetic())) || (p.len() == 3 && p.chars().all(|c| c.is_ascii_digit()))) -} - -/// Regions whose Spanish is Latin American ([`ProofLang::Es419`]): Latin America and the -/// Caribbean (`419`) and its parts (South America `005`, Central America `013`, the Caribbean -/// `029`), the Spanish-speaking countries of the Americas other than Mexico and Uruguay, and the -/// United States. -const LATIN_AMERICA: [&str; 22] = - ["419", "005", "013", "029", "AR", "BO", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "NI", "PA", "PE", "PR", "PY", "SV", "US", "VE"]; - -/// Regions whose English uses British spelling ([`ProofLang::EnGb`]): the United Kingdom (and -/// the non-standard `UK`), Ireland, Australia, New Zealand, South Africa, India and other -/// Commonwealth countries, the Crown dependencies and Europe (`150`). -const BRITISH_SPELLING: [&str; 26] = [ - "GB", "UK", "IE", "AU", "NZ", "ZA", "IN", "SG", "HK", "MY", "MT", "CY", "GI", "IM", "JE", "GG", "JM", "TT", "BS", "BB", "BZ", "KE", "NG", "GH", - "PK", "150", -]; - -/// A BCP 47 tag in canonical case (`pl-PL`, `en-GB`, `zh-Hant-TW`, `x-none`), or `None` when -/// `tag` isn't shaped like one: 1–35 ASCII letters, digits and `-`/`_` separators, starting with -/// a 2–8 letter language (or `x`/`i` for private and grandfathered tags). `_` becomes `-`. -pub fn normalize_tag(tag: &str) -> Option { - let tag = tag.trim(); - if tag.is_empty() || tag.len() > 35 || !tag.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { - return None; - } - let parts: Vec<&str> = tag.split(['-', '_']).collect(); - if parts.iter().any(|p| p.is_empty() || p.len() > 8) { - return None; - } - let first = parts.first()?; - let language = (2..=8).contains(&first.len()) && first.chars().all(|c| c.is_ascii_alphabetic()); - if !language && !first.eq_ignore_ascii_case("x") && !first.eq_ignore_ascii_case("i") { - return None; - } - let letters = |p: &str| p.chars().all(|c| c.is_ascii_alphabetic()); - // After a singleton (`x-…` private use, `u-…` extensions) subtags are lower case. - let singleton = parts.iter().position(|p| p.len() == 1).unwrap_or(usize::MAX); - let out: Vec = parts - .iter() - .enumerate() - .map(|(i, p)| { - if i >= singleton { - p.to_ascii_lowercase() - } else if i > 0 && p.len() == 2 && letters(p) { - p.to_ascii_uppercase() // region - } else if i > 0 && p.len() == 4 && letters(p) { - let lower = p.to_ascii_lowercase(); - let mut c = lower.chars(); - c.next().map(|f| f.to_ascii_uppercase().to_string() + c.as_str()).unwrap_or_default() // script - } else { - p.to_ascii_lowercase() - } - }) - .collect(); - Some(out.join("-")) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tags_normalize() { - assert_eq!(normalize_tag("PL-pl").as_deref(), Some("pl-PL")); - assert_eq!(normalize_tag(" pl_pl ").as_deref(), Some("pl-PL")); - assert_eq!(normalize_tag("pl").as_deref(), Some("pl")); - assert_eq!(normalize_tag("zh-hant-tw").as_deref(), Some("zh-Hant-TW")); - assert_eq!(normalize_tag("es-419").as_deref(), Some("es-419")); - assert_eq!(normalize_tag("x-none").as_deref(), Some("x-none")); - assert_eq!(normalize_tag("de-DE-u-co-PHONEBK").as_deref(), Some("de-DE-u-co-phonebk")); - for bad in ["", " ", "p", "1pl", "pl--PL", "pl PL", "pl-PL;", "polski język", "-pl", "pl-", "abcdefghi", &"a".repeat(40)] { - assert_eq!(normalize_tag(bad), None, "{bad:?}"); - } - } - - fn assert_tags(tags: &[&str], expected: ProofLang) { - for tag in tags { - assert_eq!(ProofLang::from_tag(Some(tag)), expected, "{tag}"); - } - } - - #[test] - fn tags_resolve_primary_languages() { - assert_tags(&["pl", "pl-PL", "PL-pl", "pl_PL", "pl_PL.UTF-8", " pl-PL ", "pol"], ProofLang::Pl); - assert_tags(&["pt", "pt-BR", "PT_br", "pt-BR.UTF-8", " pt-PT ", "por"], ProofLang::Pt); - assert_tags(&["it", "it-IT", "IT_it", "it-CH", "it-CH.UTF-8", " it-IT ", "ita"], ProofLang::It); - assert_tags(&["cs", "cs-CZ", "CS-cz", "cs_CZ", " ces ", "CES", "cze", "CZE"], ProofLang::Cs); - assert_tags(&["id", "id-ID", "ID-id", "id_ID", "id_ID.UTF-8", " id-ID ", "ind", "in", "in_ID"], ProofLang::Id); - assert_eq!(ProofLang::Cs.code(), "cs"); - assert!(ProofLang::Cs.is_proofed() && ProofLang::PROOFED.contains(&ProofLang::Cs)); - assert_eq!(ProofLang::Id.code(), "id"); - assert!(ProofLang::Id.is_proofed() && ProofLang::PROOFED.contains(&ProofLang::Id)); - } - - #[test] - fn tags_resolve_english_regions() { - assert_tags(&["en", "en-US", "EN_us", "en-Latn-US", "en-PH", "en-001", "eng", "en_US.UTF-8", "en-x-gb"], ProofLang::En); - assert_tags( - &[ - "en-GB", - "EN_gb", - "en_GB.UTF-8", - "en-AU", - "en_AU", - "en-NZ", - "en-IE", - "en-ZA", - "en-IN", - "en-Latn-GB", - "en-UK", - "en-150", - "eng-GB", - "en_GB@euro", - ], - ProofLang::EnGb, - ); - assert_tags(&["en-CA", "en_CA", "EN-ca", "en_CA.UTF-8", "en-Latn-CA"], ProofLang::EnCa); - for language in [ProofLang::En, ProofLang::EnGb, ProofLang::EnCa] { - assert!(language.is_english() && language.is_proofed() && ProofLang::PROOFED.contains(&language), "{language:?}"); - assert_eq!(language.code(), "en"); - } - assert!(!ProofLang::Pl.is_english() && !ProofLang::Other.is_english()); - // The region is the first two-letter or three-digit subtag: a script or variant isn't one. - assert_eq!(ProofLang::from_tag(Some("en-Brai")), ProofLang::En); - assert_eq!(ProofLang::from_tag(Some("en-GB-oxendict")), ProofLang::EnGb); - } - - #[test] - fn tags_resolve_additional_locales_and_defaults() { - assert_tags(&["bg", "bg-BG", "BG_bg", "bg_BG.UTF-8", "bul"], ProofLang::Bg); - assert_tags(&["tr", "tr-TR", "tr-CY", "TR_tr", "tur"], ProofLang::Tr); - assert_tags(&["de", "de-DE", "de-CH", "DE_at", "deu", "ger"], ProofLang::De); - assert_tags(&["ru", "ru-RU", "RU-ru", "ru_RU", "ru_RU.UTF-8", " ru-RU ", "rus"], ProofLang::Ru); - assert!(!ProofLang::De.is_proofed()); - assert!(!ProofLang::Bg.is_proofed()); - assert_eq!(ProofLang::Bg.code(), "bg"); - assert!(ProofLang::Pt.is_proofed() && !ProofLang::Other.is_proofed()); - assert!(ProofLang::Tr.is_proofed()); - assert!(ProofLang::Ru.is_proofed()); - assert_eq!(ProofLang::Ru.code(), "ru"); - assert!(ProofLang::PROOFED.contains(&ProofLang::Ru)); - assert_eq!(ProofLang::Pl.code(), "pl"); - assert_eq!(ProofLang::Pt.code(), "pt"); - assert!(ProofLang::It.is_proofed()); - assert_eq!(ProofLang::It.code(), "it"); - assert!(ProofLang::Hr.is_proofed()); - assert_eq!(ProofLang::Hr.code(), "hr"); - assert_eq!(ProofLang::Tr.code(), "tr"); - assert!(ProofLang::PROOFED.contains(&ProofLang::Pt), "every proofed language is listed"); - assert_eq!(ProofLang::from_tag(None), ProofLang::En); - assert_eq!(ProofLang::from_tag(Some("")), ProofLang::En); - assert_eq!(ProofLang::from_tag(Some(" ")), ProofLang::En); - assert!(ProofLang::Pl.is_proofed() && !ProofLang::Other.is_proofed()); - } - - #[test] - fn tags_reject_unknown_or_partial_language_codes() { - assert_tags(&["fr-FR", "dex", "x-none", "ja-JP", "plx", "bgx", "trx", "csx", "idx", "inx", "esx", "spax", "e", "-"], ProofLang::Other); - } - - /// #3360, #3553: Spanish is proofed, by region: Spain (and no region), Mexico, Uruguay, and - /// Latin America with the other countries of the Americas. - #[test] - fn spanish_tags_resolve_by_region() { - for t in ["es", "es-ES", "ES_es", "es_ES.UTF-8", " es-ES ", "spa", "es-Latn-ES", "es-GQ", "es-PH", "es-x-mx", "es_ES@euro"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Es, "{t}"); - } - for t in ["es-MX", "es_MX", "ES-mx", "es_MX.UTF-8", "spa-MX", "es-Latn-MX"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::EsMx, "{t}"); - } - for t in ["es-419", "es_419", "es-AR", "es-CO", "es_CL.UTF-8", "es-US", "ES-ve", "es-PR", "es-Latn-419", "es-005", "spa-PE"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Es419, "{t}"); - } - for t in ["es-UY", "es_UY", "ES-uy", "es_UY.UTF-8", "spa-UY", "es-Latn-UY"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::EsUy, "{t}"); - } - for l in [ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy] { - assert!(l.is_spanish() && l.is_proofed() && ProofLang::PROOFED.contains(&l), "{l:?}"); - assert!(!l.is_english()); - assert_eq!(l.code(), "es"); - } - assert!(!ProofLang::En.is_spanish() && !ProofLang::Pt.is_spanish() && !ProofLang::Other.is_spanish()); - assert_eq!(normalize_tag("ES_mx").as_deref(), Some("es-MX")); - } -} diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs deleted file mode 100644 index f92ad19a3..000000000 --- a/crates/proof/src/lib.rs +++ /dev/null @@ -1,2000 +0,0 @@ -//! WordCraft proofing, per language. A run's language (its BCP 47 tag) picks the tools through -//! [`ProofLang`]; the `*_in` functions take it, the older English-only ones remain. -//! -//! - **English** ([`ProofLang::En`]; also text with no language): spelling against the -//! public-domain Moby word list (~160k words, the same list that drives hyphenation) plus our -//! supplement of modern words it lacks (`assets/spelling/en-extra.txt`: `desktop`, `email`, -//! `download`…), with inflections (`-s`, `-es`, `-ed`, `-ing`, `-ly`, `'s`…), a user dictionary and ignore lists; -//! suggestions by edit distance. Grammar checks that are cheap and reliable: repeated words, -//! `a`/`an`, capital letter after a sentence end, spaces before punctuation, doubled spaces. -//! - **Polish** ([`ProofLang::Pl`], module [`pl`]): spelling against the Hunspell `pl_PL` -//! dictionary installed on the system ([`hunspell`]; not bundled, so without it Polish has no -//! spelling marks), read by the affix engine ([`affix`]), suggestions tuned for Polish -//! (diacritics, `ż`/`rz`, `u`/`ó`, missing spaces), and Polish grammar rules with Polish -//! messages. -//! - **Bulgarian** ([`ProofLang::Bg`], module [`bg`]): hyphenation by the Bulgarian syllable -//! rules; no spelling or grammar marks yet. -//! - **Portuguese** ([`ProofLang::Pt`], module [`pt`]): spelling against the Hunspell `pt_BR` -//! dictionary installed on the system (VERO and friends; not bundled), suggestions tuned for -//! Portuguese (missing accents, `seje`/`seja` confusions, missing spaces), conservative -//! grammar rules with Portuguese messages, and `hyph-utf8`'s Portuguese patterns. -//! - **Italian** ([`ProofLang::It`], module [`it`]): spelling against the Hunspell `it_IT` -//! dictionary installed on the system (not bundled — the upstream file is GPL-licensed), -//! suggestions tuned for Italian (missing accents, `REP` confusions, missing spaces), -//! conservative grammar rules with Italian messages, and `hyph-utf8`'s Italian patterns -//! (Beccari, MIT option). -//! - **Russian** ([`ProofLang::Ru`], module [`ru`]): spelling against the Hunspell `ru_RU` -//! dictionary installed on the system (not bundled either), with `ё` optional and hyphenated -//! words (`кто-то`, `по-русски`, `северо-западный`) understood, suggestions tuned for Russian -//! (`е`/`ё`, `жи`/`ши`, `тся`/`ться`, voiced and voiceless pairs, missing spaces), Russian -//! grammar rules with Russian messages, and Russian word division. -//! - **Czech** ([`ProofLang::Cs`], module [`cs`]): the installed Hunspell `cs_CZ` dictionary -//! (not bundled), dictionary-driven suggestions and generic checks with Czech messages. -//! Hyphenation uses the existing English fallback; no Czech patterns are bundled. -//! - **Indonesian** ([`ProofLang::Id`], module [`id`]): the installed Hunspell `id_ID` -//! dictionary (not bundled), dictionary-driven suggestions and generic checks with Indonesian -//! messages. Hyphenation uses the existing English fallback. -//! - **Spanish** ([`ProofLang::Es`], [`ProofLang::EsMx`], [`ProofLang::Es419`], [`ProofLang::EsUy`], -//! module [`es`]): each variety's installed Hunspell dictionary (`es_ES`, `es_MX`, `es_419`, -//! `es_UY`, with fallbacks; not bundled), dictionary-driven suggestions, Uruguayan `vos` forms, -//! and generic checks plus the opening `¿`/`¡` with Spanish messages. Hyphenation uses the -//! existing English fallback. -//! - **German** ([`ProofLang::De`]): hyphenation by `hyph-utf8`'s German patterns (reformed -//! spelling, MIT); no spelling or grammar marks yet. -//! - **Other languages** ([`ProofLang::Other`]): no spelling or grammar marks, like Word without -//! the language's proofing tools. -//! - **Hyphenation** ([`hyphen`]): dictionary, Liang patterns (English, Polish, Portuguese, -//! Italian, German), Bulgarian syllable rules, Russian word-division rules, heuristic. -//! -//! [`check_text`] checks a paragraph whose runs are in different languages. -#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] - -pub mod affix; -mod affix_suggest; -pub mod bg; -pub mod cs; -pub mod dict; -pub mod en_variants; -pub mod es; -pub mod hr; -pub mod hunspell; -pub mod hyphen; -pub mod id; -pub mod it; -pub mod lang; -pub mod patterns; -pub mod pl; -pub mod pt; -pub mod ru; -mod slot; -mod spelling; -pub mod tr; -pub mod vi; - -use std::collections::HashSet; -use std::ops::Range; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{OnceLock, RwLock}; - -/// What the spelling check skips (File › Options › Proofing). Word's defaults: all on. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct Options { - /// Words in capitals (`NASA`, `WORDCRAFT`) are taken as correct. - pub ignore_uppercase: bool, - /// Words containing digits (`B2B`, `mp3`) are taken as correct. - pub ignore_numbers: bool, - /// Web and email addresses and file paths (`https://…`, `a@b.org`, `www.…`) are skipped. - pub ignore_internet: bool, -} - -impl Default for Options { - fn default() -> Self { - Options { ignore_uppercase: true, ignore_numbers: true, ignore_internet: true } - } -} - -pub use lang::ProofLang; - -/// A problem found in text: byte range, kind, message and suggestions. -#[derive(Clone, Debug, PartialEq)] -pub struct Issue { - pub start: usize, - pub end: usize, - pub kind: IssueKind, - /// In the language of the text it was found in (Polish rules explain in Polish). - pub message: String, - pub suggestions: Vec, - /// The language the text was checked in: spelling suggestions come from its dictionary - /// ([`suggest_in`]). - pub lang: ProofLang, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IssueKind { - Spelling, - Grammar, -} - -fn user_words() -> &'static RwLock> { - static U: OnceLock>> = OnceLock::new(); - U.get_or_init(|| RwLock::new(HashSet::new())) -} - -/// Bumped whenever the user dictionary changes, so cached proofing results can be dropped. -static DICTIONARY_GENERATION: AtomicU64 = AtomicU64::new(0); - -/// The key a word has in the user dictionary and the Ignore All list: lower case and composed -/// (NFC), as spelling looks words up ([`composed`]). A word added, ignored, removed or loaded -/// decomposed (`zqprue\u{301}ba`) is then the same entry as its composed form (`zqpruéba`), and -/// the other way round (#3562 review). -fn dictionary_key(w: &str) -> String { - composed(&w.to_lowercase()).into_owned() -} - -/// Add a word to the user dictionary ("Add to Dictionary"). It applies to every language. -pub fn add_word(w: &str) { - if user_words().write().unwrap_or_else(std::sync::PoisonError::into_inner).insert(dictionary_key(w)) { - DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); - } -} - -/// Take a word out of the user dictionary (File › Options › Proofing). Returns whether it was there. -pub fn remove_word(w: &str) -> bool { - let removed = user_words().write().unwrap_or_else(std::sync::PoisonError::into_inner).remove(&dictionary_key(w)); - if removed { - DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); - } - removed -} - -fn ignored_words() -> &'static RwLock> { - static I: OnceLock>> = OnceLock::new(); - I.get_or_init(|| RwLock::new(HashSet::new())) -} - -/// Ignore a word everywhere until the app closes ("Ignore All"). Unlike [`add_word`] it isn't -/// part of the user dictionary: it is never listed, saved or loaded, as in Word. -pub fn ignore_word(w: &str) { - if ignored_words().write().unwrap_or_else(std::sync::PoisonError::into_inner).insert(dictionary_key(w)) { - DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); - } -} - -/// A language proofed against an installed Hunspell dictionary: all the functions below need to -/// know about it, so a language joins with one row in [`LANGUAGES`]. -struct Language { - lang: ProofLang, - slot: &'static dyn slot::Slot, - /// The spelling mark's message, in the language. - message: &'static str, - is_correct: fn(&str, ProofLang, &Options) -> bool, - suggest: fn(&str, usize, ProofLang) -> Vec, - /// Grammar issues; the flag says whether the first word must have a capital (not in a list - /// item). - grammar: fn(&str, ProofLang, bool) -> Vec, -} - -/// Every language proofed against a Hunspell dictionary, in the order File › Options › Proofing -/// lists them. English has a built-in word list and is handled apart. -static LANGUAGES: [Language; 12] = [ - Language { - lang: ProofLang::Pl, - slot: &pl::DICT, - message: "Możliwy błąd pisowni", - is_correct: |w, _, o| pl::is_correct_with_options(w, o), - suggest: |w, n, _| pl::suggest(w, n), - grammar: |t, _, c| pl::grammar::check_with(t, c), - }, - Language { - lang: ProofLang::Pt, - slot: &pt::DICT, - message: "Possível erro de ortografia", - is_correct: |w, _, o| pt::is_correct_with_options(w, o), - suggest: |w, n, _| pt::suggest(w, n), - grammar: |t, _, c| pt::grammar::check_with(t, c), - }, - Language { - lang: ProofLang::It, - slot: &it::DICT, - message: "Possibile errore di ortografia", - is_correct: |w, _, o| it::is_correct_with_options(w, o), - suggest: |w, n, _| it::suggest(w, n), - grammar: |t, _, c| it::grammar::check_with(t, c), - }, - Language { - lang: ProofLang::Hr, - slot: &hr::DICT, - message: "Moguća pravopisna pogreška", - is_correct: |w, _, o| hr::is_correct_with_options(w, o), - suggest: |w, n, _| hr::suggest(w, n), - grammar: |t, _, c| hr::grammar::check_with(t, c), - }, - Language { - lang: ProofLang::Tr, - slot: &tr::DICT, - message: "Olası yazım hatası", - is_correct: |w, _, o| tr::is_correct_with_options(w, o), - suggest: |w, n, _| tr::suggest(w, n), - grammar: |t, _, c| tr::grammar::check_with(t, c), - }, - Language { - lang: ProofLang::Ru, - slot: &ru::DICT, - message: "Возможна орфографическая ошибка", - is_correct: |w, _, o| ru::is_correct_with_options(w, o), - suggest: |w, n, _| ru::suggest(w, n), - grammar: |t, _, c| ru::grammar::check_with(t, c), - }, - // Czech, Indonesian and Spanish have no sentence-capital rule, so a list item needs no - // exception. - Language { - lang: ProofLang::Cs, - slot: &cs::DICT, - message: "Možná pravopisná chyba", - is_correct: |w, _, o| cs::is_correct_with_options(w, o), - suggest: |w, n, _| cs::suggest(w, n), - grammar: |t, _, _| cs::grammar::check(t), - }, - Language { - lang: ProofLang::Id, - slot: &id::DICT, - message: "Kemungkinan salah eja", - is_correct: |w, _, o| id::is_correct_with_options(w, o), - suggest: |w, n, _| id::suggest(w, n), - grammar: |t, _, _| id::grammar::check(t), - }, - Language { - lang: ProofLang::Es, - slot: &es::SPAIN_DICT, - message: "Posible error ortográfico", - is_correct: es::is_correct_with_options, - suggest: es::suggest, - grammar: |t, l, _| es::grammar::check(t, l), - }, - Language { - lang: ProofLang::EsMx, - slot: &es::MEXICO_DICT, - message: "Posible error ortográfico", - is_correct: es::is_correct_with_options, - suggest: es::suggest, - grammar: |t, l, _| es::grammar::check(t, l), - }, - Language { - lang: ProofLang::Es419, - slot: &es::LATIN_AMERICA_DICT, - message: "Posible error ortográfico", - is_correct: es::is_correct_with_options, - suggest: es::suggest, - grammar: |t, l, _| es::grammar::check(t, l), - }, - Language { - lang: ProofLang::EsUy, - slot: &es::URUGUAY_DICT, - message: "Posible error ortográfico", - is_correct: es::is_correct_with_options, - suggest: es::suggest, - grammar: |t, l, _| es::grammar::check(t, l), - }, -]; - -/// `lang`'s row in [`LANGUAGES`]; `None` for English and the languages WordCraft doesn't proof. -fn language(lang: ProofLang) -> Option<&'static Language> { - LANGUAGES.iter().find(|l| l.lang == lang) -} - -/// Search `dir` for Hunspell dictionaries before the system locations (File › Options › -/// Proofing › Dictionaries folder, #1063); `None` clears it. The dictionaries load again from -/// the new search path, and text is checked again. -pub fn set_dictionaries_folder(dir: Option) { - hunspell::set_dictionary_dir(dir); - for l in &LANGUAGES { - l.slot.reload(); - } - DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); -} - -/// Each language that reads an installed Hunspell dictionary, with the `.dic` file found for it -/// now (`None`: not installed, so the language has no spelling marks). -pub fn installed_dictionaries() -> Vec<(ProofLang, Option)> { - LANGUAGES.iter().map(|l| (l.lang, l.slot.installed_path())).collect() -} - -/// Why the dictionary found for `lang` failed to load (unsupported encoding, too large, a parse -/// error), if it was read and failed. Doesn't read anything itself: a dictionary nobody has -/// checked yet reports `None` (#2830). -pub fn dictionary_load_error(lang: ProofLang) -> Option { - hunspell::load_error(language(lang)?.slot.name()).map(|(_, reason)| reason) -} - -/// A number that changes whenever a word is added to or removed from the user dictionary, or the -/// dictionaries folder changes. -pub fn dictionary_generation() -> u64 { - DICTIONARY_GENERATION.load(Ordering::Relaxed) -} - -/// Is `lower` (a lower-case word) in the user dictionary or ignored? It is looked up composed, as -/// the entries are kept ([`dictionary_key`]). -pub(crate) fn in_user_dictionary(lower: &str) -> bool { - let key = composed(lower); - user_words().read().unwrap_or_else(std::sync::PoisonError::into_inner).contains(&*key) - || ignored_words().read().unwrap_or_else(std::sync::PoisonError::into_inner).contains(&*key) -} - -/// Words in the user dictionary. -pub fn user_dictionary() -> Vec { - let mut v: Vec = user_words().read().unwrap_or_else(std::sync::PoisonError::into_inner).iter().cloned().collect(); - v.sort(); - v -} - -/// Longest custom-dictionary word kept, in characters (mirrors `file.options` validation). -pub const MAX_CUSTOM_WORD_CHARS: usize = 64; -/// Most custom-dictionary words read from one file (caps a hostile file). -const MAX_CUSTOM_WORDS: usize = 100_000; -/// Largest custom-dictionary file read, in bytes (caps a hostile file). -const MAX_CUSTOM_FILE_BYTES: u64 = 1_000_000; - -/// `w` as a custom-dictionary word: trimmed and lower-cased, or `None` when it isn't a single -/// word of at most [`MAX_CUSTOM_WORD_CHARS`] characters (empty, spaces, control characters, or -/// bytes that weren't UTF-8). -/// Adding and loading both use it, so a word that can be added is never dropped on the next load. -pub fn custom_word(w: &str) -> Option { - let w = w.trim(); - if w.is_empty() - || w.chars().count() > MAX_CUSTOM_WORD_CHARS - || w.chars().any(|c| c.is_whitespace() || c.is_control() || c == char::REPLACEMENT_CHARACTER) - { - return None; - } - Some(dictionary_key(w)) -} - -/// The valid words of the custom-dictionary file at `path`, reading at most -/// [`MAX_CUSTOM_FILE_BYTES`] of it and keeping at most [`MAX_CUSTOM_WORDS`]. A missing or -/// unreadable file reads as no words. -fn read_dictionary_file(path: &std::path::Path) -> Vec { - use std::io::Read; - let mut bytes = Vec::new(); - let Ok(file) = std::fs::File::open(path) else { return Vec::new() }; - // `take` bounds the read itself, so a huge file is never read whole. - if file.take(MAX_CUSTOM_FILE_BYTES).read_to_end(&mut bytes).is_err() { - return Vec::new(); - } - // Lossy: a cut multi-byte tail becomes U+FFFD and is skipped by validation below, - // instead of discarding the whole file. - String::from_utf8_lossy(&bytes).lines().filter_map(custom_word).take(MAX_CUSTOM_WORDS).collect() -} - -/// Read custom-dictionary words from a file into the user dictionary (one word per line; -/// blank lines are skipped). A missing file reads as empty; a hostile file is capped, -/// never a panic and never an error that stops startup. Returns the words added. -pub fn load_user_dictionary(path: &std::path::Path) -> Vec { - let words = read_dictionary_file(path); - let mut set = user_words().write().unwrap_or_else(std::sync::PoisonError::into_inner); - let mut added = Vec::new(); - for w in words { - if set.len() >= MAX_CUSTOM_WORDS { - break; - } - if set.insert(w.clone()) { - added.push(w); - } - } - if !added.is_empty() { - DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); - } - added -} - -/// Write the user dictionary to a file (one word per line, sorted), creating its folder. -/// -/// Words another process (a second window, the MCP bridge) saved to the file since it was loaded -/// are merged in first, so neither loses the other's words; `removed` (words just taken out) stay -/// out. The file is replaced atomically: written beside it, then renamed over it. Errors (a -/// read-only folder, a full disk) go back to the caller; the in-memory words are kept. -pub fn save_user_dictionary(path: &std::path::Path, removed: &[String]) -> std::io::Result<()> { - let removed: HashSet = removed.iter().map(|w| dictionary_key(w)).collect(); - let on_disk = read_dictionary_file(path); - { - let mut set = user_words().write().unwrap_or_else(std::sync::PoisonError::into_inner); - let mut changed = false; - for w in on_disk { - if set.len() >= MAX_CUSTOM_WORDS { - break; - } - if !removed.contains(&w) && set.insert(w) { - changed = true; - } - } - for w in &removed { - changed |= set.remove(w); - } - if changed { - DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); - } - } - let mut out = String::new(); - for w in user_dictionary() { - out.push_str(&w); - out.push('\n'); - } - if let Some(dir) = path.parent().filter(|d| !d.as_os_str().is_empty()) { - std::fs::create_dir_all(dir)?; - } - let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "custom-dictionary.txt".into()); - let tmp = path.with_file_name(format!(".{name}.{}.tmp", std::process::id())); - let written = std::fs::write(&tmp, out).and_then(|()| std::fs::rename(&tmp, path)); - if written.is_err() { - let _ = std::fs::remove_file(&tmp); - } - written -} - -/// Common words the source list lacks (modern vocabulary, contractions). -const COMMON: &str = "a an the and or but if of to in on at by for with from as is am are was were be been being it its we us our you your he him his she her they them their i me my mine this that these those there here what which who whom whose when where why how all any both each few more most other some such no nor not only own same so than too very can will just should now do does did done has have had having may might must shall would could up down out over under again further then once off into onto upon about above below between through during before after until while because per via vs etc ok yes yeah oh ah hi hey bye one two three four five six seven eight nine ten first last next new old get got go goes went gone see saw seen say said make made take took come came give gave know knew think thought look want need use used find found tell told ask asked feel felt try left keep let put mean set run show turn move live play pay hear hold bring begin seem help talk start might"; - -/// Our supplement of common words the Moby list lacks (`desktop`, `email`, `download`…): one -/// lowercase base form per line, `#` comments. Inflections come from [`known_core`]. -const EXTRA_EN: &str = include_str!("../../../assets/spelling/en-extra.txt"); - -fn extra_words() -> &'static HashSet<&'static str> { - static E: OnceLock> = OnceLock::new(); - E.get_or_init(|| EXTRA_EN.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')).collect()) -} - -/// A base form in the dictionary or the supplement. -fn base_known(d: &dict::Dictionary, w: &str) -> bool { - d.get(w).is_some() || extra_words().contains(w) -} - -fn known_core(w: &str) -> bool { - let d = dict::Dictionary::en_us(); - if base_known(d, w) || COMMON.split(' ').any(|c| c == w) { - return true; - } - if in_user_dictionary(w) { - return true; - } - // Inflections. - let stems: [(&str, &[&str]); 13] = [ - ("'s", &[""]), - ("s", &[""]), - ("es", &["", "e"]), - ("ies", &["y"]), - ("ed", &["", "e"]), - ("ied", &["y"]), - ("ing", &["", "e"]), - ("ly", &["", "le"]), - ("ily", &["y"]), - // specifically → specific (#1694): only -ic adjectives, so "desktopally" stays wrong. - ("ically", &["ic"]), - ("er", &["", "e"]), - ("est", &["", "e"]), - ("ness", &[""]), - ]; - for (suf, adds) in stems { - if let Some(stem) = w.strip_suffix(suf) - && stem.len() >= 2 - { - for a in adds { - let s = format!("{stem}{a}"); - if base_known(d, &s) { - return true; - } - } - // Doubled consonant: running → run, stopped → stop. - let b = stem.as_bytes(); - if (suf == "ing" || suf == "ed" || suf == "er" || suf == "est") - && b.len() >= 3 - && b.last() == b.get(b.len() - 2) - && let Some(s) = stem.get(..stem.len() - 1) - && base_known(d, s) - { - return true; - } - } - } - // Prefixes. - for pre in ["un", "re", "non", "pre", "over", "under", "co", "mis"] { - if let Some(rest) = w.strip_prefix(pre) - && rest.len() >= 3 - && base_known(d, rest) - { - return true; - } - } - false -} - -/// Is there a proofing dictionary for this BCP 47 language tag? `None` (and empty tags) mean the -/// document default, which is en-US. Runs in a language without one are skipped (#100) instead -/// of flagging every word against another language's dictionary; see [`ProofLang::from_tag`]. -pub fn has_dictionary(lang: Option<&str>) -> bool { - ProofLang::from_tag(lang).is_proofed() -} - -/// The Hunspell dictionary name (`pl_PL`, `it_IT`) a proofing language needs but doesn't have -/// installed, for a status-bar hint (#1167). `None` when the language's dictionary is available, -/// or when it needs none (English has a built-in word list; unproofed languages aren't offered -/// one). Reads the dictionary once (it is cached afterwards). -pub fn missing_dictionary(lang: Option<&str>) -> Option<&'static str> { - let lang = ProofLang::from_tag(lang); - dictionary_needed(lang, dictionary_loaded(lang)?) -} - -/// The Hunspell name, `.dic` file and reason of a dictionary that is installed for this language -/// but couldn't be read (unsupported `SET` encoding, too large, a parse error). `None` when it -/// loaded, or when no file exists (then [`missing_dictionary`] names it). Reads the dictionary -/// once (it is cached afterwards). -pub fn unreadable_dictionary(lang: Option<&str>) -> Option<(&'static str, std::path::PathBuf, String)> { - let lang = ProofLang::from_tag(lang); - let name = dictionary_needed(lang, dictionary_loaded(lang)?)?; - hunspell::load_error(name).map(|(file, reason)| (name, file, reason)) -} - -/// Did the Hunspell dictionary of `lang` load? `None` for languages that use none (English). -fn dictionary_loaded(lang: ProofLang) -> Option { - language(lang).map(|l| l.slot.has_dictionary()) -} - -/// The Hunspell name `lang` needs when `installed` is false (the first name its module looks for; -/// a Spanish variety's is its own: `es_ES`, `es_MX`, `es_419`, `es_UY`). -fn dictionary_needed(lang: ProofLang, installed: bool) -> Option<&'static str> { - language(lang).filter(|_| !installed).map(|l| l.slot.name()) -} - -/// Is `word` spelled correctly in the run's proofing language? A language without a dictionary -/// can't be checked, so every word counts as correct (no red underlines for it). -pub fn is_correct_for(word: &str, lang: Option<&str>) -> bool { - is_correct_in(word, ProofLang::from_tag(lang)) -} - -/// Is `word` spelled correctly? Numbers, single letters, ALL-CAPS acronyms, URLs and words with -/// digits are accepted. -pub fn is_correct(word: &str) -> bool { - is_correct_with(word, &Options::default()) -} - -/// The words every language's check accepts under `opts`: single letters, words without -/// letters, words with digits (Ignore words that contain numbers), words in capitals -/// (Ignore words in UPPERCASE) and words in the user dictionary. -fn accepted_by_options(word: &str, opts: &Options) -> bool { - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'').trim_matches('\''); - w.chars().count() <= 1 - || !w.chars().any(char::is_alphabetic) - || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) - || (opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase())) - // The whole word: the languages split a hyphenated word into its parts, and an added - // `Unigranrio-RJ` is none of them (#2082). - || in_user_dictionary(&w.to_lowercase()) -} - -/// [`is_correct`] with the File › Options › Proofing choices. -pub fn is_correct_with(word: &str, opts: &Options) -> bool { - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\''); - let w = w.trim_matches('\''); - if accepted_by_options(w, opts) { - return true; - } - if !w.is_ascii() && !w.chars().any(|c| c.is_ascii_alphabetic()) { - return true; // other scripts: no dictionary - } - let lower = w.to_lowercase().replace('’', "'"); - if known_core(&lower) { - return true; - } - // Hyphenated compounds: each part. - if lower.contains('-') { - return lower.split('-').all(|p| p.is_empty() || known_core(p)); - } - false -} - -/// Spelling suggestions for a misspelled word (best first, at most `max`). -pub fn suggest(word: &str, max: usize) -> Vec { - let lower = word.to_lowercase(); - let n = lower.chars().count(); - if n == 0 || n > 40 { - return Vec::new(); - } - let d = dict::Dictionary::en_us(); - let mut scored: Vec<(usize, i64, String)> = Vec::new(); - let target: Vec = lower.chars().collect(); - for w in d.iter().map(|(w, _)| w).chain(extra_words().iter().copied()) { - let wn = w.chars().count(); - if wn + 2 < n || wn > n + 2 { - continue; - } - let dist = damerau(&target, w); - if dist <= 2 { - let same_first = w.chars().next() == target.first().copied(); - scored.push((dist, if same_first { 0 } else { 1 }, w.to_string())); - } - } - scored.sort_by(|a, b| (a.0, a.1, a.2.len().abs_diff(n)).cmp(&(b.0, b.1, b.2.len().abs_diff(n))).then(a.2.cmp(&b.2))); - scored.into_iter().take(max).map(|(_, _, w)| match_case(word, w)).collect() -} - -/// `w` (lower case) in the case of `word`: all capitals, an initial capital, or as it is. -fn match_case(word: &str, w: String) -> String { - let cap = word.chars().next().is_some_and(char::is_uppercase); - let upper = word.chars().all(|c| !c.is_lowercase()); - if upper && word.chars().count() > 1 { - w.to_uppercase() - } else if cap { - let mut c = w.chars(); - c.next().map(|f| f.to_uppercase().chain(c).collect()).unwrap_or(w) - } else { - w - } -} - -/// `lower` in the spelling `lang` (an English) wants, when it or a part of a hyphenated compound -/// is the other dialect's spelling (`colour-blind` → `color-blind` in American English). -fn english_preferred(lower: &str, lang: ProofLang) -> Option { - let mut changed = false; - let parts: Vec = lower - .split('-') - .map(|p| match en_variants::preferred(p, lang) { - Some(n) => { - changed = true; - n - } - None => p.to_string(), - }) - .collect(); - changed.then(|| parts.join("-")) -} - -/// Is `word` spelled right in the English `lang` ([`ProofLang::En`], [`ProofLang::EnGb`], -/// [`ProofLang::EnCa`])? The shared word list's words, less the other dialect's spellings -/// (`colour` in American English, `color` in British), plus this dialect's spellings of the -/// list's words ([`en_variants`]). The user dictionary overrules both. -fn is_correct_english(word: &str, lang: ProofLang, opts: &Options) -> bool { - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'').trim_matches('\''); - if accepted_by_options(w, opts) { - return true; - } - let lower = w.to_lowercase().replace('’', "'"); - if in_user_dictionary(&lower) { - return true; - } - let parts: Vec<&str> = lower.split('-').filter(|p| !p.is_empty()).collect(); - if parts.iter().any(|p| en_variants::is_foreign(p, lang)) { - return false; - } - is_correct_with(word, opts) || (!parts.is_empty() && parts.iter().all(|p| known_core(p) || en_variants::known_variant(p, lang, known_core))) -} - -/// Spelling suggestions in the English `lang`: this dialect's spelling first when `word` is the -/// other dialect's, then the shared suggestions in this dialect's spelling. -fn suggest_english(word: &str, max: usize, lang: ProofLang) -> Vec { - let lower = word.to_lowercase().replace('’', "'"); - let mut out: Vec = Vec::new(); - if let Some(p) = english_preferred(&lower, lang) { - out.push(match_case(word, p)); - } - for s in suggest(word, max.saturating_add(2)) { - let s = english_preferred(&s.to_lowercase(), lang).map_or(s, |p| match_case(word, p)); - if !out.contains(&s) { - out.push(s); - } - } - out.truncate(max); - out -} - -/// Optimal string alignment distance, early-exit at 3. -fn damerau(a: &[char], b: &str) -> usize { - let b: Vec = b.chars().collect(); - let (n, m) = (a.len(), b.len()); - let mut prev2 = vec![0usize; m + 1]; - let mut prev: Vec = (0..=m).collect(); - let mut cur = vec![0usize; m + 1]; - for i in 1..=n { - if let Some(c) = cur.get_mut(0) { - *c = i; - } - let mut row_min = i; - for j in 1..=m { - let (ai, bj) = (a.get(i - 1), b.get(j - 1)); - let cost = usize::from(ai != bj); - let mut v = (prev.get(j).copied().unwrap_or(9) + 1) - .min(cur.get(j - 1).copied().unwrap_or(9) + 1) - .min(prev.get(j - 1).copied().unwrap_or(9) + cost); - if i > 1 && j > 1 && a.get(i - 1) == b.get(j - 2) && a.get(i - 2) == b.get(j - 1) { - v = v.min(prev2.get(j - 2).copied().unwrap_or(9) + 1); - } - if let Some(c) = cur.get_mut(j) { - *c = v; - } - row_min = row_min.min(v); - } - if row_min > 2 { - return 3; - } - std::mem::swap(&mut prev2, &mut prev); - std::mem::swap(&mut prev, &mut cur); - } - prev.get(m).copied().unwrap_or(9) -} - -/// Words in `text`: (byte start, byte end). Combining marks belong to the word they follow, so a -/// word typed decomposed (`tambie\u{301}n`, NFD) is one word, as its precomposed form is. -pub fn words(text: &str) -> Vec<(usize, usize)> { - let mut out = Vec::new(); - let mut start: Option = None; - let is_w = |c: char| c.is_alphanumeric() || c == '\'' || c == '’' || c == '-' || unicode_normalization::char::is_combining_mark(c); - for (i, c) in text.char_indices() { - match (start, is_w(c)) { - (None, true) => start = Some(i), - (Some(s), false) => { - out.push((s, i)); - start = None; - } - _ => {} - } - } - if let Some(s) = start { - out.push((s, text.len())); - } - // Trim surrounding apostrophes/hyphens. - out.into_iter() - .filter_map(|(a, b)| { - let w = text.get(a..b)?; - let lead = w.len() - w.trim_start_matches(['\'', '’', '-']).len(); - let trail = w.len() - w.trim_end_matches(['\'', '’', '-']).len(); - (a + lead < b - trail).then_some((a + lead, b - trail)) - }) - .collect() -} - -/// Spelling issues in a paragraph of English text (no suggestions; ask [`suggest`] on demand). -pub fn check_spelling(text: &str) -> Vec { - check_spelling_with(text, &Options::default()) -} - -/// [`check_spelling`] with the File › Options › Proofing choices. -pub fn check_spelling_with(text: &str, opts: &Options) -> Vec { - spelling_issues(text, |_| ProofLang::En, opts) -} - -/// Spelling issues in a paragraph of text in `lang` (none for a language without a dictionary; -/// no suggestions: ask [`suggest_in`] on demand). -pub fn check_spelling_in(text: &str, lang: ProofLang) -> Vec { - if !lang.is_proofed() { - return Vec::new(); - } - spelling_issues(text, |_| lang, &Options::default()) -} - -/// Spelling issues of every word of `text`, each word spelled in `lang(word_start)`. -/// -/// The web/e-mail classification of the whitespace-delimited token around a word is computed -/// once per token and shared by every word punctuation splits out of it. Classifying per word -/// rescanned the token each time, so a long whitespace-free run of punctuation-separated words -/// cost quadratic scanning (#1335). -fn spelling_issues(text: &str, lang: impl Fn(usize) -> ProofLang, opts: &Options) -> Vec { - let mut token = (0, 0, false); - words(text) - .into_iter() - .filter_map(|(a, b)| { - if a < token.0 || a >= token.1 { - token = enclosing_token(text, a, b); - } - spelling_issue(text, a, b, lang(a), opts, token.2) - }) - .collect() -} - -/// The whitespace-delimited token enclosing the word at `a..b` of `text`: its byte range, and -/// whether it looks like a web or e-mail address. -fn enclosing_token(text: &str, a: usize, b: usize) -> (usize, usize, bool) { - // The token starts after the last white-space character, whatever its width (a no-break - // space is two bytes and an ideographic space is three). - let ts = text.get(..a).and_then(|t| t.char_indices().rev().find(|(_, c)| c.is_whitespace())).map(|(i, c)| i + c.len_utf8()).unwrap_or(0); - let te = text.get(b..).and_then(|t| t.find(char::is_whitespace)).map(|i| b + i).unwrap_or(text.len()); - (ts, te, text.get(ts..te).is_some_and(is_address)) -} - -/// Does a whitespace-delimited token look like a web or e-mail address (`https://…`, `www.…`, -/// `a@b.org`)? Spelling skips its words, and Spanish grammar its punctuation. -pub(crate) fn is_address(token: &str) -> bool { - token.contains("://") || token.contains('@') || token.starts_with("www.") -} - -/// The spelling issue of the word at `a..b` of `text`, if it is misspelled in `lang`. Words in -/// web and e-mail addresses are skipped (unless `opts` says otherwise); `internet` is the -/// classification of the word's enclosing token, from [`enclosing_token`]. -fn spelling_issue(text: &str, a: usize, b: usize, lang: ProofLang, opts: &Options, internet: bool) -> Option { - let w = text.get(a..b)?; - // A word with a Vietnamese-only letter (a name like Nguyễn in English text) isn't English. - if (opts.ignore_internet && internet) || (lang.is_english() && vi::is_vietnamese_word(w)) || is_correct_in_with(w, lang, opts) { - return None; - } - let message = language(lang).map_or("Possible spelling mistake", |l| l.message); - Some(Issue { start: a, end: b, kind: IssueKind::Spelling, message: message.into(), suggestions: Vec::new(), lang }) -} - -/// Is `word` spelled correctly in `lang`? Always true in a language without a dictionary. -pub fn is_correct_in(word: &str, lang: ProofLang) -> bool { - is_correct_in_with(word, lang, &Options::default()) -} - -/// `word` composed (NFC), as the dictionaries spell their words: `tambie\u{301}n` is looked up as -/// `también`. Words already composed (all ASCII ones among them) are borrowed as they are. -fn composed(word: &str) -> std::borrow::Cow<'_, str> { - use unicode_normalization::{IsNormalized, UnicodeNormalization, is_nfc_quick}; - if word.is_ascii() || is_nfc_quick(word.chars()) == IsNormalized::Yes { - std::borrow::Cow::Borrowed(word) - } else { - std::borrow::Cow::Owned(word.nfc().collect()) - } -} - -/// [`is_correct_in`] with the File › Options › Proofing choices. A word with combining marks is -/// checked as its composed form ([`composed`]). -pub fn is_correct_in_with(word: &str, lang: ProofLang, opts: &Options) -> bool { - let word = &*composed(word); - if lang.is_english() { - return is_correct_english(word, lang, opts); - } - // A language WordCraft doesn't proof (Bulgarian, German, any other) has no spelling marks. - language(lang).is_none_or(|l| accepted_by_options(word, opts) || (l.is_correct)(word, lang, opts)) -} - -/// Spelling suggestions for a word misspelled in `lang` (best first, at most `max`), composed -/// ([`composed`]) like the dictionaries' words. -pub fn suggest_in(word: &str, max: usize, lang: ProofLang) -> Vec { - let word = &*composed(word); - if lang.is_english() { - return suggest_english(word, max, lang); - } - language(lang).map_or_else(Vec::new, |l| (l.suggest)(word, max, lang)) -} - -/// Grammar issues in a paragraph of text in `lang`: the English rules, the Polish ones -/// ([`pl::grammar`]), the Portuguese ones ([`pt::grammar`]), the Italian ones ([`it::grammar`]), -/// the Turkish ones ([`tr::grammar`]), Czech and Indonesian generic checks ([`cs::grammar`], [`id::grammar`]), the Spanish ones -/// ([`es::grammar`]), or none for a language WordCraft doesn't proof. -pub fn check_grammar_in(text: &str, lang: ProofLang) -> Vec { - grammar_in(text, lang, true) -} - -/// [`check_grammar_in`] where `capital_first` says whether the first word must be capitalized -/// (false for a list item). -fn grammar_in(text: &str, lang: ProofLang, capital_first: bool) -> Vec { - match lang { - ProofLang::En => grammar_en(text, capital_first), - ProofLang::EnGb | ProofLang::EnCa => grammar_en(text, capital_first).into_iter().map(|i| Issue { lang, ..i }).collect(), - _ => language(lang).map_or_else(Vec::new, |l| (l.grammar)(text, lang, capital_first)), - } -} - -/// Spelling and grammar issues in a paragraph whose text is in several languages, in order. -/// `spans` gives the language of byte ranges of `text` (in order, not overlapping); text outside -/// them is English. Each word is spelled in the language of its first letter. Each language's -/// grammar rules read the whole paragraph (so sentence boundaries are seen) but report only -/// inside that language's spans: an English quotation in a Polish paragraph gets English -/// spelling and is never told a Polish rule. -pub fn check_text(text: &str, spans: &[(Range, ProofLang)]) -> Vec { - check_text_with(text, spans, &Options::default(), true, false) -} - -/// [`check_text`] with the File › Options › Proofing choices: what spelling skips, and whether -/// grammar is checked at all. -/// -/// `list_item` marks a list paragraph: items are often fragments, so the English rule asking for -/// a capital on the paragraph's first word is off for it (later sentences are still checked). -pub fn check_text_with(text: &str, spans: &[(Range, ProofLang)], opts: &Options, grammar: bool, list_item: bool) -> Vec { - // Sorted, empty ones dropped, neighbours of one language merged. - let mut sorted: Vec<&(Range, ProofLang)> = spans.iter().filter(|(r, _)| r.start < r.end).collect(); - sorted.sort_by_key(|s| s.0.start); - let mut merged: Vec<(Range, ProofLang)> = Vec::new(); - for (r, l) in sorted { - match merged.last_mut() { - Some((m, ml)) if *ml == *l && m.end == r.start => m.end = r.end, - _ => merged.push((r.clone(), *l)), - } - } - // A paragraph typed in Vietnamese under the default English language is Vietnamese, which - // has no proofing tools: no marks (Word detects the language the same way). - let default = if vi::looks_vietnamese(text) { - for (_, l) in merged.iter_mut().filter(|(_, l)| l.is_english()) { - *l = ProofLang::Other; - } - ProofLang::Other - } else { - ProofLang::En - }; - let lang_at = |i: usize| -> ProofLang { - let k = merged.partition_point(|(r, _)| r.end <= i); - merged.get(k).filter(|(r, _)| r.start <= i).map_or(default, |s| s.1) - }; - let mut v: Vec = spelling_issues(text, lang_at, opts); - if !grammar { - v.sort_by_key(|i| (i.start, i.end)); - return v; - } - let mut langs: Vec = merged.iter().map(|s| s.1).collect(); - let mut covered = 0; - for (r, _) in &merged { - if r.start > covered { - break; - } - covered = covered.max(r.end); - } - if covered < text.len() || merged.is_empty() { - langs.push(default); - } - langs.sort(); - langs.dedup(); - for lang in langs { - let found = grammar_in(text, lang, !list_item); - v.extend(found.into_iter().filter(|i| lang_at(i.start) == lang)); - } - v.sort_by_key(|i| (i.start, i.end)); - v -} - -/// Grammar issues in a paragraph of English text. -pub fn check_grammar(text: &str) -> Vec { - grammar_en(text, true) -} - -/// The English rules; `capital_first` is whether the text's first word must start a sentence -/// with a capital (false for a list item, which may be a fragment). -fn grammar_en(text: &str, capital_first: bool) -> Vec { - let mut v = Vec::new(); - let ws = words(text); - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if gap.trim().is_empty() - && w0.eq_ignore_ascii_case(w1) - && w0.chars().all(char::is_alphabetic) - && !matches!(w0.to_lowercase().as_str(), "had" | "that" | "bye" | "so") - { - v.push(Issue { - start: *a0, - end: *b1, - kind: IssueKind::Grammar, - message: format!("Repeated word: \"{w1}\""), - suggestions: vec![w0.to_string()], - lang: ProofLang::En, - }); - } - if gap == " " && (w0 == "a" || w0 == "A") && w1.chars().next().is_some_and(|c| "aeiouAEIOU".contains(c)) && !starts_consonant_sound(w1) { - v.push(Issue { - start: *a0, - end: *b0, - kind: IssueKind::Grammar, - message: "Use \"an\" before a vowel sound".into(), - suggestions: vec![if w0 == "A" { "An" } else { "an" }.into()], - lang: ProofLang::En, - }); - } - if gap == " " - && (w0 == "an" || w0 == "An") - && w1.chars().next().is_some_and(|c| c.is_alphabetic() && !"aeiouAEIOUhH".contains(c)) - && !w1.chars().all(char::is_uppercase) - { - v.push(Issue { - start: *a0, - end: *b0, - kind: IssueKind::Grammar, - message: "Use \"a\" before a consonant sound".into(), - suggestions: vec![if w0 == "An" { "A" } else { "a" }.into()], - lang: ProofLang::En, - }); - } - } - // Space before punctuation; doubled spaces. - for (i, _) in text.match_indices(" ,").chain(text.match_indices(" .")).chain(text.match_indices(" ;")) { - if text.get(i + 2..).is_some_and(|r| r.starts_with('.') || r.starts_with(',')) { - continue; // "..." or numbers like " .5" - } - v.push(Issue { - start: i, - end: i + 2, - kind: IssueKind::Grammar, - message: "Remove the space before the punctuation".into(), - suggestions: vec![text.get(i + 1..i + 2).unwrap_or("").to_string()], - lang: ProofLang::En, - }); - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(Issue { - start: i, - end: i + 2, - kind: IssueKind::Grammar, - message: "Extra space".into(), - suggestions: vec![" ".into()], - lang: ProofLang::En, - }); - } - // Sentence start capital. - let mut after_end = capital_first; - for (a, b) in &ws { - let Some(w) = text.get(*a..*b) else { continue }; - if after_end && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) { - let fixed: String = w.chars().next().map(|f| f.to_uppercase().chain(w.chars().skip(1)).collect()).unwrap_or_default(); - v.push(Issue { - start: *a, - end: *b, - kind: IssueKind::Grammar, - message: "Capitalize the first word of a sentence".into(), - suggestions: vec![fixed], - lang: ProofLang::En, - }); - } - let tail = text.get(*b..).unwrap_or(""); - let punctuation = tail.trim_start_matches(['"', '”', ')', '\'']); - // Two or more dots are an ellipsis, not a full stop ending a sentence. - // A Unicode ellipsis (U+2026) does not match sentence-ending punctuation either. - after_end = matches!(punctuation.chars().next(), Some('.' | '!' | '?')) - && !punctuation.starts_with("..") - && punctuation.trim_start_matches(['.', '!', '?', '"', '”', ')']).starts_with(' '); - } - v.sort_by_key(|i| i.start); - v -} - -fn starts_consonant_sound(w: &str) -> bool { - let l = w.to_lowercase(); - ["uni", "use", "usu", "eu", "one", "once", "ubiq", "uti", "ure", "uro"].iter().any(|p| l.starts_with(p)) -} - -#[cfg(test)] -mod tests { - #[test] - fn missing_dictionary_is_named_only_for_hunspell_languages_without_one() { - assert_eq!(dictionary_needed(ProofLang::Pl, false), Some("pl_PL")); - assert_eq!(dictionary_needed(ProofLang::Pt, false), Some("pt_BR")); - assert_eq!(dictionary_needed(ProofLang::Pl, true), None); - assert_eq!(dictionary_needed(ProofLang::Pt, true), None); - // Every Hunspell language is named by the first name its module looks for (#3150). - for (l, name) in [ - (ProofLang::It, "it_IT"), - (ProofLang::Hr, "hr_HR"), - (ProofLang::Tr, "tr_TR"), - (ProofLang::Ru, "ru_RU"), - (ProofLang::Cs, "cs_CZ"), - (ProofLang::Id, "id_ID"), - // #3360: each Spanish variety names its own. - (ProofLang::Es, "es_ES"), - (ProofLang::EsMx, "es_MX"), - (ProofLang::Es419, "es_419"), - (ProofLang::EsUy, "es_UY"), - ] { - assert_eq!(dictionary_needed(l, false), Some(name), "{l:?}"); - assert_eq!(dictionary_needed(l, true), None, "{l:?}"); - } - // Every proofed language other than English has a Hunspell dictionary to report. - for l in ProofLang::PROOFED { - let english = matches!(l, ProofLang::En | ProofLang::EnGb | ProofLang::EnCa); - assert_eq!(dictionary_needed(l, false).is_some(), !english, "{l:?}"); - } - for l in [ProofLang::En, ProofLang::Bg, ProofLang::De, ProofLang::Other] { - assert_eq!(dictionary_needed(l, false), None); - } - // Installed dictionaries and the built-in English list: no hint. - pl::test_dict::install(); - pt::test_dict::install(); - es::test_dict::install(); - for tag in [Some("pl-PL"), Some("pt-BR"), Some("es-ES"), Some("es-MX"), Some("es-419"), Some("es-UY"), Some("en-US"), Some("fr-FR"), None] { - assert_eq!(missing_dictionary(tag), None, "{tag:?}"); - } - } - - use super::*; - - /// Held by tests that swap or rely on the shared Spanish dictionary, so a folder reset can't land - /// mid-test, and by those that save the whole user dictionary and load it back, which would - /// bring back a word another test just removed. - static DICTIONARY_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(()); - - /// #1063: the dictionaries folder is searched before the system locations; the dictionaries - /// in it are reported and used, and cached checks are dropped. The folder holds the same test - /// dictionaries the other tests install, so tests running meanwhile see the same words; it - /// stays set for the rest of the run for that reason. - #[test] - fn dictionaries_folder_is_searched_first() { - let _guard = DICTIONARY_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - let dir = std::env::temp_dir().join(format!("wc-proof-dictionaries-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - for (name, aff, dic) in [ - ("pl_PL", pl::test_dict::AFF, pl::test_dict::DIC), - ("pt_BR", pt::test_dict::AFF, pt::test_dict::DIC), - ("it_IT", it::test_dict::AFF, it::test_dict::DIC), - ("hr_HR", hr::test_dict::AFF, hr::test_dict::DIC), - ("tr_TR", tr::test_dict::AFF, tr::test_dict::DIC), - ("ru_RU", ru::test_dict::AFF, ru::test_dict::DIC), - ("cs_CZ", cs::test_dict::AFF, cs::test_dict::DIC), - ("id_ID", id::test_dict::AFF, id::test_dict::DIC), - ("es_ES", es::test_dict::AFF, es::test_dict::DIC), - ("es_MX", es::test_dict::AFF, es::test_dict::DIC_AMERICAS), - ("es_419", es::test_dict::AFF, es::test_dict::DIC_AMERICAS), - ("es_UY", es::test_dict::AFF, es::test_dict::DIC_AMERICAS), - ] { - std::fs::write(dir.join(format!("{name}.aff")), aff).unwrap(); - std::fs::write(dir.join(format!("{name}.dic")), dic).unwrap(); - } - // #3252 review: force Russian and Czech to cache whatever they find before the folder is - // set (empty, with no folder and nothing installed on CI), so the folder change below has - // a stale cache to actually invalidate, not just an untouched `None`. - ru::dictionary(); - cs::dictionary(); - let before = dictionary_generation(); - set_dictionaries_folder(Some(dir.clone())); - assert!(dictionary_generation() > before, "cached checks are dropped"); - // Every language is listed, Russian and Czech too (#3226: they were once missing, so - // neither showed up in File › Options › Proofing). A language reports the folder's file, - // or none while a dictionary given with install_dictionary() is in use (#3252 review); - // the languages' own tests install theirs concurrently, so an exact path would race them. - // The folder is searched first whatever is in use. - let found = installed_dictionaries(); - let names = ["pl_PL", "pt_BR", "it_IT", "hr_HR", "tr_TR", "ru_RU", "cs_CZ", "id_ID", "es_ES", "es_MX", "es_419", "es_UY"]; - assert_eq!( - found.iter().map(|(lang, _)| *lang).collect::>(), - [ - ProofLang::Pl, - ProofLang::Pt, - ProofLang::It, - ProofLang::Hr, - ProofLang::Tr, - ProofLang::Ru, - ProofLang::Cs, - ProofLang::Id, - ProofLang::Es, - ProofLang::EsMx, - ProofLang::Es419, - ProofLang::EsUy - ] - ); - for ((lang, path), name) in found.iter().zip(names) { - let file = dir.join(format!("{name}.dic")); - assert!(path.is_none() || path.as_ref() == Some(&file), "{lang:?}: {path:?}"); - assert_eq!(hunspell::find(&[name]).map(|(_, dic)| dic), Some(file), "{lang:?}"); - } - assert!(is_correct_in("chrząszcz", ProofLang::Pl) && !is_correct_in("chrzonszcz", ProofLang::Pl)); - assert!(is_correct_in("tenés", ProofLang::EsUy) && !is_correct_in("prueva", ProofLang::EsUy)); - assert!(is_correct_in("дом", ProofLang::Ru) && !is_correct_in("дум", ProofLang::Ru)); - } - - /// A dictionary that fails to load says why in File › Options › Proofing in every language - /// that reads one; Russian and Czech were missing from the list that named their dictionaries - /// there. - #[test] - fn every_dictionary_language_reports_its_load_error() { - let _guard = DICTIONARY_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - for (lang, _) in installed_dictionaries() { - let name = dictionary_needed(lang, false).unwrap(); - let reason = format!("zq unreadable {name}"); - let _ = hunspell::note_load::<()>(name, std::path::Path::new("zq.dic"), Err(reason.clone())); - assert_eq!(dictionary_load_error(lang), Some(reason), "{lang:?}"); - hunspell::clear_load_error(name); - } - } - - /// Every proofed language other than English has a row in the language table, and only those. - #[test] - fn the_language_table_covers_the_proofed_languages() { - for l in ProofLang::PROOFED { - assert_eq!(language(l).is_some(), !l.is_english(), "{l:?}"); - } - for (i, row) in LANGUAGES.iter().enumerate() { - assert!(row.lang.is_proofed(), "{:?}", row.lang); - assert!(LANGUAGES.iter().skip(i + 1).all(|other| other.lang != row.lang), "{:?} twice", row.lang); - } - } - - /// #1741: a list item may be a fragment, so its first word needs no capital in any language - /// with the rule; a plain paragraph's does, and later sentences in the item are still checked. - #[test] - fn list_item_first_word_needs_no_capital() { - let grammar = |t: &str, lang: ProofLang, list: bool| -> Vec { - check_text_with(t, &[(0..t.len(), lang)], &Options::default(), true, list) - .into_iter() - .filter(|i| i.kind == IssueKind::Grammar) - .filter_map(|i| t.get(i.start..i.end).map(str::to_string)) - .collect() - }; - for (lang, text) in [ - (ProofLang::En, "item one"), - (ProofLang::Pl, "jabłka i gruszki"), - (ProofLang::Hr, "jabuke i kruške"), - (ProofLang::It, "mele e pere"), - (ProofLang::Tr, "elma ve armut"), - (ProofLang::Pt, "maçãs e peras"), - (ProofLang::Ru, "яблоки и груши"), - ] { - let first = text.split(' ').next().unwrap(); - assert!(grammar(text, lang, true).is_empty(), "{lang:?} list item: {:?}", grammar(text, lang, true)); - assert_eq!(grammar(text, lang, false), [first], "{lang:?} plain paragraph"); - } - assert_eq!(grammar("hello world.", ProofLang::En, false), ["hello"]); - // Later sentences in a list item are still checked. - assert_eq!(grammar("item one. then two", ProofLang::En, true), ["then"]); - assert_eq!(grammar("elma ve armut. sonra kiraz", ProofLang::Tr, true), ["sonra"]); - } - - #[test] - fn dictionary_loads() { - assert!(dict::Dictionary::en_us().len() > 100_000); - } - - #[test] - fn spelling() { - for w in [ - "if", - "we", - "hello", - "world", - "running", - "studios", - "don't", - "Thursday", - "NASA", - "2026", - "co-operate", - "unhappy", - "colour", - "it's", - "stopped", - ] { - assert!(is_correct(w), "{w}"); - } - for w in ["helo", "wrold", "sentense", "recieve"] { - assert!(!is_correct(w), "{w}"); - } - let s = suggest("recieve", 5); - assert!(s.contains(&"receive".to_string()), "{s:?}"); - assert!(suggest("Wrold", 3).contains(&"World".to_string())); - } - - /// #1694: `-ically` adverbs come from their `-ic` adjective (specific → specifically). - #[test] - fn ically_adverbs_are_known() { - for w in ["specifically", "scientifically", "automatically", "dramatically", "Specifically"] { - assert!(is_correct(w), "{w}"); - } - for w in ["specificaly", "specifcally", "zzzically"] { - assert!(!is_correct(w), "{w}"); - } - } - - /// #1694 review: only `-ic` adjectives take `-ally`; other known words (dictionary or - /// supplement) don't. - #[test] - fn non_ic_words_do_not_take_ally() { - for (base, wrong) in [("desktop", "desktopally"), ("house", "houseally"), ("final", "finalally"), ("green", "greenally")] { - assert!(is_correct(base), "{base}"); - assert!(!is_correct(wrong), "{wrong}"); - } - } - - /// #520: common modern words (and their inflections) are known; misspellings still flagged. - #[test] - fn common_modern_words() { - for w in [ - "desktop", - "Desktops", - "laptop", - "download", - "downloaded", - "uploading", - "email", - "emailing", - "website", - "online", - "offline", - "smartphones", - "login", - "username", - "spreadsheet", - "database", - "screenshots", - "blogging", - "inbox", - "workplace", - "uploader", - ] { - assert!(is_correct(w), "{w}"); - } - for w in ["dekstop", "laptp", "dowload", "spreadshet"] { - assert!(!is_correct(w), "{w}"); - } - assert!(suggest("dekstop", 5).contains(&"desktop".to_string()), "{:?}", suggest("dekstop", 5)); - assert!(check_spelling("My desktop and laptop download email online.").is_empty()); - } - - /// The supplement stays a sorted list of lowercase base forms. - /// #40: British spellings and their inflections are not flagged. - #[test] - fn british_spellings_are_known() { - for w in [ - "judgement", - "defence", - "licence", - "programme", - "catalogue", - "behaviour", - "colour", - "colourful", - "colourless", - "honour", - "honoured", - "honouring", - "honours", - "honourable", - "rigour", - "glamour", - "clamour", - "endeavour", - "paediatric", - "prioritise", - "prioritised", - "prioritising", - "privatise", - "privatised", - "synthesise", - "travelogue", - "manoeuvrable", - ] { - assert!(is_correct(w), "{w}"); - } - let text = "The programme honoured her rigour and prioritised colourful, paediatric care."; - for lang in [ProofLang::EnGb, ProofLang::EnCa] { - let flagged: Vec<&str> = check_spelling_in(text, lang).iter().map(|i| &text[i.start..i.end]).collect(); - let expect: &[&str] = if lang == ProofLang::EnCa { &["prioritised"] } else { &[] }; - assert_eq!(flagged, expect, "{lang:?}"); - } - // American English flags the British spellings (#1281). - let flagged: Vec<&str> = check_spelling(text).iter().map(|i| &text[i.start..i.end]).collect(); - assert_eq!(flagged, ["programme", "honoured", "rigour", "prioritised", "colourful", "paediatric"]); - } - - /// Each English accepts its own spellings and flags the other dialect's, suggesting its own - /// first, as Word does (#1281, #1045). - #[test] - fn english_dialects_flag_each_others_spellings() { - let (us, gb, ca) = (ProofLang::En, ProofLang::EnGb, ProofLang::EnCa); - // (word, right in en-US, right in en-CA, right in en-GB) - let cases = [ - ("color", true, false, false), - ("colour", false, true, true), - ("colours", false, true, true), - ("coloured", false, true, true), - ("Colourful", false, true, true), - ("colour's", false, true, true), - ("honorable", true, false, false), - ("neighbourhood", false, true, true), - ("center", true, false, false), - ("centre", false, true, true), - ("centred", false, true, true), - ("centring", false, true, true), - ("centering", true, false, false), - ("theater", true, false, false), - ("theatre", true, true, true), - ("organize", true, true, true), - ("organise", false, false, true), - ("organisation", false, false, true), - ("organisations", false, false, true), - ("realised", false, false, true), - ("recognising", false, false, true), - ("analyze", true, true, false), - ("analyse", false, true, true), - ("paralysed", false, true, true), - ("defense", true, false, false), - ("defence", false, true, true), - ("licence", false, true, true), - ("license", true, true, true), - ("practise", false, true, true), - ("traveled", true, false, false), - ("travelled", true, true, true), - ("catalog", true, false, false), - ("catalogue", true, true, true), - ("anaemia", false, true, true), - ("anemia", true, true, false), - ("tire", true, true, true), - ("tyre", false, false, true), - ("programme", false, true, true), - ("program", true, true, true), - ("meter", true, true, true), - ("metre", false, true, true), - ("plough", false, true, true), - ("colour-blind", false, true, true), - // Words spelled alike everywhere, and real misspellings, are unaffected. - ("humorous", true, true, true), - ("honorary", true, true, true), - ("tour", true, true, true), - ("four", true, true, true), - ("programmed", true, true, true), - ("colourr", false, false, false), - ("colorr", false, false, false), - ("organisationz", false, false, false), - ]; - for (w, in_us, in_ca, in_gb) in cases { - assert_eq!(is_correct_in(w, us), in_us, "en-US: {w}"); - assert_eq!(is_correct_in(w, ca), in_ca, "en-CA: {w}"); - assert_eq!(is_correct_in(w, gb), in_gb, "en-GB: {w}"); - assert_eq!(is_correct_for(w, Some("en-AU")), in_gb, "en-AU follows British spelling: {w}"); - } - // The dialect's own spelling comes first, in the word's case. - assert_eq!(suggest_in("colour", 5, us).first().map(String::as_str), Some("color")); - assert_eq!(suggest_in("Colours", 5, us).first().map(String::as_str), Some("Colors")); - assert_eq!(suggest_in("color", 5, gb).first().map(String::as_str), Some("colour")); - assert_eq!(suggest_in("organisation", 5, us).first().map(String::as_str), Some("organization")); - assert_eq!(suggest_in("organisation", 5, ca).first().map(String::as_str), Some("organization")); - assert_eq!(suggest_in("analyze", 5, gb).first().map(String::as_str), Some("analyse")); - assert_eq!(suggest_in("CENTER", 5, gb).first().map(String::as_str), Some("CENTRE")); - assert_eq!(suggest_in("colour's", 5, us).first().map(String::as_str), Some("color's")); - assert_eq!(suggest_in("colour-blind", 5, us).first().map(String::as_str), Some("color-blind")); - // Shared suggestions come in the dialect's spelling, never the other one's. - let gb_sugg = suggest_in("colouur", 8, gb); - assert!(gb_sugg.contains(&"colour".to_string()) && !gb_sugg.contains(&"color".to_string()), "{gb_sugg:?}"); - let us_sugg = suggest_in("colouur", 8, us); - assert!(us_sugg.contains(&"color".to_string()) && !us_sugg.contains(&"colour".to_string()), "{us_sugg:?}"); - // A word in the user dictionary is right in every English. - add_word("zzcolourzz"); - assert!(is_correct_in("zzcolourzz", us)); - // Issues carry the dialect, so suggestions on demand come from the same one. - let issues = check_text("The color is nice.", &[(0..18, gb)]); - assert_eq!(issues.len(), 1, "{issues:?}"); - assert_eq!((issues[0].lang, &"The color is nice."[issues[0].start..issues[0].end]), (gb, "color")); - let grammar = check_text("the colour is nice.", &[(0..19, gb)]); - assert!(grammar.iter().any(|i| i.kind == IssueKind::Grammar && i.lang == gb), "{grammar:?}"); - } - - /// The words #40 (folded into #1281) reported as flagged in British English, with their - /// inflections: right in en-GB, and in en-US flagged or not as `en-variants.txt` decides. - #[test] - fn british_words_reported_in_issue_40_are_right_in_en_gb() { - let (us, gb, ca) = (ProofLang::En, ProofLang::EnGb, ProofLang::EnCa); - // (word, right in en-US, right in en-CA, right in en-GB) - let cases = [ - // Both spellings are right everywhere: `judgment` is also British legal usage, and - // American dictionaries list `judgement` as a variant. - ("judgement", true, true, true), - ("judgements", true, true, true), - ("judgment", true, true, true), - ("judgments", true, true, true), - ("defence", false, true, true), - ("defences", false, true, true), - ("defense", true, false, false), - // `license` is the British verb, so only American English flags `licence`. - ("licence", false, true, true), - ("licences", false, true, true), - ("license", true, true, true), - ("licensed", true, true, true), - ("licenses", true, true, true), - // `program` is British for computing. - ("programme", false, true, true), - ("programmes", false, true, true), - ("program", true, true, true), - ("programs", true, true, true), - // American English accepts both. - ("catalogue", true, true, true), - ("catalogues", true, true, true), - ("catalogued", true, true, true), - ("cataloguing", true, true, true), - ("catalog", true, false, false), - ("catalogs", true, false, false), - ]; - for (w, in_us, in_ca, in_gb) in cases { - assert_eq!(is_correct_in(w, us), in_us, "en-US: {w}"); - assert_eq!(is_correct_in(w, ca), in_ca, "en-CA: {w}"); - assert_eq!(is_correct_in(w, gb), in_gb, "en-GB: {w}"); - } - let text = "The judgement on the defence licence put the programme in the catalogue."; - assert!(check_spelling_in(text, gb).is_empty(), "{:?}", check_spelling_in(text, gb)); - assert_eq!(suggest_in("licences", 5, us).first().map(String::as_str), Some("licenses")); - assert_eq!(suggest_in("programmes", 5, us).first().map(String::as_str), Some("programs")); - } - - /// More British spellings that must stay right in en-GB and en-AU (#1281): -re, single l, ae, - /// with their inflections. - #[test] - fn more_british_spellings_are_right_in_en_gb() { - let gb = ProofLang::EnGb; - assert_eq!(ProofLang::from_tag(Some("en-AU")), gb); - for w in [ - "goitre", - "goitres", - "reconnoitre", - "sceptre", - "sceptres", - "enrol", - "enrols", - "enrolled", - "enrolment", - "fulfil", - "fulfils", - "fulfilled", - "fulfilment", - "instalment", - "instalments", - "skilful", - "skilfully", - "wilful", - "wilfully", - "encyclopaedia", - "encyclopaedias", - ] { - assert!(is_correct_in(w, gb), "en-GB: {w}"); - } - let text = "The wilful knight took the sceptre to fulfil his enrolment, paid the instalment and read the encyclopaedia."; - assert!(check_spelling_in(text, gb).is_empty(), "{:?}", check_spelling_in(text, gb)); - } - - #[test] - fn user_dictionary_accepts_the_other_dialects_spelling() { - add_word("savour"); - assert!(is_correct_in("savour", ProofLang::En), "a word the user added is never flagged"); - assert!(!is_correct_in("savor", ProofLang::EnGb)); - remove_word("savour"); - assert!(!is_correct_in("savour", ProofLang::En)); - } - - #[test] - fn supplement_is_sorted_lowercase() { - let words: Vec<&str> = EXTRA_EN.lines().filter(|l| !l.is_empty() && !l.starts_with('#')).collect(); - assert!(words.len() > 100); - assert!(words.windows(2).all(|p| p[0] < p[1]), "not sorted / duplicate"); - assert!(words.iter().all(|w| w.chars().all(|c| c.is_lowercase() || c == '\'' || c == '-')), "not lowercase"); - } - - #[test] - fn spelling_issues_in_text() { - let t = "This sentense has a mispelled word, see https://example.com/xyzq."; - let v = check_spelling(t); - let bad: Vec<&str> = v.iter().map(|i| &t[i.start..i.end]).collect(); - assert_eq!(bad, vec!["sentense", "mispelled"]); - } - - #[test] - fn address_token_covers_every_word_split_out_of_it() { - // #1335: one whitespace-delimited token, however punctuation splits it, has one - // web/e-mail classification, shared by all its words. - let t = "zxq@mail.example.com,blorp;qwirpy wugwug"; - let bad: Vec<&str> = check_spelling(t).iter().map(|i| &t[i.start..i.end]).collect(); - assert_eq!(bad, vec!["wugwug"]); - let opts = Options { ignore_internet: false, ..Options::default() }; - let bad: Vec<&str> = check_spelling_with(t, &opts).iter().map(|i| &t[i.start..i.end]).collect(); - assert_eq!(bad.first(), Some(&"zxq"), "{bad:?}"); - assert!(bad.contains(&"qwirpy"), "{bad:?}"); - assert_eq!(bad.last(), Some(&"wugwug"), "{bad:?}"); - } - - #[test] - fn address_after_a_wide_space_is_an_internet_token() { - // A web or e-mail address after a no-break space, a narrow no-break space, a figure space - // or an ideographic space is skipped, as it is after a plain space. - let t = "see\u{a0}https://xyzq.example/blorp and\u{3000}www.qwirpy.example or\u{a0}zxq@mail.example \ - at\u{202f}https://frobq.example/zibb by\u{2007}snorf@mail.example wugwug"; - let bad: Vec<&str> = check_spelling(t).iter().map(|i| &t[i.start..i.end]).collect(); - assert_eq!(bad, vec!["wugwug"]); - let opts = Options { ignore_internet: false, ..Options::default() }; - let bad: Vec<&str> = check_spelling_with(t, &opts).iter().map(|i| &t[i.start..i.end]).collect(); - assert!(["xyzq", "blorp", "qwirpy", "zxq", "frobq", "zibb", "snorf", "wugwug"].iter().all(|w| bad.contains(w)), "{bad:?}"); - } - - #[test] - fn spelling_scales_over_one_long_token() { - // #1335: thousands of punctuation-separated words inside a single - // whitespace-free token: one classification for the token, one issue per - // misspelled word, through every public spelling entry point. - let word = |i: usize| { - let mut w = String::from("zxq"); - let mut n = i; - loop { - w.push((b'a' + (n % 26) as u8) as char); - n /= 26; - if n == 0 { - break; - } - } - w - }; - let t = (0..2000).map(word).collect::>().join(","); - assert_eq!(check_spelling(&t).len(), 2000); - assert_eq!(check_spelling_in(&t, ProofLang::En).len(), 2000); - let opts = Options { ignore_internet: false, ..Options::default() }; - assert_eq!(check_spelling_with(&t, &opts).len(), 2000); - assert_eq!(check_text(&t, &[]).into_iter().filter(|i| i.kind == IssueKind::Spelling).count(), 2000); - } - - #[test] - fn grammar_issues() { - let t = "this is is a apple , and an car. Done"; - let v = check_grammar(t); - let msgs: Vec<&str> = v.iter().map(|i| i.message.as_str()).collect(); - assert!(msgs.iter().any(|m| m.contains("Repeated")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("\"an\"")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("\"a\" before")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("space before")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("Capitalize")), "{msgs:?}"); - assert!(msgs.iter().any(|m| m.contains("Extra space")), "{msgs:?}"); - assert!(check_grammar("A university is an hour away.").is_empty(), "{:?}", check_grammar("A university is an hour away.")); - } - - #[test] - fn ellipses_do_not_start_sentences() { - for text in [ - "I saw... a figure right outside.", - "I saw… a figure right outside.", - "I saw.... a figure right outside.", - "I saw... \"a figure\" right outside.", - "I saw… “a figure” right outside.", - ] { - let capitals: Vec<_> = - check_grammar(text).into_iter().filter(|issue| issue.message == "Capitalize the first word of a sentence").collect(); - assert!(capitals.is_empty(), "{text}: {capitals:?}"); - } - - for text in ["I saw. a figure.", "I saw! a figure.", "I saw? a figure.", "I saw.\" a figure."] { - let capitals: Vec<_> = - check_grammar(text).into_iter().filter(|issue| issue.message == "Capitalize the first word of a sentence").collect(); - assert_eq!(capitals.len(), 1, "{text}: {capitals:?}"); - assert_eq!(capitals[0].suggestions, ["A"], "{text}"); - } - } - - #[test] - fn user_dictionary_works() { - assert!(!is_correct("zxqwordcrafty")); - add_word("zxqwordcrafty"); - assert!(is_correct("zxqwordcrafty")); - } - - /// #40: the user dictionary survives a save and a fresh load. - #[test] - fn user_dictionary_persists_through_a_file() { - let _guard = DICTIONARY_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - let dir = std::env::temp_dir().join(format!("wc-dict-{}-{}", std::process::id(), "persist")); - let _ = std::fs::remove_dir_all(&dir); - // The folder is created on the first save. - let path = dir.join("sub/custom-dictionary.txt"); - add_word("zxqpersistone"); - add_word("zxqpersisttwo"); - save_user_dictionary(&path, &[]).expect("save works"); - let saved = std::fs::read_to_string(&path).unwrap(); - assert!(saved.lines().any(|w| w == "zxqpersistone") && saved.lines().any(|w| w == "zxqpersisttwo"), "{saved}"); - // Written atomically: no temporary file is left beside it. - let names: Vec<_> = std::fs::read_dir(dir.join("sub")).unwrap().map(|e| e.unwrap().file_name()).collect(); - assert_eq!(names, ["custom-dictionary.txt"]); - remove_word("zxqpersistone"); - assert!(!is_correct("zxqpersistone")); - load_user_dictionary(&path); - assert!(is_correct("zxqpersistone"), "loading brings the word back"); - let _ = std::fs::remove_dir_all(&dir); - } - - /// #40: a hostile file is capped: invalid lines are skipped, and only the first megabyte is read. - #[test] - fn hostile_dictionary_files_are_capped() { - let dir = std::env::temp_dir().join(format!("wc-dict-{}-{}", std::process::id(), "hostile")); - let _ = std::fs::create_dir_all(&dir); - let path = dir.join("custom-dictionary.txt"); - let mut hostile = vec![0xff, 0xfe, b'\n']; - hostile.extend(b"zxqhostileok\n zxqhostiletrim \nzxq hostile space\nzxq\x07bell\n"); - hostile.extend("zxq".repeat(30).bytes()); // 90 characters: too long - hostile.push(b'\n'); - // Pad past the 1 MB cap with blank lines; the word after it is never read. - hostile.resize(1_000_100, b'\n'); - hostile.extend(b"zxqhostilepastcap\n"); - std::fs::write(&path, &hostile).unwrap(); - let added = load_user_dictionary(&path); - assert!(added.contains(&"zxqhostileok".to_string()) && added.contains(&"zxqhostiletrim".to_string()), "{added:?}"); - assert!(!added.iter().any(|w| w.contains("space") || w.contains("bell") || w.chars().count() > MAX_CUSTOM_WORD_CHARS), "{added:?}"); - assert!(!is_correct("zxqhostilepastcap"), "the file is read only up to the cap"); - assert_eq!(read_dictionary_file(&path).len(), 2); - let _ = std::fs::remove_dir_all(&dir); - } - - /// #40: saving merges in words another process saved, and leaves out the ones just removed. - #[test] - fn saving_merges_with_the_file() { - let dir = std::env::temp_dir().join(format!("wc-dict-{}-{}", std::process::id(), "merge")); - let _ = std::fs::create_dir_all(&dir); - let path = dir.join("custom-dictionary.txt"); - std::fs::write(&path, "zxqmergeother\nzxqmergegone\n").unwrap(); - add_word("zxqmergemine"); - save_user_dictionary(&path, &["zxqmergegone".to_string()]).unwrap(); - let saved = std::fs::read_to_string(&path).unwrap(); - assert!(saved.lines().any(|w| w == "zxqmergeother"), "the other process's word is kept: {saved}"); - assert!(saved.lines().any(|w| w == "zxqmergemine"), "{saved}"); - assert!(!saved.lines().any(|w| w == "zxqmergegone"), "a removed word stays out: {saved}"); - assert!(is_correct("zxqmergeother") && !is_correct("zxqmergegone")); - let _ = std::fs::remove_dir_all(&dir); - } - - /// Ignore All lasts for the session: the word is accepted but isn't in the dictionary. - #[test] - fn ignored_words_are_not_dictionary_words() { - assert!(!is_correct("zxqignoredword")); - ignore_word("ZxqIgnoredWord"); - assert!(is_correct("zxqignoredword")); - assert!(!user_dictionary().contains(&"zxqignoredword".to_string())); - } - - #[test] - fn custom_words_are_single_short_words() { - assert_eq!(custom_word(" Hello "), Some("hello".into())); - for bad in ["", " ", "two words", "tab\tbed", "bell\u{7}", "bad\u{FFFD}", &"x".repeat(MAX_CUSTOM_WORD_CHARS + 1)] { - assert_eq!(custom_word(bad), None, "{bad:?}"); - } - assert!(custom_word(&"é".repeat(MAX_CUSTOM_WORD_CHARS)).is_some(), "the cap counts characters, not bytes"); - } - - /// #40: a missing file loads as empty, never an error. - #[test] - fn missing_dictionary_file_loads_empty() { - let path = std::env::temp_dir().join(format!("wc-dict-{}-missing.txt", std::process::id())); - let _ = std::fs::remove_file(&path); - assert!(load_user_dictionary(&path).is_empty()); - } - - /// #100: runs are proofed when their language has a dictionary (the default language too) - /// and skipped otherwise. - #[test] - fn dictionary_lang_tags() { - for l in [None, Some(""), Some(" "), Some("en"), Some("en-US"), Some("en-GB"), Some("EN-us"), Some("en_US")] { - assert!(has_dictionary(l), "{l:?}"); - } - for l in [Some("lt-LT"), Some("fr-FR"), Some("de"), Some("uk"), Some("-US"), Some("e")] { - assert!(!has_dictionary(l), "{l:?}"); - } - // The gate follows the dictionaries, not English: every proofed language is proofed. - for d in ProofLang::PROOFED.map(ProofLang::code) { - assert!(has_dictionary(Some(d)), "{d}"); - assert!(has_dictionary(Some(&format!("{}-XX", d.to_ascii_uppercase()))), "{d}"); - } - } - - /// Combining marks stay in their word (#3562 review): a decomposed word is one word, with the - /// byte range of what was typed, and it is checked as its composed form in every language. - #[test] - fn decomposed_words_stay_whole() { - let text = "tambie\u{301}n, cafe\u{301} \u{301}x"; - let ws: Vec<&str> = words(text).into_iter().map(|(a, b)| &text[a..b]).collect(); - assert_eq!(ws, ["tambie\u{301}n", "cafe\u{301}", "\u{301}x"]); - assert_eq!(words("tambie\u{301}n"), [(0, 9)]); - assert_eq!(&*composed("tambie\u{301}n"), "también"); - assert!(matches!(composed("también"), std::borrow::Cow::Borrowed(_)) && matches!(composed("plain"), std::borrow::Cow::Borrowed(_))); - pt::test_dict::install(); - assert!(is_correct_in("corac\u{327}a\u{303}o", ProofLang::Pt), "Portuguese gets the same composition"); - } - - /// #3562 review: Add to Dictionary, Ignore All, removal and the dictionary file use the same - /// composed key as spelling, so a word added or ignored decomposed is accepted in both forms, - /// an NFD entry in a saved file loads (and saves) composed, and either form removes it. - #[test] - fn user_dictionary_and_ignore_all_match_both_normalization_forms() { - let _guard = DICTIONARY_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); - es::test_dict::install(); - let es = ProofLang::Es; - let (nfd, nfc) = ("zqprue\u{301}ba", "zqpruéba"); - let text = format!("esto {nfd}"); - let issues = check_text(&text, &[(0..text.len(), es)]); - assert_eq!(issues.iter().map(|i| (i.start, i.end)).collect::>(), [(5, text.len())], "flagged at the typed bytes"); - assert!(!is_correct_in(nfc, es)); - add_word(nfd); - assert!(is_correct_in(nfd, es) && is_correct_in(nfc, es)); - assert!(check_text(&text, &[(0..text.len(), es)]).is_empty()); - assert!(user_dictionary().contains(&nfc.to_string()) && !user_dictionary().contains(&nfd.to_string())); - assert!(remove_word(nfc), "the composed form removes the entry added decomposed"); - assert!(!is_correct_in(nfd, es) && !is_correct_in(nfc, es)); - // Ignore All, either way round. - ignore_word("zqigno\u{301}ro"); - assert!(is_correct_in("zqignóro", es) && is_correct_in("zqigno\u{301}ro", es)); - ignore_word("zqcallé"); - assert!(is_correct_in("zqcalle\u{301}", es)); - // A dictionary file with a decomposed entry loads composed and is saved composed. - let path = std::env::temp_dir().join(format!("wc-nfd-dictionary-{}.txt", std::process::id())); - std::fs::write(&path, "zqguarda\u{301}do\nzqotra\n").unwrap(); - let added = load_user_dictionary(&path); - assert!(added.contains(&"zqguardádo".to_string()) && !added.iter().any(|w| w.contains('\u{301}')), "{added:?}"); - assert!(is_correct_in("zqguardádo", es) && is_correct_in("zqguarda\u{301}do", es)); - save_user_dictionary(&path, &["zqotra".to_string()]).unwrap(); - let saved = std::fs::read_to_string(&path).unwrap(); - assert!(saved.lines().any(|l| l == "zqguardádo") && !saved.contains('\u{301}') && !saved.lines().any(|l| l == "zqotra"), "{saved}"); - // Removing the decomposed form takes out the composed entry, and the file follows. - assert!(remove_word("zqguarda\u{301}do")); - save_user_dictionary(&path, &["zqguarda\u{301}do".to_string()]).unwrap(); - assert!(!std::fs::read_to_string(&path).unwrap().lines().any(|l| l == "zqguardádo")); - assert!(!is_correct_in("zqguardádo", es)); - let _ = std::fs::remove_file(&path); - } - - /// #100: runs in a language without a dictionary are never flagged; English still is. - #[test] - fn non_english_words_are_not_flagged() { - for w in ["ąžuolas", "élève", "über", "labas", "bonjour"] { - assert!(is_correct_for(w, Some("lt-LT")), "{w}"); - assert!(is_correct_for(w, Some("fr-FR")), "{w}"); - } - assert!(!is_correct_for("helo", Some("en-US")), "English typos still flagged"); - assert!(!is_correct_for("helo", None), "default language still proofed"); - assert!(is_correct_for("hello", Some("en-US"))); - } - - /// File › Options › Proofing (#485): each "Ignore" choice turned off makes those words count. - #[test] - fn ignore_options_decide_what_is_checked() { - let t = "SENTENSE helo2 www.exampel.org"; - let bad = |o: Options| check_spelling_with(t, &o).iter().map(|i| t[i.start..i.end].to_string()).collect::>(); - assert!(bad(Options::default()).is_empty(), "Word's defaults skip all three"); - let all = Options { ignore_uppercase: false, ignore_numbers: false, ignore_internet: false }; - assert_eq!(bad(all), ["SENTENSE", "helo2", "www", "exampel"]); - assert_eq!(bad(Options { ignore_uppercase: false, ..Options::default() }), ["SENTENSE"]); - assert!(is_correct_with("NASA", &all), "capitals that are a word stay correct"); - } - - #[test] - fn hyphenation() { - let l = hyphen::Limits::default(); - assert!(hyphen::hyphenate_word("hyphenation", &l).matches('-').count() >= 2); - } - - fn flagged(t: &str, v: &[Issue]) -> Vec { - v.iter().filter(|i| i.kind == IssueKind::Spelling).map(|i| t[i.start..i.end].to_string()).collect() - } - - #[test] - fn each_language_uses_its_own_tools() { - pl::test_dict::install(); - let pl = "Zażółć gęślą jaźń, chrząszcz brzmi w trzcinie."; - assert!(check_spelling_in(pl, ProofLang::Pl).is_empty(), "{:?}", check_spelling_in(pl, ProofLang::Pl)); - assert!(check_spelling_in(pl, ProofLang::En).len() >= 3, "Polish words are English misspellings"); - assert!(check_spelling_in("Nous écrivons en français.", ProofLang::Other).is_empty()); - assert!(check_grammar_in("this is is wrong", ProofLang::Other).is_empty()); - assert!(is_correct_in("anything", ProofLang::Other) && suggest_in("xyzzy", 5, ProofLang::Other).is_empty()); - let v = check_spelling_in("Płynie żeka.", ProofLang::Pl); - assert_eq!(flagged("Płynie żeka.", &v), ["żeka"]); - assert_eq!((v[0].lang, v[0].message.as_str()), (ProofLang::Pl, "Możliwy błąd pisowni")); - assert!(suggest_in("żeka", 5, ProofLang::Pl).contains(&"rzeka".to_string())); - assert!(suggest_in("recieve", 5, ProofLang::En).contains(&"receive".to_string())); - // Polish grammar speaks Polish and has no English a/an rule. - let g = check_grammar_in("Wiem że to jest a apple.", ProofLang::Pl); - assert!(g.iter().any(|i| i.message == "Brak przecinka przed „że”"), "{g:?}"); - assert!(!g.iter().any(|i| i.message.contains("\"an\"")), "{g:?}"); - } - - #[test] - fn italian_uses_its_own_tools() { - it::test_dict::install(); - let it = "La città vecchia ha un bel caffè in piazza."; - assert!(check_spelling_in(it, ProofLang::It).is_empty(), "{:?}", check_spelling_in(it, ProofLang::It)); - assert!(check_spelling_in(it, ProofLang::En).len() >= 3, "Italian words are English misspellings"); - let v = check_spelling_in("Vado a Roma con lbiro.", ProofLang::It); - assert_eq!(flagged("Vado a Roma con lbiro.", &v), ["lbiro"]); - assert_eq!((v[0].lang, v[0].message.as_str()), (ProofLang::It, "Possibile errore di ortografia")); - assert!(suggest_in("caffe", 5, ProofLang::It).contains(&"caffè".to_string())); - // Italian grammar speaks Italian and has no English a/an rule. - let g = check_grammar_in("questo questo è un test", ProofLang::It); - assert!(g.iter().any(|i| i.message.contains("ripetuta")), "{g:?}"); - } - - #[test] - fn mixed_language_paragraphs_check_each_run_in_its_language() { - pl::test_dict::install(); - // A Polish paragraph quoting English: English spelling and rules inside the quote only. - let t = "Napisał „the quick brown fox is is here” i poszedł spać, bo żeka wylała."; - let (a, b) = (t.find("the").unwrap(), t.find('”').unwrap()); - let spans = [(0..a, ProofLang::Pl), (a..b, ProofLang::En), (b..t.len(), ProofLang::Pl)]; - let v = check_text(t, &spans); - assert_eq!(flagged(t, &v), ["żeka"], "{v:?}"); - let rep = v.iter().find(|i| i.kind == IssueKind::Grammar).unwrap(); - assert_eq!((rep.lang, rep.message.as_str()), (ProofLang::En, "Repeated word: \"is\""), "{v:?}"); - // The English rules don't run over the Polish text (no "Capitalize" for the lower-case - // Polish words), and the Polish rules don't run over the English quote. - assert!(v.iter().all(|i| i.lang == ProofLang::Pl || (a..b).contains(&i.start)), "{v:?}"); - // Text outside every span is English; no spans at all is English. - assert_eq!(flagged("helo świat", &check_text("helo świat", &[])), ["helo", "świat"]); - assert_eq!(flagged("helo świat", &check_text("helo świat", &[(5..12, ProofLang::Pl)])), ["helo"]); - // A language without tools: nothing flagged there. - assert!(check_text("Bonjour mes amis", &[(0..16, ProofLang::Other)]).is_empty()); - // Portuguese checks in their span only, with the Portuguese message. - pt::test_dict::install(); - let t = "Ele disse que coraçao e frequencia existem."; - let v = check_text(t, &[(0..t.len(), ProofLang::Pt)]); - assert_eq!(flagged(t, &v), ["coraçao", "frequencia"], "{v:?}"); - assert!(v.iter().all(|i| i.lang == ProofLang::Pt && i.message == "Possível erro de ortografia"), "{v:?}"); - // Italian checks in its span only, with the Italian message. - it::test_dict::install(); - let t = "Vado a Roma con lbiro e caffe."; - let v = check_text(t, &[(0..t.len(), ProofLang::It)]); - assert_eq!(flagged(t, &v), ["lbiro", "caffe"], "{v:?}"); - assert!(v.iter().all(|i| i.lang == ProofLang::It && i.message == "Possibile errore di ortografia"), "{v:?}"); - } - - proptest::proptest! { - #[test] - fn never_panics(s in "\\PC{0,80}") { - let _ = check_spelling(&s); - let _ = check_grammar(&s); - let _ = suggest(&s, 3); - let _ = has_dictionary(Some(&s)); - let _ = is_correct_for(&s, Some(&s)); - let _ = hyphen::hyphen_points(&s, &hyphen::Limits::default()); - for lang in [ProofLang::En, ProofLang::EnGb, ProofLang::EnCa, ProofLang::Pl, ProofLang::Pt, ProofLang::It, ProofLang::Hr, ProofLang::Tr, ProofLang::Ru, ProofLang::Cs, ProofLang::Id, ProofLang::De, ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy, ProofLang::Other] { - let _ = check_spelling_in(&s, lang); - let _ = check_grammar_in(&s, lang); - let _ = is_correct_in(&s, lang); - let _ = hyphen::hyphen_points_in(&s, &hyphen::Limits::default(), lang); - } - let _ = lang::normalize_tag(&s); - let _ = ProofLang::from_tag(Some(&s)); - } - - #[test] - fn polish_suggestions_never_panic(s in "[a-ząćęłńóśźżA-ZĄĆĘŁŃÓŚŹŻ'’ -]{0,14}") { - let _ = suggest_in(&s, 4, ProofLang::Pl); - } - - #[test] - fn portuguese_suggestions_never_panic(s in "[a-záàâãéêíóôõúüçA-ZÁÀÂÃÉÊÍÓÔÕÚÜÇ'’ -]{0,14}") { - let _ = suggest_in(&s, 4, ProofLang::Pt); - } - - #[test] - fn italian_suggestions_never_panic(s in "[a-zàèéìòóùA-ZÀÈÉÌÒÓÙ'’ -]{0,14}") { - let _ = suggest_in(&s, 4, ProofLang::It); - } - - #[test] - fn turkish_suggestions_never_panic(s in "[a-zçğıöşüA-ZÇĞİÖŞÜ'’ -]{0,14}") { - let _ = suggest_in(&s, 4, ProofLang::Tr); - } - - #[test] - fn czech_suggestions_never_panic(s in "[a-zěščřžýáíéůúĚŠČŘŽÝÁÍÉŮÚ'’ -]{0,14}") { - cs::test_dict::install(); - let _ = suggest_in(&s, 4, ProofLang::Cs); - } - - #[test] - fn indonesian_suggestions_never_panic(s in "[a-zA-Zé'’ -]{0,14}") { - id::test_dict::install(); - let _ = suggest_in(&s, 4, ProofLang::Id); - let _ = is_correct_in(&s, ProofLang::Id); - } - - #[test] - fn spanish_suggestions_never_panic(s in "[a-zA-ZáéíóúüñÁÉÍÓÚÜÑ¿¡'’ -]{0,14}") { - es::test_dict::install(); - for lang in [ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy] { - let _ = suggest_in(&s, 4, lang); - let _ = is_correct_in(&s, lang); - } - } - - #[test] - fn mixed_spans_never_panic(s in "\\PC{0,60}", cuts in proptest::collection::vec((0usize..80, 0usize..80, 0u8..15), 0..6)) { - let langs = [ProofLang::En, ProofLang::EnGb, ProofLang::EnCa, ProofLang::Pl, ProofLang::Pt, ProofLang::It, ProofLang::Tr, ProofLang::Ru, ProofLang::Cs, ProofLang::Id, ProofLang::Es, ProofLang::EsMx, ProofLang::Es419, ProofLang::EsUy, ProofLang::Other]; - let spans: Vec<(Range, ProofLang)> = cuts.iter().map(|&(a, b, l)| (a.min(b)..a.max(b), langs[l as usize % langs.len()])).collect(); - for i in check_text(&s, &spans) { - proptest::prop_assert!(i.start <= i.end && i.end <= s.len()); - } - } - } - - proptest::proptest! { - // Suggestions scan the whole word list: fewer cases keep the test quick. - #![proptest_config(proptest::prelude::ProptestConfig::with_cases(24))] - #[test] - fn english_dialect_suggestions_never_panic(s in "[a-zA-Zéß'’ -]{0,16}|(colou?r|cent(er|re)|organi[sz]ation)['’s-]{0,4}") { - for lang in [ProofLang::En, ProofLang::EnGb, ProofLang::EnCa] { - let _ = suggest_in(&s, 4, lang); - let _ = is_correct_in(&s, lang); - let _ = en_variants::preferred(&s, lang); - let _ = en_variants::known_variant(&s, lang, |_| true); - } - } - } -} diff --git a/crates/proof/src/patterns.rs b/crates/proof/src/patterns.rs deleted file mode 100644 index cb5855d49..000000000 --- a/crates/proof/src/patterns.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! Liang hyphenation patterns (TeX notation, e.g. `.ab1c`, `4t1ion`): parsing and application. -//! -//! The bundled English set is *ours*: trained from the public-domain Moby Hyphenator word list by -//! DesignCraft's patgen (see `examples/hyphgen.rs`). The Polish set is the TeX one by Hanna -//! Kołodziejska, Bogusław Jackowski and Marek Ryćko (hyph-utf8 `hyph-pl.tex`, used under its MIT -//! licence option), bundled unmodified: [`Patterns::parse`] reads TeX's `\patterns{…}` and -//! `\hyphenation{…}` blocks directly. Patterns are stored in a hash map keyed by the packed letter -//! string, so applying them to a word costs `O(len × max_pattern_len)` lookups. - -use std::collections::HashMap; -use std::hash::{BuildHasherDefault, Hasher}; - -/// Word-boundary marker in patterns. -pub const DOT: char = '.'; -/// Longest pattern the packed key supports (6 bits a letter in a `u128`; the Polish set's longest -/// is 14 including the dots). -pub const MAX_PATTERN_LEN: usize = 20; - -/// Small, fast multiplicative hasher for packed integer keys and short strings (FxHash-style). -#[derive(Default, Clone, Copy)] -pub struct FxHasher(u64); - -impl Hasher for FxHasher { - fn finish(&self) -> u64 { - self.0 - } - fn write(&mut self, bytes: &[u8]) { - let (chunks, rest) = bytes.as_chunks::<8>(); - for c in chunks { - self.write_u64(u64::from_le_bytes(*c)); - } - for &b in rest { - self.write_u64(b as u64); - } - } - fn write_u8(&mut self, i: u8) { - self.write_u64(i as u64); - } - fn write_usize(&mut self, i: usize) { - self.write_u64(i as u64); - } - fn write_u64(&mut self, i: u64) { - self.0 = (self.0.rotate_left(5) ^ i).wrapping_mul(0x51_7c_c1_b7_27_22_0a_95); - } - fn write_u128(&mut self, i: u128) { - self.write_u64(i as u64); - self.write_u64((i >> 64) as u64); - } -} - -pub type FxMap = HashMap>; - -/// Maps characters to 6-bit codes (1 = word boundary, 2.. = letters); 0 = not in the alphabet. -#[derive(Clone, Debug)] -pub struct Alphabet { - ascii: [u8; 128], - other: Vec<(char, u8)>, - next: u8, -} - -impl Default for Alphabet { - fn default() -> Self { - let mut a = Alphabet { ascii: [0; 128], other: Vec::new(), next: 2 }; - a.ascii[DOT as usize] = 1; - a - } -} - -impl Alphabet { - pub fn code(&self, c: char) -> u8 { - if (c as u32) < 128 { self.ascii[c as usize] } else { self.other.iter().find(|x| x.0 == c).map_or(0, |x| x.1) } - } - /// Code for `c`, adding it if new (None when the 6-bit alphabet is full). - pub fn intern(&mut self, c: char) -> Option { - let k = self.code(c); - if k != 0 { - return Some(k); - } - if self.next >= 63 { - return None; - } - let k = self.next; - self.next += 1; - if (c as u32) < 128 { - self.ascii[c as usize] = k; - } else { - self.other.push((c, k)); - } - Some(k) - } -} - -/// Packed key of a code string (≤ [`MAX_PATTERN_LEN`] codes): 6 bits a code, the length on top. -#[inline] -pub fn pack(codes: &[u8]) -> u128 { - let mut k = 0u128; - for &c in codes { - k = (k << 6) | c as u128; - } - k | ((codes.len() as u128) << 120) -} - -/// A compiled pattern set, with its exception words (TeX's `\hyphenation{…}`). -#[derive(Clone, Debug, Default)] -pub struct Patterns { - alpha: Alphabet, - /// Packed letters → inter-letter values (`len + 1` entries). - map: FxMap>, - max_len: usize, - /// Lowercase word → its break points (char indices), overriding the patterns. - exceptions: HashMap>, -} - -/// Which TeX block [`Patterns::parse`] is reading. -#[derive(Clone, Copy, PartialEq, Eq)] -enum Block { - Patterns, - Exceptions, - /// Another command's argument (`\message{…}`), ignored. - Skip, -} - -impl Patterns { - /// Parse patterns: whitespace-separated, `%` comments to the end of the line (`#` too at the - /// start of a line). A whole TeX pattern file works as is: `\patterns{…}` holds patterns and - /// `\hyphenation{…}` exception words written with their hyphens (`ni-gdy`), and other - /// commands' arguments (`\message{…}`) are skipped; outside any block, a token with a hyphen - /// is an exception and anything else a pattern. - pub fn parse(text: &str) -> Patterns { - let mut p = Patterns::default(); - let mut block = Block::Patterns; - for line in text.lines() { - let line = line.trim(); - if line.starts_with('#') { - continue; - } - let line = line.split('%').next().unwrap_or(""); - for tok in line.split_whitespace() { - let mut tok = tok; - if let Some(cmd) = tok.strip_prefix('\\') { - let (name, rest) = cmd.split_once('{').unwrap_or((cmd, "")); - block = match name { - "hyphenation" => Block::Exceptions, - "patterns" => Block::Patterns, - // shortcut: the argument ends at the first `}`, so nested braces would leak; no bundled file has them. - _ if cmd.contains('{') => Block::Skip, - _ => Block::Patterns, - }; - tok = rest; - } - let closes = tok.ends_with('}'); - let tok = tok.trim_end_matches('}'); - if !tok.is_empty() && block != Block::Skip { - if block == Block::Exceptions || tok.contains('-') { - p.insert_exception(tok); - } else { - p.insert(tok); - } - } - if closes { - block = Block::Patterns; - } - } - } - p - } - - /// Add one pattern like `a1b`; returns false if it can't be represented. - pub fn insert(&mut self, pat: &str) -> bool { - let mut codes = Vec::with_capacity(pat.len()); - let mut vals = vec![0u8]; - for c in pat.chars() { - if let Some(d) = c.to_digit(10) { - *vals.last_mut().unwrap_or(&mut 0) = d as u8; - } else { - let Some(k) = self.alpha.intern(c) else { return false }; - codes.push(k); - vals.push(0); - } - } - if codes.is_empty() || codes.len() > MAX_PATTERN_LEN { - return false; - } - self.max_len = self.max_len.max(codes.len()); - self.map.insert(pack(&codes), vals.into_boxed_slice()); - true - } - - /// Add an exception word written with its hyphens (`ni-gdy`; no hyphen: never broken). - pub fn insert_exception(&mut self, word: &str) { - let mut plain = String::with_capacity(word.len()); - let mut points = Vec::new(); - let mut n = 0usize; - for c in word.chars() { - if c == '-' { - if n > 0 && points.last() != Some(&n) { - points.push(n); - } - } else { - plain.extend(c.to_lowercase()); - n += 1; - } - } - points.retain(|&i| i < n); - if n > 0 { - self.exceptions.insert(plain, points.into_boxed_slice()); - } - } - - /// Break points of an exception word (lowercase), if it is one. - pub fn exception(&self, word: &str) -> Option<&[usize]> { - self.exceptions.get(word).map(|b| &b[..]) - } - - /// Number of exception words. - pub fn exception_count(&self) -> usize { - self.exceptions.len() - } - - /// Make `letters` part of the alphabet even if no pattern mentions them. - pub fn add_letters(&mut self, letters: impl IntoIterator) { - for c in letters { - let _ = self.alpha.intern(c); - } - } - - pub fn len(&self) -> usize { - self.map.len() - } - - pub fn is_empty(&self) -> bool { - self.map.is_empty() - } - - /// Inter-letter values for a lowercase word: `out[i]` is the value *before* char `i` - /// (`out.len() == word.len() + 1`). None if a char is outside the pattern alphabet. - pub fn values(&self, word: &[char]) -> Option> { - let mut codes = Vec::with_capacity(word.len() + 2); - codes.push(1); - for &c in word { - let k = self.alpha.code(c); - if k == 0 { - return None; - } - codes.push(k); - } - codes.push(1); - let mut vals = vec![0u8; codes.len() + 1]; - apply(&self.map, self.max_len, &codes, &mut vals); - // Padded position p sits before padded char p; word char i is padded char i + 1. - vals.get(1..codes.len()).map(<[u8]>::to_vec) - } - - /// Break points (char indices, a hyphen goes before that char) of a lowercase word. - pub fn points(&self, word: &[char]) -> Option> { - let v = self.values(word)?; - Some((1..word.len()).filter(|&i| v.get(i).is_some_and(|x| x % 2 == 1)).collect()) - } - - /// The patterns in TeX notation (sorted by letters), e.g. for writing a pattern file. - pub fn to_tex(&self) -> Vec { - let mut rev: Vec<(u8, char)> = - (0u8..128).filter(|&c| self.alpha.ascii[c as usize] != 0).map(|c| (self.alpha.ascii[c as usize], c as char)).collect(); - rev.extend(self.alpha.other.iter().map(|&(c, k)| (k, c))); - let ch = |k: u8| rev.iter().find(|x| x.0 == k).map_or('?', |x| x.1); - let mut out: Vec<(String, String)> = self - .map - .iter() - .map(|(&key, vals)| { - let n = ((key >> 120) as usize).min(MAX_PATTERN_LEN); - let letters: Vec = (0..n).map(|i| ch(((key >> (6 * (n - 1 - i))) & 63) as u8)).collect(); - let mut s = String::new(); - for (i, l) in letters.iter().enumerate() { - if let Some(&v) = vals.get(i).filter(|v| **v > 0) { - s.push(char::from(b'0' + v.min(9))); - } - s.push(*l); - } - if let Some(&v) = vals.get(n).filter(|v| **v > 0) { - s.push(char::from(b'0' + v.min(9))); - } - (letters.into_iter().collect(), s) - }) - .collect(); - out.sort(); - out.into_iter().map(|x| x.1).collect() - } -} - -/// Apply `map` to padded `codes`, raising `vals` (len = codes.len() + 1; shorter is tolerated). -#[inline] -pub fn apply(map: &FxMap>, max_len: usize, codes: &[u8], vals: &mut [u8]) { - let n = codes.len(); - for s in 0..n { - let mut k = 0u128; - for (len, &c) in codes.iter().skip(s).take(max_len.min(MAX_PATTERN_LEN)).enumerate() { - k = (k << 6) | c as u128; - if let Some(v) = map.get(&(k | (((len + 1) as u128) << 120))) { - for (j, &x) in v.iter().enumerate() { - if let Some(slot) = vals.get_mut(s + j) - && x > *slot - { - *slot = x; - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn liang_example() { - // The classic example from Liang's thesis: "hy-phen-ation". - let p = Patterns::parse("hy3ph he2n hena4 hen5at 1na n2at 1tio 2io o2n"); - let w: Vec = "hyphenation".chars().collect(); - assert_eq!(p.points(&w).unwrap(), vec![2, 6]); - let mut tex = p.to_tex(); - tex.sort(); - assert!(tex.contains(&"hen5at".to_string())); - assert!(p.values(&['h', 'é']).is_none()); - } - - #[test] - fn tex_files_parse_with_blocks_comments_and_exceptions() { - let p = Patterns::parse( - "% a comment\n\\patterns{\n1na n2at % trailing comment\n.ab4cdefghijklm5n\n}\n\\hyphenation{\nni-gdy\nna-dal\nzawsze\n}\nhy-phen-ation\n", - ); - assert_eq!(p.len(), 3); - assert_eq!(p.exception("nigdy"), Some(&[2][..])); - assert_eq!(p.exception("nadal"), Some(&[2][..])); - assert_eq!(p.exception("zawsze"), Some(&[][..])); - assert_eq!(p.exception("hyphenation"), Some(&[2, 6][..])); - assert_eq!(p.exception_count(), 4); - // A pattern longer than the old 9-letter limit applies. - let w: Vec = "abcdefghijklmn".chars().collect(); - assert_eq!(p.values(&w).unwrap()[13], 5); - assert!(p.to_tex().contains(&".ab4cdefghijklm5n".to_string())); - } - - #[test] - fn other_commands_arguments_are_skipped() { - let p = Patterns::parse("\\message{Some Patterns `x-y' 2024-02-28}\n\\patterns{%\na1b\n}\n\\endinput\n"); - assert_eq!((p.len(), p.exception_count()), (1, 0)); - } - - #[test] - fn apply_tolerates_short_value_buffers() { - let p = Patterns::parse("a1b"); - let mut vals = [0u8; 1]; - apply(&p.map, p.max_len, &[1, 2, 3, 1], &mut vals); - } -} diff --git a/crates/proof/src/pl/grammar.rs b/crates/proof/src/pl/grammar.rs deleted file mode 100644 index 4289f0c7b..000000000 --- a/crates/proof/src/pl/grammar.rs +++ /dev/null @@ -1,548 +0,0 @@ -//! Polish grammar and punctuation checks: cheap, rule-based and tuned to stay quiet when unsure -//! (a missed comma costs less than a wrong mark on correct text). Messages are in Polish. -//! -//! - **Repeated word** (`się się`), except interjections and words Polish doubles on purpose -//! (`to to`, `tak tak`, `ha ha`). -//! - **Space before punctuation** (`tak ,`) and **double spaces**; emoticons (` :)`) and -//! ellipses are left alone. -//! - **Capital letter at the start of a sentence**, knowing Polish abbreviations (`np.`, `itd.`, -//! `r.`, `m.in.`), initials, ordinal numbers (`5. miejsce`), ellipses and dialogue dashes -//! (`– Tak? – zapytał`), after which lower case is correct. -//! - **Missing comma** before subordinating conjunctions and relative pronouns that always open a -//! clause: `że`, `ponieważ`, `gdyż`, `aby`, `żeby`, `jeśli`, `jeżeli`, `gdyby`, `lecz` and the -//! forms of `który` — unless a conjunction or particle comes first (`mimo że`, `chyba że`, -//! `tak aby`, `i który`) or, for `który`, a preposition (`w którym`: the comma goes before it). -//! - **`we`/`ze`** where the next word's consonant cluster requires them (`we wtorek`, -//! `ze szkoły`, `ze mną`). -//! - **Doubled punctuation** (`,,` — often a typed opening quote `„` — `..`, `;;`). -//! -//! No English rule (such as `a`/`an`) applies to Polish text. - -use crate::{Issue, IssueKind, ProofLang, words}; - -/// Words Polish repeats on purpose: interjections, onomatopoeia and emphatic doubling. -const ALLOWED_REPEATS: &[&str] = &[ - "to", "tak", "nie", "no", "ha", "he", "hi", "hej", "oj", "aj", "ej", "ach", "och", "ech", "puk", "hop", "bum", "cyk", "tik", "pa", "fiu", "la", - "bardzo", "dawno", "daleko", "długo", -]; - -/// Abbreviations written with a final period after which a sentence goes on in lower case -/// (lower case, without the period). -const ABBREVIATIONS: &[&str] = &[ - "np", "itd", "itp", "tzn", "tj", "tzw", "ok", "ul", "al", "pl", "os", "in", "wg", "zob", "por", "str", "ss", "nr", "pkt", "ust", "art", "par", - "rozdz", "tab", "rys", "il", "przyp", "red", "wyd", "oprac", "tłum", "cyt", "jw", "ds", "im", "ks", "św", "bł", "prof", "dr", "doc", "hab", - "inż", "mgr", "lek", "płk", "gen", "mjr", "kpt", "ppłk", "sierż", "szer", "kmdr", "adm", "abp", "bp", "br", "ub", "bm", "pn", "płd", "płn", - "wsch", "zach", "gł", "godz", "min", "sek", "tys", "mln", "mld", "zł", "gr", "kg", "dag", "mies", "tyg", "pon", "wt", "śr", "czw", "pt", "sob", - "niedz", "sty", "lut", "mar", "kwi", "cze", "lip", "sie", "wrz", "paź", "lis", "gru", "ang", "niem", "franc", "łac", "ros", "wł", "hiszp", "pol", - "lit", "dosł", "przen", "pot", "zdrob", "fot", "ryc", "tel", "kom", "ew", "ob", "obyw", "pp", "zm", "ur", "ca", "vs", "etc", "cdn", "dot", "wz", - "zw", "zał", "cz", "rr", "ww", "jęz", "jun", "sen", "mec", "red", "dyr", "kier", "proc", "poj", "lp", "lm", "zaw", "woj", "pow", "gm", "wsp", - "nast", "poprz", "ang", "fr", -]; - -/// Subordinating conjunctions and relative pronouns that open a clause: a comma comes before -/// them. -const CLAUSE_OPENERS: &[&str] = &[ - "że", - "ponieważ", - "gdyż", - "aby", - "żeby", - "jeśli", - "jeżeli", - "gdyby", - "lecz", - "który", - "która", - "które", - "którego", - "której", - "któremu", - "którą", - "którym", - "których", - "którymi", - "którzy", -]; - -/// Words after which a clause opener takes no comma of its own: conjunctions (`i że`), particles -/// (`tylko jeśli`, `zwłaszcza że`) and the first halves of compound conjunctions (`mimo że`, -/// `chyba że`, `tak aby`, `jak gdyby`, `na wypadek gdyby`, `z tym że`). -const NO_COMMA_AFTER: &[&str] = &[ - "i", - "a", - "oraz", - "lub", - "albo", - "ani", - "czy", - "bądź", - "ale", - "lecz", - "bo", - "że", - "aby", - "żeby", - "by", - "niż", - "jak", - "jako", - "niby", - "tylko", - "nawet", - "właśnie", - "zwłaszcza", - "szczególnie", - "głównie", - "częściowo", - "jedynie", - "wyłącznie", - "chyba", - "mianowicie", - "przynajmniej", - "również", - "także", - "też", - "dopiero", - "jeszcze", - "już", - "zaś", - "więc", - "zatem", - "toteż", - "przecież", - "chociaż", - "choć", - "choćby", - "gdy", - "kiedy", - "jeśli", - "jeżeli", - "gdyby", - "skoro", - "zanim", - "dopóki", - "póki", - "aż", - "co", - "tak", - "mimo", - "pomimo", - "wtedy", - "teraz", - "dziś", - "dzisiaj", - "podczas", - "tym", - "bardziej", - "warunkiem", - "wypadek", - "zamiast", - "byle", - "lada", - "wiadomo", - "obojętnie", - "jedno", - "wszystkim", - "może", - "pewnie", - "prawdopodobnie", - "zapewne", - "dlatego", - "tyle", - "dość", - "ponieważ", - "gdyż", - "lecz", - "albowiem", - "oto", - "no", -]; - -/// Prepositions: a relative pronoun after one takes the comma before the preposition instead -/// (`dom, w którym`). -const PREPOSITIONS: &[&str] = &[ - "w", - "we", - "z", - "ze", - "na", - "do", - "od", - "ode", - "o", - "u", - "po", - "za", - "przed", - "przede", - "przez", - "przeze", - "przy", - "nad", - "nade", - "pod", - "pode", - "dla", - "bez", - "beze", - "ku", - "między", - "pomiędzy", - "wśród", - "spośród", - "spod", - "spoza", - "sprzed", - "znad", - "zza", - "poza", - "ponad", - "poprzez", - "dzięki", - "według", - "wobec", - "oprócz", - "prócz", - "obok", - "koło", - "około", - "wokół", - "dookoła", - "naprzeciw", - "naprzeciwko", - "wzdłuż", - "wewnątrz", - "zewnątrz", - "względem", - "przeciw", - "przeciwko", - "wbrew", - "wskutek", - "śród", - "wedle", - "pośród", - "ponad", - "niedaleko", - "blisko", -]; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::Pl } -} - -fn capitalize(w: &str) -> String { - crate::affix::capitalize(w) -} - -/// Polish vowels (and the accented vowels of loanwords). -fn is_vowel(c: char) -> bool { - matches!(c.to_lowercase().next().unwrap_or(c), 'a' | 'ą' | 'e' | 'ę' | 'i' | 'o' | 'ó' | 'u' | 'y' | 'é' | 'á' | 'ö' | 'ü' | 'í' | 'ú') -} - -fn is_consonant(c: char) -> bool { - c.is_alphabetic() && !is_vowel(c) -} - -/// Grammar issues in a paragraph of Polish text, in order. -pub fn check(text: &str) -> Vec { - check_with(text, true) -} - -/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital -/// (false for a list item, which may be a fragment). -pub fn check_with(text: &str, capital_first: bool) -> Vec { - let ws: Vec<(usize, usize)> = words(text); - let mut v = Vec::new(); - repeated_words(text, &ws, &mut v); - spaces(text, &mut v); - sentence_capitals(text, &ws, capital_first, &mut v); - missing_commas(text, &ws, &mut v); - we_ze(text, &ws, &mut v); - doubled_punctuation(text, &mut v); - v.sort_by_key(|i| (i.start, i.end)); - v -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() - && gap.chars().all(char::is_whitespace) - && w0.chars().all(char::is_alphabetic) - && w0.to_lowercase() == w1.to_lowercase() - && !ALLOWED_REPEATS.contains(&w0.to_lowercase().as_str()) - { - v.push(issue(*a0, *b1, format!("Powtórzone słowo: „{w1}”"), vec![w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - let flag = match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '»' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Zbędna spacja przed znakiem interpunkcyjnym".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Podwójna spacja".into(), vec![" ".into()])); - } -} - -/// Is `prev`, before a period, an abbreviation, an initial, a number or a Roman numeral (after -/// which a lower-case word doesn't start a sentence)? -fn abbreviation_or_number(prev: &str) -> bool { - prev.chars().count() <= 1 - || prev.chars().any(|c| c.is_ascii_digit()) - || (prev.chars().all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))) - || ABBREVIATIONS.contains(&prev.to_lowercase().as_str()) -} - -const CLOSING: [char; 8] = ['"', '”', '’', '\'', ')', '»', ']', '\u{1}']; -const OPENING: [char; 8] = ['„', '"', '“', '«', '(', '[', '\'', '‚']; - -/// Does the gap between two words end a sentence: a `.`, `!` or `?` (not an ellipsis, not after -/// an abbreviation), then space, with nothing but quotes and brackets around? -fn ends_sentence(gap: &str, prev: &str) -> bool { - let t = gap.trim_start_matches(CLOSING); - let Some(end) = t.chars().next() else { return false }; - if !matches!(end, '.' | '!' | '?') || t.starts_with("..") { - return false; - } - if end == '.' && abbreviation_or_number(prev) { - return false; - } - let rest = t.trim_start_matches(['.', '!', '?']).trim_start_matches(CLOSING); - rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| OPENING.contains(&c)) -} - -fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec) { - // List items (`– jabłka,`) may start in lower case. - let list_item = text.trim_end().ends_with([',', ';']); - for (k, &(a, b)) in ws.iter().enumerate() { - let Some(w) = text.get(a..b) else { continue }; - let starts = if k == 0 { - let before = text.get(..a).unwrap_or(""); - capital_first && !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c)) - } else { - let Some(&(pa, pb)) = ws.get(k - 1) else { continue }; - ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or("")) - }; - if starts && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) { - v.push(issue(a, b, "Wielka litera na początku zdania".into(), vec![capitalize(w)])); - } - } -} - -fn missing_commas(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for k in 1..ws.len() { - let (Some(&(pa, pb)), Some(&(a, b))) = (ws.get(k - 1), ws.get(k)) else { continue }; - let (Some(prev), Some(w), Some(gap)) = (text.get(pa..pb), text.get(a..b), text.get(pb..a)) else { continue }; - if !CLAUSE_OPENERS.contains(&w) || gap.is_empty() || !gap.chars().all(|c| c == ' ' || c == '\u{a0}') { - continue; - } - if !prev.chars().all(char::is_alphabetic) { - continue; - } - let p = prev.to_lowercase(); - let p = p.as_str(); - if NO_COMMA_AFTER.contains(&p) { - continue; - } - if w.starts_with("któr") && (PREPOSITIONS.contains(&p) || p == "to") { - continue; - } - v.push(issue(pb, b, format!("Brak przecinka przed „{w}”"), vec![format!(", {w}")])); - } -} - -fn we_ze(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if gap != " " && gap != "\u{a0}" { - continue; - } - let next = w1.to_lowercase(); - let c: Vec = next.chars().collect(); - let at = |i: usize| c.get(i).copied(); - let (long, wanted) = match w0 { - "w" | "W" => ("we", (at(0) == Some('w') && at(1).is_some_and(is_consonant)) || next == "mnie"), - "z" | "Z" => { - // The first sound: `sz` is one consonant (`z szafy`, but `ze szkoły`). - let after = if at(0) == Some('s') && at(1) == Some('z') { at(2) } else { at(1) }; - let sibilant = matches!(at(0), Some('s' | 'z' | 'ś' | 'ź' | 'ż')); - ("ze", (sibilant && after.is_some_and(is_consonant)) || next == "mną") - } - _ => continue, - }; - if wanted { - let fixed = if w0.starts_with(char::is_uppercase) { capitalize(long) } else { long.to_string() }; - v.push(issue(*a0, *b0, format!("Przed tym słowem piszemy „{long}”"), vec![fixed])); - } - } -} - -fn doubled_punctuation(text: &str, v: &mut Vec) { - let bytes = text.as_bytes(); - let mut i = 0; - while let Some(&b) = bytes.get(i) { - if !matches!(b, b',' | b'.' | b';') { - i += 1; - continue; - } - let run = bytes.get(i..).map_or(0, |r| r.iter().take_while(|&&x| x == b).count()); - let flag = match b { - b'.' => run == 2, - _ => run >= 2, - }; - if flag { - let end = i + run; - let suggestions: Vec = match b { - // `,,` before a word, after a space: a typed opening quote. - b',' if text.get(end..).and_then(|t| t.chars().next()).is_some_and(char::is_alphabetic) - && (i == 0 || text.get(..i).is_some_and(|t| t.ends_with(char::is_whitespace))) => - { - vec!["„".into()] - } - b'.' => vec![".".into(), "…".into()], - _ => vec![char::from(b).to_string()], - }; - v.push(issue(i, end, "Powtórzony znak interpunkcyjny".into(), suggestions)); - } - i += run.max(1); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn messages(t: &str) -> Vec { - check(t).into_iter().map(|i| format!("{}@{}", i.message, &t[i.start..i.end])).collect() - } - - fn clean(t: &str) { - let m = messages(t); - assert!(m.is_empty(), "{t:?}: {m:?}"); - } - - #[test] - fn repeated_words() { - let v = check("Ala ma ma kota i się się cieszy."); - assert_eq!(v.len(), 2, "{v:?}"); - assert_eq!(v[0].message, "Powtórzone słowo: „ma”"); - assert_eq!(v[0].suggestions, ["ma"]); - assert!(v.iter().all(|i| i.lang == ProofLang::Pl && i.kind == IssueKind::Grammar)); - clean("Tak tak, to to samo. Ha ha!"); - } - - #[test] - fn spaces_and_punctuation() { - let m = messages("Ala ma kota , a kot ma Alę . Czy na pewno ?"); - assert!(m.contains(&"Zbędna spacja przed znakiem interpunkcyjnym@ ,".to_string()), "{m:?}"); - assert!(m.contains(&"Zbędna spacja przed znakiem interpunkcyjnym@ .".to_string()), "{m:?}"); - assert!(m.contains(&"Zbędna spacja przed znakiem interpunkcyjnym@ ?".to_string()), "{m:?}"); - assert!(m.contains(&"Podwójna spacja@ ".to_string()), "{m:?}"); - clean("Fajnie :) Naprawdę :-) Albo :D Wartość .5 i wielokropek ..."); - let m = messages("Powiedział ,,tak” i poszedł.. Potem;; koniec."); - assert_eq!(m.iter().filter(|x| x.starts_with("Powtórzony znak")).count(), 3, "{m:?}"); - assert_eq!(check("Powiedział ,,tak”.").iter().find(|i| i.message.starts_with("Powtórzony")).unwrap().suggestions, ["„"]); - clean("Czekał… i czekał... aż do rana."); - } - - #[test] - fn sentence_start_capitals() { - let m = messages("to jest zdanie. a to drugie! czy trzecie? tak."); - assert_eq!(m.iter().filter(|x| x.starts_with("Wielka litera")).count(), 4, "{m:?}"); - assert_eq!(check("ala ma kota.")[0].suggestions, ["Ala"]); - // Abbreviations, initials, ordinals, dates, ellipses and dialogue dashes. - clean("Kupiłem owoce, np. jabłka, gruszki itp. oraz warzywa."); - clean("W 1990 r. powstała firma, m.in. dzięki dotacji."); - clean("Spotkałem J. Kowalskiego i prof. Nowaka przy ul. Długiej."); - clean("Zajął 5. miejsce w XX w. sporcie."); - clean("Czekałem... aż w końcu przyszedł."); - clean("– Naprawdę? – zapytał. – Tak – odpowiedziała."); - clean("Powiedział: „Wracam”. Potem wyszedł."); - // A list item may start in lower case. - clean("jabłka,"); - } - - #[test] - fn missing_commas() { - let m = messages("Wiem że przyjdzie. Mam kota który śpi. Zostałem bo musiałem. Idę aby zdążyć. Nie przyszedł ponieważ padało."); - for want in [ - "Brak przecinka przed „że”@ że", - "Brak przecinka przed „który”@ który", - "Brak przecinka przed „aby”@ aby", - "Brak przecinka przed „ponieważ”@ ponieważ", - ] { - assert!(m.contains(&want.to_string()), "{want}: {m:?}"); - } - assert_eq!(check("Wiem że tak.")[0].suggestions, [", że"]); - // Compound conjunctions, particles, conjunctions and prepositions take no comma there. - clean("Przyszedł, mimo że padało, chyba że nie."); - clean("Zrobię to, tylko jeśli zdążę, i to tak aby nikt nie widział."); - clean("To dom, w którym mieszkam, i ogród, przez który idę."); - clean("Wiem, że przyjdzie, i że zostanie."); - clean("Myślę, że gdyby przyszedł, byłoby dobrze."); - clean("Zachowywał się, jak gdyby nigdy nic."); - clean("Który z nich? To który wybierasz?"); - clean("Po raz nie wiadomo który."); - clean("Dlatego że padało, zostaliśmy."); - } - - #[test] - fn we_and_ze() { - let m = messages("Spotkamy się w wtorek w Wrocławiu z szkoły z mną."); - assert_eq!(m.iter().filter(|x| x.starts_with("Przed tym słowem")).count(), 4, "{m:?}"); - let v = check("Z szkoły wracam."); - assert_eq!(v[0].suggestions, ["Ze"]); - clean("Byłem w wodzie, w wieku dziesięciu lat, z szafą, z siostrą i z zamkiem."); - clean("Wracam ze szkoły we wtorek ze mną."); - } - - #[test] - fn correct_polish_text_is_clean() { - clean( - "Zażółć gęślą jaźń. Chrząszcz brzmi w trzcinie, a źdźbło trawy się kołysze. \ - Przyjdę jutro, chyba że będzie padać. Książka, którą czytam, jest ciekawa.", - ); - clean("Pięćdziesięciu uczniów przyszło, ponieważ ogłoszono konkurs."); - } - - proptest::proptest! { - #[test] - fn never_panics(s in "\\PC{0,80}") { - let _ = check(&s); - } - - #[test] - fn never_panics_on_polish_like_text(s in "[a-ząćęłńóśźżA-Z ,.;:!?„”…\\-]{0,60}") { - for i in check(&s) { - proptest::prop_assert!(i.start <= i.end && i.end <= s.len()); - proptest::prop_assert!(s.is_char_boundary(i.start) && s.is_char_boundary(i.end)); - } - } - } -} diff --git a/crates/proof/src/pl/mod.rs b/crates/proof/src/pl/mod.rs deleted file mode 100644 index 7cde9d0f9..000000000 --- a/crates/proof/src/pl/mod.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Polish proofing. -//! -//! - **Spelling** against the SJP.PL Hunspell dictionary installed on the system -//! ([`dictionary`], found by [`crate::hunspell`]: `pl_PL.aff`/`pl_PL.dic` from the OS, a -//! package manager or LibreOffice; it isn't bundled). Without it Polish text gets no spelling -//! marks. Capitalized stems are names (`Warszawa`); a sentence may start with any word and -//! headings may be in capitals. Hyphenated compounds are checked part by part -//! (`biało-czerwony`), foreign names inflected with an apostrophe are accepted -//! (`Kennedy'ego`), and the user dictionary applies as in English. -//! - **Suggestions** tuned for Polish ([`suggest()`]): missing diacritics first (`zolw` → `żółw`), then -//! the common spelling confusions (`ż`/`rz`, `u`/`ó`, `h`/`ch`…), a missing space (`napewno` → -//! `na pewno`), one-letter typos, and a dictionary scan for anything further off. -//! - **Grammar** ([`grammar`]): cheap, reliable rules with Polish messages. - -pub mod grammar; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of a Polish Hunspell dictionary. -const NAMES: &[&str] = &["pl_PL", "pl-PL", "pl"]; - -/// The Polish dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Polish"); - -/// The Polish dictionary: the installed Hunspell `pl_PL` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Polish spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Polish dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is a Polish dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Endings Polish adds after an apostrophe to foreign names whose last letter isn't pronounced -/// (`Kennedy'ego`, `Wilde'a`, `Shakespeare'owi`). -const APOSTROPHE_ENDINGS: &[&str] = &["a", "ach", "ami", "e", "ego", "em", "emu", "i", "m", "om", "owi", "owie", "u", "y", "ów", "ą", "ę"]; - -/// Is `word` spelled correctly in Polish? Numbers, single letters, short words in capitals -/// (acronyms: `PKO`, `ZUS`), words with digits and words in other scripts are accepted, and so -/// is every word when no Polish dictionary is installed. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - let w = w.replace('’', "'"); - if w.contains('-') { - return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); - } - known(d, &w) -} - -/// Is one word (no hyphens) in the dictionary, the user dictionary, or a foreign name with a -/// Polish ending after an apostrophe? -fn known(d: &AffixDict, w: &str) -> bool { - if d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) { - return true; - } - if let Some((stem, ending)) = w.split_once('\'') { - return stem.chars().next().is_some_and(char::is_uppercase) && stem.chars().count() >= 2 && APOSTROPHE_ENDINGS.contains(&ending); - } - false -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dictionary_loads() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert!(d.len() > 50, "{}", d.len()); - assert_eq!(d.rule_count(), 5, "the test dictionary's affix rules"); - assert!(!d.rep().is_empty() && !d.map().is_empty() && !d.try_chars().is_empty()); - } - - #[test] - fn without_a_dictionary_nothing_is_flagged_and_nothing_suggested() { - let none = AffixDict::default(); - for w in ["żeka", "kturzy", "xqzv", ""] { - assert!(is_correct_with(&none, w), "{w}"); - } - assert!(suggest::suggest_in(&none, "żeka", 5).is_empty()); - assert!(install_dictionary("FLAG long\n", "1\nkot\n").is_err(), "unsupported files are refused"); - } - - #[test] - fn polish_words_are_correct() { - test_dict::install(); - for w in [ - "źdźbło", - "gżegżółka", - "chrząszcz", - "pięćdziesięciu", - "rzeka", - "którzy", - "żółw", - "Zażółć", - "gęślą", - "jaźń", - "konstytucja", - "konstytucyjnego", - "niebieskiego", - "nieładnie", - "przyszliśmy", - "zrobilibyśmy", - "Warszawa", - "Warszawie", - "WARSZAWA", - "Kot", - "biało-czerwony", - "polsko-niemieckiej", - "Kennedy'ego", - "Shakespeare’a", - "PKO", - "2026", - "e-mail", - "w", - "się", - ] { - assert!(is_correct(w), "{w}"); - } - } - - #[test] - fn misspellings_are_flagged() { - test_dict::install(); - for w in ["żeka", "kturzy", "napewno", "wogóle", "zolw", "chuśtawka", "bżoza", "warszawa", "niewiem", "pszyjaciel", "Kennedy'xyz"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("zxqpolskisłowo")); - crate::add_word("Zxqpolskisłowo"); - assert!(is_correct("zxqpolskisłowo")); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Polish checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_polish_check() { - test_dict::install(); - let lang = crate::ProofLang::Pl; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("ŻEKA", lang, &defaults), "short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("ŻEKA", lang, &check_caps)); - assert!(crate::is_correct_in_with("RZEKA", lang, &check_caps)); - assert!(crate::is_correct_in_with("żeka2", lang, &defaults)); - assert!(!crate::is_correct_in_with("żeka2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("ŻEKA"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/pl/suggest.rs b/crates/proof/src/pl/suggest.rs deleted file mode 100644 index 7baeaa54f..000000000 --- a/crates/proof/src/pl/suggest.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Polish spelling suggestions, best first: the shared ranking ([`crate::affix_suggest`]) with the -//! words Polish runs into their neighbours (`napewno` → `na pewno`, `kochamcię` → `kocham cię`), -//! letters to try for a dictionary without a TRY line, and final devoicing (`chlep` → `chleb`). - -use crate::affix::AffixDict; -use crate::affix_suggest::{COST_ADD_DIACRITIC, COST_MISTYPE, Split, Tuning}; - -/// Short words often run into the next one (`na pewno`, `w ogóle`, `nie wiem`, `przede -/// wszystkim`). -const LEADING_WORDS: &[&str] = &[ - "a", "i", "o", "u", "w", "z", "we", "ze", "na", "po", "do", "od", "za", "co", "to", "nie", "jak", "tak", "przy", "bez", "pod", "nad", "dla", - "że", "by", "czy", "już", "ani", "przed", "przez", "przede", "ku", "aż", "bo", "ten", "ta", "te", "tym", "tej", "też", "jest", "ode", "beze", - "nade", -]; -/// Short words often run into the one before (`kocham cię`, `daj mi`, `bałem się`). -const TRAILING_WORDS: &[&str] = - &["się", "cię", "mi", "mu", "ci", "go", "ją", "je", "nam", "wam", "im", "by", "że", "no", "mnie", "tobie", "jej", "ich"]; - -/// What Polish adds to its dictionary's tables. -const TUNING: Tuning = Tuning { - split: Split::Near { leading: LEADING_WORDS, trailing: TRAILING_WORDS }, - alphabet: "aioeznrwcysptkmdłuljągbhęśćóżfńźvqx", - mistype_cost, - ..Tuning::DEFAULT -}; - -/// Final devoicing makes a voiced consonant at the end of a word sound voiceless, so writing it -/// voiceless (`chlep` for `chleb`) is a likelier slip than any other typo. -fn mistype_cost(chars: &[char], i: usize, c: char) -> u32 { - let last = i + 1 == chars.len(); - if last && matches!((chars.get(i), c), (Some('p'), 'b') | (Some('t'), 'd') | (Some('k'), 'g')) { COST_ADD_DIACRITIC - 1 } else { COST_MISTYPE } -} - -/// Spelling suggestions for a Polish word, best first, at most `max`, in the word's letter case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &TUNING) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::affix_suggest::{fold, weighted_distance}; - - fn has(word: &str, want: &str) { - let s = suggest(word, 8); - assert!(s.iter().any(|x| x == want), "{word} → {s:?}, want {want}"); - } - - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(suggest_in); - } - - #[test] - fn polish_misspellings_get_the_right_suggestion() { - crate::pl::test_dict::install(); - has("żeka", "rzeka"); - has("kturzy", "którzy"); - has("napewno", "na pewno"); - has("wogóle", "w ogóle"); - has("niewiem", "nie wiem"); - has("zolw", "żółw"); - has("gzegzolka", "gżegżółka"); - has("chuśtawka", "huśtawka"); - has("bżoza", "brzoza"); - has("pszyjaciel", "przyjaciel"); - has("warszawa", "Warszawa"); - has("Żeka", "Rzeka"); - has("ZOLW", "ŻÓŁW"); - has("konstytucija", "konstytucja"); - has("wziąść", "wziąć"); - has("dziewczyan", "dziewczyna"); - } - - #[test] - fn voiced_consonant_suggestion_ranks_first() { - crate::pl::test_dict::install(); - let result = suggest("chlep", 6); - assert_eq!(result.first().map(String::as_str), Some("chleb"), "{result:?}"); - } - - #[test] - fn the_likeliest_fix_comes_first() { - crate::pl::test_dict::install(); - assert_eq!(suggest("kturzy", 3).first().map(String::as_str), Some("którzy")); - assert_eq!(suggest("zolw", 3).first().map(String::as_str), Some("żółw")); - assert_eq!(suggest("napewno", 3).first().map(String::as_str), Some("na pewno")); - assert_eq!(suggest("żeka", 3).first().map(String::as_str), Some("rzeka")); - } - - #[test] - fn edge_cases_stay_empty_or_bounded() { - crate::pl::test_dict::install(); - assert!(suggest("", 5).is_empty()); - assert!(suggest("kot", 0).is_empty()); - assert!(suggest(&"a".repeat(41), 5).is_empty()); - assert!(suggest("dwa słowa", 5).is_empty()); - assert!(suggest("żółw", 5).len() <= 5); - assert!(!suggest("żółw", 5).contains(&"żółw".to_string())); - } - - #[test] - fn distance_weighs_diacritics_half() { - let map = vec![vec!['a', 'ą'], vec!['o', 'ó', 'u']]; - let related = |a: char, b: char| fold(a, &map) == fold(b, &map); - let c = |s: &str| s.chars().collect::>(); - assert_eq!(weighted_distance(&c("ktury"), &c("który"), &related, 9), 1); - assert_eq!(weighted_distance(&c("kot"), &c("kto"), &related, 9), 2); - assert_eq!(weighted_distance(&c("kot"), &c("kotek"), &related, 9), 4); - assert_eq!(weighted_distance(&c("kot"), &c("pies"), &related, 2), 3); - assert_eq!(weighted_distance(&c(""), &c("ab"), &related, 9), 4); - } - - #[test] - fn suggestions_are_fast_enough() { - crate::pl::test_dict::install(); - // Warm the dictionary, then time a worst case (a long word nothing near): the scan round. - let _ = suggest("kot", 1); - let t = std::time::Instant::now(); - for w in ["xqzvwpolskiegoslowa", "pszyjacielowi", "zażółćgęśląjaźń"] { - let _ = suggest(w, 6); - } - let ms = t.elapsed().as_secs_f64() * 1000.0; - eprintln!("3 suggestion calls: {ms:.0} ms"); - assert!(ms < 20_000.0, "{ms} ms"); - } -} diff --git a/crates/proof/src/pl/test_dict.rs b/crates/proof/src/pl/test_dict.rs deleted file mode 100644 index 80c1aa244..000000000 --- a/crates/proof/src/pl/test_dict.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! A tiny synthetic Polish Hunspell dictionary for tests (written for these tests; not taken from -//! any real dictionary). Real dictionaries are installed on the system, never bundled. - -/// Affix rules: a few suffixes and one prefix, plus the suggestion tables the Polish -/// suggestions use (`TRY`, `MAP` for diacritics, `REP` for common confusions). -pub const AFF: &str = "SET UTF-8 -TRY aioeznrwcysżtkpdmłjlubąęgchśóźćń -MAP 9 -MAP aą -MAP cć -MAP eę -MAP lł -MAP nń -MAP oó -MAP uó -MAP sś -MAP zżź -REP 6 -REP ż rz -REP rz ż -REP ch h -REP h ch -REP sz rz -REP ść ć -SFX A Y 1 -SFX A a ie a -SFX B Y 3 -SFX B y ego y -SFX B i iego i -SFX B i iej i -PFX N Y 1 -PFX N 0 nie . -"; - -/// Stems (the count line first, as in Hunspell files). -pub const DIC: &str = "69 -źdźbło -gżegżółka -chrząszcz -chleb -chłep -chlap -chlip -chlup -chlej -pięćdziesięciu -rzeka -którzy -żółw -zażółć -gęślą -jaźń -konstytucja -konstytucyjny/B -niebieski/B -niemiecki/B -ładnie/N -przyszliśmy -zrobilibyśmy -Warszawa/A -kot -biało -czerwony -polsko -mail -w -się -huśtawka -brzoza -przyjaciel -wziąć -dziewczyna -na -pewno -ogóle -nie -wiem -płynie -a -i -o -to -że -tak -jest -brzmi -trzcina/A -napisał -poszedł -spać -bo -wylała -świat -dom -ma -ala -kota -dzień -dobry -jutro -bardzo -dobrze -mamy -czas -ten -"; - -/// Make [`AFF`]/[`DIC`] the Polish dictionary (every test that needs one calls this; they all -/// install the same one, so tests running in parallel agree). -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} diff --git a/crates/proof/src/pt/grammar.rs b/crates/proof/src/pt/grammar.rs deleted file mode 100644 index 6b72e3226..000000000 --- a/crates/proof/src/pt/grammar.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! Portuguese grammar and punctuation checks: cheap, rule-based and tuned to stay quiet when -//! unsure (a missed mistake costs less than a wrong mark on correct text). Messages are in -//! Portuguese. -//! -//! - **Repeated word** (`ele ele`), except interjections and words Portuguese doubles on purpose -//! (`sim sim`, `não não`, `ora ora`). -//! - **Space before punctuation** (`bem ,`) and **double spaces**; emoticons (` :)`) and -//! ellipses are left alone. -//! - **Capital letter at the start of a sentence**, knowing Portuguese abbreviations (`etc.`, -//! `pág.`, `Prof.`, `Dr.`), initials, ordinal numbers (`5. colocado`), ellipses and dialogue -//! dashes (`— Vai — disse`), after which lower case is correct. -//! - **Doubled punctuation** (`,,`, `..`, `;;`). -//! -//! No comma rule (`que` opens clauses only sometimes) and no English rule (such as `a`/`an`) -//! applies to Portuguese text. - -use crate::{Issue, IssueKind, ProofLang, words}; - -/// Words Portuguese repeats on purpose: interjections, onomatopoeia and emphatic doubling. -const ALLOWED_REPEATS: &[&str] = &[ - "sim", "não", "ora", "olha", "já", "bem", "muito", "vai", "lá", "cá", "aí", "ha", "he", "hi", "oh", "ah", "ai", "ui", "uf", "uxa", "oba", "viva", - "psiu", "olá", "tchau", "xau", "adeus", "devagar", -]; - -/// Abbreviations written with a final period after which a sentence goes on in lower case -/// (lower case, without the period). -const ABBREVIATIONS: &[&str] = &[ - "etc", "ex", "pág", "págs", "séc", "sécs", "fl", "av", "vol", "p", "pp", "ed", "tel", "min", "max", "art", "par", "prof", "profa", "dr", "dra", - "sr", "sra", "srta", "eng", "arq", "op", "cit", "cf", "ibid", "id", "vide", "seg", "ter", "qua", "qui", "sex", "sáb", "dom", "jan", "fev", "mar", - "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez", "s.a", "d.c", "a.c", "pg", "ref", "cod", "dept", "univ", "inst", "lat", "ing", - "esp", "fr", "it", "al", "obs", "att", "obs", -]; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::Pt } -} - -fn capitalize(w: &str) -> String { - crate::affix::capitalize(w) -} - -/// Is `prev`, before a period, an abbreviation, an initial, a number or a Roman numeral (after -/// which a lower-case word doesn't start a sentence)? -fn abbreviation_or_number(prev: &str) -> bool { - prev.chars().count() <= 1 - || prev.chars().any(|c| c.is_ascii_digit()) - || (prev.chars().all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))) - || ABBREVIATIONS.contains(&prev.to_lowercase().as_str()) -} - -const CLOSING: [char; 8] = ['"', '”', '’', '\'', ')', '»', ']', '\u{1}']; -const OPENING: [char; 8] = ['"', '“', '«', '(', '[', '\'', '‚', '—']; - -/// Does the gap between two words end a sentence: a `.`, `!` or `?` (not an ellipsis, not after -/// an abbreviation), then space, with nothing but quotes and brackets around? -fn ends_sentence(gap: &str, prev: &str) -> bool { - let t = gap.trim_start_matches(CLOSING); - let Some(end) = t.chars().next() else { return false }; - if !matches!(end, '.' | '!' | '?') || t.starts_with("..") { - return false; - } - if end == '.' && abbreviation_or_number(prev) { - return false; - } - let rest = t.trim_start_matches(['.', '!', '?']).trim_start_matches(CLOSING); - rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| OPENING.contains(&c)) -} - -/// Grammar issues in a paragraph of Portuguese text, in order. -pub fn check(text: &str) -> Vec { - check_with(text, true) -} - -/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital -/// (false for a list item, which may be a fragment). -pub fn check_with(text: &str, capital_first: bool) -> Vec { - let ws: Vec<(usize, usize)> = words(text); - let mut v = Vec::new(); - repeated_words(text, &ws, &mut v); - spaces(text, &mut v); - sentence_capitals(text, &ws, capital_first, &mut v); - doubled_punctuation(text, &mut v); - v.sort_by_key(|i| (i.start, i.end)); - v -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() - && gap.chars().all(char::is_whitespace) - && w0.chars().all(char::is_alphabetic) - && w0.to_lowercase() == w1.to_lowercase() - && !ALLOWED_REPEATS.contains(&w0.to_lowercase().as_str()) - { - v.push(issue(*a0, *b1, format!("Palavra repetida: \"{w1}\""), vec![w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - let flag = match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '”' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Espaço antes da pontuação".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Espaço duplo".into(), vec![" ".into()])); - } -} - -fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec) { - // List items (`– maçãs,`) may start in lower case. - let list_item = text.trim_end().ends_with([',', ';']); - for (k, &(a, b)) in ws.iter().enumerate() { - let Some(w) = text.get(a..b) else { continue }; - let starts = if k == 0 { - let before = text.get(..a).unwrap_or(""); - capital_first && !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c)) - } else { - let Some(&(pa, pb)) = ws.get(k - 1) else { continue }; - ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or("")) - }; - if starts && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) { - v.push(issue(a, b, "Letra maiúscula no início da frase".into(), vec![capitalize(w)])); - } - } -} - -fn doubled_punctuation(text: &str, v: &mut Vec) { - let bytes = text.as_bytes(); - let mut i = 0; - while let Some(&b) = bytes.get(i) { - if !matches!(b, b',' | b'.' | b';') { - i += 1; - continue; - } - let run = bytes.get(i..).map_or(0, |r| r.iter().take_while(|&&x| x == b).count()); - let flag = match b { - b'.' => run == 2, - _ => run >= 2, - }; - if flag { - let end = i + run; - let suggestions: Vec = match b { - b'.' => vec![".".into(), "…".into()], - _ => vec![char::from(b).to_string()], - }; - v.push(issue(i, end, "Sinal repetido".into(), suggestions)); - } - i += run.max(1); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn messages(t: &str) -> Vec { - check(t).into_iter().map(|i| format!("{}@{}", i.message, &t[i.start..i.end])).collect() - } - - fn clean(t: &str) { - let m = messages(t); - assert!(m.is_empty(), "{t:?}: {m:?}"); - } - - #[test] - fn repeated_words() { - let v = check("Eu eu vi isso e ele ele saiu."); - assert_eq!(v.len(), 2, "{v:?}"); - assert_eq!(v[0].message, "Palavra repetida: \"eu\""); - assert_eq!(v[0].suggestions, ["Eu"]); - assert!(v.iter().all(|i| i.lang == ProofLang::Pt && i.kind == IssueKind::Grammar)); - clean("Sim, sim! Não, não! Ora ora, que surpresa."); - } - - #[test] - fn spaces_and_punctuation() { - let m = messages("Olá , mundo . Tudo bem ?"); - assert!(m.contains(&"Espaço antes da pontuação@ ,".to_string()), "{m:?}"); - assert!(m.contains(&"Espaço antes da pontuação@ .".to_string()), "{m:?}"); - assert!(m.contains(&"Espaço antes da pontuação@ ?".to_string()), "{m:?}"); - assert!(m.contains(&"Espaço duplo@ ".to_string()), "{m:?}"); - clean("Legal :) Muito bom :-) Ou :D Valor .5 e reticências ..."); - let m = messages("Falou,, e saiu.. depois;; fim."); - assert_eq!(m.iter().filter(|x| x.starts_with("Sinal repetido")).count(), 3, "{m:?}"); - clean("Esperei… e esperei... até ele voltar."); - } - - #[test] - fn sentence_start_capitals() { - let m = messages("isso é um teste. a segunda frase! terceira? ok."); - assert_eq!(m.iter().filter(|x| x.starts_with("Letra maiúscula")).count(), 4, "{m:?}"); - assert_eq!(check("ele tem razão.")[0].suggestions, ["Ele"]); - // Abbreviations, initials, ordinals, ellipses and dialogue dashes. - clean("Comprei frutas, etc. e depois saímos."); - clean("Conheci o Dr. Silva na Av. Paulista."); - clean("Zhou ficou em 5. lugar no séc. XX."); - clean("Esperei... e ele apareceu."); - clean("— Vai — disse ele. — Amanhã eu volto."); - clean("Ele disse: \"Volto amanhã\". E saiu."); - // A list item may start in lower case. - clean("maçãs,"); - } - - #[test] - fn correct_portuguese_text_is_clean() { - clean( - "O rato roeu a roupa do rei de Roma. Quem não tem cão caça com gato, \ - e quem tem paciência colhe o que plantou.", - ); - clean("Brasília, 12 de outubro de 2026 — uma data memorável."); - } - - proptest::proptest! { - #[test] - fn never_panics(s in "\\PC{0,80}") { - let _ = check(&s); - } - - #[test] - fn never_panics_on_portuguese_like_text(s in "[a-záàâãéêíóôõúüçA-Z ,.;:!?\"…\\-—]{0,60}") { - for i in check(&s) { - proptest::prop_assert!(i.start <= i.end && i.end <= s.len()); - proptest::prop_assert!(s.is_char_boundary(i.start) && s.is_char_boundary(i.end)); - } - } - } -} diff --git a/crates/proof/src/pt/mod.rs b/crates/proof/src/pt/mod.rs deleted file mode 100644 index ea91dbdb1..000000000 --- a/crates/proof/src/pt/mod.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Portuguese proofing. -//! -//! - **Spelling** against the Hunspell `pt_BR` dictionary installed on the system -//! ([`dictionary`], found by [`crate::hunspell`]: `pt_BR.aff`/`pt_BR.dic` — VERO and its -//! derivatives — from the OS, a package manager or LibreOffice; it isn't bundled). Without it -//! Portuguese text gets no spelling marks. Capitalized stems are names (`Brasília`); a sentence -//! may start with any word and headings may be in capitals. Hyphenated compounds are checked -//! part by part (`guarda-roupa`, `ex-marido`), and the user dictionary applies as in English. -//! - **Suggestions** tuned for Portuguese ([`suggest`]): missing accents first (`coraçao` → -//! `coração`, `saude` → `saúde`), then the dictionary's `REP` confusions (`seje` → `seja`), a -//! missing space, one-letter typos, and a dictionary scan for anything further off. -//! - **Grammar** ([`grammar`]): cheap, reliable rules with Portuguese messages. - -pub mod grammar; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of a Portuguese Hunspell dictionary. -const NAMES: &[&str] = &["pt_BR", "pt-BR", "pt"]; - -/// The Portuguese dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Portuguese"); - -/// The Portuguese dictionary: the installed Hunspell `pt_BR` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Portuguese spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Portuguese dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is a Portuguese dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Is `word` spelled correctly in Portuguese? Numbers, single letters, short words in capitals -/// (acronyms: `USP`, `PT`), words with digits and words in other scripts are accepted, and so -/// is every word when no Portuguese dictionary is installed. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - let Some(w) = crate::spelling::word_to_check(d, word, opts) else { - return true; - }; - if w.contains('-') { - return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p)); - } - known(d, w) -} - -/// Is one word (no hyphens) in the dictionary or the user dictionary? -fn known(d: &AffixDict, w: &str) -> bool { - d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dictionary_loads() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert!(d.len() > 50, "{}", d.len()); - assert!(d.rule_count() >= 5, "the test dictionary's affix rules"); - assert!(!d.rep().is_empty() && !d.map().is_empty() && !d.try_chars().is_empty()); - } - - #[test] - fn without_a_dictionary_nothing_is_flagged() { - let none = AffixDict::default(); - for w in ["coraçao", "saude", "xqzv", ""] { - assert!(is_correct_with(&none, w), "{w}"); - } - assert!(install_dictionary("FLAG long\n", "1\nkot\n").is_err(), "unsupported files are refused"); - } - - #[test] - fn portuguese_words_are_correct() { - test_dict::install(); - for w in [ - "coração", - "saudade", - "pássaro", - "saúde", - "árvore", - "escrever", - "computador", - "praia", - "conhecer", - "Brasília", - "São", - "autoestima", - "frequência", - "exceção", - "amanhã", - "vocês", - "coragem", - "parabéns", - "cantava", - "cantando", - "provável", - "prováveis", - "descobrir", - "trabalho", - "CORAGEM", - "USP", - "2026", - "e-mail", - "guarda-roupa", - "ex-marido", - "vice-presidente", - "e", - "não", - ] { - assert!(is_correct(w), "{w}"); - } - } - - #[test] - fn misspellings_are_flagged() { - test_dict::install(); - for w in ["coraçao", "saude", "trbalho", "excrever", "prsidente", "frequencia", "amnha", "cantaando"] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn forbidden_words_are_rejected() { - test_dict::install(); - // `frequencia/F` is listed with FORBIDDENWORD: the old spelling, never correct. - assert!(!is_correct("frequencia") && !is_correct("Frequencia") && !is_correct("FREQUENCIA")); - assert!(is_correct("frequência")); - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("zxqportuguespalavra")); - crate::add_word("Zxqportuguespalavra"); - assert!(is_correct("zxqportuguespalavra")); - } - - #[test] - fn user_dictionary_accepts_a_hyphenated_word_whole() { - test_dict::install(); - let text = "Ele disse que está na Unigranrio-RJ."; - assert_eq!(crate::check_spelling_in(text, crate::ProofLang::Pt).len(), 1, "Unigranrio isn't in the dictionary"); - // Added as the context menu adds it, hyphen and all (#2082). - crate::add_word("Unigranrio-RJ"); - assert!(crate::check_spelling_in(text, crate::ProofLang::Pt).is_empty()); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Portuguese checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_portuguese_check() { - test_dict::install(); - let lang = crate::ProofLang::Pt; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("PRAYA", lang, &defaults), "short capitals are acronyms by default"); - assert!(!crate::is_correct_in_with("PRAYA", lang, &check_caps)); - assert!(crate::is_correct_in_with("PRAIA", lang, &check_caps)); - assert!(crate::is_correct_in_with("praya2", lang, &defaults)); - assert!(!crate::is_correct_in_with("praya2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("PRAYA"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/pt/suggest.rs b/crates/proof/src/pt/suggest.rs deleted file mode 100644 index f8f33bff6..000000000 --- a/crates/proof/src/pt/suggest.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Portuguese spelling suggestions, best first: the shared ranking ([`crate::affix_suggest`]) with -//! the words Portuguese runs into their neighbours (`nãoé` → `não é`, `paraela` → `para ela`) and -//! letters to try for a dictionary without a TRY line. - -use crate::affix::AffixDict; -use crate::affix_suggest::{Split, Tuning}; - -/// Short words often run into the next one (`não é`, `da água`, `para isso`). -const LEADING_WORDS: &[&str] = &[ - "a", "o", "e", "é", "de", "do", "da", "dos", "das", "em", "no", "na", "nos", "nas", "por", "para", "com", "sem", "sob", "sobre", "ao", "aos", - "à", "às", "que", "se", "não", "como", "mas", "mais", "já", "até", "entre", "contra", "perante", "desde", "após", "ante", "durante", "mediante", - "segundo", "conforme", "salvo", "fora", "onde", "quando", "bem", "mal", "pois", "logo", "portanto", "porém", "todavia", "contudo", "assim", - "tão", "tanto", "tudo", "nada", "algo", "cada", "outro", "mesmo", "próprio", "ainda", "tampouco", "quase", "talvez", "nunca", "sempre", -]; -/// Short words often run into the one before (`amo-te`, `deu-lhe`; in writing without the -/// hyphen: `amote`, `deulhe`). -const TRAILING_WORDS: &[&str] = &[ - "me", "te", "se", "lhe", "nos", "vos", "lhes", "o", "a", "os", "as", "ele", "ela", "eles", "elas", "isso", "isto", "aquilo", "mim", "ti", "si", - "eu", "tu", "que", "de", "em", "é", -]; - -/// What Portuguese adds to its dictionary's tables. -const TUNING: Tuning = Tuning { - split: Split::Near { leading: LEADING_WORDS, trailing: TRAILING_WORDS }, - alphabet: "aeiousrntmdlcpvgqbzfhjxçãõáéíóúâêôkw", - ..Tuning::DEFAULT -}; - -/// Spelling suggestions for a Portuguese word, best first, at most `max`, in the word's letter case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &TUNING) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::affix_suggest::{fold, weighted_distance}; - - fn has(word: &str, want: &str) { - let s = suggest(word, 8); - assert!(s.iter().any(|x| x == want), "{word} → {s:?}, want {want}"); - } - - #[test] - fn nosuggest_stems_and_forms_are_never_offered() { - crate::affix::tests::assert_nosuggest_is_honoured(suggest_in); - } - - #[test] - fn portuguese_misspellings_get_the_right_suggestion() { - crate::pt::test_dict::install(); - has("coraçao", "coração"); - has("saude", "saúde"); - has("seje", "seja"); - has("excessão", "exceção"); - has("previlégio", "privilégio"); - has("prsidente", "presidente"); - has("provavel", "provável"); - has("trbalho", "trabalho"); - has("nãoé", "não é"); - has("paraela", "para ela"); - has("brasília", "Brasília"); - has("Coraçao", "Coração"); - } - - #[test] - fn the_likeliest_fix_comes_first() { - crate::pt::test_dict::install(); - assert_eq!(suggest("coraçao", 3).first().map(String::as_str), Some("coração")); - assert_eq!( - suggest("saude", 3).first().map(String::as_str), - Some("saúde"), - "an accent at the first difference beats one further right (saudê is a word too)" - ); - assert_eq!(suggest("seje", 3).first().map(String::as_str), Some("seja")); - } - - #[test] - fn edge_cases_stay_empty_or_bounded() { - crate::pt::test_dict::install(); - assert!(suggest("", 5).is_empty()); - assert!(suggest("bem", 0).is_empty()); - assert!(suggest(&"a".repeat(41), 5).is_empty()); - assert!(suggest("duas palavras", 5).is_empty()); - assert!(suggest_in(&crate::affix::AffixDict::default(), "coraçao", 5).is_empty()); - assert!(suggest("coração", 5).len() <= 5); - assert!(!suggest("coração", 5).contains(&"coração".to_string())); - } - - #[test] - fn distance_weighs_accents_half() { - let map = vec![vec!['a', 'á', 'à', 'â', 'ã'], vec!['c', 'ç']]; - let related = |a: char, b: char| fold(a, &map) == fold(b, &map); - let c = |s: &str| s.chars().collect::>(); - assert_eq!(weighted_distance(&c("coraçao"), &c("coração"), &related, 9), 1); - assert_eq!(weighted_distance(&c("coracao"), &c("coração"), &related, 9), 2, "ç and ã are two accent slips"); - assert_eq!(weighted_distance(&c("coro"), &c("coro"), &related, 9), 0); - assert_eq!(weighted_distance(&c("bem"), &c("bom"), &related, 9), 2); - assert_eq!(weighted_distance(&c("bem"), &c("bemte"), &related, 9), 4); - assert_eq!(weighted_distance(&c("bem"), &c("ver"), &related, 2), 3); - assert_eq!(weighted_distance(&c(""), &c("ab"), &related, 9), 4); - } - - #[test] - fn suggestions_are_fast_enough() { - crate::pt::test_dict::install(); - // Warm the dictionary, then time a worst case (a long word nothing near): the scan round. - let _ = suggest("bem", 1); - let t = std::time::Instant::now(); - for w in ["xqzvweportuguespalavra", "prsidentes", "paralelepípedo"] { - let _ = suggest(w, 6); - } - let ms = t.elapsed().as_secs_f64() * 1000.0; - eprintln!("3 suggestion calls: {ms:.0} ms"); - assert!(ms < 20_000.0, "{ms} ms"); - } -} diff --git a/crates/proof/src/pt/test_dict.rs b/crates/proof/src/pt/test_dict.rs deleted file mode 100644 index e02be3c16..000000000 --- a/crates/proof/src/pt/test_dict.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! A tiny synthetic Portuguese Hunspell dictionary for tests (written for these tests; not taken -//! from any real dictionary). Real dictionaries are installed on the system, never bundled. -//! -//! It exercises what VERO-like dictionaries use: `FLAG UTF-8` (multi-byte flag characters — -//! `F` marks a forbidden spelling), continuation classes (`provável` → `prováveis`), the -//! suggestion tables (`TRY`, `MAP` for accents, `REP` for common confusions) and one prefix. - -pub const AFF: &str = "SET UTF-8 -FLAG UTF-8 -FORBIDDENWORD F -TRY aeiousrntmdlcpvgqbzfhjxçãõáéíóúâêôkw -MAP 6 -MAP aáàâã -MAP eéê -MAP ií -MAP oóôõ -MAP uú -MAP cç -REP 4 -REP seje seja -REP excessão exceção -REP previlégio privilégio -REP analize análise -SFX A Y 2 -SFX A a ava a -SFX A a ando a -SFX B N 1 -SFX B 0 ável/C . -SFX C N 1 -SFX C vel veis vel -PFX X Y 1 -PFX X 0 des . -"; - -/// Stems (the count line first, as in Hunspell files). -pub const DIC: &str = "76 -coração -saudade -saudê -pássaro -saúde -árvore -escrever -computador -praia -conhecer -Brasília -São -Paulo -guarda -roupa -mail -marido -ex -vice -presidente -autoestima -frequência -frequencia/F -exceção -privilégio -análise -seja -trabalho -canta/A -prov/B -cobrir/X -avião -pássaros -amanhã -vocês -coragem -parabéns -está -ele -disse -existem -bem -não -sim -que -com -para -dos -das -pela -pelo -muito -mais -como -porque -aqui -também -já -ser -ter -ir -ver -dar -e -o -é -água -ela -da -do -de -em -no -na -um -uma -"; - -/// Make [`AFF`]/[`DIC`] the Portuguese dictionary (every test that needs one calls this; they all -/// install the same one, so tests running in parallel agree). -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} diff --git a/crates/proof/src/ru/extra.rs b/crates/proof/src/ru/extra.rs deleted file mode 100644 index 95397e617..000000000 --- a/crates/proof/src/ru/extra.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! Our supplement of Russian words older Hunspell dictionaries lack (`маркетплейс`, `промокод`, -//! `интеграция`, `бонусный`…), from `assets/spelling/ru-extra.txt`: each base form with its -//! declension, expanded here into every form. - -use std::collections::HashSet; -use std::sync::OnceLock; - -const EXTRA_RU: &str = include_str!("../../../../assets/spelling/ru-extra.txt"); - -/// Every form of the supplement: common words (lower case) and names (lower case too, but they -/// only match a word written with a capital). -struct Extra { - words: HashSet, - names: HashSet, -} - -fn extra() -> &'static Extra { - static E: OnceLock = OnceLock::new(); - E.get_or_init(|| { - let mut e = Extra { words: HashSet::new(), names: HashSet::new() }; - for line in EXTRA_RU.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')) { - let (base, class) = line.split_once('\t').map_or((line, ""), |(b, c)| (b.trim(), c.trim())); - let name = base.chars().next().is_some_and(char::is_uppercase); - let set = if name { &mut e.names } else { &mut e.words }; - for f in forms(base, class) { - set.insert(f.to_lowercase()); - } - } - e - }) -} - -/// Is `w` a form of a supplement word (a name only when written with a capital)? -pub(super) fn contains(w: &str) -> bool { - let e = extra(); - let lower = w.to_lowercase(); - e.words.contains(&lower) || (w.chars().next().is_some_and(char::is_uppercase) && e.names.contains(&lower)) -} - -fn is_velar(c: char) -> bool { - matches!(c, 'г' | 'к' | 'х') -} - -fn is_hushing(c: char) -> bool { - matches!(c, 'ж' | 'ш' | 'ч' | 'щ') -} - -/// The forms of `base` declined as `class` (see the supplement file's header). -fn forms(base: &str, class: &str) -> Vec { - let with = |stem: &str, ends: &[&str]| ends.iter().map(|e| format!("{stem}{e}")).collect::>(); - let last = |s: &str| s.chars().last().unwrap_or(' '); - if let Some(list) = class.strip_prefix('=') { - let mut v: Vec = list.split_whitespace().map(str::to_string).collect(); - v.push(base.to_string()); - return v; - } - match class { - "m" => { - let c = last(base); - let plural = if is_velar(c) || is_hushing(c) { "и" } else { "ы" }; - let gen_pl = if is_hushing(c) { "ей" } else { "ов" }; - let mut v = with(base, &["", "а", "у", "ом", "е", plural, gen_pl, "ам", "ами", "ах"]); - if is_hushing(c) || c == 'ц' { - v.push(format!("{base}ем")); - } - v - } - "f" => { - let Some(stem) = base.strip_suffix('а') else { return vec![base.to_string()] }; - let c = last(stem); - let plural = if is_velar(c) || is_hushing(c) { "и" } else { "ы" }; - let mut v = with(stem, &["а", plural, "е", "у", "ой", "ою", "", "ам", "ами", "ах"]); - if is_hushing(c) || c == 'ц' { - v.extend(with(stem, &["ей", "ею"])); - } - v - } - "fia" => { - base.strip_suffix("ия").map_or_else(|| vec![base.to_string()], |s| with(s, &["ия", "ии", "ию", "ией", "иею", "ий", "иям", "иями", "иях"])) - } - "fs" => base.strip_suffix('ь').map_or_else(|| vec![base.to_string()], |s| with(s, &["ь", "и", "ью", "ей", "ям", "ями", "ях"])), - "no" => base.strip_suffix('о').map_or_else(|| vec![base.to_string()], |s| with(s, &["о", "а", "у", "ом", "е", "", "ам", "ами", "ах"])), - "ovat" => { - // Verbs in -овать (`колеровать`, `администрировать`): the conjugation, the past, the - // imperative and the participles. - let Some(stem) = base.strip_suffix("овать") else { return vec![base.to_string()] }; - let mut v = with( - stem, - &[ - "овать", - "ую", - "уешь", - "ует", - "уем", - "уете", - "уют", - "овал", - "овала", - "овало", - "овали", - "уя", - "уй", - "уйте", - "уется", - "уются", - "уюсь", - "уешься", - "уемся", - "уетесь", - "овался", - "овалась", - "овалось", - "овались", - "оваться", - "овав", - "овавшись", - ], - ); - for participle in ["уемый", "ованный", "ующий", "овавший", "ующийся", "овавшийся"] { - let adj = format!("{stem}{participle}"); - if let Some(reflexive) = adj.strip_suffix("ся") { - v.extend(forms(reflexive, "adj").into_iter().map(|f| format!("{f}ся"))); - } else { - v.extend(forms(&adj, "adj")); - } - } - v.push(format!("{stem}ован")); - v.extend(with(stem, &["ована", "овано", "ованы"])); - v - } - "nie" => { - base.strip_suffix("ие").map_or_else(|| vec![base.to_string()], |s| with(s, &["ие", "ия", "ию", "ием", "ии", "ий", "иям", "иями", "иях"])) - } - "adj" => { - if let Some(stem) = base.strip_suffix("ый").or_else(|| base.strip_suffix("ой")) { - let mut v = with(stem, &["ого", "ому", "ым", "ом", "ая", "ой", "ую", "ое", "ые", "ых", "ыми"]); - v.push(base.to_string()); - return v; - } - let Some(stem) = base.strip_suffix("ий") else { return vec![base.to_string()] }; - let c = last(stem); - let ends: &[&str] = if is_velar(c) { - &["ий", "ого", "ому", "им", "ом", "ая", "ой", "ую", "ое", "ие", "их", "ими"] - } else if is_hushing(c) { - &["ий", "его", "ему", "им", "ем", "ая", "ей", "ую", "ее", "ие", "их", "ими"] - } else { - &["ий", "его", "ему", "им", "ем", "яя", "ей", "юю", "ее", "ие", "их", "ими"] - }; - with(stem, ends) - } - _ => vec![base.to_string()], - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn supplement_words_decline() { - for w in [ - "маркетплейс", - "маркетплейсов", - "маркетплейсах", - "Маркетплейсами", - "чекбоксы", - "промокода", - "кешбэки", - "инфографики", - "оплат", - "отгрузок", - "интеграций", - "соцсетей", - "согласий", - "бонусных", - "газоиспользующего", - "колеруется", - "заколерованный", - "онлайн", - "чат-ботом", - "Яндекса", - "Битриксе", - ] { - assert!(contains(w), "{w}"); - } - // Names need their capital; nonsense isn't in. - assert!(!contains("яндекс")); - assert!(!contains("маркетплейсыы")); - } - - #[test] - fn declension_classes() { - assert!(forms("бренд", "m").contains(&"брендов".to_string())); - assert!(forms("блог", "m").contains(&"блоги".to_string())); - assert!(forms("мерч", "m").contains(&"мерчей".to_string())); - assert!(forms("книга", "f").contains(&"книги".to_string())); - assert!(forms("синий", "adj").contains(&"синяя".to_string())); - assert!(forms("русский", "adj").contains(&"русская".to_string())); - assert!(forms("свежий", "adj").contains(&"свежая".to_string())); - assert_eq!(forms("медиа", ""), ["медиа"]); - let ovat = forms("модерировать", "ovat"); - for f in [ - "модерирую", - "модерирует", - "модерировали", - "модерируется", - "модерируемого", - "модерированной", - "модерирующая", - "модерирующемуся", - "модерирован", - ] { - assert!(ovat.contains(&f.to_string()), "{f}"); - } - assert!(forms("юрлицо", "no").contains(&"юрлиц".to_string())); - } -} diff --git a/crates/proof/src/ru/grammar.rs b/crates/proof/src/ru/grammar.rs deleted file mode 100644 index 70b9a1e62..000000000 --- a/crates/proof/src/ru/grammar.rs +++ /dev/null @@ -1,712 +0,0 @@ -//! Russian grammar and punctuation checks: cheap, rule-based and tuned to stay quiet when unsure -//! (a missed comma costs less than a wrong mark on correct text). Messages are in Russian. -//! -//! - **Repeated word** (`в в`), except words Russian doubles on purpose (`да да`, `так так`, -//! `ну ну`, `ха ха`, `очень очень`). -//! - **Space before punctuation** (`да ,`) and **double spaces**; emoticons (` :)`) and ellipses -//! are left alone. -//! - **Capital letter at the start of a sentence**, knowing Russian abbreviations (`т. е.`, `г.`, -//! `руб.`, `ул.`, `и т. д.`), initials, numbers, ellipses and dialogue dashes, after which lower -//! case is correct. -//! - **Missing comma** before conjunctions and relative pronouns that always open a clause: -//! `что`, `чтобы`, `поскольку`, `если`, `хотя` (not `хотя бы`) and the forms of `который`, and -//! before `потому`/`оттого` in `потому что`. Not after a conjunction or particle (`и что`, -//! `только что`, `даже если`, `так что`, `для того чтобы`), not after a preposition (`за что`, -//! `в котором`: the comma goes before it) and not inside `что-то`. -//! - **Doubled punctuation** (`,,` — often a typed opening quote — `..`, `;;`). -//! -//! No English rule (such as `a`/`an`) applies to Russian text. - -use crate::{Issue, IssueKind, ProofLang, words}; - -/// Words Russian repeats on purpose: interjections, onomatopoeia and emphatic doubling. -const ALLOWED_REPEATS: &[&str] = &[ - "да", - "нет", - "так", - "ну", - "ха", - "хи", - "хе", - "ой", - "ай", - "эй", - "ах", - "ох", - "эх", - "ага", - "угу", - "тук", - "кап", - "бах", - "бум", - "тик", - "так", - "ля", - "очень", - "много", - "давно", - "далеко", - "долго", - "еле", - "чуть", - "тихо", - "быстро", - "скорей", - "скорее", - "иди", - "давай", - "пока", - "ладно", - "вот", - "ещё", - "еще", - "всё", - "все", - "кто", - "что", - "то", -]; - -/// Abbreviations written with a final period after which a sentence goes on in lower case -/// (lower case, without the period). -const ABBREVIATIONS: &[&str] = &[ - "т", - "е", - "д", - "п", - "др", - "пр", - "см", - "ср", - "напр", - "т.е", - "т.д", - "т.п", - "т.к", - "т.н", - "т.о", - "г", - "гг", - "в", - "вв", - "н.э", - "до н.э", - "руб", - "коп", - "тыс", - "млн", - "млрд", - "трлн", - "ул", - "пр-т", - "пер", - "наб", - "пл", - "ш", - "д", - "кв", - "корп", - "стр", - "оф", - "эт", - "под", - "обл", - "р-н", - "пос", - "дер", - "им", - "св", - "проф", - "доц", - "акад", - "канд", - "наук", - "зав", - "зам", - "ред", - "изд", - "сост", - "пер", - "рис", - "табл", - "гл", - "разд", - "ст", - "ч", - "п", - "пп", - "абз", - "прим", - "прил", - "илл", - "с", - "сс", - "т", - "тт", - "вып", - "кн", - "экз", - "шт", - "кг", - "мин", - "сек", - "час", - "мес", - "нед", - "янв", - "фев", - "февр", - "мар", - "апр", - "авг", - "сент", - "окт", - "нояб", - "дек", - "пн", - "вт", - "чт", - "пт", - "сб", - "вс", - "англ", - "нем", - "фр", - "франц", - "лат", - "рус", - "греч", - "итал", - "исп", - "букв", - "перен", - "разг", - "устар", - "ок", - "ок", - "прибл", - "мл", - "ст", - "тел", - "моб", - "факс", - "эл", - "почт", - "вкл", - "искл", - "мм", - "см", - "дм", - "км", - "м", - "л", - "мл", - "т", - "ц", - "млн", - "долл", - "евро", - "др", - "проч", - "etc", - "vs", -]; - -/// Conjunctions and relative pronouns that open a clause: a comma comes before them. -const CLAUSE_OPENERS: &[&str] = &[ - "что", - "чтобы", - "поскольку", - "если", - "хотя", - "который", - "которая", - "которое", - "которые", - "которого", - "которой", - "которому", - "которым", - "котором", - "которую", - "которых", - "которыми", -]; - -/// Words after which a clause opener takes no comma of its own: conjunctions (`и что`, `а если`), -/// particles (`только что`, `даже если`, `лишь бы`), interrogatives and the first halves of -/// compound conjunctions (`потому что`, `так что`, `для того чтобы`, `с тем чтобы`, `несмотря на -/// то что`, `тогда как`, `как будто`). -const NO_COMMA_AFTER: &[&str] = &[ - "и", - "а", - "но", - "да", - "или", - "либо", - "ни", - "не", - "же", - "ли", - "бы", - "б", - "то", - "только", - "лишь", - "именно", - "даже", - "особенно", - "потому", - "оттого", - "поэтому", - "так", - "затем", - "тем", - "того", - "ввиду", - "вследствие", - "несмотря", - "благодаря", - "прежде", - "раньше", - "мере", - "пор", - "чем", - "будто", - "словно", - "точно", - "как", - "если", - "хотя", - "хоть", - "чтобы", - "что", - "кто", - "где", - "когда", - "почему", - "зачем", - "куда", - "откуда", - "сколько", - "разве", - "неужели", - "вот", - "вон", - "ну", - "уже", - "это", - "просто", - "чуть", - "мол", - "ведь", - "вроде", - "тоже", - "также", - "ещё", - "еще", - "всё", - "все", - "почти", - "вовсе", - "ой", - "ах", - "эх", - "ох", - "ага", - "нет", - "конечно", - "наверное", - "может", - "пусть", - "лучше", - "условии", - "случае", - "того", - "ради", - "раз", - "покуда", - "пока", - "после", - "перед", - "лишь", - "едва", - "чуть", - "дабы", - "кабы", - "коли", - "ежели", - "абы", - "вдруг", - "иначе", - "причём", - "причем", - "притом", - "зато", - "однако", - "тогда", - "сейчас", - "теперь", - "сначала", - "сперва", - "иногда", - "часто", - "всегда", - "никогда", - "т", - "др", -]; - -/// Prepositions: a relative pronoun or `что` after one takes the comma before the preposition -/// instead (`дом, в котором`, `то, за что`). -const PREPOSITIONS: &[&str] = &[ - "в", - "во", - "на", - "с", - "со", - "к", - "ко", - "о", - "об", - "обо", - "от", - "ото", - "до", - "из", - "изо", - "за", - "по", - "под", - "подо", - "над", - "надо", - "при", - "про", - "без", - "безо", - "для", - "у", - "через", - "чрез", - "между", - "меж", - "среди", - "из-за", - "из-под", - "около", - "возле", - "вокруг", - "мимо", - "против", - "вместо", - "кроме", - "сквозь", - "вдоль", - "внутри", - "вне", - "после", - "перед", - "пред", - "ради", - "благодаря", - "согласно", - "вопреки", - "навстречу", - "насчёт", - "насчет", - "ввиду", - "вследствие", - "посредством", - "путём", - "путем", - "относительно", - "касательно", - "вслед", - "ко", - "подле", - "близ", - "внутрь", - "поверх", -]; - -fn issue(start: usize, end: usize, message: String, suggestions: Vec) -> Issue { - Issue { start, end, kind: IssueKind::Grammar, message, suggestions, lang: ProofLang::Ru } -} - -fn capitalize(w: &str) -> String { - crate::affix::capitalize(w) -} - -/// Grammar issues in a paragraph of Russian text, in order. -pub fn check(text: &str) -> Vec { - check_with(text, true) -} - -/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital -/// (false for a list item, which may be a fragment). -pub fn check_with(text: &str, capital_first: bool) -> Vec { - let ws: Vec<(usize, usize)> = words(text); - let mut v = Vec::new(); - repeated_words(text, &ws, &mut v); - spaces(text, &mut v); - sentence_capitals(text, &ws, capital_first, &mut v); - missing_commas(text, &ws, &mut v); - doubled_punctuation(text, &mut v); - v.sort_by_key(|i| (i.start, i.end)); - v -} - -fn repeated_words(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for pair in ws.windows(2) { - let [(a0, b0), (a1, b1)] = pair else { continue }; - let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue }; - if !gap.is_empty() - && gap.chars().all(char::is_whitespace) - && w0.chars().all(char::is_alphabetic) - && w0.to_lowercase() == w1.to_lowercase() - && !ALLOWED_REPEATS.contains(&w0.to_lowercase().as_str()) - { - v.push(issue(*a0, *b1, format!("Повтор слова: «{w1}»"), vec![w0.to_string()])); - } - } -} - -fn spaces(text: &str, v: &mut Vec) { - for (i, c) in text.char_indices() { - if c != ' ' { - continue; - } - let rest = text.get(i + 1..).unwrap_or(""); - let mut it = rest.chars(); - let Some(p) = it.next() else { continue }; - let after = it.next(); - let flag = match p { - ',' => true, - // `;)` and `;-)` are emoticons. - ';' => !matches!(after, Some(')' | '(' | '-' | 'P' | 'D')), - // ` ...` is an ellipsis, ` .5` a number. - '.' => !matches!(after, Some('.')) && !after.is_some_and(|a| a.is_ascii_digit()), - // ` :)`, ` :-)`, ` :D` and similar are emoticons; ` ?!` is still a stray space. - ':' | '!' | '?' => after.is_none_or(|a| a.is_whitespace() || matches!(a, '"' | '»' | '”' | '’' | '!' | '?')), - _ => false, - }; - if flag { - v.push(issue(i, i + 1 + p.len_utf8(), "Лишний пробел перед знаком препинания".into(), vec![p.to_string()])); - } - } - for (i, _) in text.match_indices(" ") { - if i > 0 && text.get(..i).is_some_and(|t| t.ends_with(' ')) { - continue; - } - v.push(issue(i, i + 2, "Двойной пробел".into(), vec![" ".into()])); - } -} - -/// Is `prev`, before a period, an abbreviation, an initial, a number or a Roman numeral (after -/// which a lower-case word doesn't start a sentence)? -fn abbreviation_or_number(prev: &str) -> bool { - prev.chars().count() <= 1 - || prev.chars().any(|c| c.is_ascii_digit()) - || (prev.chars().all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))) - || ABBREVIATIONS.contains(&prev.to_lowercase().as_str()) -} - -const CLOSING: [char; 8] = ['"', '»', '”', '’', '\'', ')', ']', '\u{1}']; -const OPENING: [char; 8] = ['«', '"', '„', '“', '(', '[', '\'', '‚']; - -/// Does the gap between two words end a sentence: a `.`, `!` or `?` (not an ellipsis, not after -/// an abbreviation), then space, with nothing but quotes and brackets around? -fn ends_sentence(gap: &str, prev: &str) -> bool { - let t = gap.trim_start_matches(CLOSING); - let Some(end) = t.chars().next() else { return false }; - if !matches!(end, '.' | '!' | '?') || t.starts_with("..") { - return false; - } - if end == '.' && abbreviation_or_number(prev) { - return false; - } - let rest = t.trim_start_matches(['.', '!', '?']).trim_start_matches(CLOSING); - rest.starts_with(char::is_whitespace) && rest.trim_start().chars().all(|c| OPENING.contains(&c)) -} - -fn sentence_capitals(text: &str, ws: &[(usize, usize)], capital_first: bool, v: &mut Vec) { - // List items (`– яблоки,`) may start in lower case. - let list_item = !capital_first || text.trim_end().ends_with([',', ';']); - for (k, &(a, b)) in ws.iter().enumerate() { - let Some(w) = text.get(a..b) else { continue }; - let starts = if k == 0 { - let before = text.get(..a).unwrap_or(""); - !list_item && before.chars().all(|c| c.is_whitespace() || OPENING.contains(&c)) - } else { - let Some(&(pa, pb)) = ws.get(k - 1) else { continue }; - ends_sentence(text.get(pb..a).unwrap_or(""), text.get(pa..pb).unwrap_or("")) - }; - if starts && w.chars().next().is_some_and(char::is_lowercase) && w.chars().all(char::is_alphabetic) { - v.push(issue(a, b, "Предложение начинается со строчной буквы".into(), vec![capitalize(w)])); - } - } -} - -fn missing_commas(text: &str, ws: &[(usize, usize)], v: &mut Vec) { - for k in 1..ws.len() { - let (Some(&(pa, pb)), Some(&(a, b))) = (ws.get(k - 1), ws.get(k)) else { continue }; - let (Some(prev), Some(w), Some(gap)) = (text.get(pa..pb), text.get(a..b), text.get(pb..a)) else { continue }; - if gap.is_empty() || !gap.chars().all(|c| c == ' ' || c == '\u{a0}') || !prev.chars().all(char::is_alphabetic) { - continue; - } - let lw = w.to_lowercase(); - let next = ws.get(k + 1).and_then(|&(na, nb)| text.get(na..nb)).map(str::to_lowercase); - let next_gap_plain = ws.get(k + 1).and_then(|&(na, _)| text.get(b..na)).is_some_and(|g| g.chars().all(|c| c == ' ' || c == '\u{a0}')); - // `потому что`, `оттого что`: the comma goes before the first word. - let compound = matches!(lw.as_str(), "потому" | "оттого") && next.as_deref() == Some("что") && next_gap_plain; - if !compound && !CLAUSE_OPENERS.contains(&lw.as_str()) { - continue; - } - // `хотя бы` is a particle (`хотя бы раз`). - if lw == "хотя" && next.as_deref() == Some("бы") { - continue; - } - let p = prev.to_lowercase(); - let p = p.as_str(); - if NO_COMMA_AFTER.contains(&p) || PREPOSITIONS.contains(&p) { - continue; - } - v.push(issue(pb, b, format!("Возможно, пропущена запятая перед «{w}»"), vec![format!(", {w}")])); - } -} - -fn doubled_punctuation(text: &str, v: &mut Vec) { - let bytes = text.as_bytes(); - let mut i = 0; - while let Some(&b) = bytes.get(i) { - if !matches!(b, b',' | b'.' | b';') { - i += 1; - continue; - } - let run = bytes.get(i..).map_or(0, |r| r.iter().take_while(|&&x| x == b).count()); - let flag = match b { - b'.' => run == 2, - _ => run >= 2, - }; - if flag { - let end = i + run; - let suggestions: Vec = match b { - // `,,` before a word, after a space: a typed opening quote. - b',' if text.get(end..).and_then(|t| t.chars().next()).is_some_and(char::is_alphabetic) - && (i == 0 || text.get(..i).is_some_and(|t| t.ends_with(char::is_whitespace))) => - { - vec!["«".into()] - } - b'.' => vec![".".into(), "…".into()], - _ => vec![char::from(b).to_string()], - }; - v.push(issue(i, end, "Повтор знака препинания".into(), suggestions)); - } - i += run.max(1); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn messages(t: &str) -> Vec { - check(t).into_iter().map(|i| format!("{}@{}", i.message, &t[i.start..i.end])).collect() - } - - fn clean(t: &str) { - let m = messages(t); - assert!(m.is_empty(), "{t:?}: {m:?}"); - } - - #[test] - fn repeated_words() { - let v = check("Я пошёл в в магазин и и купил хлеб."); - assert_eq!(v.len(), 2, "{v:?}"); - assert_eq!(v[0].message, "Повтор слова: «в»"); - assert_eq!(v[0].suggestions, ["в"]); - assert!(v.iter().all(|i| i.lang == ProofLang::Ru && i.kind == IssueKind::Grammar)); - clean("Да да, так так. Ха ха! Очень очень давно."); - } - - #[test] - fn spaces_and_punctuation() { - let m = messages("Мы пришли , а он ушёл . Правда ?"); - assert!(m.contains(&"Лишний пробел перед знаком препинания@ ,".to_string()), "{m:?}"); - assert!(m.contains(&"Лишний пробел перед знаком препинания@ .".to_string()), "{m:?}"); - assert!(m.contains(&"Лишний пробел перед знаком препинания@ ?".to_string()), "{m:?}"); - assert!(m.contains(&"Двойной пробел@ ".to_string()), "{m:?}"); - clean("Отлично :) Правда :-) Число .5 и многоточие ..."); - let m = messages("Он сказал ,,да» и ушёл.. Потом;; конец."); - assert_eq!(m.iter().filter(|x| x.starts_with("Повтор знака")).count(), 3, "{m:?}"); - assert_eq!(check("Он сказал ,,да».").iter().find(|i| i.message.starts_with("Повтор знака")).unwrap().suggestions, ["«"]); - clean("Ждал… и ждал... до утра."); - } - - #[test] - fn sentence_start_capitals() { - let m = messages("это первое предложение. а это второе! правда? да."); - assert_eq!(m.iter().filter(|x| x.starts_with("Предложение начинается")).count(), 4, "{m:?}"); - assert_eq!(check("мама мыла раму.")[0].suggestions, ["Мама"]); - // Abbreviations, initials, numbers, ellipses and dialogue dashes. - clean("Купили фрукты: яблоки, груши и т. д. и овощи."); - clean("В 1990 г. открылась фирма, т. е. наша компания."); - clean("Цена 500 руб. за штуку, адрес ул. Ленина, д. 5, кв. 3."); - clean("Встретил А. С. Пушкина и проф. Иванова."); - clean("Ждал... и наконец дождался."); - clean("– Правда? – спросил он. – Да, – ответила она."); - clean("Он сказал: «Вернусь». Потом ушёл."); - clean("яблоки,"); - } - - #[test] - fn missing_commas() { - let m = messages( - "Я знаю что он придёт. Это дом который построил Джек. Пришёл чтобы помочь. Ушёл потому что устал. Приходи если сможешь. Пошёл хотя устал.", - ); - for want in [ - "Возможно, пропущена запятая перед «что»@ что", - "Возможно, пропущена запятая перед «который»@ который", - "Возможно, пропущена запятая перед «чтобы»@ чтобы", - "Возможно, пропущена запятая перед «потому»@ потому", - "Возможно, пропущена запятая перед «если»@ если", - "Возможно, пропущена запятая перед «хотя»@ хотя", - ] { - assert!(m.contains(&want.to_string()), "{want}: {m:?}"); - } - assert_eq!(check("Знаю что да.")[0].suggestions, [", что"]); - // Conjunctions, particles, compound conjunctions and prepositions take no comma there. - clean("Я знаю, что он придёт, и что останется."); - clean("Он только что пришёл, так что всё в порядке."); - clean("Это дом, в котором я живу, и сад, через который иду."); - clean("Я сделаю это, даже если не успею, лишь бы ты был рад."); - clean("Пришёл для того чтобы помочь, а не чтобы мешать."); - clean("Ушёл, потому что устал, и не потому что обиделся."); - clean("Расскажи хотя бы что-нибудь, за что тебя хвалят."); - clean("Что ты делаешь? Который час?"); - clean("Кто-то что-то сказал."); - } - - #[test] - fn correct_russian_text_is_clean() { - clean("Съешь же ещё этих мягких французских булок, да выпей чаю. Мы пришли, когда уже стемнело."); - clean("Книга, которую я читаю, очень интересная, поэтому я её советую."); - } - - proptest::proptest! { - #[test] - fn never_panics(s in "\\PC{0,80}") { - let _ = check(&s); - } - - #[test] - fn never_panics_on_russian_like_text(s in "[а-яёА-ЯЁ ,.;:!?«»…\\-]{0,60}") { - for i in check(&s) { - proptest::prop_assert!(i.start <= i.end && i.end <= s.len()); - proptest::prop_assert!(s.is_char_boundary(i.start) && s.is_char_boundary(i.end)); - } - } - } -} diff --git a/crates/proof/src/ru/hyphen.rs b/crates/proof/src/ru/hyphen.rs deleted file mode 100644 index 33d19b771..000000000 --- a/crates/proof/src/ru/hyphen.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Russian word division, from the rules of Russian spelling (no pattern file): -//! -//! - every part keeps a vowel, and at least two letters stay on each side; -//! - `ь`, `ъ` and `й` stay with the letter before them (`паль-то`, `вой-на`), and a syllable -//! ends after `ъ` (`объ-ём`, `подъ-езд`); -//! - between two vowels with one consonant between them the consonant starts the next syllable -//! (`ма-ма`, `ро-ди-на`); -//! - a cluster of consonants is divided after its first consonant (`сес-тра`, `кар-ман`, -//! `кас-са`, `под-ход`), which also keeps the common prefixes `под-`, `раз-`, `без-` whole; -//! - two vowels side by side may be divided (`по-эт`, `ма-як`). - -use super::is_cyrillic; - -/// Russian vowels. -fn is_vowel(c: char) -> bool { - matches!(c, 'а' | 'е' | 'ё' | 'и' | 'о' | 'у' | 'ы' | 'э' | 'ю' | 'я') -} - -/// Letters that never begin a line: the soft and hard signs and `й`. -fn sticks_left(c: char) -> bool { - matches!(c, 'ь' | 'ъ' | 'й') -} - -/// Break points of a lowercase Russian word (letters only): each is the index of the letter that -/// starts a new line. Empty for words with letters outside the Russian alphabet. -pub fn points(c: &[char]) -> Vec { - let n = c.len(); - if n < 4 || !c.iter().all(|&x| is_cyrillic(x) && x.is_alphabetic()) { - return vec![]; - } - let vowels: Vec = (0..n).filter(|&i| c.get(i).is_some_and(|&x| is_vowel(x))).collect(); - let mut out = vec![]; - for pair in vowels.windows(2) { - let [a, b] = pair else { continue }; - let (start, end) = (a + 1, *b); - // The letters that must stay on the left: signs and `й` right after the vowel or after a - // consonant of the cluster. - let mut cut = if start == end { - // Two vowels side by side. - end - } else if c.get(end - 1) == Some(&'ъ') { - // The hard sign closes its syllable (`объ-ём`, `подъ-езд`). - end - } else { - // The cluster's consonants, skipping signs that stick to the letter before. - let consonants: Vec = (start..end).filter(|&i| c.get(i).is_some_and(|&x| !sticks_left(x))).collect(); - match consonants.as_slice() { - // Only `й` (`май-онез`): after it. - [] => end, - // One consonant: it starts the next syllable (`ма-ма`, `паль-то`). - [only] => *only, - // Several: after the first one (`сес-тра`, `кар-ман`). - [_, second, ..] => *second, - } - }; - // A sign or `й` never starts a line. - while c.get(cut).is_some_and(|&x| sticks_left(x)) && cut < end { - cut += 1; - } - // Cuts come in increasing order (each lies after the previous pair's vowel), so a repeat - // can only be the last one: no scan of the whole list per syllable (#3657). - if cut >= 2 && n - cut >= 2 && out.last().is_none_or(|&l| l < cut) { - out.push(cut); - } - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn h(w: &str) -> String { - let c: Vec = w.chars().collect(); - let p = points(&c); - c.iter().enumerate().map(|(i, ch)| if p.contains(&i) { format!("-{ch}") } else { ch.to_string() }).collect() - } - - #[test] - fn russian_words_divide_by_the_rules() { - assert_eq!(h("мама"), "ма-ма"); - assert_eq!(h("родина"), "ро-ди-на"); - assert_eq!(h("сестра"), "сес-тра"); - assert_eq!(h("карман"), "кар-ман"); - assert_eq!(h("касса"), "кас-са"); - assert_eq!(h("подход"), "под-ход"); - assert_eq!(h("пальто"), "паль-то"); - assert_eq!(h("объём"), "объ-ём"); - assert_eq!(h("подъезд"), "подъ-езд"); - assert_eq!(h("семья"), "се-мья"); - assert_eq!(h("вьюга"), "вью-га"); - assert_eq!(h("объявление"), "объ-яв-ле-ние"); - assert_eq!(h("война"), "вой-на"); - assert_eq!(h("чайник"), "чай-ник"); - assert_eq!(h("поэзия"), "по-э-зия"); - assert_eq!(h("маяк"), "ма-як"); - assert_eq!(h("жидкие"), "жид-кие"); - assert_eq!(h("обои"), "обои"); - assert_eq!(h("декоративная"), "де-ко-ра-тив-ная"); - assert_eq!(h("штукатурка"), "шту-ка-тур-ка"); - } - - #[test] - fn short_words_and_other_scripts_stay_whole() { - for w in ["дом", "ёж", "оса", "iPhone", "дом2", ""] { - let c: Vec = w.chars().collect(); - assert!(points(&c).is_empty(), "{w}"); - } - } - - #[test] - fn no_line_starts_with_a_sign_or_ends_with_one_letter() { - for w in ["объявление", "подъезд", "семья", "вьюга", "майонез", "яйцо", "ауди", "аорта", "оазис", "ёлочка"] - { - let c: Vec = w.chars().collect(); - for p in points(&c) { - assert!(p >= 2 && c.len() - p >= 2, "{w}: {p}"); - assert!(c.get(p).is_some_and(|&x| !sticks_left(x)), "{w}: {p}"); - assert!(c.get(..p).is_some_and(|l| l.iter().any(|&x| is_vowel(x))), "{w}: {p}"); - assert!(c.get(p..).is_some_and(|r| r.iter().any(|&x| is_vowel(x))), "{w}: {p}"); - } - } - } - - proptest::proptest! { - #[test] - fn never_panics(s in "[а-яё]{0,30}") { - let c: Vec = s.chars().collect(); - for p in points(&c) { - proptest::prop_assert!(p >= 2 && p + 2 <= c.len()); - } - } - } -} diff --git a/crates/proof/src/ru/mod.rs b/crates/proof/src/ru/mod.rs deleted file mode 100644 index 67be70858..000000000 --- a/crates/proof/src/ru/mod.rs +++ /dev/null @@ -1,485 +0,0 @@ -//! Russian proofing. -//! -//! - **Spelling** against a Russian Hunspell dictionary installed on the system ([`dictionary`], -//! found by [`crate::hunspell`]: `ru_RU.aff`/`ru_RU.dic` from the OS, a package manager or -//! LibreOffice's `dict-ru`; it isn't bundled). Without it Russian text gets no spelling marks. -//! `ё` is optional, as in ordinary Russian writing: `елка` and `ёлка`, `все` and `всё` are -//! accepted whichever one the dictionary lists. Short words in capitals are abbreviations -//! (`ООО`, `НДС`). Hyphenated words are checked whole first, then by their parts, knowing the -//! particles (`кто-то`, `где-нибудь`, `давай-ка`, `всё-таки`), `кое-`/`кой-`, adverbs with -//! `по-` (`по-русски`, `по-новому`) and compound adjectives whose first part ends in a linking -//! vowel (`северо-западный`, `сине-зелёный`). Compounds written solid are checked the same way -//! (`высокоэластичный`, `газоиспользующий`), and so are adjectives, participles and verbal nouns -//! with `не` (`невскрытый`, `непредоставление`; not verbs: `незнаю` stays wrong). Our supplement -//! adds words older dictionaries lack (`маркетплейс`, `промокод`, `интеграция`: [`extra`]). -//! Words in another script (`iPhone`), with digits or superscripts (`м²`), in camel case -//! (`ВКонтакте`) or abbreviations with an ending (`ГИПа`, `ПДн`) are left alone. -//! - **Suggestions** tuned for Russian ([`suggest()`]): `е`/`ё`, the common confusions (`тся`/ -//! `ться`, `жи`/`ши` written with `ы`, `ча`/`ща` with `я`, voiced and voiceless consonant pairs, -//! doubled consonants), a missing space (`незнаю` → `не знаю`, `вобщем` → `в общем`), one-letter -//! typos, and a dictionary scan for anything further off. -//! - **Grammar** ([`grammar`]): cheap, reliable rules with Russian messages. -//! - **Hyphenation** ([`hyphen`]): the rules of Russian word division, written as code. - -mod extra; -pub mod grammar; -pub mod hyphen; -mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::affix::AffixDict; -use crate::slot::DictSlot; - -pub use suggest::suggest; - -/// File names of a Russian Hunspell dictionary. -const NAMES: &[&str] = &["ru_RU", "ru-RU", "ru"]; - -/// The Russian dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Russian"); - -/// The Russian dictionary: the installed Hunspell `ru_RU` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available (Russian spelling is off -/// then; see [`has_dictionary`]). -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Use this Russian dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to -/// UTF-8), for example one the user picked. On error the current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is a Russian dictionary available (spelling is checked)? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Is `c` a letter of the Cyrillic script (the only one the Russian dictionary covers)? -pub(crate) fn is_cyrillic(c: char) -> bool { - matches!(c, '\u{0400}'..='\u{052F}') -} - -/// Particles joined to a word with a hyphen (`кто-то`, `где-нибудь`, `давай-ка`, `всё-таки`, -/// `он-де`). -const PARTICLES: &[&str] = &["то", "либо", "нибудь", "ка", "таки", "де", "с"]; - -/// Prefixes joined to a pronoun with a hyphen (`кое-кто`, `кое-где`, `кой-какой`). -const PRONOUN_PREFIXES: &[&str] = &["кое", "кой"]; - -/// Words of fewer letters than this written in capitals are taken as abbreviations (`ООО`, `НДС`, -/// `МГУ`, `ГОСТ`). -const ABBREVIATION_LETTERS: usize = 6; - -/// Is `word` spelled correctly in Russian? Numbers, single letters, short words in capitals, -/// words with digits and words in other scripts are accepted, and so is every word when no -/// Russian dictionary is installed. -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words in UPPERCASE off, -/// short words in capitals are checked against the dictionary too, and with Ignore words that -/// contain numbers off, so are words with digits or superscripts. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary (an empty one accepts everything). -pub fn is_correct_with(d: &AffixDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &AffixDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // no Russian dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '-'); - let w = w.trim_matches('-'); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(char::is_numeric)) || !w.chars().any(char::is_alphabetic) { - return true; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= ABBREVIATION_LETTERS { - return true; - } - // Camel case (`ВКонтакте`, `МирКрасок`): a brand or an identifier, not a dictionary word. - let cs: Vec = w.chars().collect(); - if cs.windows(2).any(|p| matches!(p, [a, b] if a.is_lowercase() && b.is_uppercase())) { - return true; - } - // An abbreviation with lower-case letters or an ending (`ГИПа`, `ПДн`, `ИСПДн`). - if n <= 10 && cs.iter().take(2).all(|c| c.is_uppercase()) && cs.iter().any(|c| c.is_lowercase()) { - return true; - } - // Another script, or a mix of scripts (`iPhone`, `SMS-ка`): not this dictionary's to judge. - if w.chars().any(|c| c.is_alphabetic() && !is_cyrillic(c)) { - return true; - } - if known(d, w) { - return true; - } - if w.contains('-') { - return hyphenated(d, w, opts); - } - solid_compound(d, w) -} - -/// Endings of adjectives, participles and nouns that are written together with `не` -/// (`невскрытый`, `неисполнение`, `неготовность`). Verb endings aren't among them: `не` with a -/// verb is written apart (`не знаю`). -const NE_ENDINGS: &[&str] = &[ - "ый", - "ий", - "ой", - "ая", - "яя", - "ое", - "ее", - "ые", - "ие", - "ого", - "его", - "ому", - "ему", - "ым", - "ую", - "юю", - "ых", - "их", - "ыми", - "ими", - "ние", - "ния", - "нию", - "нием", - "нии", - "ость", - "ости", - "остью", - "остей", - "остям", - "остями", - "остях", -]; - -/// Prefixes Russian joins to any word (`ультрастойкий`, `супермаркет`, `экопакет`). -const WORD_PREFIXES: &[&str] = &[ - "ультра", - "супер", - "гипер", - "мега", - "микро", - "макро", - "мульти", - "мини", - "макси", - "анти", - "псевдо", - "квази", - "контр", - "сверх", - "полу", - "само", - "авто", - "видео", - "аудио", - "фото", - "радио", - "теле", - "эко", - "био", - "нано", - "агро", - "медиа", - "интернет", - "веб", - "бизнес", - "онлайн", - "спец", - "гос", - "мед", - "юр", - "орг", - "проф", - "соц", -]; - -/// Prefixes that make a verb perfective (`с-генерировать`, `от-модерировать`, `пере-загрузить`). -const VERB_PREFIXES: &[&str] = &[ - "с", "со", "за", "по", "про", "на", "вы", "от", "ото", "пере", "под", "подо", "при", "раз", "рас", "из", "ис", "у", "о", "об", "обо", "до", - "недо", "пред", "в", "во", "вз", "вс", -]; - -/// Endings of verb forms (infinitive, present, past, reflexive). -const VERB_ENDINGS: &[&str] = &[ - "ть", "ться", "ти", "чь", "ю", "ешь", "ет", "ем", "ете", "ют", "ишь", "ит", "им", "ите", "ят", "ал", "ала", "ало", "али", "ил", "ила", "ило", - "или", "ся", "сь", -]; - -/// A word written solid from known parts: `не` with an adjective, participle or verbal noun -/// (`невскрытой`), or two stems joined by a linking vowel (`высокоэластичная`, `газоиспользующего`). -fn solid_compound(d: &AffixDict, w: &str) -> bool { - let lower = w.to_lowercase(); - let chars: Vec = lower.chars().collect(); - let n = chars.len(); - if !(7..=40).contains(&n) { - return false; - } - if let Some(rest) = lower.strip_prefix("не") - && rest.chars().count() >= 4 - && NE_ENDINGS.iter().any(|e| rest.ends_with(e)) - && known(d, rest) - { - return true; - } - // A productive prefix (`ультрастойкий`, `экопакет`, `микроцемент`) on a known word. - if WORD_PREFIXES.iter().any(|p| lower.strip_prefix(p).is_some_and(|rest| rest.chars().count() >= 4 && known(d, rest))) { - return true; - } - // A verb with a perfective prefix (`сгенерировать`, `отмодерировали`) whose unprefixed form is - // known. - if VERB_PREFIXES.iter().any(|p| { - lower.strip_prefix(p).is_some_and(|rest| rest.chars().count() >= 5 && VERB_ENDINGS.iter().any(|e| rest.ends_with(e)) && known(d, rest)) - }) { - return true; - } - (4..=n.saturating_sub(4)).any(|i| { - let (Some(left), Some(right)) = (chars.get(..i), chars.get(i..)) else { return false }; - if !matches!(left.last(), Some('о' | 'е')) { - return false; - } - let left: String = left.iter().collect(); - let right: String = right.iter().collect(); - known(d, &right) && (known(d, &left) || linking_stem(d, &left)) - }) -} - -/// Is `part`, ending in a linking vowel, a known word's stem (`северо` → `север`, `сине` → -/// `синий`, `газо` → `газ`)? -fn linking_stem(d: &AffixDict, part: &str) -> bool { - let Some(stem) = part.strip_suffix(['о', 'е', 'О', 'Е']) else { return false }; - stem.chars().count() >= 2 && ["", "ый", "ий", "ой", "ь"].iter().any(|end| known(d, &format!("{stem}{end}"))) -} - -/// Is one word in the dictionary or the user dictionary, with `ё` optional? -fn known(d: &AffixDict, w: &str) -> bool { - if d.check(w) || crate::in_user_dictionary(&w.to_lowercase()) || extra::contains(w) { - return true; - } - // `ё` written as `е` is ordinary Russian: try the dictionary's `ё` spelling… - let without = w.replace('ё', "е").replace('Ё', "Е"); - if without != w && (d.check(&without) || crate::in_user_dictionary(&without.to_lowercase())) { - return true; - } - // …and `е` where the dictionary has `ё` (one or two of them: `Черемушки`, `еще`). - let e_positions: Vec = without.char_indices().filter(|(_, c)| matches!(c, 'е' | 'Е')).map(|(i, _)| i).collect(); - if e_positions.is_empty() || e_positions.len() > 4 { - return false; - } - let swap = |s: &str, i: usize| -> Option { - let c = s.get(i..)?.chars().next()?; - let yo = if c == 'Е' { "Ё" } else { "ё" }; - Some(format!("{}{}{}", s.get(..i)?, yo, s.get(i + c.len_utf8()..)?)) - }; - for (k, &i) in e_positions.iter().enumerate() { - let Some(one) = swap(&without, i) else { continue }; - if d.check(&one) { - return true; - } - for &j in e_positions.get(k + 1..).unwrap_or_default() { - // `ё` is two bytes like `е`, so later offsets still hold. - if swap(&one, j).is_some_and(|two| d.check(&two)) { - return true; - } - } - } - false -} - -/// A hyphenated word that isn't in the dictionary whole: its parts, with the particles, -/// prefixes and linking vowels Russian joins with a hyphen. -fn hyphenated(d: &AffixDict, w: &str, opts: &crate::Options) -> bool { - let parts: Vec<&str> = w.split('-').collect(); - if parts.iter().any(|p| p.is_empty()) || parts.len() > 4 { - return false; - } - let lower = w.to_lowercase(); - let lp: Vec<&str> = lower.split('-').collect(); - // `кто-то`, `где-нибудь`, `всё-таки`: a word and its particles. - let first_particle = lp.iter().skip(1).position(|p| PARTICLES.contains(p)).map(|i| i + 1); - if let Some(k) = first_particle - && lp.iter().skip(k).all(|p| PARTICLES.contains(p)) - { - let base = parts.get(..k).map(|b| b.join("-")).unwrap_or_default(); - return known(d, &base) || (base.contains('-') && hyphenated(d, &base, opts)); - } - match lp.as_slice() { - // `кое-кто`, `кой-какой`. - [prefix, rest] if PRONOUN_PREFIXES.contains(prefix) => return parts.get(1).is_some_and(|r| known(d, r)), - // `по-русски` (from `русский`), `по-новому`, `по-моему`. - ["по", rest] => { - let Some(r) = parts.get(1) else { return false }; - return known(d, r) || ((rest.ends_with("ски") || rest.ends_with("цки") || rest.ends_with("ьи")) && known(d, &format!("{r}й"))); - } - _ => {} - } - // Compounds part by part: `северо-западный`, `сине-зелёный`, `интернет-магазин`, `из-за`. - let last = parts.len() - 1; - parts.iter().enumerate().all(|(i, p)| { - // An abbreviation part (`ИИ-консультант`, `SMS-рассылка`) or a known word. - if known(d, p) || (opts.ignore_uppercase && p.chars().all(|c| !c.is_lowercase()) && p.chars().count() <= ABBREVIATION_LETTERS) { - return true; - } - // A first part with a linking vowel: the stem (`северо` → `север`) or its adjective - // (`сине` → `синий`, `бело` → `белый`). - i != last && linking_stem(d, p) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_dictionary_loads() { - test_dict::install(); - let d = dictionary(); - assert!(has_dictionary()); - assert!(d.len() > 50, "{}", d.len()); - } - - #[test] - fn without_a_dictionary_nothing_is_flagged_and_nothing_suggested() { - let none = AffixDict::default(); - for w in ["превет", "сдесь", "xqzv", ""] { - assert!(is_correct_with(&none, w), "{w}"); - } - assert!(suggest::suggest_in(&none, "превет", 5).is_empty()); - } - - #[test] - fn russian_words_are_correct() { - test_dict::install(); - for w in [ - "привет", - "Привет", - "ПРИВЕТ", - "дом", - "дома", - "домами", - "красный", - "красного", - "красной", - "читать", - "читаем", - "читаются", - "ёлка", - "елка", - "всё", - "все", - "ещё", - "еще", - "Москва", - "Москве", - "кто-то", - "где-нибудь", - "что-либо", - "давай-ка", - "всё-таки", - "все-таки", - "кое-кто", - "по-русски", - "по-новому", - "северо-западный", - "сине-зелёный", - "сине-зеленый", - "из-за", - "ООО", - "НДС", - "2026", - "5-й", - "iPhone", - "e-mail", - "в", - "м²", - "ВКонтакте", - "МирКрасок", - "ГИПа", - "ПДн", - "ИИ-консультант", - "маркетплейсов", - "промокода", - "невскрытой", - "некрасного", - "непредоставления", - "высокоэластичная", - "краснозелёный", - "Пн-Пт", - "ультракрасный", - "суперновый", - "прочитать", - "администрируется", - "юрлица", - ] { - assert!(is_correct(w), "{w}"); - } - } - - #[test] - fn misspellings_are_flagged() { - test_dict::install(); - for w in [ - "превет", - "сдесь", - "жыть", - "чясто", - "незнаю", - "нечитаю", - "вобщем", - "москва", - "здраствуй", - "кто-нибуть", - "по-руски", - "северо-зопадный", - "ПРИВЕТТТ", - "высокозопадный", - ] { - assert!(!is_correct(w), "{w}"); - } - } - - #[test] - fn user_dictionary_applies() { - test_dict::install(); - assert!(!is_correct("зкщфрусскоеслово")); - crate::add_word("Зкщфрусскоеслово"); - assert!(is_correct("зкщфрусскоеслово")); - } - - /// With Ignore words in UPPERCASE or Ignore words that contain numbers off, Russian checks - /// those words against the dictionary instead of accepting them. - #[test] - fn proofing_options_reach_the_russian_check() { - test_dict::install(); - let lang = crate::ProofLang::Ru; - let defaults = crate::Options::default(); - let check_caps = crate::Options { ignore_uppercase: false, ..defaults }; - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("ПРЕВЕТ", lang, &defaults), "short capitals are abbreviations by default"); - assert!(!crate::is_correct_in_with("ПРЕВЕТ", lang, &check_caps)); - assert!(crate::is_correct_in_with("ПРИВЕТ", lang, &check_caps)); - assert!(crate::is_correct_in_with("ИИ-консультант", lang, &defaults)); - assert!(!crate::is_correct_in_with("ИИ-консультант", lang, &check_caps), "a part in capitals is checked too"); - assert!(crate::is_correct_in_with("превет2", lang, &defaults)); - assert!(!crate::is_correct_in_with("превет2", lang, &check_digits)); - assert!(!crate::is_correct_in_with("м²", lang, &check_digits), "a superscript is a number too"); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("ПРЕВЕТ"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/ru/suggest.rs b/crates/proof/src/ru/suggest.rs deleted file mode 100644 index 135f4152f..000000000 --- a/crates/proof/src/ru/suggest.rs +++ /dev/null @@ -1,258 +0,0 @@ -//! Russian spelling suggestions, best first: the shared ranking ([`crate::affix_suggest`]) with -//! Russian's own letter groups and confusions, since Russian Hunspell dictionaries carry no `MAP` -//! or `REP` tables (`е`/`ё` first, then unstressed vowels, voiced and voiceless pairs, `жы` → -//! `жи`, `тся`/`ться`, dropped or doubled consonants; up to three letters changed), and its rule -//! for a missing space (`незнаю` → `не знаю`, `вобщем` → `в общем`). - -use crate::affix::AffixDict; -use crate::affix_suggest::{COST_OTHER_MAP, COST_SPLIT, COST_SPLIT_LONG, Split, Tuning}; - -/// Letters confused with each other: a slip inside a group is cheaper than another typo. -const MAP: &[&str] = &["её", "оа", "еия", "зс", "дт", "бп", "вф", "гк", "жш", "ьъ", "еэ", "ыи"]; -/// Common confusions: a written sequence and what was meant. -const REP: &[(&str, &str)] = &[ - ("жы", "жи"), - ("шы", "ши"), - ("чя", "ча"), - ("щя", "ща"), - ("чю", "чу"), - ("щю", "щу"), - ("тся", "ться"), - ("ться", "тся"), - ("сч", "щ"), - ("щ", "сч"), - ("нн", "н"), - ("н", "нн"), - ("сс", "с"), - ("с", "сс"), - ("лл", "л"), - ("л", "лл"), - ("тт", "т"), - ("т", "тт"), - ("мм", "м"), - ("м", "мм"), - ("рр", "р"), - ("р", "рр"), - ("ье", "ъе"), - ("ъе", "ье"), - ("раст", "рост"), - ("рост", "раст"), - ("лож", "лаг"), - ("лаг", "лож"), - // Unpronounced consonants: `здраствуй`, `чуствовать`, `солнце`, `лесница`, `праздник`. - ("аст", "авст"), - ("уст", "увст"), - ("лнц", "лц"), - ("лц", "лнц"), - ("сн", "стн"), - ("стн", "сн"), - ("зн", "здн"), - ("здн", "зн"), - ("ц", "тс"), - ("тс", "ц"), -]; -/// Short words often run into the next one (`не знаю`, `в общем`, `на верху`, `потому что`). -const LEADING_WORDS: &[&str] = &[ - "не", - "ни", - "в", - "во", - "на", - "с", - "со", - "к", - "ко", - "о", - "об", - "обо", - "от", - "ото", - "до", - "из", - "изо", - "за", - "по", - "под", - "над", - "при", - "про", - "без", - "для", - "у", - "а", - "и", - "но", - "да", - "же", - "как", - "так", - "то", - "что", - "уже", - "еще", - "ещё", - "все", - "всё", - "потому", - "чтобы", - "это", -]; -/// Prepositions among [`LEADING_WORDS`]. -const PREPOSITIONS: &[&str] = &[ - "в", "во", "на", "с", "со", "к", "ко", "о", "об", "обо", "от", "ото", "до", "из", "изо", "за", "по", "под", "над", "при", "про", "без", "для", - "у", -]; -/// Short words often run into the one before (`мог бы`, `он же`, `так ли`). -const TRAILING_WORDS: &[&str] = &["ли", "же", "бы", "ж", "б", "что"]; -/// The Russian alphabet, when the dictionary has no `TRY` line. -const ALPHABET: &str = "оеаинтсрвлкмдпуяыьгзбчйхжшюцщэфъё"; - -/// `е` written for `ё`, or the other way round, is barely a slip. -const COST_YO: u32 = 3; -/// A one- or two-letter preposition run into the next word (`вдоме` → `в доме`). -const COST_SPLIT_PREPOSITION: u32 = 17; - -/// What Russian adds to its dictionary's tables. -const TUNING: Tuning = - Tuning { split: Split::Custom(split), alphabet: ALPHABET, map: MAP, rep: REP, rep_cost: 9, max_map_changes: 3, map_cost, ..Tuning::DEFAULT }; - -/// The cost of changing `from` to `to` within a letter group: `е`/`ё` cheapest. -fn map_cost(from: char, to: char) -> u32 { - if matches!((from, to), ('е', 'ё') | ('ё', 'е')) { COST_YO } else { COST_OTHER_MAP } -} - -/// A missing space: cheap after a short leading word or before a short trailing one, dearer after -/// a short preposition or between two longer words, never leaving a stray short half. -fn split(a: &str, b: &str) -> Option { - let long = |p: &str| p.chars().count() >= 3; - // A preposition takes a noun, not a verb (`с генерировать` isn't a fix for - // `сгенерировать`); after one or two letters a split is likelier wrong than a typo. - let preposition = PREPOSITIONS.contains(&a); - if preposition && ["ть", "ться", "ти", "чь"].iter().any(|e| b.ends_with(e)) { - return None; - } - if preposition && a.chars().count() <= 2 && LEADING_WORDS.contains(&a) && long(b) { - Some(COST_SPLIT_PREPOSITION) - } else if (LEADING_WORDS.contains(&a) && long(b)) || (long(a) && TRAILING_WORDS.contains(&b)) { - Some(COST_SPLIT) - } else if long(a) && long(b) { - Some(COST_SPLIT_LONG) - } else { - None - } -} - -/// Spelling suggestions for a Russian word, best first, at most `max`, in the word's letter case. -pub fn suggest(word: &str, max: usize) -> Vec { - suggest_in(&super::dictionary(), word, max) -} - -/// [`suggest`] against a given dictionary. -pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { - crate::affix_suggest::suggest_in(d, word, max, &TUNING) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn has(word: &str, want: &str) { - let s = suggest(word, 8); - assert!(s.iter().any(|x| x == want), "{word} → {s:?}, want {want}"); - } - - /// Russian's own costs: `е`/`ё` is the cheapest slip, so a `ё` and one more group slip - /// (made-up `нёснаю`) beat a missing space; a REP confusion beats a `ё` and a group slip; - /// a preposition is never split off a verb, and after a one-letter one a typo is likelier. - #[test] - fn russian_costs_rank_its_slips() { - let dict = |words: &[&str]| AffixDict::from_hunspell("", &format!("{}\n{}\n", words.len(), words.join("\n")), &[]).unwrap(); - assert_eq!(suggest_in(&dict(&["не", "знаю", "нёснаю"]), "незнаю", 1), ["нёснаю"]); - assert_eq!(suggest_in(&dict(&["еча", "ёчи"]), "ечя", 1), ["еча"]); - assert!(!suggest_in(&dict(&["с", "генерировать"]), "сгенерировать", 8).contains(&"с генерировать".into())); - let s = suggest_in(&dict(&["в", "доме"]), "вдоме", 4); - assert_eq!(s.first().map(String::as_str), Some("доме"), "{s:?}"); - assert!(s.contains(&"в доме".into()), "{s:?}"); - } - - /// A word the dictionary marks NOSUGGEST (a vulgarism, a rare form) is correct but never - /// offered, in any round or letter case. Russian's MAP and REP tables are its own, so the - /// shared Latin-script check doesn't reach it. - #[test] - fn nosuggest_words_are_never_offered() { - let offered = |aff: &str, input: &str| { - let d = AffixDict::from_hunspell(aff, "2\nпривет/N\nпредмет/N\n", &[]).unwrap(); - suggest_in(&d, input, 16) - }; - assert!(offered("SET UTF-8\n", "превет").contains(&"привет".into()), "offered without the flag declared"); - for input in ["превет", "ПРЕВЕТ", "предмот", "приветт", "привте", "привет"] { - let s = offered("SET UTF-8\nNOSUGGEST N\n", input); - assert!(!s.iter().any(|w| w.to_lowercase().contains("привет") || w.to_lowercase().contains("предмет")), "{input}: {s:?}"); - } - } - - #[test] - fn russian_misspellings_get_the_right_suggestion() { - crate::ru::test_dict::install(); - has("превет", "привет"); - has("сдесь", "здесь"); - has("жыть", "жить"); - has("чясто", "часто"); - has("незнаю", "не знаю"); - has("вобщем", "в общем"); - has("здраствуй", "здравствуй"); - has("москва", "Москва"); - has("Превет", "Привет"); - has("ПРЕВЕТ", "ПРИВЕТ"); - has("читаеться", "читается"); - has("краснного", "красного"); - has("западнный", "западный"); - assert!(!suggest("сгенерировать", 8).contains(&"с генерировать".to_string())); - } - - #[test] - fn the_likeliest_fix_comes_first() { - crate::ru::test_dict::install(); - assert_eq!(suggest("превет", 3).first().map(String::as_str), Some("привет")); - assert_eq!(suggest("незнаю", 3).first().map(String::as_str), Some("не знаю")); - assert_eq!(suggest("жыть", 3).first().map(String::as_str), Some("жить")); - assert_eq!(suggest("зеленый", 3).first().map(String::as_str), Some("зелёный")); - } - - #[test] - fn edge_cases_stay_empty_or_bounded() { - crate::ru::test_dict::install(); - assert!(suggest("", 5).is_empty()); - assert!(suggest("дом", 0).is_empty()); - assert!(suggest(&"а".repeat(41), 5).is_empty()); - assert!(suggest("два слова", 5).is_empty()); - assert!(suggest("привет", 5).len() <= 5); - assert!(!suggest("привет", 5).contains(&"привет".to_string())); - } - - #[test] - fn distance_weighs_letter_groups_half() { - use crate::affix_suggest::{fold, weighted_distance}; - let map: Vec> = MAP.iter().map(|g| g.chars().collect()).collect(); - let related = |a: char, b: char| fold(a, &map) == fold(b, &map); - let c = |s: &str| s.chars().collect::>(); - assert_eq!(weighted_distance(&c("сдесь"), &c("здесь"), &related, 9), 1); - assert_eq!(weighted_distance(&c("ёлка"), &c("елка"), &related, 9), 1); - assert_eq!(weighted_distance(&c("дом"), &c("дмо"), &related, 9), 2); - assert_eq!(weighted_distance(&c("дом"), &c("домами"), &related, 9), 6); - assert_eq!(weighted_distance(&c("дом"), &c("книга"), &related, 2), 3); - } - - #[test] - fn suggestions_are_fast_enough() { - crate::ru::test_dict::install(); - let _ = suggest("дом", 1); - let t = std::time::Instant::now(); - for w in ["ыкщзвотрусскогословаааа", "здраствуйте", "краснннного"] { - let _ = suggest(w, 6); - } - let ms = t.elapsed().as_secs_f64() * 1000.0; - eprintln!("3 suggestion calls: {ms:.0} ms"); - assert!(ms < 20_000.0, "{ms} ms"); - } -} diff --git a/crates/proof/src/ru/test_dict.rs b/crates/proof/src/ru/test_dict.rs deleted file mode 100644 index 947f35520..000000000 --- a/crates/proof/src/ru/test_dict.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! A tiny synthetic Russian Hunspell dictionary for tests (written for these tests; not taken from -//! any real dictionary). Real dictionaries are installed on the system, never bundled. - -/// Affix rules: the endings of a hard masculine noun, a feminine noun in `-а`, hard and soft -/// adjectives and a verb in `-ать`, plus the letters suggestions try. -pub const AFF: &str = "SET UTF-8 -TRY оеаинтсрвлкмдпуяыьгзбчйхжшюцщэфъё -SFX A Y 9 -SFX A 0 а . -SFX A 0 у . -SFX A 0 ом . -SFX A 0 е . -SFX A 0 ы . -SFX A 0 ов . -SFX A 0 ам . -SFX A 0 ами . -SFX A 0 ах . -SFX D Y 5 -SFX D а ы а -SFX D а е а -SFX D а у а -SFX D а ой а -SFX D а ою а -SFX B Y 11 -SFX B ый ого ый -SFX B ый ому ый -SFX B ый ым ый -SFX B ый ом ый -SFX B ый ая ый -SFX B ый ой ый -SFX B ый ую ый -SFX B ый ое ый -SFX B ый ые ый -SFX B ый ых ый -SFX B ый ыми ый -SFX E Y 8 -SFX E ий его ий -SFX E ий ему ий -SFX E ий им ий -SFX E ий ей ий -SFX E ий яя ий -SFX E ий ее ий -SFX E ий ие ий -SFX E ий их ий -SFX C Y 11 -SFX C ть ю ть -SFX C ть ешь ть -SFX C ть ет ть -SFX C ть ем ть -SFX C ть ете ть -SFX C ть ют ть -SFX C ть ются ть -SFX C ть ется ть -SFX C ть л ть -SFX C ть ла ть -SFX C ть ться ть -"; - -/// Stems (the count line first, as in Hunspell files). -pub const DIC: &str = "62 -привет -здесь -здравствуй -жить -часто -знаю -знать/C -не -в -общем -общий/E -дом/A -стол/A -город/A -Москва/D -книга/D -красный/B -новый/B -белый/B -западный/B -синий/E -зелёный/B -русский -английский -читать/C -работать/C -кто -где -что -давай -всё -все -ещё -ёлка/D -север/A -из -за -на -и -а -но -или -он -она -я -ты -мы -вы -это -так -как -когда -который/B -чтобы -потому -если -пришёл -сказал -вскрытый/B -высоко -эластичный/B -консультант/A -"; - -/// Install the test dictionary as the Russian one. -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} diff --git a/crates/proof/src/tr/dict.rs b/crates/proof/src/tr/dict.rs deleted file mode 100644 index 8cbacf02e..000000000 --- a/crates/proof/src/tr/dict.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Turkish Hunspell dictionary parser with `FLAG num` support. -//! -//! Turkish uses `FLAG num`, storing thousands of numerical suffix flags (0..6465). -//! This engine parses `/usr/share/hunspell/tr_TR.aff` and `.dic` safely without panics. - -use std::collections::HashMap; - -/// A suffix rule for Turkish. -#[derive(Clone, Debug)] -pub struct TrSuffixRule { - pub strip: Box, - pub add: Box, - pub cond: Box, -} - -/// Dedicated Turkish Hunspell dictionary engine. -#[derive(Clone, Debug, Default)] -pub struct TrDict { - /// Stem -> list of numerical flags it carries. - stems: HashMap>, - /// Flag number -> list of suffix rules. - rules: HashMap>, - /// Suffix 'add' string -> list of (flag, rule_index). - suffix_index: HashMap>, - /// Longest suffix 'add' string in characters. - max_suffix_len: usize, -} - -impl crate::slot::Dictionary for TrDict { - fn load(aff: &std::path::Path, dic: &std::path::Path) -> Result { - super::load(aff, dic) - } - - fn parse(aff: &str, dic: &str) -> Result { - TrDict::from_hunspell(aff, dic) - } - - fn is_empty(&self) -> bool { - TrDict::is_empty(self) - } -} - -impl TrDict { - /// Returns true if empty (no dictionary loaded). - pub fn is_empty(&self) -> bool { - self.stems.is_empty() - } - - /// Load from `.aff` and `.dic` contents. - pub fn from_hunspell(aff: &str, dic: &str) -> Result { - let mut rules: HashMap> = HashMap::new(); - let mut max_suffix_len = 0; - - for line in aff.lines() { - let parts: Vec<&str> = line.split_whitespace().collect(); - // SFX [] - if parts.len() >= 4 && parts[0] == "SFX" { - let Ok(flag) = parts[1].parse::() else { continue }; - // Skip header line: `SFX Y/N ` - if parts.len() == 4 && (parts[2] == "Y" || parts[2] == "N") && parts[3].parse::().is_ok() { - continue; - } - let strip = if parts[2] == "0" { "" } else { parts[2] }; - let add = if parts[3] == "0" { "" } else { parts[3] }; - let cond = if parts.len() > 4 { parts[4] } else { "." }; - - max_suffix_len = max_suffix_len.max(add.chars().count()); - - rules.entry(flag).or_default().push(TrSuffixRule { strip: strip.into(), add: add.into(), cond: cond.into() }); - } - } - - let mut suffix_index: HashMap> = HashMap::new(); - for (&flag, r_list) in &rules { - for (idx, r) in r_list.iter().enumerate() { - if let Ok(idx_u16) = u16::try_from(idx) { - suffix_index.entry(r.add.to_string()).or_default().push((flag, idx_u16)); - } - } - } - - let mut stems: HashMap> = HashMap::new(); - for (i, line) in dic.lines().enumerate() { - if i == 0 && line.trim().parse::().is_ok() { - continue; - } - let line = line.trim(); - if line.is_empty() { - continue; - } - if let Some((word, flags_part)) = line.split_once('/') { - let flags: Vec = flags_part.split(',').filter_map(|f| f.parse::().ok()).collect(); - stems.insert(word.to_string(), flags.into_boxed_slice()); - } else { - stems.insert(line.to_string(), Box::new([])); - } - } - - Ok(TrDict { stems, rules, suffix_index, max_suffix_len }) - } - - /// Check if word is correct (stem or affixed). - pub fn contains(&self, word: &str) -> bool { - // Longer input is never a dictionary word, and isn't scanned (#3625). - if self.is_empty() || word.is_empty() || word.len() > crate::affix::MAX_WORD_BYTES { - return false; - } - - // Direct stem hit - if self.stems.contains_key(word) { - return true; - } - - // Try stripping suffixes matching the tail - let word_len = word.chars().count(); - let char_indices: Vec<(usize, char)> = word.char_indices().collect(); - - // Check each possible suffix length up to max_suffix_len - for cut_chars in 0..=self.max_suffix_len.min(word_len) { - let byte_idx = if cut_chars == 0 { word.len() } else { char_indices[word_len - cut_chars].0 }; - - let add = &word[byte_idx..]; - let base = &word[..byte_idx]; - - if let Some(matches) = self.suffix_index.get(add) { - for &(flag, rule_idx) in matches { - let Some(rule_list) = self.rules.get(&flag) else { continue }; - let Some(rule) = rule_list.get(rule_idx as usize) else { continue }; - - // Check condition on root - let mut candidate = String::with_capacity(base.len() + rule.strip.len()); - candidate.push_str(base); - candidate.push_str(&rule.strip); - - if self.stems.get(&candidate).is_some_and(|f_list| f_list.contains(&flag)) { - return true; - } - } - } - } - - false - } -} diff --git a/crates/proof/src/tr/grammar.rs b/crates/proof/src/tr/grammar.rs deleted file mode 100644 index b17e89cb9..000000000 --- a/crates/proof/src/tr/grammar.rs +++ /dev/null @@ -1,359 +0,0 @@ -//! Turkish grammar and punctuation checks: cheap, rule-based and tuned to stay quiet when unsure. -//! Messages are in Turkish. -//! -//! - **Repeated word** (`çok çok`), except idiomatic reduplications and doublings (`ALLOWED_REPEATS`: -//! `yavaş yavaş`, `tıkır tıkır`, `adım adım`). -//! - **Space before punctuation** (`ev ,`) and **double spaces**. -//! - **Capital letter at the start of a sentence**, knowing Turkish abbreviations (`vb.`, `vs.`, -//! `prof.`, `dr.`, `mad.`, `yy.`, `s.`…) after which lowercase or numbers may continue. -//! - **No comma before/after conjunctions**: `ve`, `veya`, `yahut`, `ile`, `de`, `da`, `hem ... hem`, -//! `ne ... ne` conjunctions should not have commas immediately before or after them. -//! - **Doubled punctuation** (`..`, `,,`, `;;`). - -use crate::{Issue, IssueKind, ProofLang, words}; - -/// Turkish reduplications and emphatic word pairings that are intentionally repeated. -const ALLOWED_REPEATS: &[&str] = &[ - "yavaş", - "hızlı", - "koşa", - "güle", - "adım", - "kara", - "tıkır", - "şakır", - "akın", - "ağır", - "açık", - "ayrı", - "bire", - "bir", - "boş", - "bölük", - "buram", - "cana", - "can", - "cılk", - "çifter", - "çit", - "dalga", - "damla", - "deste", - "didik", - "dizi", - "dolu", - "deste", - "döne", - "eğri", - "enine", - "epey", - "fıkır", - "fırıl", - "gece", - "gıcır", - "gide", - "günü", - "güzel", - "halk", - "harıl", - "hık", - "hım", - "horul", - "ılık", - "ışıl", - "için", - "içten", - "iğne", - "iri", - "iyiden", - "iyi", - "kaba", - "kaç", - "kat", - "kaynar", - "kelli", - "kese", - "kıtır", - "kıvrım", - "koca", - "kol", - "kucak", - "kütür", - "mırıl", - "öbek", - "parça", - "patır", - "pıtır", - "sabah", - "salına", - "salkım", - "sap", - "sara", - "sarı", - "serin", - "sıra", - "sırım", - "sıkı", - "sırılsıklam", - "tane", - "teker", - "tek", - "tıpış", - "top", - "tutam", - "uzun", - "üçer", - "üst", - "yalan", - "yana", - "yan", - "yarım", - "yer", - "yudum", - "yığın", - "yürekten", - "yüz", - "zaman", - "zor", -]; - -/// Turkish abbreviations that end with a period and after which lower case may continue. -const ABBREVIATIONS: &[&str] = &[ - "vb", "vs", "vd", "bkz", "bknz", "ör", "örnek", "mad", "md", "s", "sf", "sn", "dk", "sa", "yy", "m", "km", "cm", "mm", "gr", "kg", "mg", "dr", - "prof", "doç", "yrd", "av", "ist", "ank", "izm", "cad", "sok", "mah", "apt", "no", "tel", "faks", "tl", "kr", "krş", "a.ş", "ltd", "şti", "t.c", - "tbmm", "meb", "möt", "mö", "ms", -]; - -/// Conjunctions that must not be preceded by a comma. -const CONJUNCTIONS_NO_PRE_COMMA: &[&str] = &["ve", "veya", "yahut", "velev", "veyahut", "de", "da"]; - -/// Conjunctions that must not be followed by a comma. -const CONJUNCTIONS_NO_POST_COMMA: &[&str] = &["ve", "veya", "yahut", "veyahut", "hem", "ne", "ya", "gerek", "ister", "de", "da"]; - -/// Check grammar and punctuation in a Turkish text block. -pub fn check(text: &str) -> Vec { - check_with(text, true) -} - -/// [`check`] where `capital_first` says whether the text's first word must start a sentence with a capital -/// (false for a list item, which may be a fragment). -pub fn check_with(text: &str, capital_first: bool) -> Vec { - let mut v = Vec::new(); - let ws = words(text); - - // 1. Repeated words (`kelime kelime`) - for pair in ws.windows(2) { - let (w1, w2) = match (text.get(pair[0].0..pair[0].1), text.get(pair[1].0..pair[1].1)) { - (Some(a), Some(b)) => (a, b), - _ => continue, - }; - let gap = match text.get(pair[0].1..pair[1].0) { - Some(g) => g, - None => continue, - }; - let only_spaces = gap.chars().all(|c| c == ' ' || c == '\t'); - let lower1 = w1.to_lowercase(); - let lower2 = w2.to_lowercase(); - if only_spaces && lower1 == lower2 && !ALLOWED_REPEATS.contains(&lower1.as_str()) { - v.push(Issue { - start: pair[1].0, - end: pair[1].1, - kind: IssueKind::Grammar, - message: "Yinelenen sözcük".into(), - suggestions: Vec::new(), - lang: ProofLang::Tr, - }); - } - } - - // 2. Spaces before punctuation and double spaces - let chars: Vec<(usize, char)> = text.char_indices().collect(); - for i in 0..chars.len() { - let (idx, c) = chars[i]; - if matches!(c, ',' | '.' | '?' | '!' | ';' | ':') && i > 0 && chars[i - 1].1 == ' ' { - let prev_sp = chars[i - 1].0; - // Leave emoticons and ellipses alone - let is_dot_in_dots = c == '.' && (i + 1 < chars.len() && chars[i + 1].1 == '.'); - if !is_dot_in_dots { - v.push(Issue { - start: prev_sp, - end: idx + c.len_utf8(), - kind: IssueKind::Grammar, - message: "Noktalama işaretinden önce boşluk bırakılmaz".into(), - suggestions: vec![c.to_string()], - lang: ProofLang::Tr, - }); - } - } - // Double space - if c == ' ' && i + 1 < chars.len() && chars[i + 1].1 == ' ' && (i == 0 || chars[i - 1].1 != ' ') { - let mut end = idx + 1; - while end < text.len() && text.as_bytes().get(end) == Some(&b' ') { - end += 1; - } - v.push(Issue { - start: idx, - end, - kind: IssueKind::Grammar, - message: "Fazladan boşluk".into(), - suggestions: vec![" ".into()], - lang: ProofLang::Tr, - }); - } - } - - // 3. Comma before or after conjunctions (`ali, ve veli` or `ve, veli`) - for (i, &(w_start, w_end)) in ws.iter().enumerate() { - let word = match text.get(w_start..w_end) { - Some(w) => w.to_lowercase(), - None => continue, - }; - // Check comma before conjunction - if CONJUNCTIONS_NO_PRE_COMMA.contains(&word.as_str()) - && i > 0 - && let Some(between) = text.get(ws[i - 1].1..w_start) - && let Some(comma_pos) = between.find(',') - { - let actual_pos = ws[i - 1].1 + comma_pos; - v.push(Issue { - start: actual_pos, - end: actual_pos + 1, - kind: IssueKind::Grammar, - message: format!("'{word}' bağlacından önce virgül konulmaz"), - suggestions: vec![String::new()], - lang: ProofLang::Tr, - }); - } - // Check comma after conjunction - if CONJUNCTIONS_NO_POST_COMMA.contains(&word.as_str()) - && i + 1 < ws.len() - && let Some(between) = text.get(w_end..ws[i + 1].0) - && let Some(comma_pos) = between.find(',') - { - let actual_pos = w_end + comma_pos; - v.push(Issue { - start: actual_pos, - end: actual_pos + 1, - kind: IssueKind::Grammar, - message: format!("'{word}' bağlacından sonra virgül konulmaz"), - suggestions: vec![String::new()], - lang: ProofLang::Tr, - }); - } - } - - // 4. Sentence capitalization knowing Turkish abbreviations - for (i, &(w_start, w_end)) in ws.iter().enumerate() { - if i == 0 { - // First word in paragraph - if capital_first - && let Some(w) = text.get(w_start..w_end) - && w.chars().next().is_some_and(char::is_lowercase) - { - let cap = turkish_capitalize(w); - v.push(Issue { - start: w_start, - end: w_end, - kind: IssueKind::Grammar, - message: "Cümle büyük harfle başlar".into(), - suggestions: vec![cap], - lang: ProofLang::Tr, - }); - } - continue; - } - - // Check if previous character sequence ended a sentence - let prev_end = ws[i - 1].1; - let prev_word = text.get(ws[i - 1].0..prev_end).unwrap_or(""); - if let Some(gap) = text.get(prev_end..w_start) { - let trimmed = gap.trim(); - if (trimmed.ends_with('.') || trimmed.ends_with('?') || trimmed.ends_with('!')) - && !trimmed.contains("...") - && !ABBREVIATIONS.contains(&prev_word.to_lowercase().as_str()) - && let Some(w) = text.get(w_start..w_end) - && w.chars().next().is_some_and(char::is_lowercase) - { - let cap = turkish_capitalize(w); - v.push(Issue { - start: w_start, - end: w_end, - kind: IssueKind::Grammar, - message: "Cümle büyük harfle başlar".into(), - suggestions: vec![cap], - lang: ProofLang::Tr, - }); - } - } - } - - v.sort_by_key(|issue| (issue.start, issue.end)); - v -} - -/// Capitalize according to Turkish rules (`i` -> `İ`, `ı` -> `I`). -pub fn turkish_capitalize(s: &str) -> String { - let mut chars = s.chars(); - let Some(first) = chars.next() else { return String::new() }; - let first_cap = match first { - 'i' => 'İ', - 'ı' => 'I', - other => other.to_uppercase().next().unwrap_or(other), - }; - let mut out = String::with_capacity(s.len() + 1); - out.push(first_cap); - out.push_str(chars.as_str()); - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn clean_turkish_text_produces_no_issues() { - let text = "Bu bir test cümlesidir. Kitap ve defter aldım. Çocuk yavaş yavaş yürüdü."; - assert!(check(text).is_empty()); - } - - #[test] - fn repeated_words_flagged_except_allowed() { - let text = "Gitti gitti ama bulamadı."; - let issues = check(text); - assert_eq!(issues.len(), 1); - assert_eq!(issues[0].message, "Yinelenen sözcük"); - - // Allowed repeat - let text2 = "Çocuk yavaş yavaş ilerliyordu."; - assert!(check(text2).is_empty()); - } - - #[test] - fn space_before_punctuation_flagged() { - let text = "Merhaba , dünya!"; - let issues = check(text); - assert_eq!(issues.len(), 1); - assert_eq!(issues[0].message, "Noktalama işaretinden önce boşluk bırakılmaz"); - } - - #[test] - fn comma_around_conjunctions_flagged() { - let text = "Elma, ve armut."; - let issues = check(text); - assert_eq!(issues.len(), 1); - assert!(issues[0].message.contains("virgül konulmaz")); - - let text2 = "Bana kalırsa bu mesele de, haklı olan taraf bizdik."; - let issues2 = check(text2); - assert_eq!(issues2.len(), 1); - assert!(issues2[0].message.contains("'de' bağlacından sonra virgül konulmaz")); - } - - #[test] - fn sentence_capitalization_respects_abbreviations() { - let text = "Dr. ahmet geldi. Fakat geç kaldı."; - let issues = check(text); - // "Dr. ahmet" does not flag "ahmet" because "Dr." is in ABBREVIATIONS. - assert!(issues.is_empty()); - } -} diff --git a/crates/proof/src/tr/hyphen.rs b/crates/proof/src/tr/hyphen.rs deleted file mode 100644 index a8a481088..000000000 --- a/crates/proof/src/tr/hyphen.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Turkish hyphenation by rule ([`ProofLang::Tr`](crate::ProofLang::Tr)). -//! -//! Turkish words are split at syllables according to Turkish Language Association (TDK) rules. -//! Because Turkish syllable boundaries are fully regular, they can be determined directly by rule -//! without needing external pattern files or word lists: -//! - Turkish vowels: `a`, `e`, `ı`, `i`, `o`, `ö`, `u`, `ü` (both lowercase and uppercase). -//! - A single consonant between two vowels joins the following syllable (`ba-lık`, `ö-ğren-ci`). -//! - Of two consonants between two vowels, the first stays with the preceding syllable and the -//! second joins the next (`alt-lık`, `sev-gi`, `tür-kçe`). -//! - Of three consonants between two vowels, the first two stay with the preceding syllable and the -//! third joins the next (`türk-çe`, `sürp-riz`). -//! - In compound words, syllable division follows pronunciation across the boundary (`ba-şöğ-ret-men`, -//! `il-ko-kul`), which matches the general vowel/consonant distribution rules. -//! -//! A single letter left at the end or start of a line is ruled out by the paragraph's -//! [`Limits`](crate::hyphen::Limits). - -/// Is `c` a Turkish vowel? -pub fn is_vowel(c: char) -> bool { - matches!(c, 'a' | 'e' | 'ı' | 'i' | 'o' | 'ö' | 'u' | 'ü' | 'A' | 'E' | 'I' | 'İ' | 'O' | 'Ö' | 'U' | 'Ü') -} - -/// Is `c` a Turkish letter? -pub(crate) fn is_letter(c: char) -> bool { - c.is_alphabetic() -} - -/// Break points of a Turkish word (char indices; a hyphen goes before that char). -/// Empty for words with fewer than two vowels or containing non-alphabetic characters. -pub fn points(word: &[char]) -> Vec { - if word.len() < 2 || !word.iter().all(|&c| is_letter(c)) { - return Vec::new(); - } - let vowels: Vec = (0..word.len()).filter(|&i| is_vowel(word[i])).collect(); - if vowels.len() < 2 { - return Vec::new(); - } - let mut out = Vec::new(); - for pair in vowels.windows(2) { - let (v1, v2) = (pair[0], pair[1]); - if v2 <= v1 { - continue; - } - let num_consonants = v2 - v1 - 1; - let pt = match num_consonants { - 0 => v1 + 1, - 1 => v2 - 1, - 2 => v1 + 2, - _ => v2 - 1, - }; - if pt > v1 && pt <= v2 { - out.push(pt); - } - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - fn split(w: &str) -> String { - let chars: Vec = w.chars().collect(); - let pts = points(&chars); - let mut out = String::new(); - for (i, c) in chars.iter().enumerate() { - if pts.contains(&i) { - out.push('-'); - } - out.push(*c); - } - out - } - - #[test] - fn syllables_follow_turkish_rules() { - assert_eq!(split("balık"), "ba-lık"); - assert_eq!(split("araba"), "a-ra-ba"); - assert_eq!(split("öğrenci"), "öğ-ren-ci"); - assert_eq!(split("altlık"), "alt-lık"); - assert_eq!(split("sevgi"), "sev-gi"); - assert_eq!(split("türkçe"), "türk-çe"); - assert_eq!(split("saat"), "sa-at"); - assert_eq!(split("şiir"), "şi-ir"); - assert_eq!(split("ilkokul"), "il-ko-kul"); - assert_eq!(split("başöğretmen"), "ba-şöğ-ret-men"); - } - - #[test] - fn short_or_non_words_stay_unsplit() { - assert_eq!(split(""), ""); - assert_eq!(split("a"), "a"); - assert_eq!(split("ve"), "ve"); - assert_eq!(split("Türk"), "Türk"); - assert_eq!(split("123"), "123"); - assert_eq!(split("araba-ev"), "araba-ev"); - } -} diff --git a/crates/proof/src/tr/mod.rs b/crates/proof/src/tr/mod.rs deleted file mode 100644 index 967148d8b..000000000 --- a/crates/proof/src/tr/mod.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Turkish proofing. -//! -//! - **Spelling** against the Hunspell `tr_TR` dictionary installed on the system -//! ([`dictionary`], found by [`crate::hunspell`]: `tr_TR.aff`/`tr_TR.dic` from the OS, a -//! package manager or LibreOffice; it is not bundled). Without it Turkish text gets no spelling -//! marks. Capitalized stems are proper nouns (`Ankara`); foreign names inflected with an apostrophe -//! are accepted (`WordCraft'ta`), and the user dictionary applies as in English. -//! - **Suggestions** tuned for Turkish ([`suggest`]): ASCII-diacritic restoration (`turkce` -> `Türkçe`), -//! common confusions (`yalnış` -> `yanlış`), detached particles (`geldimi` -> `geldi mi`), and typos. -//! - **Grammar** ([`grammar`]): high-precision rules (reduplications, comma around conjunctions, -//! sentence start with abbreviations). -//! - **Hyphenation** ([`hyphen`]): syllable division by Turkish Language Association (TDK) rules. - -pub mod dict; -pub mod grammar; -pub mod hyphen; -pub mod suggest; -#[cfg(test)] -pub(crate) mod test_dict; - -use std::sync::Arc; - -use crate::slot::DictSlot; - -pub use dict::TrDict; -pub use suggest::suggest; - -/// File names of a Turkish Hunspell dictionary. -const NAMES: &[&str] = &["tr_TR", "tr-TR", "tr"]; - -/// The Turkish dictionary, found on first use (see [`DictSlot`]). -pub(crate) static DICT: DictSlot = DictSlot::new(NAMES, "Turkish"); - -/// The Turkish dictionary: the installed Hunspell `tr_TR` (read on first use), one given with -/// [`install_dictionary`], or an empty one when none is available. -pub fn dictionary() -> Arc { - DICT.get() -} - -/// Read a Turkish Hunspell pair from disk: size-capped and decoded per the `.aff`'s `SET` line, -/// like the other languages' dictionaries (#3661). -pub(crate) fn load(aff: &std::path::Path, dic: &std::path::Path) -> Result { - let (aff_text, dic_text) = crate::hunspell::decode(&crate::hunspell::read_capped(aff)?, &crate::hunspell::read_capped(dic)?)?; - TrDict::from_hunspell(&aff_text, &dic_text) -} - -/// Use this Turkish dictionary from now on (Hunspell `.aff` and `.dic` text). On error the -/// current dictionary stays. -pub fn install_dictionary(aff: &str, dic: &str) -> Result<(), String> { - DICT.install(aff, dic) -} - -/// Is a Turkish dictionary available? -pub fn has_dictionary() -> bool { - DICT.has_dictionary() -} - -/// Is `word` spelled correctly in Turkish? -pub fn is_correct(word: &str) -> bool { - is_correct_with(&dictionary(), word) -} - -/// [`is_correct`] with the File › Options › Proofing choices: with Ignore words that contain -/// numbers off, words with digits are checked against the dictionary too. -pub fn is_correct_with_options(word: &str, opts: &crate::Options) -> bool { - check(&dictionary(), word, opts) -} - -/// [`is_correct`] against a given dictionary. -pub fn is_correct_with(d: &TrDict, word: &str) -> bool { - check(d, word, &crate::Options::default()) -} - -/// [`is_correct_with_options`] against a given dictionary. -fn check(d: &TrDict, word: &str, opts: &crate::Options) -> bool { - if d.is_empty() { - return true; // No Turkish dictionary installed: no spelling marks - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { - return true; - } - // Check user dictionary first - if crate::in_user_dictionary(&suggest::turkish_to_lower(w)) || crate::in_user_dictionary(&w.to_lowercase()) { - return true; - } - - // Direct lookup or lowercase lookup - if d.contains(w) { - return true; - } - let lower = suggest::turkish_to_lower(w); - if d.contains(&lower) { - return true; - } - - // Handle apostrophe for proper nouns (e.g., `Ankara'da`, `WordCraft'ta`) - if let Some((stem, _suffix)) = w.split_once(['\'', '’']) - && (d.contains(stem) || d.contains(&suggest::turkish_to_lower(stem))) - { - return true; - } - - // Compound words separated by hyphens (e.g., `afyon-karahisar`) - if w.contains('-') && w.split('-').all(|part| part.is_empty() || check(d, part, opts)) { - return true; - } - - false -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn without_dictionary_everything_is_correct() { - let empty = TrDict::default(); - assert!(is_correct_with(&empty, "herhangi")); - assert!(is_correct_with(&empty, "xyzabc")); - } - - #[test] - fn apostrophe_and_compounds_are_handled() { - let empty = TrDict::default(); - assert!(is_correct_with(&empty, "Ankara'da")); - assert!(is_correct_with(&empty, "açık-mavi")); - } - - #[test] - fn installed_dictionary_checks_words() { - test_dict::install(); - assert!(is_correct("kitap")); - assert!(is_correct("kitaplar")); - assert!(is_correct("evde")); - assert!(is_correct("olacaklardan")); - assert!(is_correct("sonra")); - assert!(!is_correct("sorna")); - assert!(!is_correct("kitapppp")); - assert!(!is_correct("evdekişşş")); - - // Test user dictionary - assert!(!is_correct("benimbilmedigimkelime")); - crate::add_word("benimbilmedigimkelime"); - assert!(is_correct("benimbilmedigimkelime")); - } - - /// With Ignore words that contain numbers off, Turkish checks words with digits against the - /// dictionary instead of accepting them, hyphen parts included. - #[test] - fn proofing_options_reach_the_turkish_check() { - test_dict::install(); - let lang = crate::ProofLang::Tr; - let defaults = crate::Options::default(); - let check_digits = crate::Options { ignore_numbers: false, ..defaults }; - assert!(crate::is_correct_in_with("kitapp2", lang, &defaults)); - assert!(!crate::is_correct_in_with("kitapp2", lang, &check_digits)); - assert!(!crate::is_correct_in_with("ev-kitapp2", lang, &check_digits)); - assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); - assert!(is_correct("kitapp2"), "direct checks keep the defaults"); - } -} diff --git a/crates/proof/src/tr/suggest.rs b/crates/proof/src/tr/suggest.rs deleted file mode 100644 index 08874420e..000000000 --- a/crates/proof/src/tr/suggest.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Turkish spelling suggestions, ranked by likelihood. -//! -//! Candidates are generated and ranked by cost: -//! 1. Capitalization / case adjustments (especially dotted/dotless I: `istanbul` -> `İstanbul`). -//! 2. Missing Turkish diacritics / ASCII substitutions (most frequent Turkish typing slip): -//! `c` <-> `ç`, `g` <-> `ğ`, `i` <-> `ı`, `o` <-> `ö`, `s` <-> `ş`, `u` <-> `ü` (`turkce` -> `Türkçe`). -//! 3. Common spelling mistakes and phonetic confusions (`REP` table): -//! `yalnış` -> `yanlış`, `yanlız` -> `yalnız`, `herşey` -> `her şey`, `bir kaç` -> `birkaç`. -//! 4. Missing space for detached particles and conjunctions (`bende` -> `ben de`, `geldinmi` -> `geldin mi`). -//! 5. Single typo (Damerau-Levenshtein distance <= 2). - -use std::collections::HashSet; - -use crate::tr::dict::TrDict; - -/// Common misspellings and their canonical replacements in Turkish. -const COMMON_REPLACEMENTS: &[(&str, &str)] = &[ - ("yalnış", "yanlış"), - ("yanlız", "yalnız"), - ("herşey", "her şey"), - ("herkez", "herkes"), - ("bir kaç", "birkaç"), - ("birşey", "bir şey"), - ("tabiki", "tabii ki"), - ("orjinal", "orijinal"), - ("kiprik", "kirpik"), - ("şöför", "şoför"), - ("sarmısak", "sarımsak"), - ("makina", "makine"), - ("ünvan", "unvan"), - ("rastlantı", "rastlantı"), - ("labaratuvar", "laboratuvar"), - ("laboratuar", "laboratuvar"), - ("kravat", "kravat"), - ("gardolap", "gardırop"), - ("egzoz", "egzoz"), - ("egzozt", "egzoz"), - ("egsoz", "egzoz"), - ("gelcen", "geleceksin"), - ("gelecen", "geleceksin"), - ("gitcen", "gideceksin"), - ("gidecen", "gideceksin"), - ("yapcan", "yapacaksın"), - ("yapacan", "yapacaksın"), - ("edecen", "edeceksin"), - ("deyil", "değil"), -]; - -/// Longest word (in letters) given suggestions. Every candidate is a fresh copy of the word, so -/// the cost grows with the square of its length (#3625); Turkish words run longer than most -/// (`Çekoslovakyalılaştıramadıklarımızdanmışsınız` is 43 letters), so the cap is above the other -/// languages' 40. -const MAX_LETTERS: usize = 64; - -/// Produce spelling suggestions for a misspelled Turkish word. -pub fn suggest(word: &str, max: usize) -> Vec { - let dict = super::dictionary(); - suggest_with(&dict, word, max) -} - -/// Suggest with a given dictionary. -pub fn suggest_with(d: &TrDict, word: &str, max: usize) -> Vec { - let n = word.chars().count(); - if n == 0 || n > MAX_LETTERS || max == 0 { - return Vec::new(); - } - - let mut suggestions = Vec::new(); - let mut seen = HashSet::new(); - - let lower = turkish_to_lower(word); - - // 1. Check direct common replacement table - for &(wrong, right) in COMMON_REPLACEMENTS { - if lower == wrong && seen.insert(right.to_string()) { - suggestions.push(right.to_string()); - if suggestions.len() >= max { - return suggestions; - } - } - } - // Only the table takes more than one word (`bir kaç`). - if word.contains(char::is_whitespace) { - return suggestions; - } - - // 2. Diacritic substitutions (`c` -> `ç`, `g` -> `ğ`, `ı` -> `i`, `o` -> `ö`, `s` -> `ş`, `u` -> `ü`) - if !d.is_empty() { - let variants = generate_diacritic_variants(&lower); - for cand in variants { - if cand != lower && d.contains(&cand) && seen.insert(cand.clone()) { - suggestions.push(match_casing(word, &cand)); - if suggestions.len() >= max { - return suggestions; - } - } - } - } - - // 3. Detached suffix check: `-de`, `-da`, `-mi`, `-mı`, `-mu`, `-mü` - for suffix in ["de", "da", "te", "ta", "mi", "mı", "mu", "mü"] { - if lower.ends_with(suffix) && lower.len() > suffix.len() + 1 { - let stem = &lower[..lower.len() - suffix.len()]; - if d.is_empty() || d.contains(stem) { - let cand = format!("{stem} {suffix}"); - if seen.insert(cand.clone()) { - suggestions.push(match_casing(word, &cand)); - if suggestions.len() >= max { - return suggestions; - } - } - } - } - } - - // 4. Single typo (transposition, deletion, insertion, substitution) - if !d.is_empty() { - let chars: Vec = lower.chars().collect(); - let n = chars.len(); - - // 4a. Transposition (adjacent swap: `sorna` -> `sonra`) - for i in 0..n.saturating_sub(1) { - if chars.get(i) != chars.get(i + 1) { - let mut swapped = chars.clone(); - swapped.swap(i, i + 1); - let cand: String = swapped.into_iter().collect(); - if d.contains(&cand) && seen.insert(cand.clone()) { - suggestions.push(match_casing(word, &cand)); - if suggestions.len() >= max { - return suggestions; - } - } - } - } - - // Turkish alphabet for insertions and substitutions - const TR_ALPHABET: &[char] = &[ - 'a', 'b', 'c', 'ç', 'd', 'e', 'f', 'g', 'ğ', 'h', 'ı', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'ö', 'p', 'r', 's', 'ş', 't', 'u', 'ü', 'v', - 'y', 'z', - ]; - - // 4b. Deletion (one character left out: `kitapp` -> `kitap`) - for i in 0..n { - let mut deleted = chars.clone(); - deleted.remove(i); - let cand: String = deleted.into_iter().collect(); - if d.contains(&cand) && seen.insert(cand.clone()) { - suggestions.push(match_casing(word, &cand)); - if suggestions.len() >= max { - return suggestions; - } - } - } - - // 4c. Substitution (one wrong letter: `yolculuk` -> ...) - for i in 0..n { - for &c in TR_ALPHABET { - if chars.get(i) != Some(&c) { - let mut substituted = chars.clone(); - if let Some(target) = substituted.get_mut(i) { - *target = c; - } - let cand: String = substituted.into_iter().collect(); - if d.contains(&cand) && seen.insert(cand.clone()) { - suggestions.push(match_casing(word, &cand)); - if suggestions.len() >= max { - return suggestions; - } - } - } - } - } - - // 4d. Insertion (one missing letter: `kitp` -> `kitap`) - for i in 0..=n { - for &c in TR_ALPHABET { - let mut inserted = chars.clone(); - inserted.insert(i, c); - let cand: String = inserted.into_iter().collect(); - if d.contains(&cand) && seen.insert(cand.clone()) { - suggestions.push(match_casing(word, &cand)); - if suggestions.len() >= max { - return suggestions; - } - } - } - } - } - - // 5. Fallback: capitalization - let cap = super::grammar::turkish_capitalize(&lower); - if cap != word && (d.is_empty() || d.contains(&cap)) && seen.insert(cap.clone()) { - suggestions.push(cap); - } - - suggestions -} - -/// Convert ASCII letters to Turkish diacritic equivalents. -fn generate_diacritic_variants(s: &str) -> Vec { - let mut results = vec![String::new()]; - for c in s.chars() { - let (alt1, alt2) = match c { - 'c' => ('c', Some('ç')), - 'ç' => ('ç', Some('c')), - 'g' => ('g', Some('ğ')), - 'ğ' => ('ğ', Some('g')), - 'i' => ('i', Some('ı')), - 'ı' => ('ı', Some('i')), - 'o' => ('o', Some('ö')), - 'ö' => ('ö', Some('o')), - 's' => ('s', Some('ş')), - 'ş' => ('ş', Some('s')), - 'u' => ('u', Some('ü')), - 'ü' => ('ü', Some('u')), - other => (other, None), - }; - - let branch_mult = if alt2.is_some() { 2 } else { 1 }; - if results.len() * branch_mult > 1024 { - // Cap search branch explosion - for res in &mut results { - res.push(c); - } - continue; - } - - let mut next_gen = Vec::with_capacity(results.len() * branch_mult); - for existing in &results { - let mut s1 = existing.clone(); - s1.push(alt1); - next_gen.push(s1); - if let Some(c2) = alt2 { - let mut s2 = existing.clone(); - s2.push(c2); - next_gen.push(s2); - } - } - results = next_gen; - } - results -} - -/// Turkish lowercase with dotted/dotless I support. -pub fn turkish_to_lower(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - match c { - 'I' => out.push('ı'), - 'İ' => out.push('i'), - other => { - for lc in other.to_lowercase() { - out.push(lc); - } - } - } - } - out -} - -/// Match original casing on suggestion. -fn match_casing(orig: &str, cand: &str) -> String { - if orig.chars().all(char::is_uppercase) { - cand.to_uppercase() - } else if orig.chars().next().is_some_and(char::is_uppercase) { - super::grammar::turkish_capitalize(cand) - } else { - cand.to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn common_replacements_are_suggested() { - let sugs = suggest("yalnış", 3); - assert!(sugs.contains(&"yanlış".to_string())); - - let sugs2 = suggest("herkez", 3); - assert!(sugs2.contains(&"herkes".to_string())); - } - - #[test] - fn detached_suffix_is_suggested() { - let sugs = suggest("geldinmi", 3); - assert!(sugs.contains(&"geldin mi".to_string())); - } - - #[test] - fn overlong_words_get_no_suggestions_promptly() { - let d = TrDict::from_hunspell("SET UTF-8\nSFX 1 Y 1\nSFX 1 0 lar .\n", "2\nkitap/1\nsonra\n").unwrap(); - assert!(suggest_with(&d, "sorna", 3).contains(&"sonra".to_string())); - assert!(suggest_with(&d, "kitaplr", 3).contains(&"kitaplar".to_string())); - let t = std::time::Instant::now(); - let long = "a".repeat(100_000); - assert!(suggest_with(&d, &long, 8).is_empty()); - assert!(!d.contains(&long) && !d.contains(&format!("kitap{long}"))); - assert!(!super::super::is_correct_with(&d, &long)); - // Tens of milliseconds with the caps; without them this ran for over 5 minutes. - assert!(t.elapsed().as_secs_f64() < 10.0, "{:?}", t.elapsed()); - } - - #[test] - fn two_words_get_only_the_tables_replacement() { - let d = TrDict::from_hunspell("SET UTF-8\n", "2\nbirkaç\nsonra\n").unwrap(); - assert_eq!(suggest_with(&d, "bir kaç", 3), vec!["birkaç".to_string()]); - assert_eq!(suggest_with(&d, "Bir kaç", 3), vec!["birkaç".to_string()]); - assert!(suggest_with(&d, "sorna sorna", 3).is_empty()); - } - - #[test] - fn turkish_to_lower_handles_i() { - assert_eq!(turkish_to_lower("IŞIK"), "ışık"); - assert_eq!(turkish_to_lower("İSTANBUL"), "istanbul"); - } - - #[test] - fn typo_transposition_is_suggested() { - crate::tr::test_dict::install(); - let sugs = suggest("sorna", 3); - assert!(sugs.contains(&"sonra".to_string()), "sorna should suggest sonra, got {sugs:?}"); - - let sugs_gelcen = suggest("gelcen", 3); - assert!(sugs_gelcen.contains(&"geleceksin".to_string()), "gelcen should suggest geleceksin, got {sugs_gelcen:?}"); - } -} diff --git a/crates/proof/src/vi.rs b/crates/proof/src/vi.rs deleted file mode 100644 index 1abf7d4aa..000000000 --- a/crates/proof/src/vi.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Telling Vietnamese text apart from English, so a document typed in Vietnamese under the default -//! English proofing language isn't covered in red underlines (as Word's "detect language -//! automatically" does). There are no Vietnamese proofing tools yet, so Vietnamese text is treated -//! like any language without them: no spelling or grammar marks. -//! -//! The test is orthographic. Vietnamese writes most syllables with letters no other Latin-script -//! language shares: the horn letters (`ơ ư`), `ĩ ũ`, and the tone-marked vowels of the Latin -//! Extended Additional block (`ạ ả ấ ầ … ỹ`), precomposed or as combining hook above, dot below -//! and horn. French and Portuguese share `â ê ô à é`, Romanian `ă` and Croatian, Serbian and -//! Bosnian `đ`, so those alone don't count. - -/// Does `c` occur in Vietnamese words but (practically) nowhere else? -fn vietnamese_letter(c: char) -> bool { - matches!(c, 'ơ' | 'Ơ' | 'ư' | 'Ư' | 'ĩ' | 'Ĩ' | 'ũ' | 'Ũ' | '\u{1EA0}'..='\u{1EF9}' | '\u{0309}' | '\u{0323}' | '\u{031B}') -} - -/// Is `word` written with a Vietnamese-only letter (`Việt`, `ơn`)? Such a word is never an -/// English misspelling. -pub fn is_vietnamese_word(word: &str) -> bool { - let combining = |c: char| matches!(c, '\u{0300}'..='\u{036F}'); - // Two marks stacked on one letter (decomposed `ế` = e + circumflex + acute) are Vietnamese too. - word.chars().any(vietnamese_letter) || word.chars().zip(word.chars().skip(1)).any(|(a, b)| combining(a) && combining(b)) -} - -/// Does a paragraph read as Vietnamese? At least two of its words (any, in a heading of up to three -/// words) carry a Vietnamese-only letter, and they are at least a sixth of its words: Vietnamese syllables without -/// such a letter (`anh`, `con`, `ba`) are common, while an English paragraph quoting one -/// Vietnamese name stays English. -pub fn looks_vietnamese(text: &str) -> bool { - let mut total = 0usize; - let mut marked = 0usize; - for w in text.split(|c: char| !(c.is_alphanumeric() || matches!(c, '\u{0300}'..='\u{036F}'))).filter(|w| !w.is_empty()) { - total += 1; - if is_vietnamese_word(w) { - marked += 1; - } - } - marked > 0 && (marked >= 2 || total <= 3) && marked * 6 >= total -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn vietnamese_words() { - for w in ["Việt", "ơn", "những", "Ước", "mũi", "Nghĩa", "tie\u{0302}\u{0301}ng", "nho\u{031B}"] { - assert!(is_vietnamese_word(w), "{w}"); - } - // Shared with French, Portuguese, Spanish: not enough on their own. - // Romanian ă and Croatian đ are Vietnamese letters too, but not Vietnamese-only. - for w in ["café", "être", "côte", "água", "señor", "anh", "the", "mănânc", "acasă", "Đurđa", "đak", "đi"] { - assert!(!is_vietnamese_word(w), "{w}"); - } - } - - #[test] - fn paragraphs() { - assert!(looks_vietnamese("Tôi yêu tiếng Việt và những bài thơ của nó.")); - assert!(looks_vietnamese("Hà Nội")); - assert!(looks_vietnamese("Việt")); - // English quoting one Vietnamese name, and French, stay as they are. - assert!(!looks_vietnamese("The meeting with Mr Nguyễn is on Tuesday at the main office.")); - assert!(!looks_vietnamese("Le café de la côte est très bon.")); - assert!(!looks_vietnamese("")); - assert!(!looks_vietnamese("anh em")); - // Romanian and Croatian paragraphs, full of ă and đ, aren't Vietnamese. - assert!(!looks_vietnamese("Mă duc acasă să mănânc cu fratele meu, că e târziu.")); - assert!(!looks_vietnamese("Đurđa i Đorđe su se vratili kući, a Đakovo je daleko.")); - } -} - -#[cfg(test)] -mod checker_tests { - use crate::{Options, ProofLang, check_text_with}; - - #[test] - fn vietnamese_paragraphs_under_english_get_no_marks() { - let text = "Hôm nay tôi đi học, trời rất đẹp và mát mẻ."; - let spans = [(0..text.len(), ProofLang::En)]; - assert!(check_text_with(text, &spans, &Options::default(), true, false).is_empty()); - // No language at all (the document default) behaves the same. - assert!(check_text_with(text, &[], &Options::default(), true, false).is_empty()); - } - - #[test] - fn english_keeps_its_marks_and_skips_vietnamese_names() { - let text = "We met Nguyễn yesterdy at the office."; - let issues = check_text_with(text, &[(0..text.len(), ProofLang::En)], &Options::default(), false, false); - let flagged: Vec<&str> = issues.iter().filter_map(|i| text.get(i.start..i.end)).collect(); - assert_eq!(flagged, ["yesterdy"]); - } -} diff --git a/docs/architecture.md b/docs/architecture.md index faa82e3dd..9b26fa4e3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # WordCraft architecture -> **Last reviewed:** 2026-10-28 · **Last updated:** 2026-10-28 · **Change:** major (crate and layer inventory, source counts, proofing languages and module boundaries re-measured from this checkout; previously minor: `proof` is per language: English and Polish; previously major: first committed version; `plan/architecture.md` is local-only) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-28 · **Last updated:** 2026-11-01 · **Change:** minor (proofing moved out: `wordcraft-proof` is craft-libs' `craft-proof`, its Language data craft-i18n's `language-data/` embedded from `CRAFT_I18N_DIR`; previously major: crate and layer inventory, source counts, proofing languages and module boundaries re-measured from this checkout; previously minor: `proof` is per language: English and Polish; previously major: first committed version; `plan/architecture.md` is local-only) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 How WordCraft is built today. The longer design notes in `plan/architecture.md` are gitignored and local to the original author's machine; this file is the @@ -22,7 +22,7 @@ crate name links to its source tree. | L1 | [`doc`](../crates/doc/src) | 23,719 | Document model: stories, paragraphs, runs, tables, sections, styles, numbering, fields, notes, comments, revisions and equations; edit primitives and bidi | | L1 | [`fonts`](../crates/fonts/src) | 7,088 | Font database, metrics, outlines, HarfRust shaping, Word font substitution and OpenType MATH | | L1 | [`metafile`](../crates/metafile/src) | 3,614 | Shared Windows Metafile parsing and drawing primitives used by format readers and renderers | -| L1 | [`proof`](../crates/proof/src) | 16,326 | Fifteen spelling dictionaries: English (US, GB, CA), Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish (Spain, Mexico, Latin America, Uruguay); language modules provide spelling, suggestions and grammar, while Bulgarian and German have hyphenation only | +| L1 (external) | [`craft-proof`](https://github.com/storytold/craft-libs/tree/main/crates/proof) (package alias `wordcraft-proof`) | — | Proofing moved to craft-libs (CP1, 2026-11): fifteen spelling dictionaries: English (US, GB, CA), Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish (Spain, Mexico, Latin America, Uruguay); language modules provide spelling, suggestions and grammar, while Bulgarian and German have hyphenation only. Its word lists and patterns are craft-i18n's `language-data/`, embedded from `CRAFT_I18N_DIR` at the commit in `craft-i18n.rev` (CI and releases set `CRAFT_I18N_REQUIRED=1`); without them English is not spell-checked and hyphenation is off | | L2 | [`docbin`](../crates/docbin/src) | 8,815 | Word 97–2003 `.doc` reader (FIB, piece table, FKPs, SPRMs, stylesheet, lists, tables, notes, pictures) over a CFB container | | L2 | [`docx`](../crates/docx/src) | 37,376 | OOXML WordprocessingML reader/writer (Transitional and Strict), OMML, custom properties and preserved VBA parts | | L2 | [`formats`](../crates/formats/src) | 23,058 | TXT, Markdown, HTML, RTF, ODT and LaTeX import/export through the shared document model | @@ -69,6 +69,7 @@ crate name links to its source tree. | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | `proof` left the workspace: it is `craft-proof` in storytold/craft-libs (with its history), and the spelling and hyphenation data is craft-i18n's `language-data/` (`CRAFT_I18N_DIR`); `assets/spelling` and `assets/hyphenation` are gone | | 2026-10-28 | major | Recounted workspace crates, layers and Rust source lines; refreshed proofing languages and documented current engine/layout module boundaries and source links | | 2026-10-10 | minor | `proof`: proofing languages (English, Polish), affix engine, Polish patterns; layout and engine route each run by its language | | 2026-10-10 | major | First committed architecture doc, from the crates' sources and `AGENTS.md` | diff --git a/docs/localization-tr.md b/docs/localization-tr.md index 4c7dfb290..6969c54e7 100644 --- a/docs/localization-tr.md +++ b/docs/localization-tr.md @@ -52,7 +52,7 @@ test checks dotted and dotless I under Turkish casing with fixture labels, and t and ASCII command ids still match. The shared checks (catalog validation and the missing-translation report, `cargo xtask i18n-report`) include Turkish automatically. -Turkish proofing (`crates/proof/src/tr`) provides spelling against installed Hunspell `tr_TR` +Turkish proofing (`crates/proof/src/tr` in storytold/craft-libs, the `craft-proof` crate) provides spelling against installed Hunspell `tr_TR` dictionaries, spelling suggestions tuned for Turkish diacritics and detached particles, rule-based grammar checks (reduplications, conjunction commas, capitalization), and syllable hyphenation by TDK rules. diff --git a/packaging/linux/flatpak/ai.storyteller.wordcraft.yml b/packaging/linux/flatpak/ai.storyteller.wordcraft.yml index 11337ddda..50ac1beca 100644 --- a/packaging/linux/flatpak/ai.storyteller.wordcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.wordcraft.yml @@ -122,9 +122,10 @@ modules: - corpus - build-flatpak - .flatpak-builder - # The interface translations at the commit in craft-i18n.rev (a test keeps the two equal). + # The interface translations and Language data at the commit in craft-i18n.rev (a test keeps + # the two equal). - type: git url: https://github.com/storytold/craft-i18n - commit: 9205108cfdced5177706264038848b68b2422d2b + commit: f84aaa56adf40a595e0d279a6014cf1d07eb0289 dest: craft-i18n - cargo-sources.json diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index ee0e69e47..d7f4b702f 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -38,7 +38,6 @@ pub const TABLE: &[(&str, Class)] = &[ ("doc", Class::Layer(1)), ("fonts", Class::Layer(1)), ("metafile", Class::Layer(1)), - ("proof", Class::Layer(1)), ("layout", Class::Layer(2)), ("docx", Class::Layer(2)), ("docbin", Class::Layer(2)), From ccc8780b5e336e83aec129531e796f23a3fa8e22 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 16:17:28 +0000 Subject: [PATCH 02/15] CP1.3: spelling-dictionaries lists WordCraft's own folder on macOS and Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit craft-proof now searches it there too (ported from storytold/craft-libs#31). Co-Authored-By: Michał Marcin Brzuchalski Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- docs/spelling-dictionaries.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/spelling-dictionaries.md b/docs/spelling-dictionaries.md index 70354e7e0..a35fe35c4 100644 --- a/docs/spelling-dictionaries.md +++ b/docs/spelling-dictionaries.md @@ -1,6 +1,6 @@ # Installing spelling dictionaries -> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-23 · **Change:** minor (ISO 8859-9 Turkish dictionaries are read, #3692; previously minor: Spanish: `es_ES`, `es_MX`, `es_419` and `es_UY` with their fallbacks, #3360, #3553; previously trivial: every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824) +> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-11-01 · **Change:** trivial (WordCraft's own dictionaries folder is searched on macOS and Linux too, as on Windows, so a dictionary dropped there works without a setting; previously minor: ISO 8859-9 Turkish dictionaries are read, #3692; previously minor: Spanish: `es_ES`, `es_MX`, `es_419` and `es_UY` with their fallbacks, #3360, #3553; previously trivial: every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824) English spelling works out of the box. Polish, Russian, Portuguese, Czech, Italian, Croatian, Turkish, Indonesian and Spanish spelling use a standard **Hunspell** dictionary (an `.aff` and a `.dic` file) that @@ -61,8 +61,8 @@ The first folder holding a matching pair wins: | System | Folders | |---|---| | Windows | `%LOCALAPPDATA%\WordCraft\dictionaries`, `%LOCALAPPDATA%\hunspell`, `%APPDATA%\WordCraft\dictionaries`, `%APPDATA%\hunspell`, LibreOffice's `share\extensions\dict-*` under `Program Files` (on any drive) | -| macOS | `~/Library/Spelling`, `/Library/Spelling`, `/opt/homebrew/share/hunspell`, `/usr/local/share/hunspell`, `/Applications/LibreOffice.app/Contents/Resources/extensions/dict-*` | -| Linux and BSD | `~/.local/share/hunspell`, `/usr/share/hunspell`, `/usr/share/myspell`, `/usr/share/myspell/dicts`, `/usr/local/share/hunspell`, `/usr/local/share/myspell`, LibreOffice's `share/extensions/dict-*` under `/usr/lib/libreoffice` or `/opt/libreoffice` | +| macOS | `~/Library/Application Support/WordCraft/dictionaries`, `~/Library/Spelling`, `/Library/Spelling`, `/opt/homebrew/share/hunspell`, `/usr/local/share/hunspell`, `/Applications/LibreOffice.app/Contents/Resources/extensions/dict-*` | +| Linux and BSD | `$XDG_DATA_HOME/wordcraft/dictionaries` (default `~/.local/share/wordcraft/dictionaries`), `~/.local/share/hunspell`, `/usr/share/hunspell`, `/usr/share/myspell`, `/usr/share/myspell/dicts`, `/usr/local/share/hunspell`, `/usr/local/share/myspell`, LibreOffice's `share/extensions/dict-*` under `/usr/lib/libreoffice` or `/opt/libreoffice` | The web version can't read these folders, so these languages aren't spell-checked there. @@ -142,6 +142,7 @@ Never commit dictionary files to this repository: they stay on the user's system |---|---|---| | 2026-10-22 | minor | Spanish (#3360, #3553): the file names for Spain, Mexico, Latin America and Uruguay and their fallbacks | | 2026-10-22 | trivial | Italian, Croatian, Turkish, Russian, Czech and Indonesian without a dictionary now show **(no dictionary)** (or **(dictionary unreadable)**) too, as this page already said; before, only Polish and Portuguese did. Indonesian is listed with its file names (#3150) | +| 2026-11-01 | trivial | WordCraft's own `dictionaries` folder under the user's application data is searched on macOS (`~/Library/Application Support/WordCraft/dictionaries`) and Linux/BSD (`$XDG_DATA_HOME/wordcraft/dictionaries`) too, before the system folders, as it was on Windows (craft-proof; ported from storytold/craft-libs#31 by brzuchal) | | 2026-10-23 | trivial | ISO 8859-9 (`SET ISO8859-9`) dictionaries are read, so an older Turkish `tr_TR` loads without converting it to UTF-8; the Turkish dictionary is read through the same size-capped reader and `SET` decoding as the others (#3692) | | 2026-10-18 | trivial | ISO 8859-1 (`SET ISO8859-1`) dictionaries are read, so the original VERO `pt_BR` works without converting it to UTF-8 (#2824) | | 2026-10-18 | minor | An installed dictionary that can't be read shows **(dictionary unreadable)** in the status bar, with the file and reason in its tooltip and in File › Options › Proofing (#2830) | From 4ea6fea5129747547233c67d98ea0ab63531e947 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 16:17:34 +0000 Subject: [PATCH 03/15] CP1.3: craft-i18n pinned to its Language data merge (storytold/craft-i18n#106, 5e78e5b) craft-i18n.rev and the flatpak source move from the feat/cp1-language-data commit to the merge on craft-i18n main. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- craft-i18n.rev | 2 +- packaging/linux/flatpak/ai.storyteller.wordcraft.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/craft-i18n.rev b/craft-i18n.rev index 7179c76bd..909b5d7b5 100644 --- a/craft-i18n.rev +++ b/craft-i18n.rev @@ -1 +1 @@ -f84aaa56adf40a595e0d279a6014cf1d07eb0289 +5e78e5b63b87ffd29778e478e916a26c34e0ae88 diff --git a/packaging/linux/flatpak/ai.storyteller.wordcraft.yml b/packaging/linux/flatpak/ai.storyteller.wordcraft.yml index 50ac1beca..f00ba64dc 100644 --- a/packaging/linux/flatpak/ai.storyteller.wordcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.wordcraft.yml @@ -126,6 +126,6 @@ modules: # the two equal). - type: git url: https://github.com/storytold/craft-i18n - commit: f84aaa56adf40a595e0d279a6014cf1d07eb0289 + commit: 5e78e5b63b87ffd29778e478e916a26c34e0ae88 dest: craft-i18n - cargo-sources.json From 449d2c23926f25edea4eaaa4c029886ac6893419 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 17:27:05 +0000 Subject: [PATCH 04/15] CP1.3: craft-i18n pinned to the merge of its Language data review fixes (storytold/craft-i18n#107, 062a637) craft-proof reads hyphenation patterns as language-data//hyphenation.pat, which #107 introduced. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- craft-i18n.rev | 2 +- packaging/linux/flatpak/ai.storyteller.wordcraft.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/craft-i18n.rev b/craft-i18n.rev index 909b5d7b5..2e3a43287 100644 --- a/craft-i18n.rev +++ b/craft-i18n.rev @@ -1 +1 @@ -5e78e5b63b87ffd29778e478e916a26c34e0ae88 +062a6375c322c88b96691a17f7b5fed71c51f7a8 diff --git a/packaging/linux/flatpak/ai.storyteller.wordcraft.yml b/packaging/linux/flatpak/ai.storyteller.wordcraft.yml index f00ba64dc..c3c52b2d0 100644 --- a/packaging/linux/flatpak/ai.storyteller.wordcraft.yml +++ b/packaging/linux/flatpak/ai.storyteller.wordcraft.yml @@ -126,6 +126,6 @@ modules: # the two equal). - type: git url: https://github.com/storytold/craft-i18n - commit: 5e78e5b63b87ffd29778e478e916a26c34e0ae88 + commit: 062a6375c322c88b96691a17f7b5fed71c51f7a8 dest: craft-i18n - cargo-sources.json From d6cb183e6d4d3f9900624bbac9e3c8e810e36ff4 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 17:39:27 +0000 Subject: [PATCH 05/15] CP1.3: craft-proof pinned by git to storytold/craft-libs#482's head (961162a) instead of a local path Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- Cargo.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f8187579..81f3072db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,9 @@ wordcraft-pdf = { path = "crates/pdf" } # Proofing is craft-libs' craft-proof (it was crates/proof); its Language data is craft-i18n's # language-data/, embedded from CRAFT_I18N_DIR (craft-i18n.rev). The package keeps the # `wordcraft-proof` name here so `wordcraft_proof::` paths are unchanged. -# TODO(CP1.3): once storytold/craft-libs merges craft-proof, replace the local path with its own pin -# (separate from the main craft-libs pin): -# wordcraft-proof = { package = "craft-proof", git = "https://github.com/storytold/craft-libs", rev = "" } -wordcraft-proof = { package = "craft-proof", path = "../wt-cp1-libs/crates/proof" } +# Pinned like the other craft-libs crates (each has its own rev). This rev is the head of +# storytold/craft-libs#482, which adds craft-proof; move it to that PR's merge commit once merged. +wordcraft-proof = { package = "craft-proof", git = "https://github.com/storytold/craft-libs", rev = "961162ad15b5992166d0f310763cd09a596772e8", version = "=0.1.0" } wordcraft-engine = { path = "crates/engine" } wordcraft-mcp = { path = "crates/mcp" } wordcraft-zotero = { path = "crates/zotero" } From c967f934ca35415a7f146f04c0098cb5c3b178fa Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 18:16:44 +0000 Subject: [PATCH 06/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20gaps=20and?= =?UTF-8?q?=20localization-parity=20point=20at=20craft-proof=20(craft-libs?= =?UTF-8?q?)=20and=20craft-i18n=20language-data=20instead=20of=20the=20rem?= =?UTF-8?q?oved=20crates/proof=20and=20assets/spelling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- docs/gaps.md | 5 +++-- docs/localization-parity.md | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/gaps.md b/docs/gaps.md index 694aa5923..6a4880eb6 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where WordCraft falls short of Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** trivial (arrowheads keep their kind, width and length and are drawn at Word's sizes; no ranked gap changed); previously trivial (.doc default font from the stylesheet's standard font, #5066; no ranked gap changed); previously trivial (a middle or bottom cell of a row split across pages lines up on the first page; no ranked gap changed); previously trivial (tab leader dots spaced and placed as in Word; no ranked gap changed); previously trivial (a list level above counts its first number at the first deeper item; no ranked gap changed); previously trivial (only the PDF sent to the printer leaves out link annotations; Save these pages as PDF keeps them, #4734; no ranked gap changed); previously trivial (documents with links print: the print job leaves out link annotations, #4734; no ranked gap changed; previously trivial: aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (note conversion reference-run membership uses one ordered walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (gap 19: RTF reads and writes WMF and EMF pictures, #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (View windows: several open documents and Switch Windows in the engine; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; 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: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** trivial (gap 8 points at craft-proof in craft-libs, its data in craft-i18n, #5202; no ranked gap changed); previously trivial (arrowheads keep their kind, width and length and are drawn at Word's sizes; no ranked gap changed); previously trivial (.doc default font from the stylesheet's standard font, #5066; no ranked gap changed); previously trivial (a middle or bottom cell of a row split across pages lines up on the first page; no ranked gap changed); previously trivial (tab leader dots spaced and placed as in Word; no ranked gap changed); previously trivial (a list level above counts its first number at the first deeper item; no ranked gap changed); previously trivial (only the PDF sent to the printer leaves out link annotations; Save these pages as PDF keeps them, #4734; no ranked gap changed); previously trivial (documents with links print: the print job leaves out link annotations, #4734; no ranked gap changed; previously trivial: aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (connector rerouting indexes laid-out object positions once per batch (#3631); previously trivial (custom-property bookmark-link preservation, #4767); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (note conversion reference-run membership uses one ordered walk, #4750); previously trivial (DOCX comment-ID save lookups); previously trivial (gap 19: RTF reads and writes WMF and EMF pictures, #4259); previously trivial (#4658 bounds nested Markdown reference-label work); previously trivial (HTML import keeps preformatted lines with CR or CRLF input newlines, #4810); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (View windows: several open documents and Switch Windows in the engine; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; 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: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Highlight update:** 2026-10-25 · Highlight shortcut toggles the last chosen colour off when the whole selection has it; explicit colour commands remain setters (#4085). Readiness estimates unchanged. > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** trivial (aligned-copy review fixes retain the document axis through scrolling and include secondary selections (#4764)); previously trivial (#15: bibliography sources keep a corporate author, #4848); previously trivial (Table Properties applies to the selected rows, columns and cells, #4857); previously trivial (Relative font-size commands preserve mixed sizes in the selection (#4903); existing button steps and one-point shortcuts are unchanged; readiness estimates unchanged); previously trivial (Arabic Backspace removes one combining mark and keeps its base letter (#4757); previously trivial (Recipient labels continue across product-sized sheets instead of stopping after the first sheet (#4880)); previously trivial (#7: Custom Document Properties dialog, #2982; previously trivial: egui 0.37 migration: the interface catalogs stay shared and pinned by `craft-i18n.rev` at `9205108cfdced5177706264038848b68b2422d2b` (27 languages, English fallback); input-method handling has new regressions but managed CI passed, and native IME acceptance plus native-speaker catalog review remain open; previously trivial: Markdown code spans keep line-broken text literal, #3738; previously trivial: small follow-ups from the field, table and AutoFormat batch, #4232; previously trivial: highlight target includes selected paragraph marks, #4085; previously trivial: full-target highlight check beyond 10,000 runs, #4085; previously trivial: 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: a command that changes nothing is not an edit: no undo step, no unsaved changes, no relayout, and the redo stack survives it; previously minor: #8 narrowed: Spanish proofing (Spain, Mexico, Latin America, Uruguay) from the installed Hunspell dictionaries, #3360, #3553; previously trivial: gap 10: group connector rerouting preserves transformed free and unresolved endpoints under nonuniform scaling; obstacle routing still missing; readiness estimates unchanged; previously trivial: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: the ranked gap list held two rows each for #19, #20 and #21; each gap appears once now; a document with many relationships opens in the time its part takes, #3411; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: text mini toolbar available with the right-click menu, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: tracked table/row/cell/section property changes record in WordCraft, narrowing gap 5; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: Format Page Numbers dialog carries Word's number formats and Continue from previous section / Start at, #1113; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: fixed font inputs in four layout controls; 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: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously trivial: #8: English dialects — en-GB, en-CA, en-AU… flag each other's spellings, #1281; previously trivial: #1: shape gradients and transparency, #1500; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: line breaking: a slash opening a word travels whole to the next line, #2963; previously trivial: recipient list search and merged-only view, #1576; previously trivial: #2 impact restated: only ink rendering and OLE editing remain; previously trivial: #2 narrowed: native ink nested in groups and drawing canvases kept on save; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: automatic Asian/Latin and Asian/number spacing laid out, #2358; previously trivial: hint-reliant CJK outlines; previously trivial: numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: #24: the Spike, Ctrl+F3 and Ctrl+Shift+F3; previously trivial: VML freeforms read and kept on save; previously trivial: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously trivial: sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 @@ -72,7 +72,7 @@ Text context menus now include the existing formatting mini toolbar (#2959), inc | 6 | **Print dialog depth**: desktop printer choice, copies, collation, duplex and grayscale submit through craft-print; File › Print retains preview, page ranges and odd/even (#641) and scales pages to another paper size (#2108); driver tray/options and pages per sheet remain; web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) | | 7 | **Dialog depth**: 61 modal dialogs vs Word's ~100 (Custom Document Properties from File › Info, #2982; Drop Cap Options matches Word's menu and dialog; Split Cells and Convert Text to Table match Word's observed limits and defaults; Format Page Numbers carries Word's number formats and Continue from previous section / Start at, #1113) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs`; Cell Margins (#1960) now opens with current defaults and applies only confirmed edits | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) | | 9 | **Password-protected `.doc` files with an AES header in the Word 97-2003 format** can't be opened: [MS-OFFCRYPTO] defines AES only for encrypted packages, so these are refused with a clear message. Encrypted packages open under any Word extension (`.docx`, `.doc`, `.dot`…) with either agile (Word 2010+) or Standard (Word 2007, AES-ECB) encryption; protected `.docx` saves with agile encryption (#55, #281, gap 9) | | `crates/docx/src/crypt.rs` (agile and Standard); `crates/docbin/src/crypt.rs` opens XOR/RC4/RC4 CryptoAPI `.doc` with a password (#1079). All synthetic fixtures; untested on Word-written files | Corporate and legal users with old files | 0 (refused by design; nothing left to build) | [file-format-parity.md](file-format-parity.md) | -| 8 | **Proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish only**; no dictionaries or grammar for the other ~41 languages Word proofs (text in them is left unmarked), and no hyphenation for them except Bulgarian and German | | `crates/proof` `ProofLang::PROOFED`: English (American, British-spelling and Canadian, each flagging the other's spellings, #1281), Polish, Portuguese (pt-BR via the installed VERO dictionary, #797), Italian (it-IT via the installed Hunspell dictionary), Croatian (hr-HR, 2026-10-13), Turkish (tr_TR spelling, grammar, TDK hyphenation, 2026-10-13) Russian (the installed `ru_RU` dictionary, 2026-10-12) Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16) and Spanish (es-ES, es-MX, es-419 and es-UY via the installed RLA-ES dictionaries, Uruguayan `vos` forms, #3360, #3553, 2026-10-22), a language per run; Bulgarian (#826) and German (TeX patterns, 2026-10-14) have hyphenation only; open PRs add Danish (#723), Russian (#894) and Czech (#1048); Word ships 120 proofing tools; issues #25, #40, #100 open | Every writer outside English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish | 20–40 | [localization-parity.md](localization-parity.md) | +| 8 | **Proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish only**; no dictionaries or grammar for the other ~41 languages Word proofs (text in them is left unmarked), and no hyphenation for them except Bulgarian and German | | craft-proof (storytold/craft-libs `crates/proof`, data in craft-i18n `language-data/`) `ProofLang::PROOFED`: English (American, British-spelling and Canadian, each flagging the other's spellings, #1281), Polish, Portuguese (pt-BR via the installed VERO dictionary, #797), Italian (it-IT via the installed Hunspell dictionary), Croatian (hr-HR, 2026-10-13), Turkish (tr_TR spelling, grammar, TDK hyphenation, 2026-10-13) Russian (the installed `ru_RU` dictionary, 2026-10-12) Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16) and Spanish (es-ES, es-MX, es-419 and es-UY via the installed RLA-ES dictionaries, Uruguayan `vos` forms, #3360, #3553, 2026-10-22), a language per run; Bulgarian (#826) and German (TeX patterns, 2026-10-14) have hyphenation only; open PRs add Danish (#723), Russian (#894) and Czech (#1048); Word ships 120 proofing tools; issues #25, #40, #100 open | Every writer outside English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish | 20–40 | [localization-parity.md](localization-parity.md) | | 10 | **Objects**: text in a freely rotated text box (free rotation and flips landed, #332; live rotation preview while dragging fixed for pictures and shapes without text stories, #2277; text direction, alignment and linked text boxes landed, #369; contour wrap and wrap side landed, #354), shape effects, WordArt 3-D effects and connector routing around shapes (WordArt effects and warps and glued connectors landed, #416), dragging members inside a Drawing Canvas (the canvas landed, #344) | | [parity-checklist.md](parity-checklist.md) (Shape Format 11/11, Layout 22/22); group and ungroup landed (#267) | Newsletters, flyers, reports with diagrams | 20–30 | [target-app-parity.md](target-app-parity.md) | | 11 | **Interface languages**: 3 of the 12 key languages missing (hi, ar, id) and pt-PT (French landed, #729; Vietnamese, #1434; Korean, #1216; 21 interface languages in `crates/ui-egui/src/i18n`); open PRs for Arabic (#737, #1042, #1032), Indonesian (#572) and pt-PT (#1028); no mirrored RTL interface yet; ~5% of strings still English in the translated catalogs | | [localization-parity.md](localization-parity.md) | Most of the world's writers | 50–80 | [localization-parity.md](localization-parity.md) | | 12 | **Draw tab / ink**: pens, pencil, highlighter, eraser, Select and Hide Ink landed (#307); Lasso Select, Add Pen, Ink to Shape and Ink Replay landed (#395); Ink to Shape also recognises single-stroke arrows (a line plus a V head drawn in one stroke becomes a straight connector with an arrowhead); two-stroke arrows (a line and a separate V or closed-triangle head) and one-stroke arrows with a closed triangle head landed (#1778); missing: Ink to Math, pressure, rendering/editing Word's own ink (drawing-level and direct run-level InkML content parts now survive DOCX save) | | [parity-checklist.md](parity-checklist.md) (Draw 10/11) | Pen and tablet users | 4–8 | [hardware-parity.md](hardware-parity.md) | @@ -152,6 +152,7 @@ Recipient label generation (#4880) now includes all selected recipients across a | Date | Change | Summary | |---|---|---| +| 2026-11-01 | trivial | Gap 8's evidence names craft-proof (storytold/craft-libs `crates/proof`) and craft-i18n `language-data/` instead of WordCraft's removed `crates/proof` (#5202). No ranked gap changed. | | 2026-11-01 | trivial | Arrowheads keep their kind, width and length (DrawingML `a:headEnd`/`a:tailEnd` `type`/`w`/`len`, VML `startarrow`/`endarrow` with `…arrowwidth`/`…arrowlength`), read and written back, and are drawn 2, 3 or 5 times the line's width long and wide (small, medium, large), a line under 2 points as a 2-point one, as measured in Word for Windows; every kind is still drawn as a triangle; no ranked gap changed; readiness estimates unchanged | | 2026-10-31 | trivial | The .doc reader's default font is the stylesheet's standard font, not the font table's first entry (#5066); no ranked gap changed; readiness estimates unchanged | | 2026-11-01 | trivial | A middle or bottom cell of a row split across pages lines up on the first page; no ranked gap changed; readiness estimates unchanged | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 200498eb5..f6e5b9681 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** trivial (word count counts each Han ideograph and kana as a word, #4547; previously trivial: the Traditional Chinese interface draws hanzi to the Taiwan standard with an installed Traditional Chinese font first: PingFang TC, Microsoft JhengHei, Noto Sans CJK TC; previously minor: the egui 0.37 migration preserves the equation “Delete Around Caret in Equation” translation in all 27 catalogs through the shared catalog pin; previously minor: Spanish proofing: es-ES, es-MX, es-419 and es-UY from the installed Hunspell dictionaries, Uruguayan `vos` forms, opening `¿`/`¡` checks, #3360, #3553; previously minor: the interface catalogs moved to storytold/craft-i18n and are looked up through the `craft-i18n` crate, embedded at build time from the commit in `craft-i18n.rev`; previously trivial: interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously trivial: interface audit: missing catalog entries are reported per language and don't fail the audit; previously minor: disabled-control reasons in ribbon hints translated in every catalog, #2678; previously trivial: Slovenian interface catalog: every key of every other catalog, 2,614 entries; previously minor: Romanian interface catalog; previously trivial: the File › Home greeting is translated in every catalog; previously trivial: Japanese row: ruby (Phonetic Guide) is supported and HTML export/import keeps it, #1681; previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously minor: English dialects: en-GB, en-CA, en-AU and the other British-spelling Englishes proof their own spellings and flag the other dialect's, #1281, #1045; previously minor: how to install the Hunspell dictionaries on Windows, macOS and Linux: `docs/spelling-dictionaries.md`; previously minor: Read Aloud speaks each sentence in its own language, #1843; previously minor: Indonesian proofing: id-ID spelling from the installed Hunspell dictionary, dictionary-driven suggestions, generic checks with Indonesian messages, #498; previously trivial: print-area strings translated in every catalog; previously trivial: Italian: the interface audit checks the Italian catalog, `Privacy Settings` wording; previously trivial: Italian grammar: `po'`, `qual è`, `È`, `un`/`un'` rules; more elisions and truncations in spelling; previously trivial: Czech interface catalog completed, 2,447 entries; previously trivial: interface audit runs in CI again: size labels translated, 20 strings in every catalog, Read Aloud error race fixed, #1128; previously trivial: Review › Language reaches every language again — Set Proofing Language… and Set As Default, #860; previously minor: Italian proofing: it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar with Italian messages; previously minor: Portuguese proofing; previously trivial: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Hungarian interface added; previously trivial: sorting and table numbers follow the text's language, #836, #837; previously minor: Polish interface; Polish proofing — spelling, grammar, hyphenation — and a proofing language per run; previously minor: Turkish interface added; previously major: first version; catalogs measured from `crates/ui-egui/src/i18n`, Word's languages from its bundle listing) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** trivial (proofing file paths point at craft-proof in craft-libs and craft-i18n `language-data/`, #5202); previously trivial (word count counts each Han ideograph and kana as a word, #4547; previously trivial: the Traditional Chinese interface draws hanzi to the Taiwan standard with an installed Traditional Chinese font first: PingFang TC, Microsoft JhengHei, Noto Sans CJK TC; previously minor: the egui 0.37 migration preserves the equation “Delete Around Caret in Equation” translation in all 27 catalogs through the shared catalog pin; previously minor: Spanish proofing: es-ES, es-MX, es-419 and es-UY from the installed Hunspell dictionaries, Uruguayan `vos` forms, opening `¿`/`¡` checks, #3360, #3553; previously minor: the interface catalogs moved to storytold/craft-i18n and are looked up through the `craft-i18n` crate, embedded at build time from the commit in `craft-i18n.rev`; previously trivial: interface-language tests: a missing translation never fails a test, `cargo xtask i18n-report` in `cargo xtask ci` prints the strings each language lacks, and lookup and template tests use inline fixture catalogs or the pseudo-language; previously trivial: interface audit: missing catalog entries are reported per language and don't fail the audit; previously minor: disabled-control reasons in ribbon hints translated in every catalog, #2678; previously trivial: Slovenian interface catalog: every key of every other catalog, 2,614 entries; previously minor: Romanian interface catalog; previously trivial: the File › Home greeting is translated in every catalog; previously trivial: Japanese row: ruby (Phonetic Guide) is supported and HTML export/import keeps it, #1681; previously trivial: Russian sorting keeps ё with е and recognises decomposed ё/й; previously minor: English dialects: en-GB, en-CA, en-AU and the other British-spelling Englishes proof their own spellings and flag the other dialect's, #1281, #1045; previously minor: how to install the Hunspell dictionaries on Windows, macOS and Linux: `docs/spelling-dictionaries.md`; previously minor: Read Aloud speaks each sentence in its own language, #1843; previously minor: Indonesian proofing: id-ID spelling from the installed Hunspell dictionary, dictionary-driven suggestions, generic checks with Indonesian messages, #498; previously trivial: print-area strings translated in every catalog; previously trivial: Italian: the interface audit checks the Italian catalog, `Privacy Settings` wording; previously trivial: Italian grammar: `po'`, `qual è`, `È`, `un`/`un'` rules; more elisions and truncations in spelling; previously trivial: Czech interface catalog completed, 2,447 entries; previously trivial: interface audit runs in CI again: size labels translated, 20 strings in every catalog, Read Aloud error race fixed, #1128; previously trivial: Review › Language reaches every language again — Set Proofing Language… and Set As Default, #860; previously minor: Italian proofing: it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar with Italian messages; previously minor: Portuguese proofing; previously trivial: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Hungarian interface added; previously trivial: sorting and table numbers follow the text's language, #836, #837; previously minor: Polish interface; Polish proofing — spelling, grammar, hyphenation — and a proofing language per run; previously minor: Turkish interface added; previously major: first version; catalogs measured from `crates/ui-egui/src/i18n`, Word's languages from its bundle listing) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 Interface languages, script support and proofing per language. How translation works (adding a language, the embedded catalogs): `crates/ui-egui/src/i18n.rs`. The catalogs, their format and @@ -128,9 +128,9 @@ Set Proofing Language › Set As Default keeps the chosen language in user prefe | Language | Spelling | Suggestions | Grammar | Hyphenation | |---|---|---|---|---| -| English: American (`en-US`, other regions, text with no language), British spelling (`en-GB`, `en-AU`, `en-NZ`, `en-IE`, `en-ZA`, `en-IN`…), Canadian (`en-CA`) | Moby word list (public domain, ~160k words) with inflection rules, plus our list of ~280 American/British pairs (`assets/spelling/en-variants.txt`) with inflections by rule: each English flags the other dialect's spellings (`colour` in en-US; `color`, `center`, `analyze` in en-GB; `organise` in en-CA), British English accepts `-ize` and `-ise` (Oxford spelling), and a dialect's form of a known word is right even if the list lacks it (`realisation`). No system dictionary needed | the dialect's own spelling first (`colour` → `color`), then edit distance with the shared suggestions in the dialect's spelling | repeated word, `a`/`an`, sentence capital, space before punctuation, double space | Moby dictionary, our Liang patterns, heuristic | -| Russian (`ru`) | A Russian Hunspell dictionary installed on the system (`ru_RU` from the OS, a package manager, LibreOffice's `dict-ru` or the `WORDCRAFT_DICTIONARIES` folder; UTF-8, KOI8-R or Windows-1251; not bundled, so no Russian spelling marks without it), read by the same affix engine; `ё` optional (`елка`/`ёлка`, `еще`/`ещё`); hyphenated words whole, then by parts (`кто-то`, `по-русски`, `кое-где`, `северо-западный`, `ИИ-консультант`); solid compounds (`высокоэластичный`), `не` with adjectives, participles and verbal nouns (not verbs), productive prefixes (`ультра-`, `эко-`) and prefixed verbs; abbreviations with endings (`ГИПа`) and camel case left alone; our supplement of ~200 modern words with their declensions (`assets/spelling/ru-extra.txt`: `маркетплейс`, `промокод`, `интеграция`, `колеровать`) | `ё`/`е` first, then slips within letter groups (unstressed vowels, voiced and voiceless pairs, `ь`/`ъ`), the common confusions (`жы`, `чя`, `тся`/`ться`, doubled consonants, unpronounced consonants: `здраствуй` → `здравствуй`), a missing space (`незнаю` → `не знаю`; not a preposition before a verb), one typo, then a stem scan | Russian messages: repeated word, space before punctuation, double space, sentence capital (knows `т. е.`, `г.`, `руб.`, `ул.`, initials, numbers, dialogue dashes), possibly missing comma before `что`/`чтобы`/`если`/`хотя`/`поскольку`/`который` and before `потому что` (not after conjunctions and particles such as `и`, `только`, `даже`, `так`, `для того`; not after a preposition; not in `хотя бы`, `что-то`), doubled punctuation | The rules of Russian word division as code (no pattern file): a vowel on each side, two letters at least, `ь`/`ъ`/`й` kept with the letter before, a break after `ъ`, a consonant cluster divided after its first consonant | -| Polish (`pl`) | The Hunspell `pl_PL` dictionary installed on the system (OS package, Homebrew, LibreOffice, the folder set in File › Options › Proofing › Dictionaries folder, or `WORDCRAFT_DICTIONARIES`; not bundled, so no Polish spelling marks without it; the same pane shows which file each language found), read at runtime by a Hunspell-style affix engine (`crates/proof/src/affix.rs`); names capitalized, compounds part by part, `Kennedy'ego` | missing diacritics first (`zolw` → `żółw`), the dictionary's confusion table (`żeka` → `rzeka`, `chuśtawka` → `huśtawka`), missing space (`napewno` → `na pewno`), one typo, then a stem scan within two typos | Polish messages: repeated word, space before punctuation, double space, sentence capital (knows `np.`, `r.`, initials, ordinals, dialogue dashes), missing comma before `że`/`który`/`ponieważ`/`gdyż`/`aby`/`żeby`/`jeśli`/`jeżeli`/`gdyby`/`lecz` (not after `mimo`, `chyba`, `tak`, prepositions…), `we`/`ze` (`we wtorek`, `ze szkoły`), doubled punctuation | TeX Polish patterns (Kołodziejska, Jackowski, Ryćko; MIT option) with their 20 exception words; a Polish syllable heuristic for letters outside the alphabet | +| English: American (`en-US`, other regions, text with no language), British spelling (`en-GB`, `en-AU`, `en-NZ`, `en-IE`, `en-ZA`, `en-IN`…), Canadian (`en-CA`) | Moby word list (public domain, ~160k words) with inflection rules, plus our list of ~280 American/British pairs (craft-i18n [`language-data/en-us/en-variants.txt`](https://github.com/storytold/craft-i18n/blob/main/language-data/en-us/en-variants.txt)) with inflections by rule: each English flags the other dialect's spellings (`colour` in en-US; `color`, `center`, `analyze` in en-GB; `organise` in en-CA), British English accepts `-ize` and `-ise` (Oxford spelling), and a dialect's form of a known word is right even if the list lacks it (`realisation`). No system dictionary needed | the dialect's own spelling first (`colour` → `color`), then edit distance with the shared suggestions in the dialect's spelling | repeated word, `a`/`an`, sentence capital, space before punctuation, double space | Moby dictionary, our Liang patterns, heuristic | +| Russian (`ru`) | A Russian Hunspell dictionary installed on the system (`ru_RU` from the OS, a package manager, LibreOffice's `dict-ru` or the `WORDCRAFT_DICTIONARIES` folder; UTF-8, KOI8-R or Windows-1251; not bundled, so no Russian spelling marks without it), read by the same affix engine; `ё` optional (`елка`/`ёлка`, `еще`/`ещё`); hyphenated words whole, then by parts (`кто-то`, `по-русски`, `кое-где`, `северо-западный`, `ИИ-консультант`); solid compounds (`высокоэластичный`), `не` with adjectives, participles and verbal nouns (not verbs), productive prefixes (`ультра-`, `эко-`) and prefixed verbs; abbreviations with endings (`ГИПа`) and camel case left alone; our supplement of ~200 modern words with their declensions (craft-i18n [`language-data/ru/ru-extra.txt`](https://github.com/storytold/craft-i18n/blob/main/language-data/ru/ru-extra.txt): `маркетплейс`, `промокод`, `интеграция`, `колеровать`) | `ё`/`е` first, then slips within letter groups (unstressed vowels, voiced and voiceless pairs, `ь`/`ъ`), the common confusions (`жы`, `чя`, `тся`/`ться`, doubled consonants, unpronounced consonants: `здраствуй` → `здравствуй`), a missing space (`незнаю` → `не знаю`; not a preposition before a verb), one typo, then a stem scan | Russian messages: repeated word, space before punctuation, double space, sentence capital (knows `т. е.`, `г.`, `руб.`, `ул.`, initials, numbers, dialogue dashes), possibly missing comma before `что`/`чтобы`/`если`/`хотя`/`поскольку`/`который` and before `потому что` (not after conjunctions and particles such as `и`, `только`, `даже`, `так`, `для того`; not after a preposition; not in `хотя бы`, `что-то`), doubled punctuation | The rules of Russian word division as code (no pattern file): a vowel on each side, two letters at least, `ь`/`ъ`/`й` kept with the letter before, a break after `ъ`, a consonant cluster divided after its first consonant | +| Polish (`pl`) | The Hunspell `pl_PL` dictionary installed on the system (OS package, Homebrew, LibreOffice, the folder set in File › Options › Proofing › Dictionaries folder, or `WORDCRAFT_DICTIONARIES`; not bundled, so no Polish spelling marks without it; the same pane shows which file each language found), read at runtime by a Hunspell-style affix engine (craft-proof [`src/affix.rs`](https://github.com/storytold/craft-libs/blob/main/crates/proof/src/affix.rs) in craft-libs); names capitalized, compounds part by part, `Kennedy'ego` | missing diacritics first (`zolw` → `żółw`), the dictionary's confusion table (`żeka` → `rzeka`, `chuśtawka` → `huśtawka`), missing space (`napewno` → `na pewno`), one typo, then a stem scan within two typos | Polish messages: repeated word, space before punctuation, double space, sentence capital (knows `np.`, `r.`, initials, ordinals, dialogue dashes), missing comma before `że`/`który`/`ponieważ`/`gdyż`/`aby`/`żeby`/`jeśli`/`jeżeli`/`gdyby`/`lecz` (not after `mimo`, `chyba`, `tak`, prepositions…), `we`/`ze` (`we wtorek`, `ze szkoły`), doubled punctuation | TeX Polish patterns (Kołodziejska, Jackowski, Ryćko; MIT option) with their 20 exception words; a Polish syllable heuristic for letters outside the alphabet | | Portuguese (`pt`, any region) | The Hunspell `pt_BR` dictionary installed on the system — VERO and its derivatives (same places as Polish; not bundled, LGPL/MPL), read at runtime by the same affix engine, which reads its `FLAG UTF-8`, 106 flags, continuation classes and `FORBIDDENWORD` spellings; names capitalized, compounds part by part (`guarda-roupa`, `ex-marido`) | missing accents first (`coraçao` → `coração`, `saude` → `saúde`), the dictionary's confusion table (`seje` → `seja`, `excessão` → `exceção`), missing space (`nãoé` → `não é`), one typo, then an accent-folding stem scan | Portuguese messages: repeated word (the emphatic doubles are allowed), space before punctuation, double space, sentence capital (knows `etc.`, `pág.`, `Dr.`, initials, ordinals, dialogue dashes), doubled punctuation; no comma rule — `que` opens a clause only sometimes | TeX Portuguese patterns (Rezende, Almeida, Araujo, Benevides; BSD 3-clause) with their 2 exception words | | Czech (`cs`, `cs-CZ`, `ces`, `cze`) | The system Hunspell `cs_CZ` dictionary, read at runtime (Debian/Ubuntu: `hunspell-cs`; the same system, LibreOffice and `WORDCRAFT_DICTIONARIES` search paths as Polish). No dictionary data is bundled; without an installed dictionary spelling stays off | suggestions from the loaded dictionary's MAP/REP/TRY/KEY tables, missing spaces and a stem scan; short-word splits rank after deleting an extra letter; NOSUGGEST stems and their derived forms are accepted when typed but excluded from suggestions | Czech messages (including Czech quotation marks) for repeated words, space before punctuation and double spaces only; digit-space-colon-space-digit ratios keep their spaces; no Czech-specific grammar rules | existing English fallback, as for `Other`; no Czech patterns bundled (the TeX/LibreOffice Czech patterns are GPL-2) | | Italian (`it`, any region) | The Hunspell `it_IT` dictionary installed on the system (same places as Polish and Portuguese; not bundled — the upstream file is GPL-licensed), read at runtime by the same affix engine; elided forms check the word after the apostrophe (`l'amico`, `un'idea`, `c'è`, `dov'è`, `senz'altro`), truncations keep their apostrophe (`po'`, `mo'`), hyphenated compounds part by part | missing accents first (`caffe` → `caffè`, `perche` → `perché`), the dictionary's confusion table, missing space (`perme` → `per me`), one typo, then an accent-folding stem scan | Italian messages: repeated word (emphatic doubles like `piano piano` are allowed), space before punctuation, double space, sentence capital (knows `ecc.`, `dott.`, `prof.`, initials, ordinals), doubled punctuation, `un pò` → `un po'`, `qual'è` → `qual è`, `E'` → `È`, `un'altro` → `un altro` and `un altra` → `un'altra` (gender from the ending, masculine `-ista`/`-ma`/`-cida`… left alone); these work without a dictionary; no comma rule | TeX Italian patterns (Beccari; MIT option), no exception words | @@ -227,6 +227,7 @@ WordCraft keeps 2,54 cm). | Date | Change | Summary | |---|---|---| +| 2026-11-01 | trivial | Proofing paths updated for the move: the affix engine is craft-proof's `src/affix.rs` in storytold/craft-libs, the English variants and Russian supplement are craft-i18n `language-data/` files (#5202). | | 2026-10-28 | trivial | Traditional Chinese interface: an installed Traditional Chinese font (PingFang TC, Microsoft JhengHei, Noto Sans CJK TC; the TC face of a pan-CJK collection) goes before the embedded Noto Sans CJK SC, whose mainland glyph shapes looked wrong to Taiwan readers; native only, wasm unchanged; estimates unchanged | | 2026-10-24 | minor | Preserved “Delete Around Caret in Equation” in all 27 shared WordCraft catalogs; the native/release pin and Flatpak source advance together. | | 2026-10-23 | minor | Interface catalogs moved to storytold/craft-i18n (`locales//wordcraft.tsv`), looked up through craft-libs' `craft-i18n` crate and embedded from the pinned commit (`craft-i18n.rev`); strings other Crafting Apps already translate in the shared catalog now show translated; the "Open" paragraph-spacing preset no longer shows the file verb | @@ -280,7 +281,7 @@ WordCraft keeps 2,54 cm). | 2026-10-11 | minor | Hungarian interface catalog (2,419 entries, built-in style names shown translated; covers everything the interface audit finds), locale selection, saved preference and font coverage checks | | 2026-10-11 | minor | Turkish interface catalog (1,307 entries), locale selection, saved preference, Turkish casing in command search and font coverage checks; catalog sizes re-counted | | 2026-10-17 | trivial | English spelling (#1694): `-ically` adverbs come from their `-ic` adjective ("specifically", "scientifically"), so they're no longer flagged; other words don't take `-ally` ("desktopally" is still flagged) | -| 2026-10-11 | trivial | English spelling (#520): our supplement `assets/spelling/en-extra.txt` adds ~220 common modern words the Moby list lacks ("desktop", "download", "email"…) | +| 2026-10-11 | trivial | English spelling (#520): our supplement `assets/spelling/en-extra.txt` (since CP1.3 craft-i18n `language-data/en-us/en-extra.txt`) adds ~220 common modern words the Moby list lacks ("desktop", "download", "email"…) | | 2026-10-10 | minor | Estonian interface catalog, locale selection, saved preference and font coverage checks; count labels put the noun first so they read right at 1 | | 2026-10-10 | minor | Serbian (Cyrillic and Latin) interface merged (#250) | | 2026-10-10 | major | First version: twelve-language table, catalog coverage measured, Word's 30 UI languages and 120 proofing tools listed | From 8c3a792b4989762789f2de8f99324732d1be4762 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 18:38:55 +0000 Subject: [PATCH 07/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20engine=20te?= =?UTF-8?q?sts=20that=20need=20the=20Language=20data=20skip=20with=20a=20n?= =?UTF-8?q?ote=20naming=20CRAFT=5FI18N=5FDIR=20when=20none=20is=20embedded?= =?UTF-8?q?,=20as=20craft-proof's=20do?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo test -p wordcraft-engine now passes without CRAFT_I18N_DIR (10 tests return early) and still runs them all with it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- crates/engine/src/cmd/page/tests.rs | 3 +++ crates/engine/src/tests.rs | 33 +++++++++++++++++++++++++++++ crates/engine/src/tests/typing.rs | 6 ++++++ 3 files changed, 42 insertions(+) diff --git a/crates/engine/src/cmd/page/tests.rs b/crates/engine/src/cmd/page/tests.rs index 31ca0d8a5..78a1c7c10 100644 --- a/crates/engine/src/cmd/page/tests.rs +++ b/crates/engine/src/cmd/page/tests.rs @@ -374,6 +374,9 @@ fn line_numbering_options() { /// there breaks the word. #[test] fn hyphenation_options_and_manual_hyphenation() { + if crate::tests::no_language_data("hyphenation_options_and_manual_hyphenation") { + return; + } let mut s = Session::new(wordcraft_doc::Document::from_text(&"a extraordinarily be uncharacteristic of responsibilities the considerations ".repeat(12))); let r = s.run("layout.hyphenation", &json!({"zone": 36, "caps": false, "limit": 3})).unwrap(); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 3d60ff070..ef8d4b93d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -18,6 +18,18 @@ mod revision_checks; mod typing; mod wordart; +/// True, after saying so on stderr, when this build has no Language data embedded (craft-proof +/// built without `CRAFT_I18N_DIR`): no Moby word list, no hyphenation patterns. Tests that check +/// spelling marks or hyphenation from that data return early then, as craft-proof's own do; with +/// the data (CI, releases) they all run. +pub(crate) fn no_language_data(test: &str) -> bool { + let missing = wordcraft_proof::english_source(wordcraft_proof::ProofLang::En) != Some(wordcraft_proof::EnglishSource::Embedded); + if missing { + eprintln!("{test}: skipped, needs the Language data (set CRAFT_I18N_DIR to a craft-i18n checkout)"); + } + missing +} + fn s() -> Session { Session::new(wordcraft_doc::Document::new()) } @@ -243,6 +255,9 @@ fn reset_shortcuts_remove_direct_formatting_but_keep_styles() { #[test] fn custom_dictionary_words_persist_through_their_file() { + if no_language_data("custom_dictionary_words_persist_through_their_file") { + return; + } // #40: adding a word through a command writes it to the session's file, and loading // that file makes the word known (only the desktop app wires this up). let dir = std::env::temp_dir().join(format!("wc-engdict-{}", std::process::id())); @@ -8569,6 +8584,9 @@ fn dictionaries_folder_option_is_saved_searched_and_reported() { /// grammar, suggestions come from it, and a suggestion fixes the text. #[test] fn portuguese_proofing_through_the_review_commands() { + if no_language_data("portuguese_proofing_through_the_review_commands") { + return; + } portuguese_test_dictionary(); let mut s = s(); run(&mut s, "document.setText", json!({"text": "Saudade coraçao, e ele ele disse."})); @@ -8665,6 +8683,9 @@ fn pairs(v: &[(&str, &str)]) -> Vec<(String, String)> { /// it and Change fixes it. #[test] fn english_dialects_through_the_review_commands() { + if no_language_data("english_dialects_through_the_review_commands") { + return; + } let mut s = s(); run(&mut s, "document.setText", json!({"text": "The colour of the center."})); // US English by default: the British spelling is flagged, the American one isn't. @@ -8698,6 +8719,9 @@ fn english_dialects_through_the_review_commands() { /// and is proofed the same way when it comes back. #[test] fn english_dialects_survive_a_docx_round_trip() { + if no_language_data("english_dialects_survive_a_docx_round_trip") { + return; + } use std::io::Read; let mut s = s(); run(&mut s, "document.setText", json!({"text": "Colour and color. Centre and center."})); @@ -8726,6 +8750,9 @@ fn english_dialects_survive_a_docx_round_trip() { /// system, #1045); another region's language leaves US English. #[test] fn new_documents_follow_the_regions_english() { + if no_language_data("new_documents_follow_the_regions_english") { + return; + } let mut s = s(); s.system_region = wordcraft_geom::Region::from_tag("en_GB.UTF-8"); assert_eq!(s.region_english().as_deref(), Some("en-GB")); @@ -8862,6 +8889,9 @@ fn uruguayan_spanish_accepts_vos_forms() { /// Czech proofing through the same commands as Review, F7, Change and Add to Dictionary. #[test] fn czech_proofing_through_the_review_commands() { + if no_language_data("czech_proofing_through_the_review_commands") { + return; + } czech_test_dict::install(); let mut s = s(); run(&mut s, "document.setText", json!({"text": "Město pocitac, škola škola."})); @@ -8944,6 +8974,9 @@ fn italian_proofing_through_the_review_commands() { #[test] fn polish_proofing_through_the_review_commands() { + if no_language_data("polish_proofing_through_the_review_commands") { + return; + } polish_test_dictionary(); let mut s = s(); run(&mut s, "document.setText", json!({"text": "Płynie żeka, a wogóle to wiem że tak."})); diff --git a/crates/engine/src/tests/typing.rs b/crates/engine/src/tests/typing.rs index 7d0c42164..dc330293a 100644 --- a/crates/engine/src/tests/typing.rs +++ b/crates/engine/src/tests/typing.rs @@ -2906,6 +2906,9 @@ fn moving_the_caret_out_of_the_typed_word_marks_it() { #[test] fn a_click_elsewhere_marks_the_typed_word_and_a_click_back_keeps_the_mark() { + if crate::tests::no_language_data("a_click_elsewhere_marks_the_typed_word_and_a_click_back_keeps_the_mark") { + return; + } let mut s = typed("Fine\nThe helo"); assert_eq!(squiggles(&mut s, 1), Vec::::new()); // The canvas sets the caret directly on a click. @@ -2923,6 +2926,9 @@ fn a_click_elsewhere_marks_the_typed_word_and_a_click_back_keeps_the_mark() { #[test] fn backspace_into_a_finished_word_edits_it_again() { + if crate::tests::no_language_data("backspace_into_a_finished_word_edits_it_again") { + return; + } let mut s = typed("The helo "); assert_eq!(squiggles(&mut s, 0), ["helo"]); typ(&mut s, "\u{8}"); From 3181e502c54d92318eb443f65748e952ffe7500a Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 18:38:55 +0000 Subject: [PATCH 08/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20the=20Print?= =?UTF-8?q?ing=20workflow=20checks=20out=20craft-i18n=20before=20its=20app?= =?UTF-8?q?=20and=20UI=20tests,=20as=20the=20gate=20does?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- .github/workflows/printing.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/printing.yml b/.github/workflows/printing.yml index e22a4b368..ba843344e 100644 --- a/.github/workflows/printing.yml +++ b/.github/workflows/printing.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - 'Cargo*' + - 'craft-i18n.rev' - 'apps/wordcraft/**' - 'apps/wordcraft-web/**' - 'crates/ui-egui/**' @@ -14,6 +15,7 @@ on: branches: [main] paths: - 'Cargo*' + - 'craft-i18n.rev' - 'apps/wordcraft/**' - 'apps/wordcraft-web/**' - 'crates/ui-egui/**' @@ -42,6 +44,11 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # The interface catalogs and craft-proof's Language data come from craft-i18n at the commit in + # craft-i18n.rev, so these tests build with the same data as the gate (ci.yml). + - name: Check out craft-i18n + uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main + - name: Install Linux build and software-rendering dependencies if: runner.os == 'Linux' run: | From 452d38da0ba18d3fdebb076fcee9d2fc1cb81b98 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 18:47:50 +0000 Subject: [PATCH 09/15] CP1.3: pin craft-proof 48ed6f8 storytold/craft-libs#482's new head: without Language data English spells from the built-in Moby list plus en-extra with edit-1 suggestions, and a system English Hunspell loads leniently. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 81f3072db..caa50c2d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ wordcraft-pdf = { path = "crates/pdf" } # `wordcraft-proof` name here so `wordcraft_proof::` paths are unchanged. # Pinned like the other craft-libs crates (each has its own rev). This rev is the head of # storytold/craft-libs#482, which adds craft-proof; move it to that PR's merge commit once merged. -wordcraft-proof = { package = "craft-proof", git = "https://github.com/storytold/craft-libs", rev = "961162ad15b5992166d0f310763cd09a596772e8", version = "=0.1.0" } +wordcraft-proof = { package = "craft-proof", git = "https://github.com/storytold/craft-libs", rev = "48ed6f8cbd4104225a9ecde122d9fb8d1353b5a6", version = "=0.1.0" } wordcraft-engine = { path = "crates/engine" } wordcraft-mcp = { path = "crates/mcp" } wordcraft-zotero = { path = "crates/zotero" } From 9d5b98eb1872156ea65673d828babe2877956480 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 18:47:50 +0000 Subject: [PATCH 10/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20six=20engin?= =?UTF-8?q?e=20tests=20run=20again=20without=20the=20Language=20data=20now?= =?UTF-8?q?=20that=20craft-proof's=20built-in=20English=20is=20the=20Moby?= =?UTF-8?q?=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polish/Portuguese/Czech review commands, the custom dictionary file and the two typing squiggle tests pass without CRAFT_I18N_DIR at 48ed6f8. English dialects, regions' English and Hyphenation Options still need the variant table and patterns, so they keep the skip. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- crates/engine/src/tests.rs | 19 ++++--------------- crates/engine/src/tests/typing.rs | 6 ------ 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ef8d4b93d..70d2eab87 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -19,9 +19,10 @@ mod typing; mod wordart; /// True, after saying so on stderr, when this build has no Language data embedded (craft-proof -/// built without `CRAFT_I18N_DIR`): no Moby word list, no hyphenation patterns. Tests that check -/// spelling marks or hyphenation from that data return early then, as craft-proof's own do; with -/// the data (CI, releases) they all run. +/// built without `CRAFT_I18N_DIR`). Without it English still spells from craft-proof's built-in +/// word list, but there are no hyphenation patterns and no American/British variant table, so the +/// tests that check those return early, as craft-proof's own do; with the data (CI, releases) they +/// all run. pub(crate) fn no_language_data(test: &str) -> bool { let missing = wordcraft_proof::english_source(wordcraft_proof::ProofLang::En) != Some(wordcraft_proof::EnglishSource::Embedded); if missing { @@ -255,9 +256,6 @@ fn reset_shortcuts_remove_direct_formatting_but_keep_styles() { #[test] fn custom_dictionary_words_persist_through_their_file() { - if no_language_data("custom_dictionary_words_persist_through_their_file") { - return; - } // #40: adding a word through a command writes it to the session's file, and loading // that file makes the word known (only the desktop app wires this up). let dir = std::env::temp_dir().join(format!("wc-engdict-{}", std::process::id())); @@ -8584,9 +8582,6 @@ fn dictionaries_folder_option_is_saved_searched_and_reported() { /// grammar, suggestions come from it, and a suggestion fixes the text. #[test] fn portuguese_proofing_through_the_review_commands() { - if no_language_data("portuguese_proofing_through_the_review_commands") { - return; - } portuguese_test_dictionary(); let mut s = s(); run(&mut s, "document.setText", json!({"text": "Saudade coraçao, e ele ele disse."})); @@ -8889,9 +8884,6 @@ fn uruguayan_spanish_accepts_vos_forms() { /// Czech proofing through the same commands as Review, F7, Change and Add to Dictionary. #[test] fn czech_proofing_through_the_review_commands() { - if no_language_data("czech_proofing_through_the_review_commands") { - return; - } czech_test_dict::install(); let mut s = s(); run(&mut s, "document.setText", json!({"text": "Město pocitac, škola škola."})); @@ -8974,9 +8966,6 @@ fn italian_proofing_through_the_review_commands() { #[test] fn polish_proofing_through_the_review_commands() { - if no_language_data("polish_proofing_through_the_review_commands") { - return; - } polish_test_dictionary(); let mut s = s(); run(&mut s, "document.setText", json!({"text": "Płynie żeka, a wogóle to wiem że tak."})); diff --git a/crates/engine/src/tests/typing.rs b/crates/engine/src/tests/typing.rs index dc330293a..7d0c42164 100644 --- a/crates/engine/src/tests/typing.rs +++ b/crates/engine/src/tests/typing.rs @@ -2906,9 +2906,6 @@ fn moving_the_caret_out_of_the_typed_word_marks_it() { #[test] fn a_click_elsewhere_marks_the_typed_word_and_a_click_back_keeps_the_mark() { - if crate::tests::no_language_data("a_click_elsewhere_marks_the_typed_word_and_a_click_back_keeps_the_mark") { - return; - } let mut s = typed("Fine\nThe helo"); assert_eq!(squiggles(&mut s, 1), Vec::::new()); // The canvas sets the caret directly on a click. @@ -2926,9 +2923,6 @@ fn a_click_elsewhere_marks_the_typed_word_and_a_click_back_keeps_the_mark() { #[test] fn backspace_into_a_finished_word_edits_it_again() { - if crate::tests::no_language_data("backspace_into_a_finished_word_edits_it_again") { - return; - } let mut s = typed("The helo "); assert_eq!(squiggles(&mut s, 0), ["helo"]); typ(&mut s, "\u{8}"); From 6bb3f085aa78c35aa61716c7cad2909eb243c3b9 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 19:26:24 +0000 Subject: [PATCH 11/15] =?UTF-8?q?CP1.3:=20rebase=20onto=20main=20=E2=80=94?= =?UTF-8?q?=20remove=20the=20crates/proof=20files=20#5000=20added=20(affix?= =?UTF-8?q?=5Fsuggest,=20slot,=20spelling,=20tr/test=5Fdict;=20already=20i?= =?UTF-8?q?n=20craft-proof)=20and=20resolve=20craft-proof=20from=20git=20i?= =?UTF-8?q?n=20Cargo.lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- Cargo.lock | 26 +- crates/proof/src/affix_suggest.rs | 556 ------------------------------ crates/proof/src/slot.rs | 210 ----------- crates/proof/src/spelling.rs | 52 --- crates/proof/src/tr/test_dict.rs | 22 -- 5 files changed, 13 insertions(+), 853 deletions(-) delete mode 100644 crates/proof/src/affix_suggest.rs delete mode 100644 crates/proof/src/slot.rs delete mode 100644 crates/proof/src/spelling.rs delete mode 100644 crates/proof/src/tr/test_dict.rs diff --git a/Cargo.lock b/Cargo.lock index 7bff2098d..9396eae9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1061,6 +1061,16 @@ dependencies = [ "winsafe", ] +[[package]] +name = "craft-proof" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=48ed6f8cbd4104225a9ecde122d9fb8d1353b5a6#48ed6f8cbd4104225a9ecde122d9fb8d1353b5a6" +dependencies = [ + "log", + "miniz_oxide 0.9.1", + "unicode-normalization", +] + [[package]] name = "craft-ui" version = "0.1.0" @@ -6654,6 +6664,7 @@ dependencies = [ "chrono", "clipboard-win", "craft-print", + "craft-proof", "craft-update", "dispatch2", "eframe", @@ -6676,7 +6687,6 @@ dependencies = [ "wordcraft-doc", "wordcraft-engine", "wordcraft-fonts", - "wordcraft-proof", "wordcraft-ui-egui", "zbus", ] @@ -6759,6 +6769,7 @@ version = "0.14.0" dependencies = [ "cfb", "chrono", + "craft-proof", "encoding_rs", "getrandom 0.3.4", "image", @@ -6778,7 +6789,6 @@ dependencies = [ "wordcraft-geom", "wordcraft-layout", "wordcraft-pdf", - "wordcraft-proof", "wordcraft-render", "zeroize", "zip 3.0.0", @@ -6829,6 +6839,7 @@ dependencies = [ name = "wordcraft-layout" version = "0.14.0" dependencies = [ + "craft-proof", "image", "js-sys", "kurbo", @@ -6842,7 +6853,6 @@ dependencies = [ "wordcraft-fonts", "wordcraft-geom", "wordcraft-metafile", - "wordcraft-proof", ] [[package]] @@ -6886,16 +6896,6 @@ dependencies = [ "wordcraft-metafile", ] -[[package]] -name = "wordcraft-proof" -version = "0.14.0" -dependencies = [ - "log", - "miniz_oxide 0.9.1", - "proptest", - "unicode-normalization", -] - [[package]] name = "wordcraft-render" version = "0.14.0" diff --git a/crates/proof/src/affix_suggest.rs b/crates/proof/src/affix_suggest.rs deleted file mode 100644 index 1456f81c7..000000000 --- a/crates/proof/src/affix_suggest.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! Spelling suggestions from an affix dictionary, one engine for every language that reads one -//! (Czech, Croatian, Indonesian, Italian, Polish, Portuguese, Russian, Spanish). Candidates come -//! in rounds of rising cost, each checked against the dictionary: -//! 1. letter case (`warszawa` → `Warszawa`); -//! 2. missing or wrong diacritics and other MAP slips, a few letters changed (`zolw` → `żółw`): -//! adding a missing accent is the commonest slip and ranks first, while taking away one the -//! writer typed ranks after the known confusions; -//! 3. the REP table of common confusions (`żeka` → `rzeka`); -//! 4. a missing space (`napewno` → `na pewno`); -//! 5. keyboard neighbours from the KEY rows, then one typo: a letter left out, added, swapped with -//! the next or mistyped (TRY letters); -//! 6. when that finds fewer than asked for: the stems that start like the word (MAP relatives -//! folded) and share the most letter pairs with it, whose forms are ranked by an edit distance in -//! which a MAP slip costs half (two typos at most). -//! -//! The tables come from the dictionary; a language's [`Tuning`] adds the ones its dictionaries may -//! lack and the slips no table can say. The input is bounded ([`MAX_LETTERS`]), and so is every -//! round, so a call costs a few thousand dictionary lookups at most. - -use std::borrow::Cow; -use std::collections::{HashMap, HashSet}; - -use crate::affix::{AffixDict, Casing, capitalize, casing}; - -/// Longest word suggestions are looked for, in letters. -pub(crate) const MAX_LETTERS: usize = 40; -const MAX_MAP_VARIANTS: usize = 4096; -/// Stems the scan of round 6 expands. -const SCAN_STEMS: usize = 48; - -const COST_CASE: u32 = 0; -pub(crate) const COST_ADD_DIACRITIC: u32 = 4; -pub(crate) const COST_OTHER_MAP: u32 = 7; -const COST_DROP_DIACRITIC: u32 = 11; -const COST_REP: u32 = 10; -pub(crate) const COST_SPLIT: u32 = 12; -/// A missing space leaving a half shorter than three letters ([`Split::Anywhere`]). -const COST_SPLIT_SHORT: u32 = 17; -/// A missing space between two longer halves with no short word beside it ([`Split::Near`]). -pub(crate) const COST_SPLIT_LONG: u32 = 18; -const COST_KEY: u32 = 13; -const COST_SWAP: u32 = 14; -const COST_DROP_OR_ADD: u32 = 15; -pub(crate) const COST_MISTYPE: u32 = 16; -const COST_SCAN: u32 = 20; - -/// What a language adds to its dictionary's tables. -pub(crate) struct Tuning { - /// Where a missing space may go (round 4). - pub split: Split, - /// TRY letters for a dictionary without a TRY line. - pub alphabet: &'static str, - /// MAP groups the language adds to its dictionary's (each string is one group: `"cčć"`). - pub map: &'static [&'static str], - /// REP confusions the language adds to its dictionary's. - pub rep: &'static [(&'static str, &'static str)], - /// The cost of a REP confusion. - pub rep_cost: u32, - /// Most letters a MAP variant changes. - pub max_map_changes: usize, - /// The cost of changing letter `from` to its MAP relative `to`. - pub map_cost: fn(char, char) -> u32, - /// The cost of typing letter `c` at `i` of the word (`chars`, lower case) instead. - pub mistype_cost: fn(&[char], usize, char) -> u32, -} - -/// Where a missing space may go. -pub(crate) enum Split { - /// Anywhere, dearer when a half is shorter than three letters. - Anywhere, - /// After a short word that often runs into the next one (`na pewno`, `w ogóle`) or before one - /// that runs into the word before (`kocham cię`), dearer between two longer halves, and never - /// leaving a stray short half (`że ka`). - Near { leading: &'static [&'static str], trailing: &'static [&'static str] }, - /// The language's own rule: the cost of the halves `a b`, or `None` to skip them. - Custom(fn(&str, &str) -> Option), -} - -impl Tuning { - /// The dictionary's tables alone (Czech, Indonesian, Spanish). - pub(crate) const DEFAULT: Tuning = - Tuning { split: Split::Anywhere, alphabet: "", map: &[], rep: &[], rep_cost: COST_REP, max_map_changes: 4, map_cost, mistype_cost }; -} - -/// The cost of changing letter `from` to its MAP relative `to`. Adding a missing accent is the -/// commonest slip (typing without diacritics); an accent the writer did type is likelier right, -/// so taking one away costs more than a known confusion from the REP table. -pub(crate) fn map_cost(from: char, to: char) -> u32 { - match (from.is_ascii(), to.is_ascii()) { - (true, false) => COST_ADD_DIACRITIC, - (false, true) => COST_DROP_DIACRITIC, - _ => COST_OTHER_MAP, - } -} - -/// Any mistyped letter costs the same. -fn mistype_cost(_: &[char], _: usize, _: char) -> u32 { - COST_MISTYPE -} - -struct Collector<'a> { - d: &'a AffixDict, - /// The word, lower case. - input: String, - /// Candidate → the cheapest cost it was offered at (rounds don't run in rising cost). - tested: HashMap, - /// Suggestion → (cost, order found). - found: HashMap, - seq: usize, -} - -impl Collector<'_> { - /// Check a lower-case candidate (a word, or two separated by a space) and keep it if every - /// word is in the dictionary, as written or capitalized (names). - fn offer(&mut self, cand: &str, cost: u32) { - if cand.is_empty() || cand == self.input { - return; - } - match self.tested.get_mut(cand) { - Some(prev) if *prev <= cost => return, - Some(prev) => *prev = cost, - None => { - self.tested.insert(cand.to_string(), cost); - } - } - let mut words = Vec::new(); - let mut cost = cost; - for part in cand.split(' ') { - if self.d.contains_suggestible(part) { - words.push(part.to_string()); - } else { - let c = capitalize(part); - if !self.d.contains_suggestible(&c) { - return; - } - words.push(c); - // A name ranks after a common word at the same distance (`moze` → `może`, then - // `Mozę`). - cost += 1; - } - } - self.keep(words.join(" "), cost); - } - - /// Keep a suggestion known to be correct, unless the dictionary says never to offer it. - fn keep(&mut self, text: String, cost: u32) { - if !text.split(' ').all(|part| self.d.can_suggest(part)) { - return; - } - self.seq += 1; - let seq = self.seq; - let e = self.found.entry(text).or_insert((cost, seq)); - if cost < e.0 { - *e = (cost, seq); - } - } - - fn count_within(&self, cost: u32) -> usize { - self.found.values().filter(|(c, _)| *c <= cost).count() - } -} - -/// The MAP groups: the dictionary's, then the language's. -fn groups<'a>(d: &'a AffixDict, t: &Tuning) -> Cow<'a, [Vec]> { - if t.map.is_empty() { - return Cow::Borrowed(d.map()); - } - Cow::Owned(d.map().iter().cloned().chain(t.map.iter().map(|g| g.chars().collect())).collect()) -} - -/// The other members of every MAP group of `c` (its likely confusions), each once. -fn alternatives(c: char, map: &[Vec]) -> Vec { - let mut out: Vec = Vec::new(); - for x in map.iter().filter(|g| g.contains(&c)).flat_map(|g| g.iter().copied()) { - if x != c && !out.contains(&x) { - out.push(x); - } - } - out -} - -/// Every way of changing up to `t.max_map_changes` letters of `chars` to a MAP relative (`alts`, -/// letter by letter), with its cost, each written after `buf`. -fn map_variants(chars: &[char], alts: &[Vec], t: &Tuning, changes: usize, cost: u32, buf: &mut Vec, out: &mut Vec<(String, u32)>) { - if out.len() >= MAX_MAP_VARIANTS { - return; - } - let Some((&c, rest)) = chars.split_first() else { - if changes > 0 { - out.push((buf.iter().collect(), cost)); - } - return; - }; - let (here, later) = alts.split_first().map_or((&[][..], &[][..]), |(h, l)| (h.as_slice(), l)); - buf.push(c); - map_variants(rest, later, t, changes, cost, buf, out); - buf.pop(); - if changes < t.max_map_changes { - for &a in here { - buf.push(a); - map_variants(rest, later, t, changes + 1, cost + (t.map_cost)(c, a), buf, out); - buf.pop(); - } - } -} - -/// A letter's MAP representative (the first of its first group), lower case: `ż` → `z`, `ó` → `o`. -pub(crate) fn fold(c: char, map: &[Vec]) -> char { - let l = c.to_lowercase().next().unwrap_or(c); - map.iter().find(|g| g.contains(&l)).and_then(|g| g.first().copied()).unwrap_or(l) -} - -/// Optimal string alignment distance where a MAP slip (`a`/`ą`, `u`/`ó`) costs 1 and any other -/// edit 2; `limit + 1` once it is certainly above `limit`. -pub(crate) fn weighted_distance(a: &[char], b: &[char], related: &dyn Fn(char, char) -> bool, limit: u32) -> u32 { - let m = b.len(); - let over = limit + 1; - let mut prev2 = vec![0u32; m + 1]; - let mut prev: Vec = (0..=m as u32).map(|j| j * 2).collect(); - let mut cur = vec![0u32; m + 1]; - for (i, &ai) in a.iter().enumerate() { - if let Some(c) = cur.get_mut(0) { - *c = (i as u32 + 1) * 2; - } - let mut row_min = u32::MAX; - for (j, &bj) in b.iter().enumerate() { - let sub = if ai == bj { - 0 - } else if related(ai, bj) { - 1 - } else { - 2 - }; - let at = |v: &[u32], k: usize| v.get(k).copied().unwrap_or(u32::MAX / 4); - let mut v = (at(&prev, j + 1) + 2).min(at(&cur, j) + 2).min(at(&prev, j) + sub); - if i > 0 && j > 0 && a.get(i - 1) == Some(&bj) && b.get(j - 1) == Some(&ai) { - v = v.min(at(&prev2, j - 1) + 2); - } - if let Some(c) = cur.get_mut(j + 1) { - *c = v; - } - row_min = row_min.min(v); - } - if m > 0 && row_min > limit { - return over; - } - std::mem::swap(&mut prev2, &mut prev); - std::mem::swap(&mut prev, &mut cur); - } - prev.get(m).copied().unwrap_or(over).min(over) -} - -/// The cost of a missing space between `a` and `b`, or `None` when the split isn't offered. -fn split_cost(split: &Split, a: &str, b: &str) -> Option { - let long = |part: &str| part.chars().count() >= 3; - match split { - Split::Anywhere => Some(if long(a) && long(b) { COST_SPLIT } else { COST_SPLIT_SHORT }), - Split::Near { leading, trailing } => { - if (leading.contains(&a) && long(b)) || (long(a) && trailing.contains(&b)) { - Some(COST_SPLIT) - } else if long(a) && long(b) { - Some(COST_SPLIT_LONG) - } else { - None - } - } - Split::Custom(cost) => cost(a, b), - } -} - -/// Spelling suggestions for `word` from `d`, best first, at most `max`, in the word's letter case. -pub(crate) fn suggest_in(d: &AffixDict, word: &str, max: usize, t: &Tuning) -> Vec { - let word = word.trim(); - let n = word.chars().count(); - if n == 0 || n > MAX_LETTERS || max == 0 || d.is_empty() || word.contains(char::is_whitespace) { - return Vec::new(); - } - let case = casing(word); - let input = word.to_lowercase(); - let chars: Vec = input.chars().collect(); - let mut col = Collector { d, input: input.clone(), tested: HashMap::new(), found: HashMap::new(), seq: 0 }; - add_case(&mut col, word, &input); - let map = groups(d, t); - add_map_slips(&mut col, &chars, &map, t); - add_confusions(&mut col, &input, t); - add_splits(&mut col, &input, &t.split); - add_key_neighbours(&mut col, &chars); - add_typos(&mut col, &chars, t); - // 6. Further off: scan the stems that start like the word. - if col.count_within(COST_MISTYPE) < max { - scan(&mut col, &chars, &map); - } - let mut found: Vec<(String, (u32, usize))> = col.found.into_iter().collect(); - // Among equal costs, the change nearest the word's start is the likelier intent - // (the first changed letter breaks ties). - let diff_at = |text: &str| chars.iter().zip(text.chars()).position(|(a, b)| *a != b).unwrap_or(n); - found.sort_by_key(|(text, (cost, seq))| (*cost, diff_at(text), *seq)); - let mut out: Vec = Vec::new(); - for (text, _) in found { - let recased = match case { - Casing::Initial => capitalize(&text), - Casing::Upper if n > 1 => text.to_uppercase(), - _ => text.clone(), - }; - // A KEEPCASE word only exists in its dictionary case: `Km` would be flagged, so offer `km`. - let t = if recased != text && text.split(' ').any(|p| d.case_locked(p)) { text } else { recased }; - if t != word && !out.contains(&t) { - out.push(t); - } - if out.len() >= max { - break; - } - } - out -} - -/// Round 1: letter case. -fn add_case(col: &mut Collector, word: &str, input: &str) { - if col.d.contains(input) && word != input { - col.keep(input.to_string(), COST_CASE); - } else if col.d.contains(&capitalize(input)) && word != capitalize(input) { - col.keep(capitalize(input), COST_CASE); - } -} - -/// Round 2: accents and other MAP slips. -fn add_map_slips(col: &mut Collector, chars: &[char], map: &[Vec], t: &Tuning) { - let alts: Vec> = chars.iter().map(|&c| alternatives(c, map)).collect(); - let mut variants = Vec::new(); - map_variants(chars, &alts, t, 0, 0, &mut Vec::with_capacity(chars.len()), &mut variants); - variants.sort_by_key(|v| v.1); - for (v, cost) in &variants { - col.offer(v, *cost); - } -} - -/// Round 3: common confusions. -fn add_confusions(col: &mut Collector, input: &str, t: &Tuning) { - let rep = col.d.rep().iter().map(|(from, to)| (from.as_str(), to.as_str())).chain(t.rep.iter().copied()); - for (from, to) in rep { - if from.is_empty() { - continue; - } - for (i, _) in input.match_indices(from) { - if let (Some(a), Some(b)) = (input.get(..i), input.get(i + from.len()..)) { - col.offer(&format!("{a}{to}{b}"), t.rep_cost); - } - } - } -} - -/// Round 4: a missing space; both halves must be dictionary words. -fn add_splits(col: &mut Collector, input: &str, split: &Split) { - for (i, _) in input.char_indices().skip(1) { - let (Some(a), Some(b)) = (input.get(..i), input.get(i..)) else { continue }; - if let Some(cost) = split_cost(split, a, b) { - col.offer(&format!("{a} {b}"), cost); - } - } -} - -/// Round 5: keyboard neighbours from the dictionary's KEY rows. -fn add_key_neighbours(col: &mut Collector, chars: &[char]) { - for row in col.d.key().split('|').take(32) { - let keys: Vec = row.chars().take(128).collect(); - for pair in keys.windows(2) { - let [a, b] = pair else { continue }; - for (i, &c) in chars.iter().enumerate() { - let replacement = if c == *a { - Some(*b) - } else if c == *b { - Some(*a) - } else { - None - }; - if let Some(replacement) = replacement { - let mut v = chars.to_vec(); - if let Some(x) = v.get_mut(i) { - *x = replacement; - } - col.offer(&v.iter().collect::(), COST_KEY); - } - } - } - } -} - -/// Round 5, second half: one typo, with the TRY letters (the language's when the dictionary has -/// none), bounded and each once, in their order: a TRY line lists the commonest letters first, so -/// equal-cost candidates come out in that order. -fn add_typos(col: &mut Collector, chars: &[char], t: &Tuning) { - let d = col.d; - let n = chars.len(); - let try_chars = if d.try_chars().is_empty() { t.alphabet } else { d.try_chars() }; - let mut letters: Vec = Vec::new(); - for c in try_chars.chars().filter(|c| c.is_lowercase()).take(128) { - if !letters.contains(&c) { - letters.push(c); - } - } - let s = |v: &[char]| v.iter().collect::(); - for i in 0..n.saturating_sub(1) { - if chars.get(i) != chars.get(i + 1) { - let mut v = chars.to_vec(); - v.swap(i, i + 1); - col.offer(&s(&v), COST_SWAP); - } - } - for i in 0..n { - let mut v = chars.to_vec(); - v.remove(i); - col.offer(&s(&v), COST_DROP_OR_ADD); - } - for i in 0..=n { - for &c in &letters { - let mut v = chars.to_vec(); - v.insert(i, c); - col.offer(&s(&v), COST_DROP_OR_ADD); - } - } - for i in 0..n { - for &c in &letters { - if chars.get(i) != Some(&c) { - let mut v = chars.to_vec(); - if let Some(x) = v.get_mut(i) { - *x = c; - } - col.offer(&s(&v), (t.mistype_cost)(chars, i, c)); - } - } - } -} - -/// Round 6: rank the stems starting with the word's first letter (or a MAP relative of it, either -/// case) by shared letter pairs, then keep the forms of the best within two typos. -fn scan(col: &mut Collector, chars: &[char], map: &[Vec]) { - let d = col.d; - let Some(&first) = chars.first() else { return }; - let folded: Vec = chars.iter().map(|&c| fold(c, map)).collect(); - let pairs: HashSet<(char, char)> = std::iter::once(' ').chain(folded.iter().copied()).zip(folded.iter().copied()).collect(); - let mut firsts: Vec = std::iter::once(first).chain(alternatives(first, map)).collect(); - let upper: Vec = firsts.iter().flat_map(|c| c.to_uppercase()).collect(); - firsts.extend(upper); - firsts.sort_unstable(); - firsts.dedup(); - let n = folded.len(); - let mut best: Vec<(u32, &str)> = Vec::new(); - for f in firsts { - let prefix = f.to_string(); - for stem in d.stems_with_prefix(&prefix) { - let sf: Vec = stem.chars().map(|c| fold(c, map)).collect(); - let shared = std::iter::once(' ').chain(sf.iter().copied()).zip(sf.iter().copied()).filter(|p| pairs.contains(p)).count(); - if shared < 2 { - continue; - } - // Shared pairs, less the stem's letters the word doesn't explain, less a length gap - // beyond what an ending explains. - let extra = sf.len().saturating_sub(shared); - let gap = sf.len().abs_diff(n).saturating_sub(4); - let score = (shared * 4).saturating_sub(extra * 3 + gap * 2) as u32; - if score == 0 { - continue; - } - if best.len() < SCAN_STEMS { - best.push((score, stem)); - } else if let Some(min) = best.iter_mut().min_by_key(|b| b.0) - && score > min.0 - { - *min = (score, stem); - } - } - } - best.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(b.1))); - let related = |a: char, b: char| fold(a, map) == fold(b, map); - let limit = 4; - for (_, stem) in best { - for form in d.forms(stem) { - let lf: Vec = form.to_lowercase().chars().collect(); - if lf.len().abs_diff(chars.len()) > 2 { - continue; - } - let dist = weighted_distance(&lf, chars, &related, limit); - if dist <= limit && lf.as_slice() != chars { - col.keep(form, COST_SCAN + dist * 3); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn an_extra_trailing_letter_is_dropped_before_a_short_word_split() { - let d = AffixDict::from_hunspell("", "2\npočítač\nz\n", &[]).unwrap(); - let suggestions = suggest_in(&d, "počítačz", 8, &Tuning::DEFAULT); - assert_eq!(suggestions.first().map(String::as_str), Some("počítač"), "{suggestions:?}"); - assert!(suggestions.contains(&"počítač z".into()), "{suggestions:?}"); - } - - /// A candidate offered again at a lower cost (a REP confusion after a MAP diacritic drop) - /// ranks at the lower one. - #[test] - fn a_cheaper_later_offer_wins() { - let d = AffixDict::from_hunspell("", "1\ncafe\n", &[]).unwrap(); - let mut col = Collector { d: &d, input: "cafè".into(), tested: HashMap::new(), found: HashMap::new(), seq: 0 }; - col.offer("cafe", COST_DROP_DIACRITIC); - col.offer("cafe", COST_REP); - col.offer("cafe", COST_SCAN); - assert_eq!(col.found.get("cafe").map(|e| e.0), Some(COST_REP)); - } - - #[test] - fn distance_weighs_map_slips_half() { - let map = vec![vec!['a', 'ą'], vec!['o', 'ó', 'u']]; - let related = |a: char, b: char| fold(a, &map) == fold(b, &map); - let c = |s: &str| s.chars().collect::>(); - assert_eq!(weighted_distance(&c("ktury"), &c("który"), &related, 9), 1); - assert_eq!(weighted_distance(&c("kot"), &c("kto"), &related, 9), 2); - assert_eq!(weighted_distance(&c("kot"), &c("kotek"), &related, 9), 4); - assert_eq!(weighted_distance(&c("kot"), &c("pies"), &related, 2), 3); - assert_eq!(weighted_distance(&c(""), &c("ab"), &related, 9), 4); - } - - /// A KEEPCASE word is offered in its dictionary case when the word's own case would be flagged - /// (`Kmm` → `km`, never `Km`), and a capitalised KEEPCASE word gets its dictionary form. - #[test] - fn keepcase_suggestions_are_not_recased_into_errors() { - let d = AffixDict::from_hunspell("KEEPCASE K\n", "2\nkm/K\npies\n", &[]).unwrap(); - assert!(!d.check("Km") && !d.check("KM")); - for (word, want) in [("Kmm", "km"), ("KMM", "km"), ("Km", "km")] { - let got = suggest_in(&d, word, 4, &Tuning::DEFAULT); - assert!(got.iter().all(|s| s.split(' ').all(|p| d.check(p))), "{word}: {got:?}"); - assert_eq!(got.first().map(String::as_str), Some(want), "{word}: {got:?}"); - } - assert_eq!(suggest_in(&d, "Pies", 4, &Tuning::DEFAULT), Vec::::new()); - assert_eq!(suggest_in(&d, "Piesx", 4, &Tuning::DEFAULT).first().map(String::as_str), Some("Pies")); - } - - /// A language's tables come after its dictionary's, a TRY alphabet stands in for a missing TRY - /// line, and a split rule decides which missing spaces are offered. - #[test] - fn tuning_adds_to_the_dictionary_tables() { - let d = AffixDict::from_hunspell("", "3\nčaj\nkot\nna\n", &[]).unwrap(); - let tuned = Tuning { map: &["cč"], alphabet: "t", rep: &[("xy", "kot")], ..Tuning::DEFAULT }; - assert!(!suggest_in(&d, "caj", 4, &Tuning::DEFAULT).contains(&"čaj".into())); - assert!(suggest_in(&d, "caj", 4, &tuned).contains(&"čaj".into()), "the language's MAP group"); - assert!(suggest_in(&d, "xy", 4, &tuned).contains(&"kot".into()), "the language's REP pair"); - assert!(suggest_in(&d, "ko", 4, &tuned).contains(&"kot".into()), "the language's TRY letters"); - let near = Tuning { split: Split::Near { leading: &["na"], trailing: &[] }, ..Tuning::DEFAULT }; - assert!(suggest_in(&d, "nakot", 4, &near).contains(&"na kot".into())); - assert!(!suggest_in(&d, "kotna", 4, &near).contains(&"kot na".into()), "no stray short half"); - assert!(suggest_in(&d, "kotna", 4, &Tuning::DEFAULT).contains(&"kot na".into())); - } -} diff --git a/crates/proof/src/slot.rs b/crates/proof/src/slot.rs deleted file mode 100644 index 6255ae5a5..000000000 --- a/crates/proof/src/slot.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! The installed Hunspell dictionary of a proofing language, in one shape for every language: -//! found on first use, replaced with [`DictSlot::install`] and forgotten with [`DictSlot::reload`]. - -use std::path::{Path, PathBuf}; -use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -use crate::affix::AffixDict; - -/// A dictionary a language reads: the affix engine's ([`AffixDict`]) or Turkish's own. -pub(crate) trait Dictionary: Default + Send + Sync { - /// The installed `.aff` and `.dic` files, read. - fn load(aff: &Path, dic: &Path) -> Result; - /// Hunspell `.aff` and `.dic` text (decoded to UTF-8), parsed. - fn parse(aff: &str, dic: &str) -> Result; - /// No words: the language has no spelling marks. - fn is_empty(&self) -> bool; -} - -impl Dictionary for AffixDict { - fn load(aff: &Path, dic: &Path) -> Result { - crate::hunspell::load(aff, dic) - } - - fn parse(aff: &str, dic: &str) -> Result { - AffixDict::from_hunspell(aff, dic, &[]) - } - - fn is_empty(&self) -> bool { - AffixDict::is_empty(self) - } -} - -/// What every language's slot answers, whatever its dictionary type (the language table in -/// lib.rs holds them side by side). -pub(crate) trait Slot: Sync { - /// See [`DictSlot::name`]. - fn name(&self) -> &'static str; - /// See [`DictSlot::installed_path`]. - fn installed_path(&self) -> Option; - /// See [`DictSlot::has_dictionary`]. - fn has_dictionary(&self) -> bool; - /// See [`DictSlot::reload`]. - fn reload(&self); -} - -impl Slot for DictSlot { - fn name(&self) -> &'static str { - DictSlot::name(self) - } - - fn installed_path(&self) -> Option { - DictSlot::installed_path(self) - } - - fn has_dictionary(&self) -> bool { - DictSlot::has_dictionary(self) - } - - fn reload(&self) { - DictSlot::reload(self); - } -} - -/// The dictionary in use and whether it came from [`DictSlot::install`] rather than a file on -/// disk (it then has no file to report, even if a matching file also happens to exist): one lock, -/// so a reload and an install can't interleave their two fields. -struct State { - dict: Option>, - in_memory: bool, - /// Counts reloads, so a load that started before one is not kept. - reloads: u64, -} - -/// One language's dictionary. -pub(crate) struct DictSlot { - /// File names the dictionary may have, the usual one first (also the key of its load error). - names: &'static [&'static str], - /// The language's English name, for the log. - english: &'static str, - state: RwLock>, -} - -impl DictSlot { - pub(crate) const fn new(names: &'static [&'static str], english: &'static str) -> Self { - DictSlot { names, english, state: RwLock::new(State { dict: None, in_memory: false, reloads: 0 }) } - } - - fn read(&self) -> RwLockReadGuard<'_, State> { - self.state.read().unwrap_or_else(PoisonError::into_inner) - } - - fn write(&self) -> RwLockWriteGuard<'_, State> { - self.state.write().unwrap_or_else(PoisonError::into_inner) - } - - /// The Hunspell name the dictionary is known by (`pl_PL`): the first of its file names. - pub(crate) fn name(&self) -> &'static str { - self.names.first().copied().unwrap_or_default() - } - - /// The file names the dictionary may have, the usual one first. - #[cfg(test)] - pub(crate) fn names(&self) -> &'static [&'static str] { - self.names - } - - /// The dictionary: the installed one (read on first use), one given with [`Self::install`], or - /// an empty one when none is available (the language's spelling is off then). - pub(crate) fn get(&self) -> Arc { - let reloads = { - let s = self.read(); - if let Some(d) = s.dict.as_ref() { - return d.clone(); - } - s.reloads - }; - self.keep(reloads, Arc::new(self.load_installed())) - } - - /// Keep a dictionary loaded after `reloads` reloads. If the folder was reloaded since, the - /// load may have read the old folder: it serves this one check and the next one loads again. - fn keep(&self, reloads: u64, loaded: Arc) -> Arc { - let mut s = self.write(); - if s.reloads != reloads { - return s.dict.clone().unwrap_or(loaded); - } - // Another thread (or an install) may have filled it meanwhile: that one wins. - s.dict.get_or_insert(loaded).clone() - } - - fn load_installed(&self) -> D { - let name = self.name(); - let Some((aff, dic)) = crate::hunspell::find(self.names) else { - crate::hunspell::clear_load_error(name); - log::info!("{} spelling: no Hunspell {name} dictionary installed", self.english); - return D::default(); - }; - crate::hunspell::note_load(name, &dic, D::load(&aff, &dic)).unwrap_or_else(|e| { - log::warn!("{} spelling: {e}", self.english); - D::default() - }) - } - - /// Use this dictionary from now on (Hunspell `.aff` and `.dic` text, decoded to UTF-8), for - /// example one the user picked. On error the current dictionary stays. - pub(crate) fn install(&self, aff: &str, dic: &str) -> Result<(), String> { - let d = D::parse(aff, dic)?; - let mut slot = self.write(); - slot.dict = Some(Arc::new(d)); - slot.in_memory = true; - Ok(()) - } - - /// Forget the dictionary, so the next check looks for an installed one again (after - /// [`crate::hunspell::set_dictionary_dir`] or a dictionary was installed). - pub(crate) fn reload(&self) { - let mut slot = self.write(); - slot.dict = None; - slot.in_memory = false; - slot.reloads = slot.reloads.wrapping_add(1); - } - - /// The `.dic` file of the installed dictionary, searching the dictionaries folder and the - /// system locations now (File › Options › Proofing shows it); `None` when there is none, or - /// when the dictionary in use was given with [`Self::install`] and so has no file. - pub(crate) fn installed_path(&self) -> Option { - if self.read().in_memory { - return None; - } - crate::hunspell::find(self.names).map(|(_, dic)| dic) - } - - /// Is a dictionary available (spelling is checked)? - pub(crate) fn has_dictionary(&self) -> bool { - !self.get().is_empty() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - static SLOT: DictSlot = DictSlot::new(&["zz_SLOT_TEST"], "Test"); - - #[test] - fn an_installed_dictionary_has_no_file_until_reloaded() { - assert!(!SLOT.has_dictionary(), "nothing installed under this name"); - assert!(SLOT.install("FLAG long\n", "1\nkot\n").is_err(), "unsupported files are refused"); - assert!(!SLOT.has_dictionary(), "a refused install keeps the current one"); - SLOT.install("", "1\nkot\n").unwrap(); - assert!(SLOT.has_dictionary() && SLOT.get().check("kot")); - assert_eq!(SLOT.installed_path(), None); - SLOT.reload(); - assert!(!SLOT.has_dictionary(), "reloaded from disk, where there is none"); - assert_eq!(SLOT.name(), "zz_SLOT_TEST"); - } - - /// A load that started before a dictionaries-folder change is not kept after its reload. - #[test] - fn a_load_from_before_a_reload_is_not_kept() { - let slot: DictSlot = DictSlot::new(&["zz_SLOT_RELOAD_TEST"], "Test"); - let before = slot.read().reloads; - slot.reload(); - let _ = slot.keep(before, Arc::new(AffixDict::default())); - assert!(slot.read().dict.is_none(), "the next check loads from the new folder"); - let now = slot.read().reloads; - let _ = slot.keep(now, Arc::new(AffixDict::default())); - assert!(slot.read().dict.is_some(), "a load since the reload is kept"); - } -} diff --git a/crates/proof/src/spelling.rs b/crates/proof/src/spelling.rs deleted file mode 100644 index ab5e520e9..000000000 --- a/crates/proof/src/spelling.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! The start of every Latin-script dictionary language's spelling check (Czech, Croatian, -//! Indonesian, Italian, Polish, Portuguese, Spanish): the words that need no lookup, under the -//! File › Options › Proofing choices. Each language goes on from there with its own rules. - -use crate::Options; -use crate::affix::AffixDict; - -/// Is `c` a letter of the Latin script (the only one these dictionaries cover)? -fn latin(c: char) -> bool { - c.is_alphabetic() && (c < '\u{0250}' || ('\u{1E00}'..='\u{1EFF}').contains(&c)) -} - -/// `word` trimmed of quotes and apostrophes, when `d` must say whether it is right; `None` when it -/// is right without a lookup: there is no dictionary (no spelling marks), the word is a single -/// letter or has no letters, it has digits or is a short word in capitals (an acronym) and the -/// options skip those, or it is in another script. -pub(crate) fn word_to_check<'a>(d: &AffixDict, word: &'a str, opts: &Options) -> Option<&'a str> { - if d.is_empty() { - return None; - } - let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); - let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { - return None; - } - if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { - return None; - } - w.chars().any(latin).then_some(w) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn words_needing_no_lookup() { - let d = AffixDict::from_hunspell("", "1\nkot\n", &[]).unwrap(); - let defaults = Options::default(); - assert_eq!(word_to_check(&AffixDict::default(), "kot", &defaults), None, "no dictionary"); - for w in ["", "a", "2026", "PKO", "B2B", "日本語", "--"] { - assert_eq!(word_to_check(&d, w, &defaults), None, "{w}"); - } - assert_eq!(word_to_check(&d, "„kot”", &defaults), Some("kot")); - assert_eq!(word_to_check(&d, "'kot'", &defaults), Some("kot")); - let strict = Options { ignore_uppercase: false, ignore_numbers: false, ..defaults }; - assert_eq!(word_to_check(&d, "PKO", &strict), Some("PKO")); - assert_eq!(word_to_check(&d, "B2B", &strict), Some("B2B")); - assert_eq!(word_to_check(&d, "PRZEDSIĘBIORSTWO", &defaults), Some("PRZEDSIĘBIORSTWO"), "only short capitals are acronyms"); - } -} diff --git a/crates/proof/src/tr/test_dict.rs b/crates/proof/src/tr/test_dict.rs deleted file mode 100644 index de4a13195..000000000 --- a/crates/proof/src/tr/test_dict.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! A tiny synthetic Turkish Hunspell dictionary for tests (written for these tests; not taken from -//! any real dictionary). Real dictionaries are installed on the system, never bundled. - -/// No affix rules: the forms the tests look up are listed whole. -pub const AFF: &str = "SET UTF-8\n"; - -/// The words the Turkish tests expect to be known. -pub const DIC: &str = "7 -kitap -kitaplar -ev -evde -olacaklardan -sonra -geldin -"; - -pub fn install() { - if let Err(e) = super::install_dictionary(AFF, DIC) { - panic!("test dictionary: {e}"); - } -} From 36400f69eb4de86087bd60b93ae6c86fe16213b0 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 19:26:24 +0000 Subject: [PATCH 12/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20no=5Flangua?= =?UTF-8?q?ge=5Fdata=20says=20what=20it=20probes=20(the=20embedded=20en-us?= =?UTF-8?q?=20list,=20present=20exactly=20when=20CRAFT=5FI18N=5FDIR's=20da?= =?UTF-8?q?ta=20is)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- crates/engine/src/tests.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 70d2eab87..5d3515010 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -19,10 +19,12 @@ mod typing; mod wordart; /// True, after saying so on stderr, when this build has no Language data embedded (craft-proof -/// built without `CRAFT_I18N_DIR`). Without it English still spells from craft-proof's built-in -/// word list, but there are no hyphenation patterns and no American/British variant table, so the -/// tests that check those return early, as craft-proof's own do; with the data (CI, releases) they -/// all run. +/// built without `CRAFT_I18N_DIR`). The probe is craft-proof's public `english_source`: the +/// embedded en-us word list is there exactly when the craft-i18n data was embedded, since +/// `CRAFT_I18N_DIR` brings all of it or none. Without it English still spells from craft-proof's +/// built-in word list, but there are no hyphenation patterns and no American/British variant +/// table, so the tests that check those return early, as craft-proof's own do; with the data (CI, +/// releases) they all run. pub(crate) fn no_language_data(test: &str) -> bool { let missing = wordcraft_proof::english_source(wordcraft_proof::ProofLang::En) != Some(wordcraft_proof::EnglishSource::Embedded); if missing { From b545bc27578072f6ccb1dcc967211dc6ef7b4827 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 20:39:14 +0000 Subject: [PATCH 13/15] CP1.3: Printing checks out craft-fonts too, so the CJK interface languages from craft-i18n have glyphs in the UI tests Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- .github/workflows/printing.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/printing.yml b/.github/workflows/printing.yml index ba843344e..129b0f99b 100644 --- a/.github/workflows/printing.yml +++ b/.github/workflows/printing.yml @@ -49,6 +49,19 @@ jobs: - name: Check out craft-i18n uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main + # The catalogs include CJK interface languages, so the UI tests need the suite's fonts, as in + # the gate (same commit as ci.yml and release.yml). + - name: Check out craft-fonts + uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main + with: + ref: b076417b5880bd4154bd22f00695b1d28c6bea83 + + - name: Build with craft-fonts + shell: bash + run: | + echo "CRAFT_FONTS_DIR=$GITHUB_WORKSPACE/craft-fonts" >> "$GITHUB_ENV" + echo "CRAFT_FONTS_REQUIRED=1" >> "$GITHUB_ENV" + - name: Install Linux build and software-rendering dependencies if: runner.os == 'Linux' run: | From b7dad64d946eb3450e3a712662d14f14eb1408b8 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Mon, 2 Nov 2026 00:31:32 +0000 Subject: [PATCH 14/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20no-data=20p?= =?UTF-8?q?roofing=20described=20right,=20NOTICE=20names=20craft-i18n's=20?= =?UTF-8?q?hyphenation.pat=20files,=20Flatpak=20dictionaries=20folder,=20p?= =?UTF-8?q?rinting=20checkout=20without=20credentials,=20data=20gate=20ask?= =?UTF-8?q?s=20the=20variant=20table=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit on #5202: - AGENTS.md, ATTRIBUTION.md, README.md, docs/architecture.md: without CRAFT_I18N_DIR English spelling falls back to an installed Hunspell English or craft-proof's built-in Moby list, and only pattern hyphenation is off (Bulgarian, Turkish and Russian hyphenate by rule); they said English wasn't spell-checked. (CLAUDE.md carries the same sentence; left for the maintainer, as agents don't edit it.) - NOTICE: craft-i18n's patterns are language-data//hyphenation.pat, not en-us.pat, pl.pat, pt.pat, de.pat, it.pat. - docs/spelling-dictionaries.md: the Flatpak's dictionaries folder is ~/.var/app/ai.storyteller.wordcraft/data/wordcraft/dictionaries; the host's folders are outside its sandbox. - printing.yml: the repository checkout doesn't persist credentials. - tests.rs no_language_data: also probes the American/British variant table, so a craft-i18n checkout missing some files skips the dialect tests instead of failing them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- .github/workflows/printing.yml | 2 ++ AGENTS.md | 2 +- ATTRIBUTION.md | 2 +- NOTICE | 10 +++++----- README.md | 4 +++- crates/engine/src/tests.rs | 17 +++++++++-------- docs/architecture.md | 2 +- docs/spelling-dictionaries.md | 9 ++++++++- 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/printing.yml b/.github/workflows/printing.yml index 129b0f99b..1a5dad4c8 100644 --- a/.github/workflows/printing.yml +++ b/.github/workflows/printing.yml @@ -43,6 +43,8 @@ jobs: CARGO_INCREMENTAL: '0' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # The interface catalogs and craft-proof's Language data come from craft-i18n at the commit in # craft-i18n.rev, so these tests build with the same data as the gate (ci.yml). diff --git a/AGENTS.md b/AGENTS.md index e1ab19f45..04fb60a52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ People trust WordCraft with their writing; a crash loses their work. **This outr - Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily". - Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it. - **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file. -- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds but English isn't spell-checked and hyphenation is off, so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. +- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds, but English spelling falls back to an installed Hunspell English or the built-in Moby word list (with no American/British variant table) and pattern hyphenation is off (Bulgarian, Turkish and Russian still hyphenate by rule), so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. - Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded. ## Clean room diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 30ba3d511..36c3b38a3 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -61,4 +61,4 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra | `docs/brand/artcraft-mark.svg` | ArtCraft Team | ArtCraft brand kit (project owner) | ArtCraft trademark, `docs/brand/LICENSE-brand.txt` | Not open source; usable unmodified only | | craft-fonts (optional build input, not files in this repo): BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans CJK SC, Noto Sans Arabic | see https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md | embedded only when built with `CRAFT_FONTS_DIR` (all official releases) | OFL-1.1 | Japanese and Chinese UI, Japanese document fallback | | craft-i18n (optional build input, not files in this repo): the interface translations | see https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md, which credits every translator of WordCraft's former in-repo catalogs (`crates/ui-egui/src/i18n/*.tsv`, migrated at `wordcraft@806a945`) | embedded only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | MIT OR Apache-2.0 | Interface languages other than English; clean-room translations from the English interface labels | -| craft-i18n Language data (optional build input, not files in this repo): the spelling word lists and hyphenation patterns (`language-data/`: the Moby Hyphenator II list and our patterns for English, `en-extra.txt`, `en-variants.txt`, `ru-extra.txt`, and the hyph-utf8 German, Polish, Portuguese and Italian patterns) | see the Language data section of https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md (authors, upstream sources with SHA-256, licences); moved there from `assets/hyphenation/` and `assets/spelling/` byte for byte | embedded by craft-proof (storytold/craft-libs) only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | Public Domain / CC0-1.0, MIT, BSD-3-Clause, MIT OR Apache-2.0 (per file; see NOTICE) | Spelling and hyphenation; without it English is not spell-checked and hyphenation is off | +| craft-i18n Language data (optional build input, not files in this repo): the spelling word lists and hyphenation patterns (`language-data/`: the Moby Hyphenator II list and our patterns for English, `en-extra.txt`, `en-variants.txt`, `ru-extra.txt`, and the hyph-utf8 German, Polish, Portuguese and Italian patterns) | see the Language data section of https://github.com/storytold/craft-i18n/blob/main/ATTRIBUTION.md (authors, upstream sources with SHA-256, licences); moved there from `assets/hyphenation/` and `assets/spelling/` byte for byte | embedded by craft-proof (storytold/craft-libs) only when built with `CRAFT_I18N_DIR`, at the commit in `craft-i18n.rev` (all official releases) | Public Domain / CC0-1.0, MIT, BSD-3-Clause, MIT OR Apache-2.0 (per file; see NOTICE) | Spelling and hyphenation; without it English spelling falls back to an installed Hunspell English or craft-proof's built-in Moby list, and pattern hyphenation is off | diff --git a/NOTICE b/NOTICE index 7d762fb8d..99d190fbe 100644 --- a/NOTICE +++ b/NOTICE @@ -26,11 +26,11 @@ list (author, source, license) is in ATTRIBUTION.md. CRAFT_I18N_DIR do not include them. - English word list (craft-i18n language-data/en-us/en-us.dic) — derived from the Moby Hyphenator II list by Grady Ward, public domain (Project Gutenberg eBook #3204). The hyphenation patterns - (craft-i18n language-data/en-us/en-us.pat) were trained from it and are released as CC0-1.0. -- Polish hyphenation patterns (craft-i18n language-data/pl/pl.pat) — hyph-pl.tex from the hyph-utf8 + (craft-i18n language-data/en-us/hyphenation.pat) were trained from it and are released as CC0-1.0. +- Polish hyphenation patterns (craft-i18n language-data/pl/hyphenation.pat) — hyph-pl.tex from the hyph-utf8 package, Copyright (C) 1987-1995 Hanna Kołodziejska, Bogusław Jackowski, Marek Ryćko, bundled unmodified and used under the MIT License; the copyright and permission notice is in the file. -- Portuguese hyphenation patterns (craft-i18n language-data/pt/pt.pat) — hyph-pt.tex from the hyph-utf8 +- Portuguese hyphenation patterns (craft-i18n language-data/pt/hyphenation.pat) — hyph-pt.tex from the hyph-utf8 package, bundled unmodified and used under the BSD 3-Clause License: Copyright (C) 1987, 1994, 1996, 2015 Pedro J. de Rezende, 1996, 2015 J. Joao Dias Almeida, @@ -56,7 +56,7 @@ list (author, source, license) is in ATTRIBUTION.md. 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. -- German hyphenation patterns (craft-i18n language-data/de/de.pat) — hyph-de-1996.tex (reformed spelling) +- German hyphenation patterns (craft-i18n language-data/de/hyphenation.pat) — hyph-de-1996.tex (reformed spelling) from the hyph-utf8 package, bundled unmodified and used under the MIT License; the copyright and permission notice is in the file: @@ -78,7 +78,7 @@ list (author, source, license) is in ATTRIBUTION.md. FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- Italian hyphenation patterns (craft-i18n language-data/it/it.pat) — hyph-it.tex from the hyph-utf8 +- Italian hyphenation patterns (craft-i18n language-data/it/hyphenation.pat) — hyph-it.tex from the hyph-utf8 package, Copyright (C) 2008-2011 Claudio Beccari, bundled unmodified and used under the MIT License (offered under LPPL 1.3+ or MIT); the copyright and permission notice is in the file: diff --git a/README.md b/README.md index 2ddc4892f..2db5753ef 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,9 @@ cargo run --release -p wordcraft -- report.docx # open a document The interface translations and the spelling and hyphenation data come from [craft-i18n](https://github.com/storytold/craft-i18n), an optional build input. To build with them, check it out at the commit in `craft-i18n.rev` and point `CRAFT_I18N_DIR` at it (an absolute -path); without it WordCraft builds in English with no English spell-checking and no hyphenation: +path); without it WordCraft builds with an English interface, English spelling falls back to an +installed Hunspell English or craft-proof's built-in word list, and only Bulgarian, Turkish and +Russian hyphenate (by rule): ```sh git clone https://github.com/storytold/craft-i18n ../craft-i18n diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 5d3515010..d5033394e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -18,15 +18,16 @@ mod revision_checks; mod typing; mod wordart; -/// True, after saying so on stderr, when this build has no Language data embedded (craft-proof -/// built without `CRAFT_I18N_DIR`). The probe is craft-proof's public `english_source`: the -/// embedded en-us word list is there exactly when the craft-i18n data was embedded, since -/// `CRAFT_I18N_DIR` brings all of it or none. Without it English still spells from craft-proof's -/// built-in word list, but there are no hyphenation patterns and no American/British variant -/// table, so the tests that check those return early, as craft-proof's own do; with the data (CI, -/// releases) they all run. +/// True, after saying so on stderr, when this build lacks the Language data the proofing tests +/// need (craft-proof built without `CRAFT_I18N_DIR`). The probes are craft-proof's public +/// `english_source` (the embedded English dictionary) and its American/British variant table +/// (`colour` → `color`): a craft-i18n checkout without some of the files embeds only the rest, +/// so both are asked. Without them English still spells from craft-proof's built-in word list, +/// but there are no hyphenation patterns and no variant table, so the tests that check those +/// return early, as craft-proof's own do; with the data (CI, releases) they all run. pub(crate) fn no_language_data(test: &str) -> bool { - let missing = wordcraft_proof::english_source(wordcraft_proof::ProofLang::En) != Some(wordcraft_proof::EnglishSource::Embedded); + let missing = wordcraft_proof::english_source(wordcraft_proof::ProofLang::En) != Some(wordcraft_proof::EnglishSource::Embedded) + || wordcraft_proof::en_variants::preferred("colour", wordcraft_proof::ProofLang::En).as_deref() != Some("color"); if missing { eprintln!("{test}: skipped, needs the Language data (set CRAFT_I18N_DIR to a craft-i18n checkout)"); } diff --git a/docs/architecture.md b/docs/architecture.md index 9b26fa4e3..968f521d1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ crate name links to its source tree. | L1 | [`doc`](../crates/doc/src) | 23,719 | Document model: stories, paragraphs, runs, tables, sections, styles, numbering, fields, notes, comments, revisions and equations; edit primitives and bidi | | L1 | [`fonts`](../crates/fonts/src) | 7,088 | Font database, metrics, outlines, HarfRust shaping, Word font substitution and OpenType MATH | | L1 | [`metafile`](../crates/metafile/src) | 3,614 | Shared Windows Metafile parsing and drawing primitives used by format readers and renderers | -| L1 (external) | [`craft-proof`](https://github.com/storytold/craft-libs/tree/main/crates/proof) (package alias `wordcraft-proof`) | — | Proofing moved to craft-libs (CP1, 2026-11): fifteen spelling dictionaries: English (US, GB, CA), Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish (Spain, Mexico, Latin America, Uruguay); language modules provide spelling, suggestions and grammar, while Bulgarian and German have hyphenation only. Its word lists and patterns are craft-i18n's `language-data/`, embedded from `CRAFT_I18N_DIR` at the commit in `craft-i18n.rev` (CI and releases set `CRAFT_I18N_REQUIRED=1`); without them English is not spell-checked and hyphenation is off | +| L1 (external) | [`craft-proof`](https://github.com/storytold/craft-libs/tree/main/crates/proof) (package alias `wordcraft-proof`) | — | Proofing moved to craft-libs (CP1, 2026-11): fifteen spelling dictionaries: English (US, GB, CA), Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish (Spain, Mexico, Latin America, Uruguay); language modules provide spelling, suggestions and grammar, while Bulgarian and German have hyphenation only. Its word lists and patterns are craft-i18n's `language-data/`, embedded from `CRAFT_I18N_DIR` at the commit in `craft-i18n.rev` (CI and releases set `CRAFT_I18N_REQUIRED=1`); without them English spelling falls back to an installed Hunspell English or craft-proof's built-in Moby list, and pattern hyphenation is off (Bulgarian, Turkish and Russian hyphenate by rule) | | L2 | [`docbin`](../crates/docbin/src) | 8,815 | Word 97–2003 `.doc` reader (FIB, piece table, FKPs, SPRMs, stylesheet, lists, tables, notes, pictures) over a CFB container | | L2 | [`docx`](../crates/docx/src) | 37,376 | OOXML WordprocessingML reader/writer (Transitional and Strict), OMML, custom properties and preserved VBA parts | | L2 | [`formats`](../crates/formats/src) | 23,058 | TXT, Markdown, HTML, RTF, ODT and LaTeX import/export through the shared document model | diff --git a/docs/spelling-dictionaries.md b/docs/spelling-dictionaries.md index a35fe35c4..62c62e827 100644 --- a/docs/spelling-dictionaries.md +++ b/docs/spelling-dictionaries.md @@ -1,6 +1,6 @@ # Installing spelling dictionaries -> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-11-01 · **Change:** trivial (WordCraft's own dictionaries folder is searched on macOS and Linux too, as on Windows, so a dictionary dropped there works without a setting; previously minor: ISO 8859-9 Turkish dictionaries are read, #3692; previously minor: Spanish: `es_ES`, `es_MX`, `es_419` and `es_UY` with their fallbacks, #3360, #3553; previously trivial: every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824) +> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-11-02 · **Change:** trivial (the Flatpak's own dictionaries folder; previously trivial: WordCraft's own dictionaries folder is searched on macOS and Linux too, as on Windows, so a dictionary dropped there works without a setting; previously minor: ISO 8859-9 Turkish dictionaries are read, #3692; previously minor: Spanish: `es_ES`, `es_MX`, `es_419` and `es_UY` with their fallbacks, #3360, #3553; previously trivial: every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824) English spelling works out of the box. Polish, Russian, Portuguese, Czech, Italian, Croatian, Turkish, Indonesian and Spanish spelling use a standard **Hunspell** dictionary (an `.aff` and a `.dic` file) that @@ -64,6 +64,12 @@ The first folder holding a matching pair wins: | macOS | `~/Library/Application Support/WordCraft/dictionaries`, `~/Library/Spelling`, `/Library/Spelling`, `/opt/homebrew/share/hunspell`, `/usr/local/share/hunspell`, `/Applications/LibreOffice.app/Contents/Resources/extensions/dict-*` | | Linux and BSD | `$XDG_DATA_HOME/wordcraft/dictionaries` (default `~/.local/share/wordcraft/dictionaries`), `~/.local/share/hunspell`, `/usr/share/hunspell`, `/usr/share/myspell`, `/usr/share/myspell/dicts`, `/usr/local/share/hunspell`, `/usr/local/share/myspell`, LibreOffice's `share/extensions/dict-*` under `/usr/lib/libreoffice` or `/opt/libreoffice` | +The Flatpak runs in a sandbox: its `$XDG_DATA_HOME` is +`~/.var/app/ai.storyteller.wordcraft/data`, so its own folder is +`~/.var/app/ai.storyteller.wordcraft/data/wordcraft/dictionaries`, and it can't see the host's +`/usr/share/hunspell` or `~/.local/share/hunspell` (the paths above are for the other builds). +Put dictionaries in that folder. + The web version can't read these folders, so these languages aren't spell-checked there. ## Example: Brazilian Portuguese (`pt_BR`) @@ -140,6 +146,7 @@ Never commit dictionary files to this repository: they stay on the user's system | Date | Change | Summary | |---|---|---| +| 2026-11-02 | trivial | The Flatpak's own dictionaries folder (`~/.var/app/ai.storyteller.wordcraft/data/wordcraft/dictionaries`); the host's folders are outside its sandbox | | 2026-10-22 | minor | Spanish (#3360, #3553): the file names for Spain, Mexico, Latin America and Uruguay and their fallbacks | | 2026-10-22 | trivial | Italian, Croatian, Turkish, Russian, Czech and Indonesian without a dictionary now show **(no dictionary)** (or **(dictionary unreadable)**) too, as this page already said; before, only Polish and Portuguese did. Indonesian is listed with its file names (#3150) | | 2026-11-01 | trivial | WordCraft's own `dictionaries` folder under the user's application data is searched on macOS (`~/Library/Application Support/WordCraft/dictionaries`) and Linux/BSD (`$XDG_DATA_HOME/wordcraft/dictionaries`) too, before the system folders, as it was on Windows (craft-proof; ported from storytold/craft-libs#31 by brzuchal) | From fbe68547f444f4f86dbda1d4a3bbc27677b2e140 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Mon, 2 Nov 2026 09:28:04 +0000 Subject: [PATCH 15/15] =?UTF-8?q?CP1.3:=20review=20=E2=80=94=20CLAUDE.md?= =?UTF-8?q?=20describes=20no-data=20proofing=20the=20same=20as=20AGENTS.md?= =?UTF-8?q?=20(English=20falls=20back=20to=20Hunspell=20or=20the=20Moby=20?= =?UTF-8?q?list;=20pattern=20hyphenation=20off)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1a51b19b9..c3d28358b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ People trust WordCraft with their writing; a crash loses their work. **This outr - Never copy, trace, screenshot-crop or recreate icons, images, templates, themes, clip art, fonts, cursors, sounds or any other asset from Microsoft Word/Office or any Microsoft, Adobe, Avid or Autodesk product. Not even "temporarily". - Every asset must be **original** (drawn in code — prefer this: `crates/ui-egui/src/icons.rs`), **public domain / CC0**, **OSI open source**, or **Creative Commons that allows redistribution**, or licensed open source by a contributor who made it. - **Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md)** (author, source, licence). `cargo xtask assets` (part of `cargo xtask ci`) fails otherwise. Add the row in the same commit as the file. -- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds but English isn't spell-checked and hyphenation is off, so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. +- Fonts: new font files go to [`storytold/craft-fonts`](https://github.com/storytold/craft-fonts), never this repo; it's an optional build input (`CRAFT_FONTS_DIR`). Spelling word lists and hyphenation patterns live in [`storytold/craft-i18n`](https://github.com/storytold/craft-i18n) `language-data/`, never this repo; they are embedded from `CRAFT_I18N_DIR` (pinned by `craft-i18n.rev`, which CI checks out). Without it the app builds, but English spelling falls back to an installed Hunspell English or the built-in Moby word list (with no American/British variant table) and pattern hyphenation is off (Bulgarian, Turkish and Russian still hyphenate by rule), so proofing tests need it. Documents asking for proprietary fonts (Calibri, Aptos, Cambria…) are rendered with installed system fonts or metric-compatible open substitutes (`crates/fonts/src/word.rs`) — never bundle proprietary fonts. - Templates, sample documents and themes are our own text and colours (`crates/engine/src/sample.rs`, `cmd/design.rs`). Demo images are public domain, with sources recorded. ## Clean room