mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-15 10:28:19 -06:00
Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae022437c8 | ||
|
|
bc0e51ee4d | ||
|
|
ed66198574 | ||
|
|
e9750c5cb7 | ||
|
|
3d5a153e3d | ||
|
|
52d17a91df | ||
|
|
323cf78232 | ||
|
|
42a5df5f94 | ||
|
|
f8c66d0692 | ||
|
|
aba890e8a6 | ||
|
|
0cc92c9744 | ||
|
|
b02baeba43 | ||
|
|
3c77297b83 | ||
|
|
60415e055c | ||
|
|
0db15a1d5e | ||
|
|
33afe69094 | ||
|
|
669d5df0b1 | ||
|
|
953f43baed | ||
|
|
acf5eb8727 | ||
|
|
6272c9a4da | ||
|
|
172e56f12e | ||
|
|
b66122f718 | ||
|
|
4e1b6a71ab | ||
|
|
b2ec73926f | ||
|
|
7f90fd0bf1 | ||
|
|
4388fba5b2 | ||
|
|
c341669dbd | ||
|
|
a4d28e2586 |
+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
+339
-100
@@ -155,13 +155,28 @@ version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-no-stdlib 2.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -210,15 +225,6 @@ version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.7.0"
|
||||
@@ -523,6 +529,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb2907cc03d08cdb1c6724ad0670ce67c4a71e1c45a6d7635224011714cfc4b1"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -614,9 +626,9 @@ version = "8.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
"alloc-no-stdlib 2.0.4",
|
||||
"alloc-stdlib 0.2.4",
|
||||
"brotli-decompressor 5.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -625,8 +637,18 @@ version = "5.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"alloc-no-stdlib 2.0.4",
|
||||
"alloc-stdlib 0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "6.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75c74c9c51b9140663e196b5fb88bc94c334d7a497c8311cb759d8557874747f"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib 3.0.0",
|
||||
"alloc-stdlib 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1022,17 +1044,17 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-cfb"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
|
||||
[[package]]
|
||||
name = "craft-codepage"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
|
||||
[[package]]
|
||||
name = "craft-doc-binary"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-cfb",
|
||||
@@ -1048,7 +1070,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-docx"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
@@ -1068,7 +1090,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-drawingml"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-xml",
|
||||
"kurbo",
|
||||
@@ -1079,7 +1101,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-flow"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-wordmodel",
|
||||
"image",
|
||||
@@ -1122,6 +1144,19 @@ name = "craft-fonts-build"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
|
||||
[[package]]
|
||||
name = "craft-html"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-codepage",
|
||||
"craft-flow",
|
||||
"craft-math",
|
||||
"craft-mime",
|
||||
"craft-wordmodel",
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-i18n"
|
||||
version = "0.1.0"
|
||||
@@ -1145,19 +1180,83 @@ dependencies = [
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-image-codecs"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-jpeg",
|
||||
"craft-metadata",
|
||||
"exr",
|
||||
"flate2",
|
||||
"half",
|
||||
"image",
|
||||
"image-webp",
|
||||
"jpeg-encoder",
|
||||
"png",
|
||||
"rayon",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
"zune-jpegxl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-jpeg"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-latex"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-flow",
|
||||
"craft-math",
|
||||
"craft-mime",
|
||||
"craft-wordmodel",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-markdown"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-flow",
|
||||
"craft-html",
|
||||
"craft-wordmodel",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-math"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-xml",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-metadata"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"brotli-decompressor 6.3.1",
|
||||
"chrono",
|
||||
"craft-tiff",
|
||||
"quick-xml 0.41.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-metafile"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"image",
|
||||
@@ -1165,10 +1264,58 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-mime"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"craft-codepage",
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-odf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
"craft-flow",
|
||||
"craft-math",
|
||||
"craft-metafile",
|
||||
"craft-wordmodel",
|
||||
"craft-xml",
|
||||
"craft-zip",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"image",
|
||||
"js-sys",
|
||||
"log",
|
||||
"sha1 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"thiserror 2.0.21",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-ods"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-odf",
|
||||
"craft-sheet",
|
||||
"craft-xml",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-offcrypto"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.23.1",
|
||||
@@ -1185,7 +1332,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-opc"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-xml",
|
||||
"craft-zip",
|
||||
@@ -1230,7 +1377,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-rtf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-codepage",
|
||||
"craft-flow",
|
||||
@@ -1240,6 +1387,20 @@ dependencies = [
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-sheet"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
|
||||
[[package]]
|
||||
name = "craft-tiff"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
@@ -1284,7 +1445,7 @@ dependencies = [
|
||||
"verifysign",
|
||||
"winreg",
|
||||
"winsafe",
|
||||
"zip 9.1.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1329,7 +1490,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-wordmodel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"craft-math",
|
||||
@@ -1340,10 +1501,35 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-xls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-cfb",
|
||||
"craft-sheet",
|
||||
"md-5",
|
||||
"miniz_oxide 0.9.1",
|
||||
"sha1 0.10.7",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-xlsx"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"craft-drawingml",
|
||||
"craft-opc",
|
||||
"craft-sheet",
|
||||
"craft-xml",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-xml"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"quick-xml 0.41.0",
|
||||
"thiserror 2.0.21",
|
||||
@@ -1352,11 +1538,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-zip"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=75c70fa6a786240eb853e6267e25fc445f510cd0#75c70fa6a786240eb853e6267e25fc445f510cd0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=41fb227542efb8a7748caa1c659f271d8605303c#41fb227542efb8a7748caa1c659f271d8605303c"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"thiserror 2.0.21",
|
||||
"zip 9.1.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1484,17 +1670,6 @@ version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dify"
|
||||
version = "0.8.0"
|
||||
@@ -1906,6 +2081,22 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.74.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb29d0be702adf92c645d9b2f93472906ba83d400e59f98adb3770ce5c18c2f6"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide 0.8.9",
|
||||
"multiversion",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
@@ -2954,6 +3145,7 @@ dependencies = [
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
@@ -3102,6 +3294,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-encoder"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b454d911ac55068f53495488d8ccd0646eaa540c033a28ee15b07838afafb01f"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
@@ -3185,6 +3383,12 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
@@ -3273,6 +3477,16 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -3344,6 +3558,28 @@ dependencies = [
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiversion"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb8a68be21d0755f83bfe47e79b896cc732687cf1903f0e50585e8d6ecc53076"
|
||||
dependencies = [
|
||||
"multiversion-macros",
|
||||
"target-features",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiversion-macros"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a05e0c3c3e4edaa21d87c645852ed0b0c3663752bcc6ec43353bd6189dce35e1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"target-features",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiversion_no_op"
|
||||
version = "1.0.0"
|
||||
@@ -4287,6 +4523,15 @@ version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@@ -5564,6 +5809,12 @@ dependencies = [
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-features"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -7010,16 +7261,17 @@ dependencies = [
|
||||
name = "wordcraft-cli"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-zip",
|
||||
"image",
|
||||
"serde_json",
|
||||
"sys-locale",
|
||||
"wordcraft-doc",
|
||||
"wordcraft-engine",
|
||||
"wordcraft-fonts",
|
||||
"wordcraft-geom",
|
||||
"wordcraft-mcp",
|
||||
"wordcraft-render",
|
||||
"wordcraft-zotero",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7030,6 +7282,7 @@ version = "0.18.1"
|
||||
name = "wordcraft-doc"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-codepage",
|
||||
"base64 0.23.1",
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -7060,15 +7313,15 @@ dependencies = [
|
||||
"cfb",
|
||||
"craft-docx",
|
||||
"craft-wordmodel",
|
||||
"craft-xml",
|
||||
"craft-zip",
|
||||
"image",
|
||||
"log",
|
||||
"proptest",
|
||||
"quick-xml 0.41.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wordcraft-doc",
|
||||
"wordcraft-geom",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7080,6 +7333,7 @@ dependencies = [
|
||||
"craft-font-catalog",
|
||||
"craft-font-store",
|
||||
"craft-proof",
|
||||
"craft-zip",
|
||||
"encoding_rs",
|
||||
"getrandom 0.3.4",
|
||||
"image",
|
||||
@@ -7102,14 +7356,13 @@ dependencies = [
|
||||
"wordcraft-pdf",
|
||||
"wordcraft-render",
|
||||
"zeroize",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-fonts"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"brotli-decompressor",
|
||||
"brotli-decompressor 5.0.3",
|
||||
"harfrust 0.13.3",
|
||||
"kurbo",
|
||||
"log",
|
||||
@@ -7123,35 +7376,37 @@ dependencies = [
|
||||
name = "wordcraft-formats"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64 0.23.1",
|
||||
"cfb",
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"craft-flow",
|
||||
"craft-html",
|
||||
"craft-image-codecs",
|
||||
"craft-latex",
|
||||
"craft-markdown",
|
||||
"craft-math",
|
||||
"craft-metafile",
|
||||
"craft-mime",
|
||||
"craft-odf",
|
||||
"craft-ods",
|
||||
"craft-rtf",
|
||||
"craft-sheet",
|
||||
"craft-wordmodel",
|
||||
"craft-xls",
|
||||
"craft-xlsx",
|
||||
"craft-zip",
|
||||
"encoding_rs",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"image",
|
||||
"js-sys",
|
||||
"log",
|
||||
"miniz_oxide 0.9.1",
|
||||
"proptest",
|
||||
"quick-xml 0.41.0",
|
||||
"sha1 0.11.0",
|
||||
"sha2 0.11.1",
|
||||
"wordcraft-doc",
|
||||
"wordcraft-docx",
|
||||
"wordcraft-geom",
|
||||
"wordcraft-metafile",
|
||||
"zeroize",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-geom"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-image-codecs",
|
||||
"image",
|
||||
"serde",
|
||||
]
|
||||
@@ -7160,6 +7415,7 @@ dependencies = [
|
||||
name = "wordcraft-layout"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-metafile",
|
||||
"craft-proof",
|
||||
"image",
|
||||
"js-sys",
|
||||
@@ -7173,7 +7429,6 @@ dependencies = [
|
||||
"wordcraft-doc",
|
||||
"wordcraft-fonts",
|
||||
"wordcraft-geom",
|
||||
"wordcraft-metafile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7188,19 +7443,12 @@ dependencies = [
|
||||
"wordcraft-render",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-metafile"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-pdf"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-metafile",
|
||||
"craft-pdf",
|
||||
"hayro-cmap",
|
||||
"hayro-interpret",
|
||||
@@ -7214,13 +7462,13 @@ dependencies = [
|
||||
"wordcraft-fonts",
|
||||
"wordcraft-geom",
|
||||
"wordcraft-layout",
|
||||
"wordcraft-metafile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wordcraft-render"
|
||||
version = "0.18.1"
|
||||
dependencies = [
|
||||
"craft-metafile",
|
||||
"image",
|
||||
"log",
|
||||
"vello_cpu",
|
||||
@@ -7228,7 +7476,6 @@ dependencies = [
|
||||
"wordcraft-fonts",
|
||||
"wordcraft-geom",
|
||||
"wordcraft-layout",
|
||||
"wordcraft-metafile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7242,6 +7489,7 @@ dependencies = [
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
"craft-window",
|
||||
"craft-zip",
|
||||
"egui",
|
||||
"egui_extras",
|
||||
"egui_kittest",
|
||||
@@ -7261,7 +7509,6 @@ dependencies = [
|
||||
"wordcraft-render",
|
||||
"wordcraft-zotero",
|
||||
"zbus",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7333,7 +7580,7 @@ version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200a7b806377b65f1ff9a985874332f15357c87e5dd7855f35f06dade95b8b3d"
|
||||
dependencies = [
|
||||
"brotli-decompressor",
|
||||
"brotli-decompressor 5.0.3",
|
||||
"bytes",
|
||||
"flate2",
|
||||
]
|
||||
@@ -7693,20 +7940,6 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.1.0"
|
||||
@@ -7735,24 +7968,21 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
@@ -7762,6 +7992,15 @@ dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpegxl"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba11ecd07cc23351500e840ae03841b7e4997923ec8e4832ee3ae199ea0b6b4"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.15.0"
|
||||
|
||||
+21
-8
@@ -36,7 +36,8 @@ wordcraft-mcp = { path = "crates/mcp" }
|
||||
wordcraft-zotero = { path = "crates/zotero" }
|
||||
wordcraft-control-key = { path = "crates/control-key" }
|
||||
wordcraft-ui-egui = { path = "crates/ui-egui" }
|
||||
wordcraft-metafile = { path = "crates/metafile" }
|
||||
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
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
@@ -66,16 +67,28 @@ wuff = "0.2.9"
|
||||
# `forbid(unsafe_code)` in its default build, no_std-capable (builds for wasm), BSD-3-Clause/MIT.
|
||||
brotli-decompressor = "5"
|
||||
vello_cpu = "0.2"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "bmp", "tiff"] }
|
||||
image = { version = "0.25", default-features = false }
|
||||
craft-image-codecs = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", version = "=0.1.0", default-features = false, features = ["write"] }
|
||||
# craft-update: app-specific pretend versions apply to startup and manual checks.
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "6e03f65bf45181db77ba6229716e6624db9eb25b", features = ["egui"] }
|
||||
craft-wordmodel = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0" }
|
||||
craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0" }
|
||||
craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0", features = ["doc"] }
|
||||
craft-doc-binary = { git = "https://github.com/storytold/craft-libs", rev = "75c70fa6a786240eb853e6267e25fc445f510cd0", version = "=0.1.0" }
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
craft-wordmodel = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0", features = ["doc"] }
|
||||
craft-doc-binary = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-flow = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-odf = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0", features = ["text"] }
|
||||
craft-markdown = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-html = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0", features = ["flow"] }
|
||||
craft-latex = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-math = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-mime = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0", features = ["detect"] }
|
||||
craft-sheet = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-ods = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-xls = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
craft-zip = { git = "https://github.com/storytold/craft-libs", rev = "41fb227542efb8a7748caa1c659f271d8605303c", version = "=0.1.0" }
|
||||
# zlib streams (compressed metafile pictures in `.doc` drawings); already in the lock as a dependency of other crates.
|
||||
miniz_oxide = "0.9"
|
||||
cfb = "0.15"
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
-1
File diff suppressed because one or more lines are too long
@@ -24,5 +24,6 @@ sys-locale = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
wordcraft-fonts = { workspace = true }
|
||||
wordcraft-geom = { workspace = true, features = ["image"] }
|
||||
image = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
craft-zip = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! `convert --strict` (#6005): exit 3 and a report when a PDF or PNG would silently lack a
|
||||
//! character or a picture, nothing written; exit 0 otherwise.
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
||||
@@ -64,11 +63,11 @@ fn uncovered() -> char {
|
||||
}
|
||||
|
||||
fn png() -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
image::RgbaImage::from_pixel(4, 4, image::Rgba([20, 90, 200, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(4, 4, image::Rgba([20, 90, 200, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn pic(rid: &str) -> String {
|
||||
@@ -106,7 +105,7 @@ fn package(document: &str, document_rels: &str, extra: &[(&str, &[u8])]) -> Vec<
|
||||
r#"<Relationships xmlns="{RELS_NS}"><Relationship Id="rId1" Type="{REL}/officeDocument" Target="word/document.xml"/></Relationships>"#
|
||||
);
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="png" ContentType="image/png"/><Default Extension="bin" ContentType="application/octet-stream"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
let mut parts: Vec<(&str, &[u8])> = vec![
|
||||
("[Content_Types].xml", types.as_bytes()),
|
||||
("_rels/.rels", rels.as_bytes()),
|
||||
@@ -115,10 +114,9 @@ fn package(document: &str, document_rels: &str, extra: &[(&str, &[u8])]) -> Vec<
|
||||
];
|
||||
parts.extend_from_slice(extra);
|
||||
for (name, bytes) in parts {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(bytes).unwrap();
|
||||
zip.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1e515422afdda2f317af430c1f9d68a5f83935b4
|
||||
e56d26244bbac099bfd42e23fb12f78a4be418a7
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,16 @@ workspace = true
|
||||
craft-docx = { workspace = true }
|
||||
craft-wordmodel = { workspace = true }
|
||||
wordcraft-doc = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
craft-xml = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wordcraft-doc = { workspace = true, features = ["test-support"] }
|
||||
wordcraft-geom = { workspace = true }
|
||||
wordcraft-geom = { workspace = true, features = ["image"] }
|
||||
base64 = { workspace = true }
|
||||
cfb = { workspace = true }
|
||||
log = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
image = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
craft-zip = { workspace = true }
|
||||
|
||||
+6
-27
@@ -19,39 +19,18 @@ pub(crate) const KIND: &str = "wordcraft-fit-v1";
|
||||
/// Serialize `fit` as the custom XML store.
|
||||
pub(crate) fn write(fit: &FitState) -> Result<Vec<u8>, DocxError> {
|
||||
let json = serde_json::to_string(fit).map_err(|e| DocxError::Xml(e.to_string()))?;
|
||||
let esc_json = quick_xml::escape::escape(&json);
|
||||
let esc_json = craft_xml::esc(&json);
|
||||
Ok(format!(r#"<?xml version="1.0" encoding="UTF-8"?><{ROOT} xmlns="{NS}" kind="{KIND}"><fitState>{}</fitState></{ROOT}>"#, esc_json).into_bytes())
|
||||
}
|
||||
|
||||
/// Read the state from a custom XML part's bytes, if it is ours.
|
||||
pub(crate) fn read_from_bytes(bytes: &[u8]) -> Option<FitState> {
|
||||
let mut reader = quick_xml::Reader::from_reader(bytes);
|
||||
let mut buf = Vec::new();
|
||||
let mut is_our_kind = false;
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(quick_xml::events::Event::Start(ref e)) => {
|
||||
let name = e.local_name();
|
||||
if name.as_ref() == ROOT.as_bytes() {
|
||||
for attr in e.attributes().flatten() {
|
||||
if attr.key.local_name().as_ref() == b"kind" && attr.value.as_ref() == KIND.as_bytes() {
|
||||
is_our_kind = true;
|
||||
}
|
||||
}
|
||||
} else if name.as_ref() == b"fitState" && is_our_kind {
|
||||
let text = reader.read_text(e.to_end().name()).ok()?;
|
||||
let s = std::str::from_utf8(text.as_ref()).ok()?;
|
||||
let unescaped = quick_xml::escape::unescape(s).ok()?;
|
||||
return serde_json::from_str(&unescaped).ok();
|
||||
}
|
||||
}
|
||||
Ok(quick_xml::events::Event::Eof) | Err(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
let root = craft_xml::parse(bytes).ok()?;
|
||||
if root.local() != ROOT || root.attr("kind") != Some(KIND) {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
let text = root.child_text("fitState")?;
|
||||
serde_json::from_str(text.as_ref()).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Bounded conversion between craft-wordmodel and WordCraft's document model.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use craft_docx::DocxError;
|
||||
@@ -29,24 +30,29 @@ where
|
||||
// From Shared Model -> WordCraft Document
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn blocks_from_shared(source: craft_wordmodel::Blocks) -> Result<wordcraft_doc::Blocks, DocxError> {
|
||||
type GraphicCache = HashMap<usize, Arc<wordcraft_doc::graphic::Graphic>>;
|
||||
|
||||
fn blocks_from_shared(source: craft_wordmodel::Blocks, graphics: &mut GraphicCache) -> Result<wordcraft_doc::Blocks, DocxError> {
|
||||
source
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
let block = Arc::try_unwrap(block).unwrap_or_else(|shared| (*shared).clone());
|
||||
block_value_from_shared(block).map(Arc::new)
|
||||
block_value_from_shared(block, graphics).map(Arc::new)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn block_value_from_shared(source: craft_wordmodel::Block) -> Result<wordcraft_doc::Block, DocxError> {
|
||||
fn block_value_from_shared(source: craft_wordmodel::Block, graphics: &mut GraphicCache) -> Result<wordcraft_doc::Block, DocxError> {
|
||||
match source {
|
||||
craft_wordmodel::Block::Para(paragraph) => paragraph_value_from_shared(paragraph).map(wordcraft_doc::Block::Para),
|
||||
craft_wordmodel::Block::Table(table) => table_value_from_shared(table).map(wordcraft_doc::Block::Table),
|
||||
craft_wordmodel::Block::Para(paragraph) => paragraph_value_from_shared(paragraph, graphics).map(wordcraft_doc::Block::Para),
|
||||
craft_wordmodel::Block::Table(table) => table_value_from_shared(table, graphics).map(wordcraft_doc::Block::Table),
|
||||
}
|
||||
}
|
||||
|
||||
fn paragraph_value_from_shared(source: craft_wordmodel::para::Paragraph) -> Result<wordcraft_doc::para::Paragraph, DocxError> {
|
||||
fn paragraph_value_from_shared(
|
||||
source: craft_wordmodel::para::Paragraph,
|
||||
graphics: &mut GraphicCache,
|
||||
) -> Result<wordcraft_doc::para::Paragraph, DocxError> {
|
||||
let craft_wordmodel::para::Paragraph { text, runs, props, mark, objects, section, .. } = source;
|
||||
let mut paragraph = wordcraft_doc::para::Paragraph::new();
|
||||
paragraph.text = text;
|
||||
@@ -54,12 +60,15 @@ fn paragraph_value_from_shared(source: craft_wordmodel::para::Paragraph) -> Resu
|
||||
runs.into_iter().map(|run| Ok(wordcraft_doc::para::Run { len: run.len, props: convert(run.props)? })).collect::<Result<_, DocxError>>()?;
|
||||
paragraph.props = convert(props)?;
|
||||
paragraph.mark = convert(mark)?;
|
||||
paragraph.objects = objects.into_iter().map(object_value_from_shared).collect::<Result<_, _>>()?;
|
||||
paragraph.objects = objects.into_iter().map(|o| object_value_from_shared(o, graphics)).collect::<Result<_, _>>()?;
|
||||
paragraph.section = section.map(|section| convert(*section).map(Box::new)).transpose()?;
|
||||
Ok(paragraph)
|
||||
}
|
||||
|
||||
fn object_value_from_shared(source: craft_wordmodel::para::InlineObject) -> Result<wordcraft_doc::para::InlineObject, DocxError> {
|
||||
fn object_value_from_shared(
|
||||
source: craft_wordmodel::para::InlineObject,
|
||||
graphics: &mut GraphicCache,
|
||||
) -> Result<wordcraft_doc::para::InlineObject, DocxError> {
|
||||
use craft_wordmodel::para::InlineObject as Source;
|
||||
use wordcraft_doc::para::InlineObject as Target;
|
||||
|
||||
@@ -74,49 +83,70 @@ fn object_value_from_shared(source: craft_wordmodel::para::InlineObject) -> Resu
|
||||
fx: convert(fx)?,
|
||||
ole: ole.map(|o| convert(Arc::try_unwrap(o).unwrap_or_else(|s| (*s).clone())).map(Arc::new)).transpose()?,
|
||||
}),
|
||||
Source::Graphic { w, h, alt, float, graphic } => {
|
||||
let key = Arc::as_ptr(&graphic) as usize;
|
||||
let graphic = if let Some(cached) = graphics.get(&key) {
|
||||
Arc::clone(cached)
|
||||
} else {
|
||||
let converted: Arc<wordcraft_doc::graphic::Graphic> = Arc::new(convert(Arc::try_unwrap(graphic).unwrap_or_else(|s| (*s).clone()))?);
|
||||
graphics.insert(key, Arc::clone(&converted));
|
||||
converted
|
||||
};
|
||||
Ok(Target::Graphic { w, h, alt, float: convert(float)?, graphic })
|
||||
}
|
||||
Source::Group { w, h, float, ch_w, ch_h, children, canvas } => Ok(Target::Group {
|
||||
w,
|
||||
h,
|
||||
float: convert(float)?,
|
||||
ch_w,
|
||||
ch_h,
|
||||
children: children.into_iter().map(group_child_value_from_shared).collect::<Result<_, _>>()?,
|
||||
children: children.into_iter().map(|c| group_child_value_from_shared(c, graphics)).collect::<Result<_, _>>()?,
|
||||
canvas: canvas.map(convert).transpose()?,
|
||||
}),
|
||||
other => convert(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn group_child_value_from_shared(source: craft_wordmodel::para::GroupChild) -> Result<wordcraft_doc::para::GroupChild, DocxError> {
|
||||
Ok(wordcraft_doc::para::GroupChild { x: source.x, y: source.y, obj: object_value_from_shared(source.obj)? })
|
||||
fn group_child_value_from_shared(
|
||||
source: craft_wordmodel::para::GroupChild,
|
||||
graphics: &mut GraphicCache,
|
||||
) -> Result<wordcraft_doc::para::GroupChild, DocxError> {
|
||||
Ok(wordcraft_doc::para::GroupChild { x: source.x, y: source.y, obj: object_value_from_shared(source.obj, graphics)? })
|
||||
}
|
||||
|
||||
fn table_value_from_shared(source: craft_wordmodel::table::Table) -> Result<wordcraft_doc::table::Table, DocxError> {
|
||||
fn table_value_from_shared(source: craft_wordmodel::table::Table, graphics: &mut GraphicCache) -> Result<wordcraft_doc::table::Table, DocxError> {
|
||||
Ok(wordcraft_doc::table::Table {
|
||||
props: convert(source.props)?,
|
||||
grid: source.grid,
|
||||
rows: source.rows.into_iter().map(row_value_from_shared).collect::<Result<_, _>>()?,
|
||||
rows: source.rows.into_iter().map(|r| row_value_from_shared(r, graphics)).collect::<Result<_, _>>()?,
|
||||
controls: convert(source.controls)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn row_value_from_shared(source: craft_wordmodel::table::Row) -> Result<wordcraft_doc::table::Row, DocxError> {
|
||||
fn row_value_from_shared(source: craft_wordmodel::table::Row, graphics: &mut GraphicCache) -> Result<wordcraft_doc::table::Row, DocxError> {
|
||||
Ok(wordcraft_doc::table::Row {
|
||||
props: convert(source.props)?,
|
||||
cells: source.cells.into_iter().map(cell_value_from_shared).collect::<Result<_, _>>()?,
|
||||
cells: source.cells.into_iter().map(|c| cell_value_from_shared(c, graphics)).collect::<Result<_, _>>()?,
|
||||
controls: convert(source.controls)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn cell_value_from_shared(source: craft_wordmodel::table::Cell) -> Result<wordcraft_doc::table::Cell, DocxError> {
|
||||
Ok(wordcraft_doc::table::Cell { props: convert(source.props)?, blocks: blocks_from_shared(source.blocks)?, controls: convert(source.controls)? })
|
||||
fn cell_value_from_shared(source: craft_wordmodel::table::Cell, graphics: &mut GraphicCache) -> Result<wordcraft_doc::table::Cell, DocxError> {
|
||||
Ok(wordcraft_doc::table::Cell {
|
||||
props: convert(source.props)?,
|
||||
blocks: blocks_from_shared(source.blocks, graphics)?,
|
||||
controls: convert(source.controls)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn part_value_from_shared(source: craft_wordmodel::Part) -> Result<wordcraft_doc::Part, DocxError> {
|
||||
Ok(wordcraft_doc::Part { kind: convert(source.kind)?, blocks: blocks_from_shared(source.blocks)?, body: convert(source.body)? })
|
||||
fn part_value_from_shared(source: craft_wordmodel::Part, graphics: &mut GraphicCache) -> Result<wordcraft_doc::Part, DocxError> {
|
||||
Ok(wordcraft_doc::Part { kind: convert(source.kind)?, blocks: blocks_from_shared(source.blocks, graphics)?, body: convert(source.body)? })
|
||||
}
|
||||
|
||||
fn glossary_value_from_shared(source: craft_wordmodel::BuildingBlock) -> Result<wordcraft_doc::BuildingBlock, DocxError> {
|
||||
fn glossary_value_from_shared(
|
||||
source: craft_wordmodel::BuildingBlock,
|
||||
graphics: &mut GraphicCache,
|
||||
) -> Result<wordcraft_doc::BuildingBlock, DocxError> {
|
||||
Ok(wordcraft_doc::BuildingBlock {
|
||||
name: source.name,
|
||||
gallery: source.gallery,
|
||||
@@ -126,7 +156,7 @@ fn glossary_value_from_shared(source: craft_wordmodel::BuildingBlock) -> Result<
|
||||
style: source.style,
|
||||
types: source.types,
|
||||
behaviors: source.behaviors,
|
||||
blocks: blocks_from_shared(source.blocks)?,
|
||||
blocks: blocks_from_shared(source.blocks, graphics)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,7 +168,7 @@ fn missing_pictures_from_shared(source: craft_wordmodel::MissingPictures) -> Res
|
||||
target.part_names = source.part_names;
|
||||
target.media_parts = source.media_parts;
|
||||
for item in source.list {
|
||||
target.push(convert(item)?);
|
||||
target.list.push(convert(item)?);
|
||||
}
|
||||
Ok(target)
|
||||
}
|
||||
@@ -176,10 +206,11 @@ pub fn from_shared_model(mut source: craft_wordmodel::Document) -> Result<wordcr
|
||||
..
|
||||
} = source;
|
||||
|
||||
let mut graphics = GraphicCache::new();
|
||||
let mut document = wordcraft_doc::Document::new();
|
||||
document.body = blocks_from_shared(body)?;
|
||||
document.body = blocks_from_shared(body, &mut graphics)?;
|
||||
document.last_section = convert(last_section)?;
|
||||
document.parts = parts.into_iter().map(|(id, part)| Ok((id, part_value_from_shared(part)?))).collect::<Result<_, DocxError>>()?;
|
||||
document.parts = parts.into_iter().map(|(id, part)| Ok((id, part_value_from_shared(part, &mut graphics)?))).collect::<Result<_, DocxError>>()?;
|
||||
document.styles = Arc::new(convert((*styles).clone())?);
|
||||
document.numbering = Arc::new(convert((*numbering).clone())?);
|
||||
document.comments = convert(comments)?;
|
||||
@@ -192,7 +223,7 @@ pub fn from_shared_model(mut source: craft_wordmodel::Document) -> Result<wordcr
|
||||
document.endnote_separators = convert(endnote_separators)?;
|
||||
document.media = media;
|
||||
document.passthrough = passthrough;
|
||||
document.glossary = glossary.into_iter().map(glossary_value_from_shared).collect::<Result<_, _>>()?;
|
||||
document.glossary = glossary.into_iter().map(|g| glossary_value_from_shared(g, &mut graphics)).collect::<Result<_, _>>()?;
|
||||
document.missing_pictures = missing_pictures_from_shared(missing_pictures)?;
|
||||
document.fit_state = fit_state;
|
||||
Ok(document)
|
||||
@@ -344,7 +375,7 @@ fn missing_pictures_to_shared(source: &wordcraft_doc::MissingPictures) -> Result
|
||||
target.part_names = source.part_names.clone();
|
||||
target.media_parts = source.media_parts.clone();
|
||||
for item in &source.list {
|
||||
target.push(convert_ref(item)?);
|
||||
target.list.push(convert_ref(item)?);
|
||||
}
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Alternative format chunks (`w:altChunk`, ECMA-376 §17.17.2.1): content in another format that
|
||||
//! is brought in where the chunk stands. All markup here is synthetic.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::{Block, Document, InlineObject, StoryRef};
|
||||
use wordcraft_docx::{AltChunk, ReadOptions, read, read_with};
|
||||
|
||||
@@ -46,12 +44,11 @@ fn package_styled(body: &str, chunk: Option<(&str, &str, &[u8])>, styles: Option
|
||||
}
|
||||
types.push_str(r#"<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#);
|
||||
files.push(("[Content_Types].xml".into(), types.into_bytes()));
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in files {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(&bytes).unwrap();
|
||||
zw.add(&name, &bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
const CHUNK: &str = r#"<w:altChunk r:id="rIdC"/>"#;
|
||||
@@ -144,12 +141,11 @@ fn package_with_stories(body: &str, stories: &[(&str, &str, String)], chunk: Opt
|
||||
if let Some(c) = chunk {
|
||||
files.push(("word/afchunk.docx".into(), c.to_vec()));
|
||||
}
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in files {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(&bytes).unwrap();
|
||||
zw.add(&name, &bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A paragraph `text` that comment `id` (in the file) is anchored around.
|
||||
@@ -395,17 +391,16 @@ fn inner_with_dropped_pictures(n: usize, external: bool) -> Vec<u8> {
|
||||
(0..n).map(image).collect::<String>()
|
||||
);
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [
|
||||
("[Content_Types].xml", types.as_bytes()),
|
||||
("_rels/.rels", ROOT_RELS.as_bytes()),
|
||||
("word/document.xml", doc.as_bytes()),
|
||||
("word/_rels/document.xml.rels", rels.as_bytes()),
|
||||
] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A picture the chunk's own reader dropped is the file's too (#6005), named by the chunk part.
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
//! complex-script font, size, bold and italic, and the bidi language. Fixtures are hand-written
|
||||
//! from ECMA-376 Part 1 §17.3.1.6 (bidi), §17.3.2 (run properties).
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::{Align, CharProps, ParaProps};
|
||||
use wordcraft_doc::{Document, Paragraph, para_block};
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A package whose body is `body`.
|
||||
@@ -31,10 +28,9 @@ fn paras(d: &Document) -> Vec<&Paragraph> {
|
||||
|
||||
/// `word/document.xml` of a written package.
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// Persian text with a ZWNJ (نیمفاصله), Persian digits, Latin and punctuation.
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//! direction changes and marks the right-to-left pieces `w:rtl`, as Word does with text typed
|
||||
//! in a right-to-left paragraph.
|
||||
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
|
||||
use wordcraft_doc::props::CharProps;
|
||||
@@ -14,10 +13,9 @@ use wordcraft_doc::{Document, Paragraph, para_block};
|
||||
|
||||
/// `word/document.xml` of a written package.
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// The runs of the first paragraph of `xml`: (has `w:rtl`, text with `w:br` as `\n`).
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! `thinThickThinLargeGap`, ECMA-376 Part 1 §17.18.2 ST_Border) on paragraphs, table cells and
|
||||
//! pages: written back as read, not as a single line. Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::BorderStyle;
|
||||
|
||||
const LINES: [(&str, BorderStyle); 9] = [
|
||||
@@ -23,19 +21,17 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn side(name: &str, val: &str) -> String {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
//! Original partial-cell-margin fixture (ECMA-376 §17.4.69).
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
fn fixture() -> Vec<u8> {
|
||||
let document = r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:tbl><w:tblPr><w:tblCellMar><w:top w:w="55" w:type="dxa"/><w:left w:w="45" w:type="dxa"/><w:bottom w:w="55" w:type="dxa"/><w:right w:w="55" w:type="dxa"/></w:tblCellMar></w:tblPr><w:tblGrid><w:gridCol w:w="4320"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcMar><w:left w:w="0" w:type="dxa"/></w:tcMar></w:tcPr><w:p><w:r><w:t>Point</w:t></w:r></w:p></w:tc></w:tr></w:tbl></w:body></w:document>"#;
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
zip.start_file("word/document.xml", zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(document.as_bytes()).unwrap();
|
||||
zip.finish().unwrap().into_inner()
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
zip.add("word/document.xml", document.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -19,9 +17,8 @@ fn partial_margin_stays_partial() {
|
||||
let reopened = wordcraft_docx::read(&bytes).unwrap();
|
||||
let cell = &reopened.body[0].as_table().unwrap().rows[0].cells[0];
|
||||
assert_eq!(cell.props.resolved_margins([6.0, 7.0, 8.0, 9.0]), [6.0, 0.0, 8.0, 9.0]);
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let margins = xml.split("<w:tcMar>").nth(1).unwrap().split("</w:tcMar>").next().unwrap();
|
||||
assert!(margins.contains("<w:left w:w=\"0\""));
|
||||
assert!(!margins.contains("<w:top"));
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
//! `w:ind` `w:firstLineChars`, `w:hangingChars`, `w:leftChars` and `w:rightChars`, in hundredths
|
||||
//! of a character. Fixtures are hand-written.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::Document;
|
||||
use wordcraft_doc::props::ParaProps;
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A package whose body is `body`.
|
||||
@@ -44,9 +41,9 @@ fn character_indents_read_and_round_trip() {
|
||||
// Hostile values are clamped or dropped.
|
||||
assert_eq!((p[3].indent_first_chars, p[3].indent_left_chars), (Some(1584.0), None));
|
||||
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_docx::write(&d).unwrap())).unwrap();
|
||||
let mut xml = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let _z_bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
for attr in [r#"w:firstLineChars="200""#, r#"w:leftChars="150""#, r#"w:rightChars="50""#, r#"w:hangingChars="100""#, r#"w:firstLineChars="0""#] {
|
||||
assert!(xml.contains(attr), "{attr} missing: {xml}");
|
||||
}
|
||||
@@ -71,9 +68,9 @@ fn automatic_spacing_reads_and_round_trips() {
|
||||
assert_eq!((rp.space_before, rp.space_after, rp.before_auto), (14.0, 14.0, true), "automatic spacing is 14 pt");
|
||||
assert_eq!(d.styles.resolve_para(&p[2]).space_after, 5.0);
|
||||
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_docx::write(&d).unwrap())).unwrap();
|
||||
let mut xml = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let _z_bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
for attr in [r#"w:beforeAutospacing="1""#, r#"w:afterAutospacing="1""#, r#"w:beforeAutospacing="0""#] {
|
||||
assert!(xml.contains(attr), "{attr} missing");
|
||||
}
|
||||
@@ -90,9 +87,8 @@ fn adjust_right_indent_is_preserved() {
|
||||
assert_eq!(props(&d).iter().map(|p| p.adjust_right_indent).collect::<Vec<_>>(), [Some(false), Some(true), None]);
|
||||
for _ in 0..2 {
|
||||
let out = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(out.clone())).unwrap();
|
||||
let mut xml = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&out, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<w:adjustRightInd w:val="0""#), "explicit false was lost: {xml}");
|
||||
assert!(xml.contains("<w:adjustRightInd/>"), "explicit true was lost: {xml}");
|
||||
let tags = ["<w:adjustRightInd", "<w:snapToGrid", "<w:spacing", "<w:ind"];
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! multi-paragraph results, and custom document properties round-trip. All markup here is
|
||||
//! synthetic, written from ECMA-376.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::para::OBJ;
|
||||
use wordcraft_doc::{CharProps, Document, InlineObject, Path, StoryRef};
|
||||
use wordcraft_docx::{read, write};
|
||||
@@ -16,12 +14,11 @@ const CITE_B: &str = r#""citationItems":[{"id":7,"uris":["http://zotero.org/user
|
||||
const BIBL: &str = r#" ADDIN ZOTERO_BIBL {"uncited":[],"omitted":[],"custom":[]} CSL_BIBLIOGRAPHY "#;
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn fld(kind: &str) -> String {
|
||||
@@ -129,10 +126,9 @@ fn count(xml: &str, kind: &str) -> usize {
|
||||
}
|
||||
|
||||
fn document_xml(docx: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
|
||||
let mut s = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -174,9 +170,8 @@ fn custom_property_bookmark_links_survive_saves_and_value_edits() {
|
||||
let before = d.custom_props.clone();
|
||||
for _ in 0..2 {
|
||||
let out = write(&d).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&out)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("docProps/custom.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&out, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("docProps/custom.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"linkTarget="OriginalBookmark""#), "{xml}");
|
||||
assert!(xml.contains(r#"linkTarget="RawBookmark""#), "{xml}");
|
||||
assert_eq!(xml.matches("linkTarget=").count(), 2, "unlinked properties stay unlinked");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Independent complex-script formatting through style inheritance and DOCX round trips.
|
||||
//! Synthetic packages, written here; no external documents or installed fonts are needed.
|
||||
|
||||
use std::io::Write;
|
||||
use wordcraft_doc::props::CharProps;
|
||||
use wordcraft_doc::{Document, Paragraph, Run};
|
||||
|
||||
@@ -13,17 +12,16 @@ fn package(run: &str, defaults: &str, styles: &str, paragraph_style: &str) -> Ve
|
||||
r#"<w:document {ns}><w:body><w:p><w:pPr>{paragraph_style}<w:bidi/></w:pPr><w:r><w:rPr>{run}</w:rPr><w:t>שָׁלוֹם ABC 123</w:t></w:r></w:p></w:body></w:document>"#
|
||||
);
|
||||
let styles = format!("<w:styles {ns}><w:docDefaults><w:rPrDefault><w:rPr>{defaults}</w:rPr></w:rPrDefault></w:docDefaults>{styles}</w:styles>");
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [
|
||||
("_rels/.rels", root),
|
||||
("word/_rels/document.xml.rels", rels),
|
||||
("word/document.xml", document.as_str()),
|
||||
("word/styles.xml", styles.as_str()),
|
||||
] {
|
||||
z.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn paragraph(d: &Document) -> &Paragraph {
|
||||
@@ -180,9 +178,14 @@ fn split_runs_add_no_phantom_tracked_formatting() {
|
||||
let run = r#"<w:sz w:val="24"/><w:rPrChange w:id="7" w:author="Synthetic Author" w:date="2026-10-11T00:00:00Z"><w:rPr><w:sz w:val="20"/></w:rPr></w:rPrChange>"#;
|
||||
let d = wordcraft_docx::read(&package(run, "", "", "")).unwrap();
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap().by_name("word/document.xml").unwrap(), &mut xml)
|
||||
.unwrap();
|
||||
let xml = String::from_utf8(
|
||||
craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 })
|
||||
.unwrap()
|
||||
.read("word/document.xml", 16 << 20)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let runs: Vec<&str> = xml.split("<w:r>").skip(1).map(|r| r.split("</w:r>").next().unwrap()).collect();
|
||||
assert!(runs.len() >= 2, "the mixed run is split: {xml}");
|
||||
assert!(runs.iter().any(|r| r.contains("<w:rtl/>")), "{xml}");
|
||||
|
||||
@@ -136,18 +136,14 @@ fn the_controls_are_written_where_they_were() {
|
||||
|
||||
/// The names of the parts of a package, in the order they are in it.
|
||||
fn part_names(bytes: &[u8]) -> Vec<String> {
|
||||
let z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("zip");
|
||||
z.file_names().map(str::to_string).collect()
|
||||
let z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).expect("zip");
|
||||
z.names().map(|n| n.into_owned()).collect()
|
||||
}
|
||||
|
||||
/// The body of the document part of a package.
|
||||
fn document_body(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("zip");
|
||||
let mut xml = String::new();
|
||||
{
|
||||
use std::io::Read;
|
||||
z.by_name("word/document.xml").expect("the document part").read_to_string(&mut xml).expect("read");
|
||||
}
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).expect("zip");
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).expect("read").expect("the document part")).expect("utf8");
|
||||
let rest = xml.split_once("<w:body>").expect("the body").1;
|
||||
let body = rest.split_once("</w:body>").expect("the end of the body").0;
|
||||
format!("<w:body>{body}</w:body>")
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! All markup here is synthetic, written from ECMA-376 §17.5.2 and the [MS-DOCX] `w14`/`w15`
|
||||
//! extensions.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::control::{ControlKind, ControlLock, ListItem};
|
||||
use wordcraft_doc::{Block, ContentControl, Document, StoryRef};
|
||||
use wordcraft_docx::{read, write};
|
||||
@@ -13,12 +11,11 @@ const ROOT_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.o
|
||||
|
||||
fn package(body: &str) -> Vec<u8> {
|
||||
let doc = format!(r#"<w:document {NS}><w:body>{body}</w:body></w:document>"#);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", ROOT_RELS.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn t(s: &str) -> String {
|
||||
@@ -90,10 +87,9 @@ fn form() -> Vec<u8> {
|
||||
}
|
||||
|
||||
fn document_xml(docx: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
|
||||
let mut s = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn controls(d: &Document) -> Vec<ContentControl> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Original synthetic custom XML stores, written from the public OOXML package contract.
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use wordcraft_doc::{Path, StoryRef};
|
||||
|
||||
const REL_NS: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
|
||||
@@ -36,22 +35,19 @@ fn fixture() -> BTreeMap<String, Vec<u8>> {
|
||||
}
|
||||
|
||||
fn zip(parts: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
|
||||
let mut z = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, bytes) in parts {
|
||||
z.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
z.write_all(bytes).unwrap();
|
||||
z.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn unzip(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
|
||||
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut parts = BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let name = f.name().to_string();
|
||||
let mut bytes = Vec::new();
|
||||
f.read_to_end(&mut bytes).unwrap();
|
||||
for name in names {
|
||||
let bytes = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
assert!(parts.insert(name, bytes).is_none(), "duplicate ZIP entry");
|
||||
}
|
||||
parts
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! The document default font size (`w:docDefaults/w:rPrDefault/w:rPr/w:sz`) across a save and
|
||||
//! reopen. An unset default is resolved as `DEFAULT_FONT_SIZE`; the writer must say so, because
|
||||
//! the reader reads an omitted size as 10 pt.
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
use wordcraft_doc::Document;
|
||||
use wordcraft_doc::props::{CharProps, DEFAULT_FONT_SIZE};
|
||||
@@ -9,10 +8,9 @@ fn resolved(d: &Document) -> f32 {
|
||||
d.styles.resolve_char(None, &CharProps::default()).size
|
||||
}
|
||||
fn styles_xml(bytes: &[u8]) -> String {
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
archive.by_name("word/styles.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
let mut archive = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(archive.read("word/styles.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
fn defaults(xml: &str) -> &str {
|
||||
xml.split("<w:docDefaults>").nth(1).unwrap().split("</w:docDefaults>").next().unwrap()
|
||||
@@ -47,12 +45,11 @@ fn a_file_that_omits_the_default_size_is_still_read_as_ten_points() {
|
||||
let doc = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>x</w:t></w:r></w:p></w:body></w:document>"#;
|
||||
let styles = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults></w:styles>"#;
|
||||
let rels = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, body) in [("_rels/.rels", root), ("word/document.xml", doc), ("word/_rels/document.xml.rels", rels), ("word/styles.xml", styles)] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(body.as_bytes()).unwrap();
|
||||
zw.add(name, body.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let d = wordcraft_docx::read(&zw.finish().unwrap().into_inner()).unwrap();
|
||||
let d = wordcraft_docx::read(&zw.finish().unwrap()).unwrap();
|
||||
assert_eq!(d.styles.default_chr.size, Some(10.0));
|
||||
}
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Original nested-picture controls. No native output or external assets are embedded.
|
||||
use std::{io::Write, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use wordcraft_doc::para::{Anchor, CanvasStyle, Float, GroupChild, ShapeKind, Wrap};
|
||||
use wordcraft_doc::props::Rgb;
|
||||
use wordcraft_doc::{Document, InlineObject, Paragraph, PartKind, Pos, StoryRef, TextBody, para_block};
|
||||
@@ -71,7 +71,7 @@ fn check_box(doc: &Document) {
|
||||
assert_eq!(doc.media[media].as_slice(), BLUE_PNG);
|
||||
}
|
||||
fn package(xml: &str) -> Vec<u8> {
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
let rels = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let docrels = br#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdIsolationRect" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/isolation-blue.png"/></Relationships>"#;
|
||||
let types = br#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="png" ContentType="image/png"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
|
||||
@@ -82,10 +82,9 @@ fn package(xml: &str) -> Vec<u8> {
|
||||
("word/_rels/document.xml.rels", docrels.as_slice()),
|
||||
("word/media/isolation-blue.png", BLUE_PNG),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(bytes).unwrap();
|
||||
zip.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn model_nested_picture_roundtrip() {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! East Asian language attribute of a run (`w:lang/@w:eastAsia`, ISO/IEC 29500-1 §17.3.2.20).
|
||||
//! Verifies that saving DOCX documents preserves the East Asian language of `w:lang`.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn docx(body: &str, styles: Option<&str>) -> Vec<u8> {
|
||||
let root = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let doc_rels = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
@@ -17,19 +15,17 @@ fn docx(body: &str, styles: Option<&str>) -> Vec<u8> {
|
||||
format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">{s}</w:styles>"#),
|
||||
));
|
||||
}
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, xml) in &parts {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(xml.as_bytes()).unwrap();
|
||||
zw.add(name, xml.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn part(bytes: &[u8], name: &str) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name(name).unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn run(text: &str, rpr: &str) -> String {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! two lines in one and horizontal in vertical are read and saved again. Fixtures are
|
||||
//! hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::{CombineBrackets, EastAsianLayout};
|
||||
|
||||
fn docx(body: &str, styles: Option<&str>) -> Vec<u8> {
|
||||
@@ -20,19 +18,17 @@ fn docx(body: &str, styles: Option<&str>) -> Vec<u8> {
|
||||
format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">{s}</w:styles>"#),
|
||||
));
|
||||
}
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, xml) in &parts {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(xml.as_bytes()).unwrap();
|
||||
zw.add(name, xml.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn part(bytes: &[u8], name: &str) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name(name).unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn run(text: &str, rpr: &str) -> String {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Emphasis marks (`w:em`, ISO/IEC 29500-1 §17.3.2.12: Font › Emphasis mark) are read and
|
||||
//! saved again, in their place in `w:rPr`. Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::EmphasisMark;
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
@@ -10,12 +8,11 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, xml) in [("_rels/.rels", root.to_string()), ("word/document.xml", doc)] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(xml.as_bytes()).unwrap();
|
||||
zw.add(name, xml.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn marks(d: &wordcraft_doc::Document) -> Vec<Option<EmphasisMark>> {
|
||||
@@ -39,9 +36,8 @@ fn emphasis_marks_are_read_and_saved() {
|
||||
let want = [EmphasisMark::Dot, EmphasisMark::Comma, EmphasisMark::Circle, EmphasisMark::UnderDot, EmphasisMark::None].map(Some);
|
||||
assert_eq!(marks(&d), want);
|
||||
let saved = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&saved)).unwrap();
|
||||
let mut xml = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&saved, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
// In its place: after w:cs, before w:lang.
|
||||
assert!(xml.contains(r#"<w:cs/><w:em w:val="underDot"/><w:lang"#), "{xml}");
|
||||
assert_eq!(marks(&wordcraft_docx::read(&saved).unwrap()), want);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Hand-written minimal WordprocessingML fixtures (written for these tests from the spec).
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use wordcraft_doc::fill::Fill;
|
||||
@@ -13,12 +12,11 @@ const W_NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingm
|
||||
const ROOT_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
|
||||
pub fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn rels(list: &[(&str, &str, &str)]) -> String {
|
||||
@@ -1483,10 +1481,9 @@ fn word_watermark_survives_save() {
|
||||
};
|
||||
assert_eq!(headers(&d2), headers(&d));
|
||||
assert_eq!(headers(&d2)[0], vec!["Header label".to_string(), "Second line".to_string()]);
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
for name in ["word/header1.xml", "word/header2.xml"] {
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name(name).unwrap(), &mut xml).unwrap();
|
||||
let xml = String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(xml.matches("<v:shape ").count(), 1, "{name}");
|
||||
}
|
||||
|
||||
@@ -1662,14 +1659,12 @@ fn chart_docx() -> Vec<u8> {
|
||||
|
||||
/// The entries of a zip, by name.
|
||||
fn unzip(bytes: &[u8]) -> std::collections::BTreeMap<String, Vec<u8>> {
|
||||
use std::io::Read;
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = std::collections::BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let mut b = Vec::new();
|
||||
f.read_to_end(&mut b).unwrap();
|
||||
out.insert(f.name().to_string(), b);
|
||||
for name in names {
|
||||
let b = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
out.insert(name, b);
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -2174,9 +2169,8 @@ fn formatting_revisions_round_trip() {
|
||||
|
||||
// Written back: each change is the last child of its parent, and reads back the same.
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
for (tag, parent) in [
|
||||
("w:rPrChange", "</w:rPr></w:rPrChange></w:rPr>"),
|
||||
("w:pPrChange", "</w:pPr></w:pPrChange></w:pPr>"),
|
||||
@@ -2432,10 +2426,9 @@ fn note_separators_and_continuation_notice_round_trip() {
|
||||
|
||||
let out = wordcraft_docx::write(&d).unwrap();
|
||||
let part = |name: &str| {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&out)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name(name).unwrap(), &mut xml).unwrap();
|
||||
xml
|
||||
let mut z = craft_zip::Archive::open(&out, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap()
|
||||
};
|
||||
let notes = part("word/footnotes.xml");
|
||||
for (kind, id) in [("separator", "-1"), ("continuationSeparator", "0"), ("continuationNotice", "1")] {
|
||||
@@ -2558,13 +2551,12 @@ fn includepicture_keeps_its_embedded_picture() {
|
||||
let bytes = docx(&format!("<w:p>{field}</w:p><w:sectPr/>"), &[("rIdPic", "image", "media/picture.png")], &[]);
|
||||
let bytes = {
|
||||
// `docx` takes text parts; add the picture bytes as a binary part.
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut entries: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let mut v = Vec::new();
|
||||
std::io::Read::read_to_end(&mut f, &mut v).unwrap();
|
||||
entries.push((f.name().to_string(), v));
|
||||
for name in names {
|
||||
let v = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
entries.push((name, v));
|
||||
}
|
||||
entries.push(("word/media/picture.png".into(), png.to_vec()));
|
||||
zip(&entries.iter().map(|(n, v)| (n.as_str(), v.as_slice())).collect::<Vec<_>>())
|
||||
@@ -2678,9 +2670,8 @@ fn rounded_rectangle_corners_are_read_and_saved() {
|
||||
let d = read_body(&format!("<w:p>{body}</w:p>"));
|
||||
assert_eq!(corners(&d), expected);
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut xml = String::new();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes.as_slice())).unwrap();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<a:prstGeom prst="roundRect"><a:avLst><a:gd name="adj" fmla="val 3565"/></a:avLst></a:prstGeom>"#), "{xml}");
|
||||
assert_eq!(corners(&wordcraft_docx::read(&bytes).unwrap()), expected);
|
||||
}
|
||||
@@ -2745,9 +2736,8 @@ fn arrowheads_keep_their_kind_and_size() {
|
||||
let d = read_body(&format!("<w:p>{body}</w:p>"));
|
||||
assert_eq!(ends(&d), expected);
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut xml = String::new();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes.as_slice())).unwrap();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<a:headEnd type="stealth" w="sm" len="lg"/>"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:tailEnd type="diamond" w="lg" len="sm"/>"#), "{xml}");
|
||||
assert_eq!(ends(&wordcraft_docx::read(&bytes).unwrap()), expected);
|
||||
@@ -2870,9 +2860,8 @@ fn vml_preset_shape_survives_saving() {
|
||||
r##"<w:p><w:r><w:pict>{VML_RIGHT_ARROW_TYPE}<v:shape type="#_x0000_t13" style="width:120pt;height:40pt" fillcolor="#ffa500"/></w:pict></w:r></w:p>"##
|
||||
);
|
||||
let saved = wordcraft_docx::write(&read_body(&body)).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(saved.as_slice())).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&saved, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"prst="rightArrow""#), "{xml}");
|
||||
let again = wordcraft_docx::read(&saved).unwrap();
|
||||
assert!(
|
||||
@@ -2898,9 +2887,8 @@ fn vml_preset_shape_survives_saving() {
|
||||
fn vml_nearest_preset_shape_survives_saving() {
|
||||
let body = r##"<w:p><w:r><w:pict><v:shape type="#_x0000_t66" style="width:60pt;height:20pt" fillcolor="#ffa500"/></w:pict></w:r></w:p>"##;
|
||||
let saved = wordcraft_docx::write(&read_body(body)).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(saved.as_slice())).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&saved, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"prst="leftArrow""#), "{xml}");
|
||||
let again = wordcraft_docx::read(&saved).unwrap();
|
||||
assert!(
|
||||
@@ -3047,9 +3035,8 @@ fn drawingml_shapes_survive_saving() {
|
||||
let again = wordcraft_docx::read(&bytes).unwrap();
|
||||
assert_eq!(shapes(&again), read, "every shape survives saving");
|
||||
assert_eq!(part_text_box(&again), "Floating text box");
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes[..])).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes[..], craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(xml.matches("<wps:wsp>").count(), 7);
|
||||
for p in presets.iter().chain(&["straightConnector1"]) {
|
||||
assert!(xml.contains(&format!(r#"<a:prstGeom prst="{p}">"#)), "{p} is written");
|
||||
@@ -4044,20 +4031,19 @@ fn hyperlink_with_empty_target_is_no_link() {
|
||||
let bytes = docx(body, &[("rId2", "hyperlink", "https://x"), ("rId3", "hyperlink", "mailto:a@example.com")], &[]);
|
||||
// Blank the first target and mark the mailto one external (the helper only does that for http).
|
||||
let bytes = {
|
||||
let mut zr = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut zr = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = zr.names().map(|n| n.into_owned()).collect();
|
||||
let mut entries = Vec::new();
|
||||
for i in 0..zr.len() {
|
||||
let mut f = zr.by_index(i).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
std::io::Read::read_to_end(&mut f, &mut buf).unwrap();
|
||||
if f.name() == "word/_rels/document.xml.rels" {
|
||||
for name in names {
|
||||
let mut buf = zr.read(&name, 16 << 20).unwrap().unwrap();
|
||||
if name == "word/_rels/document.xml.rels" {
|
||||
buf = String::from_utf8(buf)
|
||||
.unwrap()
|
||||
.replace("Target=\"https://x\"", "Target=\"\"")
|
||||
.replace("a@example.com\"", "a@example.com\" TargetMode=\"External\"")
|
||||
.into_bytes();
|
||||
}
|
||||
entries.push((f.name().to_string(), buf));
|
||||
entries.push((name, buf));
|
||||
}
|
||||
let refs: Vec<(&str, &[u8])> = entries.iter().map(|(n, b)| (n.as_str(), b.as_slice())).collect();
|
||||
zip(&refs)
|
||||
@@ -4575,7 +4561,7 @@ fn shape_gradients_and_transparency_are_read_and_survive_saving() {
|
||||
rot_with_shape: false,
|
||||
flip: GradientFlip::Xy,
|
||||
}))),
|
||||
Some(grad(vec![stop(0.0, ph, 0.0), stop(100.0, Rgb(0x20, 0x40, 0x60), 0.0)], lin(90.0, false), true)),
|
||||
Some(grad(vec![stop(0.0, ph, 0.0), stop(100.0, Rgb(44, 92, 140), 0.0)], lin(90.0, false), true)),
|
||||
Some(grad(vec![stop(0.0, ph, 0.0), stop(100.0, black, 0.0)], centre, true)),
|
||||
None,
|
||||
];
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
//! edited. Hand-written fixtures (from the spec), no files produced by Word.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use wordcraft_doc::{Block, BuildingBlock, CharProps, Document, Paragraph, para_block};
|
||||
@@ -15,22 +14,20 @@ const GLOSSARY_REL: &str = "http://schemas.openxmlformats.org/officeDocument/200
|
||||
const GLOSSARY_CT: &str = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml";
|
||||
|
||||
fn zip(entries: &[(&str, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn unzip(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let mut b = Vec::new();
|
||||
f.read_to_end(&mut b).unwrap();
|
||||
assert!(out.insert(f.name().to_string(), b).is_none(), "duplicate part {}", f.name());
|
||||
for name in names {
|
||||
let b = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
assert!(out.insert(name.clone(), b).is_none(), "duplicate part {name}");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//! `styles.xml` doesn't write `w:outlineLvl` (some producers leave it out). Word for Windows,
|
||||
//! opening such a file, reports `OutlineLevel` 1 and 2 for "heading 1" and "heading 2" (whatever
|
||||
//! their style ids) and body text for a custom style.
|
||||
use std::io::Write;
|
||||
use wordcraft_doc::Paragraph;
|
||||
|
||||
const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
@@ -12,14 +11,13 @@ fn package(styles: &str, body: &str) -> Vec<u8> {
|
||||
let rels = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
let doc = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="{W}"><w:body>{body}</w:body></w:document>"#);
|
||||
let styles = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="{W}">{styles}</w:styles>"#);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, body) in
|
||||
[("_rels/.rels", root), ("word/document.xml", doc.as_str()), ("word/_rels/document.xml.rels", rels), ("word/styles.xml", styles.as_str())]
|
||||
{
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(body.as_bytes()).unwrap();
|
||||
zw.add(name, body.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn para(style: &str, text: &str) -> String {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! (`w:gridBefore`/`w:wBefore`, `w:gridAfter`/`w:wAfter`) through a round trip (#1174). All
|
||||
//! markup here is synthetic, written from ECMA-376 §17.16.17 and §17.4.14-17.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::Block;
|
||||
use wordcraft_docx::{read, write};
|
||||
|
||||
@@ -12,19 +10,17 @@ const ROOT_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.o
|
||||
|
||||
fn package(body: &str) -> Vec<u8> {
|
||||
let doc = format!(r#"<w:document {NS}><w:body>{body}</w:body></w:document>"#);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", ROOT_RELS.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(docx: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
|
||||
let mut s = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn resaved(body: &str) -> String {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! OpenType ligatures (`w14:ligatures`, [MS-DOCX]) in document defaults, styles and runs.
|
||||
//! Synthetic packages, written here; no external documents are needed.
|
||||
|
||||
use std::io::Write;
|
||||
use wordcraft_doc::props::Ligatures;
|
||||
use wordcraft_doc::{Document, Paragraph};
|
||||
|
||||
@@ -15,17 +14,16 @@ fn package(defaults: &str, style: &str, run: &str) -> Vec<u8> {
|
||||
let styles = format!(
|
||||
r#"<w:styles {ns}><w:docDefaults><w:rPrDefault><w:rPr>{defaults}</w:rPr></w:rPrDefault></w:docDefaults><w:style w:type="character" w:styleId="Fancy"><w:name w:val="Fancy"/><w:rPr>{style}</w:rPr></w:style></w:styles>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [
|
||||
("_rels/.rels", root),
|
||||
("word/_rels/document.xml.rels", rels),
|
||||
("word/document.xml", document.as_str()),
|
||||
("word/styles.xml", styles.as_str()),
|
||||
] {
|
||||
z.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn paragraph(d: &Document) -> &Paragraph {
|
||||
@@ -68,10 +66,9 @@ fn unknown_ligature_values_are_ignored() {
|
||||
fn no_ligatures_element_without_a_value() {
|
||||
let d = wordcraft_docx::read(&package("", "", "")).unwrap();
|
||||
let out = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(out)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&out, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
for name in ["word/document.xml", "word/styles.xml"] {
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name(name).unwrap(), &mut xml).unwrap();
|
||||
let xml = String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(!xml.contains("ligatures"), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ const W_NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingm
|
||||
const ROOT_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn doc_zip(document_xml: &[u8]) -> Vec<u8> {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
//! Pictures dropped at open are recorded for strict export (#6005): a missing part, an external
|
||||
//! target and a link-only picture (ECMA-376 Part 1 §20.1.8.13 `a:blip` `r:embed`/`r:link`).
|
||||
//! Original synthetic packages; nothing is fetched, the reader has no network code.
|
||||
use std::io::Write;
|
||||
use wordcraft_doc::{Document, InlineObject, MissingPictureReason};
|
||||
|
||||
const NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:v="urn:schemas-microsoft-com:vml""#;
|
||||
const REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
|
||||
fn png() -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
image::RgbaImage::from_pixel(2, 2, image::Rgba([20, 90, 200, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(2, 2, image::Rgba([20, 90, 200, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn blip(attrs: &str) -> String {
|
||||
@@ -32,10 +31,9 @@ fn package(body: &str, doc_rels: &str, header: Option<(&str, &str)>, extra: &[(&
|
||||
);
|
||||
let drels = format!(r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{doc_rels}{hdr_rel}</Relationships>"#);
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="png" ContentType="image/png"/><Default Extension="bin" ContentType="application/octet-stream"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
let mut put = |name: &str, bytes: &[u8]| {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(bytes).unwrap();
|
||||
zip.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
};
|
||||
put("[Content_Types].xml", types.as_bytes());
|
||||
put("_rels/.rels", rels.as_bytes());
|
||||
@@ -51,7 +49,7 @@ fn package(body: &str, doc_rels: &str, header: Option<(&str, &str)>, extra: &[(&
|
||||
for (n, b) in extra {
|
||||
put(n, b);
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
fn image_rel(id: &str, target: &str, external: bool) -> String {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! saved, even when it holds a single paragraph (ECMA-376 §17.5.2.29-30; [MS-DOCX] `w15`
|
||||
//! repeating sections, which must wrap whole paragraphs or rows). All markup here is synthetic.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::{ContentControl, Document, StoryRef};
|
||||
use wordcraft_docx::{read, write};
|
||||
|
||||
@@ -13,19 +11,17 @@ const ROOT_RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.o
|
||||
|
||||
fn package(body: &str) -> Vec<u8> {
|
||||
let doc = format!(r#"<w:document {NS}><w:body>{body}</w:body></w:document>"#);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", ROOT_RELS.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(docx: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
|
||||
let mut s = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn t(s: &str) -> String {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
//! and extended properties. Hand-written fixtures (from the spec), no files produced by Word.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
@@ -12,22 +11,20 @@ const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relati
|
||||
const REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
|
||||
fn zip(entries: &[(&str, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn unzip(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let mut b = Vec::new();
|
||||
f.read_to_end(&mut b).unwrap();
|
||||
assert!(out.insert(f.name().to_string(), b).is_none(), "duplicate part {}", f.name());
|
||||
for name in names {
|
||||
let b = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
assert!(out.insert(name.clone(), b).is_none(), "duplicate part {name}");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
//! A picture's hyperlink (#5267): Word keeps it on the drawing (`wp:docPr/a:hlinkClick`,
|
||||
//! ECMA-376 Part 1 §20.4.2.5) rather than in a `w:hyperlink` around the run. Original
|
||||
//! synthetic packages; the 2×2 picture is generated in code.
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
use wordcraft_doc::{CharProps, Document, InlineObject, Pos};
|
||||
|
||||
fn png() -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
image::RgbaImage::from_pixel(2, 2, image::Rgba([20, 90, 200, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(2, 2, image::Rgba([20, 90, 200, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// An inline picture's run; `link` goes into its `wp:docPr`.
|
||||
@@ -28,7 +27,7 @@ fn package(body: &str) -> Vec<u8> {
|
||||
let doc_rels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdPic" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/square.png"/><Relationship Id="rIdWeb" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com/picture" TargetMode="External"/><Relationship Id="rIdRun" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com/run" TargetMode="External"/></Relationships>"#;
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="png" ContentType="image/png"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
|
||||
let png = png();
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, bytes) in [
|
||||
("[Content_Types].xml", types.as_bytes()),
|
||||
("_rels/.rels", rels.as_bytes()),
|
||||
@@ -36,10 +35,9 @@ fn package(body: &str) -> Vec<u8> {
|
||||
("word/_rels/document.xml.rels", doc_rels.as_bytes()),
|
||||
("word/media/square.png", png.as_slice()),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(bytes).unwrap();
|
||||
zip.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
/// The link on each picture of the first paragraph, in order.
|
||||
@@ -53,10 +51,9 @@ fn picture_links(doc: &Document) -> Vec<Option<String>> {
|
||||
}
|
||||
|
||||
fn part(bytes: &[u8], name: &str) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name(name).unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -109,19 +106,16 @@ fn a_linked_picture_is_saved_with_its_link_on_the_drawing_too() {
|
||||
// The drawing's link alone (as Word writes it) reads back the same.
|
||||
let without_run_link = xml.replace(&format!(r#"<w:hyperlink r:id="{rid}" w:history="1">"#), "").replacen("</w:hyperlink>", "", 1);
|
||||
assert_ne!(without_run_link, xml);
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes.as_slice())).unwrap();
|
||||
let mut out = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let name = f.name().to_string();
|
||||
let mut data = Vec::new();
|
||||
f.read_to_end(&mut data).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = craft_zip::Writer::new();
|
||||
for name in names {
|
||||
let mut data = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
if name == "word/document.xml" {
|
||||
data = without_run_link.clone().into_bytes();
|
||||
}
|
||||
out.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
out.write_all(&data).unwrap();
|
||||
out.add(&name, &data, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let edited = out.finish().unwrap().into_inner();
|
||||
let edited = out.finish().unwrap();
|
||||
assert_eq!(picture_links(&wordcraft_docx::read(&edited).unwrap())[0], Some("https://example.com/saved".to_string()));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Positional tabs (`w:ptab`, ECMA-376 Part 1 §17.3.3.23). Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::InlineObject;
|
||||
use wordcraft_doc::para::{PTabAlign, PTabBase};
|
||||
use wordcraft_doc::props::TabLeader;
|
||||
@@ -11,19 +9,17 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn ptab(relative_to: &str, alignment: &str, leader: &str) -> String {
|
||||
|
||||
@@ -2,28 +2,27 @@
|
||||
//! (ECMA-376 Part 1 §17.15.1.69–70) are kept on save and honoured by the writer. Synthetic
|
||||
//! fixtures written for these tests from the spec; the names are made up.
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
const W_NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships""#;
|
||||
const RT: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
|
||||
|
||||
fn zip(entries: &[(&str, String)]) -> Vec<u8> {
|
||||
let mut z = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, text) in entries {
|
||||
z.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
z.write_all(text.as_bytes()).unwrap();
|
||||
z.add(name, text.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn unzip(bytes: &[u8]) -> BTreeMap<String, String> {
|
||||
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut parts = BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let mut s = String::new();
|
||||
if f.read_to_string(&mut s).is_ok() {
|
||||
parts.insert(f.name().to_string(), s);
|
||||
for name in names {
|
||||
if let Ok(Some(bytes)) = z.read(&name, 16 << 20)
|
||||
&& let Ok(s) = String::from_utf8(bytes)
|
||||
{
|
||||
parts.insert(name, s);
|
||||
}
|
||||
}
|
||||
parts
|
||||
|
||||
@@ -63,7 +63,11 @@ fn para_runs(runs: &[(&str, CharProps)]) -> Paragraph {
|
||||
fn tiny_png() -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_fn(3, 2, |x, y| image::Rgba([x as u8 * 80, y as u8 * 120, 200, 255]));
|
||||
let mut out = std::io::Cursor::new(Vec::new());
|
||||
img.write_to(&mut out, image::ImageFormat::Png).unwrap();
|
||||
std::io::Write::write_all(
|
||||
&mut out,
|
||||
&wordcraft_geom::picture::encode_format(&image::DynamicImage::from(img.clone()), image::ImageFormat::Png).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
out.into_inner()
|
||||
}
|
||||
|
||||
@@ -324,9 +328,8 @@ fn tracked_style_definition_change_round_trips() {
|
||||
});
|
||||
d.body = vec![para_block(Paragraph::with_text("x", CharProps::default()))];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/styles.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/styles.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains("<w:pPrChange") && xml.contains("<w:rPrChange"), "{xml}");
|
||||
assert!(!xml.contains("stylePrChange"), "CT_Style has no such element: {xml}");
|
||||
drop(zip);
|
||||
@@ -748,9 +751,8 @@ fn note_options_round_trip_per_section() {
|
||||
assert!(sections[1].1.footnote_pr.is_empty() && sections[1].1.endnote_pr.is_empty());
|
||||
assert_eq!(r.note_options(sections[0].1, false).num_start, 7);
|
||||
assert_eq!(r.note_options(sections[1].1, false).num_start, 4);
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let at = |s: &str| xml.find(s).unwrap_or_else(|| panic!("{s} missing"));
|
||||
assert!(
|
||||
at("<w:footnotePr><w:numFmt w:val=\"lowerRoman\"/><w:numStart w:val=\"7\"/><w:numRestart w:val=\"eachSect\"/></w:footnotePr>")
|
||||
@@ -773,9 +775,8 @@ fn note_never_references_itself() {
|
||||
d.body = vec![para_block(p)];
|
||||
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/footnotes.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/footnotes.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(!xml.contains("<w:footnoteReference"), "a footnote cites itself: {xml}");
|
||||
assert_eq!(xml.matches("<w:footnoteRef/>").count(), 1, "{xml}");
|
||||
|
||||
@@ -840,13 +841,12 @@ fn word_note_mark_round_trips_once() {
|
||||
let mut doc = d;
|
||||
for pass in 0..2 {
|
||||
let bytes = wordcraft_docx::write(&doc).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
for (file, mark, reference, style) in [
|
||||
("word/footnotes.xml", "<w:footnoteRef/>", "<w:footnoteReference", "FootnoteReference"),
|
||||
("word/endnotes.xml", "<w:endnoteRef/>", "<w:endnoteReference", "EndnoteReference"),
|
||||
] {
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name(file).unwrap(), &mut xml).unwrap();
|
||||
let xml = String::from_utf8(zip.read(file, 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(xml.matches(mark).count(), 1, "pass {pass}: {xml}");
|
||||
assert!(!xml.contains(reference), "pass {pass}: a note cites itself: {xml}");
|
||||
assert!(xml.contains(&format!(r#"<w:rStyle w:val="{style}"/></w:rPr>{mark}"#)), "pass {pass}: {xml}");
|
||||
@@ -894,9 +894,8 @@ fn toc_field_wraps_its_entries() {
|
||||
let d = doc_with(blocks);
|
||||
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let sdt = xml.find("<w:sdt>").expect("TOC content control");
|
||||
assert!(xml.contains(r#"<w:docPartGallery w:val="Table of Contents"/>"#), "{xml}");
|
||||
let (begin, fend) = (xml.find(r#"fldCharType="begin""#).unwrap(), xml.find(r#"fldCharType="end""#).unwrap());
|
||||
@@ -933,9 +932,8 @@ fn cell_text_direction_round_trips() {
|
||||
let mut d = Document::new();
|
||||
d.body = vec![Arc::new(Block::Table(t)), para_block(Paragraph::new())];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<w:textDirection w:val="tbRl"/>"#), "{xml}");
|
||||
assert!(xml.contains(r#"<w:textDirection w:val="btLr"/>"#), "{xml}");
|
||||
assert_eq!(xml.matches("w:textDirection").count(), 2, "horizontal cells write nothing: {xml}");
|
||||
@@ -988,9 +986,8 @@ fn toc_field_skips_nested_stories() {
|
||||
d.body = vec![para_block(head), para_block(last), para_block(Paragraph::with_text("Body", CharProps::default()))];
|
||||
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let boxes: Vec<(usize, usize)> = xml.match_indices("<w:txbxContent>").map(|(a, _)| (a, a + xml[a..].find("</w:txbxContent>").unwrap())).collect();
|
||||
assert_eq!(boxes.len(), 2, "{xml}");
|
||||
let in_box = |i: usize| boxes.iter().any(|(a, b)| (*a..*b).contains(&i));
|
||||
@@ -1054,9 +1051,8 @@ fn tracked_equations_round_trip() {
|
||||
p.insert_object(end, InlineObject::Equation { linear: "z=1".into(), display: false, math: Default::default() }, &plain).unwrap();
|
||||
d.body = vec![para_block(p.clone())];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<w:ins w:id="1" w:author="Alice" w:date="2026-05-01T10:00:00Z"><m:oMath>"#), "ins directly encloses inline math: {xml}");
|
||||
assert!(
|
||||
xml.contains(r#"<w:del w:id="2" w:author="Bob" w:date="2026-05-02T10:00:00Z"><m:oMathPara>"#),
|
||||
@@ -1119,9 +1115,8 @@ fn tracked_paragraph_marks_round_trip() {
|
||||
let last = Paragraph::with_text("plain", CharProps::default());
|
||||
d.body = vec![para_block(split), para_block(joined), para_block(last)];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<w:rPr><w:ins w:id="#), "{xml}");
|
||||
assert!(xml.contains(r#"<w:rPr><w:del w:id="#), "{xml}");
|
||||
let r = wordcraft_docx::read(&bytes).expect("read");
|
||||
@@ -1165,10 +1160,9 @@ fn fields_and_special_chars_round_trip() {
|
||||
|
||||
/// The `document.xml` of a written package.
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
xml
|
||||
let mut zip = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// #877: a field begin marker's `w:dirty` (the cached result is stale; Word refreshes it on
|
||||
@@ -1446,9 +1440,8 @@ fn drawing_canvas_round_trips() {
|
||||
p.insert_object(end, canvas, &CharProps::default()).unwrap();
|
||||
d.body = vec![para_block(p)];
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains("wordprocessingCanvas") && xml.contains("<wpc:wpc>") && xml.contains("<wpc:bg>") && xml.contains("<wpc:whole>"), "{xml}");
|
||||
let r = wordcraft_docx::read(&bytes).unwrap();
|
||||
let got = paras(&r);
|
||||
@@ -1494,9 +1487,8 @@ fn rotated_drawing_canvas_round_trips() {
|
||||
let end = p.len();
|
||||
p.insert_object(end, canvas(square), &CharProps::default()).unwrap();
|
||||
let bytes = wordcraft_docx::write(&doc_with(vec![p])).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(
|
||||
xml.contains(r#"<wpc:extLst><a:ext uri="urn:wordcraft:canvas-xfrm"><a:xfrm rot="1800000" flipH="1"/></a:ext></wpc:extLst></wpc:wpc>"#),
|
||||
"{xml}"
|
||||
@@ -1524,9 +1516,8 @@ fn table_contextual_spacing_compatibility_flag_round_trips() {
|
||||
assert_eq!(r.settings.compat_mode, mode);
|
||||
if allow {
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/settings.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/settings.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
// CT_Compat requires the boolean option before compatSetting.
|
||||
assert!(xml.find("<w:allowSpaceOfSameStyleInTable").unwrap() < xml.find("<w:compatSetting").unwrap());
|
||||
}
|
||||
@@ -1557,15 +1548,13 @@ fn attached_template_and_link_styles_round_trip() {
|
||||
d.settings.mirror_margins = true;
|
||||
d.settings.track_changes = true;
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/settings.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/settings.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let at = |tag: &str| xml.find(tag).unwrap_or_else(|| panic!("{tag} missing: {xml}"));
|
||||
assert!(at("<w:mirrorMargins") < at("<w:attachedTemplate"));
|
||||
assert!(at("<w:attachedTemplate") < at("<w:linkStyles"));
|
||||
assert!(at("<w:linkStyles") < at("<w:trackRevisions"));
|
||||
let mut rels = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/_rels/settings.xml.rels").unwrap(), &mut rels).unwrap();
|
||||
let rels = String::from_utf8(zip.read("word/_rels/settings.xml.rels", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(rels.contains("relationships/attachedTemplate") && rels.contains("TargetMode=\"External\""), "{rels}");
|
||||
|
||||
let r = wordcraft_docx::read(&bytes).expect("read");
|
||||
@@ -1583,10 +1572,9 @@ fn attached_template_and_link_styles_round_trip() {
|
||||
#[test]
|
||||
fn zoom_round_trips() {
|
||||
let settings = |bytes: &[u8]| {
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/settings.xml").unwrap(), &mut xml).unwrap();
|
||||
xml
|
||||
let mut zip = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(zip.read("word/settings.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
};
|
||||
let plain = wordcraft_docx::write(&Document::new()).unwrap();
|
||||
let xml = settings(&plain);
|
||||
@@ -1649,10 +1637,9 @@ fn settings_core_theme_round_trip() {
|
||||
}
|
||||
|
||||
fn part_xml(bytes: &[u8], name: &str) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name(name).unwrap(), &mut xml).unwrap();
|
||||
xml
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read(name, 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// Header parts in the document, by id.
|
||||
@@ -2047,9 +2034,8 @@ fn char_border_written_between_u_and_shd() {
|
||||
let c =
|
||||
CharProps { border: Some(Border::single(0.5)), underline: Some(Underline::Single), shading: Some(Rgb(0xFF, 0xFF, 0)), ..Default::default() };
|
||||
let bytes = wordcraft_docx::write(&doc_with(vec![para_runs(&[("x", c)])])).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let bdr = r#"<w:bdr w:val="single" w:sz="4" w:space="0" w:color="auto"/>"#;
|
||||
assert_eq!(xml.matches(bdr).count(), 1, "{xml}");
|
||||
let at = xml.find(bdr).unwrap();
|
||||
@@ -2146,9 +2132,8 @@ fn ink_and_freeforms_round_trip_as_custom_geometry() {
|
||||
}
|
||||
let d = doc_with(vec![p]);
|
||||
let bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(xml.matches("<a:custGeom>").count(), 3, "{xml}");
|
||||
assert!(xml.contains(r#"<a:path w="762000" h="254000" fill="none"><a:moveTo><a:pt x="12700" y="12700"/></a:moveTo><a:lnTo>"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:alpha val="50000"/>"#) && xml.contains(r#"cap="rnd""#), "{xml}");
|
||||
@@ -2188,9 +2173,8 @@ fn shape_effects_round_trip() {
|
||||
let mut p = Paragraph::with_text("x", CharProps::default());
|
||||
p.insert_object(1, shape, &CharProps::default()).unwrap();
|
||||
let bytes = wordcraft_docx::write(&doc_with(vec![p])).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(
|
||||
xml.contains(
|
||||
r#"<a:outerShdw blurRad="50800" dist="38100" dir="8100000" algn="tr" rotWithShape="0"><a:srgbClr val="203040"><a:alpha val="40000"/>"#
|
||||
@@ -2281,9 +2265,8 @@ fn rotation_and_flips_round_trip() {
|
||||
}
|
||||
d.body = vec![para_block(p)];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<a:xfrm rot="1800000">"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:xfrm rot="5400000" flipH="1">"#), "{xml}");
|
||||
assert!(xml.contains(r#"<a:xfrm rot="18930000" flipV="1">"#), "{xml}");
|
||||
@@ -2334,9 +2317,8 @@ fn rotated_group_and_shadow_rotation_round_trip() {
|
||||
let mut p = Paragraph::with_text("g", CharProps::default());
|
||||
p.insert_object(1, group, &CharProps::default()).unwrap();
|
||||
let bytes = wordcraft_docx::write(&doc_with(vec![p])).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<wpg:grpSpPr><a:xfrm rot="8100000" flipH="1">"#), "{xml}");
|
||||
assert!(xml.contains(r#"rotWithShape="1""#) && xml.contains(r#"rotWithShape="0""#), "{xml}");
|
||||
let r = wordcraft_docx::read(&bytes).expect("read");
|
||||
@@ -2440,9 +2422,8 @@ fn a_corporate_author_round_trips() {
|
||||
..Default::default()
|
||||
});
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes[..])).unwrap();
|
||||
let mut item_xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("customXml/item1.xml").unwrap(), &mut item_xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes[..], craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let item_xml = String::from_utf8(zip.read("customXml/item1.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(item_xml.contains("<b:Author><b:Author><b:Corporate>World Health Organization</b:Corporate></b:Author><b:Editor>"), "{item_xml}");
|
||||
assert!(item_xml.contains("<b:Corporate>Harbor Guild, Inc.</b:Corporate>"), "{item_xml}");
|
||||
assert!(!item_xml.contains("<b:Last>Rivera</b:Last>"), "the corporate author takes the person authors' place: {item_xml}");
|
||||
@@ -2452,23 +2433,20 @@ fn a_corporate_author_round_trips() {
|
||||
}
|
||||
|
||||
fn assert_bibliography_parts(bytes: &[u8]) {
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
assert!(zip.by_name("customXml/item1.xml").is_ok());
|
||||
assert!(zip.by_name("customXml/itemProps1.xml").is_ok());
|
||||
assert!(zip.by_name("customXml/_rels/item1.xml.rels").is_ok());
|
||||
let mut zip = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
assert!(zip.contains("customXml/item1.xml"));
|
||||
assert!(zip.contains("customXml/itemProps1.xml"));
|
||||
assert!(zip.contains("customXml/_rels/item1.xml.rels"));
|
||||
|
||||
let mut rels_xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("_rels/.rels").unwrap(), &mut rels_xml).unwrap();
|
||||
let rels_xml = String::from_utf8(zip.read("_rels/.rels", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(rels_xml.contains("relationships/customXml"), "{rels_xml}");
|
||||
assert!(rels_xml.contains("Target=\"customXml/item1.xml\""), "{rels_xml}");
|
||||
|
||||
let mut ct_xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("[Content_Types].xml").unwrap(), &mut ct_xml).unwrap();
|
||||
let ct_xml = String::from_utf8(zip.read("[Content_Types].xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(ct_xml.contains("PartName=\"/customXml/item1.xml\""), "{ct_xml}");
|
||||
assert!(ct_xml.contains("PartName=\"/customXml/itemProps1.xml\""), "{ct_xml}");
|
||||
|
||||
let mut item_xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("customXml/item1.xml").unwrap(), &mut item_xml).unwrap();
|
||||
let item_xml = String::from_utf8(zip.read("customXml/item1.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_bibliography_types(&item_xml);
|
||||
assert_bibliography_people(&item_xml);
|
||||
assert_bibliography_details(&item_xml);
|
||||
@@ -2596,9 +2574,8 @@ fn wrap_polygons_round_trip() {
|
||||
}
|
||||
d.body = vec![para_block(p)];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<wp:wrapPolygon edited="1"><wp:start x="-300" y="0"/><wp:lineTo x="21600" y="1200"/>"#), "{xml}");
|
||||
assert!(xml.contains(r#"<wp:lineTo x="-300" y="0"/></wp:wrapPolygon>"#), "closed: {xml}");
|
||||
let r = unstacked(wordcraft_docx::read(&bytes).expect("read"));
|
||||
@@ -2635,9 +2612,8 @@ fn enclosed_characters_round_trip_in_words_structure() {
|
||||
}
|
||||
let doc = doc_with(vec![p]);
|
||||
let bytes = wordcraft_docx::write(&doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes.clone())).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"<w:instrText xml:space="preserve"> eq \o\ac(○</w:instrText>"#), "{xml}");
|
||||
assert!(!xml.contains("separate") && !xml.contains("enlarge"), "{xml}");
|
||||
// Shrink text at 11 pt: the character at 7.5 pt (half-points 15), raised.
|
||||
@@ -2670,9 +2646,8 @@ fn ruby_phonetic_guide_round_trips_as_w_ruby() {
|
||||
p.insert_object(5, ruby.clone(), &bold).unwrap();
|
||||
let d = doc_with(vec![p]);
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
for part in [
|
||||
r#"<w:rubyAlign w:val="distributeSpace"/><w:hps w:val="11"/><w:hpsRaise w:val="20"/><w:hpsBaseText w:val="22"/><w:lid w:val="ja-JP"/>"#,
|
||||
"<w:rt><w:r>",
|
||||
@@ -2717,9 +2692,8 @@ fn wrap_text_sides_round_trip() {
|
||||
}
|
||||
d.body = vec![para_block(p)];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
for tag in [
|
||||
r#"<wp:wrapSquare wrapText="left"/>"#,
|
||||
r#"<wp:wrapTight wrapText="right">"#,
|
||||
@@ -2784,9 +2758,8 @@ fn alt_text_title_and_decorative_round_trip_for_every_object() {
|
||||
t.props.description = Some("Prices by year".into());
|
||||
d.body = vec![para_block(p), Arc::new(Block::Table(t))];
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"{C183D7F6-B498-43B3-948B-1728B52AA6E4}"><adec:decorative xmlns:adec="http://schemas.microsoft.com/office/drawing/2017/decorative" val="1"/>"#), "{xml}");
|
||||
assert!(
|
||||
xml.contains(r#"<w:tblLook "#) && xml.contains(r#"<w:tblCaption w:val="Prices"/><w:tblDescription w:val="Prices by year"/></w:tblPr>"#),
|
||||
@@ -2865,9 +2838,8 @@ fn warps_and_glued_connectors_round_trip() {
|
||||
|
||||
// Hostile: an unknown warp, a huge adjust value, a glue to a missing shape.
|
||||
let xml = wordcraft_docx::write(&r).unwrap();
|
||||
let mut pkg = zip::ZipArchive::new(std::io::Cursor::new(xml)).unwrap();
|
||||
let mut doc_xml = String::new();
|
||||
std::io::Read::read_to_string(&mut pkg.by_name("word/document.xml").unwrap(), &mut doc_xml).unwrap();
|
||||
let mut pkg = craft_zip::Archive::open(&xml, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let doc_xml = String::from_utf8(pkg.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let doc_xml = doc_xml
|
||||
.replace("prst=\"textArchUp\"", "prst=\"textNotAWarp\"")
|
||||
.replace("a:stCxn id=\"5\" idx=\"3\"", "a:stCxn id=\"404\" idx=\"99999999999\"");
|
||||
@@ -2880,29 +2852,29 @@ fn warps_and_glued_connectors_round_trip() {
|
||||
|
||||
/// `zip` with part `name` replaced by `data`.
|
||||
fn replace_part(zip_bytes: &[u8], name: &str, data: &[u8]) -> Vec<u8> {
|
||||
let mut src = zip::ZipArchive::new(std::io::Cursor::new(zip_bytes)).unwrap();
|
||||
let mut out = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for i in 0..src.len() {
|
||||
let mut f = src.by_index(i).unwrap();
|
||||
let n = f.name().to_string();
|
||||
out.start_file(n.as_str(), zip::write::SimpleFileOptions::default()).unwrap();
|
||||
let mut src = craft_zip::Archive::open(zip_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = src.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = craft_zip::Writer::new();
|
||||
for n in names {
|
||||
if n == name {
|
||||
std::io::Write::write_all(&mut out, data).unwrap();
|
||||
out.add(&n, data, craft_zip::Compression::Deflated).unwrap();
|
||||
} else {
|
||||
std::io::copy(&mut f, &mut out).unwrap();
|
||||
let bytes = src.read(&n, 16 << 20).unwrap().unwrap();
|
||||
out.add(&n, &bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
}
|
||||
out.finish().unwrap().into_inner()
|
||||
out.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A texture or picture page background (`v:background/v:fill`) survives a save (#999): the image
|
||||
/// part, its relationship and the fill attributes.
|
||||
#[test]
|
||||
fn page_background_picture_round_trips() {
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(4, 4, image::Rgba([200, 150, 100, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let png = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(4, 4, image::Rgba([200, 150, 100, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
let mut d = Document::new();
|
||||
d.settings.page_color = Some(Rgb(0xFF, 0xFF, 0xCC));
|
||||
d.media.insert("paper.png".into(), Arc::new(png.clone()));
|
||||
@@ -2932,9 +2904,8 @@ fn drop_cap_options_round_trip() {
|
||||
margin.props.drop_cap_margin = Some(true);
|
||||
let d = doc_with(vec![dropped, margin]);
|
||||
let bytes = wordcraft_docx::write(&d).expect("write");
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(xml.contains(r#"w:dropCap="drop" w:lines="2" w:hSpace="120""#), "{xml}");
|
||||
assert!(xml.contains(r#"w:dropCap="margin" w:lines="4""#), "{xml}");
|
||||
assert!(xml.contains(r#"w:ascii="Liberation Mono""#), "the letter's font on its run: {xml}");
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
//! Row-level table property exceptions (`w:tblPrEx` in `w:tr`, ECMA-376 Part 1 §17.4.61).
|
||||
//! Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
let root = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn row(ex: &str, text: &str) -> String {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Run and style properties that must survive repeated DOCX saves.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn docx(body: &str, styles: &str) -> Vec<u8> {
|
||||
let root = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let rels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
@@ -9,24 +7,22 @@ fn docx(body: &str, styles: &str) -> Vec<u8> {
|
||||
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}<w:sectPr/></w:body></w:document>"#
|
||||
);
|
||||
let styles = format!(r#"<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">{styles}</w:styles>"#);
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, xml) in [
|
||||
("_rels/.rels", root.to_string()),
|
||||
("word/document.xml", document),
|
||||
("word/_rels/document.xml.rels", rels.to_string()),
|
||||
("word/styles.xml", styles),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(xml.as_bytes()).unwrap();
|
||||
zip.add(name, xml.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
fn part(bytes: &[u8], name: &str) -> String {
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip.by_name(name).unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
let mut zip = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(zip.read(name, 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
//! Section form protection (`w:formProt` in `w:sectPr`, ECMA-376 Part 1 §17.6.6). Fixtures are
|
||||
//! hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
let root = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Section line numbering (`w:lnNumType` in `w:sectPr`, ECMA-376 Part 1 §17.6.8).
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::section::{LineNumberRestart, LineNumbering};
|
||||
|
||||
/// Builds a minimal in-memory DOCX package archive containing the given XML body.
|
||||
@@ -10,20 +8,18 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// Extracts the `word/document.xml` content string from a serialized DOCX package.
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// #6209: `<w:lnNumType>` without `w:countBy` leaves line numbering off, as Word does.
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
//! Section printer, note and chapter-number metadata survives DOCX and native JSON saves.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
let root = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let doc = format!(
|
||||
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="w15"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
//! Section page-number settings (`w:pgNumType` in `w:sectPr`, ECMA-376 Part 1 §17.6.12).
|
||||
//! Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
let root = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Section text direction (`w:textDirection` in `w:sectPr`, ECMA-376 Part 1 §17.6.20). Fixtures
|
||||
//! are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::TextDirection;
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
@@ -10,16 +8,15 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn docx_with_styles(body: &str, styles: &str) -> Vec<u8> {
|
||||
let mut bytes = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut bytes = craft_zip::Writer::new();
|
||||
let root = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let document_rels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
let doc = format!(
|
||||
@@ -31,17 +28,15 @@ fn docx_with_styles(body: &str, styles: &str) -> Vec<u8> {
|
||||
("word/document.xml", doc.as_bytes()),
|
||||
("word/styles.xml", styles.as_bytes()),
|
||||
] {
|
||||
bytes.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
bytes.write_all(value).unwrap();
|
||||
bytes.add(name, value, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
bytes.finish().unwrap().into_inner()
|
||||
bytes.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -122,8 +117,8 @@ fn paragraph_style_text_direction_is_preserved_and_direct_value_overrides_it() {
|
||||
assert_eq!(xml.matches(r#"<w:pStyle w:val="Vertical"/>"#).count(), 2, "both paragraphs keep their style: {xml}");
|
||||
assert!(xml.contains(r#"<w:pStyle w:val="Vertical"/><w:textDirection w:val="lrTb"/>"#), "direct horizontal is explicit: {xml}");
|
||||
assert_eq!(xml.matches("w:textDirection").count(), 1, "only the direct paragraph writes a direction: {xml}");
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_docx::write(&d).unwrap())).unwrap();
|
||||
let mut styles_xml = String::new();
|
||||
z.by_name("word/styles.xml").unwrap().read_to_string(&mut styles_xml).unwrap();
|
||||
let _z_bytes = wordcraft_docx::write(&d).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let styles_xml = String::from_utf8(z.read("word/styles.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(styles_xml.contains(r#"<w:textDirection w:val="tbRl"/>"#), "style value survives: {styles_xml}");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
//! Section vertical alignment (`w:vAlign` in `w:sectPr`, ECMA-376 Part 1 §17.6.23, values from
|
||||
//! ST_VerticalJc). Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::section::PageVAlign;
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
@@ -10,19 +8,17 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// #1320: "both" (Justified) reads as Justify and writes back as "both"; the others keep theirs.
|
||||
|
||||
@@ -4,29 +4,26 @@
|
||||
//! fixtures (from the spec), no files produced by Word.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
const REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
|
||||
fn zip(entries: &[(&str, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn unzip(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).unwrap();
|
||||
let mut b = Vec::new();
|
||||
f.read_to_end(&mut b).unwrap();
|
||||
assert!(out.insert(f.name().to_string(), b).is_none(), "duplicate part {}", f.name());
|
||||
for name in names {
|
||||
let b = z.read(&name, 16 << 20).unwrap().unwrap();
|
||||
assert!(out.insert(name.clone(), b).is_none(), "duplicate part {name}");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
//! ECMA-376 Part 1, 5th edition (`dml-wordprocessingDrawing.xsd`, Strict and Transitional): each
|
||||
//! drawing is written once and filled in with either set of namespaces.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::{Document, InlineObject, PartKind};
|
||||
|
||||
const STRICT_NS: &str = r#"xmlns:w="http://purl.oclc.org/ooxml/wordprocessingml/main" xmlns:r="http://purl.oclc.org/ooxml/officeDocument/relationships" xmlns:wp="http://purl.oclc.org/ooxml/drawingml/wordprocessingDrawing" xmlns:a="http://purl.oclc.org/ooxml/drawingml/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" w:conformance="strict""#;
|
||||
@@ -33,19 +31,17 @@ const TRANSITIONAL: Names =
|
||||
Names { ns: TRANSITIONAL_NS, rels: TRANSITIONAL_RELS, shape: "wps", group: "wpg", canvas: "wpc", content: "w:txbxContent" };
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn solid(rgb: &str) -> String {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
//! Word sizes a line by it when a list label is larger than the text, so a save that drops it
|
||||
//! moves every line after. Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::TextAlignment;
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
@@ -12,19 +10,17 @@ fn docx(body: &str) -> Vec<u8> {
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,26 +2,22 @@
|
||||
//! `wps:txbx/@id`, `wps:linkedTxbx`). Fixtures are hand-written from ECMA-376 Part 1 §20.4
|
||||
//! (DrawingML WordprocessingML drawing) and §21.1.2.1.1 (`bodyPr`).
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::para::ShapeKind;
|
||||
use wordcraft_doc::props::{TextVert, VAlign};
|
||||
use wordcraft_doc::{Document, InlineObject, PartKind, TextBody, para_block};
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// The text boxes' stories in body order, a group's members among them
|
||||
@@ -217,7 +213,15 @@ fn picture_in_box_keeps_its_frame() {
|
||||
let outer = text_box(&mut d, "Label", body);
|
||||
let id = outer.text_box().unwrap();
|
||||
let mut bytes = std::io::Cursor::new(Vec::new());
|
||||
image::RgbaImage::from_pixel(10, 4, image::Rgba([20, 40, 60, 255])).write_to(&mut bytes, image::ImageFormat::Png).unwrap();
|
||||
std::io::Write::write_all(
|
||||
&mut bytes,
|
||||
&wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(10, 4, image::Rgba([20, 40, 60, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let media = d.add_media(bytes.into_inner(), "png");
|
||||
let image = InlineObject::Image {
|
||||
media: media.clone(),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//! Original field-boundary fixtures from the public OOXML complex-field contract.
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::{InlineObject, Path, StoryRef};
|
||||
|
||||
fn package(body: &str) -> Vec<u8> {
|
||||
let xml = format!(r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#);
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, value) in [
|
||||
("word/document.xml", xml.as_str()),
|
||||
(
|
||||
@@ -13,10 +12,9 @@ fn package(body: &str) -> Vec<u8> {
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="main" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#,
|
||||
),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(value.as_bytes()).unwrap();
|
||||
zip.add(name, value.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
fn start(code: &str) -> String {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! A paragraph whose `w:pStyle` names a style `styles.xml` doesn't define is formatted as Normal,
|
||||
//! as in Word, not with the document defaults alone (#5077). Pandoc writes footers as
|
||||
//! `<w:pStyle w:val="Footer"/>` without defining Footer.
|
||||
use std::io::Write;
|
||||
use wordcraft_doc::props::LineSpacing;
|
||||
use wordcraft_doc::{Block, Document, PartKind};
|
||||
|
||||
@@ -20,7 +19,7 @@ fn package(normal_id: &str) -> Vec<u8> {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="{W}"><w:docDefaults><w:rPrDefault><w:rPr><w:sz w:val="24"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="200"/></w:pPr></w:pPrDefault></w:docDefaults><w:style w:type="paragraph" w:default="1" w:styleId="{normal_id}"><w:name w:val="Normal"/><w:pPr><w:spacing w:before="20" w:after="40" w:line="259" w:lineRule="auto"/></w:pPr><w:rPr><w:sz w:val="26"/></w:rPr></w:style></w:styles>"#
|
||||
);
|
||||
let rels = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/></Relationships>"#;
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, body) in [
|
||||
("_rels/.rels", root),
|
||||
("word/document.xml", doc.as_str()),
|
||||
@@ -28,10 +27,9 @@ fn package(normal_id: &str) -> Vec<u8> {
|
||||
("word/styles.xml", styles.as_str()),
|
||||
("word/footer1.xml", footer.as_str()),
|
||||
] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(body.as_bytes()).unwrap();
|
||||
zw.add(name, body.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn footer_para(d: &Document) -> &wordcraft_doc::Paragraph {
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
//! `coordsize`/`coordorigin` give them. Fixtures are hand-written from ECMA-376 Part 4
|
||||
//! (VML, §14.1 `v:group`, `coordsize`, `coordorigin`; §14.2 shapes).
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use wordcraft_doc::para::ShapeKind;
|
||||
use wordcraft_doc::{Document, InlineObject, Paragraph};
|
||||
|
||||
fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A document whose body is `body`.
|
||||
|
||||
@@ -20,7 +20,11 @@ fn part_text(doc: &Document, id: Option<u32>) -> String {
|
||||
fn tiny_png_b64() -> String {
|
||||
let img = image::RgbaImage::from_fn(2, 2, |x, y| image::Rgba([x as u8 * 100, y as u8 * 100, 50, 255]));
|
||||
let mut out = std::io::Cursor::new(Vec::new());
|
||||
img.write_to(&mut out, image::ImageFormat::Png).unwrap();
|
||||
std::io::Write::write_all(
|
||||
&mut out,
|
||||
&wordcraft_geom::picture::encode_format(&image::DynamicImage::from(img.clone()), image::ImageFormat::Png).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(out.into_inner());
|
||||
// Line breaks inside base64, as the formats write it.
|
||||
b64.as_bytes().chunks(20).map(|c| std::str::from_utf8(c).unwrap()).collect::<Vec<_>>().join("\n")
|
||||
|
||||
@@ -13,7 +13,11 @@ use wordcraft_docx::{XmlKind, read_xml, sniff_xml, write_wordml2003};
|
||||
fn tiny_png() -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_fn(3, 2, |x, y| image::Rgba([x as u8 * 80, y as u8 * 120, 200, 255]));
|
||||
let mut out = std::io::Cursor::new(Vec::new());
|
||||
img.write_to(&mut out, image::ImageFormat::Png).unwrap();
|
||||
std::io::Write::write_all(
|
||||
&mut out,
|
||||
&wordcraft_geom::picture::encode_format(&image::DynamicImage::from(img.clone()), image::ImageFormat::Png).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
out.into_inner()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ fn ragged_matrix_save_stays_small() {
|
||||
let mut doc = Document::new();
|
||||
doc.body = vec![para_block(p)];
|
||||
let bytes = wordcraft_docx::write(&doc).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let size = zip.by_name("word/document.xml").unwrap().size();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let size = zip.declared_size("word/document.xml").unwrap();
|
||||
assert!(size < 1_000_000, "document.xml is {size} bytes");
|
||||
}
|
||||
|
||||
@@ -59,5 +59,5 @@ rustix = { version = "1.1", default-features = false, features = ["std", "fs"] }
|
||||
[dev-dependencies]
|
||||
wordcraft-doc = { workspace = true, features = ["test-support"] }
|
||||
encoding_rs = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
craft-zip = { workspace = true }
|
||||
cfb = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,13 @@ use wordcraft_doc::{Path, Pos, StoryRef};
|
||||
use super::{delete_selection, find_pattern, pos_json, sel_result};
|
||||
use crate::{CmdError, CmdResult, CommandSpec, Selection, Session, p};
|
||||
|
||||
/// Browsers reserve Mod+Shift+C for their inspector. Keep the desktop binding, but
|
||||
/// advertise a web alternative through the same registry/keymap as every other command.
|
||||
/// Mod+Alt+C already inserts copyright, and Mod+Alt+Shift+V is Keep Text Only.
|
||||
fn copy_format_shortcut(web: bool) -> &'static str {
|
||||
if web { "Mod+Alt+Shift+C / Mod+Shift+C" } else { "Mod+Shift+C" }
|
||||
}
|
||||
|
||||
/// Undo several steps at once: the Undo button's list (#701). `count` is validated here and
|
||||
/// clamped to the undo depth, so hostile numbers never reach the stack.
|
||||
fn undo_to(s: &mut Session, v: &Value) -> CmdResult {
|
||||
@@ -168,7 +175,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
painter_pick(s);
|
||||
sel_result(s)
|
||||
})
|
||||
.key("Mod+Shift+C")
|
||||
.key(copy_format_shortcut(cfg!(target_arch = "wasm32")))
|
||||
.pure(),
|
||||
CommandSpec::new("edit.pasteFormat", "Paste Formatting", "Home › Clipboard", paste_format).key("Mod+Shift+V"),
|
||||
]
|
||||
@@ -854,7 +861,10 @@ fn paste_format(s: &mut Session, _: &Value) -> CmdResult {
|
||||
let f = s.sel.focus.clone();
|
||||
if let Some(pp) = s.doc.para_at(&f) {
|
||||
let (x, y) = pp.word_at(f.off);
|
||||
s.doc.format_range(&Pos { off: x, ..f.clone() }, &Pos { off: y, ..f.clone() }, &|c| *c = chr.cleared().overlaid(&chr))?;
|
||||
// Word selection includes trailing spaces; a painter click formats just the word,
|
||||
// matching the ordinary collapsed-caret formatting target.
|
||||
let word_end = pp.text.get(x..y).map(|word| x + word.trim_end().len()).unwrap_or(y);
|
||||
s.doc.format_range(&Pos { off: x, ..f.clone() }, &Pos { off: word_end, ..f.clone() }, &|c| *c = chr.cleared().overlaid(&chr))?;
|
||||
}
|
||||
s.doc.format_paragraphs(&a, &b, &|p| *p = para.clone())?;
|
||||
} else {
|
||||
@@ -872,3 +882,49 @@ fn paste_format(s: &mut Session, _: &Value) -> CmdResult {
|
||||
}
|
||||
sel_result(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod shortcut_tests {
|
||||
use super::*;
|
||||
use crate::{KeyMap, Registry};
|
||||
|
||||
#[test]
|
||||
fn copy_format_web_alias_is_unique_and_keeps_existing_shortcuts() {
|
||||
for web in [false, true] {
|
||||
let mut commands = super::super::registry().all().to_vec();
|
||||
let copy = commands.iter_mut().find(|c| c.id == "edit.copyFormat").unwrap();
|
||||
copy.shortcut = copy_format_shortcut(web);
|
||||
let registry = Registry::new(commands);
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for command in registry.all() {
|
||||
for key in command.shortcut.split(" / ").filter_map(crate::keymap::normalize) {
|
||||
assert!(seen.insert(key.clone()), "duplicate binding: {key}");
|
||||
}
|
||||
}
|
||||
let keys = KeyMap::default();
|
||||
let resolve = |key| keys.resolve(®istry, key).map(|c| c.id);
|
||||
assert_eq!(resolve("Mod+Shift+C"), Some("edit.copyFormat"));
|
||||
assert_eq!(resolve("Mod+Alt+Shift+C"), web.then_some("edit.copyFormat"));
|
||||
assert_eq!(resolve("Mod+Shift+V"), Some("edit.pasteFormat"));
|
||||
assert_eq!(resolve("Mod+Alt+Shift+V"), Some("edit.pasteText"));
|
||||
if !cfg!(target_os = "macos") {
|
||||
assert_eq!(resolve("Mod+Alt+C"), Some("text.copyright"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_format_web_alias_respects_user_removal_and_reassignment() {
|
||||
let mut commands = super::super::registry().all().to_vec();
|
||||
commands.iter_mut().find(|c| c.id == "edit.copyFormat").unwrap().shortcut = copy_format_shortcut(true);
|
||||
let registry = Registry::new(commands);
|
||||
let mut keys = KeyMap::default();
|
||||
assert_eq!(keys.keys_for(®istry, "edit.copyFormat").first().map(String::as_str), Some("Mod+Alt+Shift+C"));
|
||||
assert!(keys.remove(®istry, "edit.copyFormat", "Mod+Alt+Shift+C").unwrap());
|
||||
assert!(keys.resolve(®istry, "Mod+Alt+Shift+C").is_none());
|
||||
keys.assign(®istry, "format.bold", "Mod+Alt+Shift+C").unwrap();
|
||||
let restored = KeyMap::from_value(&serde_json::to_value(&keys).unwrap());
|
||||
assert_eq!(restored.resolve(®istry, "Mod+Alt+Shift+C").map(|c| c.id), Some("format.bold"));
|
||||
assert_eq!(restored.resolve(®istry, "Mod+Shift+C").map(|c| c.id), Some("edit.copyFormat"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ fn gallery_lists_everything() {
|
||||
|
||||
/// A session on a DOCX whose only paragraph is `<m:oMath>{math}</m:oMath>`.
|
||||
fn imported(math: &str) -> Session {
|
||||
use std::io::Write;
|
||||
let w = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
||||
let m = "http://schemas.openxmlformats.org/officeDocument/2006/math";
|
||||
let parts = [
|
||||
@@ -117,24 +116,21 @@ fn imported(math: &str) -> Session {
|
||||
),
|
||||
("word/document.xml", format!(r#"<w:document xmlns:w="{w}" xmlns:m="{m}"><w:body><w:p><m:oMath>{math}</m:oMath></w:p><w:sectPr/></w:body></w:document>"#)),
|
||||
];
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, xml) in parts {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(xml.as_bytes()).unwrap();
|
||||
zw.add(name, xml.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let bytes = zw.finish().unwrap().into_inner();
|
||||
let bytes = zw.finish().unwrap();
|
||||
let mut s = Session::new(wordcraft_docx::read(&bytes).unwrap());
|
||||
s.run("equation.edit", &json!({})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn document_xml(s: &Session) -> String {
|
||||
use std::io::Read;
|
||||
let bytes = wordcraft_docx::write(&s.doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,8 +5,7 @@ use super::*;
|
||||
#[test]
|
||||
fn a_huge_picture_width_keeps_the_size_finite() {
|
||||
let img = image::RgbaImage::from_pixel(2, 400, image::Rgba([10, 20, 30, 255]));
|
||||
let mut png = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let png = wordcraft_geom::picture::encode_format(&image::DynamicImage::ImageRgba8(img).clone(), image::ImageFormat::Png).unwrap();
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
let r = s.run("insert.picture", &json!({"data": base64_encode(&png), "width": 3e38})).unwrap();
|
||||
let (w, h) = (r["width"].as_f64().unwrap(), r["height"].as_f64().unwrap());
|
||||
@@ -17,8 +16,8 @@ fn a_huge_picture_width_keeps_the_size_finite() {
|
||||
|
||||
/// A 4 × 4 RGB TIFF that says its strips are JPEG (they aren't): its header still reads.
|
||||
fn jpeg_strip_tiff() -> Vec<u8> {
|
||||
let mut t = Vec::new();
|
||||
image::RgbImage::new(4, 4).write_to(&mut std::io::Cursor::new(&mut t), image::ImageFormat::Tiff).unwrap();
|
||||
let mut t =
|
||||
wordcraft_geom::picture::encode_format(&image::DynamicImage::from(image::RgbImage::new(4, 4).clone()), image::ImageFormat::Tiff).unwrap();
|
||||
assert_eq!(&t[..4], b"II*\0");
|
||||
let ifd = u32::from_le_bytes(t[4..8].try_into().unwrap()) as usize;
|
||||
let n = usize::from(u16::from_le_bytes([t[ifd], t[ifd + 1]]));
|
||||
@@ -37,8 +36,8 @@ fn tiffs_with_jpeg_strips_are_refused() {
|
||||
let e = s.run("insert.picture", &json!({"data": base64_encode(&tiff)})).unwrap_err().to_string();
|
||||
assert!(e.contains("TIFF"), "{e}");
|
||||
assert!(s.doc.media.is_empty());
|
||||
let mut plain = Vec::new();
|
||||
image::RgbImage::new(4, 4).write_to(&mut std::io::Cursor::new(&mut plain), image::ImageFormat::Tiff).unwrap();
|
||||
let plain =
|
||||
wordcraft_geom::picture::encode_format(&image::DynamicImage::from(image::RgbImage::new(4, 4).clone()), image::ImageFormat::Tiff).unwrap();
|
||||
s.run("insert.picture", &json!({"data": base64_encode(&plain)})).unwrap();
|
||||
let e = s.run("picture.change", &json!({"data": base64_encode(&tiff)})).unwrap_err().to_string();
|
||||
assert!(e.contains("TIFF"), "{e}");
|
||||
|
||||
@@ -189,22 +189,19 @@ fn merge_end_to_end() {
|
||||
/// A typed list (#240) keeps its column order, and rows may be arrays in that order.
|
||||
#[test]
|
||||
fn recipients_from_a_workbook_ask_for_the_sheet_when_there_are_several() {
|
||||
use std::io::Write;
|
||||
let cell = |t: &str| format!(r#"<table:table-cell office:value-type="string"><text:p>{t}</text:p></table:table-cell>"#);
|
||||
let row = |cells: &[&str]| format!("<table:table-row>{}</table:table-row>", cells.iter().map(|c| cell(c)).collect::<String>());
|
||||
let content = format!(
|
||||
r#"<office:document-content xmlns:office="o" xmlns:table="t" xmlns:text="x"><office:body><office:spreadsheet><table:table table:name="Notes">{}</table:table><table:table table:name="Guests">{}{}{}</table:table></office:spreadsheet></office:body></office:document-content>"#,
|
||||
r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"><office:body><office:spreadsheet><table:table table:name="Notes">{}</table:table><table:table table:name="Guests">{}{}{}</table:table></office:spreadsheet></office:body></office:document-content>"#,
|
||||
row(&["n"]),
|
||||
row(&["Name", "", "Name"]),
|
||||
row(&["Ada", "London", "A"]),
|
||||
row(&["Alan", "Wilmslow", "B"]),
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for (name, body) in [("mimetype", "application/vnd.oasis.opendocument.spreadsheet"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
z.write_all(body.as_bytes()).unwrap();
|
||||
}
|
||||
let data = super::super::insert::base64_encode(&z.finish().unwrap().into_inner());
|
||||
let mut z = craft_zip::Writer::new();
|
||||
z.add("mimetype", b"application/vnd.oasis.opendocument.spreadsheet", craft_zip::Compression::Stored).unwrap();
|
||||
z.add("content.xml", content.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
let data = super::super::insert::base64_encode(&z.finish().unwrap());
|
||||
let mut s = Session::new(Document::new());
|
||||
// Two sheets and none chosen: the names come back and nothing loads.
|
||||
let r = s.run("mailings.recipients", &json!({"data": data})).unwrap();
|
||||
@@ -593,8 +590,7 @@ fn recipients_from_an_xls_workbook() {
|
||||
let book = wordcraft_formats::sheet::xls_fixture(&[(
|
||||
"收件人",
|
||||
vec![row(&["姓名", "城市", "数量"]), row(&["张三", "北京", "12"]), row(&["李四", "上海", "2"])],
|
||||
)])
|
||||
.unwrap();
|
||||
)]);
|
||||
let mut s = Session::new(Document::new());
|
||||
let r = s.run("mailings.recipients", &json!({"path": "名单.xls", "data": b64(&book)})).unwrap();
|
||||
assert_eq!(r, json!({"fields": ["姓名", "城市", "数量"], "records": 2, "sheet": "收件人"}));
|
||||
|
||||
@@ -194,8 +194,7 @@ pub(super) fn adjust_media(
|
||||
e => CmdError::Failed(format!("can't decode the picture: {e}")),
|
||||
})?;
|
||||
let out = f(img)?;
|
||||
let mut png = Vec::new();
|
||||
out.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).map_err(|e| CmdError::Failed(e.to_string()))?;
|
||||
let png = wordcraft_geom::picture::encode_png(&out).map_err(CmdError::Failed)?;
|
||||
let key = s.doc.add_media(png, "png");
|
||||
// Chain onto the ultimate original (the base's, or the current picture's when the base
|
||||
// is itself derived), so Reset Picture always finds it.
|
||||
|
||||
@@ -5,9 +5,8 @@ fn png(w: u32, h: u32) -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_fn(w, h, |x, y| {
|
||||
if x > 2 && x < w - 3 && y > 2 && y < h - 3 { image::Rgba([200, 30, 30, 255]) } else { image::Rgba([255, 255, 255, 255]) }
|
||||
});
|
||||
let mut b = Vec::new();
|
||||
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut b), image::ImageFormat::Png).unwrap();
|
||||
b
|
||||
|
||||
wordcraft_geom::picture::encode_format(&image::DynamicImage::ImageRgba8(img).clone(), image::ImageFormat::Png).unwrap()
|
||||
}
|
||||
/// The laid-out area of the only picture or shape (not a text box).
|
||||
fn object_area(s: &mut Session) -> wordcraft_layout::hit::ObjectHit {
|
||||
@@ -451,8 +450,9 @@ fn remove_background_keeps_a_small_stack() {
|
||||
/// 1 × 2,000,000 picture Thick Frame asked for about 1.8 TB and aborted the process.
|
||||
#[test]
|
||||
fn picture_styles_and_borders_check_the_size_they_draw() {
|
||||
let mut thin = Vec::new();
|
||||
image::GrayImage::new(1, 2_000_000).write_to(&mut std::io::Cursor::new(&mut thin), image::ImageFormat::Png).unwrap();
|
||||
let thin =
|
||||
wordcraft_geom::picture::encode_format(&image::DynamicImage::from(image::GrayImage::new(1, 2_000_000).clone()), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
s.run("insert.picture", &json!({"data": super::super::insert::base64_encode(&thin)})).unwrap();
|
||||
let media = s.doc.media.len();
|
||||
@@ -526,7 +526,7 @@ fn transparency_is_absolute_from_the_original() {
|
||||
let (_, o) = selected(s).unwrap();
|
||||
let InlineObject::Image { media, .. } = o else { panic!("a picture") };
|
||||
let bytes = s.doc.media.get(&media).cloned().unwrap();
|
||||
image::load_from_memory(&bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect::<Vec<_>>()
|
||||
wordcraft_geom::picture::decode_memory(&bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect::<Vec<_>>()
|
||||
};
|
||||
let alpha = |s: &Session| rgba(s).iter().map(|p| p[3]).collect::<Vec<_>>();
|
||||
let orig = rgba(&s);
|
||||
@@ -579,7 +579,7 @@ fn picture_edits_replace_the_file_corrections() {
|
||||
let (_, o) = selected(s).unwrap();
|
||||
let InlineObject::Image { media, fx, .. } = o else { panic!("a picture") };
|
||||
let bytes = s.doc.media.get(&media).cloned().unwrap();
|
||||
(media, fx, image::load_from_memory(&bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect::<Vec<_>>())
|
||||
(media, fx, wordcraft_geom::picture::decode_memory(&bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect::<Vec<_>>())
|
||||
};
|
||||
let (orig_media, _, orig) = state(&s);
|
||||
// Transparency 50%: 50% (not 75%), the file's transparency dropped, its lum kept.
|
||||
@@ -670,7 +670,7 @@ fn picture_styles_borders_and_effects_replace_the_last_one() {
|
||||
let bitmap = |s: &Session| {
|
||||
let (_, o) = selected(s).unwrap();
|
||||
let InlineObject::Image { media, .. } = o else { panic!("a picture") };
|
||||
image::load_from_memory(&s.doc.media.get(&media).cloned().unwrap()).unwrap().to_rgba8()
|
||||
wordcraft_geom::picture::decode_memory(&s.doc.media.get(&media).cloned().unwrap()).unwrap().to_rgba8()
|
||||
};
|
||||
let after = |cmds: &[(&str, Value)]| {
|
||||
let mut s = picture();
|
||||
@@ -1274,9 +1274,9 @@ fn arrange_bounds_on_header_picture_writes_page_anchored_odt_frame() {
|
||||
assert_eq!((float.h_rel, float.v_rel), (Anchor::Page, Anchor::Page));
|
||||
assert_eq!((float.x, float.y), (0.0, 0.0));
|
||||
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&s.doc).unwrap())).unwrap();
|
||||
let mut styles = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("styles.xml").unwrap(), &mut styles).unwrap();
|
||||
let _z_bytes = wordcraft_formats::odt::export(&s.doc).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let styles = String::from_utf8(z.read("styles.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(styles.contains(r#"text:anchor-type="page""#), "frame should be page-anchored: {styles}");
|
||||
assert!(styles.contains(r#"svg:x="0pt""#) && styles.contains(r#"svg:y="0pt""#), "offsets should be 0pt: {styles}");
|
||||
assert!(!styles.contains(r#"text:anchor-type="paragraph""#), "frame should not be paragraph-anchored: {styles}");
|
||||
@@ -1288,9 +1288,9 @@ fn arrange_bounds_on_header_picture_writes_page_anchored_odt_frame() {
|
||||
assert_eq!((float.h_rel, float.v_rel), (Anchor::Page, Anchor::Page));
|
||||
assert_eq!((float.x, float.y), (10.0, 20.0));
|
||||
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&s.doc).unwrap())).unwrap();
|
||||
let mut styles = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("styles.xml").unwrap(), &mut styles).unwrap();
|
||||
let _z_bytes = wordcraft_formats::odt::export(&s.doc).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let styles = String::from_utf8(z.read("styles.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(styles.contains(r#"text:anchor-type="page""#), "{styles}");
|
||||
assert!(styles.contains(r#"svg:x="10pt""#) && styles.contains(r#"svg:y="20pt""#), "{styles}");
|
||||
}
|
||||
@@ -1325,9 +1325,9 @@ fn a_moved_body_picture_is_placed_by_its_paragraph_even_when_fixed_to_the_page()
|
||||
let hit = s.layout().object(&pos, 0).expect("laid out");
|
||||
assert!((hit.rect.x - 50.0).abs() < 1.0 && (hit.rect.y - 80.0).abs() < 1.0, "dropped: {:?}", hit.rect);
|
||||
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&s.doc).unwrap())).unwrap();
|
||||
let mut content = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("content.xml").unwrap(), &mut content).unwrap();
|
||||
let _z_bytes = wordcraft_formats::odt::export(&s.doc).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let content = String::from_utf8(z.read("content.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(content.contains(r#"text:anchor-type="paragraph""#), "body picture should export with paragraph anchor: {content}");
|
||||
assert!(!content.contains("anchor-page-number"), "{content}");
|
||||
}
|
||||
@@ -1606,7 +1606,6 @@ fn mirrored_indents_make_no_extra_column() {
|
||||
/// A Word file whose only paragraph holds a group of a rectangle and Word's own ink (with its
|
||||
/// picture fallback).
|
||||
fn group_with_word_ink() -> Vec<u8> {
|
||||
use std::io::Write;
|
||||
let ns = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk""#;
|
||||
let rect = r#"<wps:wsp><wps:cNvPr id="2" name="Box"/><wps:cNvSpPr/><wps:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></wps:spPr><wps:bodyPr/></wps:wsp>"#;
|
||||
let ink = r#"<mc:AlternateContent><mc:Choice Requires="wpi"><w14:contentPart bwMode="auto" r:id="rInk"><w14:nvContentPartPr><w14:cNvPr id="3" name="Ink 3"/><w14:cNvContentPartPr/></w14:nvContentPartPr><w14:xfrm><a:off x="914400" y="0"/><a:ext cx="914400" cy="457200"/></w14:xfrm></w14:contentPart></mc:Choice><mc:Fallback><pic:pic><pic:nvPicPr><pic:cNvPr id="3" name="Ink 3"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="rPreview"/></pic:blipFill><pic:spPr><a:xfrm><a:off x="914400" y="0"/><a:ext cx="914400" cy="457200"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></mc:Fallback></mc:AlternateContent>"#;
|
||||
@@ -1626,14 +1625,12 @@ fn group_with_word_ink() -> Vec<u8> {
|
||||
),
|
||||
("word/ink/ink1.xml", r#"<ink xmlns="http://www.w3.org/2003/InkML"><trace>0 0, 1 1</trace></ink>"#.to_string()),
|
||||
];
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, xml) in parts {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(xml.as_bytes()).unwrap();
|
||||
zw.add(name, xml.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.start_file("word/media/preview.png", zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(&png(8, 4)).unwrap();
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.add("word/media/preview.png", &png(8, 4), craft_zip::Compression::Deflated).unwrap();
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
fn ink_members(doc: &wordcraft_doc::Document) -> usize {
|
||||
@@ -1653,7 +1650,6 @@ fn ink_members(doc: &wordcraft_doc::Document) -> usize {
|
||||
/// yet), so open → Ungroup → save → reopen keeps the ink in the group with its part.
|
||||
#[test]
|
||||
fn ungroup_refuses_a_group_holding_word_ink_and_the_ink_survives_saving() {
|
||||
use std::io::Read;
|
||||
let mut s = Session::new(wordcraft_docx::read(&group_with_word_ink()).unwrap());
|
||||
assert_eq!(ink_members(&s.doc), 1, "the fixture's ink is read into the group");
|
||||
let pos = Pos::body(0, 0);
|
||||
@@ -1666,16 +1662,14 @@ fn ungroup_refuses_a_group_holding_word_ink_and_the_ink_survives_saving() {
|
||||
let err = ungroup(&mut s, &json!({})).unwrap_err();
|
||||
assert!(matches!(err, CmdError::Disabled(_)), "{err:?}");
|
||||
let bytes = wordcraft_docx::write(&s.doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes.clone())).unwrap();
|
||||
let mut xml = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(xml.matches("<w14:contentPart").count(), 1, "{xml}");
|
||||
assert!(xml.contains("<mc:Fallback>"), "{xml}");
|
||||
assert_eq!(bytes, before, "the refused ungroup leaves the document unchanged");
|
||||
let names: Vec<String> = z.file_names().map(String::from).collect();
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
assert_eq!(names.iter().filter(|n| n.starts_with("word/ink/")).count(), 1, "{names:?}");
|
||||
let mut rels = String::new();
|
||||
z.by_name("word/_rels/document.xml.rels").unwrap().read_to_string(&mut rels).unwrap();
|
||||
let rels = String::from_utf8(z.read("word/_rels/document.xml.rels", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(rels.matches("/customXml\"").count(), 1, "one ink relationship: {rels}");
|
||||
let back = wordcraft_docx::read(&bytes).unwrap();
|
||||
assert_eq!(ink_members(&back), 1);
|
||||
|
||||
@@ -56,7 +56,7 @@ fn docx_styleref_header_shows_each_page_s_chapter() {
|
||||
);
|
||||
let root = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let rels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdH" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/><Relationship Id="rIdS" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, text) in [
|
||||
("_rels/.rels", root),
|
||||
("word/document.xml", body.as_str()),
|
||||
@@ -64,10 +64,9 @@ fn docx_styleref_header_shows_each_page_s_chapter() {
|
||||
("word/header1.xml", header.as_str()),
|
||||
("word/styles.xml", styles.as_str()),
|
||||
] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut zw, text.as_bytes()).unwrap();
|
||||
zw.add(name, text.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let bytes = zw.finish().unwrap().into_inner();
|
||||
let bytes = zw.finish().unwrap();
|
||||
let headers = |s: &mut Session| -> Vec<String> {
|
||||
s.layout()
|
||||
.pages
|
||||
@@ -714,12 +713,11 @@ fn output_numbers_text_box_captions_where_their_boxes_are_anchored() {
|
||||
let body: String = paras.iter().map(|p| format!("<w:p>{p}</w:p>")).collect();
|
||||
let document = format!("<w:document {ns}><w:body>{body}<w:sectPr/></w:body></w:document>");
|
||||
let root = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, text) in [("_rels/.rels", root), ("word/document.xml", document.as_str())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut zw, text.as_bytes()).unwrap();
|
||||
zw.add(name, text.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let s = Session::new(crate::io::open_bytes("boxes.docx", &zw.finish().unwrap().into_inner()).unwrap());
|
||||
let s = Session::new(crate::io::open_bytes("boxes.docx", &zw.finish().unwrap()).unwrap());
|
||||
let out = crate::cmd::references::output_fields(&s.doc).expect("fresh numbers");
|
||||
// The SEQ and REF results in reading order, text boxes where they are anchored.
|
||||
let mut results = Vec::new();
|
||||
@@ -774,12 +772,11 @@ fn output_shows_references_to_bookmarks_outside_the_body() {
|
||||
("word/endnotes.xml", endnotes),
|
||||
("word/comments.xml", comments),
|
||||
];
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, text) in &files {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut zw, text.as_bytes()).unwrap();
|
||||
zw.add(name, text.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let s = Session::new(crate::io::open_bytes("stories.docx", &zw.finish().unwrap().into_inner()).unwrap());
|
||||
let s = Session::new(crate::io::open_bytes("stories.docx", &zw.finish().unwrap()).unwrap());
|
||||
let out = crate::cmd::references::output_fields(&s.doc).expect("fresh references");
|
||||
let mut results = Vec::new();
|
||||
for path in out.para_paths(StoryRef::Body) {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -28,7 +28,7 @@ const ITEMS: &str = r#"[string | {"text": string, "level"?: 0-7, "color"?: RRGGB
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("insert.smartArt", "SmartArt", "Insert › Illustrations", insert).params(
|
||||
CommandSpec::new("insert.smartArt", "Diagram", "Insert › Illustrations", insert).params(
|
||||
r#"{"layout"?: "basicList|process|cycle|hierarchy|pyramid|radial|matrix|venn", "items"?: [string | {"text": string, "level"?: 0-7, "color"?: RRGGBB}], "colors"?: "accent1…accent6|colorful", "width"?: pt, "height"?: pt} (no items: sample items for the layout)"#,
|
||||
),
|
||||
CommandSpec::new("smartArt.items", "Edit Items", "SmartArt Design › Create Graphic", |s, v| {
|
||||
@@ -53,7 +53,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let below = p::bool(v, "below").unwrap_or(false);
|
||||
edit(s, move |g| {
|
||||
if g.items.len() >= MAX_ITEMS {
|
||||
return Err(CmdError::Failed(format!("a SmartArt graphic holds at most {MAX_ITEMS} items")));
|
||||
return Err(CmdError::Failed(format!("a diagram holds at most {MAX_ITEMS} items")));
|
||||
}
|
||||
let Some(last) = g.items.len().checked_sub(1) else {
|
||||
g.items.push(SmartArtItem::new(text.clone(), 0));
|
||||
@@ -120,7 +120,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
})
|
||||
.params(r#"{"layout": "basicList|process|cycle|hierarchy|pyramid|radial|matrix|venn"}"#)
|
||||
.when(has_smart_art),
|
||||
CommandSpec::new("smartArt.colors", "Change Colors", "SmartArt Design › SmartArt Styles", |s, v| {
|
||||
CommandSpec::new("smartArt.colors", "Change Colors", "SmartArt Design › Diagram Styles", |s, v| {
|
||||
let colors = colors_of(v)?.ok_or_else(|| CmdError::Params(format!("`variant` is required: {COLORS}")))?;
|
||||
edit(s, move |g| {
|
||||
g.colors = colors;
|
||||
@@ -130,7 +130,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
})
|
||||
.params(r#"{"variant": "accent1|accent2|accent3|accent4|accent5|accent6|colorful"} (the items' own colours are dropped)"#)
|
||||
.when(has_smart_art),
|
||||
CommandSpec::new("smartArt.itemColor", "Item Color", "SmartArt Design › SmartArt Styles", |s, v| {
|
||||
CommandSpec::new("smartArt.itemColor", "Item Color", "SmartArt Design › Diagram Styles", |s, v| {
|
||||
let i = index(v, "index")?;
|
||||
let color = match v.get("color") {
|
||||
None | Some(Value::Null) => None,
|
||||
@@ -174,9 +174,9 @@ fn has_smart_art(s: &Session) -> Option<&'static str> {
|
||||
match selected(s) {
|
||||
Some((_, InlineObject::Graphic { graphic, .. })) if graphic.smart_art.is_some() => None,
|
||||
Some((_, InlineObject::Graphic { graphic, .. })) if graphic.kind == GraphicKind::Diagram => {
|
||||
Some("this SmartArt was made in another program: WordCraft shows it but can't edit it")
|
||||
Some("this diagram was made in another program: WordCraft shows it but can't edit it")
|
||||
}
|
||||
_ => Some("select a SmartArt graphic first"),
|
||||
_ => Some("select a diagram first"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,9 +299,9 @@ fn insert(s: &mut Session, v: &Value) -> CmdResult {
|
||||
|
||||
/// Change the selected graphic's model with `f`, then lay it out again at its size.
|
||||
fn edit(s: &mut Session, f: impl Fn(&mut SmartArtSpec) -> Result<(), CmdError>) -> CmdResult {
|
||||
let (pos, obj) = selected(s).ok_or_else(|| CmdError::Disabled("select a SmartArt graphic first".into()))?;
|
||||
let InlineObject::Graphic { w, h, graphic, .. } = obj else { return Err(CmdError::Disabled("select a SmartArt graphic first".into())) };
|
||||
let mut spec = graphic.smart_art.as_deref().cloned().ok_or_else(|| CmdError::Disabled("this SmartArt graphic can't be edited".into()))?;
|
||||
let (pos, obj) = selected(s).ok_or_else(|| CmdError::Disabled("select a diagram first".into()))?;
|
||||
let InlineObject::Graphic { w, h, graphic, .. } = obj else { return Err(CmdError::Disabled("select a diagram first".into())) };
|
||||
let mut spec = graphic.smart_art.as_deref().cloned().ok_or_else(|| CmdError::Disabled("this diagram can't be edited".into()))?;
|
||||
f(&mut spec)?;
|
||||
let next = Arc::new(smart_art_graphic(spec, &s.doc.settings.theme_colors, w, h));
|
||||
let out = json!({"smartArt": next.smart_art.as_deref()});
|
||||
|
||||
@@ -19,24 +19,24 @@ const EFFECT_PARAMS: &str = r#"{"style"?: "solid"|"outlined"|"hollow"|"gradient"
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("insert.wordArt", "WordArt", "Insert › Text", insert).params(
|
||||
r#"{"text"?: string, "style"?: WordArt style id (see format.textEffects), "transform"?: "textArchUp"|"textArchDown"|"textCircle"|"textWave1"|… (a DrawingML preset text warp)}"#,
|
||||
CommandSpec::new("insert.wordArt", "Decorative Text", "Insert › Text", insert).params(
|
||||
r#"{"text"?: string, "style"?: Decorative Text style id (see format.textEffects), "transform"?: "textArchUp"|"textArchDown"|"textCircle"|"textWave1"|… (a DrawingML preset text warp)}"#,
|
||||
),
|
||||
CommandSpec::new("format.textEffects", "Text Effects", "Home › Font", text_effects).params(EFFECT_PARAMS),
|
||||
CommandSpec::new("wordArt.style", "WordArt Styles", "Shape Format › WordArt Styles", |s, v| {
|
||||
CommandSpec::new("wordArt.style", "Decorative Text Styles", "Shape Format › Decorative Text Styles", |s, v| {
|
||||
let id = p::req_str(v, "style")?;
|
||||
text_effects(s, &json!({"style": id}))
|
||||
})
|
||||
.params(r#"{"style": "solid"|"outlined"|"hollow"|"gradient"|"shadowed"|"glowing"|"reflected"|"sunset"}"#),
|
||||
CommandSpec::new("wordArt.textFill", "Text Fill", "Shape Format › WordArt Styles", |s, v| {
|
||||
CommandSpec::new("wordArt.textFill", "Text Fill", "Shape Format › Decorative Text Styles", |s, v| {
|
||||
text_effects(s, &json!({"fill": v.get("fill").or_else(|| v.get("color")).cloned().unwrap_or(Value::Null)}))
|
||||
})
|
||||
.params(r#"{"fill": "RRGGBB"|"none"|{"gradient": ["RRGGBB", …], "angle"?: deg}|null}"#),
|
||||
CommandSpec::new("wordArt.textOutline", "Text Outline", "Shape Format › WordArt Styles", |s, v| {
|
||||
CommandSpec::new("wordArt.textOutline", "Text Outline", "Shape Format › Decorative Text Styles", |s, v| {
|
||||
text_effects(s, &json!({"outline": v.get("outline").cloned().unwrap_or_else(|| v.clone())}))
|
||||
})
|
||||
.params(r#"{"color"?: "RRGGBB"|null, "width"?: pt} or {"outline": "none"|null}"#),
|
||||
CommandSpec::new("wordArt.transform", "Transform", "Shape Format › WordArt Styles", transform)
|
||||
CommandSpec::new("wordArt.transform", "Transform", "Shape Format › Decorative Text Styles", transform)
|
||||
.params(r#"{"preset": "textArchUp"|"textArchDown"|"textCircle"|"textWave1"|"textSlantUp"|"textSlantDown"|"textInflate"|"textDeflate"|"textChevron"|"textChevronInverted"|"textTriangle"|"textTriangleInverted"|… (any DrawingML preset text warp)|"none", "adj"?: n (the preset's adjust value)}"#),
|
||||
]
|
||||
}
|
||||
@@ -186,7 +186,7 @@ fn text_effects(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let theme = theme(s);
|
||||
let accent = theme.get(4).copied().unwrap_or(Rgb(0x15, 0x60, 0x82));
|
||||
let style = match p::str(v, "style") {
|
||||
Some(id) => Some(art_style(id, &theme).ok_or_else(|| CmdError::Params(format!("unknown WordArt style `{id}`")))?),
|
||||
Some(id) => Some(art_style(id, &theme).ok_or_else(|| CmdError::Params(format!("unknown decorative text style `{id}`")))?),
|
||||
None => None,
|
||||
};
|
||||
let clear = p::bool(v, "clear").unwrap_or(false);
|
||||
@@ -244,11 +244,11 @@ fn transform(s: &mut Session, v: &Value) -> CmdResult {
|
||||
Some(w)
|
||||
}
|
||||
};
|
||||
let pos = warp_target(s).ok_or_else(|| CmdError::Disabled("select a shape or WordArt first".into()))?;
|
||||
let pos = warp_target(s).ok_or_else(|| CmdError::Disabled("select a shape or decorative text first".into()))?;
|
||||
let para = s.doc.para_mut(pos.story, &pos.path)?;
|
||||
match para.object_at_mut(pos.off) {
|
||||
Some(InlineObject::Shape { extra, .. }) => extra.warp = warp.clone(),
|
||||
_ => return Err(CmdError::Disabled("select a shape or WordArt first".into())),
|
||||
_ => return Err(CmdError::Disabled("select a shape or decorative text first".into())),
|
||||
}
|
||||
para.touch();
|
||||
Ok(json!({"transform": warp.map(|w| w.preset)}))
|
||||
@@ -269,7 +269,7 @@ fn insert(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let text: String = p::str(v, "text").unwrap_or("Your text here").chars().filter(|c| !c.is_control()).take(1000).collect();
|
||||
let text = if text.trim().is_empty() { "Your text here".to_string() } else { text };
|
||||
let style = p::str(v, "style").unwrap_or("solid");
|
||||
let fx = art_style(style, &theme(s)).ok_or_else(|| CmdError::Params(format!("unknown WordArt style `{style}`")))?;
|
||||
let fx = art_style(style, &theme(s)).ok_or_else(|| CmdError::Params(format!("unknown decorative text style `{style}`")))?;
|
||||
let warp = match p::str(v, "transform") {
|
||||
None | Some("none") => None,
|
||||
Some(id) => Some(TextWarp::new(id).ok_or_else(|| CmdError::Params(format!("unknown text warp `{id}`")))?),
|
||||
|
||||
@@ -468,7 +468,7 @@ pub fn save_bytes_opts(name: &str, doc: &Document, opts: &ExportOptions) -> Resu
|
||||
pub fn save_bytes_with(name: &str, doc: &Document, password: Option<&str>, opts: &ExportOptions) -> Result<Vec<u8>, String> {
|
||||
let bytes = save_bytes_opts(name, doc, opts)?;
|
||||
match password {
|
||||
Some(pw) if matches!(ext_of(name).as_str(), "odt" | "ott") => wordcraft_formats::odt::encrypt(&bytes, pw),
|
||||
Some(pw) if matches!(ext_of(name).as_str(), "odt" | "ott") => wordcraft_formats::odt::encrypt(&bytes, pw).map_err(|e| e.to_string()),
|
||||
Some(pw) if can_encrypt(name) => wordcraft_docx::encrypt(&bytes, pw).map_err(|e| e.to_string()),
|
||||
_ => Ok(bytes),
|
||||
}
|
||||
|
||||
+109
-130
@@ -4271,7 +4271,6 @@ fn toc_menu_offers_update_field_after_save_and_reopen() {
|
||||
/// doesn't grow the document.
|
||||
#[test]
|
||||
fn word_toc_control_with_the_field_end_after_the_entries_survives_update_and_save() {
|
||||
use std::io::{Read, Write};
|
||||
let style = |st: &str, inner: &str| format!(r#"<w:p><w:pPr><w:pStyle w:val="{st}"/></w:pPr>{inner}</w:p>"#);
|
||||
let t = |s: &str| format!(r#"<w:r><w:t xml:space="preserve">{s}</w:t></w:r>"#);
|
||||
let begin = r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve"> TOC \o "1-3" \h \z \u </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r>"#;
|
||||
@@ -4289,7 +4288,7 @@ fn word_toc_control_with_the_field_end_after_the_entries_survives_update_and_sav
|
||||
style("Heading1", &t("Chapter One")),
|
||||
style("Heading2", &t("Section"))
|
||||
);
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, value) in [
|
||||
("word/document.xml", xml.as_str()),
|
||||
(
|
||||
@@ -4297,16 +4296,21 @@ fn word_toc_control_with_the_field_end_after_the_entries_survives_update_and_sav
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="main" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#,
|
||||
),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(value.as_bytes()).unwrap();
|
||||
zip.add(name, value.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let mut s = Session::new(wordcraft_docx::read(&zip.finish().unwrap().into_inner()).unwrap());
|
||||
let mut s = Session::new(wordcraft_docx::read(&zip.finish().unwrap()).unwrap());
|
||||
let mut blocks = None;
|
||||
for round in 0..3 {
|
||||
run(&mut s, "references.updateToc", json!({}));
|
||||
let bytes = wordcraft_docx::write(&s.doc).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap().by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let xml = String::from_utf8(
|
||||
craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 })
|
||||
.unwrap()
|
||||
.read("word/document.xml", 16 << 20)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(xml.matches("<w:sdt>").count(), 1, "round {round}: {xml}");
|
||||
let (open, close) = (xml.find("<w:sdt>").unwrap(), xml.find("</w:sdt>").unwrap());
|
||||
assert!(open < xml.find(">Contents<").unwrap(), "round {round}: the heading is in the control: {xml}");
|
||||
@@ -4328,11 +4332,10 @@ fn style_xml(id: &str, name: &str, ppr: &str, rpr: &str) -> String {
|
||||
|
||||
/// A DOCX package with `body` and the paragraph `styles` after a default Normal style.
|
||||
fn docx_with_styles(body: &str, styles: &str) -> Vec<u8> {
|
||||
use std::io::Write;
|
||||
const W: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main""#;
|
||||
const RELS: &str = r#"xmlns="http://schemas.openxmlformats.org/package/2006/relationships""#;
|
||||
const TYPE: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, value) in [
|
||||
("word/document.xml", format!(r#"<w:document {W}><w:body>{body}</w:body></w:document>"#)),
|
||||
(
|
||||
@@ -4350,10 +4353,9 @@ fn docx_with_styles(body: &str, styles: &str) -> Vec<u8> {
|
||||
format!(r#"<Relationships {RELS}><Relationship Id="main" Type="{TYPE}/officeDocument" Target="word/document.xml"/></Relationships>"#),
|
||||
),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(value.as_bytes()).unwrap();
|
||||
zip.add(name, value.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
/// #3891: Update Table on a TOC Word wrote in Portuguese keeps the formatting of its TOC styles,
|
||||
@@ -4671,12 +4673,8 @@ fn update_toc_clears_stale_field_result_for_docx_span() {
|
||||
// DOCX round-trip: entries must be enclosed within the TOC field rather than outside it.
|
||||
let bytes = wordcraft_docx::write(&s.doc).unwrap();
|
||||
let xml = {
|
||||
let cursor = std::io::Cursor::new(bytes);
|
||||
let mut archive = zip::ZipArchive::new(cursor).unwrap();
|
||||
let mut file = archive.by_name("word/document.xml").unwrap();
|
||||
let mut s = String::new();
|
||||
std::io::Read::read_to_string(&mut file, &mut s).unwrap();
|
||||
s
|
||||
let mut archive = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
String::from_utf8(archive.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
};
|
||||
let b = xml.find(r#"w:fldCharType="begin""#).expect("TOC begin marker present");
|
||||
// The TOC's end is the last one: the entries' page numbers are `PAGEREF` fields inside it.
|
||||
@@ -4900,9 +4898,8 @@ fn toc_page_numbers_are_pageref_fields() {
|
||||
let expected = text(&s);
|
||||
assert!(expected.contains("Alpha\t2") && expected.contains("Bravo\t3"), "{expected}");
|
||||
let bytes = crate::io::save_bytes("toc.docx", &s.doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(xml.matches("PAGEREF _Toc").count(), 2, "{xml}");
|
||||
let mut s = Session::new(crate::io::open_bytes("toc.docx", &bytes).unwrap());
|
||||
run(&mut s, "references.updateFields", json!({}));
|
||||
@@ -5549,17 +5546,15 @@ fn an_odt_frame_moved_from_its_page_loses_the_page_number() {
|
||||
let content = format!(
|
||||
r#"<office:document-content {ns}><office:body><office:text><draw:frame text:anchor-type="page" text:anchor-page-number="3" svg:x="5cm" svg:y="5cm" svg:width="4cm" svg:height="2cm"><draw:text-box><text:p>On it</text:p></draw:text-box></draw:frame><text:p>One</text:p><text:p>Two</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
std::io::Write::write_all(&mut z, data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let odt = z.finish().unwrap().into_inner();
|
||||
let odt = z.finish().unwrap();
|
||||
let frame_tag = |s: &Session| {
|
||||
let mut a = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&s.doc).unwrap())).unwrap();
|
||||
let mut c = String::new();
|
||||
std::io::Read::read_to_string(&mut a.by_name("content.xml").unwrap(), &mut c).unwrap();
|
||||
let _a_bytes = wordcraft_formats::odt::export(&s.doc).unwrap();
|
||||
let mut a = craft_zip::Archive::open(&_a_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let c = String::from_utf8(a.read("content.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
c.split("<draw:frame").nth(1).and_then(|t| t.split('>').next()).unwrap_or_default().to_string()
|
||||
};
|
||||
let mut s = Session::new(crate::io::open_bytes("t.odt", &odt).unwrap());
|
||||
@@ -6033,10 +6028,11 @@ fn html_pictures_load_relative_to_the_file() {
|
||||
// Issue #98: `<img src="logo.png">` beside an HTML file is embedded when it is opened.
|
||||
let dir = std::env::temp_dir().join(format!("wordcraft-html-img-{}", std::process::id()));
|
||||
std::fs::create_dir_all(dir.join("img")).unwrap();
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(4, 2, image::Rgba([10, 20, 30, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let png = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(4, 2, image::Rgba([10, 20, 30, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(dir.join("img/my logo.png"), &png).unwrap();
|
||||
std::fs::write(dir.join("secret.txt"), b"not a picture").unwrap();
|
||||
let html = r#"<p><img src="img/my%20logo.png" alt="Logo"></p><p><img src="secret.txt" alt="T"></p><p><img src="http://example.com/x.png" alt="Web"></p>"#;
|
||||
@@ -6060,11 +6056,11 @@ fn scratch_dir(name: &str) -> std::path::PathBuf {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn tiny_png() -> Vec<u8> {
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(2, 2, image::Rgba([200, 30, 30, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
png
|
||||
wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(2, 2, image::Rgba([200, 30, 30, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -8276,7 +8272,7 @@ fn insert_icon_places_a_picture_of_the_requested_size_that_saves_and_recolours()
|
||||
assert_eq!((r["width"].as_f64(), r["height"].as_f64(), r["icon"].as_str()), (Some(48.0), Some(48.0), Some("heart")));
|
||||
let Some((_, InlineObject::Image { media, w, h, alt, .. })) = crate::cmd::objects::selected(&s) else { panic!("no picture") };
|
||||
assert_eq!((w, h, alt.as_str()), (48.0, 48.0, "Heart"));
|
||||
let png = image::load_from_memory(&s.doc.media[&media]).unwrap().to_rgba8();
|
||||
let png = wordcraft_geom::picture::decode_memory(&s.doc.media[&media]).unwrap().to_rgba8();
|
||||
assert_eq!(png.dimensions(), (200, 200), "about 300 pixels per inch");
|
||||
let inked: Vec<_> = png.pixels().filter(|p| p.0[3] == 255).collect();
|
||||
assert!(!inked.is_empty() && inked.iter().all(|p| p.0[..3] == [0xC0, 0, 0]));
|
||||
@@ -8288,7 +8284,7 @@ fn insert_icon_places_a_picture_of_the_requested_size_that_saves_and_recolours()
|
||||
// Icon Color repaints it, keeping the transparency.
|
||||
run(&mut s, "picture.iconColor", json!({"color": "1F4E79"}));
|
||||
let Some((_, InlineObject::Image { media: m2, .. })) = crate::cmd::objects::selected(&s) else { panic!("no picture") };
|
||||
let png2 = image::load_from_memory(&s.doc.media[&m2]).unwrap().to_rgba8();
|
||||
let png2 = wordcraft_geom::picture::decode_memory(&s.doc.media[&m2]).unwrap().to_rgba8();
|
||||
assert!(png2.pixels().zip(png.pixels()).all(|(a, b)| a.0[3] == b.0[3] && (a.0[3] == 0 || a.0[..3] == [0x1F, 0x4E, 0x79])));
|
||||
assert!(s.run("picture.iconColor", &json!({})).is_err());
|
||||
}
|
||||
@@ -9839,7 +9835,6 @@ fn english_dialects_survive_a_docx_round_trip() {
|
||||
if no_language_data("english_dialects_survive_a_docx_round_trip") {
|
||||
return;
|
||||
}
|
||||
use std::io::Read;
|
||||
let mut s = s();
|
||||
run(&mut s, "document.setText", json!({"text": "Colour and color. Centre and center."}));
|
||||
run(&mut s, "select.all", json!({}));
|
||||
@@ -9849,12 +9844,8 @@ fn english_dialects_survive_a_docx_round_trip() {
|
||||
let before = spelling_with_first_suggestion(&mut s);
|
||||
assert_eq!(before, pairs(&[("color", "colour"), ("Centre", "Center")]));
|
||||
let bytes = crate::io::save_bytes("british.docx", &s.doc).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut part = |name: &str| {
|
||||
let mut xml = String::new();
|
||||
zip.by_name(name).unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
};
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let mut part = |name: &str| String::from_utf8(zip.read(name, 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(part("word/styles.xml").contains(r#"w:val="en-GB""#), "the default language is written");
|
||||
assert!(part("word/document.xml").contains(r#"w:val="en-US""#), "the run's language is written");
|
||||
let doc = crate::io::open_bytes("british.docx", &bytes).unwrap();
|
||||
@@ -10335,7 +10326,6 @@ fn select_graphic(s: &mut Session) {
|
||||
/// reopens as the same editable graphic; an edited data part stays read-only.
|
||||
#[test]
|
||||
fn smart_art_round_trips_through_docx() {
|
||||
use std::io::Read;
|
||||
let mut s = s();
|
||||
let r = run(
|
||||
&mut s,
|
||||
@@ -10345,12 +10335,11 @@ fn smart_art_round_trips_through_docx() {
|
||||
assert_eq!(r["smartArt"]["layout"], "hierarchy");
|
||||
let spec = cmd::smart_art::selected_smart_art(&s).expect("selected after insert");
|
||||
let bytes = crate::io::save_bytes("sa.docx", &s.doc).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
for part in ["data1", "layout1", "quickStyle1", "colors1", "drawing1"] {
|
||||
assert!(zip.by_name(&format!("word/diagrams/{part}.xml")).is_ok(), "{part} written");
|
||||
assert!(zip.contains(&format!("word/diagrams/{part}.xml")), "{part} written");
|
||||
}
|
||||
let mut ct = String::new();
|
||||
zip.by_name("[Content_Types].xml").unwrap().read_to_string(&mut ct).unwrap();
|
||||
let ct = String::from_utf8(zip.read("[Content_Types].xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(ct.contains("diagramDrawing+xml") && ct.contains("diagramData+xml"), "{ct}");
|
||||
let mut s2 = Session::new(crate::io::open_bytes("sa.docx", &bytes).unwrap());
|
||||
select_graphic(&mut s2);
|
||||
@@ -10362,20 +10351,17 @@ fn smart_art_round_trips_through_docx() {
|
||||
select_graphic(&mut s3);
|
||||
assert_eq!(cmd::smart_art::selected_smart_art(&s3).map(|g| g.layout), Some(wordcraft_doc::smart_art::SmartArtLayout::Venn));
|
||||
// A data part another program rewrote: shown, not editable.
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&again)).unwrap();
|
||||
let mut out = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for i in 0..zip.len() {
|
||||
let mut f = zip.by_index(i).unwrap();
|
||||
let name = f.name().to_string();
|
||||
let mut buf = Vec::new();
|
||||
f.read_to_end(&mut buf).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&again, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let names: Vec<String> = zip.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = craft_zip::Writer::new();
|
||||
for name in names {
|
||||
let mut buf = zip.read(&name, 16 << 20).unwrap().unwrap();
|
||||
if name.ends_with("data1.xml") {
|
||||
buf = String::from_utf8(buf).unwrap().replace("<dgm:bg/>", "<dgm:bg><a:noFill/></dgm:bg>").into_bytes();
|
||||
}
|
||||
out.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut out, &buf).unwrap();
|
||||
out.add(&name, &buf, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
let foreign = out.finish().unwrap().into_inner();
|
||||
let foreign = out.finish().unwrap();
|
||||
let mut s4 = Session::new(crate::io::open_bytes("sa.docx", &foreign).unwrap());
|
||||
select_graphic(&mut s4);
|
||||
assert!(cmd::smart_art::selected_smart_art(&s4).is_none());
|
||||
@@ -10641,10 +10627,11 @@ fn outline_many_collapsed_headings_stay_fast_after_edits() {
|
||||
#[test]
|
||||
fn imported_pictures_fit_the_text_width() {
|
||||
let png = |w: u32, h: u32| {
|
||||
let mut b = Vec::new();
|
||||
image::RgbaImage::from_pixel(w, h, image::Rgba([200, 30, 30, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut b), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let b = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(w, h, image::Rgba([200, 30, 30, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
cmd::insert::base64_encode(&b)
|
||||
};
|
||||
let sizes = |doc: &wordcraft_doc::Document| -> Vec<(f32, f32)> {
|
||||
@@ -11245,8 +11232,11 @@ fn picture_change_keeps_the_replacement_format() {
|
||||
let dir = scratch_dir("picture-change-format");
|
||||
let first = dir.join("first.png");
|
||||
std::fs::write(&first, tiny_png()).unwrap();
|
||||
let mut jpeg = Vec::new();
|
||||
image::RgbImage::from_pixel(2, 2, image::Rgb([30, 30, 200])).write_to(&mut std::io::Cursor::new(&mut jpeg), image::ImageFormat::Jpeg).unwrap();
|
||||
let jpeg = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbImage::from_pixel(2, 2, image::Rgb([30, 30, 200])).clone()),
|
||||
image::ImageFormat::Jpeg,
|
||||
)
|
||||
.unwrap();
|
||||
let second = dir.join("second.jpg");
|
||||
std::fs::write(&second, &jpeg).unwrap();
|
||||
|
||||
@@ -11271,10 +11261,16 @@ fn picture_change_keeps_the_replacement_format() {
|
||||
#[test]
|
||||
fn picture_change_keeps_width_and_takes_new_aspect() {
|
||||
let dir = scratch_dir("picture-change-aspect");
|
||||
let mut wide = Vec::new();
|
||||
image::RgbImage::from_pixel(40, 20, image::Rgb([200, 30, 30])).write_to(&mut std::io::Cursor::new(&mut wide), image::ImageFormat::Png).unwrap();
|
||||
let mut tall = Vec::new();
|
||||
image::RgbImage::from_pixel(30, 60, image::Rgb([30, 30, 200])).write_to(&mut std::io::Cursor::new(&mut tall), image::ImageFormat::Png).unwrap();
|
||||
let wide = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbImage::from_pixel(40, 20, image::Rgb([200, 30, 30])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
let tall = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbImage::from_pixel(30, 60, image::Rgb([30, 30, 200])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
let (first, second) = (dir.join("wide.png"), dir.join("tall.png"));
|
||||
std::fs::write(&first, &wide).unwrap();
|
||||
std::fs::write(&second, &tall).unwrap();
|
||||
@@ -11407,7 +11403,6 @@ fn links_to_places_in_this_document() {
|
||||
/// formula field's result is edited (#1037). The first field's result is split across the field
|
||||
/// end; the second has its whole result deleted.
|
||||
fn tracked_field_result_docx() -> Vec<u8> {
|
||||
use std::io::Write;
|
||||
const WHO: &str = r#"w:author="Tester" w:date="2026-10-12T00:00:00Z""#;
|
||||
let field = |instr: &str, inside: &str, after: &str| {
|
||||
format!(
|
||||
@@ -11421,7 +11416,7 @@ fn tracked_field_result_docx() -> Vec<u8> {
|
||||
let xml = format!(
|
||||
r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p>{p1}</w:p><w:p>{p2}</w:p></w:body></w:document>"#
|
||||
);
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, value) in [
|
||||
("word/document.xml", xml.as_str()),
|
||||
(
|
||||
@@ -11429,10 +11424,9 @@ fn tracked_field_result_docx() -> Vec<u8> {
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="main" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#,
|
||||
),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(value.as_bytes()).unwrap();
|
||||
zip.add(name, value.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -11446,9 +11440,8 @@ fn tracked_deletion_inside_a_field_result_is_a_revision_and_is_saved() {
|
||||
assert_eq!(p.text.replace(wordcraft_doc::para::OBJ, ""), "$150.00$140.00");
|
||||
// Saving keeps the deletion inside the result, with its text as delText.
|
||||
let bytes = wordcraft_docx::write(&doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut xml).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(z.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let sep = xml.find(r#"w:fldCharType="separate""#).unwrap();
|
||||
let end = xml.find(r#"w:fldCharType="end""#).unwrap();
|
||||
let inside = &xml[sep..end];
|
||||
@@ -12053,13 +12046,11 @@ fn opened_documents_with_their_own_page_size_keep_it() {
|
||||
fn html_converted_to_docx_keeps_bold_headers_and_right_aligned_cells() {
|
||||
// #1381: `wordcraft-cli convert S05.html out.docx`: the <th> runs are bold and the
|
||||
// `text-align:right` cells' paragraphs carry `w:jc w:val="right"`.
|
||||
use std::io::Read;
|
||||
let mut s = s();
|
||||
run(&mut s, "file.open", json!({"path": "S05.html", "data": crate::cmd::insert::base64_encode(SAMPLE_1381.as_bytes())}));
|
||||
let bytes = crate::io::save_bytes("out.docx", &s.doc).unwrap();
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let mut zip = craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let xml = String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let cell = |text: &str| {
|
||||
let at = xml.find(&format!(">{text}<")).unwrap_or_else(|| panic!("{text} in {xml}"));
|
||||
let start = xml[..at].rfind("<w:tc>").or_else(|| xml[..at].rfind("<w:tc ")).unwrap();
|
||||
@@ -12204,7 +12195,6 @@ fn typing_after_a_note_number_is_not_superscript() {
|
||||
/// an ODT save, so Reject All (undo the deletion, then the insertion) and Accept All both remove it.
|
||||
#[test]
|
||||
fn odt_inserted_then_deleted_text_round_trips() {
|
||||
use std::io::Write;
|
||||
let ns = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/""#;
|
||||
let info = |who: &str| format!("<office:change-info><dc:creator>{who}</dc:creator><dc:date>2024-03-01T10:00:00</dc:date></office:change-info>");
|
||||
let content = format!(
|
||||
@@ -12212,13 +12202,11 @@ fn odt_inserted_then_deleted_text_round_trips() {
|
||||
info("Alice"),
|
||||
info("Bob")
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap().into_inner()).unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap()).unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &wordcraft_formats::odt::export(&doc).unwrap()).unwrap();
|
||||
for cmd in ["review.rejectAll", "review.acceptAll"] {
|
||||
let mut s = Session::new(doc.clone());
|
||||
@@ -12231,7 +12219,6 @@ fn odt_inserted_then_deleted_text_round_trips() {
|
||||
/// Reject All turns back into the original and Accept All into the edited text.
|
||||
#[test]
|
||||
fn odt_tracked_changes_accept_and_reject_all() {
|
||||
use std::io::Write;
|
||||
let ns = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/""#;
|
||||
let info = |who: &str| format!("<office:change-info><dc:creator>{who}</dc:creator><dc:date>2024-03-01T10:00:00</dc:date></office:change-info>");
|
||||
let content = format!(
|
||||
@@ -12239,13 +12226,11 @@ fn odt_tracked_changes_accept_and_reject_all() {
|
||||
info("Alice"),
|
||||
info("Bob")
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let bytes = z.finish().unwrap().into_inner();
|
||||
let bytes = z.finish().unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &bytes).unwrap();
|
||||
|
||||
let mut sr = Session::new(doc.clone());
|
||||
@@ -12263,7 +12248,6 @@ fn odt_tracked_changes_accept_and_reject_all() {
|
||||
/// inserted, so Reject All keeps the two paragraphs (after an ODT save, too).
|
||||
#[test]
|
||||
fn odt_deleted_mark_inside_an_insertion_survives_reject_all() {
|
||||
use std::io::Write;
|
||||
let ns = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/""#;
|
||||
let info = |who: &str| format!("<office:change-info><dc:creator>{who}</dc:creator><dc:date>2024-03-01T10:00:00</dc:date></office:change-info>");
|
||||
let content = format!(
|
||||
@@ -12271,13 +12255,11 @@ fn odt_deleted_mark_inside_an_insertion_survives_reject_all() {
|
||||
info("Alice"),
|
||||
info("Bob")
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap().into_inner()).unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap()).unwrap();
|
||||
let saved = crate::io::open_bytes("t.odt", &wordcraft_formats::odt::export(&doc).unwrap()).unwrap();
|
||||
for d in [doc, saved] {
|
||||
let mut s = Session::new(d);
|
||||
@@ -13091,9 +13073,9 @@ fn an_odt_charts_paragraph_is_written_as_read_after_drawing_it() {
|
||||
run(&mut s, "insert.chart", json!({"type": "column"}));
|
||||
let first = wordcraft_formats::odt::export(&s.doc).unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &first).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&doc).unwrap())).unwrap();
|
||||
let mut content = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("content.xml").unwrap(), &mut content).unwrap();
|
||||
let _z_bytes = wordcraft_formats::odt::export(&doc).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let content = String::from_utf8(z.read("content.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(content.contains(r#"draw:name="Chart1""#) && !content.contains("wcChart"), "{content}");
|
||||
}
|
||||
|
||||
@@ -13109,18 +13091,16 @@ fn an_odts_picture_wider_than_the_text_keeps_its_size() {
|
||||
r#"<office:document-content {ns}><office:body><office:text><text:p><draw:frame draw:name="Rule" text:anchor-type="paragraph" svg:x="-1cm" svg:width="18cm" svg:height="0.2cm"><draw:image xlink:href="Pictures/r.png"/></draw:frame>Text</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let png = [b"\x89PNG\r\n\x1a\n".as_slice(), &[7u8; 64]].concat();
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [
|
||||
("mimetype", b"application/vnd.oasis.opendocument.text".as_slice()),
|
||||
("content.xml", content.as_bytes()),
|
||||
("styles.xml", styles.as_bytes()),
|
||||
("Pictures/r.png", &png),
|
||||
] {
|
||||
z.start_file(name, o).unwrap();
|
||||
std::io::Write::write_all(&mut z, data).unwrap();
|
||||
z.add(name, data, craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap().into_inner()).unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap()).unwrap();
|
||||
let width = doc.body[0]
|
||||
.as_para()
|
||||
.unwrap()
|
||||
@@ -13129,10 +13109,10 @@ fn an_odts_picture_wider_than_the_text_keeps_its_size() {
|
||||
.find_map(|o| if let wordcraft_doc::para::InlineObject::Image { w, .. } = o { Some(*w) } else { None });
|
||||
let want = 18.0 * 72.0 / 2.54;
|
||||
assert!(width.is_some_and(|w| (w - want).abs() < 0.1), "{width:?}, not fitted to the text");
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(wordcraft_formats::odt::export(&doc).unwrap())).unwrap();
|
||||
let mut saved = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("content.xml").unwrap(), &mut saved).unwrap();
|
||||
assert!(saved.contains(r#"draw:name="Rule""#) && saved.contains(r#"svg:width="18cm""#), "written as read: {saved}");
|
||||
let _z_bytes = wordcraft_formats::odt::export(&doc).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&_z_bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let saved = String::from_utf8(z.read("content.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert!(saved.contains(r#"svg:width="510.24pt""#), "written as read: {saved}");
|
||||
}
|
||||
|
||||
/// Numbering or a new style makes the styles be written from the model on save: a document in
|
||||
@@ -13146,14 +13126,12 @@ fn an_odts_language_survives_a_save_after_numbering_or_a_new_style() {
|
||||
let content = format!(
|
||||
r#"<office:document-content {ns}><office:body><office:text><text:p>Hallo</text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str()), ("styles.xml", styles.as_str())]
|
||||
{
|
||||
z.start_file(name, o).unwrap();
|
||||
std::io::Write::write_all(&mut z, data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let odt = z.finish().unwrap().into_inner();
|
||||
let odt = z.finish().unwrap();
|
||||
for (command, params) in [("para.numbering", json!({})), ("styles.create", json!({"name": "Notiz"}))] {
|
||||
let mut s = Session::new(crate::io::open_bytes("t.odt", &odt).unwrap());
|
||||
assert_eq!(cmd::default_lang(&s).as_deref(), Some("de-DE"), "the document's, not English");
|
||||
@@ -13167,7 +13145,6 @@ fn an_odts_language_survives_a_save_after_numbering_or_a_new_style() {
|
||||
/// A Word package whose body holds `body` and, as `rIdC`, the chunk part `word/afchunk.{ext}`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn docx_with_chunk(body: &str, ext: &str, content_type: &str, chunk: &[u8]) -> Vec<u8> {
|
||||
use std::io::Write;
|
||||
let files: [(String, Vec<u8>); 4] = [
|
||||
(
|
||||
"[Content_Types].xml".into(),
|
||||
@@ -13183,12 +13160,11 @@ fn docx_with_chunk(body: &str, ext: &str, content_type: &str, chunk: &[u8]) -> V
|
||||
format!(r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>{body}</w:body></w:document>"#).into_bytes(),
|
||||
),
|
||||
];
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, bytes) in files.iter().map(|(n, b)| (n.as_str(), b.as_slice())).chain([(format!("word/afchunk.{ext}").as_str(), chunk)]) {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
zw.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A Single File Web Page under a Word or RTF name opens as one, like the web pages of #2853.
|
||||
@@ -13417,24 +13393,27 @@ fn an_odts_picture_is_fitted_only_when_it_states_no_size() {
|
||||
r#"<office:document-styles {ns}><office:automatic-styles><style:page-layout style:name="pm"><style:page-layout-properties fo:page-width="14.8cm" fo:page-height="21cm" 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="pm"/></office:master-styles></office:document-styles>"#
|
||||
);
|
||||
let mut png = std::io::Cursor::new(Vec::new());
|
||||
image::RgbImage::new(1000, 500).write_to(&mut png, image::ImageFormat::Png).unwrap();
|
||||
std::io::Write::write_all(
|
||||
&mut png,
|
||||
&wordcraft_geom::picture::encode_format(&image::DynamicImage::from(image::RgbImage::new(1000, 500).clone()), image::ImageFormat::Png)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let png = png.into_inner();
|
||||
let open = |attrs: &str| {
|
||||
let content = format!(
|
||||
r#"<office:document-content {ns}><office:body><office:text><text:p><draw:frame text:anchor-type="as-char"{attrs}><draw:image xlink:href="Pictures/a.png"/></draw:frame></text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [
|
||||
("mimetype", b"application/vnd.oasis.opendocument.text".as_slice()),
|
||||
("content.xml", content.as_bytes()),
|
||||
("styles.xml", styles.as_bytes()),
|
||||
("Pictures/a.png", &png),
|
||||
] {
|
||||
z.start_file(name, o).unwrap();
|
||||
std::io::Write::write_all(&mut z, data).unwrap();
|
||||
z.add(name, data, craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap().into_inner()).unwrap();
|
||||
let doc = crate::io::open_bytes("t.odt", &z.finish().unwrap()).unwrap();
|
||||
doc.body[0]
|
||||
.as_para()
|
||||
.unwrap()
|
||||
|
||||
@@ -273,10 +273,15 @@ fn typing_kurdish_in_new_document_advances_caret_smoothly() {
|
||||
|
||||
/// The runs of the first paragraph in a saved DOCX: (has `w:rtl`, text).
|
||||
fn saved_runs(s: &Session) -> Vec<(bool, String)> {
|
||||
use std::io::Read;
|
||||
let bytes = crate::io::save_bytes("x.docx", &s.doc).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap().by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
let xml = String::from_utf8(
|
||||
craft_zip::Archive::open(&bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 })
|
||||
.unwrap()
|
||||
.read("word/document.xml", 16 << 20)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let para = xml.split("<w:body>").nth(1).unwrap().split("</w:p>").next().unwrap().to_string();
|
||||
para.split("<w:r>")
|
||||
.skip(1)
|
||||
|
||||
@@ -149,12 +149,11 @@ fn docx_embedding(family: &str, font: Option<(&[u8], &str)>, text: &str) -> Vec<
|
||||
}
|
||||
table.push_str("</w:font></w:fonts>");
|
||||
files.push(("word/fontTable.xml".into(), table.into_bytes()));
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, bytes) in files {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut zip, &bytes).unwrap();
|
||||
zip.add(&name, &bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -200,7 +200,6 @@ fn enter_on_an_empty_nested_item_moves_it_up_a_level_first() {
|
||||
/// up a level and then ends the list, as with direct numbering.
|
||||
#[test]
|
||||
fn enter_on_an_empty_item_numbered_by_its_style_moves_it_up_a_level() {
|
||||
use std::io::Write;
|
||||
let w = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main""#;
|
||||
let style = |id: &str, name: &str, ilvl: u8| {
|
||||
format!(
|
||||
@@ -227,8 +226,7 @@ fn enter_on_an_empty_item_numbered_by_its_style_moves_it_up_a_level() {
|
||||
let doc_rels =
|
||||
format!("<Relationships {rels_ns}>{}{}</Relationships>", rel("rId1", "styles", "styles.xml"), rel("rId2", "numbering", "numbering.xml"));
|
||||
let ct = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/></Types>"#;
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default();
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [
|
||||
("[Content_Types].xml", ct),
|
||||
("_rels/.rels", main.as_str()),
|
||||
@@ -237,10 +235,9 @@ fn enter_on_an_empty_item_numbered_by_its_style_moves_it_up_a_level() {
|
||||
("word/styles.xml", styles.as_str()),
|
||||
("word/numbering.xml", numbering.as_str()),
|
||||
] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let doc = crate::io::open_bytes("t.docx", &z.finish().unwrap().into_inner()).unwrap();
|
||||
let doc = crate::io::open_bytes("t.docx", &z.finish().unwrap()).unwrap();
|
||||
let mut s = Session::new(doc);
|
||||
// The list level each paragraph shows, wherever its numbering comes from.
|
||||
let levels = |s: &Session| -> Vec<Option<u8>> {
|
||||
@@ -2433,12 +2430,11 @@ fn two_section_docx(kind: &str) -> Vec<u8> {
|
||||
),
|
||||
),
|
||||
];
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, data) in files {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut zip, data.as_bytes()).unwrap();
|
||||
zip.add(name, data.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
/// #2127: Backspace just after a section break, or Delete just before it, removes the break, as
|
||||
@@ -2504,12 +2500,11 @@ fn break_on_the_last_paragraph_docx(then: Option<&str>) -> Vec<u8> {
|
||||
),
|
||||
),
|
||||
];
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
for (name, data) in files {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
std::io::Write::write_all(&mut zip, data.as_bytes()).unwrap();
|
||||
zip.add(name, data.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
/// #2127: Delete at the end of the last paragraph removes a section break on its mark too,
|
||||
|
||||
@@ -200,3 +200,12 @@ fn wordart_reflection_and_outline_none_options() {
|
||||
assert_eq!(outline.color, None);
|
||||
assert_eq!(outline.width, 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptive_feature_labels_keep_existing_command_ids() {
|
||||
let s = Session::new(Document::new());
|
||||
for (id, label) in [("insert.smartArt", "Diagram"), ("insert.wordArt", "Decorative Text"), ("wordArt.style", "Decorative Text Styles")] {
|
||||
assert_eq!(s.registry.get(id).map(|spec| spec.label), Some(label));
|
||||
}
|
||||
assert!(s.registry.get("smartArt.colors").unwrap().location.starts_with("SmartArt Design › "));
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ use std::io::Write;
|
||||
use wordcraft_doc::{InlineObject, StoryRef};
|
||||
|
||||
fn zip(entries: &[(&str, &str)]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, body) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(body.as_bytes()).unwrap();
|
||||
zw.add(name, body.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// A field run: its code, its cached result, and whether it carries `w:fldLock`.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//! macro-enabled document keeps its VBA project (as opaque bytes) when it is saved again.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use wordcraft_doc::Document;
|
||||
use wordcraft_engine::io::{open_bytes, save_bytes};
|
||||
@@ -18,26 +17,24 @@ const REL_VBA_DATA: &str = "http://schemas.microsoft.com/office/2006/relationshi
|
||||
const VBA_DATA: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><wne:vbaSuppData xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"><wne:mcds><wne:mcd wne:macroName="PROJECT.NEWMACROS.HELLO" wne:name="Project.NewMacros.Hello" wne:bEncrypt="00" wne:cmg="56"/></wne:mcds></wne:vbaSuppData>"#;
|
||||
|
||||
fn unzip(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
|
||||
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
|
||||
let mut z = craft_zip::Archive::open(bytes, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).expect("zip");
|
||||
let names: Vec<String> = z.names().map(|n| n.into_owned()).collect();
|
||||
let mut out = BTreeMap::new();
|
||||
for i in 0..z.len() {
|
||||
let mut f = z.by_index(i).expect("entry");
|
||||
let mut b = Vec::new();
|
||||
f.read_to_end(&mut b).expect("read entry");
|
||||
out.insert(f.name().to_string(), b);
|
||||
for name in names {
|
||||
let b = z.read(&name, 16 << 20).expect("read entry").expect("entry");
|
||||
out.insert(name, b);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn zip(entries: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
// Content types first, as Word writes them.
|
||||
let order = entries.iter().filter(|(n, _)| *n == "[Content_Types].xml").chain(entries.iter().filter(|(n, _)| *n != "[Content_Types].xml"));
|
||||
for (name, bytes) in order {
|
||||
zw.start_file(name.as_str(), zip::write::SimpleFileOptions::default()).expect("start");
|
||||
zw.write_all(bytes).expect("write");
|
||||
zw.add(name.as_str(), bytes, craft_zip::Compression::Deflated).expect("write");
|
||||
}
|
||||
zw.finish().expect("finish").into_inner()
|
||||
zw.finish().expect("finish")
|
||||
}
|
||||
|
||||
fn text(files: &BTreeMap<String, Vec<u8>>, name: &str) -> String {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! WordCraft draws and edits: Chart Design's commands change it like a chart made here. The ODT
|
||||
//! is original and synthetic, shaped like LibreOffice's.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use serde_json::json;
|
||||
use wordcraft_doc::chart::ChartType;
|
||||
use wordcraft_doc::para::InlineObject;
|
||||
@@ -17,14 +15,12 @@ fn odt() -> Vec<u8> {
|
||||
);
|
||||
// A well-formed chart part, its prefixes declared as LibreOffice writes them.
|
||||
let chart = r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"><office:body><office:chart><chart:chart chart:class="chart:line"><chart:title><text:p>Visits</text:p></chart:title><chart:plot-area><chart:axis chart:dimension="x"><chart:categories table:cell-range-address="local-table.$A$2:.$A$4"/></chart:axis><chart:series chart:values-cell-range-address="local-table.$B$2:.$B$4" chart:label-cell-address="local-table.$B$1"/></chart:plot-area><table:table table:name="local-table"><table:table-header-rows><table:table-row><table:table-cell/><table:table-cell office:value-type="string"><text:p>Site</text:p></table:table-cell></table:table-row></table:table-header-rows><table:table-rows><table:table-row><table:table-cell office:value-type="string"><text:p>Mon</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="3"/></table:table-row><table:table-row><table:table-cell office:value-type="string"><text:p>Tue</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="5"/></table:table-row><table:table-row><table:table-cell office:value-type="string"><text:p>Wed</text:p></table:table-cell><table:table-cell office:value-type="float" office:value="4"/></table:table-row></table:table-rows></table:table></chart:chart></office:chart></office:body></office:document-content>"#;
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str()), ("Object 1/content.xml", chart)]
|
||||
{
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
/// The document's charts: their type, title, and whether they're drawn.
|
||||
@@ -55,20 +51,18 @@ fn an_odt_chart_opens_drawn_and_editable() {
|
||||
fn a_chart_in_a_group_opens_drawn() {
|
||||
// The chart is a member of a drawing group (`draw:g`): it is drawn too.
|
||||
let odt = odt();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&odt)).unwrap();
|
||||
let chart = std::io::read_to_string(z.by_name("Object 1/content.xml").unwrap()).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&odt, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let chart = String::from_utf8(z.read("Object 1/content.xml", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
let content = format!(
|
||||
r#"<office:document-content {NS}><office:body><office:text><text:p>Group <draw:g text:anchor-type="as-char"><draw:frame svg:x="0in" svg:y="0in" svg:width="3in" svg:height="2in"><draw:object xlink:href="./Object 1"/></draw:frame><draw:rect svg:x="3in" svg:y="0in" svg:width="1in" svg:height="1in"/></draw:g></text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut w = craft_zip::Writer::new();
|
||||
for (name, data) in
|
||||
[("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str()), ("Object 1/content.xml", chart.as_str())]
|
||||
{
|
||||
w.start_file(name, o).unwrap();
|
||||
w.write_all(data.as_bytes()).unwrap();
|
||||
w.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let doc = io::open_bytes("group.odt", &w.finish().unwrap().into_inner()).unwrap();
|
||||
let doc = io::open_bytes("group.odt", &w.finish().unwrap()).unwrap();
|
||||
let members: Vec<bool> = doc
|
||||
.body
|
||||
.iter()
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! the bookmarked text (a bookmark over text, `text:bookmark-start`…`-end`, not a point). The
|
||||
//! ODT is original and synthetic, shaped like LibreOffice's.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use serde_json::json;
|
||||
use wordcraft_engine::{Session, io};
|
||||
|
||||
@@ -11,13 +9,11 @@ const NS: &str = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1
|
||||
|
||||
fn odt(body: &str) -> Vec<u8> {
|
||||
let content = format!(r#"<office:document-content {NS}><office:body><office:text>{body}</office:text></office:body></office:document-content>"#);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn text(s: &Session) -> Vec<String> {
|
||||
@@ -87,13 +83,11 @@ fn a_flat_document_opens_and_saves_by_its_extension() {
|
||||
fn a_user_field_updates_from_its_property() {
|
||||
// An ODT user field opens as `DOCPROPERTY`: updating fields shows the property's value.
|
||||
let content = r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"><office:body><office:text><text:user-field-decls><text:user-field-decl text:name="Owner" office:value-type="string" office:string-value="Ada"/></text:user-field-decls><text:p>By <text:user-field-get text:name="Owner">stale</text:user-field-get>.</text:p></office:text></office:body></office:document-content>"#;
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content)] {
|
||||
z.start_file(name, o).unwrap();
|
||||
std::io::Write::write_all(&mut z, data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let mut s = Session::new(io::open_bytes("user.odt", &z.finish().unwrap().into_inner()).unwrap());
|
||||
let mut s = Session::new(io::open_bytes("user.odt", &z.finish().unwrap()).unwrap());
|
||||
s.run("references.updateFields", &json!({"all": true})).unwrap();
|
||||
assert_eq!(results(&s), ["Ada"]);
|
||||
}
|
||||
|
||||
@@ -2,21 +2,17 @@
|
||||
//! stay what they are when the document is saved as DOCX (`w:sdt`) and as ODT again. The ODT is
|
||||
//! original and synthetic, shaped like LibreOffice's.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::control::ControlKind;
|
||||
use wordcraft_doc::para::InlineObject;
|
||||
use wordcraft_engine::io;
|
||||
|
||||
fn odt() -> Vec<u8> {
|
||||
let content = r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"><office:body><office:text><office:forms><form:form form:name="Form"><form:listbox form:name="Colour" form:id="c1" form:dropdown="true"><form:option form:label="Red"/><form:option form:label="Green" form:current-selected="true"/></form:listbox><form:checkbox form:name="Agree" form:id="c2" form:current-state="unchecked"/></form:form></office:forms><text:p>Colour: <draw:control draw:control="c1"/> Agree: <draw:control draw:control="c2"/></text:p></office:text></office:body></office:document-content>"#;
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content)] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn kinds(doc: &wordcraft_doc::Document) -> Vec<&'static str> {
|
||||
@@ -36,8 +32,14 @@ fn odt_form_controls_become_content_controls_everywhere() {
|
||||
assert_eq!(text, "Colour: Green Agree: \u{2610}");
|
||||
// DOCX: Word content controls.
|
||||
let docx = io::save_bytes("form.docx", &doc).unwrap();
|
||||
let mut body = String::new();
|
||||
zip::ZipArchive::new(std::io::Cursor::new(&docx)).unwrap().by_name("word/document.xml").unwrap().read_to_string(&mut body).unwrap();
|
||||
let body = String::from_utf8(
|
||||
craft_zip::Archive::open(&docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 })
|
||||
.unwrap()
|
||||
.read("word/document.xml", 16 << 20)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(body.contains("<w:dropDownList") && body.contains("w14:checkbox"), "{body}");
|
||||
let again = io::open_bytes("form.docx", &docx).unwrap();
|
||||
assert_eq!(kinds(&again), ["dropDown", "checkBox"]);
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! the object behind: saved as ODT it is the spreadsheet again; saved as DOCX it is the picture
|
||||
//! alone, never ODF markup. The ODT is original and synthetic, shaped like LibreOffice's.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::para::InlineObject;
|
||||
use wordcraft_engine::io;
|
||||
|
||||
@@ -14,8 +12,7 @@ fn odt() -> Vec<u8> {
|
||||
r#"<office:document-content {NS}><office:body><office:text><text:p>Costs <draw:frame svg:width="3in" svg:height="1in"><draw:object xlink:href="./Object 1"/><draw:image xlink:href="./ObjectReplacements/Object 1"/></draw:frame></text:p></office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let sheet = r#"<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"><office:body><office:spreadsheet/></office:body></office:document-content>"#;
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
let entries: [(&str, &[u8]); 4] = [
|
||||
("mimetype", b"application/vnd.oasis.opendocument.text"),
|
||||
("content.xml", content.as_bytes()),
|
||||
@@ -23,17 +20,14 @@ fn odt() -> Vec<u8> {
|
||||
("ObjectReplacements/Object 1", b"VCLMTF not a picture WordCraft draws"),
|
||||
];
|
||||
for (name, data) in entries {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data).unwrap();
|
||||
z.add(name, data, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn entry(pkg: &[u8], name: &str) -> Option<String> {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(pkg)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name(name).ok()?.read_to_string(&mut s).ok()?;
|
||||
Some(s)
|
||||
let mut z = craft_zip::Archive::open(pkg, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
String::from_utf8(z.read(name, 16 << 20).ok()??).ok()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,9 +22,8 @@ fn an_odt_is_saved_and_opened_with_a_password() {
|
||||
fn an_odt_template_opens_and_saves_as_a_template_with_a_password_too() {
|
||||
let doc = io::open_bytes("plain.txt", b"Letterhead.").unwrap();
|
||||
let ott = io::save_bytes("letter.ott", &doc).unwrap();
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(&ott)).unwrap();
|
||||
let mut mime = String::new();
|
||||
std::io::Read::read_to_string(&mut z.by_name("mimetype").unwrap(), &mut mime).unwrap();
|
||||
let mut z = craft_zip::Archive::open(&ott, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
let mime = String::from_utf8(z.read("mimetype", 16 << 20).unwrap().unwrap()).unwrap();
|
||||
assert_eq!(mime, wordcraft_formats::odt::TEMPLATE_MIME);
|
||||
let opened = io::open_bytes("letter.ott", &ott).unwrap();
|
||||
assert_eq!(opened.body.iter().filter_map(|b| b.as_para()).map(|p| p.text.as_str()).collect::<Vec<_>>(), ["Letterhead."]);
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
//! whose automatic styles name a master page (`style:master-page-name`), one restarting page
|
||||
//! numbering (`style:page-number`), and a two-column `text:section`.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_engine::io;
|
||||
|
||||
const NS: &str = r#"xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0""#;
|
||||
@@ -33,20 +31,17 @@ fn odt() -> Vec<u8> {
|
||||
<text:p text:style-name="PBack">Closing words</text:p><text:p>Last line.</text:p>
|
||||
</office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", &content), ("styles.xml", &styles)] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
fn document_xml(docx: &[u8]) -> String {
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
let mut zip = craft_zip::Archive::open(docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
//! headings, with or without a title, and it is saved back as an ODF table of contents. The ODT
|
||||
//! is original and synthetic, shaped like LibreOffice's.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use serde_json::json;
|
||||
use wordcraft_engine::{Session, io};
|
||||
|
||||
@@ -26,13 +24,11 @@ fn odt(titled: bool) -> Vec<u8> {
|
||||
<text:h text:outline-level="3">Too deep</text:h><text:p>Last.</text:p>
|
||||
</office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
z.finish().unwrap().into_inner()
|
||||
z.finish().unwrap()
|
||||
}
|
||||
|
||||
/// The text of the body's paragraphs in the TOC styles (without an untitled table's field
|
||||
@@ -76,13 +72,11 @@ fn an_odt_index_updates_from_its_marks() {
|
||||
<text:p>Clay <text:alphabetical-index-mark text:string-value="kilns"/>is fired; <text:alphabetical-index-mark-start text:id="m"/>glazes<text:alphabetical-index-mark-end text:id="m"/> shine.</text:p>
|
||||
</office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let mut s = Session::new(io::open_bytes("index.odt", &z.finish().unwrap().into_inner()).unwrap());
|
||||
let mut s = Session::new(io::open_bytes("index.odt", &z.finish().unwrap()).unwrap());
|
||||
s.run("references.updateIndex", &json!({})).unwrap();
|
||||
let text: Vec<String> = s.doc.body.iter().filter_map(|b| b.as_para()).map(|p| p.text.replace(wordcraft_doc::para::OBJ, "")).collect();
|
||||
assert!(text.iter().any(|t| t.starts_with("glazes")), "{text:?}");
|
||||
@@ -99,15 +93,68 @@ fn an_odt_bibliography_updates_from_its_citations_sources() {
|
||||
<text:bibliography text:name="B"><text:bibliography-source/><text:index-body><text:p>stale</text:p></text:index-body></text:bibliography>
|
||||
</office:text></office:body></office:document-content>"#
|
||||
);
|
||||
let mut z = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let o = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let mut z = craft_zip::Writer::new();
|
||||
for (name, data) in [("mimetype", "application/vnd.oasis.opendocument.text"), ("content.xml", content.as_str())] {
|
||||
z.start_file(name, o).unwrap();
|
||||
z.write_all(data.as_bytes()).unwrap();
|
||||
z.add(name, data.as_bytes(), craft_zip::Compression::Stored).unwrap();
|
||||
}
|
||||
let mut s = Session::new(io::open_bytes("bib.odt", &z.finish().unwrap().into_inner()).unwrap());
|
||||
let mut s = Session::new(io::open_bytes("bib.odt", &z.finish().unwrap()).unwrap());
|
||||
s.run("references.updateIndex", &json!({})).unwrap();
|
||||
let text: Vec<String> = s.doc.body.iter().filter_map(|b| b.as_para()).map(|p| p.text.replace(wordcraft_doc::para::OBJ, "")).collect();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
//! Switch semantics: Microsoft MS-OI29500, Part 1 Section 17.16.5.51 (REF).
|
||||
//! All OOXML packages and document text are constructed in memory; no external documents.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use serde_json::json;
|
||||
use wordcraft_doc::{InlineObject, StoryRef};
|
||||
|
||||
@@ -11,12 +9,11 @@ const W: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2
|
||||
|
||||
/// Build an OOXML ZIP entirely in memory.
|
||||
fn zip(entries: &[(&str, &str)]) -> Vec<u8> {
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zw = craft_zip::Writer::new();
|
||||
for (name, body) in entries {
|
||||
zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(body.as_bytes()).unwrap();
|
||||
zw.add(name, body.as_bytes(), craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
zw.finish().unwrap()
|
||||
}
|
||||
|
||||
/// One synthetic numbering level.
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
//! page defaults, then `\sectd` + section words + the section's header/footer groups, a `\sect`
|
||||
//! after the last `\par` (or after a table), and a `\sectd` inside a footer's field result.
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use wordcraft_engine::io;
|
||||
|
||||
const RTF: &str = r"{\rtf1\ansi\ansicpg1252\deff0{\fonttbl{\f0 Times New Roman;}}
|
||||
@@ -28,10 +26,9 @@ const RTF: &str = r"{\rtf1\ansi\ansicpg1252\deff0{\fonttbl{\f0 Times New Roman;}
|
||||
\pard\plain Last line.\par}";
|
||||
|
||||
fn document_xml(docx: &[u8]) -> String {
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
|
||||
let mut xml = String::new();
|
||||
zip.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap();
|
||||
xml
|
||||
let mut zip = craft_zip::Archive::open(docx, craft_zip::Limits { max_entries: 10_000, max_total_bytes: 64 << 20 }).unwrap();
|
||||
|
||||
String::from_utf8(zip.read("word/document.xml", 16 << 20).unwrap().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -194,10 +194,11 @@ fn an_undecodable_page_background_picture_is_refused_for_pdf_and_png() {
|
||||
|
||||
#[test]
|
||||
fn a_decodable_page_background_picture_passes() {
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(4, 4, image::Rgba([200, 200, 255, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let png = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(4, 4, image::Rgba([200, 200, 255, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
let mut doc = Document::from_text("Hello");
|
||||
doc.media.insert("bg.png".into(), std::sync::Arc::new(png));
|
||||
doc.settings.page_picture = Some(wordcraft_doc::PagePicture { media: "bg.png".into(), fill_type: "stretch".into(), title: String::new() });
|
||||
@@ -208,10 +209,11 @@ fn a_decodable_page_background_picture_passes() {
|
||||
#[test]
|
||||
fn a_picture_wider_than_the_pdf_draws_is_refused_for_pdf_only() {
|
||||
// 40000 x 10: past the PDF's decode limit (30000), within what the page raster draws.
|
||||
let mut png = Vec::new();
|
||||
image::RgbaImage::from_pixel(40_000, 10, image::Rgba([9, 9, 9, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let png = wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(40_000, 10, image::Rgba([9, 9, 9, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap();
|
||||
let mut doc = Document::from_text("Hello");
|
||||
doc.media.insert("wide.png".into(), std::sync::Arc::new(png));
|
||||
doc.settings.page_picture = Some(wordcraft_doc::PagePicture { media: "wide.png".into(), fill_type: "stretch".into(), title: String::new() });
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Pictures a layout draws that the exporters can't decode (#6005): a DOCX opened, laid out, and
|
||||
//! asked which of its pictures would be grey boxes, and in which story.
|
||||
use std::io::Write;
|
||||
use wordcraft_doc::{Document, MissingPictureReason};
|
||||
use wordcraft_layout::display::DisplayOptions;
|
||||
use wordcraft_layout::{LayoutCache, LayoutOptions, StrictOutput, layout, media_check, undecodable_pictures};
|
||||
@@ -9,11 +8,11 @@ const NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/
|
||||
const REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
|
||||
fn png() -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
image::RgbaImage::from_pixel(4, 4, image::Rgba([20, 90, 200, 255]))
|
||||
.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
out
|
||||
wordcraft_geom::picture::encode_format(
|
||||
&image::DynamicImage::from(image::RgbaImage::from_pixel(4, 4, image::Rgba([20, 90, 200, 255])).clone()),
|
||||
image::ImageFormat::Png,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn pic(rid: &str) -> String {
|
||||
@@ -39,7 +38,7 @@ fn docx(header_media: &[u8], body_media: &[u8]) -> Vec<u8> {
|
||||
r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdI" Type="{REL}/image" Target="media/image2.bin"/></Relationships>"#
|
||||
);
|
||||
let types = r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="png" ContentType="image/png"/><Default Extension="bin" ContentType="application/octet-stream"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>"#;
|
||||
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
let mut zip = craft_zip::Writer::new();
|
||||
let hdr = format!(r#"<w:hdr {NS}><w:p>{}</w:p></w:hdr>"#, pic("rIdI"));
|
||||
for (name, bytes) in [
|
||||
("[Content_Types].xml", types.as_bytes()),
|
||||
@@ -51,10 +50,9 @@ fn docx(header_media: &[u8], body_media: &[u8]) -> Vec<u8> {
|
||||
("word/media/image2.bin", header_media),
|
||||
("word/media/body.png", body_media),
|
||||
] {
|
||||
zip.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(bytes).unwrap();
|
||||
zip.add(name, bytes, craft_zip::Compression::Deflated).unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
zip.finish().unwrap()
|
||||
}
|
||||
|
||||
fn report(d: &Document, pages: Option<std::ops::RangeInclusive<usize>>) -> Vec<(String, String, MissingPictureReason)> {
|
||||
|
||||
+15
-20
@@ -11,33 +11,28 @@ workspace = true
|
||||
[dependencies]
|
||||
wordcraft-doc = { workspace = true }
|
||||
wordcraft-docx = { workspace = true }
|
||||
craft-wordmodel = { workspace = true }
|
||||
craft-rtf = { workspace = true }
|
||||
craft-flow = { workspace = true }
|
||||
craft-odf = { workspace = true }
|
||||
craft-markdown = { workspace = true }
|
||||
craft-html = { workspace = true }
|
||||
craft-latex = { workspace = true }
|
||||
craft-math = { workspace = true }
|
||||
craft-mime = { workspace = true }
|
||||
craft-sheet = { workspace = true }
|
||||
craft-ods = { workspace = true }
|
||||
craft-xls = { workspace = true }
|
||||
craft-xlsx = { workspace = true }
|
||||
craft-zip = { workspace = true }
|
||||
wordcraft-geom = { workspace = true, features = ["image"] }
|
||||
wordcraft-metafile = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
image = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
cfb = { workspace = true }
|
||||
craft-metafile = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
chardetng = { workspace = true }
|
||||
log = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
# Password-protected ODT (ODF 1.3 Part 2 §3.4): AES-256-CBC, SHA-1/SHA-256, PBKDF2 (HMAC-SHA-1).
|
||||
aes = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
getrandom = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
miniz_oxide = { workspace = true }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
# The browser clock (`SystemTime::now()` panics on wasm32-unknown-unknown): ODT change dates.
|
||||
js-sys = "0.3"
|
||||
# The browser's `crypto.getRandomValues` for the salts and vectors of encrypted saves.
|
||||
getrandom = { workspace = true, features = ["wasm_js"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wordcraft-doc = { workspace = true, features = ["test-support"] }
|
||||
craft-image-codecs = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
|
||||
+35
-42
@@ -1,48 +1,41 @@
|
||||
//! HTML.
|
||||
//!
|
||||
//! Import is a tolerant tag-soup reader (no DOM): a tokenizer plus a stack of open elements with
|
||||
//! HTML's implicit closes for `p`, `li`, `tr`, `td`/`th` and headings. It understands `p`, `div`,
|
||||
//! `h1`–`h6`, `b`/`strong`, `i`/`em`, `u`, `s`/`strike`/`del`, `sup`/`sub`, `code`/`kbd`, `a`
|
||||
//! (links and `id`/`name` anchors), `br`, `hr`, `ul`/`ol`/`li` (nested), `table`/`tr`/`td`/`th`
|
||||
//! (colspan/rowspan), `img` (`data:` URIs, other sources through a caller's [`ImageLoader`]), `blockquote`, `pre`, `font`, `ruby`
|
||||
//! (`rb`/`rt`/`rp`: furigana and other phonetic guides, #1681), and the CSS
|
||||
//! properties `color`, `background-color`, `font-weight`, `font-style`, `font-size`,
|
||||
//! `font-family`, `text-decoration`, `vertical-align`, `text-align`, `white-space` and page
|
||||
//! breaks, and on paragraphs `margin`, `text-indent` and `line-height`. CSS comes from `style`
|
||||
//! attributes and from `style` elements' rules with simple selectors (`p`, `.note`, `p.note`),
|
||||
//! applied in cascade order (specificity, then source order, then the `style` attribute).
|
||||
//! Scripts are skipped; nesting is capped.
|
||||
//!
|
||||
//! Export writes clean semantic HTML with inline CSS, pictures as `data:` URIs and ruby as
|
||||
//! `<ruby>`.
|
||||
//! HTML import and export via `craft-html`.
|
||||
|
||||
use std::sync::Arc;
|
||||
use wordcraft_doc::{Document, SectionProps};
|
||||
|
||||
use wordcraft_doc::props::LineSpacing;
|
||||
use wordcraft_doc::props::VAlign;
|
||||
use wordcraft_doc::{Align, Document};
|
||||
pub use craft_html::doc::{ImageLoader, PAGE_FONT, css_len_pt, decode, decode_as, decode_entity, esc, export_flow, parse, parse_with, unescape};
|
||||
|
||||
use crate::model::{
|
||||
self, Cell, FBlock, FTable, Flow, Fmt, Inline, Kind, ListInfo, Meta, Para, Spacing, base64_encode, data_uri, make_img, mime_of, parse_color,
|
||||
};
|
||||
fn shared_section(page: &SectionProps) -> craft_wordmodel::SectionProps {
|
||||
let mut d = Document::new();
|
||||
d.last_section = page.clone();
|
||||
wordcraft_docx::to_shared_model(&d).map(|s| s.last_section).unwrap_or_default()
|
||||
}
|
||||
|
||||
mod builder;
|
||||
mod css;
|
||||
mod entity;
|
||||
mod export;
|
||||
mod import;
|
||||
mod lexer;
|
||||
mod math_ruby;
|
||||
pub fn import(html: &str) -> Document {
|
||||
wordcraft_docx::from_shared_model(craft_html::doc::import(html)).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub use builder::ImageLoader;
|
||||
pub use css::css_len_pt;
|
||||
pub use entity::{decode, decode_as, decode_entity, unescape};
|
||||
pub use export::{esc, export, export_flow, export_fragment, inlines_html};
|
||||
#[cfg(test)]
|
||||
use import::parse_in;
|
||||
pub use import::{PAGE_FONT, import, import_on, import_page, import_with, parse, parse_with};
|
||||
pub fn import_page(html: &str, images: ImageLoader<'_>) -> Document {
|
||||
wordcraft_docx::from_shared_model(craft_html::doc::import_page(html, images)).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use css::css_unescape;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub fn import_with(html: &str, images: ImageLoader<'_>) -> Document {
|
||||
wordcraft_docx::from_shared_model(craft_html::doc::import_with(html, images)).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn import_on(html: &str, images: ImageLoader<'_>, page: &SectionProps) -> Document {
|
||||
wordcraft_docx::from_shared_model(craft_html::doc::import_on(html, images, &shared_section(page))).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn export(doc: &Document) -> String {
|
||||
match wordcraft_docx::to_shared_model(doc) {
|
||||
Ok(shared) => craft_html::doc::export(&shared),
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_fragment(doc: &Document) -> String {
|
||||
match wordcraft_docx::to_shared_model(doc) {
|
||||
Ok(shared) => craft_html::doc::export_fragment(&shared),
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,167 +0,0 @@
|
||||
use super::builder::decls;
|
||||
use super::import::PAGE_FONT;
|
||||
use super::{Align, Fmt, model, parse_color};
|
||||
|
||||
/// A CSS length (or font-size keyword) in points; `None` when it is not one.
|
||||
pub fn css_len_pt(v: &str) -> Option<f32> {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
let num = |s: &str| s.trim().parse::<f32>().ok().filter(|x| x.is_finite());
|
||||
if let Some(x) = v.strip_suffix("pt") {
|
||||
num(x)
|
||||
} else if let Some(x) = v.strip_suffix("px") {
|
||||
num(x).map(|p| p * 0.75)
|
||||
} else if let Some(x) = v.strip_suffix("rem").or_else(|| v.strip_suffix("em")) {
|
||||
num(x).map(|p| p * 12.0)
|
||||
} else if let Some(x) = v.strip_suffix("in") {
|
||||
num(x).map(|p| p * 72.0)
|
||||
} else if let Some(x) = v.strip_suffix("cm") {
|
||||
num(x).map(|p| p * 72.0 / 2.54)
|
||||
} else if let Some(x) = v.strip_suffix("mm") {
|
||||
num(x).map(|p| p * 72.0 / 25.4)
|
||||
} else if let Some(x) = v.strip_suffix('%') {
|
||||
num(x).map(|p| p * 0.12)
|
||||
} else {
|
||||
match v.as_str() {
|
||||
"xx-small" => Some(7.0),
|
||||
"x-small" => Some(7.5),
|
||||
"small" | "smaller" => Some(10.0),
|
||||
"medium" => Some(12.0),
|
||||
"large" | "larger" => Some(13.5),
|
||||
"x-large" => Some(18.0),
|
||||
"xx-large" => Some(24.0),
|
||||
"xxx-large" => Some(36.0),
|
||||
_ => num(&v).map(|p| p * 0.75),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn strip_important(v: &str) -> &str {
|
||||
let s = v.trim();
|
||||
if let Some(pos) = s.rfind('!') {
|
||||
let after = s[pos + 1..].trim();
|
||||
if after.eq_ignore_ascii_case("important") {
|
||||
return s[..pos].trim();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Decode CSS escapes (CSS Syntax §4.3.7): `\` and one to six hex digits, with one whitespace
|
||||
/// after them, is that code point; `\` and any other character is that character. CR LF, CR and
|
||||
/// form feed count as one newline, as CSS reads them (§3.3).
|
||||
pub(super) fn css_unescape(s: &str) -> String {
|
||||
let mut o = String::with_capacity(s.len());
|
||||
let mut it = s.chars().peekable();
|
||||
// Takes a newline (CR LF as one) or other whitespace off the front, if there is one.
|
||||
let skip_space = |it: &mut std::iter::Peekable<std::str::Chars>| match it.peek() {
|
||||
Some('\r') => {
|
||||
it.next();
|
||||
it.next_if_eq(&'\n');
|
||||
}
|
||||
Some(' ' | '\t' | '\n' | '\u{C}') => {
|
||||
it.next();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
while let Some(c) = it.next() {
|
||||
if c != '\\' {
|
||||
o.push(c);
|
||||
continue;
|
||||
}
|
||||
let mut hex = String::new();
|
||||
while hex.len() < 6 && it.peek().is_some_and(char::is_ascii_hexdigit) {
|
||||
hex.extend(it.next());
|
||||
}
|
||||
if hex.is_empty() {
|
||||
// Before a newline it continues the line; at the end it is dropped.
|
||||
match it.peek() {
|
||||
Some('\r' | '\n' | '\u{C}') => skip_space(&mut it),
|
||||
Some(_) => o.extend(it.next()),
|
||||
None => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
skip_space(&mut it);
|
||||
let cp = u32::from_str_radix(&hex, 16).unwrap_or(0);
|
||||
o.push(char::from_u32(cp).filter(|c| *c != '\0').unwrap_or('\u{FFFD}'));
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// Apply a `style` attribute to formatting / block properties.
|
||||
pub(super) fn apply_style(
|
||||
css: &str,
|
||||
f: &mut Fmt,
|
||||
align: &mut Option<Align>,
|
||||
preserve: &mut bool,
|
||||
page_break: &mut bool,
|
||||
page_break_after: &mut bool,
|
||||
) {
|
||||
for (k, v) in decls(css) {
|
||||
let lv = v.to_ascii_lowercase();
|
||||
match k.as_str() {
|
||||
"font-weight" => f.bold = lv.starts_with("bold") || lv.parse::<u32>().is_ok_and(|w| w >= 600),
|
||||
"font-style" => f.italic = lv == "italic" || lv == "oblique",
|
||||
"text-decoration" | "text-decoration-line" => {
|
||||
if lv.contains("none") {
|
||||
f.underline = false;
|
||||
f.strike = false;
|
||||
}
|
||||
if lv.contains("underline") {
|
||||
f.underline = true;
|
||||
}
|
||||
if lv.contains("line-through") {
|
||||
f.strike = true;
|
||||
}
|
||||
}
|
||||
"color" => {
|
||||
if let Some(c) = parse_color(v) {
|
||||
f.color = Some(c);
|
||||
}
|
||||
}
|
||||
"background-color" | "background" => {
|
||||
if let Some(c) = v.split_whitespace().find_map(parse_color) {
|
||||
f.background = Some(c);
|
||||
}
|
||||
}
|
||||
"font-size" => {
|
||||
if let Some(p) = css_len_pt(v).filter(|p| *p >= 1.0 && *p <= 1638.0) {
|
||||
f.size = Some(p);
|
||||
}
|
||||
}
|
||||
"font-family" => {
|
||||
let fam = css_unescape(v.split(',').next().unwrap_or("").trim().trim_matches(['"', '\'']));
|
||||
if model::is_mono(&fam) || fam.eq_ignore_ascii_case("monospace") {
|
||||
f.code = true;
|
||||
} else if !fam.is_empty() && !matches!(fam.to_ascii_lowercase().as_str(), "serif" | "sans-serif" | "inherit" | "initial") {
|
||||
f.font = model::kept_font(&fam).or(f.font.take());
|
||||
} else if f.font.is_some() {
|
||||
// A generic family replaces a face the text has (a page's Times New Roman);
|
||||
// text without one keeps the document's font.
|
||||
match fam.to_ascii_lowercase().as_str() {
|
||||
"serif" => f.font = Some(PAGE_FONT.into()),
|
||||
"sans-serif" => f.font = Some("Arial".into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"vertical-align" => {
|
||||
f.sup = lv == "super";
|
||||
f.sub = lv == "sub";
|
||||
}
|
||||
"text-align" => {
|
||||
*align = match lv.as_str() {
|
||||
"center" => Some(Align::Center),
|
||||
"right" | "end" => Some(Align::Right),
|
||||
"justify" => Some(Align::Justify),
|
||||
"left" | "start" => Some(Align::Left),
|
||||
_ => *align,
|
||||
}
|
||||
}
|
||||
"white-space" => *preserve = lv.starts_with("pre"),
|
||||
"page-break-before" | "break-before" => *page_break = lv == "always" || lv == "page",
|
||||
"page-break-after" | "break-after" => *page_break_after = lv == "always" || lv == "page",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Entities and decoding
|
||||
|
||||
/// Decode an entity body (`amp`, `#38`, `#x26`) to its text.
|
||||
pub fn decode_entity(name: &str) -> Option<String> {
|
||||
decode_char(name).map(String::from)
|
||||
}
|
||||
|
||||
fn decode_char(name: &str) -> Option<char> {
|
||||
if let Some(num) = name.strip_prefix('#') {
|
||||
let (digits, radix) = match num.strip_prefix(['x', 'X']) {
|
||||
Some(h) => (h, 16),
|
||||
None => (num, 10),
|
||||
};
|
||||
if !digits.chars().all(|c| c.is_digit(radix)) {
|
||||
return None;
|
||||
}
|
||||
let v = u32::from_str_radix(digits, radix).ok()?;
|
||||
let c = match v {
|
||||
0 => '\u{FFFD}',
|
||||
0x80..=0x9F => crate::txt::cp1252(v as u8),
|
||||
_ => char::from_u32(v).unwrap_or('\u{FFFD}'),
|
||||
};
|
||||
return Some(c);
|
||||
}
|
||||
let c = match name {
|
||||
"amp" | "AMP" => '&',
|
||||
"lt" | "LT" => '<',
|
||||
"gt" | "GT" => '>',
|
||||
"quot" | "QUOT" => '"',
|
||||
"apos" | "APOS" => '\'',
|
||||
"nbsp" | "NBSP" => '\u{00A0}',
|
||||
"ensp" => '\u{2002}',
|
||||
"emsp" => '\u{2003}',
|
||||
"thinsp" => '\u{2009}',
|
||||
"shy" => '\u{00AD}',
|
||||
"copy" | "COPY" => '©',
|
||||
"reg" | "REG" => '®',
|
||||
"trade" | "TRADE" => '™',
|
||||
"hellip" => '…',
|
||||
"mdash" => '—',
|
||||
"ndash" => '–',
|
||||
"minus" => '−',
|
||||
"lsquo" => '‘',
|
||||
"rsquo" => '’',
|
||||
"sbquo" => '‚',
|
||||
"ldquo" => '“',
|
||||
"rdquo" => '”',
|
||||
"bdquo" => '„',
|
||||
"laquo" => '«',
|
||||
"raquo" => '»',
|
||||
"bull" => '•',
|
||||
"middot" => '·',
|
||||
"deg" => '°',
|
||||
"plusmn" => '±',
|
||||
"times" => '×',
|
||||
"divide" => '÷',
|
||||
"frac12" => '½',
|
||||
"frac14" => '¼',
|
||||
"frac34" => '¾',
|
||||
"frac13" => '⅓',
|
||||
"frac23" => '⅔',
|
||||
"frac18" => '⅛',
|
||||
"frac38" => '⅜',
|
||||
"frac58" => '⅝',
|
||||
"frac78" => '⅞',
|
||||
"sup1" => '¹',
|
||||
"sup2" => '²',
|
||||
"sup3" => '³',
|
||||
"micro" => 'µ',
|
||||
"permil" => '‰',
|
||||
"prime" => '′',
|
||||
"Prime" => '″',
|
||||
"oline" => '‾',
|
||||
"frasl" => '⁄',
|
||||
"sect" => '§',
|
||||
"para" => '¶',
|
||||
"euro" | "EURO" => '€',
|
||||
"pound" | "POUND" => '£',
|
||||
"yen" | "YEN" => '¥',
|
||||
"cent" | "CENT" => '¢',
|
||||
"curren" => '¤',
|
||||
"dagger" => '†',
|
||||
"Dagger" => '‡',
|
||||
"larr" => '←',
|
||||
"rarr" => '→',
|
||||
"uarr" => '↑',
|
||||
"darr" => '↓',
|
||||
"harr" => '↔',
|
||||
"lArr" => '⇐',
|
||||
"rArr" => '⇒',
|
||||
"uArr" => '⇑',
|
||||
"dArr" => '⇓',
|
||||
"hArr" => '⇔',
|
||||
"crarr" => '↵',
|
||||
"forall" => '∀',
|
||||
"part" => '∂',
|
||||
"exist" => '∃',
|
||||
"empty" => '∅',
|
||||
"nabla" => '∇',
|
||||
"isin" => '∈',
|
||||
"notin" => '∉',
|
||||
"ni" => '∋',
|
||||
"prod" => '∏',
|
||||
"sum" => '∑',
|
||||
"radic" => '√',
|
||||
"prop" => '∝',
|
||||
"ang" => '∠',
|
||||
"and" => '∧',
|
||||
"or" => '∨',
|
||||
"cap" => '∩',
|
||||
"cup" => '∪',
|
||||
"int" => '∫',
|
||||
"there4" => '∴',
|
||||
"sim" => '∼',
|
||||
"cong" => '≅',
|
||||
"asymp" | "approx" => '≈',
|
||||
"ne" => '≠',
|
||||
"equiv" => '≡',
|
||||
"le" => '≤',
|
||||
"ge" => '≥',
|
||||
"sub" => '⊂',
|
||||
"sup" => '⊃',
|
||||
"nsub" => '⊄',
|
||||
"sube" => '⊆',
|
||||
"supe" => '⊇',
|
||||
"oplus" => '⊕',
|
||||
"otimes" => '⊗',
|
||||
"perp" => '⊥',
|
||||
"sdot" => '⋅',
|
||||
"infin" => '∞',
|
||||
"loz" => '◊',
|
||||
"spades" => '♠',
|
||||
"clubs" => '♣',
|
||||
"hearts" => '♥',
|
||||
"diams" => '♦',
|
||||
"check" => '✓',
|
||||
"iexcl" => '¡',
|
||||
"iquest" => '¿',
|
||||
"szlig" => 'ß',
|
||||
"auml" => 'ä',
|
||||
"ouml" => 'ö',
|
||||
"uuml" => 'ü',
|
||||
"Auml" => 'Ä',
|
||||
"Ouml" => 'Ö',
|
||||
"Uuml" => 'Ü',
|
||||
"eacute" => 'é',
|
||||
"egrave" => 'è',
|
||||
"ecirc" => 'ê',
|
||||
"euml" => 'ë',
|
||||
"Eacute" => 'É',
|
||||
"Egrave" => 'È',
|
||||
"Ecirc" => 'Ê',
|
||||
"Euml" => 'Ë',
|
||||
"aacute" => 'á',
|
||||
"agrave" => 'à',
|
||||
"acirc" => 'â',
|
||||
"atilde" => 'ã',
|
||||
"Agrave" => 'À',
|
||||
"Aacute" => 'Á',
|
||||
"Acirc" => 'Â',
|
||||
"Atilde" => 'Ã',
|
||||
"iacute" => 'í',
|
||||
"igrave" => 'ì',
|
||||
"icirc" => 'î',
|
||||
"iuml" => 'ï',
|
||||
"Iacute" => 'Í',
|
||||
"Igrave" => 'Ì',
|
||||
"Icirc" => 'Î',
|
||||
"Iuml" => 'Ï',
|
||||
"oacute" => 'ó',
|
||||
"ograve" => 'ò',
|
||||
"ocirc" => 'ô',
|
||||
"otilde" => 'õ',
|
||||
"Oacute" => 'Ó',
|
||||
"Ograve" => 'Ò',
|
||||
"Ocirc" => 'Ô',
|
||||
"Otilde" => 'Õ',
|
||||
"uacute" => 'ú',
|
||||
"ugrave" => 'ù',
|
||||
"ucirc" => 'û',
|
||||
"Uacute" => 'Ú',
|
||||
"Ugrave" => 'Ù',
|
||||
"Ucirc" => 'Û',
|
||||
"yacute" => 'ý',
|
||||
"yuml" => 'ÿ',
|
||||
"Yacute" => 'Ý',
|
||||
"ntilde" => 'ñ',
|
||||
"Ntilde" => 'Ñ',
|
||||
"ccedil" => 'ç',
|
||||
"Ccedil" => 'Ç',
|
||||
"aring" => 'å',
|
||||
"Aring" => 'Å',
|
||||
"oslash" => 'ø',
|
||||
"Oslash" => 'Ø',
|
||||
"aelig" => 'æ',
|
||||
"AElig" => 'Æ',
|
||||
"alpha" => 'α',
|
||||
"beta" => 'β',
|
||||
"gamma" => 'γ',
|
||||
"delta" => 'δ',
|
||||
"epsilon" => 'ε',
|
||||
"zeta" => 'ζ',
|
||||
"eta" => 'η',
|
||||
"theta" => 'θ',
|
||||
"iota" => 'ι',
|
||||
"kappa" => 'κ',
|
||||
"lambda" => 'λ',
|
||||
"mu" => 'μ',
|
||||
"nu" => 'ν',
|
||||
"xi" => 'ξ',
|
||||
"pi" => 'π',
|
||||
"rho" => 'ρ',
|
||||
"sigma" => 'σ',
|
||||
"tau" => 'τ',
|
||||
"upsilon" => 'υ',
|
||||
"phi" => 'φ',
|
||||
"chi" => 'χ',
|
||||
"psi" => 'ψ',
|
||||
"omega" => 'ω',
|
||||
"Gamma" => 'Γ',
|
||||
"Delta" => 'Δ',
|
||||
"Theta" => 'Θ',
|
||||
"Lambda" => 'Λ',
|
||||
"Xi" => 'Ξ',
|
||||
"Pi" => 'Π',
|
||||
"Sigma" => 'Σ',
|
||||
"Upsilon" => 'Υ',
|
||||
"Phi" => 'Φ',
|
||||
"Psi" => 'Ψ',
|
||||
"Omega" => 'Ω',
|
||||
"zwj" => '\u{200D}',
|
||||
"zwnj" => '\u{200C}',
|
||||
_ => return None,
|
||||
};
|
||||
Some(c)
|
||||
}
|
||||
|
||||
/// Legacy names HTML5 also decodes without a `;` (only those [`decode_char`] knows), sorted by bytes.
|
||||
const BARE_NAMES: &[&str] = &[
|
||||
"AElig", "AMP", "Aacute", "Acirc", "Agrave", "Aring", "Atilde", "Auml", "COPY", "Ccedil", "Eacute", "Ecirc", "Egrave", "Euml", "GT", "Iacute",
|
||||
"Icirc", "Igrave", "Iuml", "LT", "Ntilde", "Oacute", "Ocirc", "Ograve", "Oslash", "Otilde", "Ouml", "QUOT", "REG", "Uacute", "Ucirc", "Ugrave",
|
||||
"Uuml", "Yacute", "aacute", "acirc", "aelig", "agrave", "amp", "aring", "atilde", "auml", "ccedil", "cent", "copy", "curren", "deg", "divide",
|
||||
"eacute", "ecirc", "egrave", "euml", "frac12", "frac14", "frac34", "gt", "iacute", "icirc", "iexcl", "igrave", "iquest", "iuml", "laquo", "lt",
|
||||
"micro", "middot", "nbsp", "ntilde", "oacute", "ocirc", "ograve", "oslash", "otilde", "ouml", "para", "plusmn", "pound", "quot", "raquo", "reg",
|
||||
"sect", "shy", "sup1", "sup2", "sup3", "szlig", "times", "uacute", "ucirc", "ugrave", "uuml", "yacute", "yen", "yuml",
|
||||
];
|
||||
|
||||
/// A reference written without its `;` at the start of `after` (the text after `&`): a numeric one,
|
||||
/// or the longest legacy name. In attribute values a name followed by `=` or an alphanumeric stays text.
|
||||
fn bare_reference(after: &str, attr: bool) -> Option<(char, usize)> {
|
||||
if let Some(num) = after.strip_prefix('#') {
|
||||
let hex = num.starts_with(['x', 'X']);
|
||||
let radix = if hex { 16 } else { 10 };
|
||||
let skip = usize::from(hex);
|
||||
let n = num.get(skip..)?.bytes().take_while(|b| char::from(*b).is_digit(radix)).count();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
let len = 1 + skip + n;
|
||||
return decode_char(after.get(..len)?).map(|c| (c, len));
|
||||
}
|
||||
// Legacy names are 2 to 6 ASCII alphanumerics: try the longest prefix first.
|
||||
let run = after.bytes().take(6).take_while(u8::is_ascii_alphanumeric).count();
|
||||
let len = (2..=run).rev().find(|&l| after.get(..l).is_some_and(|p| BARE_NAMES.binary_search(&p).is_ok()))?;
|
||||
let next = after.as_bytes().get(len);
|
||||
if attr && next.is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'=') {
|
||||
return None;
|
||||
}
|
||||
decode_char(after.get(..len)?).map(|c| (c, len))
|
||||
}
|
||||
|
||||
/// Replace entity references in text `s`.
|
||||
pub fn unescape(s: &str) -> String {
|
||||
unescape_in(s, false)
|
||||
}
|
||||
|
||||
/// Replace entity references in an attribute value `s`.
|
||||
pub fn unescape_attr(s: &str) -> String {
|
||||
unescape_in(s, true)
|
||||
}
|
||||
|
||||
fn unescape_in(s: &str, attr: bool) -> String {
|
||||
if !s.contains('&') {
|
||||
return s.to_string();
|
||||
}
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut rest = s;
|
||||
while let Some(k) = rest.find('&') {
|
||||
out.push_str(&rest[..k]);
|
||||
let after = &rest[k + 1..];
|
||||
// An entity name is short; looking further would rescan the whole text for every stray `&`.
|
||||
let mut cut = after.len().min(33);
|
||||
while !after.is_char_boundary(cut) {
|
||||
cut -= 1;
|
||||
}
|
||||
let end = after.get(..cut).and_then(|w| w.find(';'));
|
||||
match end.and_then(|e| decode_char(&after[..e]).map(|c| (c, e + 1))).or_else(|| bare_reference(after, attr)) {
|
||||
Some((c, e)) => {
|
||||
out.push(c);
|
||||
rest = &after[e..];
|
||||
}
|
||||
None => {
|
||||
out.push('&');
|
||||
rest = after;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_str(rest);
|
||||
out
|
||||
}
|
||||
|
||||
fn sniff_charset(head: &str) -> Option<&str> {
|
||||
let (i, n) = head.find("charset").map(|i| (i, 7)).or_else(|| head.find("encoding").map(|i| (i, 8)))?;
|
||||
let after = head.get(i + n..)?.trim_start().strip_prefix('=')?;
|
||||
let trimmed = after.trim_start().trim_start_matches(['"', '\'']);
|
||||
let end = trimmed.find(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | ';' | '>' | '/')).unwrap_or(trimmed.len());
|
||||
let label = trimmed[..end].trim();
|
||||
if label.is_empty() { None } else { Some(label) }
|
||||
}
|
||||
|
||||
/// Decode HTML bytes: BOM, then a `charset` in the first 2 KB, else UTF-8 with a legacy fallback.
|
||||
pub fn decode(b: &[u8]) -> String {
|
||||
decode_as(b, None)
|
||||
}
|
||||
|
||||
/// [`decode`] in the `charset` its container states (a MIME part's, a content type's), when it
|
||||
/// states one. Single-byte Windows code pages (`windows-1250`…, `iso-8859-1`, `us-ascii` as 1252)
|
||||
/// are decoded as such, other labels by their WHATWG decoders.
|
||||
pub fn decode_as(b: &[u8], charset: Option<&str>) -> String {
|
||||
if b.starts_with(&[0xFF, 0xFE]) || b.starts_with(&[0xFE, 0xFF]) || b.starts_with(&[0xEF, 0xBB, 0xBF]) {
|
||||
return crate::txt::decode(b);
|
||||
}
|
||||
let head = String::from_utf8_lossy(b.get(..b.len().min(2048)).unwrap_or(b)).to_ascii_lowercase();
|
||||
let named = charset.map(str::to_ascii_lowercase).or_else(|| sniff_charset(&head).map(str::to_string));
|
||||
let code_page = match named.as_deref().map(str::trim) {
|
||||
Some("iso-8859-1" | "latin1" | "us-ascii") => Some(1252),
|
||||
Some(cs) => cs
|
||||
.strip_prefix("windows-")
|
||||
.or_else(|| cs.strip_prefix("cp"))
|
||||
.and_then(|n| n.parse::<u16>().ok())
|
||||
.filter(|cp| wordcraft_doc::encoding::SINGLE_BYTE_CODE_PAGES.contains(cp)),
|
||||
None => None,
|
||||
};
|
||||
// Other labels (ISO 8859, KOI8, Shift_JIS, GBK, Big5, EUC-KR…) by their WHATWG decoders; a
|
||||
// UTF-16 one without a BOM means UTF-8, as in browsers.
|
||||
let other = named
|
||||
.as_deref()
|
||||
.and_then(|l| encoding_rs::Encoding::for_label(l.trim().as_bytes()))
|
||||
.filter(|e| ![encoding_rs::UTF_8, encoding_rs::UTF_16LE, encoding_rs::UTF_16BE].contains(e));
|
||||
match (code_page, other) {
|
||||
(Some(cp), _) => b.iter().map(|&c| wordcraft_doc::encoding::windows_byte(cp, c)).collect(),
|
||||
(None, Some(enc)) => enc.decode_without_bom_handling(b).0.into_owned(),
|
||||
(None, None) => match std::str::from_utf8(b) {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => crate::txt::decode(b),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bare_names_are_sorted_and_known() {
|
||||
assert!(BARE_NAMES.windows(2).all(|w| w[0] < w[1]));
|
||||
assert!(BARE_NAMES.iter().all(|n| (2..=6).contains(&n.len()) && decode_char(n).is_some()));
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
use super::{Align, Document, FBlock, FTable, Flow, Fmt, Inline, Kind, Para, VAlign, base64_encode, mime_of, model};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Export
|
||||
|
||||
/// Escape text for HTML content and attribute values.
|
||||
pub fn esc(s: &str) -> String {
|
||||
let mut o = String::with_capacity(s.len() + 8);
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'&' => o.push_str("&"),
|
||||
'<' => o.push_str("<"),
|
||||
'>' => o.push_str(">"),
|
||||
'"' => o.push_str("""),
|
||||
'\u{000C}' | '\u{000E}' => {}
|
||||
_ => o.push(c),
|
||||
}
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// A font name as a quoted CSS string. Quotes, `\` and control characters would end the string,
|
||||
/// and `;` and `,` the declaration or the family list for a reader that splits on them, so all of
|
||||
/// these are written as hex escapes ([`css_unescape`] reads them back).
|
||||
fn css_font_name(name: &str) -> String {
|
||||
let mut o = String::with_capacity(name.len() + 2);
|
||||
o.push('\'');
|
||||
for c in name.chars() {
|
||||
match c {
|
||||
'\0' => o.push('\u{FFFD}'),
|
||||
'\'' | '"' | '\\' | ';' | ',' => o.push_str(&format!("\\{:x} ", u32::from(c))),
|
||||
c if c.is_control() => o.push_str(&format!("\\{:x} ", u32::from(c))),
|
||||
c => o.push(c),
|
||||
}
|
||||
}
|
||||
o.push('\'');
|
||||
o
|
||||
}
|
||||
|
||||
fn align_css(a: Option<Align>) -> Option<&'static str> {
|
||||
match a? {
|
||||
Align::Center => Some("text-align:center"),
|
||||
Align::Right => Some("text-align:right"),
|
||||
Align::Justify | Align::Distribute => Some("text-align:justify"),
|
||||
Align::Left => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_html(t: &str, at_start: bool) -> String {
|
||||
let mut o = String::new();
|
||||
let mut prev_space = at_start;
|
||||
for c in t.chars() {
|
||||
match c {
|
||||
'\n' => {
|
||||
o.push_str("<br>");
|
||||
prev_space = true;
|
||||
continue;
|
||||
}
|
||||
'\t' => o.push_str("<span style=\"white-space:pre\">\t</span>"),
|
||||
' ' if prev_space => o.push_str(" "),
|
||||
_ => o.push_str(&esc(&c.to_string())),
|
||||
}
|
||||
prev_space = c == ' ';
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// The tags that carry `f`'s character formatting: what opens them, and what closes them
|
||||
/// (innermost last, so the closes are written in reverse).
|
||||
fn fmt_tags(f: &Fmt) -> (String, Vec<&'static str>) {
|
||||
let mut open = String::new();
|
||||
let mut close: Vec<&str> = Vec::new();
|
||||
// A script or other non-web target is written as plain text (`is_safe_href`).
|
||||
if let Some(l) = f.link.as_deref().filter(|l| wordcraft_doc::link::is_safe_href(l)) {
|
||||
open.push_str(&format!("<a href=\"{}\">", esc(l)));
|
||||
close.push("</a>");
|
||||
}
|
||||
let mut tag = |o: &str, c: &'static str| {
|
||||
open.push_str(o);
|
||||
close.push(c);
|
||||
};
|
||||
if f.bold {
|
||||
tag("<strong>", "</strong>");
|
||||
}
|
||||
if f.italic {
|
||||
tag("<em>", "</em>");
|
||||
}
|
||||
if f.underline {
|
||||
tag("<u>", "</u>");
|
||||
}
|
||||
if f.strike {
|
||||
tag("<s>", "</s>");
|
||||
}
|
||||
if f.sup {
|
||||
tag("<sup>", "</sup>");
|
||||
} else if f.sub {
|
||||
tag("<sub>", "</sub>");
|
||||
}
|
||||
if f.code {
|
||||
tag("<code>", "</code>");
|
||||
}
|
||||
let mut css = Vec::new();
|
||||
if let Some(c) = f.color {
|
||||
css.push(format!("color:#{}", c.hex()));
|
||||
}
|
||||
if let Some(c) = f.background {
|
||||
css.push(format!("background-color:#{}", c.hex()));
|
||||
}
|
||||
if let Some(s) = f.size {
|
||||
css.push(format!("font-size:{}pt", trim_num(s)));
|
||||
}
|
||||
if let Some(fam) = &f.font {
|
||||
css.push(format!("font-family:{}", esc(&css_font_name(fam))));
|
||||
}
|
||||
if !css.is_empty() {
|
||||
open.push_str(&format!("<span style=\"{}\">", css.join(";")));
|
||||
close.push("</span>");
|
||||
}
|
||||
(open, close)
|
||||
}
|
||||
|
||||
/// Inline spans → HTML.
|
||||
pub fn inlines_html(inlines: &[Inline]) -> String {
|
||||
let mut o = String::new();
|
||||
for i in inlines.iter() {
|
||||
match i {
|
||||
Inline::Text(t, f) => {
|
||||
let (open, close) = fmt_tags(f);
|
||||
let at_start = o.is_empty();
|
||||
o.push_str(&open);
|
||||
o.push_str(&text_html(t, at_start));
|
||||
for c in close.iter().rev() {
|
||||
o.push_str(c);
|
||||
}
|
||||
}
|
||||
// A phonetic guide: the base text with its reading in `<rt>` (#1681). HTML ruby has
|
||||
// no alignment or size, so the guide's own attributes stay in the document model.
|
||||
Inline::Ruby { base, ruby, fmt } => {
|
||||
let (open, close) = fmt_tags(fmt);
|
||||
let at_start = o.is_empty();
|
||||
o.push_str(&open);
|
||||
o.push_str(&format!("<ruby>{}<rt>{}</rt></ruby>", text_html(base, at_start), text_html(ruby, false)));
|
||||
for c in close.iter().rev() {
|
||||
o.push_str(c);
|
||||
}
|
||||
}
|
||||
Inline::Image(img) => {
|
||||
o.push_str(&format!(
|
||||
"<img src=\"data:{};base64,{}\" alt=\"{}\" width=\"{}\" height=\"{}\">",
|
||||
mime_of(&img.ext),
|
||||
base64_encode(&img.data),
|
||||
esc(&img.alt),
|
||||
(img.w / 0.75).round(),
|
||||
(img.h / 0.75).round()
|
||||
));
|
||||
}
|
||||
Inline::Anchor(a) => o.push_str(&format!("<a id=\"{}\"></a>", esc(a))),
|
||||
Inline::Note { .. }
|
||||
| Inline::Comment { .. }
|
||||
| Inline::CommentEnd(_)
|
||||
| Inline::Shape { .. }
|
||||
| Inline::Group { .. }
|
||||
| Inline::FieldStart(_)
|
||||
| Inline::FieldEnd
|
||||
| Inline::AnchorEnd(_) => {}
|
||||
// ASK and SET show nothing (see `model::shows_nothing`).
|
||||
Inline::Field { instr, .. } if model::shows_nothing(instr) => {}
|
||||
Inline::Field { result, fmt, .. } | Inline::Control { text: result, fmt, .. } => {
|
||||
let (open, close) = fmt_tags(fmt);
|
||||
let at_start = o.is_empty();
|
||||
o.push_str(&open);
|
||||
o.push_str(&text_html(result, at_start));
|
||||
for c in close.iter().rev() {
|
||||
o.push_str(c);
|
||||
}
|
||||
}
|
||||
// No picture to show: the browser shows the alt text in its place.
|
||||
Inline::Figure(alt) | Inline::Chart(crate::model::Chart { alt, .. }) => o.push_str(&format!("<img alt=\"{}\">", esc(alt))),
|
||||
// MathML, which browsers, Word and LibreOffice show as an equation (#816).
|
||||
Inline::Equation { linear, display } => o.push_str(&crate::mathml::linear_to_mathml(linear, *display)),
|
||||
}
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
fn trim_num(v: f32) -> String {
|
||||
let s = format!("{:.2}", v);
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
fn para_attrs(p: &Para) -> String {
|
||||
let mut css: Vec<&str> = Vec::new();
|
||||
if let Some(a) = align_css(p.align) {
|
||||
css.push(a);
|
||||
}
|
||||
if p.page_break {
|
||||
css.push("break-before:page;page-break-before:always");
|
||||
}
|
||||
if css.is_empty() { String::new() } else { format!(" style=\"{}\"", css.join(";")) }
|
||||
}
|
||||
|
||||
fn para_html(p: &Para) -> String {
|
||||
let body = inlines_html(&p.inlines);
|
||||
let attrs = para_attrs(p);
|
||||
match p.kind {
|
||||
Kind::Heading(n) => format!("<h{n}{attrs}>{body}</h{n}>", n = n.clamp(1, 6)),
|
||||
Kind::Title => format!("<h1{attrs}>{body}</h1>"),
|
||||
Kind::Rule => "<hr>".to_string(),
|
||||
_ if body.is_empty() => format!("<p{attrs}> </p>"),
|
||||
_ => format!("<p{attrs}>{body}</p>"),
|
||||
}
|
||||
}
|
||||
|
||||
fn table_html(t: &FTable, out: &mut String, depth: usize) {
|
||||
out.push_str("<table style=\"border-collapse:collapse\">\n");
|
||||
for row in &t.rows {
|
||||
out.push_str("<tr>");
|
||||
for c in row {
|
||||
if c.covered {
|
||||
continue;
|
||||
}
|
||||
let tag = if c.header { "th" } else { "td" };
|
||||
let mut attrs = String::new();
|
||||
if c.colspan > 1 {
|
||||
attrs.push_str(&format!(" colspan=\"{}\"", c.colspan));
|
||||
}
|
||||
if c.rowspan > 1 {
|
||||
attrs.push_str(&format!(" rowspan=\"{}\"", c.rowspan));
|
||||
}
|
||||
let v_css = match c.valign {
|
||||
Some(VAlign::Center) => "vertical-align:middle",
|
||||
Some(VAlign::Bottom) => "vertical-align:bottom",
|
||||
_ => "vertical-align:top",
|
||||
};
|
||||
let mut css = vec!["padding:4px 6px".to_string(), v_css.to_string()];
|
||||
if !t.borderless {
|
||||
css.insert(0, "border:1px solid #999".to_string());
|
||||
}
|
||||
if let Some(s) = c.shading {
|
||||
css.push(format!("background-color:#{}", s.hex()));
|
||||
}
|
||||
let single = match c.blocks.as_slice() {
|
||||
[FBlock::Para(p)] if p.kind == Kind::Normal && p.list.is_none() && !p.page_break => Some(p),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(a) = single.and_then(|p| align_css(p.align)) {
|
||||
css.push(a.to_string());
|
||||
}
|
||||
out.push_str(&format!("<{tag}{attrs} style=\"{}\">", css.join(";")));
|
||||
match single {
|
||||
Some(p) => out.push_str(&inlines_html(&p.inlines)),
|
||||
None => {
|
||||
out.push('\n');
|
||||
blocks_html(&c.blocks, out, depth + 1);
|
||||
}
|
||||
}
|
||||
out.push_str(&format!("</{tag}>"));
|
||||
}
|
||||
out.push_str("</tr>\n");
|
||||
}
|
||||
out.push_str("</table>\n");
|
||||
}
|
||||
|
||||
fn blocks_html(blocks: &[FBlock], out: &mut String, depth: usize) {
|
||||
let mut i = 0;
|
||||
while let Some(b) = blocks.get(i) {
|
||||
match b {
|
||||
FBlock::Table(t) => {
|
||||
if depth < model::MAX_DEPTH {
|
||||
table_html(t, out, depth);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
FBlock::Para(p) if p.list.is_some() => {
|
||||
// Nested lists from consecutive list paragraphs.
|
||||
let mut stack: Vec<(bool, bool)> = Vec::new(); // (ordered, li open)
|
||||
while let Some(FBlock::Para(q)) = blocks.get(i) {
|
||||
let Some(li) = q.list else { break };
|
||||
let lv = li.level.min(8) as usize;
|
||||
while stack.len() > lv + 1 {
|
||||
close_list(&mut stack, out);
|
||||
}
|
||||
if stack.len() == lv + 1 && stack.last().is_some_and(|s| s.0 != li.ordered) {
|
||||
close_list(&mut stack, out);
|
||||
}
|
||||
while stack.len() < lv + 1 {
|
||||
if let Some(top) = stack.last_mut()
|
||||
&& !top.1
|
||||
{
|
||||
out.push_str("<li>");
|
||||
top.1 = true;
|
||||
}
|
||||
out.push_str(if li.ordered { "<ol>" } else { "<ul>" });
|
||||
stack.push((li.ordered, false));
|
||||
}
|
||||
if let Some(top) = stack.last_mut() {
|
||||
if top.1 {
|
||||
out.push_str("</li>\n");
|
||||
}
|
||||
top.1 = true;
|
||||
}
|
||||
let body = inlines_html(&q.inlines);
|
||||
let attrs = para_attrs(q);
|
||||
out.push_str(&format!("<li{attrs}>{body}"));
|
||||
i += 1;
|
||||
}
|
||||
while !stack.is_empty() {
|
||||
close_list(&mut stack, out);
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
FBlock::Para(p) if p.kind == Kind::Code => {
|
||||
out.push_str("<pre><code>");
|
||||
let mut first = true;
|
||||
while let Some(FBlock::Para(q)) = blocks.get(i) {
|
||||
if q.kind != Kind::Code || q.list.is_some() {
|
||||
break;
|
||||
}
|
||||
if !first {
|
||||
out.push('\n');
|
||||
}
|
||||
first = false;
|
||||
out.push_str(&esc(&q.text()));
|
||||
i += 1;
|
||||
}
|
||||
out.push_str("</code></pre>\n");
|
||||
}
|
||||
FBlock::Para(p) if p.kind == Kind::Quote => {
|
||||
out.push_str("<blockquote>\n");
|
||||
while let Some(FBlock::Para(q)) = blocks.get(i) {
|
||||
if q.kind != Kind::Quote || q.list.is_some() {
|
||||
break;
|
||||
}
|
||||
out.push_str(¶_html(&Para { kind: Kind::Normal, ..q.clone() }));
|
||||
out.push('\n');
|
||||
i += 1;
|
||||
}
|
||||
out.push_str("</blockquote>\n");
|
||||
}
|
||||
FBlock::Para(p) => {
|
||||
out.push_str(¶_html(p));
|
||||
out.push('\n');
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close_list(stack: &mut Vec<(bool, bool)>, out: &mut String) {
|
||||
if let Some((ordered, li_open)) = stack.pop() {
|
||||
if li_open {
|
||||
out.push_str("</li>");
|
||||
}
|
||||
out.push_str(if ordered { "</ol>" } else { "</ul>" });
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete HTML document.
|
||||
pub fn export(doc: &Document) -> String {
|
||||
let flow = model::from_doc(doc);
|
||||
export_flow(&flow, doc.styles.default_chr.lang.as_deref().unwrap_or("en"))
|
||||
}
|
||||
|
||||
/// `doc`'s content as an HTML fragment (no head or style sheet): what a copy puts on the system
|
||||
/// clipboard beside its text, equations as MathML (#816).
|
||||
pub fn export_fragment(doc: &Document) -> String {
|
||||
let flow = model::from_doc(doc);
|
||||
let mut out = String::from("<meta charset=\"utf-8\">");
|
||||
blocks_html(&flow.blocks, &mut out, 0);
|
||||
out
|
||||
}
|
||||
|
||||
pub fn export_flow(flow: &Flow, lang: &str) -> String {
|
||||
let m = &flow.meta;
|
||||
let mut out = String::new();
|
||||
out.push_str("<!DOCTYPE html>\n");
|
||||
out.push_str(&format!("<html lang=\"{}\">\n<head>\n<meta charset=\"utf-8\">\n", esc(lang)));
|
||||
out.push_str("<meta name=\"generator\" content=\"WordCraft\">\n");
|
||||
out.push_str(&format!("<title>{}</title>\n", esc(&m.title)));
|
||||
for (k, v) in [("author", &m.author), ("description", &m.description), ("keywords", &m.keywords), ("subject", &m.subject)] {
|
||||
if !v.is_empty() {
|
||||
out.push_str(&format!("<meta name=\"{k}\" content=\"{}\">\n", esc(v)));
|
||||
}
|
||||
}
|
||||
out.push_str(
|
||||
"<style>\nbody{font-family:Aptos,Calibri,'Segoe UI',Arial,sans-serif;font-size:12pt;line-height:1.15;max-width:48em;margin:2em auto;padding:0 1em}\n\
|
||||
table{border-collapse:collapse;margin:0.5em 0}\nblockquote{margin:0.5em 2em;font-style:italic;color:#404040}\n\
|
||||
pre{background:#f5f5f5;padding:0.5em;font-family:'Courier New',monospace;font-size:10pt}\n</style>\n</head>\n<body>\n",
|
||||
);
|
||||
blocks_html(&flow.blocks, &mut out, 0);
|
||||
out.push_str("</body>\n</html>\n");
|
||||
out
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
use super::builder::{Builder, ImageLoader, parse_sheet};
|
||||
use super::lexer::{Lexer, Tok};
|
||||
use super::math_ruby::{read_math, read_ruby};
|
||||
use super::{Document, Flow, Inline, model};
|
||||
|
||||
/// Parse HTML text into the flow model.
|
||||
pub fn parse(s: &str) -> Flow {
|
||||
parse_with(s, &|_| None)
|
||||
}
|
||||
|
||||
/// [`parse`], loading pictures that aren't `data:` URIs through `images`.
|
||||
pub fn parse_with(s: &str, images: ImageLoader) -> Flow {
|
||||
parse_in(s, images, false)
|
||||
}
|
||||
|
||||
pub(super) fn parse_in(s: &str, images: ImageLoader, page: bool) -> Flow {
|
||||
// HTML normalizes input newlines before tokenization, not after decoding character references.
|
||||
let normalized = if s.contains('\r') {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
let mut chunks = s.split('\r');
|
||||
out.push_str(chunks.next().unwrap_or_default());
|
||||
for chunk in chunks {
|
||||
out.push('\n');
|
||||
out.push_str(chunk.strip_prefix('\n').unwrap_or(chunk));
|
||||
}
|
||||
std::borrow::Cow::Owned(out)
|
||||
} else {
|
||||
std::borrow::Cow::Borrowed(s)
|
||||
};
|
||||
// The lexer's offsets (a style element's text is cut out by them) are into the normalized text.
|
||||
let s: &str = &normalized;
|
||||
let mut lx = Lexer { s, pos: 0 };
|
||||
let mut b = Builder::new(images, page);
|
||||
let mut guard = 0usize;
|
||||
while let Some(t) = lx.next() {
|
||||
guard += 1;
|
||||
if guard > 50_000_000 {
|
||||
break;
|
||||
}
|
||||
match t {
|
||||
Tok::Text(t) => b.text(&t),
|
||||
Tok::Start { name, attrs, self_close } => {
|
||||
b.close_head_for_start(&name);
|
||||
if crate::mathml::local(&name) == "math" && !self_close {
|
||||
let el = read_math(&mut lx, name, attrs);
|
||||
if !b.in_head()
|
||||
&& let Some((linear, display)) = crate::mathml::read(&el)
|
||||
{
|
||||
b.start_para().inlines.push(Inline::Equation { linear, display });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if name == "ruby" && !self_close {
|
||||
let r = read_ruby(&mut lx);
|
||||
if !b.in_head() {
|
||||
b.push_ruby(&r.base, &r.ruby);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if matches!(name.as_str(), "script" | "style" | "noscript" | "template" | "textarea") && !self_close {
|
||||
let from = lx.pos;
|
||||
let to = lx.skip_past_end_tag(&name);
|
||||
if name == "style" {
|
||||
parse_sheet(s.get(from..to).unwrap_or(""), &mut b.sheet);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
b.start(&name, &attrs, self_close);
|
||||
}
|
||||
Tok::End(name) => b.end(&name),
|
||||
}
|
||||
}
|
||||
b.finish()
|
||||
}
|
||||
|
||||
/// Collapse white space inside ruby markup as body text does (HTML's white space only: an
|
||||
/// ideographic space is text), without the space the element's indentation leaves at the edges.
|
||||
pub(super) fn collapse_ws(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
if matches!(c, ' ' | '\t' | '\n' | '\r' | '\u{000C}') {
|
||||
if !out.ends_with(' ') {
|
||||
out.push(' ');
|
||||
}
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out.trim_matches(' ').to_string()
|
||||
}
|
||||
|
||||
/// Parse HTML into a document.
|
||||
pub fn import(s: &str) -> Document {
|
||||
model::begin_import();
|
||||
model::to_doc(&parse(s))
|
||||
}
|
||||
|
||||
/// [`import`], loading pictures that aren't `data:` URIs through `images`.
|
||||
pub fn import_with(s: &str, images: ImageLoader) -> Document {
|
||||
model::begin_import();
|
||||
model::to_doc(&parse_with(s, images))
|
||||
}
|
||||
|
||||
/// A web page opened as a document, as Word opens one (a file, or a document's HTML
|
||||
/// `w:altChunk`): its text is Times New Roman 12 pt on single lines, headings bold and black at
|
||||
/// the sizes browsers give them, and a `p` or heading whose CSS leaves a margin out (or writes it
|
||||
/// invalidly) gets HTML's automatic spacing there; a margin a span states replaces the one its
|
||||
/// paragraph states (both as Word's import does). [`import`] reads a pasted fragment, which takes
|
||||
/// the document's own formatting instead (but its CSS still applies).
|
||||
pub fn import_page(s: &str, images: ImageLoader) -> Document {
|
||||
model::begin_import();
|
||||
model::to_doc(&parse_in(s, images, true))
|
||||
}
|
||||
|
||||
/// [`import_page`] onto `page` (its size and margins), which tables size to: HTML gives no page
|
||||
/// size, so the caller picks it (the region's paper, #1381).
|
||||
pub fn import_on(s: &str, images: ImageLoader, page: &wordcraft_doc::SectionProps) -> Document {
|
||||
model::begin_import();
|
||||
model::to_doc_in(&parse_in(s, images, true), page.clone())
|
||||
}
|
||||
|
||||
/// The font of a web page's text when its CSS names none.
|
||||
pub const PAGE_FONT: &str = "Times New Roman";
|
||||
/// Heading sizes on a web page, `h1`–`h6`, points.
|
||||
pub(super) const PAGE_HEADING_SIZES: [f32; 6] = [24.0, 18.0, 13.5, 12.0, 10.0, 7.5];
|
||||
@@ -1,176 +0,0 @@
|
||||
use super::entity::{unescape, unescape_attr};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Tokenizer
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum Tok {
|
||||
Text(String),
|
||||
Start { name: String, attrs: Vec<(String, String)>, self_close: bool },
|
||||
End(String),
|
||||
}
|
||||
|
||||
pub(super) struct Lexer<'a> {
|
||||
pub(super) s: &'a str,
|
||||
pub(super) pos: usize,
|
||||
}
|
||||
|
||||
impl Lexer<'_> {
|
||||
fn rest(&self) -> &str {
|
||||
self.s.get(self.pos..).unwrap_or("")
|
||||
}
|
||||
|
||||
/// Skip past the next occurrence of `pat` (ASCII case-insensitive), or to the end.
|
||||
fn skip_past(&mut self, pat: &str) -> usize {
|
||||
let start = self.pos;
|
||||
let hay = self.rest().as_bytes();
|
||||
let p = pat.as_bytes();
|
||||
let found = hay.windows(p.len().max(1)).position(|w| w.eq_ignore_ascii_case(p));
|
||||
match found {
|
||||
Some(k) => self.pos += k + p.len(),
|
||||
None => self.pos = self.s.len(),
|
||||
}
|
||||
found.map(|k| start + k).unwrap_or(self.s.len())
|
||||
}
|
||||
|
||||
/// Skip past the next matching end tag `</tag...>` (ASCII case-insensitive), or to the end.
|
||||
/// Skip past the end tag of raw text element `tag`; returns where that end tag starts (the
|
||||
/// end of the input when there is none).
|
||||
pub(super) fn skip_past_end_tag(&mut self, tag: &str) -> usize {
|
||||
let prefix = format!("</{tag}");
|
||||
let p = prefix.as_bytes();
|
||||
while self.pos < self.s.len() {
|
||||
let hay = self.rest().as_bytes();
|
||||
let found = hay.windows(p.len().max(1)).position(|w| w.eq_ignore_ascii_case(p));
|
||||
let Some(k) = found else {
|
||||
self.pos = self.s.len();
|
||||
return self.pos;
|
||||
};
|
||||
let after_tag = k + p.len();
|
||||
let next_byte = hay.get(after_tag);
|
||||
if next_byte.is_none_or(|b| b.is_ascii_whitespace() || *b == b'>' || *b == b'/') {
|
||||
let at = self.pos + k;
|
||||
self.pos += after_tag;
|
||||
self.skip_past(">");
|
||||
return at;
|
||||
}
|
||||
self.pos += k + 2;
|
||||
}
|
||||
self.s.len()
|
||||
}
|
||||
|
||||
pub(super) fn next(&mut self) -> Option<Tok> {
|
||||
let rest = self.rest();
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !rest.starts_with('<') {
|
||||
let end = rest.find('<').unwrap_or(rest.len());
|
||||
let t = unescape(&rest[..end]);
|
||||
self.pos += end;
|
||||
return Some(Tok::Text(t));
|
||||
}
|
||||
let b = rest.as_bytes();
|
||||
if rest.starts_with("<!--") {
|
||||
if rest.starts_with("<!-->") {
|
||||
self.pos += 5;
|
||||
} else if rest.starts_with("<!--->") {
|
||||
self.pos += 6;
|
||||
} else {
|
||||
self.pos += 4;
|
||||
self.skip_past("-->");
|
||||
}
|
||||
return Some(Tok::Text(String::new()));
|
||||
}
|
||||
if rest.starts_with("<!") || rest.starts_with("<?") {
|
||||
self.skip_past(">");
|
||||
return Some(Tok::Text(String::new()));
|
||||
}
|
||||
if rest.starts_with("</") {
|
||||
let name: String = rest
|
||||
.strip_prefix("</")
|
||||
.unwrap_or("")
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_alphanumeric() || *c == ':' || *c == '-')
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect();
|
||||
if name.is_empty() {
|
||||
self.pos += 2;
|
||||
return Some(Tok::Text("</".into()));
|
||||
}
|
||||
self.skip_past(">");
|
||||
return Some(Tok::End(name));
|
||||
}
|
||||
if !b.get(1).is_some_and(u8::is_ascii_alphabetic) {
|
||||
self.pos += 1;
|
||||
return Some(Tok::Text("<".into()));
|
||||
}
|
||||
// Start tag.
|
||||
let name: String =
|
||||
rest[1..].chars().take_while(|c| c.is_ascii_alphanumeric() || *c == ':' || *c == '-').map(|c| c.to_ascii_lowercase()).collect();
|
||||
let mut i = 1 + name.len();
|
||||
let mut attrs = Vec::new();
|
||||
let mut self_close = false;
|
||||
loop {
|
||||
while b.get(i).is_some_and(u8::is_ascii_whitespace) {
|
||||
i += 1;
|
||||
}
|
||||
match b.get(i) {
|
||||
None => break,
|
||||
Some(b'>') => {
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
Some(b'/') => {
|
||||
if b.get(i + 1) == Some(&b'>') {
|
||||
self_close = true;
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let a0 = i;
|
||||
while b.get(i).is_some_and(|c| !c.is_ascii_whitespace() && !matches!(c, b'=' | b'>' | b'/')) {
|
||||
i += 1;
|
||||
}
|
||||
let an = rest.get(a0..i).unwrap_or("").to_ascii_lowercase();
|
||||
if an.is_empty() {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
while b.get(i).is_some_and(u8::is_ascii_whitespace) {
|
||||
i += 1;
|
||||
}
|
||||
let mut val = String::new();
|
||||
if b.get(i) == Some(&b'=') {
|
||||
i += 1;
|
||||
while b.get(i).is_some_and(u8::is_ascii_whitespace) {
|
||||
i += 1;
|
||||
}
|
||||
match b.get(i) {
|
||||
Some(&q) if q == b'"' || q == b'\'' => {
|
||||
let v0 = i + 1;
|
||||
let e = rest.get(v0..).and_then(|r| r.find(q as char)).map(|k| v0 + k).unwrap_or(rest.len());
|
||||
val = unescape_attr(rest.get(v0..e).unwrap_or(""));
|
||||
i = e + 1;
|
||||
}
|
||||
_ => {
|
||||
let v0 = i;
|
||||
while b.get(i).is_some_and(|c| !c.is_ascii_whitespace() && *c != b'>') {
|
||||
i += 1;
|
||||
}
|
||||
val = unescape_attr(rest.get(v0..i).unwrap_or(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
if attrs.len() < 64 {
|
||||
attrs.push((an, val));
|
||||
}
|
||||
}
|
||||
self.pos += i.min(rest.len());
|
||||
Some(Tok::Start { name, attrs, self_close })
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
use super::builder::block_kind;
|
||||
use super::lexer::{Lexer, Tok};
|
||||
|
||||
/// A `<math>` element (#816): its tokens up to the matching end tag, as a tree. Nesting and size
|
||||
/// are bounded; an unclosed element ends with the input.
|
||||
pub(super) fn read_math(lx: &mut Lexer, name: String, attrs: Vec<(String, String)>) -> crate::mathml::El {
|
||||
use crate::mathml::{El, Item, MAX_NODES, local};
|
||||
let mut stack: Vec<El> = vec![El { name, attrs, children: Vec::new() }];
|
||||
let mut count = 0usize;
|
||||
let close = |stack: &mut Vec<El>| {
|
||||
if stack.len() > 1
|
||||
&& let Some(done) = stack.pop()
|
||||
&& let Some(parent) = stack.last_mut()
|
||||
{
|
||||
parent.children.push(Item::El(done));
|
||||
}
|
||||
};
|
||||
while let Some(t) = lx.next() {
|
||||
count += 1;
|
||||
if count > MAX_NODES * 4 {
|
||||
// Over the cap: drop the rest of this element so its tokens aren't read as HTML text.
|
||||
skip_math(lx, t);
|
||||
break;
|
||||
}
|
||||
match t {
|
||||
Tok::Text(t) => {
|
||||
if let Some(top) = stack.last_mut()
|
||||
&& !t.is_empty()
|
||||
{
|
||||
top.children.push(Item::Text(t));
|
||||
}
|
||||
}
|
||||
Tok::Start { name, attrs, self_close } => {
|
||||
let el = El { name, attrs, children: Vec::new() };
|
||||
if self_close || stack.len() > wordcraft_doc::math::MAX_DEPTH {
|
||||
if let Some(top) = stack.last_mut() {
|
||||
top.children.push(Item::El(el));
|
||||
}
|
||||
} else {
|
||||
stack.push(el);
|
||||
}
|
||||
}
|
||||
Tok::End(name) => {
|
||||
let name = local(&name).to_string();
|
||||
// Close up to the matching open element; a stray end tag is ignored.
|
||||
if let Some(k) = stack.iter().rposition(|e| local(&e.name) == name) {
|
||||
if k == 0 {
|
||||
break;
|
||||
}
|
||||
while stack.len() > k {
|
||||
close(&mut stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while stack.len() > 1 {
|
||||
close(&mut stack);
|
||||
}
|
||||
stack.pop().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Skip tokens (starting with `first`) up to and including the `</math>` that closes the element
|
||||
/// being read; nested `<math>` elements are counted. An unclosed element ends with the input.
|
||||
fn skip_math(lx: &mut Lexer, first: Tok) {
|
||||
use crate::mathml::local;
|
||||
let mut depth = 0usize;
|
||||
let mut t = Some(first);
|
||||
while let Some(tok) = t {
|
||||
match tok {
|
||||
Tok::Start { name, self_close: false, .. } if local(&name) == "math" => depth = depth.saturating_add(1),
|
||||
Tok::End(name) if local(&name) == "math" => {
|
||||
if depth == 0 {
|
||||
return;
|
||||
}
|
||||
depth = depth.saturating_sub(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
t = lx.next();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `<ruby>` element's base text and reading (#1681).
|
||||
#[derive(Default)]
|
||||
pub(super) struct RubyText {
|
||||
pub(super) base: String,
|
||||
pub(super) ruby: String,
|
||||
}
|
||||
|
||||
/// Which part of a `<ruby>` element the text being read belongs to.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum RubyPart {
|
||||
/// The text being annotated: an `<rb>` child, or the text outside `<rt>`/`<rp>`.
|
||||
Base,
|
||||
/// The reading: an `<rt>` child.
|
||||
Ruby,
|
||||
/// `<rp>` fallback parentheses, which are not text: browsers show them only where ruby isn't
|
||||
/// supported.
|
||||
Fallback,
|
||||
}
|
||||
|
||||
/// A block-level tag ends an open `<ruby>` implicitly, as it does in a browser.
|
||||
fn ends_ruby(t: &Tok) -> bool {
|
||||
match t {
|
||||
Tok::Start { name, .. } | Tok::End(name) => block_kind(name).is_some(),
|
||||
Tok::Text(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a `<ruby>` element's content: its base text from `<rb>` children (or the text outside
|
||||
/// them), its reading from `<rt>` children, and `<rp>` fallback parentheses dropped. Tolerant,
|
||||
/// like the rest of the reader: tags inside are ignored, an unclosed element ends at the next
|
||||
/// block-level tag (handed back to the caller) or with the input, and the tokens read are capped.
|
||||
pub(super) fn read_ruby(lx: &mut Lexer) -> RubyText {
|
||||
/// Most tokens one `<ruby>` element is read from: a missing end tag must not scan the rest of
|
||||
/// a hostile file here.
|
||||
const MAX_TOKENS: usize = 4096;
|
||||
let mut out = RubyText::default();
|
||||
let mut part = RubyPart::Base;
|
||||
let mut nested = 0usize;
|
||||
let mut count = 0usize;
|
||||
while count < MAX_TOKENS {
|
||||
count += 1;
|
||||
let start = lx.pos;
|
||||
let Some(t) = lx.next() else { break };
|
||||
if nested == 0 && ends_ruby(&t) {
|
||||
// The boundary ends the element: give the token back to the main reader.
|
||||
lx.pos = start;
|
||||
break;
|
||||
}
|
||||
match t {
|
||||
Tok::Text(t) => match part {
|
||||
RubyPart::Base => out.base.push_str(&t),
|
||||
RubyPart::Ruby => out.ruby.push_str(&t),
|
||||
RubyPart::Fallback => {}
|
||||
},
|
||||
// The self-closing slash is ignored on HTML elements (`<rt/>` opens an `rt`), as in a
|
||||
// browser's parser.
|
||||
Tok::Start { name, self_close, .. } => match name.as_str() {
|
||||
"rb" => part = RubyPart::Base,
|
||||
"rt" => part = RubyPart::Ruby,
|
||||
"rp" => part = RubyPart::Fallback,
|
||||
"ruby" if !self_close => nested += 1,
|
||||
_ => {}
|
||||
},
|
||||
Tok::End(name) => match name.as_str() {
|
||||
"rb" | "rt" | "rp" => part = RubyPart::Base,
|
||||
"ruby" if nested > 0 => nested -= 1,
|
||||
"ruby" => break,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user