mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-15 10:28:19 -06:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae022437c8 | ||
|
|
bc0e51ee4d | ||
|
|
ed66198574 | ||
|
|
e9750c5cb7 | ||
|
|
3d5a153e3d |
+5
-1
@@ -1 +1,5 @@
|
||||
craft-i18n.rev text eol=lf
|
||||
# Text lives in the repository with LF. A Windows checkout once committed 50 files with CRLF
|
||||
# (#6537) and `cargo fmt --check` still passed, so the gate could not see it. Normalise on add.
|
||||
* text=auto
|
||||
|
||||
craft-i18n.rev text eol=lf
|
||||
Generated
+1
@@ -7282,6 +7282,7 @@ version = "0.18.1"
|
||||
name = "wordcraft-doc"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-codepage",
|
||||
"base64 0.23.1",
|
||||
"log",
|
||||
"proptest",
|
||||
|
||||
@@ -36,6 +36,7 @@ wordcraft-mcp = { path = "crates/mcp" }
|
||||
wordcraft-zotero = { path = "crates/zotero" }
|
||||
wordcraft-control-key = { path = "crates/control-key" }
|
||||
wordcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
craft-codepage = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-metafile = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
|
||||
# foundation
|
||||
|
||||
@@ -124,7 +124,7 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes
|
||||
| **References** | Table of contents, footnotes and endnotes, citations and bibliography (APA, MLA, Chicago, IEEE), captions, table of figures, cross-references, index, table of authorities |
|
||||
| **Review** | Spelling and grammar with suggestions (English and Polish, by each run's language), thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector |
|
||||
| **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels |
|
||||
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어, Svenska, Hrvatski, Nederlands, Dansk, Română, العربية (mirrored) or Slovenščina (follows the system language by default) |
|
||||
| **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어, Svenska, Hrvatski, Nederlands, Dansk, Română, العربية (mirrored) Slovenščina or Bosanski (follows the system language by default) |
|
||||
| **Files** | .docx read/write, legacy 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images |
|
||||
|
||||
The honest picture, area by area, is in [ROADMAP.md](ROADMAP.md), the
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ workspace = true
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
craft-codepage = { workspace = true }
|
||||
wordcraft-geom = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
+44
-337
@@ -1,345 +1,52 @@
|
||||
//! Windows-1252 (the byte encoding legacy Word text pieces use) and the other single-byte Windows
|
||||
//! code pages RTF files name with `\ansicpg` or a font's `\fcharset` (Cyrillic 1251, Central
|
||||
//! European 1250, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai), Mac OS Roman
|
||||
//! (10000, RTF `\mac`), and the East Asian multibyte code pages (932 Shift-JIS, 936 GBK, 949 UHC,
|
||||
//! 950 Big5), whose bytes decode as runs rather than one at a time.
|
||||
//! Legacy character decoding delegates to craft-codepage; document fallback policy stays here.
|
||||
pub use craft_codepage::{
|
||||
MAC_ROMAN_CODE_PAGE, MULTIBYTE_CODE_PAGES, OEM_CODE_PAGES, SINGLE_BYTE_CODE_PAGES, cp1252, is_multibyte_code_page, multibyte_encoding_name,
|
||||
oem_byte, windows_byte,
|
||||
};
|
||||
|
||||
/// Windows-1252 code points for 0x80..=0x9F.
|
||||
const CP1252: [u16; 32] = [
|
||||
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 0x0090, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
|
||||
];
|
||||
|
||||
/// One Windows-1252 byte as a char.
|
||||
pub fn cp1252(b: u8) -> char {
|
||||
if (0x80..0xA0).contains(&b) {
|
||||
CP1252.get((b - 0x80) as usize).and_then(|u| char::from_u32(u32::from(*u))).unwrap_or('\u{FFFD}')
|
||||
} else {
|
||||
b as char
|
||||
}
|
||||
/// Word's supported single-byte document code pages (symbol-font policy is separate).
|
||||
pub fn is_byte_code_page(cp: u16) -> bool {
|
||||
SINGLE_BYTE_CODE_PAGES.contains(&cp) || OEM_CODE_PAGES.contains(&cp) || cp == MAC_ROMAN_CODE_PAGE
|
||||
}
|
||||
|
||||
/// Windows-874 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP874: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2026, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x00A0, 0x0E01, 0x0E02, 0x0E03,
|
||||
0x0E04, 0x0E05, 0x0E06, 0x0E07, 0x0E08, 0x0E09, 0x0E0A, 0x0E0B, 0x0E0C, 0x0E0D, 0x0E0E, 0x0E0F, 0x0E10, 0x0E11, 0x0E12, 0x0E13, 0x0E14, 0x0E15,
|
||||
0x0E16, 0x0E17, 0x0E18, 0x0E19, 0x0E1A, 0x0E1B, 0x0E1C, 0x0E1D, 0x0E1E, 0x0E1F, 0x0E20, 0x0E21, 0x0E22, 0x0E23, 0x0E24, 0x0E25, 0x0E26, 0x0E27,
|
||||
0x0E28, 0x0E29, 0x0E2A, 0x0E2B, 0x0E2C, 0x0E2D, 0x0E2E, 0x0E2F, 0x0E30, 0x0E31, 0x0E32, 0x0E33, 0x0E34, 0x0E35, 0x0E36, 0x0E37, 0x0E38, 0x0E39,
|
||||
0x0E3A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x0E3F, 0x0E40, 0x0E41, 0x0E42, 0x0E43, 0x0E44, 0x0E45, 0x0E46, 0x0E47, 0x0E48, 0x0E49, 0x0E4A, 0x0E4B,
|
||||
0x0E4C, 0x0E4D, 0x0E4E, 0x0E4F, 0x0E50, 0x0E51, 0x0E52, 0x0E53, 0x0E54, 0x0E55, 0x0E56, 0x0E57, 0x0E58, 0x0E59, 0x0E5A, 0x0E5B, 0xFFFD, 0xFFFD,
|
||||
0xFFFD, 0xFFFD,
|
||||
];
|
||||
|
||||
/// Windows-1250 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1250: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0xFFFD, 0x201E, 0x2026, 0x2020, 0x2021, 0xFFFD, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, 0x00A0, 0x02C7, 0x02D8, 0x0141,
|
||||
0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, 0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7,
|
||||
0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E,
|
||||
0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB,
|
||||
0x011B, 0x00ED, 0x00EE, 0x010F, 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD,
|
||||
0x0163, 0x02D9,
|
||||
];
|
||||
|
||||
/// Windows-1251 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1251: [u16; 128] = [
|
||||
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, 0x0452, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, 0x00A0, 0x040E, 0x045E, 0x0408,
|
||||
0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
|
||||
0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429,
|
||||
0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B,
|
||||
0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D,
|
||||
0x044E, 0x044F,
|
||||
];
|
||||
|
||||
/// Windows-1253 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1253: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0xFFFD, 0x2030, 0xFFFD, 0x2039, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0xFFFD, 0x203A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x00A0, 0x0385, 0x0386, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0xFFFD, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x2015, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x0384, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x0388, 0x0389, 0x038A, 0x00BB, 0x038C, 0x00BD, 0x038E, 0x038F, 0x0390, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
|
||||
0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0xFFFD, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9,
|
||||
0x03AA, 0x03AB, 0x03AC, 0x03AD, 0x03AE, 0x03AF, 0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB,
|
||||
0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD,
|
||||
0x03CE, 0xFFFD,
|
||||
];
|
||||
|
||||
/// Windows-1254 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1254: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFD, 0xFFFD, 0x0178, 0x00A0, 0x00A1, 0x00A2, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
|
||||
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9,
|
||||
0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF, 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
|
||||
0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131,
|
||||
0x015F, 0x00FF,
|
||||
];
|
||||
|
||||
/// Windows-1255 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1255: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0xFFFD, 0x2039, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0xFFFD, 0x203A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x00A0, 0x00A1, 0x00A2, 0x00A3,
|
||||
0x20AA, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00D7, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00F7, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x05B0, 0x05B1, 0x05B2, 0x05B3, 0x05B4, 0x05B5, 0x05B6, 0x05B7,
|
||||
0x05B8, 0x05B9, 0x05BA, 0x05BB, 0x05BC, 0x05BD, 0x05BE, 0x05BF, 0x05C0, 0x05C1, 0x05C2, 0x05C3, 0x05F0, 0x05F1, 0x05F2, 0x05F3, 0x05F4, 0xFFFD,
|
||||
0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB,
|
||||
0x05DC, 0x05DD, 0x05DE, 0x05DF, 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0xFFFD, 0xFFFD, 0x200E,
|
||||
0x200F, 0xFFFD,
|
||||
];
|
||||
|
||||
/// Windows-1256 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1256: [u16; 128] = [
|
||||
0x20AC, 0x067E, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0679, 0x2039, 0x0152, 0x0686, 0x0698, 0x0688, 0x06AF, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x06A9, 0x2122, 0x0691, 0x203A, 0x0153, 0x200C, 0x200D, 0x06BA, 0x00A0, 0x060C, 0x00A2, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x06BE, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x061B, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x061F, 0x06C1, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627,
|
||||
0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x00D7, 0x0637, 0x0638,
|
||||
0x0639, 0x063A, 0x0640, 0x0641, 0x0642, 0x0643, 0x00E0, 0x0644, 0x00E2, 0x0645, 0x0646, 0x0647, 0x0648, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
|
||||
0x0649, 0x064A, 0x00EE, 0x00EF, 0x064B, 0x064C, 0x064D, 0x064E, 0x00F4, 0x064F, 0x0650, 0x00F7, 0x0651, 0x00F9, 0x0652, 0x00FB, 0x00FC, 0x200E,
|
||||
0x200F, 0x06D2,
|
||||
];
|
||||
|
||||
/// Windows-1257 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1257: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0xFFFD, 0x201E, 0x2026, 0x2020, 0x2021, 0xFFFD, 0x2030, 0xFFFD, 0x2039, 0xFFFD, 0x00A8, 0x02C7, 0x00B8, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0xFFFD, 0x203A, 0xFFFD, 0x00AF, 0x02DB, 0xFFFD, 0x00A0, 0xFFFD, 0x00A2, 0x00A3,
|
||||
0x00A4, 0xFFFD, 0x00A6, 0x00A7, 0x00D8, 0x00A9, 0x0156, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00C6, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00F8, 0x00B9, 0x0157, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00E6, 0x0104, 0x012E, 0x0100, 0x0106, 0x00C4, 0x00C5, 0x0118, 0x0112,
|
||||
0x010C, 0x00C9, 0x0179, 0x0116, 0x0122, 0x0136, 0x012A, 0x013B, 0x0160, 0x0143, 0x0145, 0x00D3, 0x014C, 0x00D5, 0x00D6, 0x00D7, 0x0172, 0x0141,
|
||||
0x015A, 0x016A, 0x00DC, 0x017B, 0x017D, 0x00DF, 0x0105, 0x012F, 0x0101, 0x0107, 0x00E4, 0x00E5, 0x0119, 0x0113, 0x010D, 0x00E9, 0x017A, 0x0117,
|
||||
0x0123, 0x0137, 0x012B, 0x013C, 0x0161, 0x0144, 0x0146, 0x00F3, 0x014D, 0x00F5, 0x00F6, 0x00F7, 0x0173, 0x0142, 0x015B, 0x016B, 0x00FC, 0x017C,
|
||||
0x017E, 0x02D9,
|
||||
];
|
||||
|
||||
/// Windows-1258 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1258: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0xFFFD, 0x2039, 0x0152, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0xFFFD, 0x203A, 0x0153, 0xFFFD, 0xFFFD, 0x0178, 0x00A0, 0x00A1, 0x00A2, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
|
||||
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0300, 0x00CD, 0x00CE, 0x00CF, 0x0110, 0x00D1, 0x0309, 0x00D3, 0x00D4, 0x01A0, 0x00D6, 0x00D7, 0x00D8, 0x00D9,
|
||||
0x00DA, 0x00DB, 0x00DC, 0x01AF, 0x0303, 0x00DF, 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
|
||||
0x0301, 0x00ED, 0x00EE, 0x00EF, 0x0111, 0x00F1, 0x0323, 0x00F3, 0x00F4, 0x01A1, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x01B0,
|
||||
0x20AB, 0x00FF,
|
||||
];
|
||||
|
||||
/// Mac OS Roman (code page 10000, RTF `\mac`) code points for 0x80..=0xFF, with the euro sign at
|
||||
/// 0xDB (Mac OS 8.5 and later).
|
||||
const MAC_ROMAN: [u16; 128] = [
|
||||
0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, 0x00EA, 0x00EB,
|
||||
0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, 0x2020, 0x00B0, 0x00A2, 0x00A3,
|
||||
0x00A7, 0x2022, 0x00B6, 0x00DF, 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5,
|
||||
0x2202, 0x2211, 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x03A9, 0x00E6, 0x00F8, 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB,
|
||||
0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, 0x00FF, 0x0178,
|
||||
0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02, 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, 0x00CB, 0x00C8, 0x00CD, 0x00CE,
|
||||
0x00CF, 0x00CC, 0x00D3, 0x00D4, 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD,
|
||||
0x02DB, 0x02C7,
|
||||
];
|
||||
/// IBM PC (OEM) code page 437 code points for 0x80..=0xFF (the Unicode Consortium's CP437
|
||||
/// mapping file).
|
||||
const CP437: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, 0x00C9, 0x00E6,
|
||||
0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
|
||||
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4,
|
||||
0x221E, 0x03C6, 0x03B5, 0x2229, 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 850 (DOS Latin-1, Western European) code points for 0x80..=0xFF (the Unicode
|
||||
/// Consortium's CP850 mapping file).
|
||||
const CP850: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, 0x00C9, 0x00E6,
|
||||
0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x0192, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1,
|
||||
0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, 0x00F0, 0x00D0, 0x00CA, 0x00CB, 0x00C8, 0x0131, 0x00CD, 0x00CE, 0x00CF, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0x00FE, 0x00DE, 0x00DA, 0x00DB, 0x00D9,
|
||||
0x00FD, 0x00DD, 0x00AF, 0x00B4, 0x00AD, 0x00B1, 0x2017, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 852 (DOS Latin-2, Central European) code points for 0x80..=0xFF (the Unicode Consortium's CP852
|
||||
/// mapping file).
|
||||
const CP852: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x016F, 0x0107, 0x00E7, 0x0142, 0x00EB, 0x0150, 0x0151, 0x00EE, 0x0179, 0x00C4, 0x0106, 0x00C9, 0x0139,
|
||||
0x013A, 0x00F4, 0x00F6, 0x013D, 0x013E, 0x015A, 0x015B, 0x00D6, 0x00DC, 0x0164, 0x0165, 0x0141, 0x00D7, 0x010D, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x0104, 0x0105, 0x017D, 0x017E, 0x0118, 0x0119, 0x00AC, 0x017A, 0x010C, 0x015F, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1,
|
||||
0x00C2, 0x011A, 0x015E, 0x2563, 0x2551, 0x2557, 0x255D, 0x017B, 0x017C, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0102, 0x0103,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, 0x0111, 0x0110, 0x010E, 0x00CB, 0x010F, 0x0147, 0x00CD, 0x00CE, 0x011B, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x0162, 0x016E, 0x2580, 0x00D3, 0x00DF, 0x00D4, 0x0143, 0x0144, 0x0148, 0x0160, 0x0161, 0x0154, 0x00DA, 0x0155, 0x0170,
|
||||
0x00FD, 0x00DD, 0x0163, 0x00B4, 0x00AD, 0x02DD, 0x02DB, 0x02C7, 0x02D8, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x02D9, 0x0171, 0x0158, 0x0159,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 866 (DOS Cyrillic) code points for 0x80..=0xFF (the Unicode Consortium's CP866
|
||||
/// mapping file).
|
||||
const CP866: [u16; 128] = [
|
||||
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421,
|
||||
0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0430, 0x0431, 0x0432, 0x0433,
|
||||
0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
|
||||
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B,
|
||||
0x044C, 0x044D, 0x044E, 0x044F, 0x0401, 0x0451, 0x0404, 0x0454, 0x0407, 0x0457, 0x040E, 0x045E, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x2116, 0x00A4,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 737 (DOS Greek) code points for 0x80..=0xFF (the Unicode Consortium's CP737
|
||||
/// mapping file).
|
||||
const CP737: [u16; 128] = [
|
||||
0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3,
|
||||
0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC,
|
||||
0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
|
||||
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x03C9, 0x03AC, 0x03AD, 0x03AE, 0x03CA, 0x03AF, 0x03CC, 0x03CD, 0x03CB, 0x03CE, 0x0386, 0x0388,
|
||||
0x0389, 0x038A, 0x038C, 0x038E, 0x038F, 0x00B1, 0x2265, 0x2264, 0x03AA, 0x03AB, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 857 (DOS Turkish; U+FFFD where the byte is unassigned) code points for 0x80..=0xFF (the Unicode Consortium's CP857
|
||||
/// mapping file).
|
||||
const CP857: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x0131, 0x00C4, 0x00C5, 0x00C9, 0x00E6,
|
||||
0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x0130, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x015E, 0x015F, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x00F1, 0x00D1, 0x011E, 0x011F, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1,
|
||||
0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, 0x00BA, 0x00AA, 0x00CA, 0x00CB, 0x00C8, 0xFFFD, 0x00CD, 0x00CE, 0x00CF, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0xFFFD, 0x00D7, 0x00DA, 0x00DB, 0x00D9,
|
||||
0x00EC, 0x00FF, 0x00AF, 0x00B4, 0x00AD, 0x00B1, 0xFFFD, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// The glyphs the OEM code pages show for 0x01..=0x1F (smileys, card suits, arrows), as
|
||||
/// Windows' Alt+numpad entry produces them rather than the control characters.
|
||||
const OEM_GLYPHS: [u16; 31] = [
|
||||
0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, 0x25BA, 0x25C4, 0x2195,
|
||||
0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8, 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC,
|
||||
];
|
||||
|
||||
/// The OEM code pages [`oem_byte`] decodes.
|
||||
pub const OEM_CODE_PAGES: [u16; 6] = [437, 737, 850, 852, 857, 866];
|
||||
|
||||
/// One byte of an OEM code page in [`OEM_CODE_PAGES`] (any other code page reads as 437) as a
|
||||
/// char, with the graphic glyphs for 0x01..=0x1F and 0x7F (⌂); 0x00 stays NUL.
|
||||
pub fn oem_byte(code_page: u16, b: u8) -> char {
|
||||
let table: &[u16; 128] = match code_page {
|
||||
737 => &CP737,
|
||||
850 => &CP850,
|
||||
852 => &CP852,
|
||||
857 => &CP857,
|
||||
866 => &CP866,
|
||||
_ => &CP437,
|
||||
};
|
||||
let u = match b {
|
||||
0x01..=0x1F => OEM_GLYPHS.get(usize::from(b - 1)).copied(),
|
||||
0x7F => Some(0x2302),
|
||||
0x80..=0xFF => table.get(usize::from(b - 0x80)).copied(),
|
||||
_ => Some(u16::from(b)),
|
||||
};
|
||||
u.and_then(|u| char::from_u32(u32::from(u))).unwrap_or('\u{FFFD}')
|
||||
/// Decode document text without applying OEM control glyphs or symbol-font remapping.
|
||||
pub fn code_page_byte(cp: u16, b: u8) -> char {
|
||||
if b >= 0x80 && OEM_CODE_PAGES.contains(&cp) { oem_byte(cp, b) } else { windows_byte(cp, b) }
|
||||
}
|
||||
|
||||
/// The single-byte Windows code pages [`windows_byte`] decodes (1252 included).
|
||||
pub const SINGLE_BYTE_CODE_PAGES: [u16; 10] = [874, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258];
|
||||
|
||||
/// Mac OS Roman, RTF's `\mac` character set.
|
||||
pub const MAC_ROMAN_CODE_PAGE: u16 = 10000;
|
||||
|
||||
/// One byte of a single-byte code page as a char: a Windows one in [`SINGLE_BYTE_CODE_PAGES`] or
|
||||
/// Mac OS Roman ([`MAC_ROMAN_CODE_PAGE`]); any other code page reads as Windows-1252.
|
||||
pub fn windows_byte(code_page: u16, b: u8) -> char {
|
||||
let table: &[u16; 128] = match code_page {
|
||||
874 => &CP874,
|
||||
1250 => &CP1250,
|
||||
1251 => &CP1251,
|
||||
1253 => &CP1253,
|
||||
1254 => &CP1254,
|
||||
1255 => &CP1255,
|
||||
1256 => &CP1256,
|
||||
1257 => &CP1257,
|
||||
1258 => &CP1258,
|
||||
MAC_ROMAN_CODE_PAGE => &MAC_ROMAN,
|
||||
_ => return cp1252(b),
|
||||
};
|
||||
if b < 0x80 {
|
||||
return b as char;
|
||||
}
|
||||
table.get(usize::from(b - 0x80)).and_then(|u| char::from_u32(u32::from(*u))).unwrap_or('\u{FFFD}')
|
||||
/// Whether Word's document import accepts this encoding.
|
||||
pub fn is_text_code_page(cp: u16) -> bool {
|
||||
is_byte_code_page(cp) || is_multibyte_code_page(cp)
|
||||
}
|
||||
|
||||
/// Whether [`code_page_byte`] decodes `code_page`: a Windows one ([`SINGLE_BYTE_CODE_PAGES`]), an
|
||||
/// OEM (DOS) one ([`OEM_CODE_PAGES`]) or Mac OS Roman. Document formats (RTF) name all three.
|
||||
pub fn is_byte_code_page(code_page: u16) -> bool {
|
||||
SINGLE_BYTE_CODE_PAGES.contains(&code_page) || OEM_CODE_PAGES.contains(&code_page) || code_page == MAC_ROMAN_CODE_PAGE
|
||||
}
|
||||
|
||||
/// A byte of 8-bit document text in `code_page` (see [`is_byte_code_page`]) as a char. Below 0x80
|
||||
/// it is ASCII in every code page (text, so an OEM page's glyphs for control codes don't apply).
|
||||
pub fn code_page_byte(code_page: u16, b: u8) -> char {
|
||||
if b >= 0x80 && OEM_CODE_PAGES.contains(&code_page) { oem_byte(code_page, b) } else { windows_byte(code_page, b) }
|
||||
}
|
||||
|
||||
/// The East Asian multibyte code pages a document format names with `\ansicpg` or a font's `\cpg`:
|
||||
/// 932 Shift-JIS (Japanese), 936 GBK (Simplified Chinese), 949 UHC (Korean) and 950 Big5
|
||||
/// (Traditional Chinese).
|
||||
pub const MULTIBYTE_CODE_PAGES: [u16; 4] = [932, 936, 949, 950];
|
||||
|
||||
/// Whether `code_page` is one of the East Asian multibyte code pages ([`MULTIBYTE_CODE_PAGES`]):
|
||||
/// its bytes decode as a run, a lead byte together with its trail byte.
|
||||
pub fn is_multibyte_code_page(code_page: u16) -> bool {
|
||||
multibyte_encoding_name(code_page).is_some()
|
||||
}
|
||||
|
||||
/// The WHATWG encoding name of an East Asian multibyte code page (see [`MULTIBYTE_CODE_PAGES`]),
|
||||
/// for `encoding_rs::Encoding::for_label`; `None` for the single-byte code pages, whose bytes
|
||||
/// decode one at a time, and anything else.
|
||||
pub fn multibyte_encoding_name(code_page: u16) -> Option<&'static str> {
|
||||
Some(match code_page {
|
||||
932 => "shift_jis",
|
||||
936 => "gbk",
|
||||
949 => "euc-kr",
|
||||
950 => "big5",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a document format can name `code_page` for its text: a single-byte Windows, DOS or Mac
|
||||
/// one ([`is_byte_code_page`]) or an East Asian multibyte one ([`is_multibyte_code_page`]).
|
||||
pub fn is_text_code_page(code_page: u16) -> bool {
|
||||
is_byte_code_page(code_page) || is_multibyte_code_page(code_page)
|
||||
}
|
||||
|
||||
/// The code page of an RTF or Word font charset (`\fcharset`): 204 Russian → 1251, 238 Eastern
|
||||
/// European → 1250, 77 Mac → 10000, 254 PC / 255 OEM → 437, 128 Japanese → 932, 129 Korean → 949,
|
||||
/// 134 Simplified Chinese → 936, 136 Traditional Chinese → 950… `None` for the ANSI and default
|
||||
/// charsets (the document's code page applies) and symbol fonts.
|
||||
/// Preserve Word's existing symbol-font selection policy, not the shared symbol extension.
|
||||
pub fn charset_code_page(charset: i32) -> Option<u16> {
|
||||
Some(match charset {
|
||||
77 => MAC_ROMAN_CODE_PAGE,
|
||||
128 => 932,
|
||||
129 => 949,
|
||||
134 => 936,
|
||||
136 => 950,
|
||||
161 => 1253,
|
||||
162 => 1254,
|
||||
163 => 1258,
|
||||
177 => 1255,
|
||||
178 => 1256,
|
||||
186 => 1257,
|
||||
204 => 1251,
|
||||
222 => 874,
|
||||
238 => 1250,
|
||||
254 | 255 => 437,
|
||||
_ => return None,
|
||||
})
|
||||
if charset == 2 { None } else { craft_codepage::charset_code_page(charset) }
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
#[path = "encoding_legacy_tests.rs"]
|
||||
mod legacy;
|
||||
#[cfg(test)]
|
||||
mod parity {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn every_supported_byte_and_fallback_matches_original() {
|
||||
let pages = [0, 42, 437, 737, 850, 852, 857, 866, 874, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 10000, 9999, u16::MAX];
|
||||
for cp in pages {
|
||||
for b in 0..=u8::MAX {
|
||||
assert_eq!(windows_byte(cp, b), legacy::windows_byte(cp, b), "windows {cp}/{b}");
|
||||
assert_eq!(oem_byte(cp, b), legacy::oem_byte(cp, b), "oem {cp}/{b}");
|
||||
assert_eq!(code_page_byte(cp, b), legacy::code_page_byte(cp, b), "text {cp}/{b}");
|
||||
assert_eq!(cp1252(b), legacy::cp1252(b));
|
||||
}
|
||||
}
|
||||
for cp in 0..=u16::MAX {
|
||||
assert_eq!(is_byte_code_page(cp), legacy::is_byte_code_page(cp));
|
||||
assert_eq!(is_text_code_page(cp), legacy::is_text_code_page(cp));
|
||||
assert_eq!(multibyte_encoding_name(cp), legacy::multibyte_encoding_name(cp));
|
||||
}
|
||||
for charset in -1..=256 {
|
||||
assert_eq!(charset_code_page(charset), legacy::charset_code_page(charset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
// Frozen pre-cutover implementation, test-only parity oracle. Original authors retained in file history.
|
||||
//! Windows-1252 (the byte encoding legacy Word text pieces use) and the other single-byte Windows
|
||||
//! code pages RTF files name with `\ansicpg` or a font's `\fcharset` (Cyrillic 1251, Central
|
||||
//! European 1250, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai), Mac OS Roman
|
||||
//! (10000, RTF `\mac`), and the East Asian multibyte code pages (932 Shift-JIS, 936 GBK, 949 UHC,
|
||||
//! 950 Big5), whose bytes decode as runs rather than one at a time.
|
||||
|
||||
/// Windows-1252 code points for 0x80..=0x9F.
|
||||
const CP1252: [u16; 32] = [
|
||||
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 0x0090, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178,
|
||||
];
|
||||
|
||||
/// One Windows-1252 byte as a char.
|
||||
pub fn cp1252(b: u8) -> char {
|
||||
if (0x80..0xA0).contains(&b) {
|
||||
CP1252.get((b - 0x80) as usize).and_then(|u| char::from_u32(u32::from(*u))).unwrap_or('\u{FFFD}')
|
||||
} else {
|
||||
b as char
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows-874 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP874: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2026, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x00A0, 0x0E01, 0x0E02, 0x0E03,
|
||||
0x0E04, 0x0E05, 0x0E06, 0x0E07, 0x0E08, 0x0E09, 0x0E0A, 0x0E0B, 0x0E0C, 0x0E0D, 0x0E0E, 0x0E0F, 0x0E10, 0x0E11, 0x0E12, 0x0E13, 0x0E14, 0x0E15,
|
||||
0x0E16, 0x0E17, 0x0E18, 0x0E19, 0x0E1A, 0x0E1B, 0x0E1C, 0x0E1D, 0x0E1E, 0x0E1F, 0x0E20, 0x0E21, 0x0E22, 0x0E23, 0x0E24, 0x0E25, 0x0E26, 0x0E27,
|
||||
0x0E28, 0x0E29, 0x0E2A, 0x0E2B, 0x0E2C, 0x0E2D, 0x0E2E, 0x0E2F, 0x0E30, 0x0E31, 0x0E32, 0x0E33, 0x0E34, 0x0E35, 0x0E36, 0x0E37, 0x0E38, 0x0E39,
|
||||
0x0E3A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x0E3F, 0x0E40, 0x0E41, 0x0E42, 0x0E43, 0x0E44, 0x0E45, 0x0E46, 0x0E47, 0x0E48, 0x0E49, 0x0E4A, 0x0E4B,
|
||||
0x0E4C, 0x0E4D, 0x0E4E, 0x0E4F, 0x0E50, 0x0E51, 0x0E52, 0x0E53, 0x0E54, 0x0E55, 0x0E56, 0x0E57, 0x0E58, 0x0E59, 0x0E5A, 0x0E5B, 0xFFFD, 0xFFFD,
|
||||
0xFFFD, 0xFFFD,
|
||||
];
|
||||
|
||||
/// Windows-1250 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1250: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0xFFFD, 0x201E, 0x2026, 0x2020, 0x2021, 0xFFFD, 0x2030, 0x0160, 0x2039, 0x015A, 0x0164, 0x017D, 0x0179, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0x0161, 0x203A, 0x015B, 0x0165, 0x017E, 0x017A, 0x00A0, 0x02C7, 0x02D8, 0x0141,
|
||||
0x00A4, 0x0104, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x015E, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x017B, 0x00B0, 0x00B1, 0x02DB, 0x0142, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x0105, 0x015F, 0x00BB, 0x013D, 0x02DD, 0x013E, 0x017C, 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7,
|
||||
0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, 0x0158, 0x016E,
|
||||
0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, 0x010D, 0x00E9, 0x0119, 0x00EB,
|
||||
0x011B, 0x00ED, 0x00EE, 0x010F, 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD,
|
||||
0x0163, 0x02D9,
|
||||
];
|
||||
|
||||
/// Windows-1251 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1251: [u16; 128] = [
|
||||
0x0402, 0x0403, 0x201A, 0x0453, 0x201E, 0x2026, 0x2020, 0x2021, 0x20AC, 0x2030, 0x0409, 0x2039, 0x040A, 0x040C, 0x040B, 0x040F, 0x0452, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0x0459, 0x203A, 0x045A, 0x045C, 0x045B, 0x045F, 0x00A0, 0x040E, 0x045E, 0x0408,
|
||||
0x00A4, 0x0490, 0x00A6, 0x00A7, 0x0401, 0x00A9, 0x0404, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0407, 0x00B0, 0x00B1, 0x0406, 0x0456, 0x0491, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x0451, 0x2116, 0x0454, 0x00BB, 0x0458, 0x0405, 0x0455, 0x0457, 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
|
||||
0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429,
|
||||
0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B,
|
||||
0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D,
|
||||
0x044E, 0x044F,
|
||||
];
|
||||
|
||||
/// Windows-1253 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1253: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0xFFFD, 0x2030, 0xFFFD, 0x2039, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0xFFFD, 0x203A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x00A0, 0x0385, 0x0386, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0xFFFD, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x2015, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x0384, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x0388, 0x0389, 0x038A, 0x00BB, 0x038C, 0x00BD, 0x038E, 0x038F, 0x0390, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
|
||||
0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0xFFFD, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9,
|
||||
0x03AA, 0x03AB, 0x03AC, 0x03AD, 0x03AE, 0x03AF, 0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB,
|
||||
0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD,
|
||||
0x03CE, 0xFFFD,
|
||||
];
|
||||
|
||||
/// Windows-1254 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1254: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFD, 0xFFFD, 0x0178, 0x00A0, 0x00A1, 0x00A2, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
|
||||
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, 0x00D8, 0x00D9,
|
||||
0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF, 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
|
||||
0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131,
|
||||
0x015F, 0x00FF,
|
||||
];
|
||||
|
||||
/// Windows-1255 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1255: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0xFFFD, 0x2039, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0xFFFD, 0x203A, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x00A0, 0x00A1, 0x00A2, 0x00A3,
|
||||
0x20AA, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00D7, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00F7, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x05B0, 0x05B1, 0x05B2, 0x05B3, 0x05B4, 0x05B5, 0x05B6, 0x05B7,
|
||||
0x05B8, 0x05B9, 0x05BA, 0x05BB, 0x05BC, 0x05BD, 0x05BE, 0x05BF, 0x05C0, 0x05C1, 0x05C2, 0x05C3, 0x05F0, 0x05F1, 0x05F2, 0x05F3, 0x05F4, 0xFFFD,
|
||||
0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7, 0x05D8, 0x05D9, 0x05DA, 0x05DB,
|
||||
0x05DC, 0x05DD, 0x05DE, 0x05DF, 0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7, 0x05E8, 0x05E9, 0x05EA, 0xFFFD, 0xFFFD, 0x200E,
|
||||
0x200F, 0xFFFD,
|
||||
];
|
||||
|
||||
/// Windows-1256 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1256: [u16; 128] = [
|
||||
0x20AC, 0x067E, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0679, 0x2039, 0x0152, 0x0686, 0x0698, 0x0688, 0x06AF, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x06A9, 0x2122, 0x0691, 0x203A, 0x0153, 0x200C, 0x200D, 0x06BA, 0x00A0, 0x060C, 0x00A2, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x06BE, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x061B, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x061F, 0x06C1, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627,
|
||||
0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x00D7, 0x0637, 0x0638,
|
||||
0x0639, 0x063A, 0x0640, 0x0641, 0x0642, 0x0643, 0x00E0, 0x0644, 0x00E2, 0x0645, 0x0646, 0x0647, 0x0648, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
|
||||
0x0649, 0x064A, 0x00EE, 0x00EF, 0x064B, 0x064C, 0x064D, 0x064E, 0x00F4, 0x064F, 0x0650, 0x00F7, 0x0651, 0x00F9, 0x0652, 0x00FB, 0x00FC, 0x200E,
|
||||
0x200F, 0x06D2,
|
||||
];
|
||||
|
||||
/// Windows-1257 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1257: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0xFFFD, 0x201E, 0x2026, 0x2020, 0x2021, 0xFFFD, 0x2030, 0xFFFD, 0x2039, 0xFFFD, 0x00A8, 0x02C7, 0x00B8, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0xFFFD, 0x2122, 0xFFFD, 0x203A, 0xFFFD, 0x00AF, 0x02DB, 0xFFFD, 0x00A0, 0xFFFD, 0x00A2, 0x00A3,
|
||||
0x00A4, 0xFFFD, 0x00A6, 0x00A7, 0x00D8, 0x00A9, 0x0156, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00C6, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00F8, 0x00B9, 0x0157, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00E6, 0x0104, 0x012E, 0x0100, 0x0106, 0x00C4, 0x00C5, 0x0118, 0x0112,
|
||||
0x010C, 0x00C9, 0x0179, 0x0116, 0x0122, 0x0136, 0x012A, 0x013B, 0x0160, 0x0143, 0x0145, 0x00D3, 0x014C, 0x00D5, 0x00D6, 0x00D7, 0x0172, 0x0141,
|
||||
0x015A, 0x016A, 0x00DC, 0x017B, 0x017D, 0x00DF, 0x0105, 0x012F, 0x0101, 0x0107, 0x00E4, 0x00E5, 0x0119, 0x0113, 0x010D, 0x00E9, 0x017A, 0x0117,
|
||||
0x0123, 0x0137, 0x012B, 0x013C, 0x0161, 0x0144, 0x0146, 0x00F3, 0x014D, 0x00F5, 0x00F6, 0x00F7, 0x0173, 0x0142, 0x015B, 0x016B, 0x00FC, 0x017C,
|
||||
0x017E, 0x02D9,
|
||||
];
|
||||
|
||||
/// Windows-1258 code points for 0x80..=0xFF (U+FFFD where the byte is unassigned).
|
||||
const CP1258: [u16; 128] = [
|
||||
0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0xFFFD, 0x2039, 0x0152, 0xFFFD, 0xFFFD, 0xFFFD, 0xFFFD, 0x2018,
|
||||
0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0xFFFD, 0x203A, 0x0153, 0xFFFD, 0xFFFD, 0x0178, 0x00A0, 0x00A1, 0x00A2, 0x00A3,
|
||||
0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
|
||||
0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, 0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
|
||||
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0300, 0x00CD, 0x00CE, 0x00CF, 0x0110, 0x00D1, 0x0309, 0x00D3, 0x00D4, 0x01A0, 0x00D6, 0x00D7, 0x00D8, 0x00D9,
|
||||
0x00DA, 0x00DB, 0x00DC, 0x01AF, 0x0303, 0x00DF, 0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x00E5, 0x00E6, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
|
||||
0x0301, 0x00ED, 0x00EE, 0x00EF, 0x0111, 0x00F1, 0x0323, 0x00F3, 0x00F4, 0x01A1, 0x00F6, 0x00F7, 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x01B0,
|
||||
0x20AB, 0x00FF,
|
||||
];
|
||||
|
||||
/// Mac OS Roman (code page 10000, RTF `\mac`) code points for 0x80..=0xFF, with the euro sign at
|
||||
/// 0xDB (Mac OS 8.5 and later).
|
||||
const MAC_ROMAN: [u16; 128] = [
|
||||
0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, 0x00E0, 0x00E2, 0x00E4, 0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, 0x00EA, 0x00EB,
|
||||
0x00ED, 0x00EC, 0x00EE, 0x00EF, 0x00F1, 0x00F3, 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, 0x2020, 0x00B0, 0x00A2, 0x00A3,
|
||||
0x00A7, 0x2022, 0x00B6, 0x00DF, 0x00AE, 0x00A9, 0x2122, 0x00B4, 0x00A8, 0x2260, 0x00C6, 0x00D8, 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5,
|
||||
0x2202, 0x2211, 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x03A9, 0x00E6, 0x00F8, 0x00BF, 0x00A1, 0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB,
|
||||
0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3, 0x00D5, 0x0152, 0x0153, 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA, 0x00FF, 0x0178,
|
||||
0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02, 0x2021, 0x00B7, 0x201A, 0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, 0x00CB, 0x00C8, 0x00CD, 0x00CE,
|
||||
0x00CF, 0x00CC, 0x00D3, 0x00D4, 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, 0x00AF, 0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD,
|
||||
0x02DB, 0x02C7,
|
||||
];
|
||||
/// IBM PC (OEM) code page 437 code points for 0x80..=0xFF (the Unicode Consortium's CP437
|
||||
/// mapping file).
|
||||
const CP437: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, 0x00C9, 0x00E6,
|
||||
0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
|
||||
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4, 0x03A6, 0x0398, 0x03A9, 0x03B4,
|
||||
0x221E, 0x03C6, 0x03B5, 0x2229, 0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 850 (DOS Latin-1, Western European) code points for 0x80..=0xFF (the Unicode
|
||||
/// Consortium's CP850 mapping file).
|
||||
const CP850: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5, 0x00C9, 0x00E6,
|
||||
0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x00FF, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x00D7, 0x0192, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x00F1, 0x00D1, 0x00AA, 0x00BA, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1,
|
||||
0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, 0x00F0, 0x00D0, 0x00CA, 0x00CB, 0x00C8, 0x0131, 0x00CD, 0x00CE, 0x00CF, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0x00FE, 0x00DE, 0x00DA, 0x00DB, 0x00D9,
|
||||
0x00FD, 0x00DD, 0x00AF, 0x00B4, 0x00AD, 0x00B1, 0x2017, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 852 (DOS Latin-2, Central European) code points for 0x80..=0xFF (the Unicode Consortium's CP852
|
||||
/// mapping file).
|
||||
const CP852: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x016F, 0x0107, 0x00E7, 0x0142, 0x00EB, 0x0150, 0x0151, 0x00EE, 0x0179, 0x00C4, 0x0106, 0x00C9, 0x0139,
|
||||
0x013A, 0x00F4, 0x00F6, 0x013D, 0x013E, 0x015A, 0x015B, 0x00D6, 0x00DC, 0x0164, 0x0165, 0x0141, 0x00D7, 0x010D, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x0104, 0x0105, 0x017D, 0x017E, 0x0118, 0x0119, 0x00AC, 0x017A, 0x010C, 0x015F, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1,
|
||||
0x00C2, 0x011A, 0x015E, 0x2563, 0x2551, 0x2557, 0x255D, 0x017B, 0x017C, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x0102, 0x0103,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, 0x0111, 0x0110, 0x010E, 0x00CB, 0x010F, 0x0147, 0x00CD, 0x00CE, 0x011B, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x0162, 0x016E, 0x2580, 0x00D3, 0x00DF, 0x00D4, 0x0143, 0x0144, 0x0148, 0x0160, 0x0161, 0x0154, 0x00DA, 0x0155, 0x0170,
|
||||
0x00FD, 0x00DD, 0x0163, 0x00B4, 0x00AD, 0x02DD, 0x02DB, 0x02C7, 0x02D8, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x02D9, 0x0171, 0x0158, 0x0159,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 866 (DOS Cyrillic) code points for 0x80..=0xFF (the Unicode Consortium's CP866
|
||||
/// mapping file).
|
||||
const CP866: [u16; 128] = [
|
||||
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, 0x0420, 0x0421,
|
||||
0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, 0x0430, 0x0431, 0x0432, 0x0433,
|
||||
0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
|
||||
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B,
|
||||
0x044C, 0x044D, 0x044E, 0x044F, 0x0401, 0x0451, 0x0404, 0x0454, 0x0407, 0x0457, 0x040E, 0x045E, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x2116, 0x00A4,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 737 (DOS Greek) code points for 0x80..=0xFF (the Unicode Consortium's CP737
|
||||
/// mapping file).
|
||||
const CP737: [u16; 128] = [
|
||||
0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3,
|
||||
0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC,
|
||||
0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C3, 0x03C2, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561,
|
||||
0x2562, 0x2556, 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567, 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B, 0x256A, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580, 0x03C9, 0x03AC, 0x03AD, 0x03AE, 0x03CA, 0x03AF, 0x03CC, 0x03CD, 0x03CB, 0x03CE, 0x0386, 0x0388,
|
||||
0x0389, 0x038A, 0x038C, 0x038E, 0x038F, 0x00B1, 0x2265, 0x2264, 0x03AA, 0x03AB, 0x00F7, 0x2248, 0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// OEM code page 857 (DOS Turkish; U+FFFD where the byte is unassigned) code points for 0x80..=0xFF (the Unicode Consortium's CP857
|
||||
/// mapping file).
|
||||
const CP857: [u16; 128] = [
|
||||
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7, 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x0131, 0x00C4, 0x00C5, 0x00C9, 0x00E6,
|
||||
0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9, 0x0130, 0x00D6, 0x00DC, 0x00F8, 0x00A3, 0x00D8, 0x015E, 0x015F, 0x00E1, 0x00ED, 0x00F3, 0x00FA,
|
||||
0x00F1, 0x00D1, 0x011E, 0x011F, 0x00BF, 0x00AE, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB, 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x00C1,
|
||||
0x00C2, 0x00C0, 0x00A9, 0x2563, 0x2551, 0x2557, 0x255D, 0x00A2, 0x00A5, 0x2510, 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x00E3, 0x00C3,
|
||||
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x00A4, 0x00BA, 0x00AA, 0x00CA, 0x00CB, 0x00C8, 0xFFFD, 0x00CD, 0x00CE, 0x00CF, 0x2518,
|
||||
0x250C, 0x2588, 0x2584, 0x00A6, 0x00CC, 0x2580, 0x00D3, 0x00DF, 0x00D4, 0x00D2, 0x00F5, 0x00D5, 0x00B5, 0xFFFD, 0x00D7, 0x00DA, 0x00DB, 0x00D9,
|
||||
0x00EC, 0x00FF, 0x00AF, 0x00B4, 0x00AD, 0x00B1, 0xFFFD, 0x00BE, 0x00B6, 0x00A7, 0x00F7, 0x00B8, 0x00B0, 0x00A8, 0x00B7, 0x00B9, 0x00B3, 0x00B2,
|
||||
0x25A0, 0x00A0,
|
||||
];
|
||||
|
||||
/// The glyphs the OEM code pages show for 0x01..=0x1F (smileys, card suits, arrows), as
|
||||
/// Windows' Alt+numpad entry produces them rather than the control characters.
|
||||
const OEM_GLYPHS: [u16; 31] = [
|
||||
0x263A, 0x263B, 0x2665, 0x2666, 0x2663, 0x2660, 0x2022, 0x25D8, 0x25CB, 0x25D9, 0x2642, 0x2640, 0x266A, 0x266B, 0x263C, 0x25BA, 0x25C4, 0x2195,
|
||||
0x203C, 0x00B6, 0x00A7, 0x25AC, 0x21A8, 0x2191, 0x2193, 0x2192, 0x2190, 0x221F, 0x2194, 0x25B2, 0x25BC,
|
||||
];
|
||||
|
||||
/// The OEM code pages [`oem_byte`] decodes.
|
||||
pub const OEM_CODE_PAGES: [u16; 6] = [437, 737, 850, 852, 857, 866];
|
||||
|
||||
/// One byte of an OEM code page in [`OEM_CODE_PAGES`] (any other code page reads as 437) as a
|
||||
/// char, with the graphic glyphs for 0x01..=0x1F and 0x7F (⌂); 0x00 stays NUL.
|
||||
pub fn oem_byte(code_page: u16, b: u8) -> char {
|
||||
let table: &[u16; 128] = match code_page {
|
||||
737 => &CP737,
|
||||
850 => &CP850,
|
||||
852 => &CP852,
|
||||
857 => &CP857,
|
||||
866 => &CP866,
|
||||
_ => &CP437,
|
||||
};
|
||||
let u = match b {
|
||||
0x01..=0x1F => OEM_GLYPHS.get(usize::from(b - 1)).copied(),
|
||||
0x7F => Some(0x2302),
|
||||
0x80..=0xFF => table.get(usize::from(b - 0x80)).copied(),
|
||||
_ => Some(u16::from(b)),
|
||||
};
|
||||
u.and_then(|u| char::from_u32(u32::from(u))).unwrap_or('\u{FFFD}')
|
||||
}
|
||||
|
||||
/// The single-byte Windows code pages [`windows_byte`] decodes (1252 included).
|
||||
pub const SINGLE_BYTE_CODE_PAGES: [u16; 10] = [874, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258];
|
||||
|
||||
/// Mac OS Roman, RTF's `\mac` character set.
|
||||
pub const MAC_ROMAN_CODE_PAGE: u16 = 10000;
|
||||
|
||||
/// One byte of a single-byte code page as a char: a Windows one in [`SINGLE_BYTE_CODE_PAGES`] or
|
||||
/// Mac OS Roman ([`MAC_ROMAN_CODE_PAGE`]); any other code page reads as Windows-1252.
|
||||
pub fn windows_byte(code_page: u16, b: u8) -> char {
|
||||
let table: &[u16; 128] = match code_page {
|
||||
874 => &CP874,
|
||||
1250 => &CP1250,
|
||||
1251 => &CP1251,
|
||||
1253 => &CP1253,
|
||||
1254 => &CP1254,
|
||||
1255 => &CP1255,
|
||||
1256 => &CP1256,
|
||||
1257 => &CP1257,
|
||||
1258 => &CP1258,
|
||||
MAC_ROMAN_CODE_PAGE => &MAC_ROMAN,
|
||||
_ => return cp1252(b),
|
||||
};
|
||||
if b < 0x80 {
|
||||
return b as char;
|
||||
}
|
||||
table.get(usize::from(b - 0x80)).and_then(|u| char::from_u32(u32::from(*u))).unwrap_or('\u{FFFD}')
|
||||
}
|
||||
|
||||
/// Whether [`code_page_byte`] decodes `code_page`: a Windows one ([`SINGLE_BYTE_CODE_PAGES`]), an
|
||||
/// OEM (DOS) one ([`OEM_CODE_PAGES`]) or Mac OS Roman. Document formats (RTF) name all three.
|
||||
pub fn is_byte_code_page(code_page: u16) -> bool {
|
||||
SINGLE_BYTE_CODE_PAGES.contains(&code_page) || OEM_CODE_PAGES.contains(&code_page) || code_page == MAC_ROMAN_CODE_PAGE
|
||||
}
|
||||
|
||||
/// A byte of 8-bit document text in `code_page` (see [`is_byte_code_page`]) as a char. Below 0x80
|
||||
/// it is ASCII in every code page (text, so an OEM page's glyphs for control codes don't apply).
|
||||
pub fn code_page_byte(code_page: u16, b: u8) -> char {
|
||||
if b >= 0x80 && OEM_CODE_PAGES.contains(&code_page) { oem_byte(code_page, b) } else { windows_byte(code_page, b) }
|
||||
}
|
||||
|
||||
/// The East Asian multibyte code pages a document format names with `\ansicpg` or a font's `\cpg`:
|
||||
/// 932 Shift-JIS (Japanese), 936 GBK (Simplified Chinese), 949 UHC (Korean) and 950 Big5
|
||||
/// (Traditional Chinese).
|
||||
pub const MULTIBYTE_CODE_PAGES: [u16; 4] = [932, 936, 949, 950];
|
||||
|
||||
/// Whether `code_page` is one of the East Asian multibyte code pages ([`MULTIBYTE_CODE_PAGES`]):
|
||||
/// its bytes decode as a run, a lead byte together with its trail byte.
|
||||
pub fn is_multibyte_code_page(code_page: u16) -> bool {
|
||||
multibyte_encoding_name(code_page).is_some()
|
||||
}
|
||||
|
||||
/// The WHATWG encoding name of an East Asian multibyte code page (see [`MULTIBYTE_CODE_PAGES`]),
|
||||
/// for `encoding_rs::Encoding::for_label`; `None` for the single-byte code pages, whose bytes
|
||||
/// decode one at a time, and anything else.
|
||||
pub fn multibyte_encoding_name(code_page: u16) -> Option<&'static str> {
|
||||
Some(match code_page {
|
||||
932 => "shift_jis",
|
||||
936 => "gbk",
|
||||
949 => "euc-kr",
|
||||
950 => "big5",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a document format can name `code_page` for its text: a single-byte Windows, DOS or Mac
|
||||
/// one ([`is_byte_code_page`]) or an East Asian multibyte one ([`is_multibyte_code_page`]).
|
||||
pub fn is_text_code_page(code_page: u16) -> bool {
|
||||
is_byte_code_page(code_page) || is_multibyte_code_page(code_page)
|
||||
}
|
||||
|
||||
/// The code page of an RTF or Word font charset (`\fcharset`): 204 Russian → 1251, 238 Eastern
|
||||
/// European → 1250, 77 Mac → 10000, 254 PC / 255 OEM → 437, 128 Japanese → 932, 129 Korean → 949,
|
||||
/// 134 Simplified Chinese → 936, 136 Traditional Chinese → 950… `None` for the ANSI and default
|
||||
/// charsets (the document's code page applies) and symbol fonts.
|
||||
pub fn charset_code_page(charset: i32) -> Option<u16> {
|
||||
Some(match charset {
|
||||
77 => MAC_ROMAN_CODE_PAGE,
|
||||
128 => 932,
|
||||
129 => 949,
|
||||
134 => 936,
|
||||
136 => 950,
|
||||
161 => 1253,
|
||||
162 => 1254,
|
||||
163 => 1258,
|
||||
177 => 1255,
|
||||
178 => 1256,
|
||||
186 => 1257,
|
||||
204 => 1251,
|
||||
222 => 874,
|
||||
238 => 1250,
|
||||
254 | 255 => 437,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_byte_code_pages() {
|
||||
let decode = |cp: u16, bytes: &[u8]| bytes.iter().map(|&b| windows_byte(cp, b)).collect::<String>();
|
||||
assert_eq!(decode(1251, &[0xD1, 0xF7, 0xE5, 0xF2, 0x20, 0xB9]), "Счет №");
|
||||
assert_eq!(decode(1251, &[0xA8, 0xB8]), "Ёё");
|
||||
assert_eq!(decode(1250, &[0xA3, 0xF3, 0x9F]), "Łóź");
|
||||
assert_eq!(decode(1253, &[0xC1, 0xE2]), "Αβ");
|
||||
assert_eq!(decode(1252, &[0x80, 0xE9]), "ێ");
|
||||
assert_eq!(decode(9999, &[0xE9]), "é");
|
||||
// Mac OS Roman (#1653): accents, the euro sign at 0xDB, typographic quotes.
|
||||
assert_eq!(decode(10000, &[0x80, 0x8E, 0xDB, 0xF1, 0xF2, 0xF3, 0xD2, 0xD3, 0x41]), "Äé€ÒÚÛ“”A");
|
||||
assert_eq!(charset_code_page(77), Some(10000));
|
||||
assert_eq!(charset_code_page(254), Some(437));
|
||||
assert_eq!(charset_code_page(255), Some(437));
|
||||
assert_eq!(charset_code_page(204), Some(1251));
|
||||
assert_eq!(charset_code_page(0), None);
|
||||
for cp in SINGLE_BYTE_CODE_PAGES {
|
||||
for b in 0..=255u8 {
|
||||
let _ = windows_byte(cp, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_1255_holam_haser_for_vav() {
|
||||
assert_eq!(windows_byte(1255, 0xCA), '\u{05BA}');
|
||||
}
|
||||
|
||||
/// #1653: one decoder for document text in any code page RTF names: Windows, DOS or Mac.
|
||||
#[test]
|
||||
fn document_text_in_windows_dos_and_mac_code_pages() {
|
||||
let decode = |cp: u16, bytes: &[u8]| bytes.iter().map(|&b| code_page_byte(cp, b)).collect::<String>();
|
||||
assert_eq!(decode(437, &[0x82, 0xA0, 0x41, 0x01]), "éáA\u{1}", "DOS text; ASCII stays ASCII");
|
||||
assert_eq!(decode(850, &[0x9D, 0xE9]), "ØÚ");
|
||||
assert_eq!(decode(10000, &[0x8E]), "é");
|
||||
assert_eq!(decode(1251, &[0xC4]), "Д");
|
||||
for cp in [437, 850, 866, 1250, 1252, 10000] {
|
||||
assert!(is_byte_code_page(cp), "{cp}");
|
||||
}
|
||||
assert!(!is_byte_code_page(932) && !is_byte_code_page(0) && !SINGLE_BYTE_CODE_PAGES.contains(&10000));
|
||||
}
|
||||
|
||||
/// #1653: the East Asian charsets and code pages RTF names are multibyte: their bytes decode
|
||||
/// as runs (a lead byte and its trail byte together), not one at a time.
|
||||
#[test]
|
||||
fn east_asian_code_pages_are_multibyte() {
|
||||
assert_eq!(charset_code_page(128), Some(932), "Japanese");
|
||||
assert_eq!(charset_code_page(129), Some(949), "Korean");
|
||||
assert_eq!(charset_code_page(134), Some(936), "Simplified Chinese");
|
||||
assert_eq!(charset_code_page(136), Some(950), "Traditional Chinese");
|
||||
assert_eq!(charset_code_page(255), Some(437), "OEM is single-byte");
|
||||
assert_eq!(MULTIBYTE_CODE_PAGES, [932, 936, 949, 950]);
|
||||
for cp in MULTIBYTE_CODE_PAGES {
|
||||
assert!(is_multibyte_code_page(cp) && is_text_code_page(cp), "{cp}");
|
||||
assert!(!is_byte_code_page(cp) && !SINGLE_BYTE_CODE_PAGES.contains(&cp), "{cp}");
|
||||
assert!(multibyte_encoding_name(cp).is_some(), "{cp}");
|
||||
}
|
||||
assert!(is_text_code_page(1251) && is_text_code_page(437) && is_text_code_page(10000));
|
||||
assert!(!is_text_code_page(0) && !is_text_code_page(9999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oem_code_pages() {
|
||||
let decode = |cp: u16, bytes: &[u8]| bytes.iter().map(|&b| oem_byte(cp, b)).collect::<String>();
|
||||
assert_eq!(decode(437, &[0x82, 0xE9, 0x01, 0x7F]), "éΘ☺⌂");
|
||||
assert_eq!(decode(850, &[0x9B, 0xE9]), "øÚ");
|
||||
assert_eq!(decode(852, &[0xA5, 0x88, 0xBE, 0xA9]), "ąłżę");
|
||||
assert_eq!(decode(866, &[0x8F, 0xE0, 0xA8, 0xA2, 0xA5, 0xE2, 0xF0, 0xFC]), "ПриветЁ№");
|
||||
assert_eq!(decode(737, &[0x80, 0x99, 0xE0]), "Αβω");
|
||||
assert_eq!(decode(857, &[0x8D, 0x98, 0xA6, 0x9F]), "ıİĞş");
|
||||
assert_eq!(oem_byte(857, 0xD5), '\u{FFFD}', "unassigned in 857");
|
||||
assert_eq!(decode(9999, &[0xE9]), "Θ", "unknown pages read as 437");
|
||||
for cp in OEM_CODE_PAGES {
|
||||
for b in 0..=255u8 {
|
||||
let _ = oem_byte(cp, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,15 @@ pub fn toc_instr(v: &Value) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// A TOC entry: the paragraph it points at, its level (0-based), its text, and the leading page
|
||||
/// breaks of that paragraph (its page is where the text after them lands).
|
||||
/// A TOC entry: the paragraph it points at, its level (0-based), its text, the leading page
|
||||
/// breaks of that paragraph (its page is where the text after them lands), and, for a number
|
||||
/// followed by a tab, how far the heading's text starts after where its number starts.
|
||||
struct TocEntry {
|
||||
path: Path,
|
||||
level: u8,
|
||||
text: String,
|
||||
lead: usize,
|
||||
number_gap: Option<f32>,
|
||||
}
|
||||
|
||||
/// The headings of the body, in document order, as (body block, level 1–9, text, leading page
|
||||
@@ -117,9 +119,13 @@ fn toc_entries(s: &Session, f: &FieldCode) -> Vec<TocEntry> {
|
||||
.and_then(|n| counters.next_label(&s.doc.numbering, n.num, n.level))
|
||||
.filter(|(label, def)| !label.is_empty() && !matches!(def.format, NumFormat::Bullet | NumFormat::None))
|
||||
.map(|(label, def)| match def.suffix {
|
||||
LevelSuffix::Tab => format!("{label}\t"),
|
||||
LevelSuffix::Space => format!("{label} "),
|
||||
LevelSuffix::Nothing => label,
|
||||
LevelSuffix::Tab => {
|
||||
let start = def.indent - def.hanging;
|
||||
let gap = def.tab.filter(|t| *t > start).unwrap_or(def.indent) - start;
|
||||
(format!("{label}\t"), Some(gap).filter(|g| g.is_finite() && *g > 0.0))
|
||||
}
|
||||
LevelSuffix::Space => (format!("{label} "), None),
|
||||
LevelSuffix::Nothing => (label, None),
|
||||
});
|
||||
let style = p.props.style.as_deref().unwrap_or("Normal");
|
||||
if style.starts_with("TOC") {
|
||||
@@ -144,8 +150,9 @@ fn toc_entries(s: &Session, f: &FieldCode) -> Vec<TocEntry> {
|
||||
if let Some(l) = by_style.or(by_outline).or(if hidden { None } else { by_heading }) {
|
||||
let t = p.plain_text().replace(['\u{000C}', '\n', '\t'], " ").trim().to_string();
|
||||
if !t.is_empty() {
|
||||
let text = number.map_or_else(|| t.clone(), |n| format!("{n}{t}"));
|
||||
v.push(TocEntry { path: path.clone(), level: l - 1, text, lead });
|
||||
let text = number.as_ref().map_or_else(|| t.clone(), |(n, _)| format!("{n}{t}"));
|
||||
let number_gap = number.as_ref().and_then(|(_, g)| *g);
|
||||
v.push(TocEntry { path: path.clone(), level: l - 1, text, lead, number_gap });
|
||||
}
|
||||
}
|
||||
if let Some(id) = &tc {
|
||||
@@ -159,7 +166,7 @@ fn toc_entries(s: &Session, f: &FieldCode) -> Vec<TocEntry> {
|
||||
let l = c.arg('l').and_then(|l| l.trim().parse::<u8>().ok()).unwrap_or(1).clamp(1, 9);
|
||||
let text = c.args.first().map(|t| t.trim().to_string()).unwrap_or_default();
|
||||
if (id.is_empty() || *id == kind) && (tc_levels.0..=tc_levels.1).contains(&l) && !text.is_empty() {
|
||||
v.push(TocEntry { path: path.clone(), level: l - 1, text, lead: off });
|
||||
v.push(TocEntry { path: path.clone(), level: l - 1, text, lead: off, number_gap: None });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,6 +602,16 @@ fn regenerate_toc(s: &mut Session, leader: Option<TabLeader>, tabs: &[(String, V
|
||||
para.mark = CharProps::default();
|
||||
if e.text.contains('\t') {
|
||||
para.props.tabs = tabs.iter().find(|(style, _)| Some(style) == para.props.style.as_ref()).map(|(_, tabs)| tabs.clone());
|
||||
// Without a stop for the number (LibreOffice's Contents styles have only the page
|
||||
// number's right stop), its tab would run to the page number: the entry gets a
|
||||
// left stop as far after its indent as the heading's text is after its number.
|
||||
let style = s.doc.styles.resolve_para(¶.props);
|
||||
let has_stop =
|
||||
style.tabs.iter().any(|t| t.pos < width - 1.0 && matches!(t.align, TabAlign::Left | TabAlign::Center | TabAlign::Decimal));
|
||||
if let (None, false, Some(gap)) = (¶.props.tabs, has_stop, e.number_gap) {
|
||||
let pos = style.indent_left.max(0.0) + gap;
|
||||
para.props.tabs = (pos < width - 1.0).then(|| vec![TabStop { pos, align: TabAlign::Left, leader: TabLeader::None }]);
|
||||
}
|
||||
}
|
||||
if show_page && sep.contains('\t') {
|
||||
let right = TabStop { pos: width - 0.5, align: TabAlign::Right, leader };
|
||||
|
||||
@@ -103,3 +103,58 @@ fn an_odt_bibliography_updates_from_its_citations_sources() {
|
||||
assert!(text.iter().any(|t| t.contains("Kilns and Fire")), "{text:?}");
|
||||
assert!(!text.iter().any(|t| t.contains("stale")), "{text:?}");
|
||||
}
|
||||
|
||||
/// #6486: LibreOffice numbers headings through its outline style, the number followed by a tab,
|
||||
/// and its Contents styles carry only the right-aligned dotted stop for the page number. After
|
||||
/// Update Table, the tab after an entry's number stops near the number, not at the page-number
|
||||
/// stop: the title stays at the left and each entry stays on one line.
|
||||
#[test]
|
||||
fn an_odt_toc_with_numbered_headings_keeps_its_entries_on_one_line() {
|
||||
let outline = |level: u8, cm: f32| {
|
||||
format!(
|
||||
r#"<text:outline-level-style text:level="{level}" style:num-format="1" style:num-suffix="." text:display-levels="{level}"><style:list-level-properties text:list-level-position-and-space-mode="label-alignment"><style:list-level-label-alignment text:label-followed-by="listtab" text:list-tab-stop-position="{cm}cm" fo:text-indent="-{cm}cm" fo:margin-left="{cm}cm"/></style:list-level-properties></text:outline-level-style>"#
|
||||
)
|
||||
};
|
||||
let contents = |n: u8, indent: f32| {
|
||||
format!(
|
||||
r#"<style:style style:name="Contents_20_{n}" style:display-name="Contents {n}" style:family="paragraph"><style:paragraph-properties fo:margin-left="{indent}cm"><style:tab-stops><style:tab-stop style:position="16.99cm" style:type="right" style:leader-style="dotted" style:leader-text="."/></style:tab-stops></style:paragraph-properties></style:style>"#
|
||||
)
|
||||
};
|
||||
let styles = format!(
|
||||
r#"<office:document-styles {NS}><office:styles>{}{}<text:outline-style style:name="Outline">{}{}</text:outline-style></office:styles><office:automatic-styles><style:page-layout style:name="pm1"><style:page-layout-properties fo:page-width="21.001cm" fo:page-height="29.7cm" fo:margin-left="2cm" fo:margin-right="2cm" fo:margin-top="2cm" fo:margin-bottom="2cm"/></style:page-layout></office:automatic-styles><office:master-styles><style:master-page style:name="Standard" style:page-layout-name="pm1"/></office:master-styles></office:document-styles>"#,
|
||||
contents(1, 0.0),
|
||||
contents(2, 0.499),
|
||||
outline(1, 0.762),
|
||||
outline(2, 1.016)
|
||||
);
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS}><office:body><office:text>
|
||||
<text:table-of-content text:name="T"><text:table-of-content-source text:outline-level="2"/><text:index-body><text:p text:style-name="Contents_20_1">1. Planning<text:tab/>1</text:p><text:p text:style-name="Contents_20_2">1.1. Budget<text:tab/>1</text:p></text:index-body></text:table-of-content>
|
||||
<text:h text:outline-level="1">Planning</text:h><text:p>Text.</text:p>
|
||||
<text:h text:outline-level="2">Budget</text:h><text:p>More text.</text:p>
|
||||
</office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("styles.xml", styles.as_str()), ("content.xml", content.as_str())]
|
||||
{
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let mut s = Session::new(io::open_bytes("toc.odt", &z.finish().unwrap()).unwrap());
|
||||
s.run("references.updateToc", &json!({})).unwrap();
|
||||
let layout = s.layout();
|
||||
let mut checked = 0;
|
||||
for (i, b) in s.doc.body.iter().enumerate() {
|
||||
let Some(p) = b.as_para().filter(|p| p.props.style.as_deref().is_some_and(|st| st.starts_with("TOC") && st != "TOCHeading")) else {
|
||||
continue;
|
||||
};
|
||||
let Some(tab) = p.text.find('\t') else { continue };
|
||||
let pos = |off| wordcraft_doc::Pos { story: wordcraft_doc::StoryRef::Body, path: wordcraft_doc::Path::top(i), off };
|
||||
let title = layout.caret(&pos(tab + 1)).unwrap();
|
||||
let end = layout.caret(&pos(p.len())).unwrap();
|
||||
assert!(p.plain_text().contains("Planning") || p.plain_text().contains("Budget"), "{:?}", p.text);
|
||||
assert_eq!((title.page, title.top), (end.page, end.top), "{:?} wraps", p.text);
|
||||
assert!(title.x < 200.0, "{:?}: the title starts at {}", p.text, title.x);
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, 2);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,8 @@ impl WordApp {
|
||||
fonts_frames: 0,
|
||||
applied_theme: None,
|
||||
native_scale: 1.0,
|
||||
auto_scale_limit: f32::INFINITY,
|
||||
monitor_zoom: 1.0,
|
||||
legacy_interface_size: None,
|
||||
// Inert until the app binary calls `watch_desktop_theme`.
|
||||
desktop_theme: desktop_theme::DesktopTheme::default(),
|
||||
@@ -301,7 +303,25 @@ impl WordApp {
|
||||
{
|
||||
self.ui.interface_size = theme::migrate_legacy_interface_size(old, native);
|
||||
}
|
||||
let zoom = self.ui.interface_size.zoom_factor(self.native_scale);
|
||||
// Auto never draws larger than the screen holds (#4030): a system that guesses 3× on a
|
||||
// 1366x768 panel would put the window's controls off screen. The zoom is shared by every
|
||||
// window, so the smallest screen of all the open windows sets the cap. A viewport's
|
||||
// `monitor_size` is in points of the zoom in force before a pending change applied, so
|
||||
// its pixels are that size times `monitor_zoom` and the window's own native scale; each window's allowed zoom is its own, the smallest is shared.
|
||||
let monitors: Vec<_> = self
|
||||
.windows
|
||||
.viewport_ids()
|
||||
.into_iter()
|
||||
.filter_map(|v| {
|
||||
ctx.input_for(v, |i| {
|
||||
let info = i.viewport();
|
||||
Some(((info.monitor_size?.x, info.monitor_size?.y), info.native_pixels_per_point?))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.auto_scale_limit = theme::auto_scale_limit_over(monitors, self.monitor_zoom, self.native_scale);
|
||||
self.monitor_zoom = ctx.zoom_factor();
|
||||
let zoom = self.ui.interface_size.zoom_factor_within(self.native_scale, self.auto_scale_limit);
|
||||
if ctx.zoom_factor() != zoom {
|
||||
ctx.set_zoom_factor(zoom);
|
||||
}
|
||||
@@ -410,6 +430,15 @@ impl WordApp {
|
||||
/// Pane's Tab keys and dropped files are read in the first UI pass
|
||||
/// ([`WordApp::handle_ui_input_events`]).
|
||||
pub(crate) fn window_input(&mut self, ctx: &egui::Context) {
|
||||
// The way back when a large interface size has pushed File › Options off screen (#4030).
|
||||
// A modal dialog keeps the keys (Customize Keyboard records them), and a command the user
|
||||
// bound to the chord runs instead.
|
||||
if !self.modal_open()
|
||||
&& self.session.keymap.resolve(&self.session.registry, "Mod+Shift+0").is_none()
|
||||
&& ctx.input_mut(super::consume_reset_interface_size)
|
||||
{
|
||||
let _ = self.run("ui.interfaceSize", serde_json::json!({"value": "auto"}));
|
||||
}
|
||||
if self.windows.native() && ctx.input_mut(|i| i.consume_shortcut(&QUIT)) {
|
||||
self.quit();
|
||||
}
|
||||
|
||||
@@ -667,7 +667,7 @@ impl WordApp {
|
||||
}
|
||||
}
|
||||
let size = self.ui.interface_size;
|
||||
json!({"auto": size.is_auto(), "percent": size.drawn_percent(self.native_scale)})
|
||||
json!({"auto": size.is_auto(), "percent": size.drawn_percent_within(self.native_scale, self.auto_scale_limit)})
|
||||
}
|
||||
"ui.animations" => {
|
||||
// `system` (follow the system's animation setting), `on` or `off`; no value reads
|
||||
|
||||
@@ -70,7 +70,7 @@ pub struct LangInfo {
|
||||
|
||||
/// The languages WordCraft offers, when their catalogs are embedded. English first: it is the
|
||||
/// fallback and the source language.
|
||||
pub static LANGUAGES: [LangInfo; 28] = [
|
||||
pub static LANGUAGES: [LangInfo; 29] = [
|
||||
LangInfo { code: "en", prefer_hans: false, editing_lang: None },
|
||||
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` resolve here (the crate's matching).
|
||||
LangInfo { code: "zh-hans", prefer_hans: true, editing_lang: None },
|
||||
@@ -129,6 +129,9 @@ pub static LANGUAGES: [LangInfo; 28] = [
|
||||
LangInfo { code: "ar", prefer_hans: false, editing_lang: None },
|
||||
// Slovenian; `sl`, `sl-SI` and `sl_SI.UTF-8` resolve here.
|
||||
LangInfo { code: "sl", prefer_hans: false, editing_lang: None },
|
||||
// Bosnian, Latin script; `bs`, `bs-BA`, `bs_BA.UTF-8` and `bs-Latn-BA` resolve here. Cyrillic
|
||||
// Bosnian (`bs-Cyrl-*`) has no catalog, so it stays in English.
|
||||
LangInfo { code: "bs", prefer_hans: false, editing_lang: None },
|
||||
];
|
||||
|
||||
/// Interface languages written right to left. Their catalogs are written in typing order and
|
||||
|
||||
@@ -1143,6 +1143,75 @@ fn bundled_interface_fonts_cover_slovenian_letters() {
|
||||
assert!(!lang("sl").uses_cjk());
|
||||
}
|
||||
|
||||
/// Bosnian (Latin script) follows `bs-BA` system locales and a saved choice, and choosing it leaves
|
||||
/// the document as it was. Cyrillic Bosnian has no catalog: a script is never crossed.
|
||||
#[test]
|
||||
fn bosnian_locales_and_saved_preference_work_without_changing_the_document() {
|
||||
needs_catalogs!();
|
||||
let bs = lang("bs");
|
||||
for tag in ["bs", "bs-BA", "bs_BA.UTF-8", "BS-ba", "bs-Latn-BA"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(bs), "{tag}");
|
||||
}
|
||||
assert_eq!(lang_from_tag("bs-Cyrl-BA"), None, "Cyrillic Bosnian has no catalog");
|
||||
assert_eq!(first_supported([UNSUPPORTED, "bs-BA", "en-US"]), Some(bs));
|
||||
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Default::default());
|
||||
let before = serde_json::to_value(&app.session.doc).unwrap();
|
||||
let result = app.run("ui.language", serde_json::json!({"value": "BS"})).unwrap();
|
||||
assert_eq!(result["effective"], "bs");
|
||||
assert_eq!(app.ui.language, "bs");
|
||||
assert!(result["available"].as_array().unwrap().iter().any(|l| l["code"] == "bs" && l["name"] == "Bosanski"));
|
||||
assert_eq!(serde_json::to_value(&app.session.doc).unwrap(), before);
|
||||
let saved = serde_json::to_string(&app.ui).unwrap();
|
||||
let restored: crate::UiState = serde_json::from_str(&saved).unwrap();
|
||||
assert_eq!(Lang::from_pref(&restored.language), bs);
|
||||
assert_eq!(bs.editing_lang(), None, "WordCraft doesn't proof Bosnian, so documents stay in its default");
|
||||
assert!(!bs.uses_cjk());
|
||||
}
|
||||
|
||||
/// Bosnian counts take CLDR's three forms through the embedded catalog: `one` for 1, 21, 101 (not
|
||||
/// 11), `few` for 2–4, 22–24 (not 12–14) and `other` for the rest.
|
||||
#[test]
|
||||
fn bosnian_plurals_take_one_few_other() {
|
||||
needs_catalogs!();
|
||||
let bs = lang("bs");
|
||||
let count = |n| I18N.trn_plain(bs.lang, n, "{n} item", "{n} items");
|
||||
for (n, want) in [
|
||||
(1, "1 stavka"),
|
||||
(21, "21 stavka"),
|
||||
(101, "101 stavka"),
|
||||
(2, "2 stavke"),
|
||||
(4, "4 stavke"),
|
||||
(22, "22 stavke"),
|
||||
(0, "0 stavki"),
|
||||
(5, "5 stavki"),
|
||||
(11, "11 stavki"),
|
||||
(12, "12 stavki"),
|
||||
(14, "14 stavki"),
|
||||
(111, "111 stavki"),
|
||||
] {
|
||||
assert_eq!(count(n), want, "{n}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The bundled interface faces draw every Bosnian letter (č ć đ š ž), every other accented letter
|
||||
/// the catalog uses, and its punctuation, without a system fallback.
|
||||
#[test]
|
||||
fn bundled_interface_fonts_cover_bosnian_letters() {
|
||||
let db = wordcraft_fonts::FontDb::with_font_dirs(Vec::new());
|
||||
needs_catalogs!();
|
||||
let extra = ['č', 'ć', 'đ', 'š', 'ž', 'Č', 'Ć', 'Đ', 'Š', 'Ž', '„', '”', '–', '…'];
|
||||
// Every accented Latin letter the catalog uses (Latin-1 and Latin Extended-A), and the above.
|
||||
let accented = |c: &char| ('\u{C0}'..='\u{17F}').contains(c) && c.is_alphabetic();
|
||||
let chars: std::collections::HashSet<char> = translations("bs").iter().flat_map(|e| e.chars()).filter(accented).chain(extra).collect();
|
||||
for (family, style) in [("Inter", "Regular"), ("Inter", "Medium"), ("Inter", "SemiBold"), ("JetBrains Mono", "Regular")] {
|
||||
let face = db.face(family, style);
|
||||
assert_eq!(face.family, family, "must use the bundled interface face");
|
||||
for ch in &chars {
|
||||
assert_ne!(face.glyph_for(*ch), 0, "{family} {style} lacks {ch}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The "Open" paragraph-spacing preset is roomy spacing, not the file verb: only a `spacing` row
|
||||
/// translates it.
|
||||
#[test]
|
||||
|
||||
@@ -449,6 +449,24 @@ pub const DEFAULT_TAB: &str = "Home";
|
||||
|
||||
/// ⌘Q, which only the Mac sends: quit.
|
||||
const QUIT: egui::KeyboardShortcut = egui::KeyboardShortcut::new(egui::Modifiers::MAC_CMD, egui::Key::Q);
|
||||
/// Ctrl+Shift+0 (⌘⇧0): back to the Auto interface size (#4030). Not Ctrl+Alt+0, which is AltGr+0
|
||||
/// on some layouts and types a character there (#3424).
|
||||
const RESET_INTERFACE_SIZE: egui::KeyboardShortcut =
|
||||
egui::KeyboardShortcut::new(egui::Modifiers::COMMAND.plus(egui::Modifiers::SHIFT), egui::Key::Num0);
|
||||
|
||||
/// Consume Ctrl+Shift+0 (⌘⇧0), matched on the key the layout types and exactly these modifiers.
|
||||
/// Where Shift+0 types `=` (German, Swiss and Nordic layouts) the press is Ctrl+Shift+=, which
|
||||
/// stays Superscript; an extra Alt (AltGr+Shift+0) doesn't match.
|
||||
fn consume_reset_interface_size(input: &mut egui::InputState) -> bool {
|
||||
let mut found = false;
|
||||
input.events.retain(|e| {
|
||||
let hit = matches!(e, egui::Event::Key { key: egui::Key::Num0, pressed: true, modifiers, .. }
|
||||
if modifiers.matches_exact(RESET_INTERFACE_SIZE.modifiers));
|
||||
found |= hit;
|
||||
!hit
|
||||
});
|
||||
found
|
||||
}
|
||||
/// Most recent documents kept (File › Options › Advanced offers 0–50, as Word does).
|
||||
pub const MAX_RECENT: usize = 50;
|
||||
/// Most custom dictionary words read back from `ui.json`.
|
||||
@@ -730,6 +748,12 @@ pub struct WordApp {
|
||||
/// The display scale (native pixels per point) seen in the last frame, 1.0 before the first;
|
||||
/// `ui.interfaceSize` reports the size Auto draws at with it.
|
||||
native_scale: f32,
|
||||
/// The most pixels per point Auto may draw at on this screen ([`theme::auto_scale_limit`], #4030).
|
||||
auto_scale_limit: f32,
|
||||
/// The zoom factor in force during the previous frame's `logic`: egui-winit turns each
|
||||
/// window's monitor size into points with the zoom of the frame before a pending change is
|
||||
/// applied, so the physical size is that size times this zoom ([`theme::auto_scale_limit_over`]).
|
||||
monitor_zoom: f32,
|
||||
/// A pre-#822 interface size (percent on top of the display scale) waiting for the first frame
|
||||
/// that knows the display's scale, to become the absolute size it drew at
|
||||
/// ([`theme::migrate_legacy_interface_size`]).
|
||||
|
||||
@@ -771,8 +771,8 @@ fn interface_size_picker(app: &mut WordApp, ui: &mut Ui) {
|
||||
let native = ui.ctx().native_pixels_per_point().unwrap_or(1.0);
|
||||
// The template keeps the percent sign outside its placeholder, so `fmt` isolates the number
|
||||
// without it and a right-to-left line draws "(%200)": the sign goes inside the isolate.
|
||||
let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &InterfaceSize::AUTO.drawn_percent(native).to_string())])
|
||||
.replace("\u{2069}%", "%\u{2069}");
|
||||
let drawn = InterfaceSize::AUTO.drawn_percent_within(native, app.auto_scale_limit);
|
||||
let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &drawn.to_string())]).replace("\u{2069}%", "%\u{2069}");
|
||||
let selected = size.percent().map_or_else(|| auto.clone(), |pct| format!("{pct}%"));
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Interface size:"));
|
||||
|
||||
@@ -6220,6 +6220,189 @@ fn interface_size_survives_an_unusable_display_scale() {
|
||||
assert_eq!(InterfaceSize::AUTO.drawn_percent(f32::MAX), u16::MAX);
|
||||
}
|
||||
|
||||
/// #4030: a system that reports a huge scale (an 11-inch 1366x768 panel on X11) must not make Auto
|
||||
/// draw an interface larger than the screen holds; the controls stay on screen.
|
||||
#[test]
|
||||
fn auto_interface_size_is_capped_to_what_fits_the_screen() {
|
||||
use theme::{AUTO_MIN_POINTS, InterfaceSize, auto_scale_limit};
|
||||
let limit = auto_scale_limit(Some((1366.0, 768.0)));
|
||||
assert!((1.0..1.6).contains(&limit), "{limit}");
|
||||
// Scales that fit are left as the system reports them.
|
||||
for (screen, natives) in [((1920.0, 1080.0), [1.0, 1.25, 1.5]), ((1366.0, 768.0), [1.0, 1.25, 1.5])] {
|
||||
let limit = auto_scale_limit(Some(screen));
|
||||
for native in natives {
|
||||
assert_eq!(f32::from(InterfaceSize::AUTO.drawn_percent_within(native, limit)), native * 100.0, "{screen:?} {native}");
|
||||
}
|
||||
}
|
||||
assert!((auto_scale_limit(Some((1920.0, 1080.0))) - 2.13).abs() < 0.01);
|
||||
for native in [2.0, 3.17] {
|
||||
let drawn = f32::from(InterfaceSize::AUTO.drawn_percent_within(native, limit)) / 100.0;
|
||||
assert!(1366.0 / drawn >= AUTO_MIN_POINTS.0 - 3.0 && 768.0 / drawn >= AUTO_MIN_POINTS.1 - 3.0, "{native}: {drawn}");
|
||||
}
|
||||
// A scale that already fits, an unknown screen and junk are left alone.
|
||||
assert_eq!(InterfaceSize::AUTO.drawn_percent_within(1.25, limit), 125);
|
||||
assert_eq!(auto_scale_limit(None), f32::INFINITY);
|
||||
assert_eq!(auto_scale_limit(Some((f32::NAN, 768.0))), f32::INFINITY);
|
||||
assert_eq!(InterfaceSize::AUTO.drawn_percent_within(3.0, auto_scale_limit(None)), 300);
|
||||
// A screen below the minimum is never scaled under 100 %; a 4K screen at 2x is untouched.
|
||||
assert_eq!(auto_scale_limit(Some((640.0, 480.0))), 1.0);
|
||||
assert_eq!(InterfaceSize::AUTO.drawn_percent_within(2.0, auto_scale_limit(Some((3840.0, 2160.0)))), 200);
|
||||
// A chosen size is the user's and is not capped.
|
||||
assert_eq!(InterfaceSize::from_percent(300.0).unwrap().drawn_percent_within(3.0, limit), 300);
|
||||
}
|
||||
|
||||
/// #4030: the zoom is shared, so the smallest screen of all the open windows caps Auto, and a
|
||||
/// monitor size made with the zoom before a change still gives the same physical screen.
|
||||
#[test]
|
||||
fn auto_cap_follows_the_smallest_window_screen_and_the_zoom_of_its_points() {
|
||||
use theme::auto_scale_limit_over;
|
||||
let small = auto_scale_limit_over([((1366.0, 768.0), 1.0)], 1.0, 1.0);
|
||||
let large = auto_scale_limit_over([((3840.0, 2160.0), 1.0)], 1.0, 1.0);
|
||||
assert!(small < large);
|
||||
assert_eq!(auto_scale_limit_over([((3840.0, 2160.0), 1.0), ((1366.0, 768.0), 1.0)], 1.0, 1.0), small);
|
||||
// A 1366x768 panel at native scale 3: the points are 455x256 at zoom 1, 304x171 at zoom 1.5.
|
||||
// The pixels, and so the cap, don't depend on which zoom made the points.
|
||||
let a = auto_scale_limit_over([((1366.0 / 3.0, 768.0 / 3.0), 3.0)], 1.0, 3.0);
|
||||
let b = auto_scale_limit_over([((1366.0 / 4.5, 768.0 / 4.5), 3.0)], 1.5, 3.0);
|
||||
assert!((a - b).abs() < 1e-4 && (a - small).abs() < 1e-4, "{a} {b} {small}");
|
||||
assert_eq!(auto_scale_limit_over([], 1.0, 1.0), f32::INFINITY);
|
||||
}
|
||||
|
||||
/// #4030: with a 1x window on a large screen and a 3x window on a 1366x768 panel, the shared zoom
|
||||
/// is the one the 3x window needs, so both windows draw within their own screen.
|
||||
#[test]
|
||||
fn auto_cap_keeps_every_window_within_its_own_screen_when_scales_differ() {
|
||||
use theme::{AUTO_MIN_POINTS, InterfaceSize, auto_scale_limit, usable_scale};
|
||||
let windows = [((3840.0, 2160.0), 1.0_f32), ((1366.0 / 3.0, 768.0 / 3.0), 3.0)];
|
||||
for root in [1.0_f32, 3.0] {
|
||||
let limit = theme::auto_scale_limit_over(windows, 1.0, root);
|
||||
let zoom = InterfaceSize::AUTO.zoom_factor_within(root, limit);
|
||||
for ((w, h), own) in windows {
|
||||
let pixels_per_point = usable_scale(own) * zoom;
|
||||
let (px_w, px_h) = (w * own, h * own);
|
||||
assert!(pixels_per_point <= auto_scale_limit(Some((px_w, px_h))) + 1e-4, "root {root}, window {own}: {pixels_per_point}");
|
||||
assert!(
|
||||
px_w / pixels_per_point >= AUTO_MIN_POINTS.0 - 1.0 && px_h / pixels_per_point >= AUTO_MIN_POINTS.1 - 1.0,
|
||||
"root {root}, window {own}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #4030: the cap shows in `ui.interfaceSize` and is never saved; Ctrl+Shift+0 returns to Auto
|
||||
/// while Ctrl+Alt+0 (AltGr+0, which types `}` on a German layout) neither resets nor swallows it.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn reset_interface_size_shortcut_and_capped_report() {
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
app(),
|
||||
);
|
||||
for _ in 0..6 {
|
||||
h.step();
|
||||
}
|
||||
{
|
||||
let a = h.state_mut();
|
||||
a.native_scale = 3.0;
|
||||
a.auto_scale_limit = 1.5;
|
||||
assert_eq!(a.run("ui.interfaceSize", json!({})).unwrap(), json!({"auto": true, "percent": 150}));
|
||||
assert!(!serde_json::to_string(&a.ui).unwrap().contains("150"), "the cap is not saved");
|
||||
a.run("ui.interfaceSize", json!({"percent": 300})).unwrap();
|
||||
}
|
||||
let altgr = egui::Modifiers { alt: true, ctrl: true, shift: false, mac_cmd: false, command: true };
|
||||
h.event(egui::Event::Key { key: egui::Key::Num0, physical_key: None, pressed: true, repeat: false, modifiers: altgr });
|
||||
h.event(egui::Event::ModifiersChanged(altgr));
|
||||
h.event(egui::Event::Text("}".into()));
|
||||
h.step();
|
||||
assert_eq!(h.state().ui.interface_size.percent(), Some(300), "AltGr+0 doesn't reset");
|
||||
assert_eq!(h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body), "}", "AltGr+0 types the character");
|
||||
let chord = egui::Modifiers { alt: false, ctrl: true, shift: true, mac_cmd: false, command: true };
|
||||
h.event(egui::Event::Key { key: egui::Key::Num0, physical_key: Some(egui::Key::Num0), pressed: true, repeat: false, modifiers: chord });
|
||||
h.step();
|
||||
assert!(h.state().ui.interface_size.is_auto(), "Ctrl+Shift+0 returns to Auto");
|
||||
// AltGr+Shift+0, where the layout leaves the key a zero, doesn't reset either.
|
||||
h.state_mut().run("ui.interfaceSize", json!({"percent": 300})).unwrap();
|
||||
let altgr_shift = egui::Modifiers { alt: true, ctrl: true, shift: true, mac_cmd: false, command: true };
|
||||
h.event(egui::Event::Key { key: egui::Key::Num0, physical_key: Some(egui::Key::Num0), pressed: true, repeat: false, modifiers: altgr_shift });
|
||||
h.step();
|
||||
assert_eq!(h.state().ui.interface_size.percent(), Some(300), "AltGr+Shift+0 doesn't reset");
|
||||
// With File › Options open, the place a too-large size pushes off screen, the chord resets.
|
||||
h.state_mut().run("ui.backstage", json!({"value": true})).unwrap();
|
||||
h.state_mut().ui.backstage_page = "options".into();
|
||||
h.step();
|
||||
h.event(egui::Event::Key { key: egui::Key::Num0, physical_key: Some(egui::Key::Num0), pressed: true, repeat: false, modifiers: chord });
|
||||
h.step();
|
||||
assert!(h.state().ui.interface_size.is_auto(), "Ctrl+Shift+0 returns to Auto from File › Options");
|
||||
// A modal dialog keeps the keys: Customize Keyboard records the chord instead.
|
||||
h.state_mut().run("ui.backstage", json!({"value": false})).unwrap();
|
||||
h.state_mut().run("ui.interfaceSize", json!({"percent": 300})).unwrap();
|
||||
h.state_mut().run("ui.dialog", json!({"name": "customizeKeyboard"})).unwrap();
|
||||
h.step();
|
||||
h.event(egui::Event::Key { key: egui::Key::Num0, physical_key: Some(egui::Key::Num0), pressed: true, repeat: false, modifiers: chord });
|
||||
h.step();
|
||||
assert_eq!(h.state().ui.interface_size.percent(), Some(300), "no reset while a modal dialog is open");
|
||||
}
|
||||
|
||||
/// #4030 review: where Shift+0 types `=` (German, Swiss and Nordic layouts) egui-winit reports `=`
|
||||
/// as the key and the zero only as the physical key. That press is Ctrl+Shift+=, Superscript, as
|
||||
/// on main; it doesn't reset the interface size.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn ctrl_shift_equals_on_the_zero_key_stays_superscript() {
|
||||
let mut a = app();
|
||||
a.run("document.setText", json!({"text": "x"})).unwrap();
|
||||
a.run("select.all", json!({})).unwrap();
|
||||
a.run("ui.interfaceSize", json!({"percent": 300})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
for _ in 0..6 {
|
||||
h.step();
|
||||
}
|
||||
let chord = egui::Modifiers { alt: false, ctrl: true, shift: true, mac_cmd: false, command: true };
|
||||
h.event(egui::Event::Key { key: egui::Key::Equals, physical_key: Some(egui::Key::Num0), pressed: true, repeat: false, modifiers: chord });
|
||||
h.step();
|
||||
assert_eq!(h.state().ui.interface_size.percent(), Some(300), "Ctrl+Shift+= doesn't reset");
|
||||
let pos = wordcraft_doc::Pos { story: wordcraft_doc::StoryRef::Body, path: wordcraft_doc::Path::top(0), off: 0 };
|
||||
let props = h.state().session.doc.para_at(&pos).map(|p| p.props_of_char(0).clone()).unwrap_or_default();
|
||||
assert_eq!(props.vert_align, Some(wordcraft_doc::props::VertAlign::Superscript), "Ctrl+Shift+= runs Superscript");
|
||||
}
|
||||
|
||||
/// #4030 review: a Ctrl+Shift+0 the user binds in Customize Keyboard runs their command instead
|
||||
/// of resetting the interface size.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn a_bound_ctrl_shift_0_beats_the_interface_size_reset() {
|
||||
let mut a = app();
|
||||
a.run("ui.interfaceSize", json!({"percent": 300})).unwrap();
|
||||
a.session.keymap = wordcraft_engine::keymap::KeyMap::from_value(&json!({"assigned": {"Mod+Shift+0": "view.focus"}}));
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
for _ in 0..6 {
|
||||
h.step();
|
||||
}
|
||||
let chord = egui::Modifiers { alt: false, ctrl: true, shift: true, mac_cmd: false, command: true };
|
||||
h.event(egui::Event::Key { key: egui::Key::Num0, physical_key: Some(egui::Key::Num0), pressed: true, repeat: false, modifiers: chord });
|
||||
h.step();
|
||||
assert_eq!(h.state().ui.interface_size.percent(), Some(300), "the size stays");
|
||||
assert!(h.state().session.view.focus_mode, "the user's binding runs");
|
||||
}
|
||||
|
||||
/// #115: System follows the OS appearance (light when unknown); the manual choices ignore it.
|
||||
#[test]
|
||||
fn interface_theme_resolves_against_the_os_appearance() {
|
||||
|
||||
@@ -125,11 +125,51 @@ impl InterfaceSize {
|
||||
|
||||
/// The size the interface is drawn at on that display, in percent (screen pixels per point × 100).
|
||||
pub fn drawn_percent(self, native: f32) -> u16 {
|
||||
let drawn = usable_scale(native) * self.zoom_factor(native) * 100.0;
|
||||
self.drawn_percent_within(native, f32::INFINITY)
|
||||
}
|
||||
|
||||
/// [`InterfaceSize::zoom_factor`] on a screen that [`auto_scale_limit`] caps at `limit` pixels
|
||||
/// per point: Auto never draws larger than that.
|
||||
pub fn zoom_factor_within(self, native: f32, limit: f32) -> f32 {
|
||||
match self.0 {
|
||||
None => (limit / usable_scale(native)).min(1.0),
|
||||
Some(_) => self.zoom_factor(native),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`InterfaceSize::drawn_percent`] within [`auto_scale_limit`]'s cap.
|
||||
pub fn drawn_percent_within(self, native: f32, limit: f32) -> u16 {
|
||||
let drawn = usable_scale(native) * self.zoom_factor_within(native, limit) * 100.0;
|
||||
drawn.round().clamp(0.0, f32::from(u16::MAX)) as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// The smallest window, in points, the interface is laid out for under Auto.
|
||||
pub const AUTO_MIN_POINTS: (f32, f32) = (900.0, 460.0);
|
||||
|
||||
/// The largest pixels-per-point Auto may draw at on a screen of `screen_px` pixels (#4030): the
|
||||
/// one that still leaves [`AUTO_MIN_POINTS`], so the ribbon and the window's controls stay on
|
||||
/// screen. Never below 1.0, and unlimited when the screen size is unknown.
|
||||
pub fn auto_scale_limit(screen_px: Option<(f32, f32)>) -> f32 {
|
||||
screen_px
|
||||
.filter(|(w, h)| w.is_finite() && h.is_finite() && *w > 0.0 && *h > 0.0)
|
||||
.map_or(f32::INFINITY, |(w, h)| (w / AUTO_MIN_POINTS.0).min(h / AUTO_MIN_POINTS.1).max(1.0))
|
||||
}
|
||||
|
||||
/// [`auto_scale_limit`] for the several windows that share one zoom, expressed as pixels per point
|
||||
/// at the scale `native` (the one [`InterfaceSize::zoom_factor_within`] divides by). Each window
|
||||
/// is a monitor size in points and its own native scale; `zoom` is the zoom factor those points
|
||||
/// were made with, so the size in pixels is the three multiplied. Every window must stay within
|
||||
/// its own screen, so the allowed zoom is worked out per window and the smallest one is shared.
|
||||
/// Unlimited when no screen is known.
|
||||
pub fn auto_scale_limit_over(monitors: impl IntoIterator<Item = ((f32, f32), f32)>, zoom: f32, native: f32) -> f32 {
|
||||
let shared_zoom = monitors
|
||||
.into_iter()
|
||||
.map(|((w, h), own)| auto_scale_limit(Some((w * zoom * own, h * zoom * own))) / usable_scale(own))
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
shared_zoom * usable_scale(native)
|
||||
}
|
||||
|
||||
/// The display's scale, or 1.0 when egui doesn't know it (or reports nonsense).
|
||||
pub fn usable_scale(native: f32) -> f32 {
|
||||
if native.is_finite() && native > 0.0 { native } else { 1.0 }
|
||||
|
||||
@@ -196,6 +196,8 @@ impl WordApp {
|
||||
fonts_frames: _,
|
||||
applied_theme: _,
|
||||
native_scale: _,
|
||||
auto_scale_limit: _,
|
||||
monitor_zoom: _,
|
||||
legacy_interface_size: _,
|
||||
desktop_theme: _,
|
||||
desktop_dark: _,
|
||||
|
||||
@@ -187,6 +187,18 @@ impl Windows {
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewports the open windows draw in, the root's included.
|
||||
pub(crate) fn viewport_ids(&self) -> Vec<ViewportId> {
|
||||
let mut ids = vec![ViewportId::ROOT];
|
||||
for w in &self.open {
|
||||
let v = self.viewport(w.key);
|
||||
if !ids.contains(&v) {
|
||||
ids.push(v);
|
||||
}
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// The title View › Window › Switch Windows gives the window, as the frame started.
|
||||
fn menu_title(&self, key: u64) -> Option<&str> {
|
||||
self.menu.title(self.get(key)?.id?)
|
||||
|
||||
@@ -84,7 +84,7 @@ let apps move windows, so there Arrange All and Side by Side only resize.
|
||||
|
||||
| Method | Params | Result |
|
||||
|---|---|---|
|
||||
| `engine.execute` | `command`, `params`, `doc?` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.displayServer` (`{"value": "auto"\|"wayland"\|"x11"}`; no value reads it; File › Options › Advanced › Linux display server, which the desktop app reads on Linux and the BSDs at its next launch; `WORDCRAFT_WAYLAND` overrides it, #2184), `ui.scrollSpeed` (`{"value": "slow"\|"normal"\|"fast"}`; no value reads it; mouse-wheel scroll speed, #6146), `ui.touchpadScrollSpeed` (`{"value": "slow"\|"normal"\|"fast"}`; no value reads it; scales touchpad scrolling on Wayland: slow 2×, normal 4×, fast 8×; on macOS: 0.5×, 1×, 2×; File › Options › Advanced › Scrolling › Touchpad scroll speed, #6146), `ui.statusCount` (`{"value": "words"\|"characters"\|"charactersNoSpaces"}`; no value reads it; what the status bar's count shows — words (the default), characters with spaces, or characters without spaces — kept between runs, #2797), `ui.recentRemove` (`{"path": entry}` takes that document off the recent list — `path` must be an entry on the list; on the desktop the new list is saved right away: the result's `saved` is true then, false with `saveError` when the write failed (the removal still holds for this run) and null where the host keeps no preferences (the web app, or preferences switched off), #2716), `ui.recentClear` (`{}` clears the whole list; the result reports `removed`, the new `recent` and `saved` as above), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float", "before"?: pane\|null}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756; `before` puts the pane directly before that pane in the order, outside it on their edge, as a drop over a docked pane's outer third does, and `null` after every pane; the order is kept between runs)…) |
|
||||
| `engine.execute` | `command`, `params`, `doc?` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, capped so a window of at least 900x460 points fits the screen (#4030), a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.displayServer` (`{"value": "auto"\|"wayland"\|"x11"}`; no value reads it; File › Options › Advanced › Linux display server, which the desktop app reads on Linux and the BSDs at its next launch; `WORDCRAFT_WAYLAND` overrides it, #2184), `ui.scrollSpeed` (`{"value": "slow"\|"normal"\|"fast"}`; no value reads it; mouse-wheel scroll speed, #6146), `ui.touchpadScrollSpeed` (`{"value": "slow"\|"normal"\|"fast"}`; no value reads it; scales touchpad scrolling on Wayland: slow 2×, normal 4×, fast 8×; on macOS: 0.5×, 1×, 2×; File › Options › Advanced › Scrolling › Touchpad scroll speed, #6146), `ui.statusCount` (`{"value": "words"\|"characters"\|"charactersNoSpaces"}`; no value reads it; what the status bar's count shows — words (the default), characters with spaces, or characters without spaces — kept between runs, #2797), `ui.recentRemove` (`{"path": entry}` takes that document off the recent list — `path` must be an entry on the list; on the desktop the new list is saved right away: the result's `saved` is true then, false with `saveError` when the write failed (the removal still holds for this run) and null where the host keeps no preferences (the web app, or preferences switched off), #2716), `ui.recentClear` (`{}` clears the whole list; the result reports `removed`, the new `recent` and `saved` as above), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float", "before"?: pane\|null}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756; `before` puts the pane directly before that pane in the order, outside it on their edge, as a drop over a docked pane's outer third does, and `null` after every pane; the order is kept between runs)…) |
|
||||
| `engine.commands` | — | every command: id, label, location, shortcut, params, enabled |
|
||||
| `document.inspect` | `text?`, `doc?` | blocks (text, style, runs, lists, tables), parts, sections, selection, pages |
|
||||
| `ui.inspect` | — | UI state, view, dialog, window size, page rects on screen, caret, comment balloons (`balloons`: id + screen rect; `balloon`: the selected one), perf; `windows`: the other windows as the drawn window sees them (`available`, `others`, `sideBySide`, `syncScroll`); `document`: the active document's id; `documents`: the open documents (`id`, `title`, `path`, `dirty`, `active`) |
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Bosnian interface
|
||||
|
||||
> **Last reviewed:** 2026-10-29 · **Last updated:** 2026-10-29 · **Change:** major (first version) · **Target:** WordCraft 0.7.0
|
||||
|
||||
Choose **File › Options › Interface language › Bosanski**, or follow a Bosnian system display
|
||||
language (`bs-BA`, `bs_BA.UTF-8`, `bs-Latn-BA`). The catalog code is `bs`; `ui.language` with
|
||||
`{"value":"bs"}` selects it explicitly, and `WORDCRAFT_LANGUAGE=bs-BA` does the same for a headless
|
||||
screenshot or a launch while the preference is `auto`. Document text, command ids, CLI/MCP/control
|
||||
responses and file paths do not change.
|
||||
|
||||
The catalog is craft-i18n's `locales/bs/wordcraft.tsv` (storytold/craft-i18n, 2,508 entries) over the
|
||||
shared `locales/bs/common.tsv`. Both are embedded from the
|
||||
craft-i18n commit pinned in `craft-i18n.rev` (1e51542…), which includes them. The
|
||||
catalog is written from the meaning of the English labels; no proprietary translation resources were
|
||||
consulted. It is an AI-assisted draft that a native speaker has not yet reviewed.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Latin script, ijekavian, standard Bosnian vocabulary where the neighbouring languages differ:
|
||||
*historija*, *sačuvaj*, *tačka*, *hiljada*, *sedmica*, *datoteka*, *postavke*. Cyrillic Bosnian
|
||||
(`bs-Cyrl-*`) has no catalog and stays in English.
|
||||
- Commands and buttons use the imperative (`Sačuvaj`, `Umetni fusnotu`, `Prihvati`); ribbon groups
|
||||
and panes are nouns (`Pasus`, `Stilovi`, `Navigacijsko okno`).
|
||||
- Messages address the user with the polite plural (`Unesite lozinku`).
|
||||
- Only the first word of a label is capitalised.
|
||||
- `@plural` rows use Bosnian's three CLDR forms, `one|few|other` (`{n} stavka|{n} stavke|{n}
|
||||
stavki`); the `craft-i18n` crate picks the form from CLDR's rules.
|
||||
- Quotation marks „…”; placeholders, a trailing `…` and the spaces around joined fragments match the
|
||||
English.
|
||||
|
||||
## Terminology
|
||||
|
||||
| Source | Bosnian |
|
||||
|---|---|
|
||||
| File / Home / Insert / Draw / Design / Layout | Datoteka / Početna / Umetni / Crtaj / Dizajn / Raspored |
|
||||
| References / Mailings / Review / View / Help | Reference / Pošiljke / Pregled izmjena / Prikaz / Pomoć |
|
||||
| Font / Paragraph / Styles | Font / Pasus / Stilovi |
|
||||
| Header / Footer / Heading | Zaglavlje / Podnožje / Naslov |
|
||||
| Footnotes / Endnotes / Caption | Fusnote / Krajnje bilješke / Natpis |
|
||||
| Table of Contents / Index | Sadržaj / Indeks |
|
||||
| Citations / Citation Style / Bibliography | Citati / Stil citiranja / Bibliografija |
|
||||
| Track Changes / Accept / Reject | Praćenje promjena / Prihvati / Odbij |
|
||||
| Comment / Show Markup | Komentar / Prikaži oznake |
|
||||
| Mail Merge | Spajanje pošte |
|
||||
| Sections / Section Break | Odjeljci / Prelom odjeljka |
|
||||
| Undo / Redo | Poništi / Ponovi |
|
||||
| Ribbon / Navigation Pane / Quick Access Toolbar | Traka / Navigacijsko okno / Traka za brzi pristup |
|
||||
| Bold / Italic / Underline / Strikethrough | Podebljano / Kurziv / Podvučeno / Precrtano |
|
||||
| Hyphenation | Rastavljanje riječi |
|
||||
| Watermark | Vodeni žig |
|
||||
| Save / Save As / Open / Print | Sačuvaj / Sačuvaj kao / Otvori / Štampaj |
|
||||
| Read Mode | Način čitanja |
|
||||
| Text Box | Okvir za tekst |
|
||||
|
||||
## Verification and remaining scope
|
||||
|
||||
Tests in `crates/ui-egui/src/i18n/tests.rs` check locale selection (Cyrillic Bosnian excluded), the
|
||||
saved preference, that choosing Bosnian leaves the document unchanged, and the bundled fonts'
|
||||
coverage of č, ć, đ, š, ž and the catalog's punctuation. The shared checks (catalog validation and
|
||||
the missing-translation report, `cargo xtask i18n-report`) include Bosnian automatically. Bosnian
|
||||
spelling, grammar and hyphenation are not included. Native speaker review is welcome.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-29 | major | First version: Bosnian (`bs`) interface language, conventions and terminology |
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (Arabic: the Home tab's Styles gallery reads from the right, its arrows at the left (#5566); readiness unchanged); previously trivial (table sort and formulas read the Panamanian balboa's `B/.` around a number (`B/. 1,250.50`, `1,250.50 B/.`), as Word does, matched only at the ends of the text so `1/.5` stays a fraction); previously trivial (Arabic: the dialogs of the mirrored Arabic interface read right to left: label-and-field rows, tables (list columns, the Models table), tile galleries and the Add/Remove arrows follow the reading direction, through one `rtl::Table` helper that draws exactly as before in English; no ranked gap closed, readiness estimates unchanged); previously minor (Options › Language redesign: Your languages, Add a language…, Set as default; the Dictionaries folder moved from Proofing); previously trivial (Language packs: the missing-pack bar works on a fresh install and counts the caret's language); previously trivial (Language lists: every installed and available Language pack's language can be chosen in Set Proofing Language, installed ones in Review › Language and the status bar; craft-i18n.rev at 9d6d54d: region names and the Language (Region) template in 35 locales, craft-i18n#145); previously trivial (craft-i18n.rev at cde4900: rows for Options › Language, the missing-pack bar and 28 Language pack language names in 35 locales, craft-i18n#136); previously trivial (craft-proof pin: French elision suggestions first, ca-ES-valencia reads the ca-valencia pack); previously minor (Arabic `ar` interface: craft-i18n catalog, mirrored shell, font fallback, #1195; 28 languages); previously trivial (Language packs: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland)); previously trivial (Language packs: language-packs-1 is published and pinned, 61 packs; the language requests it covers); previously minor (Language packs: languages beyond English install their spelling dictionary and hyphenation as a pack from File › Options › Language or the missing-pack bar; the default build embeds English Language data only) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Bosnian interface: `bs` in the language list, `bs-BA` system locales, catalog in storytold/craft-i18n); previously trivial (Arabic: the Home tab's Styles gallery reads from the right, its arrows at the left (#5566); readiness unchanged); previously trivial (table sort and formulas read the Panamanian balboa's `B/.` around a number (`B/. 1,250.50`, `1,250.50 B/.`), as Word does, matched only at the ends of the text so `1/.5` stays a fraction); previously trivial (Arabic: the dialogs of the mirrored Arabic interface read right to left: label-and-field rows, tables (list columns, the Models table), tile galleries and the Add/Remove arrows follow the reading direction, through one `rtl::Table` helper that draws exactly as before in English; no ranked gap closed, readiness estimates unchanged); previously minor (Options › Language redesign: Your languages, Add a language…, Set as default; the Dictionaries folder moved from Proofing); previously trivial (Language packs: the missing-pack bar works on a fresh install and counts the caret's language); previously trivial (Language lists: every installed and available Language pack's language can be chosen in Set Proofing Language, installed ones in Review › Language and the status bar; craft-i18n.rev at 9d6d54d: region names and the Language (Region) template in 35 locales, craft-i18n#145); previously trivial (craft-i18n.rev at cde4900: rows for Options › Language, the missing-pack bar and 28 Language pack language names in 35 locales, craft-i18n#136); previously trivial (craft-proof pin: French elision suggestions first, ca-ES-valencia reads the ca-valencia pack); previously minor (Arabic `ar` interface: craft-i18n catalog, mirrored shell, font fallback, #1195; 28 languages); previously trivial (Language packs: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland)); previously trivial (Language packs: language-packs-1 is published and pinned, 61 packs; the language requests it covers); previously minor (Language packs: languages beyond English install their spelling dictionary and hyphenation as a pack from File › Options › Language or the missing-pack bar; the default build embeds English Language data only) · **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
|
||||
@@ -21,8 +21,8 @@ native-speaker review (human).
|
||||
|
||||
## What WordCraft ships (measured)
|
||||
|
||||
- **28 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, Korean `ko` (#1216), `uk`, `es`, `pt-br`,
|
||||
Serbian `sr`/`sr-latn` (#250), Estonian `et`, Turkish `tr`, Russian `ru`, Italian `it` (#594), German `de` (#574), Polish `pl`, Czech `cs`, Hungarian `hu`, French `fr`, Bulgarian `bg`, Vietnamese `vi` (#1434), Swedish `sv` (#1219), Croatian `hr` (#1271), Romanian `ro`, Slovenian `sl`, Dutch `nl`, Danish `da` (#2722) and Arabic `ar` (#1195). Catalogs have 2,061–2,730 entries
|
||||
- **29 interface languages**: English plus `zh-hans`, `zh-hant`, `ja`, Korean `ko` (#1216), `uk`, `es`, `pt-br`,
|
||||
Serbian `sr`/`sr-latn` (#250), Estonian `et`, Turkish `tr`, Russian `ru`, Italian `it` (#594), German `de` (#574), Polish `pl`, Czech `cs`, Hungarian `hu`, French `fr`, Bulgarian `bg`, Vietnamese `vi` (#1434), Swedish `sv` (#1219), Croatian `hr` (#1271), Romanian `ro`, Slovenian `sl`, Dutch `nl`, Danish `da` (#2722) Arabic `ar` (#1195) and Bosnian `bs`. Catalogs have 2,061–2,730 entries
|
||||
each; Polish has every key of every other catalog, including the built-in style names and the equation gallery, except the citation style names only Hungarian, Bulgarian, Swedish, Romanian and Slovenian translate so far. A missing translation falls back to English and never fails a test or
|
||||
build. `cargo xtask i18n-report`, part of `cargo xtask ci`, prints per language the interface
|
||||
strings its catalog lacks: everything the interface audit saw looked up, the ribbon tabs, command
|
||||
@@ -72,8 +72,8 @@ shared items below.
|
||||
| Vietnamese | vi | 2,448 entries; every key of the reference catalogs (#1434); 98% of extracted, ~95% of visible | most | Latin ✅ | no | partial | 0–1 (review) |
|
||||
| Romanian | ro | 2,674 entries; every key of the other catalogs; AI-assisted draft | most | Latin ✅ (ă â î ș ț in the bundled faces) | no | partial | 0–1 (review) |
|
||||
|
||||
Other shipped languages: **20** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian
|
||||
Cyrillic and Latin (`sr`, `sr-latn`, #250), Estonian (`et`), Turkish (`tr`), Russian (`ru`), Italian (`it`, #594), Polish (`pl`), Czech (`cs`), Hungarian (`hu`), French (`fr`), Bulgarian (`bg`), Vietnamese (`vi`, #1434), Swedish (`sv`, #1219), Croatian (`hr`, #1271), Romanian (`ro`), Slovenian (`sl`), Dutch (`nl`) and Danish (`da`, #2722), all partial.
|
||||
Other shipped languages: **21** — Traditional Chinese (`zh-hant`), Ukrainian (`uk`), Serbian
|
||||
Cyrillic and Latin (`sr`, `sr-latn`, #250), Estonian (`et`), Turkish (`tr`), Russian (`ru`), Italian (`it`, #594), Polish (`pl`), Czech (`cs`), Hungarian (`hu`), French (`fr`), Bulgarian (`bg`), Vietnamese (`vi`, #1434), Swedish (`sv`, #1219), Croatian (`hr`, #1271), Romanian (`ro`), Slovenian (`sl`), Dutch (`nl`), Danish (`da`, #2722) and Bosnian (`bs`), all partial.
|
||||
Swedish (`sv-SE` and `sv-FI` system locales, `{"value":"sv"}`) has 2,504 entries. Counts use
|
||||
count-neutral wording (`{words} ord`: the noun is the same in the singular and the plural). The
|
||||
catalog is an AI-assisted draft whose terminology and running app were reviewed by a native
|
||||
@@ -97,6 +97,7 @@ hyphenated by the Bulgarian syllable rules (#826); Bulgarian spelling and gramma
|
||||
Slovenian (`sl-SI` system locales, `{"value":"sl"}`) has 2,614 entries: every key of every other
|
||||
catalog. Counts use count-neutral wording (`Besede: 21`); terminology: [`localization-sl.md`](localization-sl.md).
|
||||
Slovenian proofing is not included.
|
||||
Bosnian (`bs-BA` system locales, `{"value":"bs"}`) is Latin script; Cyrillic Bosnian stays in English. Its craft-i18n catalog has 2,508 WordCraft entries plus the shared catalog, with `one|few|other` plural rows. The catalog is an AI-assisted draft not yet reviewed by a native speaker; Bosnian proofing is not included; terminology: [`localization-bs.md`](localization-bs.md).
|
||||
Hungarian follows `hu-HU` system locales and `{"value":"hu"}`, with a complete catalog of 2,419 entries: every key of
|
||||
the Russian catalog, built-in style names shown translated, plus the citation style names. Hungarian proofing is not included; terminology and conventions:
|
||||
[`localization-hu.md`](localization-hu.md).
|
||||
@@ -275,6 +276,7 @@ WordCraft keeps 2,54 cm).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-29 | minor | Bosnian interface (`bs`, Latin script): listed in `LANGUAGES`, `bs-BA` system locales, saved preference and bundled-font tests; catalog in storytold/craft-i18n (`locales/bs/wordcraft.tsv`); no Bosnian proofing |
|
||||
| 2026-11-06 | trivial | Arabic: the Home tab's Styles gallery reads from the right: Normal is the rightmost tile and the scroll arrows and More sit at the gallery's left end, as the rest of the mirrored ribbon (#5566); left to right is unchanged; regression test `the_styles_gallery_reads_from_the_right_in_a_right_to_left_interface` |
|
||||
| 2026-11-05 | trivial | Arabic letters join across a change of formatting and past hidden or deleted text (#4093) |
|
||||
| 2026-11-04 | trivial | Table sort and formulas read the Panamanian balboa's `B/.` around a number (`B/. 1,250.50`, `1,250.50 B/.`), as Word does; `B/.1,250.50` and `B/ 1,250.50` stay text, matching Word 16.0, and the sign is matched only at the ends of the text, so `1/.5` is unchanged |
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user