mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-15 15:02:23 +00:00
Merge PR #1761: Hyphenation: German text follows the German patterns
crates/proof/src/lang.rs: main's English-dialect helpers kept; code() doc lists `de`. crates/proof/src/lib.rs: hostile-input test loops over main's languages plus De. crates/ui-egui/src/options.rs: language_name maps EnGb/EnCa and De to no dictionary name. ROADMAP.md: main's status lines; both progress-log and revision rows kept. docs/gaps.md: main's status line; gap 8 row is main's with German hyphenation added; revision rows unioned. docs/layout-parity.md: main's status line, Tabs and Drop caps rows; the PR's Hyphenation row; revision rows unioned. docs/localization-parity.md: main's status line; revision rows unioned.
This commit is contained in:
commit
6d90c345d4
13 files changed
+36915
-43
No files matched your search
@@ -16,6 +16,7 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
|
||||
| `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 `<image>`, 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") |
|
||||
|
||||
@@ -52,6 +52,28 @@ 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)
|
||||
from the hyph-utf8 package, bundled unmodified and used under the MIT License; the copyright
|
||||
and permission notice is in the file:
|
||||
|
||||
Copyright (c) 2013-2024 Stephan Hennig, Werner Lemberg, Günter Milde, Sander van Geloven,
|
||||
Georg Pfeiffer, Gisbert W. Selke, Tobias Wendorf, Keno Wehr
|
||||
|
||||
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.
|
||||
- Italian hyphenation patterns (assets/hyphenation/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:
|
||||
|
||||
+4
-2
@@ -113,10 +113,10 @@ Weights and evidence per area: [docs/target-app-parity.md](docs/target-app-parit
|
||||
| Vietnamese | vi | partial (#1434) | ~95% |
|
||||
|
||||
Also shipped: Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian (`sr`, `sr-latn`, #250), Russian (`ru`) and Polish (`pl`), all partial (~95%). Bulgarian (`bg`) covers everything the interface audit finds (2,419 entries); Swedish (`sv`) covers that too, plus 44 newer strings no other catalog has yet (2,504 entries). Estonian (`et`), Italian (`it`, #594) and Czech (`cs`) now have complete interface catalogs, and Turkish (`tr`) a complete 2,325-entry one (every key of the Russian catalog), all with
|
||||
system-locale selection and a saved language choice (Italian also has 51 strings none of the others had yet); Estonian, Turkish and Italian proofing are not included. Hungarian (`hu`) has a
|
||||
system-locale selection and a saved language choice (Italian also has 51 strings none of the others had yet); Estonian proofing is not included. Hungarian (`hu`) has a
|
||||
complete 2,419-entry catalog, also without proofing. Vietnamese (`vi`, #1434) has a complete
|
||||
2,448-entry catalog, also without proofing.
|
||||
Proofing: English, Polish, Portuguese and Russian (spelling, grammar, hyphenation), plus Czech and Indonesian (system Hunspell spelling, generic checks), chosen per run from its language. Word ships
|
||||
Proofing: English, Polish, Portuguese, Italian, Croatian, Turkish and Russian (spelling, grammar; hyphenation except Croatian), plus Czech and Indonesian (system Hunspell spelling, generic checks), chosen per run from its language; Bulgarian and German hyphenation only. Word ships
|
||||
30 interface languages and proofing for ~50. Detail: [docs/localization-parity.md](docs/localization-parity.md).
|
||||
|
||||
## Upcoming
|
||||
@@ -311,6 +311,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
|
||||
| 2026-10-14 | HTML import (#1381): header cells (`<th>`) are bold unless their own CSS says otherwise, and `text-align` on a cell, its row, row group or table aligns the cell's paragraphs (a paragraph's own alignment wins), so `wordcraft-cli convert page.html out.docx` writes the bold header and `w:jc` right-aligned cells. HTML, Markdown and plain text, which give no page size, open on the region's paper (A4 in Italy, Letter in the US) like a new blank document, in the app and in `wordcraft-cli`; documents with their own page size keep it |
|
||||
| 2026-10-14 | Custom page sizes (#1433): Layout › Size ends with More Paper Sizes…, which opens Page Setup's new Paper group — a Paper size list (the named sizes, or Custom size) and Width and Height in the measurement unit; OK sets the size only when it changed, and a wider-than-tall size comes out landscape. `layout.size` now clamps each side to Word's 0.1"–22" and refuses a page the margins don't fit (the margins rule: 36 pt of text room), and Page Setup checks the size and margins together before applying either |
|
||||
| 2026-10-14 | Bold text in Arial Black and other Black or Heavy fonts looks as in Word: older copies of these fonts (Arial Black in the core fonts) call themselves regular weight, so bold was drawn with an extra synthetic bold; a family named Black/Heavy now counts as that heavy |
|
||||
| 2026-10-14 | German text hyphenates by German rules: runs in German (`de`, `de-DE`, `de-AT`, `de-CH`) use hyph-utf8's German patterns for the reformed spelling (MIT), so automatic hyphenation gives `un-ter-lie-gen-de` and `öf-fent-lich-recht-li-cher` instead of English-pattern breaks like `unterlieg-ende` and `rechtli-cher`; Swiss `ss` splits like any double consonant (`Stras-se`). German still has no spelling or grammar marks (#1564) |
|
||||
| 2026-10-13 | Highlighted text reads on a dark page (View › Switch Modes, #1252): highlights and character shading are dimmed, keeping their hue (yellow turns a dim olive, as in Word), and the text on them is drawn light (coloured text below 4.5:1 is drawn white, and underlines and strikethroughs follow the same rule), instead of white text on bright yellow; light page, print and exports unchanged |
|
||||
| 2026-10-13 | Full screen (#980): F11, or View › Window › Full Screen, fills the screen with the window (⌃⌘F on macOS); F11 again or Escape gives back the window as it was. Next Field, which Word puts on F11, keeps Word's Alt+F1 and Alt+Shift+F1 (and Shift+F11). The web app leaves full screen to the browser's own F11 |
|
||||
| 2026-10-13 | Double-clicking the top or bottom margin of a page creates its header or footer when it has none and puts the caret in it, as in Word (#960): the one that page shows (different first page, odd and even pages), only in Print Layout; one left empty is taken away again when you leave it, so nothing empty is saved. `insert.editHeader`/`insert.editFooter` take `{"page"}` for agents |
|
||||
@@ -517,6 +518,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | Progress log: German text hyphenates by the German patterns |
|
||||
| 2026-10-21 | trivial | Progress log: File › Recover follows the app theme (#3243) |
|
||||
| 2026-10-19 | trivial | Progress log: forms protection keeps its content controls: no inserting, Properties or Remove Content Control |
|
||||
| 2026-10-19 | trivial | Progress log: text typed into a content control takes the control's formatting; localized placeholder styles recognised |
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+41
-24
@@ -15,6 +15,9 @@
|
||||
//!
|
||||
//! **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;
|
||||
@@ -36,6 +39,9 @@ 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).
|
||||
@@ -77,6 +83,12 @@ impl Patterns {
|
||||
static P: OnceLock<Patterns> = 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<Patterns> = OnceLock::new();
|
||||
P.get_or_init(|| Patterns::parse(DE_PAT))
|
||||
}
|
||||
}
|
||||
|
||||
/// Hyphenation limits from the paragraph's Hyphenation settings.
|
||||
@@ -130,11 +142,11 @@ fn core_breaks(core: &[char], lang: ProofLang) -> (Vec<usize>, Source) {
|
||||
if lang == ProofLang::Tr && core.iter().all(|&c| crate::tr::hyphen::is_letter(c)) {
|
||||
return (crate::tr::hyphen::points(core), Source::Rules);
|
||||
}
|
||||
if lang == ProofLang::Pt {
|
||||
return core_breaks_pt(core);
|
||||
}
|
||||
if lang == ProofLang::It {
|
||||
return core_breaks_it(core);
|
||||
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);
|
||||
@@ -178,25 +190,9 @@ fn core_breaks_pl(core: &[char]) -> (Vec<usize>, Source) {
|
||||
(heuristic_points_pl(core), Source::Heuristic)
|
||||
}
|
||||
|
||||
/// [`core_breaks_pl`] for Portuguese: the bundled patterns, then the generic heuristic.
|
||||
fn core_breaks_pt(core: &[char]) -> (Vec<usize>, Source) {
|
||||
let pats = Patterns::pt();
|
||||
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)
|
||||
}
|
||||
|
||||
/// [`core_breaks_pt`] for Italian: the bundled patterns (the file has no exception words),
|
||||
/// then the generic heuristic.
|
||||
fn core_breaks_it(core: &[char]) -> (Vec<usize>, Source) {
|
||||
let pats = Patterns::it();
|
||||
/// [`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<usize>, Source) {
|
||||
let s: String = core.iter().collect();
|
||||
if let Some(p) = pats.exception(&s) {
|
||||
return (p.to_vec(), Source::Dictionary);
|
||||
@@ -862,6 +858,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
@@ -49,6 +49,10 @@ pub enum ProofLang {
|
||||
/// `id_ID` dictionary, dictionary-driven suggestions and generic checks with Indonesian
|
||||
/// messages. Hyphenation keeps the fallback English patterns.
|
||||
Id,
|
||||
/// 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
|
||||
@@ -98,6 +102,8 @@ impl ProofLang {
|
||||
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 ["de", "deu", "ger"].iter().any(|c| primary.eq_ignore_ascii_case(c)) {
|
||||
ProofLang::De
|
||||
} else {
|
||||
ProofLang::Other
|
||||
}
|
||||
@@ -127,7 +133,7 @@ impl ProofLang {
|
||||
matches!(self, ProofLang::En | ProofLang::EnGb | ProofLang::EnCa)
|
||||
}
|
||||
|
||||
/// The primary language subtag (`en`, `pl`, `bg`, `tr`, `ru`, `cs`, `id`); empty for [`ProofLang::Other`].
|
||||
/// The primary language subtag (`en`, `pl`, `bg`, `tr`, `ru`, `cs`, `id`, `de`); empty for [`ProofLang::Other`].
|
||||
pub fn code(self) -> &'static str {
|
||||
match self {
|
||||
ProofLang::En | ProofLang::EnGb | ProofLang::EnCa => "en",
|
||||
@@ -140,6 +146,7 @@ impl ProofLang {
|
||||
ProofLang::Ru => "ru",
|
||||
ProofLang::Cs => "cs",
|
||||
ProofLang::Id => "id",
|
||||
ProofLang::De => "de",
|
||||
ProofLang::Other => "",
|
||||
}
|
||||
}
|
||||
@@ -275,7 +282,11 @@ mod tests {
|
||||
for t in ["tr", "tr-TR", "tr-CY", "TR_tr", "tur"] {
|
||||
assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Tr, "{t}");
|
||||
}
|
||||
for t in ["fr-FR", "de", "x-none", "ja-JP", "plx", "bgx", "trx", "csx", "idx", "inx", "e", "-"] {
|
||||
for t in ["de", "de-DE", "de-CH", "DE_at", "deu", "ger"] {
|
||||
assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::De, "{t}");
|
||||
}
|
||||
assert!(!ProofLang::De.is_proofed());
|
||||
for t in ["fr-FR", "dex", "x-none", "ja-JP", "plx", "bgx", "trx", "csx", "idx", "inx", "e", "-"] {
|
||||
assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Other, "{t}");
|
||||
}
|
||||
assert_eq!(ProofLang::from_tag(None), ProofLang::En);
|
||||
|
||||
@@ -34,10 +34,12 @@
|
||||
//! - **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.
|
||||
//! - **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),
|
||||
//! Bulgarian syllable rules, Russian word-division rules, heuristic.
|
||||
//! - **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)]
|
||||
@@ -724,7 +726,7 @@ pub fn is_correct_in_with(word: &str, lang: ProofLang, opts: &Options) -> bool {
|
||||
ProofLang::Ru => accepted_by_options(word, opts) || ru::is_correct(word),
|
||||
ProofLang::Cs => accepted_by_options(word, opts) || cs::is_correct(word),
|
||||
ProofLang::Id => accepted_by_options(word, opts) || id::is_correct(word),
|
||||
ProofLang::Bg | ProofLang::Other => true,
|
||||
ProofLang::Bg | ProofLang::De | ProofLang::Other => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,7 +742,7 @@ pub fn suggest_in(word: &str, max: usize, lang: ProofLang) -> Vec<String> {
|
||||
ProofLang::Ru => ru::suggest(word, max),
|
||||
ProofLang::Cs => cs::suggest(word, max),
|
||||
ProofLang::Id => id::suggest(word, max),
|
||||
ProofLang::Bg | ProofLang::Other => Vec::new(),
|
||||
ProofLang::Bg | ProofLang::De | ProofLang::Other => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,7 +768,7 @@ fn grammar_in(text: &str, lang: ProofLang, capital_first: bool) -> Vec<Issue> {
|
||||
// Czech has no sentence-capital rule, so a list item needs no exception.
|
||||
ProofLang::Cs => cs::grammar::check(text),
|
||||
ProofLang::Id => id::grammar::check(text),
|
||||
ProofLang::Bg | ProofLang::Other => Vec::new(),
|
||||
ProofLang::Bg | ProofLang::De | ProofLang::Other => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,7 +957,7 @@ mod tests {
|
||||
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);
|
||||
for l in [ProofLang::En, ProofLang::Bg, ProofLang::Other] {
|
||||
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.
|
||||
@@ -1682,7 +1684,7 @@ mod tests {
|
||||
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::Other] {
|
||||
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::Other] {
|
||||
let _ = check_spelling_in(&s, lang);
|
||||
let _ = check_grammar_in(&s, lang);
|
||||
let _ = is_correct_in(&s, lang);
|
||||
|
||||
@@ -116,13 +116,16 @@ pub struct Patterns {
|
||||
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`); outside any
|
||||
/// block, a token with a hyphen is an exception and anything else a pattern.
|
||||
/// `\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;
|
||||
@@ -136,12 +139,18 @@ impl Patterns {
|
||||
let mut tok = tok;
|
||||
if let Some(cmd) = tok.strip_prefix('\\') {
|
||||
let (name, rest) = cmd.split_once('{').unwrap_or((cmd, ""));
|
||||
block = if name == "hyphenation" { Block::Exceptions } else { Block::Patterns };
|
||||
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() {
|
||||
if !tok.is_empty() && block != Block::Skip {
|
||||
if block == Block::Exceptions || tok.contains('-') {
|
||||
p.insert_exception(tok);
|
||||
} else {
|
||||
@@ -337,6 +346,12 @@ mod tests {
|
||||
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");
|
||||
|
||||
@@ -352,8 +352,8 @@ fn language_name(lang: wordcraft_engine::proof::ProofLang) -> &'static str {
|
||||
ProofLang::Ru => tl!("Russian"),
|
||||
ProofLang::Cs => tl!("Czech"),
|
||||
ProofLang::Id => tl!("Indonesian"),
|
||||
// English uses its bundled word list; Bulgarian has no dictionary yet.
|
||||
ProofLang::En | ProofLang::EnGb | ProofLang::EnCa | ProofLang::Bg | ProofLang::Other => "",
|
||||
// English uses its bundled word list; Bulgarian and German have no dictionary yet.
|
||||
ProofLang::En | ProofLang::EnGb | ProofLang::EnCa | ProofLang::Bg | ProofLang::De | ProofLang::Other => "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -729,6 +729,7 @@ pub(crate) fn language_name(tag: Option<&str>) -> String {
|
||||
wordcraft_engine::proof::ProofLang::Ru => "Russian".into(),
|
||||
wordcraft_engine::proof::ProofLang::Cs => "Czech".into(),
|
||||
wordcraft_engine::proof::ProofLang::Id => "Indonesian".into(),
|
||||
wordcraft_engine::proof::ProofLang::De => "German (Germany)".into(),
|
||||
wordcraft_engine::proof::ProofLang::En if tag.eq_ignore_ascii_case("en") => "English (United States)".into(),
|
||||
_ => tag.to_string(),
|
||||
},
|
||||
|
||||
+2
-1
@@ -43,7 +43,7 @@ Active footnote and endnote text stays readable while editing (#2545). This fixe
|
||||
| 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**: 59 modal dialogs vs Word's ~100 (Drop Cap Options matches Word's menu and dialog; Split Cells and Convert Text to Table match Word's observed limits and defaults) | 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** (AES) can't be opened; password-protected `.docx` opens and saves (agile encryption, #55 closed by #281) | | `crates/docx/src/crypt.rs`; `crates/docbin/src/lib.rs` opens XOR/RC4/RC4 CryptoAPI `.doc` with a password (#1079, untested on Word-written files; AES `.doc` refused) | Corporate and legal users with old files | 3–5 | [file-format-parity.md](file-format-parity.md) |
|
||||
| 8 | **Proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech and Indonesian only**; no dictionaries, grammar or hyphenation for the other ~42 languages Word proofs (text in them is left unmarked) | | `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) and Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16), a language per run; Bulgarian has hyphenation only (#826); 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 and Indonesian | 20–40 | [localization-parity.md](localization-parity.md) |
|
||||
| 8 | **Proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech and Indonesian only**; no dictionaries or grammar for the other ~42 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) and Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16), 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 and Indonesian | 20–40 | [localization-parity.md](localization-parity.md) |
|
||||
| 10 | **Objects**: text in a freely rotated text box (free rotation and flips landed, #332; 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) |
|
||||
@@ -106,6 +106,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | Gap 8 narrowed: German text hyphenates by the TeX German patterns (reformed spelling); German spelling and grammar remain |
|
||||
| 2026-10-19 | trivial | Restrict Editing: with Filling in forms enforced, inserting content controls, their Properties and Remove Content Control are refused, as in Word (it greys them out), so a form's fields stay as they are and only their content is filled in |
|
||||
| 2026-10-19 | trivial | Content controls: text typed over a placeholder takes the control's own run properties (`w:sdtPr/w:rPr`, ECMA-376 §17.5.2.27), as in Word, and the placeholder style is recognised by its built-in name "Placeholder Text" whatever its localized id (German Word: `Platzhaltertext`), so entries in forms from localized Word no longer stay grey |
|
||||
| 2026-10-18 | trivial | Font list starts at its first row after clearing a no-match query; recent heading remains visible (#2068) |
|
||||
|
||||
@@ -47,7 +47,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
|
||||
| Paragraph boundaries inside field instructions | ✅ | Synthetic DOCX observed in Word: instruction-only paragraphs add no height; the empty result keeps a line. `docx::fixtures::field_code_spans_paragraphs`, `layout::tests::hidden_marks_take_no_space`. Other field-code display modes remain unverified | — |
|
||||
| Widow/orphan control, keep with next, keep lines together, page break before | ✅ | `lib.rs` keep_next/keep_lines; a line holding only a page break stays at the bottom of a full page instead of leaving a blank page (#534); a paragraph ending with a page break keeps its space after on the old page, so the next page starts at the top (compatibility modes 12 to 15, `layout::tests::space_before_at_the_top_of_a_page_like_word`); a three-line paragraph with room for two moves whole (neither a widow nor an orphan is allowed), as in Word | — |
|
||||
| Tabs (left, center, right, decimal, bar), leaders | 🟡 | Bar tabs untested. Positional tabs (`w:ptab`, ECMA-376 §17.3.3.23) go to the left edge, centre or right edge of the margins or indents whatever the tab stops, and to the next line when the text has passed that place (#2367, `layout::tests::right_positional_tab_ends_text_on_the_right_margin` and the three after it); not compared with Word on lines narrowed by floating objects or right-to-left paragraphs | 1–2 |
|
||||
| Hyphenation (automatic, zone from the document, words in capitals, soft hyphens, consecutive limit, Manual Hyphenation, #407) | 🟡 | English patterns only; other languages hyphenate wrongly or not at all | in localization |
|
||||
| Hyphenation (automatic, zone from the document, words in capitals, soft hyphens, consecutive limit, Manual Hyphenation, #407) | 🟡 | Per the text's language: English, Polish, Portuguese, Italian and German patterns, Bulgarian and Turkish syllable rules, Russian word-division rules; other languages use the English patterns | in localization |
|
||||
| Drop caps (dropped and in margin, lines, distance, font; `w:framePr w:dropCap`) | ✅ | 2026-10-17 | — |
|
||||
| Caret, clicks and selection inside a ligature (fi, ffi, lam-alef) | ✅ | The glyph's advance is shared evenly among the letters' grapheme clusters, mirrored in right-to-left text; a grapheme cluster (a letter with a combining mark, an emoji ZWJ sequence, an Indic conjunct) is never split. `layout::tests_ligature`, `engine::tests_typing::right_arrow_moves_through_a_ligature_one_letter_at_a_time` (#1974); observed in Word for Mac 16.113.4 | — |
|
||||
| All Caps and Small Caps: the full capital of each letter, so ß draws as SS (ff as FF); the capitals drawn for one character are one cluster, so the caret, selection, hit testing and line breaking treat ß as one character | ✅ | `layout::tests::caps_draw_sharp_s_as_two_capitals_in_one_cluster` (#1581). The PDF text layer still reads the document's lowercase letters until #1473 | — |
|
||||
@@ -216,6 +216,7 @@ Page-relative footer tables keep their physical wrap bounds without adding to fo
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | Hyphenation row: lists the languages with their own patterns or rules (English, Polish, Portuguese, Italian, German, Bulgarian, Turkish, Russian) |
|
||||
| 2026-10-17 | trivial | A float above its anchor whose wrap pushes the anchor onto the next page settles with no text under it on any page (#2048 review) |
|
||||
| 2026-10-15 | trivial | A float placed above its anchor paragraph (fixed on the page, or moved up from it) has the text of the paragraphs above the anchor on its page wrap around it, as in Word, instead of running behind it (#2048) |
|
||||
| 2026-10-18 | trivial | A word after a slash (" /word") wraps whole instead of splitting between two letters (#2963) |
|
||||
|
||||
@@ -110,6 +110,7 @@ other than envelopes (Letter, Legal, A4, B5 (JIS)) stay as they are (#1128).
|
||||
| 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 |
|
||||
| Turkish (`tr`, any region) | The Hunspell `tr_TR` dictionary installed on the system (same places as Polish; not bundled), read at runtime; proper nouns capitalized, apostrophe suffixes (`Ankara'da`, `WordCraft'ta`) and hyphenated compounds | missing diacritics / ASCII restoration first (`turkce` → `Türkçe`, `isik` → `ışık`), common confusion table (`yalnış` → `yanlış`, `herşey` → `her şey`), detached suffixes (`bende` → `ben de`, `geldinmi` → `geldi mi`), one typo | Turkish messages: repeated word (idiomatic reduplications allowed: `yavaş yavaş`), space before punctuation, double space, sentence capital (knows `vb.`, `vs.`, `prof.`, `dr.`), no comma before/after conjunctions (`ve`, `veya`, `de`), doubled punctuation | Turkish syllable rules (TDK rules); no external pattern file or word list needed |
|
||||
| German (`de`, any region) | — (no marks yet, #1564) | — | — | TeX German patterns for the reformed spelling (Trennmustermannschaft; MIT), no exception words; Swiss text uses them too. The patterns leave ambiguous compound joints unbroken on purpose (`Staubecken`, `Urinstinkt`), so German text can hyphenate a little less than in Word |
|
||||
| Indonesian (`id`, `id-ID`, `ind`, legacy `in`) | The system Hunspell `id_ID` dictionary, read at runtime (Debian/Ubuntu: `hunspell-id`; LibreOffice's `dict-id`; the same system, LibreOffice, Dictionaries folder and `WORDCRAFT_DICTIONARIES` search paths as Polish). No dictionary data is bundled; without an installed dictionary spelling stays off. Hyphenated reduplication is accepted whole (`kupu-kupu`) or by parts (`anak-anak`); hyphenated clitics after capitals and abbreviations (`KTP-nya`, `se-Indonesia`) are accepted | the same dictionary-driven ranking as Czech: MAP/REP/TRY/KEY tables of the loaded dictionary, missing spaces and a stem scan; nothing Indonesian hard-coded | Indonesian messages for repeated words (offering the hyphenated reduplication `anak-anak` first, then the single word), space before punctuation and double spaces only; no Indonesian-specific grammar rules | existing English fallback, as for `Other`; no Indonesian patterns bundled |
|
||||
| Any other | — (no marks) | — | — | the English patterns, as before |
|
||||
|
||||
@@ -198,6 +199,7 @@ WordCraft keeps 2,54 cm).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | German hyphenation: runs in `de` (any region) use the TeX German patterns for the reformed spelling (MIT) instead of the English ones; still no German spelling or grammar |
|
||||
| 2026-10-14 | trivial | The sentence-capital rule (English, Polish, Croatian, Italian, Turkish, Portuguese) skips the first word of a list item (#1741) |
|
||||
| 2026-10-16 | minor | Indonesian proofing (#498): id-ID spelling from the installed Hunspell `id_ID` dictionary (not bundled), Czech's dictionary-driven suggestions, repeated-word (hyphenated reduplication offered) and spacing checks with Indonesian messages; Review › Language, the status bar, Set Proofing Language and File › Options › Proofing list Indonesian; estimates unchanged |
|
||||
| 2026-10-16 | trivial | Read Aloud picks a voice for the sentence's language (run tag, else script) when the default voice doesn't speak it, reads Windows text as UTF-8 |
|
||||
|
||||
Reference in new issue
Block a user