mirror of
https://github.com/typst/typst
synced 2025-07-27 14:27:56 +08:00
Compare commits
29 Commits
6ab77ccc37
...
13a9549d05
Author | SHA1 | Date | |
---|---|---|---|
|
13a9549d05 | ||
|
78355421ad | ||
|
af2253ba16 | ||
|
b036fd97ab | ||
|
e81a5a6ef2 | ||
|
c9c2315ad3 | ||
|
4bbd4e195b | ||
|
eed75ca4d6 | ||
|
a43b7e785c | ||
|
55dad02887 | ||
|
b790c6d59c | ||
|
b1c79b50d4 | ||
|
4629ede020 | ||
|
627f5b9d4f | ||
|
5661c20580 | ||
|
7897e86bcc | ||
|
8e0e0f1a3b | ||
|
0a4b72f8f6 | ||
|
c58766440c | ||
|
ea5272bb2b | ||
|
cdbf60e883 | ||
|
9a6268050f | ||
|
f51cb4b03e | ||
|
0c12828c9a | ||
|
b1a091a236 | ||
|
0264534928 | ||
|
70710deb2b | ||
|
275012d7c6 | ||
|
98802dde7e |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -40,7 +40,7 @@ jobs:
|
|||||||
sudo dpkg --add-architecture i386
|
sudo dpkg --add-architecture i386
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386
|
sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386
|
||||||
- uses: dtolnay/rust-toolchain@1.87.0
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
|
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
@ -73,7 +73,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.87.0
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
with:
|
with:
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
@ -88,7 +88,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83.0
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo check --workspace
|
- run: cargo check --workspace
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: nightly-2024-10-29
|
toolchain: nightly-2025-05-10
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo install --locked cargo-fuzz@0.12.0
|
- run: cargo install --locked cargo-fuzz@0.12.0
|
||||||
- run: cd tests/fuzz && cargo fuzz build --dev
|
- run: cd tests/fuzz && cargo fuzz build --dev
|
||||||
@ -112,6 +112,6 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
components: miri
|
components: miri
|
||||||
toolchain: nightly-2024-10-29
|
toolchain: nightly-2025-05-10
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo miri test -p typst-library test_miri
|
- run: cargo miri test -p typst-library test_miri
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.87.0
|
- uses: dtolnay/rust-toolchain@1.88.0
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
149
Cargo.lock
generated
149
Cargo.lock
generated
@ -181,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.8.0"
|
version = "2.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -214,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.21.0"
|
version = "1.23.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck_derive",
|
"bytemuck_derive",
|
||||||
]
|
]
|
||||||
@ -748,9 +748,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.0"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"libz-rs-sys",
|
"libz-rs-sys",
|
||||||
@ -964,6 +964,69 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hayro"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"hayro-interpret",
|
||||||
|
"image",
|
||||||
|
"kurbo",
|
||||||
|
"rustc-hash",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hayro-font"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"phf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hayro-interpret"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"hayro-font",
|
||||||
|
"hayro-syntax",
|
||||||
|
"kurbo",
|
||||||
|
"log",
|
||||||
|
"phf",
|
||||||
|
"qcms",
|
||||||
|
"skrifa",
|
||||||
|
"smallvec",
|
||||||
|
"yoke 0.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hayro-syntax"
|
||||||
|
version = "0.0.1"
|
||||||
|
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"kurbo",
|
||||||
|
"log",
|
||||||
|
"rustc-hash",
|
||||||
|
"smallvec",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hayro-write"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"hayro-syntax",
|
||||||
|
"log",
|
||||||
|
"pdf-writer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1198,17 +1261,11 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "if_chain"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.5"
|
version = "0.25.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
@ -1271,7 +1328,7 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"inotify-sys",
|
"inotify-sys",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@ -1367,7 +1424,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "krilla"
|
name = "krilla"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
|
source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
@ -1376,6 +1433,7 @@ dependencies = [
|
|||||||
"float-cmp 0.10.0",
|
"float-cmp 0.10.0",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"gif",
|
"gif",
|
||||||
|
"hayro-write",
|
||||||
"image-webp",
|
"image-webp",
|
||||||
"imagesize",
|
"imagesize",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -1385,6 +1443,7 @@ dependencies = [
|
|||||||
"rustybuzz",
|
"rustybuzz",
|
||||||
"siphasher",
|
"siphasher",
|
||||||
"skrifa",
|
"skrifa",
|
||||||
|
"smallvec",
|
||||||
"subsetter",
|
"subsetter",
|
||||||
"tiny-skia-path",
|
"tiny-skia-path",
|
||||||
"xmp-writer",
|
"xmp-writer",
|
||||||
@ -1395,7 +1454,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "krilla-svg"
|
name = "krilla-svg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
|
source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
@ -1408,9 +1467,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kurbo"
|
name = "kurbo"
|
||||||
version = "0.11.1"
|
version = "0.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f"
|
checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
@ -1462,16 +1521,16 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libz-rs-sys"
|
name = "libz-rs-sys"
|
||||||
version = "0.4.2"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d"
|
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zlib-rs",
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
@ -1628,7 +1687,7 @@ version = "8.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
|
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify",
|
||||||
@ -1710,7 +1769,7 @@ version = "0.10.72"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"foreign-types",
|
"foreign-types",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1847,7 +1906,7 @@ version = "0.13.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc"
|
checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
"ryu",
|
||||||
@ -2005,7 +2064,7 @@ version = "0.9.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"getopts",
|
"getopts",
|
||||||
"memchr",
|
"memchr",
|
||||||
"unicase",
|
"unicase",
|
||||||
@ -2118,7 +2177,7 @@ version = "0.5.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2221,7 +2280,7 @@ version = "0.38.44"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
@ -2240,7 +2299,7 @@ version = "0.20.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
|
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"core_maths",
|
"core_maths",
|
||||||
"log",
|
"log",
|
||||||
@ -2288,7 +2347,7 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
@ -2451,9 +2510,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
@ -2861,7 +2920,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-assets"
|
name = "typst-assets"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1"
|
source = "git+https://github.com/typst/typst-assets?rev=fbf00f9#fbf00f9539fdb0825bef4d39fb57d5986c51b756"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-cli"
|
name = "typst-cli"
|
||||||
@ -2911,7 +2970,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-dev-assets"
|
name = "typst-dev-assets"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
|
source = "git+https://github.com/typst/typst-dev-assets?rev=c6c2acf#c6c2acf6cdc31f99a23a478d3d614f8bf806a4f5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-docs"
|
name = "typst-docs"
|
||||||
@ -2943,7 +3002,6 @@ version = "0.13.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"if_chain",
|
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
"stacker",
|
"stacker",
|
||||||
"toml",
|
"toml",
|
||||||
@ -2991,7 +3049,6 @@ version = "0.13.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"if_chain",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3063,7 +3120,7 @@ name = "typst-library"
|
|||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"az",
|
"az",
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"chinese-number",
|
"chinese-number",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
@ -3075,6 +3132,7 @@ dependencies = [
|
|||||||
"fontdb",
|
"fontdb",
|
||||||
"glidesort",
|
"glidesort",
|
||||||
"hayagriva",
|
"hayagriva",
|
||||||
|
"hayro-syntax",
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
"icu_provider_blob",
|
"icu_provider_blob",
|
||||||
@ -3173,11 +3231,13 @@ version = "0.13.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"comemo",
|
"comemo",
|
||||||
|
"hayro",
|
||||||
"image",
|
"image",
|
||||||
"pixglyph",
|
"pixglyph",
|
||||||
"resvg",
|
"resvg",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
|
"typst-assets",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
@ -3191,8 +3251,10 @@ dependencies = [
|
|||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"hayro",
|
||||||
"image",
|
"image",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
|
"typst-assets",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
@ -3589,7 +3651,7 @@ version = "0.221.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083"
|
checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3724,7 +3786,7 @@ version = "0.33.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3932,13 +3994,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "2.5.0"
|
version = "4.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
|
checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"crossbeam-utils",
|
|
||||||
"flate2",
|
"flate2",
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -3947,9 +4008,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlib-rs"
|
name = "zlib-rs"
|
||||||
version = "0.4.2"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"
|
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
|
17
Cargo.toml
17
Cargo.toml
@ -5,9 +5,9 @@ resolver = "2"
|
|||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
rust-version = "1.83" # also change in ci.yml
|
rust-version = "1.88" # also change in ci.yml
|
||||||
authors = ["The Typst Project Developers"]
|
authors = ["The Typst Project Developers"]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
homepage = "https://typst.app"
|
homepage = "https://typst.app"
|
||||||
repository = "https://github.com/typst/typst"
|
repository = "https://github.com/typst/typst"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
@ -32,8 +32,8 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
|
|||||||
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
|
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
|
||||||
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
|
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
|
||||||
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
|
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
|
||||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" }
|
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fbf00f9" }
|
||||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
|
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "c6c2acf" }
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
az = "1.2"
|
az = "1.2"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
@ -61,6 +61,8 @@ fontdb = { version = "0.23", default-features = false }
|
|||||||
fs_extra = "1.3"
|
fs_extra = "1.3"
|
||||||
glidesort = "0.1.2"
|
glidesort = "0.1.2"
|
||||||
hayagriva = "0.8.1"
|
hayagriva = "0.8.1"
|
||||||
|
hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
|
||||||
|
hayro = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
|
||||||
heck = "0.5"
|
heck = "0.5"
|
||||||
hypher = "0.1.4"
|
hypher = "0.1.4"
|
||||||
icu_properties = { version = "1.4", features = ["serde"] }
|
icu_properties = { version = "1.4", features = ["serde"] }
|
||||||
@ -68,13 +70,12 @@ icu_provider = { version = "1.4", features = ["sync"] }
|
|||||||
icu_provider_adapters = "1.4"
|
icu_provider_adapters = "1.4"
|
||||||
icu_provider_blob = "1.4"
|
icu_provider_blob = "1.4"
|
||||||
icu_segmenter = { version = "1.4", features = ["serde"] }
|
icu_segmenter = { version = "1.4", features = ["serde"] }
|
||||||
if_chain = "1"
|
|
||||||
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
|
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
infer = { version = "0.19.0", default-features = false }
|
infer = { version = "0.19.0", default-features = false }
|
||||||
kamadak-exif = "0.6"
|
kamadak-exif = "0.6"
|
||||||
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] }
|
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] }
|
||||||
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" }
|
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00"}
|
||||||
kurbo = "0.11"
|
kurbo = "0.11"
|
||||||
libfuzzer-sys = "0.4"
|
libfuzzer-sys = "0.4"
|
||||||
lipsum = "0.9"
|
lipsum = "0.9"
|
||||||
@ -143,7 +144,7 @@ xmlparser = "0.13.5"
|
|||||||
xmlwriter = "0.1.0"
|
xmlwriter = "0.1.0"
|
||||||
xz2 = { version = "0.1", features = ["static"] }
|
xz2 = { version = "0.1", features = ["static"] }
|
||||||
yaml-front-matter = "0.1"
|
yaml-front-matter = "0.1"
|
||||||
zip = { version = "2.5", default-features = false, features = ["deflate"] }
|
zip = { version = "4.3", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
@ -174,7 +174,10 @@ typst help watch
|
|||||||
```
|
```
|
||||||
|
|
||||||
If you prefer an integrated IDE-like experience with autocompletion and instant
|
If you prefer an integrated IDE-like experience with autocompletion and instant
|
||||||
preview, you can also check out [Typst's free web app][app].
|
preview, you can also check out our [free web app][app]. Alternatively, there is
|
||||||
|
a community-created language server called
|
||||||
|
[Tinymist](https://myriad-dreamin.github.io/tinymist/) which is integrated into
|
||||||
|
various editor extensions.
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
The main places where the community gathers are our [Forum][forum] and our
|
The main places where the community gathers are our [Forum][forum] and our
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{File, create_dir_all};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use clap::{CommandFactory, ValueEnum};
|
use clap::{CommandFactory, ValueEnum};
|
||||||
use clap_complete::{generate_to, Shell};
|
use clap_complete::{Shell, generate_to};
|
||||||
use clap_mangen::Man;
|
use clap_mangen::Man;
|
||||||
|
|
||||||
#[path = "src/args.rs"]
|
#[path = "src/args.rs"]
|
||||||
|
@ -10,14 +10,14 @@ use ecow::eco_format;
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use pathdiff::diff_paths;
|
use pathdiff::diff_paths;
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
|
use typst::WorldExt;
|
||||||
use typst::diag::{
|
use typst::diag::{
|
||||||
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail,
|
||||||
};
|
};
|
||||||
use typst::foundations::{Datetime, Smart};
|
use typst::foundations::{Datetime, Smart};
|
||||||
use typst::html::HtmlDocument;
|
|
||||||
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
||||||
use typst::syntax::{FileId, Lines, Span};
|
use typst::syntax::{FileId, Lines, Span};
|
||||||
use typst::WorldExt;
|
use typst_html::HtmlDocument;
|
||||||
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
|
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
|
||||||
|
|
||||||
use crate::args::{
|
use crate::args::{
|
||||||
@ -513,7 +513,9 @@ fn write_make_deps(
|
|||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
else {
|
else {
|
||||||
bail!("failed to create make dependencies file because output path was not valid unicode")
|
bail!(
|
||||||
|
"failed to create make dependencies file because output path was not valid unicode"
|
||||||
|
)
|
||||||
};
|
};
|
||||||
if output_paths.is_empty() {
|
if output_paths.is_empty() {
|
||||||
bail!("failed to create make dependencies file because output was stdout")
|
bail!("failed to create make dependencies file because output was stdout")
|
||||||
|
@ -8,8 +8,8 @@ use codespan_reporting::term::termcolor::WriteColor;
|
|||||||
use typst::utils::format_duration;
|
use typst::utils::format_duration;
|
||||||
use typst_kit::download::{DownloadState, Downloader, Progress};
|
use typst_kit::download::{DownloadState, Downloader, Progress};
|
||||||
|
|
||||||
use crate::terminal::{self, TermOut};
|
|
||||||
use crate::ARGS;
|
use crate::ARGS;
|
||||||
|
use crate::terminal::{self, TermOut};
|
||||||
|
|
||||||
/// Prints download progress by writing `downloading {0}` followed by repeatedly
|
/// Prints download progress by writing `downloading {0}` followed by repeatedly
|
||||||
/// updating the last terminal line.
|
/// updating the last terminal line.
|
||||||
|
@ -4,7 +4,7 @@ use std::path::Path;
|
|||||||
use codespan_reporting::term::termcolor::{Color, ColorSpec, WriteColor};
|
use codespan_reporting::term::termcolor::{Color, ColorSpec, WriteColor};
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use fs_extra::dir::CopyOptions;
|
use fs_extra::dir::CopyOptions;
|
||||||
use typst::diag::{bail, FileError, StrResult};
|
use typst::diag::{FileError, StrResult, bail};
|
||||||
use typst::syntax::package::{
|
use typst::syntax::package::{
|
||||||
PackageManifest, PackageSpec, TemplateInfo, VersionlessPackageSpec,
|
PackageManifest, PackageSpec, TemplateInfo, VersionlessPackageSpec,
|
||||||
};
|
};
|
||||||
|
@ -21,8 +21,8 @@ use std::io::{self, Write};
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use clap::error::ErrorKind;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use clap::error::ErrorKind;
|
||||||
use codespan_reporting::term;
|
use codespan_reporting::term;
|
||||||
use codespan_reporting::term::termcolor::WriteColor;
|
use codespan_reporting::term::termcolor::WriteColor;
|
||||||
use typst::diag::HintedStrResult;
|
use typst::diag::HintedStrResult;
|
||||||
@ -102,7 +102,7 @@ fn print_error(msg: &str) -> io::Result<()> {
|
|||||||
|
|
||||||
#[cfg(not(feature = "self-update"))]
|
#[cfg(not(feature = "self-update"))]
|
||||||
mod update {
|
mod update {
|
||||||
use typst::diag::{bail, StrResult};
|
use typst::diag::{StrResult, bail};
|
||||||
|
|
||||||
use crate::args::UpdateCommand;
|
use crate::args::UpdateCommand;
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use comemo::Track;
|
use comemo::Track;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
|
use typst::World;
|
||||||
|
use typst::diag::{HintedStrResult, StrResult, Warned, bail};
|
||||||
use typst::engine::Sink;
|
use typst::engine::Sink;
|
||||||
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
||||||
use typst::layout::PagedDocument;
|
use typst::layout::PagedDocument;
|
||||||
use typst::syntax::{Span, SyntaxMode};
|
use typst::syntax::{Span, SyntaxMode};
|
||||||
use typst::World;
|
|
||||||
use typst_eval::eval_string;
|
use typst_eval::eval_string;
|
||||||
|
|
||||||
use crate::args::{QueryCommand, SerializationFormat};
|
use crate::args::{QueryCommand, SerializationFormat};
|
||||||
|
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use parking_lot::{Condvar, Mutex, MutexGuard};
|
use parking_lot::{Condvar, Mutex, MutexGuard};
|
||||||
use tiny_http::{Header, Request, Response, StatusCode};
|
use tiny_http::{Header, Request, Response, StatusCode};
|
||||||
use typst::diag::{bail, StrResult};
|
use typst::diag::{StrResult, bail};
|
||||||
|
|
||||||
use crate::args::{Input, ServerArgs};
|
use crate::args::{Input, ServerArgs};
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ impl<T> Bucket<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the current data in the bucket.
|
/// Retrieves the current data in the bucket.
|
||||||
fn get(&self) -> MutexGuard<T> {
|
fn get(&self) -> MutexGuard<'_, T> {
|
||||||
self.mutex.lock()
|
self.mutex.lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,9 @@ use std::fs::File;
|
|||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use typst::diag::{bail, StrResult};
|
|
||||||
use typst::syntax::Span;
|
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
use typst::diag::{StrResult, bail};
|
||||||
|
use typst::syntax::Span;
|
||||||
|
|
||||||
use crate::args::{CliArguments, Command};
|
use crate::args::{CliArguments, Command};
|
||||||
use crate::world::SystemWorld;
|
use crate::world::SystemWorld;
|
||||||
|
@ -6,7 +6,7 @@ use ecow::eco_format;
|
|||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use typst::diag::{bail, StrResult};
|
use typst::diag::{StrResult, bail};
|
||||||
use typst_kit::download::Downloader;
|
use typst_kit::download::Downloader;
|
||||||
use xz2::bufread::XzDecoder;
|
use xz2::bufread::XzDecoder;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
@ -10,12 +10,12 @@ use codespan_reporting::term::{self, termcolor};
|
|||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
|
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
|
||||||
use same_file::is_same_file;
|
use same_file::is_same_file;
|
||||||
use typst::diag::{bail, warning, StrResult};
|
use typst::diag::{StrResult, bail, warning};
|
||||||
use typst::syntax::Span;
|
use typst::syntax::Span;
|
||||||
use typst::utils::format_duration;
|
use typst::utils::format_duration;
|
||||||
|
|
||||||
use crate::args::{Input, Output, WatchCommand};
|
use crate::args::{Input, Output, WatchCommand};
|
||||||
use crate::compile::{compile_once, print_diagnostics, CompileConfig};
|
use crate::compile::{CompileConfig, compile_once, print_diagnostics};
|
||||||
use crate::timings::Timer;
|
use crate::timings::Timer;
|
||||||
use crate::world::{SystemWorld, WorldCreationError};
|
use crate::world::{SystemWorld, WorldCreationError};
|
||||||
use crate::{print_error, terminal};
|
use crate::{print_error, terminal};
|
||||||
|
@ -5,7 +5,7 @@ use std::sync::{LazyLock, OnceLock};
|
|||||||
use std::{fmt, fs, io, mem};
|
use std::{fmt, fs, io, mem};
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
|
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use typst::diag::{FileError, FileResult};
|
use typst::diag::{FileError, FileResult};
|
||||||
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
|
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
|
||||||
@ -361,22 +361,22 @@ impl<T: Clone> SlotCell<T> {
|
|||||||
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
|
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
|
||||||
) -> FileResult<T> {
|
) -> FileResult<T> {
|
||||||
// If we accessed the file already in this compilation, retrieve it.
|
// If we accessed the file already in this compilation, retrieve it.
|
||||||
if mem::replace(&mut self.accessed, true) {
|
if mem::replace(&mut self.accessed, true)
|
||||||
if let Some(data) = &self.data {
|
&& let Some(data) = &self.data
|
||||||
|
{
|
||||||
return data.clone();
|
return data.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Read and hash the file.
|
// Read and hash the file.
|
||||||
let result = timed!("loading file", load());
|
let result = timed!("loading file", load());
|
||||||
let fingerprint = timed!("hashing file", typst::utils::hash128(&result));
|
let fingerprint = timed!("hashing file", typst::utils::hash128(&result));
|
||||||
|
|
||||||
// If the file contents didn't change, yield the old processed data.
|
// If the file contents didn't change, yield the old processed data.
|
||||||
if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
|
if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint
|
||||||
if let Some(data) = &self.data {
|
&& let Some(data) = &self.data
|
||||||
|
{
|
||||||
return data.clone();
|
return data.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let prev = self.data.take().and_then(Result::ok);
|
let prev = self.data.take().and_then(Result::ok);
|
||||||
let value = result.and_then(|data| f(data, prev));
|
let value = result.and_then(|data| f(data, prev));
|
||||||
|
@ -20,7 +20,6 @@ typst-timing = { workspace = true }
|
|||||||
typst-utils = { workspace = true }
|
typst-utils = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
if_chain = { workspace = true }
|
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use typst_library::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
|
use typst_library::diag::{At, Hint, SourceResult, Trace, Tracepoint, bail};
|
||||||
use typst_library::foundations::{Dict, Value};
|
use typst_library::foundations::{Dict, Value};
|
||||||
use typst_syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
use crate::{call_method_access, is_accessor_method, Eval, Vm};
|
use crate::{Eval, Vm, call_method_access, is_accessor_method};
|
||||||
|
|
||||||
/// Access an expression mutably.
|
/// Access an expression mutably.
|
||||||
pub(crate) trait Access {
|
pub(crate) trait Access {
|
||||||
@ -29,11 +29,11 @@ impl Access for ast::Expr<'_> {
|
|||||||
impl Access for ast::Ident<'_> {
|
impl Access for ast::Ident<'_> {
|
||||||
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
|
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
|
||||||
let span = self.span();
|
let span = self.span();
|
||||||
if vm.inspected == Some(span) {
|
if vm.inspected == Some(span)
|
||||||
if let Ok(binding) = vm.scopes.get(&self) {
|
&& let Ok(binding) = vm.scopes.get(&self)
|
||||||
|
{
|
||||||
vm.trace(binding.read().clone());
|
vm.trace(binding.read().clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
vm.scopes
|
vm.scopes
|
||||||
.get_mut(&self)
|
.get_mut(&self)
|
||||||
.and_then(|b| b.write().map_err(Into::into))
|
.and_then(|b| b.write().map_err(Into::into))
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
|
use typst_library::diag::{At, SourceDiagnostic, SourceResult, bail, error};
|
||||||
use typst_library::foundations::{Array, Dict, Value};
|
use typst_library::foundations::{Array, Dict, Value};
|
||||||
use typst_syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
use ecow::{eco_format, EcoString, EcoVec};
|
use ecow::{EcoString, EcoVec, eco_format};
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::diag::{
|
use typst_library::diag::{
|
||||||
bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult,
|
At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult, Trace, Tracepoint,
|
||||||
Trace, Tracepoint,
|
bail, error,
|
||||||
};
|
};
|
||||||
use typst_library::engine::{Engine, Sink, Traced};
|
use typst_library::engine::{Engine, Sink, Traced};
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
@ -12,12 +13,11 @@ use typst_library::foundations::{
|
|||||||
use typst_library::introspection::Introspector;
|
use typst_library::introspection::Introspector;
|
||||||
use typst_library::math::LrElem;
|
use typst_library::math::LrElem;
|
||||||
use typst_library::routines::Routines;
|
use typst_library::routines::Routines;
|
||||||
use typst_library::World;
|
|
||||||
use typst_syntax::ast::{self, AstNode, Ident};
|
use typst_syntax::ast::{self, AstNode, Ident};
|
||||||
use typst_syntax::{Span, Spanned, SyntaxNode};
|
use typst_syntax::{Span, Spanned, SyntaxNode};
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use crate::{call_method_mut, is_mutating_method, Access, Eval, FlowEvent, Route, Vm};
|
use crate::{Access, Eval, FlowEvent, Route, Vm, call_method_mut, is_mutating_method};
|
||||||
|
|
||||||
impl Eval for ast::FuncCall<'_> {
|
impl Eval for ast::FuncCall<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use ecow::{eco_vec, EcoVec};
|
use ecow::{EcoVec, eco_vec};
|
||||||
use typst_library::diag::{bail, error, warning, At, SourceResult};
|
use typst_library::diag::{At, SourceResult, bail, error, warning};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement,
|
Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Selector,
|
||||||
Selector, Str, Value,
|
Str, Value, ops,
|
||||||
};
|
};
|
||||||
use typst_library::introspection::{Counter, State};
|
use typst_library::introspection::{Counter, State};
|
||||||
use typst_syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
@ -324,22 +324,18 @@ impl Eval for ast::FieldAccess<'_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check whether this is a get rule field access.
|
// Check whether this is a get rule field access.
|
||||||
if_chain::if_chain! {
|
if let Value::Func(func) = &value
|
||||||
if let Value::Func(func) = &value;
|
&& let Some(element) = func.element()
|
||||||
if let Some(element) = func.element();
|
&& let Some(id) = element.field_id(&field)
|
||||||
if let Some(id) = element.field_id(&field);
|
&& let styles = vm.context.styles().at(field.span())
|
||||||
let styles = vm.context.styles().at(field.span());
|
&& let Ok(value) = element
|
||||||
if let Ok(value) = element.field_from_styles(
|
.field_from_styles(id, styles.as_ref().map(|&s| s).unwrap_or_default())
|
||||||
id,
|
{
|
||||||
styles.as_ref().map(|&s| s).unwrap_or_default(),
|
|
||||||
);
|
|
||||||
then {
|
|
||||||
// Only validate the context once we know that this is indeed
|
// Only validate the context once we know that this is indeed
|
||||||
// a field from the style chain.
|
// a field from the style chain.
|
||||||
let _ = styles?;
|
let _ = styles?;
|
||||||
return Ok(value);
|
return Ok(value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
|
use typst_library::diag::{At, SourceDiagnostic, SourceResult, bail, error};
|
||||||
use typst_library::foundations::{ops, IntoValue, Value};
|
use typst_library::foundations::{IntoValue, Value, ops};
|
||||||
use typst_syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
use typst_syntax::{Span, SyntaxKind, SyntaxNode};
|
use typst_syntax::{Span, SyntaxKind, SyntaxNode};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::{destructure, Eval, Vm};
|
use crate::{Eval, Vm, destructure};
|
||||||
|
|
||||||
/// The maximum number of loop iterations.
|
/// The maximum number of loop iterations.
|
||||||
const MAX_ITERATIONS: usize = 10_000;
|
const MAX_ITERATIONS: usize = 10_000;
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
use comemo::TrackedMut;
|
use comemo::TrackedMut;
|
||||||
use ecow::{eco_format, eco_vec, EcoString};
|
use ecow::{EcoString, eco_format, eco_vec};
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::diag::{
|
use typst_library::diag::{
|
||||||
bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
|
At, FileError, SourceResult, Trace, Tracepoint, bail, error, warning,
|
||||||
};
|
};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Binding, Content, Module, Value};
|
use typst_library::foundations::{Binding, Content, Module, Value};
|
||||||
use typst_library::World;
|
|
||||||
use typst_syntax::ast::{self, AstNode, BareImportError};
|
use typst_syntax::ast::{self, AstNode, BareImportError};
|
||||||
use typst_syntax::package::{PackageManifest, PackageSpec};
|
use typst_syntax::package::{PackageManifest, PackageSpec};
|
||||||
use typst_syntax::{FileId, Span, VirtualPath};
|
use typst_syntax::{FileId, Span, VirtualPath};
|
||||||
|
|
||||||
use crate::{eval, Eval, Vm};
|
use crate::{Eval, Vm, eval};
|
||||||
|
|
||||||
impl Eval for ast::ModuleImport<'_> {
|
impl Eval for ast::ModuleImport<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
@ -46,15 +46,15 @@ impl Eval for ast::ModuleImport<'_> {
|
|||||||
// If there is a rename, import the source itself under that name.
|
// If there is a rename, import the source itself under that name.
|
||||||
let new_name = self.new_name();
|
let new_name = self.new_name();
|
||||||
if let Some(new_name) = new_name {
|
if let Some(new_name) = new_name {
|
||||||
if let ast::Expr::Ident(ident) = self.source() {
|
if let ast::Expr::Ident(ident) = self.source()
|
||||||
if ident.as_str() == new_name.as_str() {
|
&& ident.as_str() == new_name.as_str()
|
||||||
|
{
|
||||||
// Warn on `import x as x`
|
// Warn on `import x as x`
|
||||||
vm.engine.sink.warn(warning!(
|
vm.engine.sink.warn(warning!(
|
||||||
new_name.span(),
|
new_name.span(),
|
||||||
"unnecessary import rename to same name",
|
"unnecessary import rename to same name",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Define renamed module on the scope.
|
// Define renamed module on the scope.
|
||||||
vm.define(new_name, source.clone());
|
vm.define(new_name, source.clone());
|
||||||
@ -142,8 +142,8 @@ impl Eval for ast::ModuleImport<'_> {
|
|||||||
// it.
|
// it.
|
||||||
|
|
||||||
// Warn on `import ...: x as x`
|
// Warn on `import ...: x as x`
|
||||||
if let ast::ImportItem::Renamed(renamed_item) = &item {
|
if let ast::ImportItem::Renamed(renamed_item) = &item
|
||||||
if renamed_item.original_name().as_str()
|
&& renamed_item.original_name().as_str()
|
||||||
== renamed_item.new_name().as_str()
|
== renamed_item.new_name().as_str()
|
||||||
{
|
{
|
||||||
vm.engine.sink.warn(warning!(
|
vm.engine.sink.warn(warning!(
|
||||||
@ -151,7 +151,6 @@ impl Eval for ast::ModuleImport<'_> {
|
|||||||
"unnecessary import rename to same name",
|
"unnecessary import rename to same name",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
vm.bind(item.bound_name(), binding.clone());
|
vm.bind(item.bound_name(), binding.clone());
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ mod methods;
|
|||||||
mod rules;
|
mod rules;
|
||||||
mod vm;
|
mod vm;
|
||||||
|
|
||||||
pub use self::call::{eval_closure, CapturesVisitor};
|
pub use self::call::{CapturesVisitor, eval_closure};
|
||||||
pub use self::flow::FlowEvent;
|
pub use self::flow::FlowEvent;
|
||||||
pub use self::import::import;
|
pub use self::import::import;
|
||||||
pub use self::vm::Vm;
|
pub use self::vm::Vm;
|
||||||
@ -24,14 +24,14 @@ use self::binding::*;
|
|||||||
use self::methods::*;
|
use self::methods::*;
|
||||||
|
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::World;
|
||||||
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value};
|
use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value};
|
||||||
use typst_library::introspection::Introspector;
|
use typst_library::introspection::Introspector;
|
||||||
use typst_library::math::EquationElem;
|
use typst_library::math::EquationElem;
|
||||||
use typst_library::routines::Routines;
|
use typst_library::routines::Routines;
|
||||||
use typst_library::World;
|
use typst_syntax::{Source, Span, SyntaxMode, ast, parse, parse_code, parse_math};
|
||||||
use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span, SyntaxMode};
|
|
||||||
|
|
||||||
/// Evaluate a source file and return the resulting module.
|
/// Evaluate a source file and return the resulting module.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use typst_library::diag::{warning, At, SourceResult};
|
use typst_library::diag::{At, SourceResult, warning};
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value,
|
Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value,
|
||||||
};
|
};
|
||||||
@ -251,7 +251,7 @@ impl Eval for ast::EnumItem<'_> {
|
|||||||
let body = self.body().eval(vm)?;
|
let body = self.body().eval(vm)?;
|
||||||
let mut elem = EnumItem::new(body);
|
let mut elem = EnumItem::new(body);
|
||||||
if let Some(number) = self.number() {
|
if let Some(number) = self.number() {
|
||||||
elem.number.set(Some(number));
|
elem.number.set(Smart::Custom(number));
|
||||||
}
|
}
|
||||||
Ok(elem.pack())
|
Ok(elem.pack())
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use typst_library::diag::{At, HintedStrResult, SourceResult};
|
use typst_library::diag::{At, HintedStrResult, SourceResult};
|
||||||
use typst_library::foundations::{ops, IntoValue, Value};
|
use typst_library::foundations::{IntoValue, Value, ops};
|
||||||
use typst_syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
use crate::{access_dict, Access, Eval, Vm};
|
use crate::{Access, Eval, Vm, access_dict};
|
||||||
|
|
||||||
impl Eval for ast::Unary<'_> {
|
impl Eval for ast::Unary<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
@ -76,13 +76,13 @@ fn apply_assignment(
|
|||||||
|
|
||||||
// An assignment to a dictionary field is different from a normal access
|
// An assignment to a dictionary field is different from a normal access
|
||||||
// since it can create the field instead of just modifying it.
|
// since it can create the field instead of just modifying it.
|
||||||
if binary.op() == ast::BinOp::Assign {
|
if binary.op() == ast::BinOp::Assign
|
||||||
if let ast::Expr::FieldAccess(access) = lhs {
|
&& let ast::Expr::FieldAccess(access) = lhs
|
||||||
|
{
|
||||||
let dict = access_dict(vm, access)?;
|
let dict = access_dict(vm, access)?;
|
||||||
dict.insert(access.field().get().clone().into(), rhs);
|
dict.insert(access.field().get().clone().into(), rhs);
|
||||||
return Ok(Value::None);
|
return Ok(Value::None);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let location = binary.lhs().access(vm)?;
|
let location = binary.lhs().access(vm)?;
|
||||||
let lhs = std::mem::take(&mut *location);
|
let lhs = std::mem::take(&mut *location);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use typst_library::diag::{warning, At, SourceResult};
|
use typst_library::diag::{At, SourceResult, warning};
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Element, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
|
Element, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
|
||||||
};
|
};
|
||||||
@ -12,11 +12,11 @@ impl Eval for ast::SetRule<'_> {
|
|||||||
type Output = Styles;
|
type Output = Styles;
|
||||||
|
|
||||||
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||||
if let Some(condition) = self.condition() {
|
if let Some(condition) = self.condition()
|
||||||
if !condition.eval(vm)?.cast::<bool>().at(condition.span())? {
|
&& !condition.eval(vm)?.cast::<bool>().at(condition.span())?
|
||||||
|
{
|
||||||
return Ok(Styles::new());
|
return Ok(Styles::new());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let target = self.target();
|
let target = self.target();
|
||||||
let target = target
|
let target = target
|
||||||
@ -58,12 +58,11 @@ impl Eval for ast::ShowRule<'_> {
|
|||||||
|
|
||||||
/// Migration hint for `show par: set block(spacing: ..)`.
|
/// Migration hint for `show par: set block(spacing: ..)`.
|
||||||
fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) {
|
fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) {
|
||||||
if_chain::if_chain! {
|
if let Some(Selector::Elem(elem, _)) = recipe.selector()
|
||||||
if let Some(Selector::Elem(elem, _)) = recipe.selector();
|
&& *elem == Element::of::<ParElem>()
|
||||||
if *elem == Element::of::<ParElem>();
|
&& let Transformation::Style(styles) = recipe.transform()
|
||||||
if let Transformation::Style(styles) = recipe.transform();
|
&& (styles.has(BlockElem::above) || styles.has(BlockElem::below))
|
||||||
if styles.has(BlockElem::above) || styles.has(BlockElem::below);
|
{
|
||||||
then {
|
|
||||||
vm.engine.sink.warn(warning!(
|
vm.engine.sink.warn(warning!(
|
||||||
recipe.span(),
|
recipe.span(),
|
||||||
"`show par: set block(spacing: ..)` has no effect anymore";
|
"`show par: set block(spacing: ..)` has no effect anymore";
|
||||||
@ -71,5 +70,4 @@ fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) {
|
|||||||
hint: "this is specific to paragraphs as they are not considered blocks anymore"
|
hint: "this is specific to paragraphs as they are not considered blocks anymore"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::diag::warning;
|
use typst_library::diag::warning;
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value};
|
use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value};
|
||||||
use typst_library::World;
|
|
||||||
use typst_syntax::ast::{self, AstNode};
|
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
use crate::FlowEvent;
|
use crate::FlowEvent;
|
||||||
|
|
||||||
|
195
crates/typst-html/src/attr.rs
Normal file
195
crates/typst-html/src/attr.rs
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
|
||||||
|
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use crate::HtmlAttr;
|
||||||
|
|
||||||
|
pub const abbr: HtmlAttr = HtmlAttr::constant("abbr");
|
||||||
|
pub const accept: HtmlAttr = HtmlAttr::constant("accept");
|
||||||
|
pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset");
|
||||||
|
pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey");
|
||||||
|
pub const action: HtmlAttr = HtmlAttr::constant("action");
|
||||||
|
pub const allow: HtmlAttr = HtmlAttr::constant("allow");
|
||||||
|
pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen");
|
||||||
|
pub const alpha: HtmlAttr = HtmlAttr::constant("alpha");
|
||||||
|
pub const alt: HtmlAttr = HtmlAttr::constant("alt");
|
||||||
|
pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant");
|
||||||
|
pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic");
|
||||||
|
pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete");
|
||||||
|
pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy");
|
||||||
|
pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked");
|
||||||
|
pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount");
|
||||||
|
pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex");
|
||||||
|
pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan");
|
||||||
|
pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls");
|
||||||
|
pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current");
|
||||||
|
pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby");
|
||||||
|
pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details");
|
||||||
|
pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled");
|
||||||
|
pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage");
|
||||||
|
pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded");
|
||||||
|
pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto");
|
||||||
|
pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup");
|
||||||
|
pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden");
|
||||||
|
pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid");
|
||||||
|
pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts");
|
||||||
|
pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label");
|
||||||
|
pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby");
|
||||||
|
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
|
||||||
|
pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live");
|
||||||
|
pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal");
|
||||||
|
pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline");
|
||||||
|
pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable");
|
||||||
|
pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation");
|
||||||
|
pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns");
|
||||||
|
pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder");
|
||||||
|
pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset");
|
||||||
|
pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed");
|
||||||
|
pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly");
|
||||||
|
pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant");
|
||||||
|
pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required");
|
||||||
|
pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription");
|
||||||
|
pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount");
|
||||||
|
pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex");
|
||||||
|
pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan");
|
||||||
|
pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected");
|
||||||
|
pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize");
|
||||||
|
pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort");
|
||||||
|
pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax");
|
||||||
|
pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin");
|
||||||
|
pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow");
|
||||||
|
pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext");
|
||||||
|
pub const r#as: HtmlAttr = HtmlAttr::constant("as");
|
||||||
|
pub const r#async: HtmlAttr = HtmlAttr::constant("async");
|
||||||
|
pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize");
|
||||||
|
pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete");
|
||||||
|
pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect");
|
||||||
|
pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus");
|
||||||
|
pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay");
|
||||||
|
pub const blocking: HtmlAttr = HtmlAttr::constant("blocking");
|
||||||
|
pub const charset: HtmlAttr = HtmlAttr::constant("charset");
|
||||||
|
pub const checked: HtmlAttr = HtmlAttr::constant("checked");
|
||||||
|
pub const cite: HtmlAttr = HtmlAttr::constant("cite");
|
||||||
|
pub const class: HtmlAttr = HtmlAttr::constant("class");
|
||||||
|
pub const closedby: HtmlAttr = HtmlAttr::constant("closedby");
|
||||||
|
pub const color: HtmlAttr = HtmlAttr::constant("color");
|
||||||
|
pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace");
|
||||||
|
pub const cols: HtmlAttr = HtmlAttr::constant("cols");
|
||||||
|
pub const colspan: HtmlAttr = HtmlAttr::constant("colspan");
|
||||||
|
pub const command: HtmlAttr = HtmlAttr::constant("command");
|
||||||
|
pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor");
|
||||||
|
pub const content: HtmlAttr = HtmlAttr::constant("content");
|
||||||
|
pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable");
|
||||||
|
pub const controls: HtmlAttr = HtmlAttr::constant("controls");
|
||||||
|
pub const coords: HtmlAttr = HtmlAttr::constant("coords");
|
||||||
|
pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin");
|
||||||
|
pub const data: HtmlAttr = HtmlAttr::constant("data");
|
||||||
|
pub const datetime: HtmlAttr = HtmlAttr::constant("datetime");
|
||||||
|
pub const decoding: HtmlAttr = HtmlAttr::constant("decoding");
|
||||||
|
pub const default: HtmlAttr = HtmlAttr::constant("default");
|
||||||
|
pub const defer: HtmlAttr = HtmlAttr::constant("defer");
|
||||||
|
pub const dir: HtmlAttr = HtmlAttr::constant("dir");
|
||||||
|
pub const dirname: HtmlAttr = HtmlAttr::constant("dirname");
|
||||||
|
pub const disabled: HtmlAttr = HtmlAttr::constant("disabled");
|
||||||
|
pub const download: HtmlAttr = HtmlAttr::constant("download");
|
||||||
|
pub const draggable: HtmlAttr = HtmlAttr::constant("draggable");
|
||||||
|
pub const enctype: HtmlAttr = HtmlAttr::constant("enctype");
|
||||||
|
pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint");
|
||||||
|
pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority");
|
||||||
|
pub const r#for: HtmlAttr = HtmlAttr::constant("for");
|
||||||
|
pub const form: HtmlAttr = HtmlAttr::constant("form");
|
||||||
|
pub const formaction: HtmlAttr = HtmlAttr::constant("formaction");
|
||||||
|
pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype");
|
||||||
|
pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod");
|
||||||
|
pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate");
|
||||||
|
pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget");
|
||||||
|
pub const headers: HtmlAttr = HtmlAttr::constant("headers");
|
||||||
|
pub const height: HtmlAttr = HtmlAttr::constant("height");
|
||||||
|
pub const hidden: HtmlAttr = HtmlAttr::constant("hidden");
|
||||||
|
pub const high: HtmlAttr = HtmlAttr::constant("high");
|
||||||
|
pub const href: HtmlAttr = HtmlAttr::constant("href");
|
||||||
|
pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang");
|
||||||
|
pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv");
|
||||||
|
pub const id: HtmlAttr = HtmlAttr::constant("id");
|
||||||
|
pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes");
|
||||||
|
pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset");
|
||||||
|
pub const inert: HtmlAttr = HtmlAttr::constant("inert");
|
||||||
|
pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode");
|
||||||
|
pub const integrity: HtmlAttr = HtmlAttr::constant("integrity");
|
||||||
|
pub const is: HtmlAttr = HtmlAttr::constant("is");
|
||||||
|
pub const ismap: HtmlAttr = HtmlAttr::constant("ismap");
|
||||||
|
pub const itemid: HtmlAttr = HtmlAttr::constant("itemid");
|
||||||
|
pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop");
|
||||||
|
pub const itemref: HtmlAttr = HtmlAttr::constant("itemref");
|
||||||
|
pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope");
|
||||||
|
pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype");
|
||||||
|
pub const kind: HtmlAttr = HtmlAttr::constant("kind");
|
||||||
|
pub const label: HtmlAttr = HtmlAttr::constant("label");
|
||||||
|
pub const lang: HtmlAttr = HtmlAttr::constant("lang");
|
||||||
|
pub const list: HtmlAttr = HtmlAttr::constant("list");
|
||||||
|
pub const loading: HtmlAttr = HtmlAttr::constant("loading");
|
||||||
|
pub const r#loop: HtmlAttr = HtmlAttr::constant("loop");
|
||||||
|
pub const low: HtmlAttr = HtmlAttr::constant("low");
|
||||||
|
pub const max: HtmlAttr = HtmlAttr::constant("max");
|
||||||
|
pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength");
|
||||||
|
pub const media: HtmlAttr = HtmlAttr::constant("media");
|
||||||
|
pub const method: HtmlAttr = HtmlAttr::constant("method");
|
||||||
|
pub const min: HtmlAttr = HtmlAttr::constant("min");
|
||||||
|
pub const minlength: HtmlAttr = HtmlAttr::constant("minlength");
|
||||||
|
pub const multiple: HtmlAttr = HtmlAttr::constant("multiple");
|
||||||
|
pub const muted: HtmlAttr = HtmlAttr::constant("muted");
|
||||||
|
pub const name: HtmlAttr = HtmlAttr::constant("name");
|
||||||
|
pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule");
|
||||||
|
pub const nonce: HtmlAttr = HtmlAttr::constant("nonce");
|
||||||
|
pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate");
|
||||||
|
pub const open: HtmlAttr = HtmlAttr::constant("open");
|
||||||
|
pub const optimum: HtmlAttr = HtmlAttr::constant("optimum");
|
||||||
|
pub const pattern: HtmlAttr = HtmlAttr::constant("pattern");
|
||||||
|
pub const ping: HtmlAttr = HtmlAttr::constant("ping");
|
||||||
|
pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder");
|
||||||
|
pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline");
|
||||||
|
pub const popover: HtmlAttr = HtmlAttr::constant("popover");
|
||||||
|
pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget");
|
||||||
|
pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction");
|
||||||
|
pub const poster: HtmlAttr = HtmlAttr::constant("poster");
|
||||||
|
pub const preload: HtmlAttr = HtmlAttr::constant("preload");
|
||||||
|
pub const readonly: HtmlAttr = HtmlAttr::constant("readonly");
|
||||||
|
pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy");
|
||||||
|
pub const rel: HtmlAttr = HtmlAttr::constant("rel");
|
||||||
|
pub const required: HtmlAttr = HtmlAttr::constant("required");
|
||||||
|
pub const reversed: HtmlAttr = HtmlAttr::constant("reversed");
|
||||||
|
pub const role: HtmlAttr = HtmlAttr::constant("role");
|
||||||
|
pub const rows: HtmlAttr = HtmlAttr::constant("rows");
|
||||||
|
pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan");
|
||||||
|
pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox");
|
||||||
|
pub const scope: HtmlAttr = HtmlAttr::constant("scope");
|
||||||
|
pub const selected: HtmlAttr = HtmlAttr::constant("selected");
|
||||||
|
pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable");
|
||||||
|
pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry");
|
||||||
|
pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus");
|
||||||
|
pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode");
|
||||||
|
pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable");
|
||||||
|
pub const shape: HtmlAttr = HtmlAttr::constant("shape");
|
||||||
|
pub const size: HtmlAttr = HtmlAttr::constant("size");
|
||||||
|
pub const sizes: HtmlAttr = HtmlAttr::constant("sizes");
|
||||||
|
pub const slot: HtmlAttr = HtmlAttr::constant("slot");
|
||||||
|
pub const span: HtmlAttr = HtmlAttr::constant("span");
|
||||||
|
pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck");
|
||||||
|
pub const src: HtmlAttr = HtmlAttr::constant("src");
|
||||||
|
pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc");
|
||||||
|
pub const srclang: HtmlAttr = HtmlAttr::constant("srclang");
|
||||||
|
pub const srcset: HtmlAttr = HtmlAttr::constant("srcset");
|
||||||
|
pub const start: HtmlAttr = HtmlAttr::constant("start");
|
||||||
|
pub const step: HtmlAttr = HtmlAttr::constant("step");
|
||||||
|
pub const style: HtmlAttr = HtmlAttr::constant("style");
|
||||||
|
pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex");
|
||||||
|
pub const target: HtmlAttr = HtmlAttr::constant("target");
|
||||||
|
pub const title: HtmlAttr = HtmlAttr::constant("title");
|
||||||
|
pub const translate: HtmlAttr = HtmlAttr::constant("translate");
|
||||||
|
pub const r#type: HtmlAttr = HtmlAttr::constant("type");
|
||||||
|
pub const usemap: HtmlAttr = HtmlAttr::constant("usemap");
|
||||||
|
pub const value: HtmlAttr = HtmlAttr::constant("value");
|
||||||
|
pub const width: HtmlAttr = HtmlAttr::constant("width");
|
||||||
|
pub const wrap: HtmlAttr = HtmlAttr::constant("wrap");
|
||||||
|
pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions");
|
81
crates/typst-html/src/charsets.rs
Normal file
81
crates/typst-html/src/charsets.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
//! Defines syntactical properties of HTML tags, attributes, and text.
|
||||||
|
|
||||||
|
/// Check whether a character is in a tag name.
|
||||||
|
pub const fn is_valid_in_tag_name(c: char) -> bool {
|
||||||
|
c.is_ascii_alphanumeric()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a character is valid in an attribute name.
|
||||||
|
pub const fn is_valid_in_attribute_name(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
// These are forbidden.
|
||||||
|
'\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false,
|
||||||
|
c if is_whatwg_control_char(c) => false,
|
||||||
|
c if is_whatwg_non_char(c) => false,
|
||||||
|
// _Everything_ else is allowed, including U+2029 paragraph
|
||||||
|
// separator. Go wild.
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a character can be an used in an attribute value without
|
||||||
|
/// escaping.
|
||||||
|
///
|
||||||
|
/// See <https://html.spec.whatwg.org/multipage/syntax.html#attributes-2>
|
||||||
|
pub const fn is_valid_in_attribute_value(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
|
||||||
|
// ampersands_) but it is not worth the trouble to check for that.
|
||||||
|
'&' => false,
|
||||||
|
// Quotation marks are not allowed in double-quote-delimited attribute
|
||||||
|
// values.
|
||||||
|
'"' => false,
|
||||||
|
// All other text characters are allowed.
|
||||||
|
c => is_w3c_text_char(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a character can be an used in normal text without
|
||||||
|
/// escaping.
|
||||||
|
pub const fn is_valid_in_normal_element_text(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
|
||||||
|
// ampersands_) but it is not worth the trouble to check for that.
|
||||||
|
'&' => false,
|
||||||
|
// Less-than signs are not allowed in text.
|
||||||
|
'<' => false,
|
||||||
|
// All other text characters are allowed.
|
||||||
|
c => is_w3c_text_char(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if something is valid text in HTML.
|
||||||
|
pub const fn is_w3c_text_char(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
// Non-characters are obviously not text characters.
|
||||||
|
c if is_whatwg_non_char(c) => false,
|
||||||
|
// Control characters are disallowed, except for whitespace.
|
||||||
|
c if is_whatwg_control_char(c) => c.is_ascii_whitespace(),
|
||||||
|
// Everything else is allowed.
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn is_whatwg_non_char(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
'\u{fdd0}'..='\u{fdef}' => true,
|
||||||
|
// Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive).
|
||||||
|
c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn is_whatwg_control_char(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
// C0 control characters.
|
||||||
|
'\u{00}'..='\u{1f}' => true,
|
||||||
|
// Other control characters.
|
||||||
|
'\u{7f}'..='\u{9f}' => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
127
crates/typst-html/src/convert.rs
Normal file
127
crates/typst-html/src/convert.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
use typst_library::diag::{SourceResult, warning};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
|
||||||
|
use typst_library::introspection::{SplitLocator, TagElem};
|
||||||
|
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
|
||||||
|
use typst_library::model::ParElem;
|
||||||
|
use typst_library::routines::Pair;
|
||||||
|
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||||
|
|
||||||
|
use crate::fragment::html_fragment;
|
||||||
|
use crate::{FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, attr, tag};
|
||||||
|
|
||||||
|
/// Converts realized content into HTML nodes.
|
||||||
|
pub fn convert_to_nodes<'a>(
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: &mut SplitLocator,
|
||||||
|
children: impl IntoIterator<Item = Pair<'a>>,
|
||||||
|
) -> SourceResult<Vec<HtmlNode>> {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
for (child, styles) in children {
|
||||||
|
handle(engine, child, locator, styles, &mut output)?;
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert one element into HTML node(s).
|
||||||
|
fn handle(
|
||||||
|
engine: &mut Engine,
|
||||||
|
child: &Content,
|
||||||
|
locator: &mut SplitLocator,
|
||||||
|
styles: StyleChain,
|
||||||
|
output: &mut Vec<HtmlNode>,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
if let Some(elem) = child.to_packed::<TagElem>() {
|
||||||
|
output.push(HtmlNode::Tag(elem.tag.clone()));
|
||||||
|
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
|
||||||
|
let mut children = vec![];
|
||||||
|
if let Some(body) = elem.body.get_ref(styles) {
|
||||||
|
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
||||||
|
}
|
||||||
|
let element = HtmlElement {
|
||||||
|
tag: elem.tag,
|
||||||
|
attrs: elem.attrs.get_cloned(styles),
|
||||||
|
children,
|
||||||
|
span: elem.span(),
|
||||||
|
};
|
||||||
|
output.push(element.into());
|
||||||
|
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
||||||
|
let children =
|
||||||
|
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
|
||||||
|
output.push(
|
||||||
|
HtmlElement::new(tag::p)
|
||||||
|
.with_children(children)
|
||||||
|
.spanned(elem.span())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
} else if let Some(elem) = child.to_packed::<BoxElem>() {
|
||||||
|
// TODO: This is rather incomplete.
|
||||||
|
if let Some(body) = elem.body.get_ref(styles) {
|
||||||
|
let children =
|
||||||
|
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
||||||
|
output.push(
|
||||||
|
HtmlElement::new(tag::span)
|
||||||
|
.with_attr(attr::style, "display: inline-block;")
|
||||||
|
.with_children(children)
|
||||||
|
.spanned(elem.span())
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if let Some((elem, body)) =
|
||||||
|
child
|
||||||
|
.to_packed::<BlockElem>()
|
||||||
|
.and_then(|elem| match elem.body.get_ref(styles) {
|
||||||
|
Some(BlockBody::Content(body)) => Some((elem, body)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// TODO: This is rather incomplete.
|
||||||
|
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
||||||
|
output.push(
|
||||||
|
HtmlElement::new(tag::div)
|
||||||
|
.with_children(children)
|
||||||
|
.spanned(elem.span())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
} else if child.is::<SpaceElem>() {
|
||||||
|
output.push(HtmlNode::text(' ', child.span()));
|
||||||
|
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
||||||
|
let text = if let Some(case) = styles.get(TextElem::case) {
|
||||||
|
case.apply(&elem.text).into()
|
||||||
|
} else {
|
||||||
|
elem.text.clone()
|
||||||
|
};
|
||||||
|
output.push(HtmlNode::text(text, elem.span()));
|
||||||
|
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
|
||||||
|
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
|
||||||
|
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
|
||||||
|
output.push(HtmlNode::text(
|
||||||
|
if elem.double.get(styles) { '"' } else { '\'' },
|
||||||
|
child.span(),
|
||||||
|
));
|
||||||
|
} else if let Some(elem) = child.to_packed::<FrameElem>() {
|
||||||
|
let locator = locator.next(&elem.span());
|
||||||
|
let style = TargetElem::target.set(Target::Paged).wrap();
|
||||||
|
let frame = (engine.routines.layout_frame)(
|
||||||
|
engine,
|
||||||
|
&elem.body,
|
||||||
|
locator,
|
||||||
|
styles.chain(&style),
|
||||||
|
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
|
||||||
|
)?;
|
||||||
|
output.push(HtmlNode::Frame(HtmlFrame::new(frame, styles)));
|
||||||
|
} else {
|
||||||
|
engine.sink.warn(warning!(
|
||||||
|
child.span(),
|
||||||
|
"{} was ignored during HTML export",
|
||||||
|
child.elem().name()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the given element is an inline-level HTML element.
|
||||||
|
pub fn is_inline(elem: &Content) -> bool {
|
||||||
|
elem.to_packed::<HtmlElem>()
|
||||||
|
.is_some_and(|elem| tag::is_inline_by_default(elem.tag))
|
||||||
|
}
|
@ -3,28 +3,10 @@
|
|||||||
use std::fmt::{self, Display, Write};
|
use std::fmt::{self, Display, Write};
|
||||||
|
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst_library::html::{attr, HtmlElem};
|
|
||||||
use typst_library::layout::{Length, Rel};
|
use typst_library::layout::{Length, Rel};
|
||||||
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
|
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
/// Additional methods for [`HtmlElem`].
|
|
||||||
pub trait HtmlElemExt {
|
|
||||||
/// Adds the styles to an element if the property list is non-empty.
|
|
||||||
fn with_styles(self, properties: Properties) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HtmlElemExt for HtmlElem {
|
|
||||||
/// Adds CSS styles to an element.
|
|
||||||
fn with_styles(self, properties: Properties) -> Self {
|
|
||||||
if let Some(value) = properties.into_inline_styles() {
|
|
||||||
self.with_attr(attr::style, value)
|
|
||||||
} else {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A list of CSS properties with values.
|
/// A list of CSS properties with values.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Properties(EcoString);
|
pub struct Properties(EcoString);
|
||||||
|
236
crates/typst-html/src/document.rs
Normal file
236
crates/typst-html/src/document.rs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use comemo::{Tracked, TrackedMut};
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_library::diag::{SourceResult, bail};
|
||||||
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
|
use typst_library::introspection::{
|
||||||
|
Introspector, IntrospectorBuilder, Location, Locator,
|
||||||
|
};
|
||||||
|
use typst_library::layout::{Point, Position, Transform};
|
||||||
|
use typst_library::model::DocumentInfo;
|
||||||
|
use typst_library::routines::{Arenas, RealizationKind, Routines};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
use typst_utils::NonZeroExt;
|
||||||
|
|
||||||
|
use crate::{HtmlDocument, HtmlElement, HtmlNode, attr, tag};
|
||||||
|
|
||||||
|
/// Produce an HTML document from content.
|
||||||
|
///
|
||||||
|
/// This first performs root-level realization and then turns the resulting
|
||||||
|
/// elements into HTML.
|
||||||
|
#[typst_macros::time(name = "html document")]
|
||||||
|
pub fn html_document(
|
||||||
|
engine: &mut Engine,
|
||||||
|
content: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<HtmlDocument> {
|
||||||
|
html_document_impl(
|
||||||
|
engine.routines,
|
||||||
|
engine.world,
|
||||||
|
engine.introspector,
|
||||||
|
engine.traced,
|
||||||
|
TrackedMut::reborrow_mut(&mut engine.sink),
|
||||||
|
engine.route.track(),
|
||||||
|
content,
|
||||||
|
styles,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal implementation of `html_document`.
|
||||||
|
#[comemo::memoize]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn html_document_impl(
|
||||||
|
routines: &Routines,
|
||||||
|
world: Tracked<dyn World + '_>,
|
||||||
|
introspector: Tracked<Introspector>,
|
||||||
|
traced: Tracked<Traced>,
|
||||||
|
sink: TrackedMut<Sink>,
|
||||||
|
route: Tracked<Route>,
|
||||||
|
content: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<HtmlDocument> {
|
||||||
|
let mut locator = Locator::root().split();
|
||||||
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
|
world,
|
||||||
|
introspector,
|
||||||
|
traced,
|
||||||
|
sink,
|
||||||
|
route: Route::extend(route).unnested(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark the external styles as "outside" so that they are valid at the page
|
||||||
|
// level.
|
||||||
|
let styles = styles.to_map().outside();
|
||||||
|
let styles = StyleChain::new(&styles);
|
||||||
|
|
||||||
|
let arenas = Arenas::default();
|
||||||
|
let mut info = DocumentInfo::default();
|
||||||
|
let children = (engine.routines.realize)(
|
||||||
|
RealizationKind::HtmlDocument {
|
||||||
|
info: &mut info,
|
||||||
|
is_inline: crate::convert::is_inline,
|
||||||
|
},
|
||||||
|
&mut engine,
|
||||||
|
&mut locator,
|
||||||
|
&arenas,
|
||||||
|
content,
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let output = crate::convert::convert_to_nodes(
|
||||||
|
&mut engine,
|
||||||
|
&mut locator,
|
||||||
|
children.iter().copied(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut link_targets = HashSet::new();
|
||||||
|
let mut introspector = introspect_html(&output, &mut link_targets);
|
||||||
|
let mut root = root_element(output, &info)?;
|
||||||
|
crate::link::identify_link_targets(&mut root, &mut introspector, link_targets);
|
||||||
|
|
||||||
|
Ok(HtmlDocument { info, root, introspector })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Introspects HTML nodes.
|
||||||
|
#[typst_macros::time(name = "introspect html")]
|
||||||
|
fn introspect_html(
|
||||||
|
output: &[HtmlNode],
|
||||||
|
link_targets: &mut HashSet<Location>,
|
||||||
|
) -> Introspector {
|
||||||
|
fn discover(
|
||||||
|
builder: &mut IntrospectorBuilder,
|
||||||
|
sink: &mut Vec<(Content, Position)>,
|
||||||
|
link_targets: &mut HashSet<Location>,
|
||||||
|
nodes: &[HtmlNode],
|
||||||
|
) {
|
||||||
|
for node in nodes {
|
||||||
|
match node {
|
||||||
|
HtmlNode::Tag(tag) => {
|
||||||
|
builder.discover_in_tag(
|
||||||
|
sink,
|
||||||
|
tag,
|
||||||
|
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
HtmlNode::Text(_, _) => {}
|
||||||
|
HtmlNode::Element(elem) => {
|
||||||
|
discover(builder, sink, link_targets, &elem.children)
|
||||||
|
}
|
||||||
|
HtmlNode::Frame(frame) => {
|
||||||
|
builder.discover_in_frame(
|
||||||
|
sink,
|
||||||
|
&frame.inner,
|
||||||
|
NonZeroUsize::ONE,
|
||||||
|
Transform::identity(),
|
||||||
|
);
|
||||||
|
crate::link::introspect_frame_links(&frame.inner, link_targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut elems = Vec::new();
|
||||||
|
let mut builder = IntrospectorBuilder::new();
|
||||||
|
discover(&mut builder, &mut elems, link_targets, output);
|
||||||
|
builder.finalize(elems)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
|
||||||
|
/// supplying a suitable `<head>`.
|
||||||
|
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
|
||||||
|
let head = head_element(info);
|
||||||
|
let body = match classify_output(output)? {
|
||||||
|
OutputKind::Html(element) => return Ok(element),
|
||||||
|
OutputKind::Body(body) => body,
|
||||||
|
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
|
||||||
|
};
|
||||||
|
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a `<head>` element.
|
||||||
|
fn head_element(info: &DocumentInfo) -> HtmlElement {
|
||||||
|
let mut children = vec![];
|
||||||
|
|
||||||
|
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
|
||||||
|
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::meta)
|
||||||
|
.with_attr(attr::name, "viewport")
|
||||||
|
.with_attr(attr::content, "width=device-width, initial-scale=1")
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(title) = &info.title {
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::title)
|
||||||
|
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = &info.description {
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::meta)
|
||||||
|
.with_attr(attr::name, "description")
|
||||||
|
.with_attr(attr::content, description.clone())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.author.is_empty() {
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::meta)
|
||||||
|
.with_attr(attr::name, "authors")
|
||||||
|
.with_attr(attr::content, info.author.join(", "))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.keywords.is_empty() {
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::meta)
|
||||||
|
.with_attr(attr::name, "keywords")
|
||||||
|
.with_attr(attr::content, info.keywords.join(", "))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
HtmlElement::new(tag::head).with_children(children)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine which kind of output the user generated.
|
||||||
|
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
|
||||||
|
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
|
||||||
|
for node in &mut output {
|
||||||
|
let HtmlNode::Element(elem) = node else { continue };
|
||||||
|
let tag = elem.tag;
|
||||||
|
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
|
||||||
|
match (tag, count) {
|
||||||
|
(tag::html, 1) => return Ok(OutputKind::Html(take())),
|
||||||
|
(tag::body, 1) => return Ok(OutputKind::Body(take())),
|
||||||
|
(tag::html | tag::body, _) => bail!(
|
||||||
|
elem.span,
|
||||||
|
"`{}` element must be the only element in the document",
|
||||||
|
elem.tag,
|
||||||
|
),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(OutputKind::Leafs(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What kinds of output the user generated.
|
||||||
|
enum OutputKind {
|
||||||
|
/// The user generated their own `<html>` element. We do not need to supply
|
||||||
|
/// one.
|
||||||
|
Html(HtmlElement),
|
||||||
|
/// The user generate their own `<body>` element. We do not need to supply
|
||||||
|
/// one, but need supply the `<html>` element.
|
||||||
|
Body(HtmlElement),
|
||||||
|
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
|
||||||
|
Leafs(Vec<HtmlNode>),
|
||||||
|
}
|
308
crates/typst-html/src/dom.rs
Normal file
308
crates/typst-html/src/dom.rs
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
|
||||||
|
use ecow::{EcoString, EcoVec};
|
||||||
|
use typst_library::diag::{HintedStrResult, StrResult, bail};
|
||||||
|
use typst_library::foundations::{Dict, Repr, Str, StyleChain, cast};
|
||||||
|
use typst_library::introspection::{Introspector, Tag};
|
||||||
|
use typst_library::layout::{Abs, Frame, Point};
|
||||||
|
use typst_library::model::DocumentInfo;
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_syntax::Span;
|
||||||
|
use typst_utils::{PicoStr, ResolvedPicoStr};
|
||||||
|
|
||||||
|
use crate::charsets;
|
||||||
|
|
||||||
|
/// An HTML document.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HtmlDocument {
|
||||||
|
/// The document's root HTML element.
|
||||||
|
pub root: HtmlElement,
|
||||||
|
/// Details about the document.
|
||||||
|
pub info: DocumentInfo,
|
||||||
|
/// Provides the ability to execute queries on the document.
|
||||||
|
pub introspector: Introspector,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A child of an HTML element.
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub enum HtmlNode {
|
||||||
|
/// An introspectable element that produced something within this node.
|
||||||
|
Tag(Tag),
|
||||||
|
/// Plain text.
|
||||||
|
Text(EcoString, Span),
|
||||||
|
/// Another element.
|
||||||
|
Element(HtmlElement),
|
||||||
|
/// Layouted content that will be embedded into HTML as an SVG.
|
||||||
|
Frame(HtmlFrame),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlNode {
|
||||||
|
/// Create a plain text node.
|
||||||
|
pub fn text(text: impl Into<EcoString>, span: Span) -> Self {
|
||||||
|
Self::Text(text.into(), span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HtmlElement> for HtmlNode {
|
||||||
|
fn from(element: HtmlElement) -> Self {
|
||||||
|
Self::Element(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An HTML element.
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub struct HtmlElement {
|
||||||
|
/// The HTML tag.
|
||||||
|
pub tag: HtmlTag,
|
||||||
|
/// The element's attributes.
|
||||||
|
pub attrs: HtmlAttrs,
|
||||||
|
/// The element's children.
|
||||||
|
pub children: Vec<HtmlNode>,
|
||||||
|
/// The span from which the element originated, if any.
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlElement {
|
||||||
|
/// Create a new, blank element without attributes or children.
|
||||||
|
pub fn new(tag: HtmlTag) -> Self {
|
||||||
|
Self {
|
||||||
|
tag,
|
||||||
|
attrs: HtmlAttrs::default(),
|
||||||
|
children: vec![],
|
||||||
|
span: Span::detached(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach children to the element.
|
||||||
|
///
|
||||||
|
/// Note: This overwrites potential previous children.
|
||||||
|
pub fn with_children(mut self, children: Vec<HtmlNode>) -> Self {
|
||||||
|
self.children = children;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an atribute to the element.
|
||||||
|
pub fn with_attr(mut self, key: HtmlAttr, value: impl Into<EcoString>) -> Self {
|
||||||
|
self.attrs.push(key, value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach a span to the element.
|
||||||
|
pub fn spanned(mut self, span: Span) -> Self {
|
||||||
|
self.span = span;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tag of an HTML element.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct HtmlTag(PicoStr);
|
||||||
|
|
||||||
|
impl HtmlTag {
|
||||||
|
/// Intern an HTML tag string at runtime.
|
||||||
|
pub fn intern(string: &str) -> StrResult<Self> {
|
||||||
|
if string.is_empty() {
|
||||||
|
bail!("tag name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) {
|
||||||
|
bail!("the character {} is not valid in a tag name", c.repr());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(PicoStr::intern(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a compile-time constant `HtmlTag`.
|
||||||
|
///
|
||||||
|
/// Should only be used in const contexts because it can panic.
|
||||||
|
#[track_caller]
|
||||||
|
pub const fn constant(string: &'static str) -> Self {
|
||||||
|
if string.is_empty() {
|
||||||
|
panic!("tag name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = string.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < bytes.len() {
|
||||||
|
if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) {
|
||||||
|
panic!("not all characters are valid in a tag name");
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(PicoStr::constant(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the tag to a string.
|
||||||
|
pub fn resolve(self) -> ResolvedPicoStr {
|
||||||
|
self.0.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the tag into its inner interned string.
|
||||||
|
pub const fn into_inner(self) -> PicoStr {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for HtmlTag {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for HtmlTag {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "<{}>", self.resolve())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
HtmlTag,
|
||||||
|
self => self.0.resolve().as_str().into_value(),
|
||||||
|
v: Str => Self::intern(&v)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attributes of an HTML element.
|
||||||
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
|
||||||
|
|
||||||
|
impl HtmlAttrs {
|
||||||
|
/// Creates an empty attribute list.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an attribute.
|
||||||
|
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||||
|
self.0.push((attr, value.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an attribute to the start of the list.
|
||||||
|
pub fn push_front(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||||
|
self.0.insert(0, (attr, value.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds an attribute value.
|
||||||
|
pub fn get(&self, attr: HtmlAttr) -> Option<&EcoString> {
|
||||||
|
self.0.iter().find(|&&(k, _)| k == attr).map(|(_, v)| v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
HtmlAttrs,
|
||||||
|
self => self.0
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, value)| (key.resolve().as_str().into(), value.into_value()))
|
||||||
|
.collect::<Dict>()
|
||||||
|
.into_value(),
|
||||||
|
values: Dict => Self(values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
let attr = HtmlAttr::intern(&k)?;
|
||||||
|
let value = v.cast::<EcoString>()?;
|
||||||
|
Ok((attr, value))
|
||||||
|
})
|
||||||
|
.collect::<HintedStrResult<_>>()?),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An attribute of an HTML element.
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct HtmlAttr(PicoStr);
|
||||||
|
|
||||||
|
impl HtmlAttr {
|
||||||
|
/// Intern an HTML attribute string at runtime.
|
||||||
|
pub fn intern(string: &str) -> StrResult<Self> {
|
||||||
|
if string.is_empty() {
|
||||||
|
bail!("attribute name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) =
|
||||||
|
string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c))
|
||||||
|
{
|
||||||
|
bail!("the character {} is not valid in an attribute name", c.repr());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(PicoStr::intern(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a compile-time constant `HtmlAttr`.
|
||||||
|
///
|
||||||
|
/// Must only be used in const contexts (in a constant definition or
|
||||||
|
/// explicit `const { .. }` block) because otherwise a panic for a malformed
|
||||||
|
/// attribute or not auto-internible constant will only be caught at
|
||||||
|
/// runtime.
|
||||||
|
#[track_caller]
|
||||||
|
pub const fn constant(string: &'static str) -> Self {
|
||||||
|
if string.is_empty() {
|
||||||
|
panic!("attribute name must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = string.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < bytes.len() {
|
||||||
|
if !bytes[i].is_ascii()
|
||||||
|
|| !charsets::is_valid_in_attribute_name(bytes[i] as char)
|
||||||
|
{
|
||||||
|
panic!("not all characters are valid in an attribute name");
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(PicoStr::constant(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the attribute to a string.
|
||||||
|
pub fn resolve(self) -> ResolvedPicoStr {
|
||||||
|
self.0.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the attribute into its inner interned string.
|
||||||
|
pub const fn into_inner(self) -> PicoStr {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for HtmlAttr {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for HtmlAttr {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.resolve())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
HtmlAttr,
|
||||||
|
self => self.0.resolve().as_str().into_value(),
|
||||||
|
v: Str => Self::intern(&v)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layouted content that will be embedded into HTML as an SVG.
|
||||||
|
#[derive(Debug, Clone, Hash)]
|
||||||
|
pub struct HtmlFrame {
|
||||||
|
/// The frame that will be displayed as an SVG.
|
||||||
|
pub inner: Frame,
|
||||||
|
/// The text size where the frame was defined. This is used to size the
|
||||||
|
/// frame with em units to make text in and outside of the frame sized
|
||||||
|
/// consistently.
|
||||||
|
pub text_size: Abs,
|
||||||
|
/// An ID to assign to the SVG itself.
|
||||||
|
pub id: Option<EcoString>,
|
||||||
|
/// IDs to assign to destination jump points within the SVG.
|
||||||
|
pub link_points: Vec<(Point, EcoString)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlFrame {
|
||||||
|
/// Wraps a laid-out frame.
|
||||||
|
pub fn new(inner: Frame, styles: StyleChain) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
text_size: styles.resolve(TextElem::size),
|
||||||
|
id: None,
|
||||||
|
link_points: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,17 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use typst_library::diag::{bail, At, SourceResult, StrResult};
|
use typst_library::diag::{At, SourceResult, StrResult, bail};
|
||||||
use typst_library::foundations::Repr;
|
use typst_library::foundations::Repr;
|
||||||
use typst_library::html::{
|
use typst_library::introspection::Introspector;
|
||||||
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
|
|
||||||
};
|
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag, attr, charsets, tag,
|
||||||
|
};
|
||||||
|
|
||||||
/// Encodes an HTML document into a string.
|
/// Encodes an HTML document into a string.
|
||||||
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
||||||
let mut w = Writer { pretty: true, ..Writer::default() };
|
let mut w = Writer::new(&document.introspector, true);
|
||||||
w.buf.push_str("<!DOCTYPE html>");
|
w.buf.push_str("<!DOCTYPE html>");
|
||||||
write_indent(&mut w);
|
write_indent(&mut w);
|
||||||
write_element(&mut w, &document.root)?;
|
write_element(&mut w, &document.root)?;
|
||||||
@ -19,16 +21,25 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
|||||||
Ok(w.buf)
|
Ok(w.buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
/// Encodes HTML.
|
||||||
struct Writer {
|
struct Writer<'a> {
|
||||||
/// The output buffer.
|
/// The output buffer.
|
||||||
buf: String,
|
buf: String,
|
||||||
/// The current indentation level
|
/// The current indentation level
|
||||||
level: usize,
|
level: usize,
|
||||||
|
/// The document's introspector.
|
||||||
|
introspector: &'a Introspector,
|
||||||
/// Whether pretty printing is enabled.
|
/// Whether pretty printing is enabled.
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Writer<'a> {
|
||||||
|
/// Creates a new writer.
|
||||||
|
fn new(introspector: &'a Introspector, pretty: bool) -> Self {
|
||||||
|
Self { buf: String::new(), level: 0, introspector, pretty }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Writes a newline and indent, if pretty printing is enabled.
|
/// Writes a newline and indent, if pretty printing is enabled.
|
||||||
fn write_indent(w: &mut Writer) {
|
fn write_indent(w: &mut Writer) {
|
||||||
if w.pretty {
|
if w.pretty {
|
||||||
@ -120,6 +131,7 @@ fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
|||||||
let pretty_inside = allows_pretty_inside(element.tag)
|
let pretty_inside = allows_pretty_inside(element.tag)
|
||||||
&& element.children.iter().any(|node| match node {
|
&& element.children.iter().any(|node| match node {
|
||||||
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
HtmlNode::Element(child) => wants_pretty_around(child.tag),
|
||||||
|
HtmlNode::Frame(_) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -250,11 +262,7 @@ impl RawMode {
|
|||||||
{
|
{
|
||||||
// Template literals can be multi-line, so indent may change
|
// Template literals can be multi-line, so indent may change
|
||||||
// the semantics of the JavaScript.
|
// the semantics of the JavaScript.
|
||||||
if text.contains('`') {
|
if text.contains('`') { Self::Wrap } else { Self::Indent }
|
||||||
Self::Wrap
|
|
||||||
} else {
|
|
||||||
Self::Indent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tag::style => Self::Indent,
|
tag::style => Self::Indent,
|
||||||
_ => Self::Keep,
|
_ => Self::Keep,
|
||||||
@ -304,14 +312,12 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
|
|||||||
|
|
||||||
/// Encode a laid out frame into the writer.
|
/// Encode a laid out frame into the writer.
|
||||||
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
|
||||||
// FIXME: This string replacement is obviously a hack.
|
let svg = typst_svg::svg_html_frame(
|
||||||
let svg = typst_svg::svg_frame(&frame.inner).replace(
|
&frame.inner,
|
||||||
"<svg class",
|
frame.text_size,
|
||||||
&format!(
|
frame.id.as_deref(),
|
||||||
"<svg style=\"overflow: visible; width: {}em; height: {}em;\" class",
|
&frame.link_points,
|
||||||
frame.inner.width() / frame.text_size,
|
w.introspector,
|
||||||
frame.inner.height() / frame.text_size,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
w.buf.push_str(&svg);
|
w.buf.push_str(&svg);
|
||||||
}
|
}
|
||||||
|
76
crates/typst-html/src/fragment.rs
Normal file
76
crates/typst-html/src/fragment.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
|
use typst_library::diag::{At, SourceResult};
|
||||||
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
|
use typst_library::introspection::{Introspector, Locator, LocatorLink};
|
||||||
|
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_library::routines::{Arenas, FragmentKind, RealizationKind, Routines};
|
||||||
|
|
||||||
|
use crate::HtmlNode;
|
||||||
|
|
||||||
|
/// Produce HTML nodes from content.
|
||||||
|
#[typst_macros::time(name = "html fragment")]
|
||||||
|
pub fn html_fragment(
|
||||||
|
engine: &mut Engine,
|
||||||
|
content: &Content,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Vec<HtmlNode>> {
|
||||||
|
html_fragment_impl(
|
||||||
|
engine.routines,
|
||||||
|
engine.world,
|
||||||
|
engine.introspector,
|
||||||
|
engine.traced,
|
||||||
|
TrackedMut::reborrow_mut(&mut engine.sink),
|
||||||
|
engine.route.track(),
|
||||||
|
content,
|
||||||
|
locator.track(),
|
||||||
|
styles,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cached, internal implementation of [`html_fragment`].
|
||||||
|
#[comemo::memoize]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn html_fragment_impl(
|
||||||
|
routines: &Routines,
|
||||||
|
world: Tracked<dyn World + '_>,
|
||||||
|
introspector: Tracked<Introspector>,
|
||||||
|
traced: Tracked<Traced>,
|
||||||
|
sink: TrackedMut<Sink>,
|
||||||
|
route: Tracked<Route>,
|
||||||
|
content: &Content,
|
||||||
|
locator: Tracked<Locator>,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Vec<HtmlNode>> {
|
||||||
|
let link = LocatorLink::new(locator);
|
||||||
|
let mut locator = Locator::link(&link).split();
|
||||||
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
|
world,
|
||||||
|
introspector,
|
||||||
|
traced,
|
||||||
|
sink,
|
||||||
|
route: Route::extend(route),
|
||||||
|
};
|
||||||
|
|
||||||
|
engine.route.check_html_depth().at(content.span())?;
|
||||||
|
|
||||||
|
let arenas = Arenas::default();
|
||||||
|
let children = (engine.routines.realize)(
|
||||||
|
// No need to know about the `FragmentKind` because we handle both
|
||||||
|
// uniformly.
|
||||||
|
RealizationKind::HtmlFragment {
|
||||||
|
kind: &mut FragmentKind::Block,
|
||||||
|
is_inline: crate::convert::is_inline,
|
||||||
|
},
|
||||||
|
&mut engine,
|
||||||
|
&mut locator,
|
||||||
|
&arenas,
|
||||||
|
content,
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
crate::convert::convert_to_nodes(&mut engine, &mut locator, children.iter().copied())
|
||||||
|
}
|
@ -1,33 +1,29 @@
|
|||||||
//! Typst's HTML exporter.
|
//! Typst's HTML exporter.
|
||||||
|
|
||||||
|
mod attr;
|
||||||
|
mod charsets;
|
||||||
|
mod convert;
|
||||||
mod css;
|
mod css;
|
||||||
|
mod document;
|
||||||
|
mod dom;
|
||||||
mod encode;
|
mod encode;
|
||||||
|
mod fragment;
|
||||||
|
mod link;
|
||||||
mod rules;
|
mod rules;
|
||||||
|
mod tag;
|
||||||
mod typed;
|
mod typed;
|
||||||
|
|
||||||
|
pub use self::document::html_document;
|
||||||
|
pub use self::dom::*;
|
||||||
pub use self::encode::html;
|
pub use self::encode::html;
|
||||||
pub use self::rules::register;
|
pub use self::rules::register;
|
||||||
|
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use ecow::EcoString;
|
||||||
use typst_library::diag::{bail, warning, At, SourceResult};
|
use typst_library::Category;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::foundations::{Content, Module, Scope};
|
||||||
use typst_library::foundations::{
|
use typst_macros::elem;
|
||||||
Content, Module, Scope, StyleChain, Target, TargetElem,
|
|
||||||
};
|
|
||||||
use typst_library::html::{
|
|
||||||
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
|
|
||||||
};
|
|
||||||
use typst_library::introspection::{
|
|
||||||
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
|
|
||||||
};
|
|
||||||
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
|
|
||||||
use typst_library::model::{DocumentInfo, ParElem};
|
|
||||||
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
|
||||||
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
|
||||||
use typst_library::{Category, World};
|
|
||||||
use typst_syntax::Span;
|
|
||||||
|
|
||||||
/// Create a module with all HTML definitions.
|
/// Creates the module with all HTML definitions.
|
||||||
pub fn module() -> Module {
|
pub fn module() -> Module {
|
||||||
let mut html = Scope::deduplicating();
|
let mut html = Scope::deduplicating();
|
||||||
html.start_category(Category::Html);
|
html.start_category(Category::Html);
|
||||||
@ -37,337 +33,86 @@ pub fn module() -> Module {
|
|||||||
Module::new("html", html)
|
Module::new("html", html)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Produce an HTML document from content.
|
/// An HTML element that can contain Typst content.
|
||||||
///
|
///
|
||||||
/// This first performs root-level realization and then turns the resulting
|
/// Typst's HTML export automatically generates the appropriate tags for most
|
||||||
/// elements into HTML.
|
/// elements. However, sometimes, it is desirable to retain more control. For
|
||||||
#[typst_macros::time(name = "html document")]
|
/// example, when using Typst to generate your blog, you could use this function
|
||||||
pub fn html_document(
|
/// to wrap each article in an `<article>` tag.
|
||||||
engine: &mut Engine,
|
///
|
||||||
content: &Content,
|
/// Typst is aware of what is valid HTML. A tag and its attributes must form
|
||||||
styles: StyleChain,
|
/// syntactically valid HTML. Some tags, like `meta` do not accept content.
|
||||||
) -> SourceResult<HtmlDocument> {
|
/// Hence, you must not provide a body for them. We may add more checks in the
|
||||||
html_document_impl(
|
/// future, so be sure that you are generating valid HTML when using this
|
||||||
engine.routines,
|
/// function.
|
||||||
engine.world,
|
///
|
||||||
engine.introspector,
|
/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
|
||||||
engine.traced,
|
/// you instead create them with this function, Typst will omit its own tags.
|
||||||
TrackedMut::reborrow_mut(&mut engine.sink),
|
///
|
||||||
engine.route.track(),
|
/// ```typ
|
||||||
content,
|
/// #html.elem("div", attrs: (style: "background: aqua"))[
|
||||||
styles,
|
/// A div with _Typst content_ inside!
|
||||||
)
|
/// ]
|
||||||
|
/// ```
|
||||||
|
#[elem(name = "elem")]
|
||||||
|
pub struct HtmlElem {
|
||||||
|
/// The element's tag.
|
||||||
|
#[required]
|
||||||
|
pub tag: HtmlTag,
|
||||||
|
|
||||||
|
/// The element's HTML attributes.
|
||||||
|
pub attrs: HtmlAttrs,
|
||||||
|
|
||||||
|
/// The contents of the HTML element.
|
||||||
|
///
|
||||||
|
/// The body can be arbitrary Typst content.
|
||||||
|
#[positional]
|
||||||
|
pub body: Option<Content>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The internal implementation of `html_document`.
|
impl HtmlElem {
|
||||||
#[comemo::memoize]
|
/// Add an attribute to the element.
|
||||||
#[allow(clippy::too_many_arguments)]
|
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
|
||||||
fn html_document_impl(
|
self.attrs
|
||||||
routines: &Routines,
|
.as_option_mut()
|
||||||
world: Tracked<dyn World + '_>,
|
.get_or_insert_with(Default::default)
|
||||||
introspector: Tracked<Introspector>,
|
.push(attr, value);
|
||||||
traced: Tracked<Traced>,
|
self
|
||||||
sink: TrackedMut<Sink>,
|
|
||||||
route: Tracked<Route>,
|
|
||||||
content: &Content,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<HtmlDocument> {
|
|
||||||
let mut locator = Locator::root().split();
|
|
||||||
let mut engine = Engine {
|
|
||||||
routines,
|
|
||||||
world,
|
|
||||||
introspector,
|
|
||||||
traced,
|
|
||||||
sink,
|
|
||||||
route: Route::extend(route).unnested(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mark the external styles as "outside" so that they are valid at the page
|
|
||||||
// level.
|
|
||||||
let styles = styles.to_map().outside();
|
|
||||||
let styles = StyleChain::new(&styles);
|
|
||||||
|
|
||||||
let arenas = Arenas::default();
|
|
||||||
let mut info = DocumentInfo::default();
|
|
||||||
let children = (engine.routines.realize)(
|
|
||||||
RealizationKind::HtmlDocument(&mut info),
|
|
||||||
&mut engine,
|
|
||||||
&mut locator,
|
|
||||||
&arenas,
|
|
||||||
content,
|
|
||||||
styles,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
|
|
||||||
let introspector = Introspector::html(&output);
|
|
||||||
let root = root_element(output, &info)?;
|
|
||||||
|
|
||||||
Ok(HtmlDocument { info, root, introspector })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produce HTML nodes from content.
|
|
||||||
#[typst_macros::time(name = "html fragment")]
|
|
||||||
pub fn html_fragment(
|
|
||||||
engine: &mut Engine,
|
|
||||||
content: &Content,
|
|
||||||
locator: Locator,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Vec<HtmlNode>> {
|
|
||||||
html_fragment_impl(
|
|
||||||
engine.routines,
|
|
||||||
engine.world,
|
|
||||||
engine.introspector,
|
|
||||||
engine.traced,
|
|
||||||
TrackedMut::reborrow_mut(&mut engine.sink),
|
|
||||||
engine.route.track(),
|
|
||||||
content,
|
|
||||||
locator.track(),
|
|
||||||
styles,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The cached, internal implementation of [`html_fragment`].
|
|
||||||
#[comemo::memoize]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn html_fragment_impl(
|
|
||||||
routines: &Routines,
|
|
||||||
world: Tracked<dyn World + '_>,
|
|
||||||
introspector: Tracked<Introspector>,
|
|
||||||
traced: Tracked<Traced>,
|
|
||||||
sink: TrackedMut<Sink>,
|
|
||||||
route: Tracked<Route>,
|
|
||||||
content: &Content,
|
|
||||||
locator: Tracked<Locator>,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Vec<HtmlNode>> {
|
|
||||||
let link = LocatorLink::new(locator);
|
|
||||||
let mut locator = Locator::link(&link).split();
|
|
||||||
let mut engine = Engine {
|
|
||||||
routines,
|
|
||||||
world,
|
|
||||||
introspector,
|
|
||||||
traced,
|
|
||||||
sink,
|
|
||||||
route: Route::extend(route),
|
|
||||||
};
|
|
||||||
|
|
||||||
engine.route.check_html_depth().at(content.span())?;
|
|
||||||
|
|
||||||
let arenas = Arenas::default();
|
|
||||||
let children = (engine.routines.realize)(
|
|
||||||
// No need to know about the `FragmentKind` because we handle both
|
|
||||||
// uniformly.
|
|
||||||
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
|
|
||||||
&mut engine,
|
|
||||||
&mut locator,
|
|
||||||
&arenas,
|
|
||||||
content,
|
|
||||||
styles,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
handle_list(&mut engine, &mut locator, children.iter().copied())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert children into HTML nodes.
|
|
||||||
fn handle_list<'a>(
|
|
||||||
engine: &mut Engine,
|
|
||||||
locator: &mut SplitLocator,
|
|
||||||
children: impl IntoIterator<Item = Pair<'a>>,
|
|
||||||
) -> SourceResult<Vec<HtmlNode>> {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
for (child, styles) in children {
|
|
||||||
handle(engine, child, locator, styles, &mut output)?;
|
|
||||||
}
|
}
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a child into HTML node(s).
|
/// Adds the attribute to the element if value is not `None`.
|
||||||
fn handle(
|
pub fn with_optional_attr(
|
||||||
engine: &mut Engine,
|
self,
|
||||||
child: &Content,
|
attr: HtmlAttr,
|
||||||
locator: &mut SplitLocator,
|
value: Option<impl Into<EcoString>>,
|
||||||
styles: StyleChain,
|
) -> Self {
|
||||||
output: &mut Vec<HtmlNode>,
|
if let Some(value) = value { self.with_attr(attr, value) } else { self }
|
||||||
) -> SourceResult<()> {
|
|
||||||
if let Some(elem) = child.to_packed::<TagElem>() {
|
|
||||||
output.push(HtmlNode::Tag(elem.tag.clone()));
|
|
||||||
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
|
|
||||||
let mut children = vec![];
|
|
||||||
if let Some(body) = elem.body.get_ref(styles) {
|
|
||||||
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
|
||||||
}
|
}
|
||||||
let element = HtmlElement {
|
|
||||||
tag: elem.tag,
|
/// Adds CSS styles to an element.
|
||||||
attrs: elem.attrs.get_cloned(styles),
|
fn with_styles(self, properties: css::Properties) -> Self {
|
||||||
children,
|
if let Some(value) = properties.into_inline_styles() {
|
||||||
span: elem.span(),
|
self.with_attr(attr::style, value)
|
||||||
};
|
|
||||||
output.push(element.into());
|
|
||||||
} else if let Some(elem) = child.to_packed::<ParElem>() {
|
|
||||||
let children =
|
|
||||||
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
|
|
||||||
output.push(
|
|
||||||
HtmlElement::new(tag::p)
|
|
||||||
.with_children(children)
|
|
||||||
.spanned(elem.span())
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
} else if let Some(elem) = child.to_packed::<BoxElem>() {
|
|
||||||
// TODO: This is rather incomplete.
|
|
||||||
if let Some(body) = elem.body.get_ref(styles) {
|
|
||||||
let children =
|
|
||||||
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
|
||||||
output.push(
|
|
||||||
HtmlElement::new(tag::span)
|
|
||||||
.with_attr(attr::style, "display: inline-block;")
|
|
||||||
.with_children(children)
|
|
||||||
.spanned(elem.span())
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if let Some((elem, body)) =
|
|
||||||
child
|
|
||||||
.to_packed::<BlockElem>()
|
|
||||||
.and_then(|elem| match elem.body.get_ref(styles) {
|
|
||||||
Some(BlockBody::Content(body)) => Some((elem, body)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// TODO: This is rather incomplete.
|
|
||||||
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
|
|
||||||
output.push(
|
|
||||||
HtmlElement::new(tag::div)
|
|
||||||
.with_children(children)
|
|
||||||
.spanned(elem.span())
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
} else if child.is::<SpaceElem>() {
|
|
||||||
output.push(HtmlNode::text(' ', child.span()));
|
|
||||||
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
|
||||||
output.push(HtmlNode::text(elem.text.clone(), elem.span()));
|
|
||||||
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
|
|
||||||
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
|
|
||||||
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
|
|
||||||
output.push(HtmlNode::text(
|
|
||||||
if elem.double.get(styles) { '"' } else { '\'' },
|
|
||||||
child.span(),
|
|
||||||
));
|
|
||||||
} else if let Some(elem) = child.to_packed::<FrameElem>() {
|
|
||||||
let locator = locator.next(&elem.span());
|
|
||||||
let style = TargetElem::target.set(Target::Paged).wrap();
|
|
||||||
let frame = (engine.routines.layout_frame)(
|
|
||||||
engine,
|
|
||||||
&elem.body,
|
|
||||||
locator,
|
|
||||||
styles.chain(&style),
|
|
||||||
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
|
|
||||||
)?;
|
|
||||||
output.push(HtmlNode::Frame(HtmlFrame {
|
|
||||||
inner: frame,
|
|
||||||
text_size: styles.resolve(TextElem::size),
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
engine.sink.warn(warning!(
|
self
|
||||||
child.span(),
|
}
|
||||||
"{} was ignored during HTML export",
|
|
||||||
child.elem().name()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
|
/// An element that lays out its content as an inline SVG.
|
||||||
/// supplying a suitable `<head>`.
|
///
|
||||||
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
|
/// Sometimes, converting Typst content to HTML is not desirable. This can be
|
||||||
let head = head_element(info);
|
/// the case for plots and other content that relies on positioning and styling
|
||||||
let body = match classify_output(output)? {
|
/// to convey its message.
|
||||||
OutputKind::Html(element) => return Ok(element),
|
///
|
||||||
OutputKind::Body(body) => body,
|
/// This function allows you to use the Typst layout engine that would also be
|
||||||
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
|
/// used for PDF, SVG, and PNG export to render a part of your document exactly
|
||||||
};
|
/// how it would appear when exported in one of these formats. It embeds the
|
||||||
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
|
/// content as an inline SVG.
|
||||||
}
|
#[elem]
|
||||||
|
pub struct FrameElem {
|
||||||
/// Generate a `<head>` element.
|
/// The content that shall be laid out.
|
||||||
fn head_element(info: &DocumentInfo) -> HtmlElement {
|
#[positional]
|
||||||
let mut children = vec![];
|
#[required]
|
||||||
|
pub body: Content,
|
||||||
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
|
|
||||||
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::meta)
|
|
||||||
.with_attr(attr::name, "viewport")
|
|
||||||
.with_attr(attr::content, "width=device-width, initial-scale=1")
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(title) = &info.title {
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::title)
|
|
||||||
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(description) = &info.description {
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::meta)
|
|
||||||
.with_attr(attr::name, "description")
|
|
||||||
.with_attr(attr::content, description.clone())
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.author.is_empty() {
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::meta)
|
|
||||||
.with_attr(attr::name, "authors")
|
|
||||||
.with_attr(attr::content, info.author.join(", "))
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.keywords.is_empty() {
|
|
||||||
children.push(
|
|
||||||
HtmlElement::new(tag::meta)
|
|
||||||
.with_attr(attr::name, "keywords")
|
|
||||||
.with_attr(attr::content, info.keywords.join(", "))
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HtmlElement::new(tag::head).with_children(children)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determine which kind of output the user generated.
|
|
||||||
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
|
|
||||||
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
|
|
||||||
for node in &mut output {
|
|
||||||
let HtmlNode::Element(elem) = node else { continue };
|
|
||||||
let tag = elem.tag;
|
|
||||||
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
|
|
||||||
match (tag, count) {
|
|
||||||
(tag::html, 1) => return Ok(OutputKind::Html(take())),
|
|
||||||
(tag::body, 1) => return Ok(OutputKind::Body(take())),
|
|
||||||
(tag::html | tag::body, _) => bail!(
|
|
||||||
elem.span,
|
|
||||||
"`{}` element must be the only element in the document",
|
|
||||||
elem.tag,
|
|
||||||
),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(OutputKind::Leafs(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What kinds of output the user generated.
|
|
||||||
enum OutputKind {
|
|
||||||
/// The user generated their own `<html>` element. We do not need to supply
|
|
||||||
/// one.
|
|
||||||
Html(HtmlElement),
|
|
||||||
/// The user generate their own `<body>` element. We do not need to supply
|
|
||||||
/// one, but need supply the `<html>` element.
|
|
||||||
Body(HtmlElement),
|
|
||||||
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
|
|
||||||
Leafs(Vec<HtmlNode>),
|
|
||||||
}
|
}
|
||||||
|
290
crates/typst-html/src/link.rs
Normal file
290
crates/typst-html/src/link.rs
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
|
|
||||||
|
use comemo::Track;
|
||||||
|
use ecow::{EcoString, eco_format};
|
||||||
|
use typst_library::foundations::{Label, NativeElement};
|
||||||
|
use typst_library::introspection::{Introspector, Location, Tag};
|
||||||
|
use typst_library::layout::{Frame, FrameItem, Point};
|
||||||
|
use typst_library::model::{Destination, LinkElem};
|
||||||
|
use typst_utils::PicoStr;
|
||||||
|
|
||||||
|
use crate::{HtmlElement, HtmlNode, attr, tag};
|
||||||
|
|
||||||
|
/// Searches for links within a frame.
|
||||||
|
///
|
||||||
|
/// If all links are created via `LinkElem` in the future, this can be removed
|
||||||
|
/// in favor of the query in `identify_link_targets`. For the time being, some
|
||||||
|
/// links are created without existence of a `LinkElem`, so this is
|
||||||
|
/// unfortunately necessary.
|
||||||
|
pub fn introspect_frame_links(frame: &Frame, targets: &mut HashSet<Location>) {
|
||||||
|
for (_, item) in frame.items() {
|
||||||
|
match item {
|
||||||
|
FrameItem::Link(Destination::Location(loc), _) => {
|
||||||
|
targets.insert(*loc);
|
||||||
|
}
|
||||||
|
FrameItem::Group(group) => introspect_frame_links(&group.frame, targets),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches IDs to nodes produced by link targets to make them linkable.
|
||||||
|
///
|
||||||
|
/// May produce `<span>`s for link targets that turned into text nodes or no
|
||||||
|
/// nodes at all. See the [`LinkElem`] documentation for more details.
|
||||||
|
pub fn identify_link_targets(
|
||||||
|
root: &mut HtmlElement,
|
||||||
|
introspector: &mut Introspector,
|
||||||
|
mut targets: HashSet<Location>,
|
||||||
|
) {
|
||||||
|
// Query for all links with an intra-doc (i.e. `Location`) destination to
|
||||||
|
// know what needs IDs.
|
||||||
|
targets.extend(
|
||||||
|
introspector
|
||||||
|
.query(&LinkElem::ELEM.select())
|
||||||
|
.iter()
|
||||||
|
.map(|elem| elem.to_packed::<LinkElem>().unwrap())
|
||||||
|
.filter_map(|elem| match elem.dest.resolve(introspector.track()) {
|
||||||
|
Ok(Destination::Location(loc)) => Some(loc),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if targets.is_empty() {
|
||||||
|
// Nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign IDs to all link targets.
|
||||||
|
let mut work = Work::new();
|
||||||
|
traverse(
|
||||||
|
&mut work,
|
||||||
|
&targets,
|
||||||
|
&mut Identificator::new(introspector),
|
||||||
|
&mut root.children,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the mapping from locations to IDs to the introspector to make it
|
||||||
|
// available to links in the next iteration.
|
||||||
|
introspector.set_html_ids(work.ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traverses a list of nodes.
|
||||||
|
fn traverse(
|
||||||
|
work: &mut Work,
|
||||||
|
targets: &HashSet<Location>,
|
||||||
|
identificator: &mut Identificator<'_>,
|
||||||
|
nodes: &mut Vec<HtmlNode>,
|
||||||
|
) {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < nodes.len() {
|
||||||
|
let node = &mut nodes[i];
|
||||||
|
match node {
|
||||||
|
// When visiting a start tag, we check whether the element needs an
|
||||||
|
// ID and if so, add it to the queue, so that its first child node
|
||||||
|
// receives an ID.
|
||||||
|
HtmlNode::Tag(Tag::Start(elem)) => {
|
||||||
|
let loc = elem.location().unwrap();
|
||||||
|
if targets.contains(&loc) {
|
||||||
|
work.enqueue(loc, elem.label());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we reach an end tag, we check whether it closes an element
|
||||||
|
// that is still in our queue. If so, that means the element
|
||||||
|
// produced no nodes and we need to insert an empty span.
|
||||||
|
HtmlNode::Tag(Tag::End(loc, _)) => {
|
||||||
|
work.remove(*loc, |label| {
|
||||||
|
let mut element = HtmlElement::new(tag::span);
|
||||||
|
let id = identificator.assign(&mut element, label);
|
||||||
|
nodes.insert(i + 1, HtmlNode::Element(element));
|
||||||
|
id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// When visiting an element and the queue is non-empty, we assign an
|
||||||
|
// ID. Then, we traverse its children.
|
||||||
|
HtmlNode::Element(element) => {
|
||||||
|
work.drain(|label| identificator.assign(element, label));
|
||||||
|
traverse(work, targets, identificator, &mut element.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When visiting text and the queue is non-empty, we generate a span
|
||||||
|
// and assign an ID.
|
||||||
|
HtmlNode::Text(..) => {
|
||||||
|
work.drain(|label| {
|
||||||
|
let mut element =
|
||||||
|
HtmlElement::new(tag::span).with_children(vec![node.clone()]);
|
||||||
|
let id = identificator.assign(&mut element, label);
|
||||||
|
*node = HtmlNode::Element(element);
|
||||||
|
id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// When visiting a frame and the queue is non-empty, we assign an
|
||||||
|
// ID to it (will be added to the resulting SVG element).
|
||||||
|
HtmlNode::Frame(frame) => {
|
||||||
|
work.drain(|label| {
|
||||||
|
frame.id.get_or_insert_with(|| identificator.identify(label)).clone()
|
||||||
|
});
|
||||||
|
traverse_frame(
|
||||||
|
work,
|
||||||
|
targets,
|
||||||
|
identificator,
|
||||||
|
&frame.inner,
|
||||||
|
&mut frame.link_points,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traverses a frame embedded in HTML.
|
||||||
|
fn traverse_frame(
|
||||||
|
work: &mut Work,
|
||||||
|
targets: &HashSet<Location>,
|
||||||
|
identificator: &mut Identificator<'_>,
|
||||||
|
frame: &Frame,
|
||||||
|
link_points: &mut Vec<(Point, EcoString)>,
|
||||||
|
) {
|
||||||
|
for (_, item) in frame.items() {
|
||||||
|
match item {
|
||||||
|
FrameItem::Tag(Tag::Start(elem)) => {
|
||||||
|
let loc = elem.location().unwrap();
|
||||||
|
if targets.contains(&loc) {
|
||||||
|
let pos = identificator.introspector.position(loc).point;
|
||||||
|
let id = identificator.identify(elem.label());
|
||||||
|
work.ids.insert(loc, id.clone());
|
||||||
|
link_points.push((pos, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FrameItem::Group(group) => {
|
||||||
|
traverse_frame(work, targets, identificator, &group.frame, link_points);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keeps track of the work to be done during ID generation.
|
||||||
|
struct Work {
|
||||||
|
/// The locations and labels of elements we need to assign an ID to right
|
||||||
|
/// now.
|
||||||
|
queue: VecDeque<(Location, Option<Label>)>,
|
||||||
|
/// The resulting mapping from element location's to HTML IDs.
|
||||||
|
ids: HashMap<Location, EcoString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Work {
|
||||||
|
/// Sets up.
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { queue: VecDeque::new(), ids: HashMap::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the element with the given location and label as in need of an
|
||||||
|
/// ID. A subsequent call to `drain` will call `f`.
|
||||||
|
fn enqueue(&mut self, loc: Location, label: Option<Label>) {
|
||||||
|
self.queue.push_back((loc, label))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If one or multiple elements are in need of an ID, calls `f` to generate
|
||||||
|
/// an ID and apply it to the current node with `f`, and then establishes a
|
||||||
|
/// mapping from the elements' locations to that ID.
|
||||||
|
fn drain(&mut self, f: impl FnOnce(Option<Label>) -> EcoString) {
|
||||||
|
if let Some(&(_, label)) = self.queue.front() {
|
||||||
|
let id = f(label);
|
||||||
|
for (loc, _) in self.queue.drain(..) {
|
||||||
|
self.ids.insert(loc, id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Similar to `drain`, but only for a specific given location.
|
||||||
|
fn remove(&mut self, loc: Location, f: impl FnOnce(Option<Label>) -> EcoString) {
|
||||||
|
if let Some(i) = self.queue.iter().position(|&(l, _)| l == loc) {
|
||||||
|
let (_, label) = self.queue.remove(i).unwrap();
|
||||||
|
let id = f(label);
|
||||||
|
self.ids.insert(loc, id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates unique IDs for elements.
|
||||||
|
struct Identificator<'a> {
|
||||||
|
introspector: &'a Introspector,
|
||||||
|
loc_counter: usize,
|
||||||
|
label_counter: HashMap<Label, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Identificator<'a> {
|
||||||
|
/// Creates a new identificator.
|
||||||
|
fn new(introspector: &'a Introspector) -> Self {
|
||||||
|
Self {
|
||||||
|
introspector,
|
||||||
|
loc_counter: 0,
|
||||||
|
label_counter: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assigns an ID to an element or reuses an existing ID.
|
||||||
|
fn assign(&mut self, element: &mut HtmlElement, label: Option<Label>) -> EcoString {
|
||||||
|
element.attrs.get(attr::id).cloned().unwrap_or_else(|| {
|
||||||
|
let id = self.identify(label);
|
||||||
|
element.attrs.push_front(attr::id, id.clone());
|
||||||
|
id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates an ID, potentially based on a label.
|
||||||
|
fn identify(&mut self, label: Option<Label>) -> EcoString {
|
||||||
|
if let Some(label) = label {
|
||||||
|
let resolved = label.resolve();
|
||||||
|
let text = resolved.as_str();
|
||||||
|
if can_use_label_as_id(text) {
|
||||||
|
if self.introspector.label_count(label) == 1 {
|
||||||
|
return text.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = self.label_counter.entry(label).or_insert(0);
|
||||||
|
*counter += 1;
|
||||||
|
return disambiguate(self.introspector, text, counter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loc_counter += 1;
|
||||||
|
disambiguate(self.introspector, "loc", &mut self.loc_counter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the label is both a valid CSS identifier and a valid URL fragment
|
||||||
|
/// for linking.
|
||||||
|
///
|
||||||
|
/// This is slightly more restrictive than HTML and CSS, but easier to
|
||||||
|
/// understand and explain.
|
||||||
|
fn can_use_label_as_id(label: &str) -> bool {
|
||||||
|
!label.is_empty()
|
||||||
|
&& label.chars().all(|c| c.is_alphanumeric() || matches!(c, '-' | '_'))
|
||||||
|
&& !label.starts_with(|c: char| c.is_numeric() || c == '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disambiguates `text` with the suffix `-{counter}`, while ensuring that this
|
||||||
|
/// does not result in a collision with an existing label.
|
||||||
|
fn disambiguate(
|
||||||
|
introspector: &Introspector,
|
||||||
|
text: &str,
|
||||||
|
counter: &mut usize,
|
||||||
|
) -> EcoString {
|
||||||
|
loop {
|
||||||
|
let disambiguated = eco_format!("{text}-{counter}");
|
||||||
|
if PicoStr::get(&disambiguated)
|
||||||
|
.and_then(Label::new)
|
||||||
|
.is_some_and(|label| introspector.label_count(label) > 0)
|
||||||
|
{
|
||||||
|
*counter += 1;
|
||||||
|
} else {
|
||||||
|
break disambiguated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoVec};
|
use ecow::{EcoVec, eco_format};
|
||||||
use typst_library::diag::warning;
|
use typst_library::diag::{At, warning};
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
|
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
|
||||||
};
|
};
|
||||||
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
|
|
||||||
use typst_library::introspection::{Counter, Locator};
|
use typst_library::introspection::{Counter, Locator};
|
||||||
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
|
use typst_library::layout::resolve::{Cell, CellGrid, Entry, table_to_cellgrid};
|
||||||
use typst_library::layout::{OuterVAlignment, Sizing};
|
use typst_library::layout::{OuterVAlignment, Sizing};
|
||||||
use typst_library::model::{
|
use typst_library::model::{
|
||||||
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
||||||
@ -15,16 +14,16 @@ use typst_library::model::{
|
|||||||
RefElem, StrongElem, TableCell, TableElem, TermsElem,
|
RefElem, StrongElem, TableCell, TableElem, TermsElem,
|
||||||
};
|
};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
|
||||||
SubElem, SuperElem, UnderlineElem,
|
SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
|
||||||
};
|
};
|
||||||
use typst_library::visualize::ImageElem;
|
use typst_library::visualize::ImageElem;
|
||||||
|
|
||||||
use crate::css::{self, HtmlElemExt};
|
use crate::{FrameElem, HtmlAttrs, HtmlElem, HtmlTag, attr, css, tag};
|
||||||
|
|
||||||
/// Register show rules for the [HTML target](Target::Html).
|
/// Registers show rules for the [HTML target](Target::Html).
|
||||||
pub fn register(rules: &mut NativeRuleMap) {
|
pub fn register(rules: &mut NativeRuleMap) {
|
||||||
use Target::Html;
|
use Target::{Html, Paged};
|
||||||
|
|
||||||
// Model.
|
// Model.
|
||||||
rules.register(Html, STRONG_RULE);
|
rules.register(Html, STRONG_RULE);
|
||||||
@ -48,26 +47,24 @@ pub fn register(rules: &mut NativeRuleMap) {
|
|||||||
rules.register(Html, OVERLINE_RULE);
|
rules.register(Html, OVERLINE_RULE);
|
||||||
rules.register(Html, STRIKE_RULE);
|
rules.register(Html, STRIKE_RULE);
|
||||||
rules.register(Html, HIGHLIGHT_RULE);
|
rules.register(Html, HIGHLIGHT_RULE);
|
||||||
|
rules.register(Html, SMALLCAPS_RULE);
|
||||||
rules.register(Html, RAW_RULE);
|
rules.register(Html, RAW_RULE);
|
||||||
rules.register(Html, RAW_LINE_RULE);
|
rules.register(Html, RAW_LINE_RULE);
|
||||||
|
|
||||||
// Visualize.
|
// Visualize.
|
||||||
rules.register(Html, IMAGE_RULE);
|
rules.register(Html, IMAGE_RULE);
|
||||||
|
|
||||||
|
// For the HTML target, `html.frame` is a primitive. In the laid-out target,
|
||||||
|
// it should be a no-op so that nested frames don't break (things like `show
|
||||||
|
// math.equation: html.frame` can result in nested ones).
|
||||||
|
rules.register::<FrameElem>(Paged, |elem, _, _| Ok(elem.body.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
|
const STRONG_RULE: ShowFn<StrongElem> =
|
||||||
Ok(HtmlElem::new(tag::strong)
|
|elem, _, _| Ok(HtmlElem::new(tag::strong).with_body(Some(elem.body.clone())).pack());
|
||||||
.with_body(Some(elem.body.clone()))
|
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPH_RULE: ShowFn<EmphElem> = |elem, _, _| {
|
const EMPH_RULE: ShowFn<EmphElem> =
|
||||||
Ok(HtmlElem::new(tag::em)
|
|elem, _, _| Ok(HtmlElem::new(tag::em).with_body(Some(elem.body.clone())).pack());
|
||||||
.with_body(Some(elem.body.clone()))
|
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
|
||||||
|
|
||||||
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
|
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
|
||||||
Ok(HtmlElem::new(tag::ul)
|
Ok(HtmlElem::new(tag::ul)
|
||||||
@ -82,8 +79,7 @@ const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
|
|||||||
.pack()
|
.pack()
|
||||||
.spanned(item.span())
|
.spanned(item.span())
|
||||||
}))))
|
}))))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
||||||
@ -99,7 +95,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
|||||||
|
|
||||||
let body = Content::sequence(elem.children.iter().map(|item| {
|
let body = Content::sequence(elem.children.iter().map(|item| {
|
||||||
let mut li = HtmlElem::new(tag::li);
|
let mut li = HtmlElem::new(tag::li);
|
||||||
if let Some(nr) = item.number.get(styles) {
|
if let Smart::Custom(nr) = item.number.get(styles) {
|
||||||
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
li = li.with_attr(attr::value, eco_format!("{nr}"));
|
||||||
}
|
}
|
||||||
// Text in wide enums shall always turn into paragraphs.
|
// Text in wide enums shall always turn into paragraphs.
|
||||||
@ -110,7 +106,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
|
|||||||
li.with_body(Some(body)).pack().spanned(item.span())
|
li.with_body(Some(body)).pack().spanned(item.span())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Ok(ol.with_body(Some(body)).pack().spanned(elem.span()))
|
Ok(ol.with_body(Some(body)).pack())
|
||||||
};
|
};
|
||||||
|
|
||||||
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
||||||
@ -137,20 +133,32 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
|
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
|
||||||
let body = elem.body.clone();
|
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
|
||||||
Ok(if let LinkTarget::Dest(Destination::Url(url)) = &elem.dest {
|
|
||||||
HtmlElem::new(tag::a)
|
let href = match dest {
|
||||||
.with_attr(attr::href, url.clone().into_inner())
|
Destination::Url(url) => Some(url.clone().into_inner()),
|
||||||
.with_body(Some(body))
|
Destination::Location(location) => {
|
||||||
.pack()
|
let id = engine
|
||||||
.spanned(elem.span())
|
.introspector
|
||||||
} else {
|
.html_id(location)
|
||||||
|
.cloned()
|
||||||
|
.ok_or("failed to determine link anchor")
|
||||||
|
.at(elem.span())?;
|
||||||
|
Some(eco_format!("#{id}"))
|
||||||
|
}
|
||||||
|
Destination::Position(_) => {
|
||||||
engine.sink.warn(warning!(
|
engine.sink.warn(warning!(
|
||||||
elem.span(),
|
elem.span(),
|
||||||
"non-URL links are not yet supported by HTML export"
|
"positional link was ignored during HTML export"
|
||||||
));
|
));
|
||||||
body
|
None
|
||||||
})
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HtmlElem::new(tag::a)
|
||||||
|
.with_optional_attr(attr::href, href)
|
||||||
|
.with_body(Some(elem.body.clone()))
|
||||||
|
.pack())
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
||||||
@ -186,10 +194,9 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
|||||||
.with_attr(attr::role, "heading")
|
.with_attr(attr::role, "heading")
|
||||||
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
|
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(span)
|
|
||||||
} else {
|
} else {
|
||||||
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
|
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
|
||||||
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
|
HtmlElem::new(t).with_body(Some(realized)).pack()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,17 +215,13 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
|||||||
// Ensure that the body is considered a paragraph.
|
// Ensure that the body is considered a paragraph.
|
||||||
realized += ParbreakElem::shared().clone().spanned(span);
|
realized += ParbreakElem::shared().clone().spanned(span);
|
||||||
|
|
||||||
Ok(HtmlElem::new(tag::figure)
|
Ok(HtmlElem::new(tag::figure).with_body(Some(realized)).pack())
|
||||||
.with_body(Some(realized))
|
|
||||||
.pack()
|
|
||||||
.spanned(span))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
|
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
|
||||||
Ok(HtmlElem::new(tag::figcaption)
|
Ok(HtmlElem::new(tag::figcaption)
|
||||||
.with_body(Some(elem.realize(engine, styles)?))
|
.with_body(Some(elem.realize(engine, styles)?))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
||||||
@ -235,13 +238,11 @@ const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
|||||||
|
|
||||||
if block {
|
if block {
|
||||||
let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized));
|
let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized));
|
||||||
if let Some(Attribution::Content(attribution)) = attribution {
|
if let Some(Attribution::Content(attribution)) = attribution
|
||||||
if let Some(link) = attribution.to_packed::<LinkElem>() {
|
&& let Some(link) = attribution.to_packed::<LinkElem>()
|
||||||
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
|
&& let LinkTarget::Dest(Destination::Url(url)) = &link.dest
|
||||||
blockquote =
|
{
|
||||||
blockquote.with_attr(attr::cite, url.clone().into_inner());
|
blockquote = blockquote.with_attr(attr::cite, url.clone().into_inner());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
realized = blockquote.pack().spanned(span);
|
realized = blockquote.pack().spanned(span);
|
||||||
@ -359,19 +360,11 @@ fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
|||||||
.spanned(cell.span())
|
.spanned(cell.span())
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUB_RULE: ShowFn<SubElem> = |elem, _, _| {
|
const SUB_RULE: ShowFn<SubElem> =
|
||||||
Ok(HtmlElem::new(tag::sub)
|
|elem, _, _| Ok(HtmlElem::new(tag::sub).with_body(Some(elem.body.clone())).pack());
|
||||||
.with_body(Some(elem.body.clone()))
|
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
|
||||||
|
|
||||||
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, _| {
|
const SUPER_RULE: ShowFn<SuperElem> =
|
||||||
Ok(HtmlElem::new(tag::sup)
|
|elem, _, _| Ok(HtmlElem::new(tag::sup).with_body(Some(elem.body.clone())).pack());
|
||||||
.with_body(Some(elem.body.clone()))
|
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
|
||||||
|
|
||||||
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
|
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
|
||||||
// Note: In modern HTML, `<u>` is not the underline element, but
|
// Note: In modern HTML, `<u>` is not the underline element, but
|
||||||
@ -396,6 +389,20 @@ const STRIKE_RULE: ShowFn<StrikeElem> =
|
|||||||
const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|
const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|
||||||
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
|
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
|
||||||
|
|
||||||
|
const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
|
||||||
|
Ok(HtmlElem::new(tag::span)
|
||||||
|
.with_attr(
|
||||||
|
attr::style,
|
||||||
|
if elem.all.get(styles) {
|
||||||
|
"font-variant-caps: all-small-caps"
|
||||||
|
} else {
|
||||||
|
"font-variant-caps: small-caps"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_body(Some(elem.body.clone()))
|
||||||
|
.pack())
|
||||||
|
};
|
||||||
|
|
||||||
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
||||||
let lines = elem.lines.as_deref().unwrap_or_default();
|
let lines = elem.lines.as_deref().unwrap_or_default();
|
||||||
|
|
||||||
@ -410,8 +417,7 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
|||||||
|
|
||||||
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
|
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
|
||||||
.with_body(Some(Content::sequence(seq)))
|
.with_body(Some(Content::sequence(seq)))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
|
||||||
|
271
crates/typst-html/src/tag.rs
Normal file
271
crates/typst-html/src/tag.rs
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
//! Predefined constants for HTML tags.
|
||||||
|
|
||||||
|
#![allow(non_upper_case_globals)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use crate::HtmlTag;
|
||||||
|
|
||||||
|
pub const a: HtmlTag = HtmlTag::constant("a");
|
||||||
|
pub const abbr: HtmlTag = HtmlTag::constant("abbr");
|
||||||
|
pub const address: HtmlTag = HtmlTag::constant("address");
|
||||||
|
pub const area: HtmlTag = HtmlTag::constant("area");
|
||||||
|
pub const article: HtmlTag = HtmlTag::constant("article");
|
||||||
|
pub const aside: HtmlTag = HtmlTag::constant("aside");
|
||||||
|
pub const audio: HtmlTag = HtmlTag::constant("audio");
|
||||||
|
pub const b: HtmlTag = HtmlTag::constant("b");
|
||||||
|
pub const base: HtmlTag = HtmlTag::constant("base");
|
||||||
|
pub const bdi: HtmlTag = HtmlTag::constant("bdi");
|
||||||
|
pub const bdo: HtmlTag = HtmlTag::constant("bdo");
|
||||||
|
pub const blockquote: HtmlTag = HtmlTag::constant("blockquote");
|
||||||
|
pub const body: HtmlTag = HtmlTag::constant("body");
|
||||||
|
pub const br: HtmlTag = HtmlTag::constant("br");
|
||||||
|
pub const button: HtmlTag = HtmlTag::constant("button");
|
||||||
|
pub const canvas: HtmlTag = HtmlTag::constant("canvas");
|
||||||
|
pub const caption: HtmlTag = HtmlTag::constant("caption");
|
||||||
|
pub const cite: HtmlTag = HtmlTag::constant("cite");
|
||||||
|
pub const code: HtmlTag = HtmlTag::constant("code");
|
||||||
|
pub const col: HtmlTag = HtmlTag::constant("col");
|
||||||
|
pub const colgroup: HtmlTag = HtmlTag::constant("colgroup");
|
||||||
|
pub const data: HtmlTag = HtmlTag::constant("data");
|
||||||
|
pub const datalist: HtmlTag = HtmlTag::constant("datalist");
|
||||||
|
pub const dd: HtmlTag = HtmlTag::constant("dd");
|
||||||
|
pub const del: HtmlTag = HtmlTag::constant("del");
|
||||||
|
pub const details: HtmlTag = HtmlTag::constant("details");
|
||||||
|
pub const dfn: HtmlTag = HtmlTag::constant("dfn");
|
||||||
|
pub const dialog: HtmlTag = HtmlTag::constant("dialog");
|
||||||
|
pub const div: HtmlTag = HtmlTag::constant("div");
|
||||||
|
pub const dl: HtmlTag = HtmlTag::constant("dl");
|
||||||
|
pub const dt: HtmlTag = HtmlTag::constant("dt");
|
||||||
|
pub const em: HtmlTag = HtmlTag::constant("em");
|
||||||
|
pub const embed: HtmlTag = HtmlTag::constant("embed");
|
||||||
|
pub const fieldset: HtmlTag = HtmlTag::constant("fieldset");
|
||||||
|
pub const figcaption: HtmlTag = HtmlTag::constant("figcaption");
|
||||||
|
pub const figure: HtmlTag = HtmlTag::constant("figure");
|
||||||
|
pub const footer: HtmlTag = HtmlTag::constant("footer");
|
||||||
|
pub const form: HtmlTag = HtmlTag::constant("form");
|
||||||
|
pub const h1: HtmlTag = HtmlTag::constant("h1");
|
||||||
|
pub const h2: HtmlTag = HtmlTag::constant("h2");
|
||||||
|
pub const h3: HtmlTag = HtmlTag::constant("h3");
|
||||||
|
pub const h4: HtmlTag = HtmlTag::constant("h4");
|
||||||
|
pub const h5: HtmlTag = HtmlTag::constant("h5");
|
||||||
|
pub const h6: HtmlTag = HtmlTag::constant("h6");
|
||||||
|
pub const head: HtmlTag = HtmlTag::constant("head");
|
||||||
|
pub const header: HtmlTag = HtmlTag::constant("header");
|
||||||
|
pub const hgroup: HtmlTag = HtmlTag::constant("hgroup");
|
||||||
|
pub const hr: HtmlTag = HtmlTag::constant("hr");
|
||||||
|
pub const html: HtmlTag = HtmlTag::constant("html");
|
||||||
|
pub const i: HtmlTag = HtmlTag::constant("i");
|
||||||
|
pub const iframe: HtmlTag = HtmlTag::constant("iframe");
|
||||||
|
pub const img: HtmlTag = HtmlTag::constant("img");
|
||||||
|
pub const input: HtmlTag = HtmlTag::constant("input");
|
||||||
|
pub const ins: HtmlTag = HtmlTag::constant("ins");
|
||||||
|
pub const kbd: HtmlTag = HtmlTag::constant("kbd");
|
||||||
|
pub const label: HtmlTag = HtmlTag::constant("label");
|
||||||
|
pub const legend: HtmlTag = HtmlTag::constant("legend");
|
||||||
|
pub const li: HtmlTag = HtmlTag::constant("li");
|
||||||
|
pub const link: HtmlTag = HtmlTag::constant("link");
|
||||||
|
pub const main: HtmlTag = HtmlTag::constant("main");
|
||||||
|
pub const map: HtmlTag = HtmlTag::constant("map");
|
||||||
|
pub const mark: HtmlTag = HtmlTag::constant("mark");
|
||||||
|
pub const menu: HtmlTag = HtmlTag::constant("menu");
|
||||||
|
pub const meta: HtmlTag = HtmlTag::constant("meta");
|
||||||
|
pub const meter: HtmlTag = HtmlTag::constant("meter");
|
||||||
|
pub const nav: HtmlTag = HtmlTag::constant("nav");
|
||||||
|
pub const noscript: HtmlTag = HtmlTag::constant("noscript");
|
||||||
|
pub const object: HtmlTag = HtmlTag::constant("object");
|
||||||
|
pub const ol: HtmlTag = HtmlTag::constant("ol");
|
||||||
|
pub const optgroup: HtmlTag = HtmlTag::constant("optgroup");
|
||||||
|
pub const option: HtmlTag = HtmlTag::constant("option");
|
||||||
|
pub const output: HtmlTag = HtmlTag::constant("output");
|
||||||
|
pub const p: HtmlTag = HtmlTag::constant("p");
|
||||||
|
pub const picture: HtmlTag = HtmlTag::constant("picture");
|
||||||
|
pub const pre: HtmlTag = HtmlTag::constant("pre");
|
||||||
|
pub const progress: HtmlTag = HtmlTag::constant("progress");
|
||||||
|
pub const q: HtmlTag = HtmlTag::constant("q");
|
||||||
|
pub const rp: HtmlTag = HtmlTag::constant("rp");
|
||||||
|
pub const rt: HtmlTag = HtmlTag::constant("rt");
|
||||||
|
pub const ruby: HtmlTag = HtmlTag::constant("ruby");
|
||||||
|
pub const s: HtmlTag = HtmlTag::constant("s");
|
||||||
|
pub const samp: HtmlTag = HtmlTag::constant("samp");
|
||||||
|
pub const script: HtmlTag = HtmlTag::constant("script");
|
||||||
|
pub const search: HtmlTag = HtmlTag::constant("search");
|
||||||
|
pub const section: HtmlTag = HtmlTag::constant("section");
|
||||||
|
pub const select: HtmlTag = HtmlTag::constant("select");
|
||||||
|
pub const slot: HtmlTag = HtmlTag::constant("slot");
|
||||||
|
pub const small: HtmlTag = HtmlTag::constant("small");
|
||||||
|
pub const source: HtmlTag = HtmlTag::constant("source");
|
||||||
|
pub const span: HtmlTag = HtmlTag::constant("span");
|
||||||
|
pub const strong: HtmlTag = HtmlTag::constant("strong");
|
||||||
|
pub const style: HtmlTag = HtmlTag::constant("style");
|
||||||
|
pub const sub: HtmlTag = HtmlTag::constant("sub");
|
||||||
|
pub const summary: HtmlTag = HtmlTag::constant("summary");
|
||||||
|
pub const sup: HtmlTag = HtmlTag::constant("sup");
|
||||||
|
pub const table: HtmlTag = HtmlTag::constant("table");
|
||||||
|
pub const tbody: HtmlTag = HtmlTag::constant("tbody");
|
||||||
|
pub const td: HtmlTag = HtmlTag::constant("td");
|
||||||
|
pub const template: HtmlTag = HtmlTag::constant("template");
|
||||||
|
pub const textarea: HtmlTag = HtmlTag::constant("textarea");
|
||||||
|
pub const tfoot: HtmlTag = HtmlTag::constant("tfoot");
|
||||||
|
pub const th: HtmlTag = HtmlTag::constant("th");
|
||||||
|
pub const thead: HtmlTag = HtmlTag::constant("thead");
|
||||||
|
pub const time: HtmlTag = HtmlTag::constant("time");
|
||||||
|
pub const title: HtmlTag = HtmlTag::constant("title");
|
||||||
|
pub const tr: HtmlTag = HtmlTag::constant("tr");
|
||||||
|
pub const track: HtmlTag = HtmlTag::constant("track");
|
||||||
|
pub const u: HtmlTag = HtmlTag::constant("u");
|
||||||
|
pub const ul: HtmlTag = HtmlTag::constant("ul");
|
||||||
|
pub const var: HtmlTag = HtmlTag::constant("var");
|
||||||
|
pub const video: HtmlTag = HtmlTag::constant("video");
|
||||||
|
pub const wbr: HtmlTag = HtmlTag::constant("wbr");
|
||||||
|
|
||||||
|
/// Whether this is a void tag whose associated element may not have
|
||||||
|
/// children.
|
||||||
|
pub fn is_void(tag: HtmlTag) -> bool {
|
||||||
|
matches!(
|
||||||
|
tag,
|
||||||
|
self::area
|
||||||
|
| self::base
|
||||||
|
| self::br
|
||||||
|
| self::col
|
||||||
|
| self::embed
|
||||||
|
| self::hr
|
||||||
|
| self::img
|
||||||
|
| self::input
|
||||||
|
| self::link
|
||||||
|
| self::meta
|
||||||
|
| self::source
|
||||||
|
| self::track
|
||||||
|
| self::wbr
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this is a tag containing raw text.
|
||||||
|
pub fn is_raw(tag: HtmlTag) -> bool {
|
||||||
|
matches!(tag, self::script | self::style)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this is a tag containing escapable raw text.
|
||||||
|
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
|
||||||
|
matches!(tag, self::textarea | self::title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an element is considered metadata.
|
||||||
|
pub fn is_metadata(tag: HtmlTag) -> bool {
|
||||||
|
matches!(
|
||||||
|
tag,
|
||||||
|
self::base
|
||||||
|
| self::link
|
||||||
|
| self::meta
|
||||||
|
| self::noscript
|
||||||
|
| self::script
|
||||||
|
| self::style
|
||||||
|
| self::template
|
||||||
|
| self::title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether nodes with the tag have the CSS property `display: block` by
|
||||||
|
/// default.
|
||||||
|
pub fn is_block_by_default(tag: HtmlTag) -> bool {
|
||||||
|
matches!(
|
||||||
|
tag,
|
||||||
|
self::html
|
||||||
|
| self::head
|
||||||
|
| self::body
|
||||||
|
| self::article
|
||||||
|
| self::aside
|
||||||
|
| self::h1
|
||||||
|
| self::h2
|
||||||
|
| self::h3
|
||||||
|
| self::h4
|
||||||
|
| self::h5
|
||||||
|
| self::h6
|
||||||
|
| self::hgroup
|
||||||
|
| self::nav
|
||||||
|
| self::section
|
||||||
|
| self::dd
|
||||||
|
| self::dl
|
||||||
|
| self::dt
|
||||||
|
| self::menu
|
||||||
|
| self::ol
|
||||||
|
| self::ul
|
||||||
|
| self::address
|
||||||
|
| self::blockquote
|
||||||
|
| self::dialog
|
||||||
|
| self::div
|
||||||
|
| self::fieldset
|
||||||
|
| self::figure
|
||||||
|
| self::figcaption
|
||||||
|
| self::footer
|
||||||
|
| self::form
|
||||||
|
| self::header
|
||||||
|
| self::hr
|
||||||
|
| self::legend
|
||||||
|
| self::main
|
||||||
|
| self::p
|
||||||
|
| self::pre
|
||||||
|
| self::search
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the element is inline-level as opposed to being block-level.
|
||||||
|
///
|
||||||
|
/// Not sure whether this distinction really makes sense. But we somehow
|
||||||
|
/// need to decide what to put into automatic paragraphs. A `<strong>`
|
||||||
|
/// should merged into a paragraph created by realization, but a `<div>`
|
||||||
|
/// shouldn't.
|
||||||
|
///
|
||||||
|
/// <https://www.w3.org/TR/html401/struct/global.html#block-inline>
|
||||||
|
/// <https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content>
|
||||||
|
/// <https://github.com/orgs/mdn/discussions/353>
|
||||||
|
pub fn is_inline_by_default(tag: HtmlTag) -> bool {
|
||||||
|
matches!(
|
||||||
|
tag,
|
||||||
|
self::abbr
|
||||||
|
| self::a
|
||||||
|
| self::bdi
|
||||||
|
| self::b
|
||||||
|
| self::br
|
||||||
|
| self::bdo
|
||||||
|
| self::code
|
||||||
|
| self::cite
|
||||||
|
| self::dfn
|
||||||
|
| self::data
|
||||||
|
| self::i
|
||||||
|
| self::em
|
||||||
|
| self::mark
|
||||||
|
| self::kbd
|
||||||
|
| self::rp
|
||||||
|
| self::q
|
||||||
|
| self::ruby
|
||||||
|
| self::rt
|
||||||
|
| self::samp
|
||||||
|
| self::s
|
||||||
|
| self::span
|
||||||
|
| self::small
|
||||||
|
| self::sub
|
||||||
|
| self::strong
|
||||||
|
| self::time
|
||||||
|
| self::sup
|
||||||
|
| self::var
|
||||||
|
| self::u
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
|
||||||
|
/// by default.
|
||||||
|
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
|
||||||
|
matches!(
|
||||||
|
tag,
|
||||||
|
self::table
|
||||||
|
| self::thead
|
||||||
|
| self::tbody
|
||||||
|
| self::tfoot
|
||||||
|
| self::tr
|
||||||
|
| self::th
|
||||||
|
| self::td
|
||||||
|
| self::caption
|
||||||
|
| self::col
|
||||||
|
| self::colgroup
|
||||||
|
)
|
||||||
|
}
|
@ -9,22 +9,20 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::{eco_format, eco_vec, EcoString};
|
use ecow::{EcoString, eco_format, eco_vec};
|
||||||
use typst_assets::html as data;
|
use typst_assets::html as data;
|
||||||
use typst_library::diag::{bail, At, Hint, HintedStrResult, SourceResult};
|
use typst_library::diag::{At, Hint, HintedStrResult, SourceResult, bail};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
|
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
|
||||||
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
|
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
|
||||||
PositiveF64, Reflect, Scope, Str, Type, Value,
|
PositiveF64, Reflect, Scope, Str, Type, Value,
|
||||||
};
|
};
|
||||||
use typst_library::html::tag;
|
|
||||||
use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
|
|
||||||
use typst_library::layout::{Axes, Axis, Dir, Length};
|
use typst_library::layout::{Axes, Axis, Dir, Length};
|
||||||
use typst_library::visualize::Color;
|
use typst_library::visualize::Color;
|
||||||
use typst_macros::cast;
|
use typst_macros::cast;
|
||||||
|
|
||||||
use crate::css;
|
use crate::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag, css, tag};
|
||||||
|
|
||||||
/// Hook up all typed HTML definitions.
|
/// Hook up all typed HTML definitions.
|
||||||
pub(super) fn define(html: &mut Scope) {
|
pub(super) fn define(html: &mut Scope) {
|
||||||
|
@ -17,7 +17,6 @@ typst = { workspace = true }
|
|||||||
typst-eval = { workspace = true }
|
typst-eval = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
if_chain = { workspace = true }
|
|
||||||
pathdiff = { workspace = true }
|
pathdiff = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
unscanny = { workspace = true }
|
unscanny = { workspace = true }
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use comemo::Track;
|
use comemo::Track;
|
||||||
use ecow::{eco_vec, EcoString, EcoVec};
|
use ecow::{EcoString, EcoVec, eco_vec};
|
||||||
use typst::foundations::{Label, Styles, Value};
|
use typst::foundations::{Label, Styles, Value};
|
||||||
use typst::layout::PagedDocument;
|
use typst::layout::PagedDocument;
|
||||||
use typst::model::{BibliographyElem, FigureElem};
|
use typst::model::{BibliographyElem, FigureElem};
|
||||||
use typst::syntax::{ast, LinkedNode, SyntaxKind};
|
use typst::syntax::{LinkedNode, SyntaxKind, ast};
|
||||||
|
|
||||||
use crate::IdeWorld;
|
use crate::IdeWorld;
|
||||||
|
|
||||||
@ -25,17 +27,18 @@ pub fn analyze_expr(
|
|||||||
ast::Expr::Numeric(v) => Value::numeric(v.get()),
|
ast::Expr::Numeric(v) => Value::numeric(v.get()),
|
||||||
ast::Expr::Str(v) => Value::Str(v.get().into()),
|
ast::Expr::Str(v) => Value::Str(v.get().into()),
|
||||||
_ => {
|
_ => {
|
||||||
if node.kind() == SyntaxKind::Contextual {
|
if node.kind() == SyntaxKind::Contextual
|
||||||
if let Some(child) = node.children().next_back() {
|
&& let Some(child) = node.children().next_back()
|
||||||
|
{
|
||||||
return analyze_expr(world, &child);
|
return analyze_expr(world, &child);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(parent) = node.parent() {
|
if let Some(parent) = node.parent()
|
||||||
if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 {
|
&& parent.kind() == SyntaxKind::FieldAccess
|
||||||
|
&& node.index() > 0
|
||||||
|
{
|
||||||
return analyze_expr(world, parent);
|
return analyze_expr(world, parent);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return typst::trace::<PagedDocument>(world.upcast(), node.span());
|
return typst::trace::<PagedDocument>(world.upcast(), node.span());
|
||||||
}
|
}
|
||||||
@ -66,14 +69,22 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value
|
|||||||
/// - All labels and descriptions for them, if available
|
/// - All labels and descriptions for them, if available
|
||||||
/// - A split offset: All labels before this offset belong to nodes, all after
|
/// - A split offset: All labels before this offset belong to nodes, all after
|
||||||
/// belong to a bibliography.
|
/// belong to a bibliography.
|
||||||
|
///
|
||||||
|
/// Note: When multiple labels in the document have the same identifier,
|
||||||
|
/// this only returns the first one.
|
||||||
pub fn analyze_labels(
|
pub fn analyze_labels(
|
||||||
document: &PagedDocument,
|
document: &PagedDocument,
|
||||||
) -> (Vec<(Label, Option<EcoString>)>, usize) {
|
) -> (Vec<(Label, Option<EcoString>)>, usize) {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
|
let mut seen_labels = HashSet::new();
|
||||||
|
|
||||||
// Labels in the document.
|
// Labels in the document.
|
||||||
for elem in document.introspector.all() {
|
for elem in document.introspector.all() {
|
||||||
let Some(label) = elem.label() else { continue };
|
let Some(label) = elem.label() else { continue };
|
||||||
|
if !seen_labels.insert(label) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let details = elem
|
let details = elem
|
||||||
.to_packed::<FigureElem>()
|
.to_packed::<FigureElem>()
|
||||||
.and_then(|figure| match figure.caption.as_option() {
|
.and_then(|figure| match figure.caption.as_option() {
|
||||||
|
@ -2,18 +2,17 @@ use std::cmp::Reverse;
|
|||||||
use std::collections::{BTreeMap, HashSet};
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use if_chain::if_chain;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typst::foundations::{
|
use typst::foundations::{
|
||||||
fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr,
|
AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, StyleChain, Styles,
|
||||||
StyleChain, Styles, Type, Value,
|
Type, Value, fields_on, repr,
|
||||||
};
|
};
|
||||||
use typst::layout::{Alignment, Dir, PagedDocument};
|
use typst::layout::{Alignment, Dir, PagedDocument};
|
||||||
use typst::syntax::ast::AstNode;
|
use typst::syntax::ast::AstNode;
|
||||||
use typst::syntax::{
|
use typst::syntax::{
|
||||||
ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source,
|
FileId, LinkedNode, Side, Source, SyntaxKind, ast, is_id_continue, is_id_start,
|
||||||
SyntaxKind,
|
is_ident,
|
||||||
};
|
};
|
||||||
use typst::text::{FontFlags, RawElem};
|
use typst::text::{FontFlags, RawElem};
|
||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
@ -22,7 +21,7 @@ use unscanny::Scanner;
|
|||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
check_value_recursively, globals, plain_docs_sentence, summarize_font_family,
|
check_value_recursively, globals, plain_docs_sentence, summarize_font_family,
|
||||||
};
|
};
|
||||||
use crate::{analyze_expr, analyze_import, analyze_labels, named_items, IdeWorld};
|
use crate::{IdeWorld, analyze_expr, analyze_import, analyze_labels, named_items};
|
||||||
|
|
||||||
/// Autocomplete a cursor position in a source file.
|
/// Autocomplete a cursor position in a source file.
|
||||||
///
|
///
|
||||||
@ -76,7 +75,7 @@ pub struct Completion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A kind of item that can be completed.
|
/// A kind of item that can be completed.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum CompletionKind {
|
pub enum CompletionKind {
|
||||||
/// A syntactical structure.
|
/// A syntactical structure.
|
||||||
@ -130,7 +129,14 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start of a reference: "@|" or "@he|".
|
// Start of a reference: "@|".
|
||||||
|
if ctx.leaf.kind() == SyntaxKind::Text && ctx.before.ends_with("@") {
|
||||||
|
ctx.from = ctx.cursor;
|
||||||
|
ctx.label_completions();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An existing reference: "@he|".
|
||||||
if ctx.leaf.kind() == SyntaxKind::RefMarker {
|
if ctx.leaf.kind() == SyntaxKind::RefMarker {
|
||||||
ctx.from = ctx.leaf.offset() + 1;
|
ctx.from = ctx.leaf.offset() + 1;
|
||||||
ctx.label_completions();
|
ctx.label_completions();
|
||||||
@ -138,27 +144,23 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Behind a half-completed binding: "#let x = |".
|
// Behind a half-completed binding: "#let x = |".
|
||||||
if_chain! {
|
if let Some(prev) = ctx.leaf.prev_leaf()
|
||||||
if let Some(prev) = ctx.leaf.prev_leaf();
|
&& prev.kind() == SyntaxKind::Eq
|
||||||
if prev.kind() == SyntaxKind::Eq;
|
&& prev.parent_kind() == Some(SyntaxKind::LetBinding)
|
||||||
if prev.parent_kind() == Some(SyntaxKind::LetBinding);
|
{
|
||||||
then {
|
|
||||||
ctx.from = ctx.cursor;
|
ctx.from = ctx.cursor;
|
||||||
code_completions(ctx, false);
|
code_completions(ctx, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Behind a half-completed context block: "#context |".
|
// Behind a half-completed context block: "#context |".
|
||||||
if_chain! {
|
if let Some(prev) = ctx.leaf.prev_leaf()
|
||||||
if let Some(prev) = ctx.leaf.prev_leaf();
|
&& prev.kind() == SyntaxKind::Context
|
||||||
if prev.kind() == SyntaxKind::Context;
|
{
|
||||||
then {
|
|
||||||
ctx.from = ctx.cursor;
|
ctx.from = ctx.cursor;
|
||||||
code_completions(ctx, false);
|
code_completions(ctx, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Directly after a raw block.
|
// Directly after a raw block.
|
||||||
let mut s = Scanner::new(ctx.text);
|
let mut s = Scanner::new(ctx.text);
|
||||||
@ -366,38 +368,35 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Behind an expression plus dot: "emoji.|".
|
// Behind an expression plus dot: "emoji.|".
|
||||||
if_chain! {
|
if (ctx.leaf.kind() == SyntaxKind::Dot
|
||||||
if ctx.leaf.kind() == SyntaxKind::Dot
|
|
||||||
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
||||||
&& ctx.leaf.text() == ".");
|
&& ctx.leaf.text() == "."))
|
||||||
if ctx.leaf.range().end == ctx.cursor;
|
&& ctx.leaf.range().end == ctx.cursor
|
||||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
&& let Some(prev) = ctx.leaf.prev_sibling()
|
||||||
if !in_markup || prev.range().end == ctx.leaf.range().start;
|
&& (!in_markup || prev.range().end == ctx.leaf.range().start)
|
||||||
if prev.is::<ast::Expr>();
|
&& prev.is::<ast::Expr>()
|
||||||
if prev.parent_kind() != Some(SyntaxKind::Markup) ||
|
&& (prev.parent_kind() != Some(SyntaxKind::Markup)
|
||||||
prev.prev_sibling_kind() == Some(SyntaxKind::Hash);
|
|| prev.prev_sibling_kind() == Some(SyntaxKind::Hash))
|
||||||
if let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next();
|
&& let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next()
|
||||||
then {
|
{
|
||||||
ctx.from = ctx.cursor;
|
ctx.from = ctx.cursor;
|
||||||
field_access_completions(ctx, &value, &styles);
|
field_access_completions(ctx, &value, &styles);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Behind a started field access: "emoji.fa|".
|
// Behind a started field access: "emoji.fa|".
|
||||||
if_chain! {
|
if ctx.leaf.kind() == SyntaxKind::Ident
|
||||||
if ctx.leaf.kind() == SyntaxKind::Ident;
|
&& let Some(prev) = ctx.leaf.prev_sibling()
|
||||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
&& prev.kind() == SyntaxKind::Dot
|
||||||
if prev.kind() == SyntaxKind::Dot;
|
&& let Some(prev_prev) = prev.prev_sibling()
|
||||||
if let Some(prev_prev) = prev.prev_sibling();
|
&& prev_prev.is::<ast::Expr>()
|
||||||
if prev_prev.is::<ast::Expr>();
|
&& let Some((value, styles)) =
|
||||||
if let Some((value, styles)) = analyze_expr(ctx.world, &prev_prev).into_iter().next();
|
analyze_expr(ctx.world, &prev_prev).into_iter().next()
|
||||||
then {
|
{
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
field_access_completions(ctx, &value, &styles);
|
field_access_completions(ctx, &value, &styles);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -500,14 +499,11 @@ fn complete_open_labels(ctx: &mut CompletionContext) -> bool {
|
|||||||
fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
||||||
// In an import path for a file or package:
|
// In an import path for a file or package:
|
||||||
// "#import "|",
|
// "#import "|",
|
||||||
if_chain! {
|
if let Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude) =
|
||||||
if matches!(
|
ctx.leaf.parent_kind()
|
||||||
ctx.leaf.parent_kind(),
|
&& let Some(ast::Expr::Str(str)) = ctx.leaf.cast()
|
||||||
Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude)
|
{
|
||||||
);
|
|
||||||
if let Some(ast::Expr::Str(str)) = ctx.leaf.cast();
|
|
||||||
let value = str.get();
|
let value = str.get();
|
||||||
then {
|
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
if value.starts_with('@') {
|
if value.starts_with('@') {
|
||||||
let all_versions = value.contains(':');
|
let all_versions = value.contains(':');
|
||||||
@ -517,41 +513,36 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Behind an import list:
|
// Behind an import list:
|
||||||
// "#import "path.typ": |",
|
// "#import "path.typ": |",
|
||||||
// "#import "path.typ": a, b, |".
|
// "#import "path.typ": a, b, |".
|
||||||
if_chain! {
|
if let Some(prev) = ctx.leaf.prev_sibling()
|
||||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
&& let Some(ast::Expr::ModuleImport(import)) = prev.get().cast()
|
||||||
if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast();
|
&& let Some(ast::Imports::Items(items)) = import.imports()
|
||||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
&& let Some(source) = prev.children().find(|child| child.is::<ast::Expr>())
|
||||||
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
|
{
|
||||||
then {
|
|
||||||
ctx.from = ctx.cursor;
|
ctx.from = ctx.cursor;
|
||||||
import_item_completions(ctx, items, &source);
|
import_item_completions(ctx, items, &source);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Behind a half-started identifier in an import list:
|
// Behind a half-started identifier in an import list:
|
||||||
// "#import "path.typ": thi|",
|
// "#import "path.typ": thi|",
|
||||||
if_chain! {
|
if ctx.leaf.kind() == SyntaxKind::Ident
|
||||||
if ctx.leaf.kind() == SyntaxKind::Ident;
|
&& let Some(parent) = ctx.leaf.parent()
|
||||||
if let Some(parent) = ctx.leaf.parent();
|
&& parent.kind() == SyntaxKind::ImportItemPath
|
||||||
if parent.kind() == SyntaxKind::ImportItemPath;
|
&& let Some(grand) = parent.parent()
|
||||||
if let Some(grand) = parent.parent();
|
&& grand.kind() == SyntaxKind::ImportItems
|
||||||
if grand.kind() == SyntaxKind::ImportItems;
|
&& let Some(great) = grand.parent()
|
||||||
if let Some(great) = grand.parent();
|
&& let Some(ast::Expr::ModuleImport(import)) = great.get().cast()
|
||||||
if let Some(ast::Expr::ModuleImport(import)) = great.get().cast();
|
&& let Some(ast::Imports::Items(items)) = import.imports()
|
||||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
&& let Some(source) = great.children().find(|child| child.is::<ast::Expr>())
|
||||||
if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
|
{
|
||||||
then {
|
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
import_item_completions(ctx, items, &source);
|
import_item_completions(ctx, items, &source);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -600,16 +591,14 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Behind a half-completed show rule: "show strong: |".
|
// Behind a half-completed show rule: "show strong: |".
|
||||||
if_chain! {
|
if let Some(prev) = ctx.leaf.prev_leaf()
|
||||||
if let Some(prev) = ctx.leaf.prev_leaf();
|
&& matches!(prev.kind(), SyntaxKind::Colon)
|
||||||
if matches!(prev.kind(), SyntaxKind::Colon);
|
&& matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule))
|
||||||
if matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule));
|
{
|
||||||
then {
|
|
||||||
ctx.from = ctx.cursor;
|
ctx.from = ctx.cursor;
|
||||||
show_rule_recipe_completions(ctx);
|
show_rule_recipe_completions(ctx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -675,26 +664,23 @@ fn show_rule_recipe_completions(ctx: &mut CompletionContext) {
|
|||||||
/// Complete call and set rule parameters.
|
/// Complete call and set rule parameters.
|
||||||
fn complete_params(ctx: &mut CompletionContext) -> bool {
|
fn complete_params(ctx: &mut CompletionContext) -> bool {
|
||||||
// Ensure that we are in a function call or set rule's argument list.
|
// Ensure that we are in a function call or set rule's argument list.
|
||||||
let (callee, set, args, args_linked) = if_chain! {
|
let (callee, set, args, args_linked) = if let Some(parent) = ctx.leaf.parent()
|
||||||
if let Some(parent) = ctx.leaf.parent();
|
&& let Some(parent) = match parent.kind() {
|
||||||
if let Some(parent) = match parent.kind() {
|
|
||||||
SyntaxKind::Named => parent.parent(),
|
SyntaxKind::Named => parent.parent(),
|
||||||
_ => Some(parent),
|
_ => Some(parent),
|
||||||
};
|
}
|
||||||
if let Some(args) = parent.get().cast::<ast::Args>();
|
&& let Some(args) = parent.get().cast::<ast::Args>()
|
||||||
if let Some(grand) = parent.parent();
|
&& let Some(grand) = parent.parent()
|
||||||
if let Some(expr) = grand.get().cast::<ast::Expr>();
|
&& let Some(expr) = grand.get().cast::<ast::Expr>()
|
||||||
let set = matches!(expr, ast::Expr::SetRule(_));
|
&& let set = matches!(expr, ast::Expr::SetRule(_))
|
||||||
if let Some(callee) = match expr {
|
&& let Some(callee) = match expr {
|
||||||
ast::Expr::FuncCall(call) => Some(call.callee()),
|
ast::Expr::FuncCall(call) => Some(call.callee()),
|
||||||
ast::Expr::SetRule(set) => Some(set.target()),
|
ast::Expr::SetRule(set) => Some(set.target()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
} {
|
||||||
then {
|
|
||||||
(callee, set, args, parent)
|
(callee, set, args, parent)
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the piece of syntax that decides what we're completing.
|
// Find the piece of syntax that decides what we're completing.
|
||||||
@ -711,11 +697,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parameter values: "func(param:|)", "func(param: |)".
|
// Parameter values: "func(param:|)", "func(param: |)".
|
||||||
if_chain! {
|
if let SyntaxKind::Colon = deciding.kind()
|
||||||
if deciding.kind() == SyntaxKind::Colon;
|
&& let Some(prev) = deciding.prev_leaf()
|
||||||
if let Some(prev) = deciding.prev_leaf();
|
&& let Some(param) = prev.get().cast::<ast::Ident>()
|
||||||
if let Some(param) = prev.get().cast::<ast::Ident>();
|
{
|
||||||
then {
|
|
||||||
if let Some(next) = deciding.next_leaf() {
|
if let Some(next) = deciding.next_leaf() {
|
||||||
ctx.from = ctx.cursor.min(next.offset());
|
ctx.from = ctx.cursor.min(next.offset());
|
||||||
}
|
}
|
||||||
@ -723,13 +708,11 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
|
|||||||
named_param_value_completions(ctx, callee, ¶m);
|
named_param_value_completions(ctx, callee, ¶m);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Parameters: "func(|)", "func(hi|)", "func(12,|)".
|
// Parameters: "func(|)", "func(hi|)", "func(12,|)".
|
||||||
if_chain! {
|
if let SyntaxKind::LeftParen | SyntaxKind::Comma = deciding.kind()
|
||||||
if matches!(deciding.kind(), SyntaxKind::LeftParen | SyntaxKind::Comma);
|
&& (deciding.kind() != SyntaxKind::Comma || deciding.range().end < ctx.cursor)
|
||||||
if deciding.kind() != SyntaxKind::Comma || deciding.range().end < ctx.cursor;
|
{
|
||||||
then {
|
|
||||||
if let Some(next) = deciding.next_leaf() {
|
if let Some(next) = deciding.next_leaf() {
|
||||||
ctx.from = ctx.cursor.min(next.offset());
|
ctx.from = ctx.cursor.min(next.offset());
|
||||||
}
|
}
|
||||||
@ -737,7 +720,6 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
|
|||||||
param_completions(ctx, callee, set, args, args_linked);
|
param_completions(ctx, callee, set, args, args_linked);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -852,7 +834,7 @@ fn param_value_completions<'a>(
|
|||||||
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
|
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
|
||||||
Some(match (func.name(), param.name) {
|
Some(match (func.name(), param.name) {
|
||||||
(Some("image"), "source") => {
|
(Some("image"), "source") => {
|
||||||
&["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp"]
|
&["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp", "pdf"]
|
||||||
}
|
}
|
||||||
(Some("csv"), "source") => &["csv"],
|
(Some("csv"), "source") => &["csv"],
|
||||||
(Some("plugin"), "source") => &["wasm"],
|
(Some("plugin"), "source") => &["wasm"],
|
||||||
@ -1097,15 +1079,13 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
|||||||
fn is_in_equation_show_rule(leaf: &LinkedNode<'_>) -> bool {
|
fn is_in_equation_show_rule(leaf: &LinkedNode<'_>) -> bool {
|
||||||
let mut node = leaf;
|
let mut node = leaf;
|
||||||
while let Some(parent) = node.parent() {
|
while let Some(parent) = node.parent() {
|
||||||
if_chain! {
|
if let Some(expr) = parent.get().cast::<ast::Expr>()
|
||||||
if let Some(expr) = parent.get().cast::<ast::Expr>();
|
&& let ast::Expr::ShowRule(show) = expr
|
||||||
if let ast::Expr::ShowRule(show) = expr;
|
&& let Some(ast::Expr::FieldAccess(field)) = show.selector()
|
||||||
if let Some(ast::Expr::FieldAccess(field)) = show.selector();
|
&& field.field().as_str() == "equation"
|
||||||
if field.field().as_str() == "equation";
|
{
|
||||||
then {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
node = parent;
|
node = parent;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@ -1375,11 +1355,12 @@ impl<'a> CompletionContext<'a> {
|
|||||||
}
|
}
|
||||||
} else if at {
|
} else if at {
|
||||||
apply = Some(eco_format!("at(\"{label}\")"));
|
apply = Some(eco_format!("at(\"{label}\")"));
|
||||||
} else if label.starts_with('"') && self.after.starts_with('"') {
|
} else if label.starts_with('"')
|
||||||
if let Some(trimmed) = label.strip_suffix('"') {
|
&& self.after.starts_with('"')
|
||||||
|
&& let Some(trimmed) = label.strip_suffix('"')
|
||||||
|
{
|
||||||
apply = Some(trimmed.into());
|
apply = Some(trimmed.into());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.completions.push(Completion {
|
self.completions.push(Completion {
|
||||||
kind: kind.unwrap_or_else(|| match value {
|
kind: kind.unwrap_or_else(|| match value {
|
||||||
@ -1564,7 +1545,7 @@ mod tests {
|
|||||||
|
|
||||||
use typst::layout::PagedDocument;
|
use typst::layout::PagedDocument;
|
||||||
|
|
||||||
use super::{autocomplete, Completion};
|
use super::{Completion, CompletionKind, autocomplete};
|
||||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||||
|
|
||||||
/// Quote a string.
|
/// Quote a string.
|
||||||
@ -1644,6 +1625,19 @@ mod tests {
|
|||||||
test_with_doc(world, pos, doc.as_ref())
|
test_with_doc(world, pos, doc.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn test_with_addition(
|
||||||
|
initial_text: &str,
|
||||||
|
addition: &str,
|
||||||
|
pos: impl FilePos,
|
||||||
|
) -> Response {
|
||||||
|
let mut world = TestWorld::new(initial_text);
|
||||||
|
let doc = typst::compile(&world).output.ok();
|
||||||
|
let end = world.main.text().len();
|
||||||
|
world.main.edit(end..end, addition);
|
||||||
|
test_with_doc(&world, pos, doc.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn test_with_doc(
|
fn test_with_doc(
|
||||||
world: impl WorldLike,
|
world: impl WorldLike,
|
||||||
@ -1709,6 +1703,30 @@ mod tests {
|
|||||||
.must_exclude(["bib"]);
|
.must_exclude(["bib"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_ref_function() {
|
||||||
|
test_with_addition("x<test>", " #ref(<)", -2).must_include(["test"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_ref_shorthand() {
|
||||||
|
test_with_addition("x<test>", " @", -1).must_include(["test"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_ref_shorthand_with_partial_identifier() {
|
||||||
|
test_with_addition("x<test>", " @te", -1).must_include(["test"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_ref_identical_labels_returns_single_completion() {
|
||||||
|
let result = test_with_addition("x<test> y<test>", " @t", -1);
|
||||||
|
let completions = result.completions();
|
||||||
|
let label_count =
|
||||||
|
completions.iter().filter(|c| c.kind == CompletionKind::Label).count();
|
||||||
|
assert_eq!(label_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
/// Test what kind of brackets we autocomplete for function calls depending
|
/// Test what kind of brackets we autocomplete for function calls depending
|
||||||
/// on the function and existing parens.
|
/// on the function and existing parens.
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use typst::foundations::{Label, Selector, Value};
|
use typst::foundations::{Label, Selector, Value};
|
||||||
use typst::layout::PagedDocument;
|
use typst::layout::PagedDocument;
|
||||||
use typst::syntax::{ast, LinkedNode, Side, Source, Span};
|
use typst::syntax::{LinkedNode, Side, Source, Span, ast};
|
||||||
use typst::utils::PicoStr;
|
use typst::utils::PicoStr;
|
||||||
|
|
||||||
use crate::utils::globals;
|
use crate::utils::globals;
|
||||||
use crate::{
|
use crate::{
|
||||||
analyze_expr, analyze_import, deref_target, named_items, DerefTarget, IdeWorld,
|
DerefTarget, IdeWorld, NamedItem, analyze_expr, analyze_import, deref_target,
|
||||||
NamedItem,
|
named_items,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A definition of some item.
|
/// A definition of some item.
|
||||||
@ -90,11 +90,11 @@ mod tests {
|
|||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use typst::WorldExt;
|
||||||
use typst::foundations::{IntoValue, NativeElement};
|
use typst::foundations::{IntoValue, NativeElement};
|
||||||
use typst::syntax::Side;
|
use typst::syntax::Side;
|
||||||
use typst::WorldExt;
|
|
||||||
|
|
||||||
use super::{definition, Definition};
|
use super::{Definition, definition};
|
||||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||||
|
|
||||||
type Response = (TestWorld, Option<Definition>);
|
type Response = (TestWorld, Option<Definition>);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
|
use typst::WorldExt;
|
||||||
use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
|
use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
|
||||||
use typst::model::{Destination, Url};
|
use typst::model::{Destination, Url};
|
||||||
use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind};
|
use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind};
|
||||||
use typst::visualize::{Curve, CurveItem, FillRule, Geometry};
|
use typst::visualize::{Curve, CurveItem, FillRule, Geometry};
|
||||||
use typst::WorldExt;
|
|
||||||
|
|
||||||
use crate::IdeWorld;
|
use crate::IdeWorld;
|
||||||
|
|
||||||
@ -36,8 +36,9 @@ pub fn jump_from_click(
|
|||||||
) -> Option<Jump> {
|
) -> Option<Jump> {
|
||||||
// Try to find a link first.
|
// Try to find a link first.
|
||||||
for (pos, item) in frame.items() {
|
for (pos, item) in frame.items() {
|
||||||
if let FrameItem::Link(dest, size) = item {
|
if let FrameItem::Link(dest, size) = item
|
||||||
if is_in_rect(*pos, *size, click) {
|
&& is_in_rect(*pos, *size, click)
|
||||||
|
{
|
||||||
return Some(match dest {
|
return Some(match dest {
|
||||||
Destination::Url(url) => Jump::Url(url.clone()),
|
Destination::Url(url) => Jump::Url(url.clone()),
|
||||||
Destination::Position(pos) => Jump::Position(*pos),
|
Destination::Position(pos) => Jump::Position(*pos),
|
||||||
@ -47,18 +48,17 @@ pub fn jump_from_click(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no link, search for a jump target.
|
// If there's no link, search for a jump target.
|
||||||
for (mut pos, item) in frame.items().rev() {
|
for &(mut pos, ref item) in frame.items().rev() {
|
||||||
match item {
|
match item {
|
||||||
FrameItem::Group(group) => {
|
FrameItem::Group(group) => {
|
||||||
let pos = click - pos;
|
let pos = click - pos;
|
||||||
if let Some(clip) = &group.clip {
|
if let Some(clip) = &group.clip
|
||||||
if !clip.contains(FillRule::NonZero, pos) {
|
&& !clip.contains(FillRule::NonZero, pos)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Realistic transforms should always be invertible.
|
// Realistic transforms should always be invertible.
|
||||||
// An example of one that isn't is a scale of 0, which would
|
// An example of one that isn't is a scale of 0, which would
|
||||||
// not be clickable anyway.
|
// not be clickable anyway.
|
||||||
@ -177,12 +177,12 @@ pub fn jump_from_cursor(
|
|||||||
|
|
||||||
/// Find the position of a span in a frame.
|
/// Find the position of a span in a frame.
|
||||||
fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> {
|
fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> {
|
||||||
for (mut pos, item) in frame.items() {
|
for &(mut pos, ref item) in frame.items() {
|
||||||
if let FrameItem::Group(group) = item {
|
if let FrameItem::Group(group) = item
|
||||||
if let Some(point) = find_in_frame(&group.frame, span) {
|
&& let Some(point) = find_in_frame(&group.frame, span)
|
||||||
|
{
|
||||||
return Some(pos + point.transform(group.transform));
|
return Some(pos + point.transform(group.transform));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let FrameItem::Text(text) = item {
|
if let FrameItem::Text(text) = item {
|
||||||
for glyph in &text.glyphs {
|
for glyph in &text.glyphs {
|
||||||
@ -222,7 +222,7 @@ mod tests {
|
|||||||
|
|
||||||
use typst::layout::{Abs, Point, Position};
|
use typst::layout::{Abs, Point, Position};
|
||||||
|
|
||||||
use super::{jump_from_click, jump_from_cursor, Jump};
|
use super::{Jump, jump_from_click, jump_from_cursor};
|
||||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||||
|
|
||||||
fn point(x: f64, y: f64) -> Point {
|
fn point(x: f64, y: f64) -> Point {
|
||||||
|
@ -9,16 +9,16 @@ mod tooltip;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use self::analyze::{analyze_expr, analyze_import, analyze_labels};
|
pub use self::analyze::{analyze_expr, analyze_import, analyze_labels};
|
||||||
pub use self::complete::{autocomplete, Completion, CompletionKind};
|
pub use self::complete::{Completion, CompletionKind, autocomplete};
|
||||||
pub use self::definition::{definition, Definition};
|
pub use self::definition::{Definition, definition};
|
||||||
pub use self::jump::{jump_from_click, jump_from_cursor, Jump};
|
pub use self::jump::{Jump, jump_from_click, jump_from_cursor};
|
||||||
pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem};
|
pub use self::matchers::{DerefTarget, NamedItem, deref_target, named_items};
|
||||||
pub use self::tooltip::{tooltip, Tooltip};
|
pub use self::tooltip::{Tooltip, tooltip};
|
||||||
|
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst::syntax::package::PackageSpec;
|
|
||||||
use typst::syntax::FileId;
|
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
use typst::syntax::FileId;
|
||||||
|
use typst::syntax::package::PackageSpec;
|
||||||
|
|
||||||
/// Extends the `World` for IDE functionality.
|
/// Extends the `World` for IDE functionality.
|
||||||
pub trait IdeWorld: World {
|
pub trait IdeWorld: World {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst::foundations::{Module, Value};
|
use typst::foundations::{Module, Value};
|
||||||
use typst::syntax::ast::AstNode;
|
use typst::syntax::ast::AstNode;
|
||||||
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
|
use typst::syntax::{LinkedNode, Span, SyntaxKind, ast};
|
||||||
|
|
||||||
use crate::{analyze_import, IdeWorld};
|
use crate::{IdeWorld, analyze_import};
|
||||||
|
|
||||||
/// Find the named items starting from the given position.
|
/// Find the named items starting from the given position.
|
||||||
pub fn named_items<T>(
|
pub fn named_items<T>(
|
||||||
@ -59,11 +59,11 @@ pub fn named_items<T>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Seeing the module itself.
|
// Seeing the module itself.
|
||||||
if let Some((name, span)) = name_and_span {
|
if let Some((name, span)) = name_and_span
|
||||||
if let Some(res) = recv(NamedItem::Module(&name, span, module)) {
|
&& let Some(res) = recv(NamedItem::Module(&name, span, module))
|
||||||
|
{
|
||||||
return Some(res);
|
return Some(res);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Seeing the imported items.
|
// Seeing the imported items.
|
||||||
match imports {
|
match imports {
|
||||||
@ -124,8 +124,9 @@ pub fn named_items<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent) = node.parent() {
|
if let Some(parent) = node.parent() {
|
||||||
if let Some(v) = parent.cast::<ast::ForLoop>() {
|
if let Some(v) = parent.cast::<ast::ForLoop>()
|
||||||
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
|
&& node.prev_sibling_kind() != Some(SyntaxKind::In)
|
||||||
|
{
|
||||||
let pattern = v.pattern();
|
let pattern = v.pattern();
|
||||||
for ident in pattern.bindings() {
|
for ident in pattern.bindings() {
|
||||||
if let Some(res) = recv(NamedItem::Var(ident)) {
|
if let Some(res) = recv(NamedItem::Var(ident)) {
|
||||||
@ -133,7 +134,6 @@ pub fn named_items<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = parent.cast::<ast::Closure>().filter(|v| {
|
if let Some(v) = parent.cast::<ast::Closure>().filter(|v| {
|
||||||
// Check if the node is in the body of the closure.
|
// Check if the node is in the body of the closure.
|
||||||
@ -155,15 +155,15 @@ pub fn named_items<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ast::Param::Spread(s) => {
|
ast::Param::Spread(s) => {
|
||||||
if let Some(sink_ident) = s.sink_ident() {
|
if let Some(sink_ident) = s.sink_ident()
|
||||||
if let Some(t) = recv(NamedItem::Var(sink_ident)) {
|
&& let Some(t) = recv(NamedItem::Var(sink_ident))
|
||||||
|
{
|
||||||
return Some(t);
|
return Some(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ancestor = Some(parent.clone());
|
ancestor = Some(parent.clone());
|
||||||
continue;
|
continue;
|
||||||
@ -216,7 +216,7 @@ impl<'a> NamedItem<'a> {
|
|||||||
|
|
||||||
/// Categorize an expression into common classes IDE functionality can operate
|
/// Categorize an expression into common classes IDE functionality can operate
|
||||||
/// on.
|
/// on.
|
||||||
pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
|
pub fn deref_target(node: LinkedNode<'_>) -> Option<DerefTarget<'_>> {
|
||||||
// Move to the first ancestor that is an expression.
|
// Move to the first ancestor that is an expression.
|
||||||
let mut ancestor = node;
|
let mut ancestor = node;
|
||||||
while !ancestor.is::<ast::Expr>() {
|
while !ancestor.is::<ast::Expr>() {
|
||||||
|
@ -9,7 +9,7 @@ use typst::layout::{Abs, Margin, PageElem};
|
|||||||
use typst::syntax::package::{PackageSpec, PackageVersion};
|
use typst::syntax::package::{PackageSpec, PackageVersion};
|
||||||
use typst::syntax::{FileId, Source, VirtualPath};
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
use typst::text::{Font, FontBook, TextElem, TextSize};
|
use typst::text::{Font, FontBook, TextElem, TextSize};
|
||||||
use typst::utils::{singleton, LazyHash};
|
use typst::utils::{LazyHash, singleton};
|
||||||
use typst::{Feature, Library, LibraryExt, World};
|
use typst::{Feature, Library, LibraryExt, World};
|
||||||
|
|
||||||
use crate::IdeWorld;
|
use crate::IdeWorld;
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use if_chain::if_chain;
|
|
||||||
use typst::engine::Sink;
|
use typst::engine::Sink;
|
||||||
use typst::foundations::{repr, Binding, Capturer, CastInfo, Repr, Value};
|
use typst::foundations::{Binding, Capturer, CastInfo, Repr, Value, repr};
|
||||||
use typst::layout::{Length, PagedDocument};
|
use typst::layout::{Length, PagedDocument};
|
||||||
use typst::syntax::ast::AstNode;
|
use typst::syntax::ast::AstNode;
|
||||||
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
use typst::syntax::{LinkedNode, Side, Source, SyntaxKind, ast};
|
||||||
use typst::utils::{round_with_precision, Numeric};
|
use typst::utils::{Numeric, round_with_precision};
|
||||||
use typst_eval::CapturesVisitor;
|
use typst_eval::CapturesVisitor;
|
||||||
|
|
||||||
use crate::utils::{plain_docs_sentence, summarize_font_family};
|
use crate::utils::{plain_docs_sentence, summarize_font_family};
|
||||||
use crate::{analyze_expr, analyze_import, analyze_labels, IdeWorld};
|
use crate::{IdeWorld, analyze_expr, analyze_import, analyze_labels};
|
||||||
|
|
||||||
/// Describe the item under the cursor.
|
/// Describe the item under the cursor.
|
||||||
///
|
///
|
||||||
@ -66,12 +65,12 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
|||||||
return Some(Tooltip::Text(plain_docs_sentence(docs)));
|
return Some(Tooltip::Text(plain_docs_sentence(docs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let &Value::Length(length) = value {
|
if let &Value::Length(length) = value
|
||||||
if let Some(tooltip) = length_tooltip(length) {
|
&& let Some(tooltip) = length_tooltip(length)
|
||||||
|
{
|
||||||
return Some(tooltip);
|
return Some(tooltip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if expr.is_literal() {
|
if expr.is_literal() {
|
||||||
return None;
|
return None;
|
||||||
@ -93,11 +92,11 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
|||||||
last = Some((value, 1));
|
last = Some((value, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((_, count)) = last {
|
if let Some((_, count)) = last
|
||||||
if count > 1 {
|
&& count > 1
|
||||||
|
{
|
||||||
write!(pieces.last_mut().unwrap(), " (×{count})").unwrap();
|
write!(pieces.last_mut().unwrap(), " (×{count})").unwrap();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if iter.next().is_some() {
|
if iter.next().is_some() {
|
||||||
pieces.push("...".into());
|
pieces.push("...".into());
|
||||||
@ -109,20 +108,18 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
|||||||
|
|
||||||
/// Tooltips for imports.
|
/// Tooltips for imports.
|
||||||
fn import_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
fn import_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||||
if_chain! {
|
if leaf.kind() == SyntaxKind::Star
|
||||||
if leaf.kind() == SyntaxKind::Star;
|
&& let Some(parent) = leaf.parent()
|
||||||
if let Some(parent) = leaf.parent();
|
&& let Some(import) = parent.cast::<ast::ModuleImport>()
|
||||||
if let Some(import) = parent.cast::<ast::ModuleImport>();
|
&& let Some(node) = parent.find(import.source().span())
|
||||||
if let Some(node) = parent.find(import.source().span());
|
&& let Some(value) = analyze_import(world, &node)
|
||||||
if let Some(value) = analyze_import(world, &node);
|
&& let Some(scope) = value.scope()
|
||||||
if let Some(scope) = value.scope();
|
{
|
||||||
then {
|
|
||||||
let names: Vec<_> =
|
let names: Vec<_> =
|
||||||
scope.iter().map(|(name, ..)| eco_format!("`{name}`")).collect();
|
scope.iter().map(|(name, ..)| eco_format!("`{name}`")).collect();
|
||||||
let list = repr::separated_list(&names, "and");
|
let list = repr::separated_list(&names, "and");
|
||||||
return Some(Tooltip::Text(eco_format!("This star imports {list}")));
|
return Some(Tooltip::Text(eco_format!("This star imports {list}")));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -190,51 +187,46 @@ fn label_tooltip(document: &PagedDocument, leaf: &LinkedNode) -> Option<Tooltip>
|
|||||||
|
|
||||||
/// Tooltips for components of a named parameter.
|
/// Tooltips for components of a named parameter.
|
||||||
fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||||
let (func, named) = if_chain! {
|
let (func, named) =
|
||||||
// Ensure that we are in a named pair in the arguments to a function
|
// Ensure that we are in a named pair in the arguments to a function
|
||||||
// call or set rule.
|
// call or set rule.
|
||||||
if let Some(parent) = leaf.parent();
|
if let Some(parent) = leaf.parent()
|
||||||
if let Some(named) = parent.cast::<ast::Named>();
|
&& let Some(named) = parent.cast::<ast::Named>()
|
||||||
if let Some(grand) = parent.parent();
|
&& let Some(grand) = parent.parent()
|
||||||
if matches!(grand.kind(), SyntaxKind::Args);
|
&& matches!(grand.kind(), SyntaxKind::Args)
|
||||||
if let Some(grand_grand) = grand.parent();
|
&& let Some(grand_grand) = grand.parent()
|
||||||
if let Some(expr) = grand_grand.cast::<ast::Expr>();
|
&& let Some(expr) = grand_grand.cast::<ast::Expr>()
|
||||||
if let Some(ast::Expr::Ident(callee)) = match expr {
|
&& let Some(ast::Expr::Ident(callee)) = match expr {
|
||||||
ast::Expr::FuncCall(call) => Some(call.callee()),
|
ast::Expr::FuncCall(call) => Some(call.callee()),
|
||||||
ast::Expr::SetRule(set) => Some(set.target()),
|
ast::Expr::SetRule(set) => Some(set.target()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
}
|
||||||
|
|
||||||
// Find metadata about the function.
|
// Find metadata about the function.
|
||||||
if let Some(Value::Func(func)) = world
|
&& let Some(Value::Func(func)) = world
|
||||||
.library()
|
.library()
|
||||||
.global
|
.global
|
||||||
.scope()
|
.scope()
|
||||||
.get(&callee)
|
.get(&callee)
|
||||||
.map(Binding::read);
|
.map(Binding::read)
|
||||||
then { (func, named) }
|
{ (func, named) }
|
||||||
else { return None; }
|
else { return None; };
|
||||||
};
|
|
||||||
|
|
||||||
// Hovering over the parameter name.
|
// Hovering over the parameter name.
|
||||||
if_chain! {
|
if leaf.index() == 0
|
||||||
if leaf.index() == 0;
|
&& let Some(ident) = leaf.cast::<ast::Ident>()
|
||||||
if let Some(ident) = leaf.cast::<ast::Ident>();
|
&& let Some(param) = func.param(&ident)
|
||||||
if let Some(param) = func.param(&ident);
|
{
|
||||||
then {
|
|
||||||
return Some(Tooltip::Text(plain_docs_sentence(param.docs)));
|
return Some(Tooltip::Text(plain_docs_sentence(param.docs)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Hovering over a string parameter value.
|
// Hovering over a string parameter value.
|
||||||
if_chain! {
|
if let Some(string) = leaf.cast::<ast::Str>()
|
||||||
if let Some(string) = leaf.cast::<ast::Str>();
|
&& let Some(param) = func.param(&named.name())
|
||||||
if let Some(param) = func.param(&named.name());
|
&& let Some(docs) = find_string_doc(¶m.input, &string.get())
|
||||||
if let Some(docs) = find_string_doc(¶m.input, &string.get());
|
{
|
||||||
then {
|
|
||||||
return Some(Tooltip::Text(docs.into()));
|
return Some(Tooltip::Text(docs.into()));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -252,27 +244,24 @@ fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> {
|
|||||||
|
|
||||||
/// Tooltip for font.
|
/// Tooltip for font.
|
||||||
fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||||
if_chain! {
|
|
||||||
// Ensure that we are on top of a string.
|
// Ensure that we are on top of a string.
|
||||||
if let Some(string) = leaf.cast::<ast::Str>();
|
if let Some(string) = leaf.cast::<ast::Str>()
|
||||||
let lower = string.get().to_lowercase();
|
&& let lower = string.get().to_lowercase()
|
||||||
|
|
||||||
// Ensure that we are in the arguments to the text function.
|
// Ensure that we are in the arguments to the text function.
|
||||||
if let Some(parent) = leaf.parent();
|
&& let Some(parent) = leaf.parent()
|
||||||
if let Some(named) = parent.cast::<ast::Named>();
|
&& let Some(named) = parent.cast::<ast::Named>()
|
||||||
if named.name().as_str() == "font";
|
&& named.name().as_str() == "font"
|
||||||
|
|
||||||
// Find the font family.
|
// Find the font family.
|
||||||
if let Some((_, iter)) = world
|
&& let Some((_, iter)) = world
|
||||||
.book()
|
.book()
|
||||||
.families()
|
.families()
|
||||||
.find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str());
|
.find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str())
|
||||||
|
{
|
||||||
then {
|
|
||||||
let detail = summarize_font_family(iter.collect());
|
let detail = summarize_font_family(iter.collect());
|
||||||
return Some(Tooltip::Text(detail));
|
return Some(Tooltip::Text(detail));
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -283,7 +272,7 @@ mod tests {
|
|||||||
|
|
||||||
use typst::syntax::Side;
|
use typst::syntax::Side;
|
||||||
|
|
||||||
use super::{tooltip, Tooltip};
|
use super::{Tooltip, tooltip};
|
||||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||||
|
|
||||||
type Response = Option<Tooltip>;
|
type Response = Option<Tooltip>;
|
||||||
|
@ -2,7 +2,7 @@ use std::fmt::Write;
|
|||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
|
|
||||||
use comemo::Track;
|
use comemo::Track;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use typst::engine::{Engine, Route, Sink, Traced};
|
use typst::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst::foundations::{Scope, Value};
|
use typst::foundations::{Scope, Value};
|
||||||
use typst::introspection::Introspector;
|
use typst::introspection::Introspector;
|
||||||
@ -119,11 +119,7 @@ pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope {
|
|||||||
.is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash));
|
.is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash));
|
||||||
|
|
||||||
let library = world.library();
|
let library = world.library();
|
||||||
if in_math {
|
if in_math { library.math.scope() } else { library.global.scope() }
|
||||||
library.math.scope()
|
|
||||||
} else {
|
|
||||||
library.global.scope()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether the given value or any of its constituent parts satisfy the
|
/// Checks whether the given value or any of its constituent parts satisfy the
|
||||||
|
@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
|
use typst_library::diag::{PackageError, PackageResult, StrResult, bail};
|
||||||
use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
|
use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
|
||||||
|
|
||||||
use crate::download::{Downloader, Progress};
|
use crate::download::{Downloader, Progress};
|
||||||
@ -189,7 +189,7 @@ impl PackageStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(PackageError::NetworkFailed(Some(eco_format!("{err}"))))
|
return Err(PackageError::NetworkFailed(Some(eco_format!("{err}"))));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -407,11 +407,11 @@ fn distribute<'a>(
|
|||||||
// If there is still something remaining, apply it to the
|
// If there is still something remaining, apply it to the
|
||||||
// last region (it will overflow, but there's nothing else
|
// last region (it will overflow, but there's nothing else
|
||||||
// we can do).
|
// we can do).
|
||||||
if !remaining.approx_empty() {
|
if !remaining.approx_empty()
|
||||||
if let Some(last) = buf.last_mut() {
|
&& let Some(last) = buf.last_mut()
|
||||||
|
{
|
||||||
*last += remaining;
|
*last += remaining;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Distribute the heights to the first region and the
|
// Distribute the heights to the first region and the
|
||||||
// backlog. There is no last region, since the height is
|
// backlog. There is no last region, since the height is
|
||||||
|
@ -2,10 +2,11 @@ use std::cell::{LazyCell, RefCell};
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
use bumpalo::boxed::Box as BumpBox;
|
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
|
use bumpalo::boxed::Box as BumpBox;
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use typst_library::diag::{bail, warning, SourceResult};
|
use typst_library::World;
|
||||||
|
use typst_library::diag::{SourceResult, bail, warning};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||||
use typst_library::introspection::{
|
use typst_library::introspection::{
|
||||||
@ -19,10 +20,9 @@ use typst_library::layout::{
|
|||||||
use typst_library::model::ParElem;
|
use typst_library::model::ParElem;
|
||||||
use typst_library::routines::{Pair, Routines};
|
use typst_library::routines::{Pair, Routines};
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_library::World;
|
|
||||||
use typst_utils::SliceExt;
|
use typst_utils::SliceExt;
|
||||||
|
|
||||||
use super::{layout_multi_block, layout_single_block, FlowMode};
|
use super::{FlowMode, layout_multi_block, layout_single_block};
|
||||||
use crate::inline::ParSituation;
|
use crate::inline::ParSituation;
|
||||||
use crate::modifiers::layout_and_modify;
|
use crate::modifiers::layout_and_modify;
|
||||||
|
|
||||||
@ -684,11 +684,11 @@ impl<T> CachedCell<T> {
|
|||||||
let input_hash = typst_utils::hash128(&input);
|
let input_hash = typst_utils::hash128(&input);
|
||||||
|
|
||||||
let mut slot = self.0.borrow_mut();
|
let mut slot = self.0.borrow_mut();
|
||||||
if let Some((hash, output)) = &*slot {
|
if let Some((hash, output)) = &*slot
|
||||||
if *hash == input_hash {
|
&& *hash == input_hash
|
||||||
|
{
|
||||||
return output.clone();
|
return output.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let output = f(input);
|
let output = f(input);
|
||||||
*slot = Some((input_hash, output.clone()));
|
*slot = Some((input_hash, output.clone()));
|
||||||
|
@ -18,7 +18,7 @@ use typst_syntax::Span;
|
|||||||
use typst_utils::{NonZeroExt, Numeric};
|
use typst_utils::{NonZeroExt, Numeric};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work,
|
Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work, distribute,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Composes the contents of a single page/region. A region can have multiple
|
/// Composes the contents of a single page/region. A region can have multiple
|
||||||
@ -319,11 +319,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
|||||||
let used = base.y - remaining;
|
let used = base.y - remaining;
|
||||||
let half = need / 2.0;
|
let half = need / 2.0;
|
||||||
let ratio = (used + half) / base.y;
|
let ratio = (used + half) / base.y;
|
||||||
if ratio <= 0.5 {
|
if ratio <= 0.5 { FixedAlignment::Start } else { FixedAlignment::End }
|
||||||
FixedAlignment::Start
|
|
||||||
} else {
|
|
||||||
FixedAlignment::End
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select the insertion area where we'll put this float.
|
// Select the insertion area where we'll put this float.
|
||||||
|
@ -14,7 +14,8 @@ use std::rc::Rc;
|
|||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use ecow::EcoVec;
|
use ecow::EcoVec;
|
||||||
use typst_library::diag::{bail, At, SourceDiagnostic, SourceResult};
|
use typst_library::World;
|
||||||
|
use typst_library::diag::{At, SourceDiagnostic, SourceResult, bail};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||||
use typst_library::introspection::{
|
use typst_library::introspection::{
|
||||||
@ -27,14 +28,13 @@ use typst_library::layout::{
|
|||||||
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
||||||
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_library::World;
|
|
||||||
use typst_utils::{NonZeroExt, Numeric};
|
use typst_utils::{NonZeroExt, Numeric};
|
||||||
|
|
||||||
use self::block::{layout_multi_block, layout_single_block};
|
use self::block::{layout_multi_block, layout_single_block};
|
||||||
use self::collect::{
|
use self::collect::{
|
||||||
collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild,
|
Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, collect,
|
||||||
};
|
};
|
||||||
use self::compose::{compose, Composer};
|
use self::compose::{Composer, compose};
|
||||||
use self::distribute::distribute;
|
use self::distribute::distribute;
|
||||||
|
|
||||||
/// Lays out content into a single region, producing a single frame.
|
/// Lays out content into a single region, producing a single frame.
|
||||||
@ -143,7 +143,7 @@ fn layout_fragment_impl(
|
|||||||
let mut kind = FragmentKind::Block;
|
let mut kind = FragmentKind::Block;
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let children = (engine.routines.realize)(
|
let children = (engine.routines.realize)(
|
||||||
RealizationKind::LayoutFragment(&mut kind),
|
RealizationKind::LayoutFragment { kind: &mut kind },
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Resolve, StyleChain};
|
use typst_library::foundations::{Resolve, StyleChain};
|
||||||
use typst_library::layout::grid::resolve::{
|
use typst_library::layout::grid::resolve::{
|
||||||
@ -16,8 +16,8 @@ use typst_syntax::Span;
|
|||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
|
LineSegment, Rowspan, UnbreakableRowGroup, generate_line_segments,
|
||||||
LineSegment, Rowspan, UnbreakableRowGroup,
|
hline_stroke_at_column, layout_cell, vline_stroke_at_row,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Performs grid layout.
|
/// Performs grid layout.
|
||||||
@ -274,32 +274,33 @@ impl<'a> GridLayouter<'a> {
|
|||||||
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
|
||||||
self.measure_columns(engine)?;
|
self.measure_columns(engine)?;
|
||||||
|
|
||||||
if let Some(footer) = &self.grid.footer {
|
if let Some(footer) = &self.grid.footer
|
||||||
if footer.repeated {
|
&& footer.repeated
|
||||||
|
{
|
||||||
// Ensure rows in the first region will be aware of the
|
// Ensure rows in the first region will be aware of the
|
||||||
// possible presence of the footer.
|
// possible presence of the footer.
|
||||||
self.prepare_footer(footer, engine, 0)?;
|
self.prepare_footer(footer, engine, 0)?;
|
||||||
self.regions.size.y -= self.current.footer_height;
|
self.regions.size.y -= self.current.footer_height;
|
||||||
self.current.initial_after_repeats = self.regions.size.y;
|
self.current.initial_after_repeats = self.regions.size.y;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
let mut consecutive_header_count = 0;
|
let mut consecutive_header_count = 0;
|
||||||
while y < self.grid.rows.len() {
|
while y < self.grid.rows.len() {
|
||||||
if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
|
if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
|
||||||
|
&& next_header.range.contains(&y)
|
||||||
{
|
{
|
||||||
if next_header.range.contains(&y) {
|
|
||||||
self.place_new_headers(&mut consecutive_header_count, engine)?;
|
self.place_new_headers(&mut consecutive_header_count, engine)?;
|
||||||
y = next_header.range.end;
|
y = next_header.range.end;
|
||||||
|
|
||||||
// Skip header rows during normal layout.
|
// Skip header rows during normal layout.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(footer) = &self.grid.footer {
|
if let Some(footer) = &self.grid.footer
|
||||||
if footer.repeated && y >= footer.start {
|
&& footer.repeated
|
||||||
|
&& y >= footer.start
|
||||||
|
{
|
||||||
if y == footer.start {
|
if y == footer.start {
|
||||||
self.layout_footer(footer, engine, self.finished.len())?;
|
self.layout_footer(footer, engine, self.finished.len())?;
|
||||||
self.flush_orphans();
|
self.flush_orphans();
|
||||||
@ -307,7 +308,6 @@ impl<'a> GridLayouter<'a> {
|
|||||||
y = footer.end;
|
y = footer.end;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.layout_row(y, engine, 0)?;
|
self.layout_row(y, engine, 0)?;
|
||||||
|
|
||||||
@ -1228,7 +1228,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
.skip(parent.y)
|
.skip(parent.y)
|
||||||
.take(rowspan)
|
.take(rowspan)
|
||||||
.rev()
|
.rev()
|
||||||
.find(|(_, &row)| row == Sizing::Auto)
|
.find(|&(_, &row)| row == Sizing::Auto)
|
||||||
.map(|(y, _)| y);
|
.map(|(y, _)| y);
|
||||||
|
|
||||||
if last_spanned_auto_row != Some(y) {
|
if last_spanned_auto_row != Some(y) {
|
||||||
@ -1283,15 +1283,13 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// remeasure.
|
// remeasure.
|
||||||
if let Some([first, rest @ ..]) =
|
if let Some([first, rest @ ..]) =
|
||||||
frames.get(measurement_data.frames_in_previous_regions..)
|
frames.get(measurement_data.frames_in_previous_regions..)
|
||||||
{
|
&& can_skip
|
||||||
if can_skip
|
|
||||||
&& breakable
|
&& breakable
|
||||||
&& first.is_empty()
|
&& first.is_empty()
|
||||||
&& rest.iter().any(|frame| !frame.is_empty())
|
&& rest.iter().any(|frame| !frame.is_empty())
|
||||||
{
|
{
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Skip frames from previous regions if applicable.
|
// Skip frames from previous regions if applicable.
|
||||||
let mut sizes = frames
|
let mut sizes = frames
|
||||||
@ -1529,8 +1527,9 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// The latest rows have orphan prevention (headers) and no other rows
|
// The latest rows have orphan prevention (headers) and no other rows
|
||||||
// were placed, so remove those rows and try again in a new region,
|
// were placed, so remove those rows and try again in a new region,
|
||||||
// unless this is the last region.
|
// unless this is the last region.
|
||||||
if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take() {
|
if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take()
|
||||||
if !last {
|
&& !last
|
||||||
|
{
|
||||||
self.current.lrows.truncate(orphan_snapshot);
|
self.current.lrows.truncate(orphan_snapshot);
|
||||||
self.current.repeated_header_rows =
|
self.current.repeated_header_rows =
|
||||||
self.current.repeated_header_rows.min(orphan_snapshot);
|
self.current.repeated_header_rows.min(orphan_snapshot);
|
||||||
@ -1540,7 +1539,6 @@ impl<'a> GridLayouter<'a> {
|
|||||||
self.current.last_repeated_header_end = 0;
|
self.current.last_repeated_header_end = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.current
|
.current
|
||||||
@ -1571,8 +1569,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
&& self.current.could_progress_at_top;
|
&& self.current.could_progress_at_top;
|
||||||
|
|
||||||
let mut laid_out_footer_start = None;
|
let mut laid_out_footer_start = None;
|
||||||
if !footer_would_be_widow {
|
if !footer_would_be_widow && let Some(footer) = &self.grid.footer {
|
||||||
if let Some(footer) = &self.grid.footer {
|
|
||||||
// Don't layout the footer if it would be alone with the header
|
// Don't layout the footer if it would be alone with the header
|
||||||
// in the page (hence the widow check), and don't layout it
|
// in the page (hence the widow check), and don't layout it
|
||||||
// twice (check below).
|
// twice (check below).
|
||||||
@ -1587,7 +1584,6 @@ impl<'a> GridLayouter<'a> {
|
|||||||
self.layout_footer(footer, engine, self.finished.len())?;
|
self.layout_footer(footer, engine, self.finished.len())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the height of existing rows in the region.
|
// Determine the height of existing rows in the region.
|
||||||
let mut used = Abs::zero();
|
let mut used = Abs::zero();
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use typst_library::foundations::{AlternativeFold, Fold};
|
use typst_library::foundations::{AlternativeFold, Fold};
|
||||||
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
|
|
||||||
use typst_library::layout::Abs;
|
use typst_library::layout::Abs;
|
||||||
|
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
|
||||||
use typst_library::visualize::Stroke;
|
use typst_library::visualize::Stroke;
|
||||||
|
|
||||||
use super::RowPiece;
|
use super::RowPiece;
|
||||||
@ -291,14 +291,14 @@ pub fn vline_stroke_at_row(
|
|||||||
// We would then analyze the cell one column after (if at a gutter
|
// We would then analyze the cell one column after (if at a gutter
|
||||||
// column), and/or one row below (if at a gutter row), in order to
|
// column), and/or one row below (if at a gutter row), in order to
|
||||||
// check if it would be merged with a cell before the vline.
|
// check if it would be merged with a cell before the vline.
|
||||||
if let Some(parent) = grid.effective_parent_cell_position(x, y) {
|
if let Some(parent) = grid.effective_parent_cell_position(x, y)
|
||||||
if parent.x < x {
|
&& parent.x < x
|
||||||
|
{
|
||||||
// There is a colspan cell going through this vline's position,
|
// There is a colspan cell going through this vline's position,
|
||||||
// so don't draw it here.
|
// so don't draw it here.
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let (left_cell_stroke, left_cell_prioritized) = x
|
let (left_cell_stroke, left_cell_prioritized) = x
|
||||||
.checked_sub(1)
|
.checked_sub(1)
|
||||||
@ -416,8 +416,9 @@ pub fn hline_stroke_at_column(
|
|||||||
// We would then analyze the cell one column after (if at a gutter
|
// We would then analyze the cell one column after (if at a gutter
|
||||||
// column), and/or one row below (if at a gutter row), in order to
|
// column), and/or one row below (if at a gutter row), in order to
|
||||||
// check if it would be merged with a cell before the hline.
|
// check if it would be merged with a cell before the hline.
|
||||||
if let Some(parent) = grid.effective_parent_cell_position(x, y) {
|
if let Some(parent) = grid.effective_parent_cell_position(x, y)
|
||||||
if parent.y < y {
|
&& parent.y < y
|
||||||
|
{
|
||||||
// Get the first 'y' spanned by the possible rowspan in this region.
|
// Get the first 'y' spanned by the possible rowspan in this region.
|
||||||
// The 'parent.y' row and any other spanned rows above 'y' could be
|
// The 'parent.y' row and any other spanned rows above 'y' could be
|
||||||
// missing from this region, which could have lead the check above
|
// missing from this region, which could have lead the check above
|
||||||
@ -438,7 +439,6 @@ pub fn hline_stroke_at_column(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// When the hline is at the top of the region and this isn't the first
|
// When the hline is at the top of the region and this isn't the first
|
||||||
// region, fold with the top stroke of the topmost cell at this column,
|
// region, fold with the top stroke of the topmost cell at this column,
|
||||||
|
@ -9,13 +9,13 @@ use typst_library::diag::SourceResult;
|
|||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell};
|
use typst_library::layout::grid::resolve::{Cell, grid_to_cellgrid, table_to_cellgrid};
|
||||||
use typst_library::layout::{Fragment, GridElem, Regions};
|
use typst_library::layout::{Fragment, GridElem, Regions};
|
||||||
use typst_library::model::TableElem;
|
use typst_library::model::TableElem;
|
||||||
|
|
||||||
use self::layouter::RowPiece;
|
use self::layouter::RowPiece;
|
||||||
use self::lines::{
|
use self::lines::{
|
||||||
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment,
|
LineSegment, generate_line_segments, hline_stroke_at_column, vline_stroke_at_row,
|
||||||
};
|
};
|
||||||
use self::rowspans::{Rowspan, UnbreakableRowGroup};
|
use self::rowspans::{Rowspan, UnbreakableRowGroup};
|
||||||
|
|
||||||
|
@ -240,8 +240,10 @@ impl<'a> GridLayouter<'a> {
|
|||||||
self.current.initial_after_repeats = self.regions.size.y;
|
self.current.initial_after_repeats = self.regions.size.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(footer) = &self.grid.footer {
|
if let Some(footer) = &self.grid.footer
|
||||||
if footer.repeated && skipped_region {
|
&& footer.repeated
|
||||||
|
&& skipped_region
|
||||||
|
{
|
||||||
// Simulate the footer again; the region's 'full' might have
|
// Simulate the footer again; the region's 'full' might have
|
||||||
// changed.
|
// changed.
|
||||||
self.regions.size.y += self.current.footer_height;
|
self.regions.size.y += self.current.footer_height;
|
||||||
@ -250,7 +252,6 @@ impl<'a> GridLayouter<'a> {
|
|||||||
.height;
|
.height;
|
||||||
self.regions.size.y -= self.current.footer_height;
|
self.regions.size.y -= self.current.footer_height;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let repeating_header_rows =
|
let repeating_header_rows =
|
||||||
total_header_row_count(self.repeating_headers.iter().copied());
|
total_header_row_count(self.repeating_headers.iter().copied());
|
||||||
|
@ -4,8 +4,8 @@ use typst_library::foundations::Resolve;
|
|||||||
use typst_library::layout::grid::resolve::Repeatable;
|
use typst_library::layout::grid::resolve::Repeatable;
|
||||||
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
||||||
|
|
||||||
use super::layouter::{points, Row};
|
use super::layouter::{Row, points};
|
||||||
use super::{layout_cell, Cell, GridLayouter};
|
use super::{Cell, GridLayouter, layout_cell};
|
||||||
|
|
||||||
/// All information needed to layout a single rowspan.
|
/// All information needed to layout a single rowspan.
|
||||||
pub struct Rowspan {
|
pub struct Rowspan {
|
||||||
@ -238,8 +238,10 @@ impl GridLayouter<'_> {
|
|||||||
// current row is dynamic and depends on the amount of upcoming
|
// current row is dynamic and depends on the amount of upcoming
|
||||||
// unbreakable cells (with or without a rowspan setting).
|
// unbreakable cells (with or without a rowspan setting).
|
||||||
let mut amount_unbreakable_rows = None;
|
let mut amount_unbreakable_rows = None;
|
||||||
if let Some(footer) = &self.grid.footer {
|
if let Some(footer) = &self.grid.footer
|
||||||
if !footer.repeated && current_row >= footer.start {
|
&& !footer.repeated
|
||||||
|
&& current_row >= footer.start
|
||||||
|
{
|
||||||
// Non-repeated footer, so keep it unbreakable.
|
// Non-repeated footer, so keep it unbreakable.
|
||||||
//
|
//
|
||||||
// TODO(subfooters): This will become unnecessary
|
// TODO(subfooters): This will become unnecessary
|
||||||
@ -247,7 +249,6 @@ impl GridLayouter<'_> {
|
|||||||
// have widow prevention.
|
// have widow prevention.
|
||||||
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
|
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let row_group = self.simulate_unbreakable_row_group(
|
let row_group = self.simulate_unbreakable_row_group(
|
||||||
current_row,
|
current_row,
|
||||||
@ -1268,9 +1269,9 @@ fn subtract_end_sizes(sizes: &mut Vec<Abs>, mut subtract: Abs) {
|
|||||||
while subtract > Abs::zero() && sizes.last().is_some_and(|&size| size <= subtract) {
|
while subtract > Abs::zero() && sizes.last().is_some_and(|&size| size <= subtract) {
|
||||||
subtract -= sizes.pop().unwrap();
|
subtract -= sizes.pop().unwrap();
|
||||||
}
|
}
|
||||||
if subtract > Abs::zero() {
|
if subtract > Abs::zero()
|
||||||
if let Some(last_size) = sizes.last_mut() {
|
&& let Some(last_size) = sizes.last_mut()
|
||||||
|
{
|
||||||
*last_size -= subtract;
|
*last_size -= subtract;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,14 @@ use typst_library::layout::{
|
|||||||
};
|
};
|
||||||
use typst_library::routines::Pair;
|
use typst_library::routines::Pair;
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem,
|
||||||
SpaceElem, TextElem,
|
is_default_ignorable,
|
||||||
};
|
};
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
|
use crate::modifiers::{FrameModifiers, FrameModify, layout_and_modify};
|
||||||
|
|
||||||
// The characters by which spacing, inline content and pins are replaced in the
|
// The characters by which spacing, inline content and pins are replaced in the
|
||||||
// full text.
|
// full text.
|
||||||
@ -274,12 +274,12 @@ impl<'a> Collector<'a> {
|
|||||||
let segment_len = self.full.len() - prev;
|
let segment_len = self.full.len() - prev;
|
||||||
|
|
||||||
// Merge adjacent text segments with the same styles.
|
// Merge adjacent text segments with the same styles.
|
||||||
if let Some(Segment::Text(last_len, last_styles)) = self.segments.last_mut() {
|
if let Some(Segment::Text(last_len, last_styles)) = self.segments.last_mut()
|
||||||
if *last_styles == styles {
|
&& *last_styles == styles
|
||||||
|
{
|
||||||
*last_len += segment_len;
|
*last_len += segment_len;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.segments.push(Segment::Text(segment_len, styles));
|
self.segments.push(Segment::Text(segment_len, styles));
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use typst_library::foundations::Resolve;
|
|||||||
use typst_library::introspection::{SplitLocator, Tag};
|
use typst_library::introspection::{SplitLocator, Tag};
|
||||||
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
||||||
use typst_library::model::ParLineMarker;
|
use typst_library::model::ParLineMarker;
|
||||||
use typst_library::text::{variant, Lang, TextElem};
|
use typst_library::text::{Lang, TextElem, variant};
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -155,18 +155,18 @@ pub fn line<'a>(
|
|||||||
let mut items = collect_items(engine, p, range, trim);
|
let mut items = collect_items(engine, p, range, trim);
|
||||||
|
|
||||||
// Add a hyphen at the line start, if a previous dash should be repeated.
|
// Add a hyphen at the line start, if a previous dash should be repeated.
|
||||||
if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) {
|
if pred.is_some_and(|pred| should_repeat_hyphen(pred, full))
|
||||||
if let Some(shaped) = items.first_text_mut() {
|
&& let Some(shaped) = items.first_text_mut()
|
||||||
|
{
|
||||||
shaped.prepend_hyphen(engine, p.config.fallback);
|
shaped.prepend_hyphen(engine, p.config.fallback);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add a hyphen at the line end, if we ended on a soft hyphen.
|
// Add a hyphen at the line end, if we ended on a soft hyphen.
|
||||||
if dash == Some(Dash::Soft) {
|
if dash == Some(Dash::Soft)
|
||||||
if let Some(shaped) = items.last_text_mut() {
|
&& let Some(shaped) = items.last_text_mut()
|
||||||
|
{
|
||||||
shaped.push_hyphen(engine, p.config.fallback);
|
shaped.push_hyphen(engine, p.config.fallback);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Deal with CJ characters at line boundaries.
|
// Deal with CJ characters at line boundaries.
|
||||||
adjust_cj_at_line_boundaries(p, full, &mut items);
|
adjust_cj_at_line_boundaries(p, full, &mut items);
|
||||||
@ -218,11 +218,11 @@ fn collect_items<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add fallback text to expand the line height, if necessary.
|
// Add fallback text to expand the line height, if necessary.
|
||||||
if !items.iter().any(|item| matches!(item, Item::Text(_))) {
|
if !items.iter().any(|item| matches!(item, Item::Text(_)))
|
||||||
if let Some(fallback) = fallback {
|
&& let Some(fallback) = fallback
|
||||||
|
{
|
||||||
items.push(fallback, usize::MAX);
|
items.push(fallback, usize::MAX);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
@ -461,9 +461,9 @@ pub fn commit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle hanging punctuation to the left.
|
// Handle hanging punctuation to the left.
|
||||||
if let Some(Item::Text(text)) = line.items.first() {
|
if let Some(Item::Text(text)) = line.items.first()
|
||||||
if let Some(glyph) = text.glyphs.first() {
|
&& let Some(glyph) = text.glyphs.first()
|
||||||
if !text.dir.is_positive()
|
&& !text.dir.is_positive()
|
||||||
&& text.styles.get(TextElem::overhang)
|
&& text.styles.get(TextElem::overhang)
|
||||||
&& (line.items.len() > 1 || text.glyphs.len() > 1)
|
&& (line.items.len() > 1 || text.glyphs.len() > 1)
|
||||||
{
|
{
|
||||||
@ -471,21 +471,17 @@ pub fn commit(
|
|||||||
offset -= amount;
|
offset -= amount;
|
||||||
remaining += amount;
|
remaining += amount;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle hanging punctuation to the right.
|
// Handle hanging punctuation to the right.
|
||||||
if let Some(Item::Text(text)) = line.items.last() {
|
if let Some(Item::Text(text)) = line.items.last()
|
||||||
if let Some(glyph) = text.glyphs.last() {
|
&& let Some(glyph) = text.glyphs.last()
|
||||||
if text.dir.is_positive()
|
&& text.dir.is_positive()
|
||||||
&& text.styles.get(TextElem::overhang)
|
&& text.styles.get(TextElem::overhang)
|
||||||
&& (line.items.len() > 1 || text.glyphs.len() > 1)
|
&& (line.items.len() > 1 || text.glyphs.len() > 1)
|
||||||
{
|
{
|
||||||
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size);
|
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size);
|
||||||
remaining += amount;
|
remaining += amount;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine how much additional space is needed. The justification_ratio is
|
// Determine how much additional space is needed. The justification_ratio is
|
||||||
// for the first step justification, extra_justification is for the last
|
// for the first step justification, extra_justification is for the last
|
||||||
|
@ -2,8 +2,8 @@ use std::ops::{Add, Sub};
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use az::SaturatingAs;
|
use az::SaturatingAs;
|
||||||
use icu_properties::maps::{CodePointMapData, CodePointMapDataBorrowed};
|
|
||||||
use icu_properties::LineBreak;
|
use icu_properties::LineBreak;
|
||||||
|
use icu_properties::maps::{CodePointMapData, CodePointMapDataBorrowed};
|
||||||
use icu_provider::AsDeserializingBufferProvider;
|
use icu_provider::AsDeserializingBufferProvider;
|
||||||
use icu_provider_adapters::fork::ForkByKeyProvider;
|
use icu_provider_adapters::fork::ForkByKeyProvider;
|
||||||
use icu_provider_blob::BlobDataProvider;
|
use icu_provider_blob::BlobDataProvider;
|
||||||
@ -11,7 +11,7 @@ use icu_segmenter::LineSegmenter;
|
|||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::layout::{Abs, Em};
|
use typst_library::layout::{Abs, Em};
|
||||||
use typst_library::model::Linebreaks;
|
use typst_library::model::Linebreaks;
|
||||||
use typst_library::text::{is_default_ignorable, Lang, TextElem};
|
use typst_library::text::{Lang, TextElem, is_default_ignorable};
|
||||||
use typst_syntax::link_prefix;
|
use typst_syntax::link_prefix;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
@ -136,13 +136,13 @@ fn linebreak_simple<'a>(
|
|||||||
// If the line doesn't fit anymore, we push the last fitting attempt
|
// If the line doesn't fit anymore, we push the last fitting attempt
|
||||||
// into the stack and rebuild the line from the attempt's end. The
|
// into the stack and rebuild the line from the attempt's end. The
|
||||||
// resulting line cannot be broken up further.
|
// resulting line cannot be broken up further.
|
||||||
if !width.fits(attempt.width) {
|
if !width.fits(attempt.width)
|
||||||
if let Some((last_attempt, last_end)) = last.take() {
|
&& let Some((last_attempt, last_end)) = last.take()
|
||||||
|
{
|
||||||
lines.push(last_attempt);
|
lines.push(last_attempt);
|
||||||
start = last_end;
|
start = last_end;
|
||||||
attempt = line(engine, p, start..end, breakpoint, lines.last());
|
attempt = line(engine, p, start..end, breakpoint, lines.last());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Finish the current line if there is a mandatory line break (i.e. due
|
// Finish the current line if there is a mandatory line break (i.e. due
|
||||||
// to "\n") or if the line doesn't fit horizontally already since then
|
// to "\n") or if the line doesn't fit horizontally already since then
|
||||||
@ -894,11 +894,7 @@ impl CostMetrics {
|
|||||||
/// we allow less because otherwise we get an invalid layout fairly often,
|
/// we allow less because otherwise we get an invalid layout fairly often,
|
||||||
/// which makes our bound useless.
|
/// which makes our bound useless.
|
||||||
fn min_ratio(&self, approx: bool) -> f64 {
|
fn min_ratio(&self, approx: bool) -> f64 {
|
||||||
if approx {
|
if approx { self.min_approx_ratio } else { self.min_ratio }
|
||||||
self.min_approx_ratio
|
|
||||||
} else {
|
|
||||||
self.min_ratio
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ pub use self::box_::layout_box;
|
|||||||
pub use self::shaping::create_shape_plan;
|
pub use self::shaping::create_shape_plan;
|
||||||
|
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||||
@ -23,18 +24,17 @@ use typst_library::model::{
|
|||||||
};
|
};
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
use typst_library::text::{Costs, Lang, TextElem};
|
use typst_library::text::{Costs, Lang, TextElem};
|
||||||
use typst_library::World;
|
|
||||||
use typst_utils::{Numeric, SliceExt};
|
use typst_utils::{Numeric, SliceExt};
|
||||||
|
|
||||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
use self::collect::{Item, Segment, SpanMapper, collect};
|
||||||
use self::deco::decorate;
|
use self::deco::decorate;
|
||||||
use self::finalize::finalize;
|
use self::finalize::finalize;
|
||||||
use self::line::{apply_shift, commit, line, Line};
|
use self::line::{Line, apply_shift, commit, line};
|
||||||
use self::linebreak::{linebreak, Breakpoint};
|
use self::linebreak::{Breakpoint, linebreak};
|
||||||
use self::prepare::{prepare, Preparation};
|
use self::prepare::{Preparation, prepare};
|
||||||
use self::shaping::{
|
use self::shaping::{
|
||||||
cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText,
|
BEGIN_PUNCT_PAT, END_PUNCT_PAT, ShapedGlyph, ShapedText, cjk_punct_style,
|
||||||
BEGIN_PUNCT_PAT, END_PUNCT_PAT,
|
is_of_cj_script, shape_range,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Range of a substring of text.
|
/// Range of a substring of text.
|
||||||
@ -190,11 +190,7 @@ fn configuration(
|
|||||||
Config {
|
Config {
|
||||||
justify,
|
justify,
|
||||||
linebreaks: base.linebreaks.unwrap_or_else(|| {
|
linebreaks: base.linebreaks.unwrap_or_else(|| {
|
||||||
if justify {
|
if justify { Linebreaks::Optimized } else { Linebreaks::Simple }
|
||||||
Linebreaks::Optimized
|
|
||||||
} else {
|
|
||||||
Linebreaks::Simple
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
first_line_indent: {
|
first_line_indent: {
|
||||||
let FirstLineIndent { amount, all } = base.first_line_indent;
|
let FirstLineIndent { amount, all } = base.first_line_indent;
|
||||||
|
@ -4,21 +4,21 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use az::SaturatingAs;
|
use az::SaturatingAs;
|
||||||
use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer};
|
use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer};
|
||||||
use ttf_parser::gsub::SubstitutionSubtable;
|
|
||||||
use ttf_parser::Tag;
|
use ttf_parser::Tag;
|
||||||
|
use ttf_parser::gsub::SubstitutionSubtable;
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Smart, StyleChain};
|
use typst_library::foundations::{Smart, StyleChain};
|
||||||
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
|
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
families, features, is_default_ignorable, language, variant, Font, FontFamily,
|
Font, FontFamily, FontVariant, Glyph, Lang, Region, ShiftSettings, TextEdgeBounds,
|
||||||
FontVariant, Glyph, Lang, Region, ShiftSettings, TextEdgeBounds, TextElem, TextItem,
|
TextElem, TextItem, families, features, is_default_ignorable, language, variant,
|
||||||
};
|
};
|
||||||
use typst_library::World;
|
|
||||||
use typst_utils::SliceExt;
|
use typst_utils::SliceExt;
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
use unicode_script::{Script, UnicodeScript};
|
use unicode_script::{Script, UnicodeScript};
|
||||||
|
|
||||||
use super::{decorate, Item, Range, SpanMapper};
|
use super::{Item, Range, SpanMapper, decorate};
|
||||||
use crate::modifiers::FrameModifyText;
|
use crate::modifiers::FrameModifyText;
|
||||||
|
|
||||||
/// The result of shaping text.
|
/// The result of shaping text.
|
||||||
@ -539,11 +539,7 @@ impl<'a> ShapedText<'a> {
|
|||||||
// Find any glyph with the text index.
|
// Find any glyph with the text index.
|
||||||
let found = self.glyphs.binary_search_by(|g: &ShapedGlyph| {
|
let found = self.glyphs.binary_search_by(|g: &ShapedGlyph| {
|
||||||
let ordering = g.range.start.cmp(&text_index);
|
let ordering = g.range.start.cmp(&text_index);
|
||||||
if ltr {
|
if ltr { ordering } else { ordering.reverse() }
|
||||||
ordering
|
|
||||||
} else {
|
|
||||||
ordering.reverse()
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut idx = match found {
|
let mut idx = match found {
|
||||||
@ -719,6 +715,10 @@ fn glyphs_width(glyphs: &[ShapedGlyph]) -> Abs {
|
|||||||
struct ShapingContext<'a, 'v> {
|
struct ShapingContext<'a, 'v> {
|
||||||
engine: &'a Engine<'v>,
|
engine: &'a Engine<'v>,
|
||||||
glyphs: Vec<ShapedGlyph>,
|
glyphs: Vec<ShapedGlyph>,
|
||||||
|
/// Font families that have been used with unlimited coverage.
|
||||||
|
///
|
||||||
|
/// These font families are considered exhausted and will not be used again,
|
||||||
|
/// even if they are declared again (e.g., during fallback after normal selection).
|
||||||
used: Vec<Font>,
|
used: Vec<Font>,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
size: Abs,
|
size: Abs,
|
||||||
@ -777,7 +777,10 @@ fn shape_segment<'a>(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This font has been exhausted and will not be used again.
|
||||||
|
if covers.is_none() {
|
||||||
ctx.used.push(font.clone());
|
ctx.used.push(font.clone());
|
||||||
|
}
|
||||||
|
|
||||||
// Fill the buffer with our text.
|
// Fill the buffer with our text.
|
||||||
let mut buffer = UnicodeBuffer::new();
|
let mut buffer = UnicodeBuffer::new();
|
||||||
|
@ -24,11 +24,7 @@ pub fn layout_list(
|
|||||||
let body_indent = elem.body_indent.get(styles);
|
let body_indent = elem.body_indent.get(styles);
|
||||||
let tight = elem.tight.get(styles);
|
let tight = elem.tight.get(styles);
|
||||||
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
|
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
|
||||||
if tight {
|
if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
|
||||||
styles.get(ParElem::leading)
|
|
||||||
} else {
|
|
||||||
styles.get(ParElem::spacing)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let Depth(depth) = styles.get(ListElem::depth);
|
let Depth(depth) = styles.get(ListElem::depth);
|
||||||
@ -88,22 +84,15 @@ pub fn layout_enum(
|
|||||||
let body_indent = elem.body_indent.get(styles);
|
let body_indent = elem.body_indent.get(styles);
|
||||||
let tight = elem.tight.get(styles);
|
let tight = elem.tight.get(styles);
|
||||||
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
|
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
|
||||||
if tight {
|
if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
|
||||||
styles.get(ParElem::leading)
|
|
||||||
} else {
|
|
||||||
styles.get(ParElem::spacing)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
let mut locator = locator.split();
|
let mut locator = locator.split();
|
||||||
let mut number = elem.start.get(styles).unwrap_or_else(|| {
|
let mut number = elem
|
||||||
if reversed {
|
.start
|
||||||
elem.children.len() as u64
|
.get(styles)
|
||||||
} else {
|
.unwrap_or_else(|| if reversed { elem.children.len() as u64 } else { 1 });
|
||||||
1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut parents = styles.get_cloned(EnumElem::parents);
|
let mut parents = styles.get_cloned(EnumElem::parents);
|
||||||
|
|
||||||
let full = elem.full.get(styles);
|
let full = elem.full.get(styles);
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{Em, Frame, Point, Size};
|
use typst_library::layout::{Em, Frame, Point, Size};
|
||||||
use typst_library::math::AccentElem;
|
use typst_library::math::AccentElem;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext,
|
FrameFragment, MathContext, MathFragment, style_cramped, style_dtls, style_flac,
|
||||||
MathFragment,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How much the accent can be shorter than the base.
|
/// How much the accent can be shorter than the base.
|
||||||
@ -27,14 +26,17 @@ pub fn layout_accent(
|
|||||||
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
|
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
|
||||||
|
|
||||||
let cramped = style_cramped();
|
let cramped = style_cramped();
|
||||||
let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
|
let base_styles = base_styles.chain(&cramped);
|
||||||
|
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
|
||||||
|
|
||||||
|
let (font, size) = base.font(ctx, base_styles, elem.base.span())?;
|
||||||
|
|
||||||
// Preserve class to preserve automatic spacing.
|
// Preserve class to preserve automatic spacing.
|
||||||
let base_class = base.class();
|
let base_class = base.class();
|
||||||
let base_attach = base.accent_attach();
|
let base_attach = base.accent_attach();
|
||||||
|
|
||||||
// Try to replace the accent glyph with its flattened variant.
|
// Try to replace the accent glyph with its flattened variant.
|
||||||
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
let flattened_base_height = value!(font, flattened_accent_base_height).at(size);
|
||||||
let flac = style_flac();
|
let flac = style_flac();
|
||||||
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
|
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
|
||||||
styles.chain(&flac)
|
styles.chain(&flac)
|
||||||
@ -42,23 +44,25 @@ pub fn layout_accent(
|
|||||||
styles
|
styles
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut glyph =
|
let mut accent = ctx.layout_into_fragment(
|
||||||
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;
|
&SymbolElem::packed(accent.0).spanned(elem.span()),
|
||||||
|
accent_styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Forcing the accent to be at least as large as the base makes it too wide
|
// Forcing the accent to be at least as large as the base makes it too wide
|
||||||
// in many cases.
|
// in many cases.
|
||||||
let width = elem.size.resolve(styles).relative_to(base.width());
|
let width = elem.size.resolve(styles).relative_to(base.width());
|
||||||
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
|
let short_fall = ACCENT_SHORT_FALL.at(size);
|
||||||
glyph.stretch_horizontal(ctx, width - short_fall);
|
accent.stretch_horizontal(ctx, width - short_fall);
|
||||||
let accent_attach = glyph.accent_attach.0;
|
let accent_attach = accent.accent_attach().0;
|
||||||
let accent = glyph.into_frame();
|
let accent = accent.into_frame();
|
||||||
|
|
||||||
let (gap, accent_pos, base_pos) = if top_accent {
|
let (gap, accent_pos, base_pos) = if top_accent {
|
||||||
// Descent is negative because the accent's ink bottom is above the
|
// Descent is negative because the accent's ink bottom is above the
|
||||||
// baseline. Therefore, the default gap is the accent's negated descent
|
// baseline. Therefore, the default gap is the accent's negated descent
|
||||||
// minus the accent base height. Only if the base is very small, we
|
// minus the accent base height. Only if the base is very small, we
|
||||||
// need a larger gap so that the accent doesn't move too low.
|
// need a larger gap so that the accent doesn't move too low.
|
||||||
let accent_base_height = scaled!(ctx, styles, accent_base_height);
|
let accent_base_height = value!(font, accent_base_height).at(size);
|
||||||
let gap = -accent.descent() - base.ascent().min(accent_base_height);
|
let gap = -accent.descent() - base.ascent().min(accent_base_height);
|
||||||
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
|
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
|
||||||
let base_pos = Point::with_y(accent.height() + gap);
|
let base_pos = Point::with_y(accent.height() + gap);
|
||||||
|
@ -4,11 +4,13 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
|
|||||||
use typst_library::math::{
|
use typst_library::math::{
|
||||||
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
||||||
};
|
};
|
||||||
|
use typst_library::text::Font;
|
||||||
|
use typst_syntax::Span;
|
||||||
use typst_utils::OptionExt;
|
use typst_utils::OptionExt;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits,
|
FrameFragment, Limits, MathContext, MathFragment, stretch_fragment,
|
||||||
MathContext, MathFragment,
|
style_for_subscript, style_for_superscript,
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! measure {
|
macro_rules! measure {
|
||||||
@ -83,7 +85,7 @@ pub fn layout_attach(
|
|||||||
layout!(br, sub_style_chain)?,
|
layout!(br, sub_style_chain)?,
|
||||||
];
|
];
|
||||||
|
|
||||||
layout_attachments(ctx, styles, base, fragments)
|
layout_attachments(ctx, styles, base, elem.base.span(), fragments)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays out a [`PrimeElem`].
|
/// Lays out a [`PrimeElem`].
|
||||||
@ -102,13 +104,19 @@ pub fn layout_primes(
|
|||||||
4 => '⁗',
|
4 => '⁗',
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
|
let f = ctx.layout_into_fragment(
|
||||||
|
&SymbolElem::packed(c).spanned(elem.span()),
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
ctx.push(f);
|
ctx.push(f);
|
||||||
}
|
}
|
||||||
count => {
|
count => {
|
||||||
// Custom amount of primes
|
// Custom amount of primes
|
||||||
let prime = ctx
|
let prime = ctx
|
||||||
.layout_into_fragment(&SymbolElem::packed('′'), styles)?
|
.layout_into_fragment(
|
||||||
|
&SymbolElem::packed('′').spanned(elem.span()),
|
||||||
|
styles,
|
||||||
|
)?
|
||||||
.into_frame();
|
.into_frame();
|
||||||
let width = prime.width() * (count + 1) as f64 / 2.0;
|
let width = prime.width() * (count + 1) as f64 / 2.0;
|
||||||
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
||||||
@ -170,22 +178,25 @@ fn layout_attachments(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
base: MathFragment,
|
base: MathFragment,
|
||||||
|
span: Span,
|
||||||
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
|
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let base_class = base.class();
|
let class = base.class();
|
||||||
|
let (font, size) = base.font(ctx, styles, span)?;
|
||||||
|
let cramped = styles.get(EquationElem::cramped);
|
||||||
|
|
||||||
// Calculate the distance from the base's baseline to the superscripts' and
|
// Calculate the distance from the base's baseline to the superscripts' and
|
||||||
// subscripts' baseline.
|
// subscripts' baseline.
|
||||||
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
|
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
|
||||||
(Abs::zero(), Abs::zero())
|
(Abs::zero(), Abs::zero())
|
||||||
} else {
|
} else {
|
||||||
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
|
compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate the distance from the base's baseline to the top attachment's
|
// Calculate the distance from the base's baseline to the top attachment's
|
||||||
// and bottom attachment's baseline.
|
// and bottom attachment's baseline.
|
||||||
let (t_shift, b_shift) =
|
let (t_shift, b_shift) =
|
||||||
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
|
compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
|
||||||
|
|
||||||
// Calculate the final frame height.
|
// Calculate the final frame height.
|
||||||
let ascent = base
|
let ascent = base
|
||||||
@ -215,7 +226,7 @@ fn layout_attachments(
|
|||||||
// `space_after_script` is extra spacing that is at the start before each
|
// `space_after_script` is extra spacing that is at the start before each
|
||||||
// pre-script, and at the end after each post-script (see the MathConstants
|
// pre-script, and at the end after each post-script (see the MathConstants
|
||||||
// table in the OpenType MATH spec).
|
// table in the OpenType MATH spec).
|
||||||
let space_after_script = scaled!(ctx, styles, space_after_script);
|
let space_after_script = value!(font, space_after_script).at(size);
|
||||||
|
|
||||||
// Calculate the distance each pre-script extends to the left of the base's
|
// Calculate the distance each pre-script extends to the left of the base's
|
||||||
// width.
|
// width.
|
||||||
@ -272,7 +283,7 @@ fn layout_attachments(
|
|||||||
layout!(b, b_x, b_y); // lower-limit
|
layout!(b, b_x, b_y); // lower-limit
|
||||||
|
|
||||||
// Done! Note that we retain the class of the base.
|
// Done! Note that we retain the class of the base.
|
||||||
ctx.push(FrameFragment::new(styles, frame).with_class(base_class));
|
ctx.push(FrameFragment::new(styles, frame).with_class(class));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -364,8 +375,8 @@ fn compute_limit_widths(
|
|||||||
/// Returns two lengths, the first being the distance to the upper-limit's
|
/// Returns two lengths, the first being the distance to the upper-limit's
|
||||||
/// baseline and the second being the distance to the lower-limit's baseline.
|
/// baseline and the second being the distance to the lower-limit's baseline.
|
||||||
fn compute_limit_shifts(
|
fn compute_limit_shifts(
|
||||||
ctx: &MathContext,
|
font: &Font,
|
||||||
styles: StyleChain,
|
font_size: Abs,
|
||||||
base: &MathFragment,
|
base: &MathFragment,
|
||||||
[t, b]: [Option<&MathFragment>; 2],
|
[t, b]: [Option<&MathFragment>; 2],
|
||||||
) -> (Abs, Abs) {
|
) -> (Abs, Abs) {
|
||||||
@ -373,16 +384,15 @@ fn compute_limit_shifts(
|
|||||||
// ascender of the limits respectively, whereas `upper_rise_min` and
|
// ascender of the limits respectively, whereas `upper_rise_min` and
|
||||||
// `lower_drop_min` give gaps to each limit's baseline (see the
|
// `lower_drop_min` give gaps to each limit's baseline (see the
|
||||||
// MathConstants table in the OpenType MATH spec).
|
// MathConstants table in the OpenType MATH spec).
|
||||||
|
|
||||||
let t_shift = t.map_or_default(|t| {
|
let t_shift = t.map_or_default(|t| {
|
||||||
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
|
let upper_gap_min = value!(font, upper_limit_gap_min).at(font_size);
|
||||||
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
|
let upper_rise_min = value!(font, upper_limit_baseline_rise_min).at(font_size);
|
||||||
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
|
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
|
||||||
});
|
});
|
||||||
|
|
||||||
let b_shift = b.map_or_default(|b| {
|
let b_shift = b.map_or_default(|b| {
|
||||||
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
|
let lower_gap_min = value!(font, lower_limit_gap_min).at(font_size);
|
||||||
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
|
let lower_drop_min = value!(font, lower_limit_baseline_drop_min).at(font_size);
|
||||||
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
|
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -393,25 +403,27 @@ fn compute_limit_shifts(
|
|||||||
/// Returns two lengths, the first being the distance to the superscripts'
|
/// Returns two lengths, the first being the distance to the superscripts'
|
||||||
/// baseline and the second being the distance to the subscripts' baseline.
|
/// baseline and the second being the distance to the subscripts' baseline.
|
||||||
fn compute_script_shifts(
|
fn compute_script_shifts(
|
||||||
ctx: &MathContext,
|
font: &Font,
|
||||||
styles: StyleChain,
|
font_size: Abs,
|
||||||
|
cramped: bool,
|
||||||
base: &MathFragment,
|
base: &MathFragment,
|
||||||
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
|
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
|
||||||
) -> (Abs, Abs) {
|
) -> (Abs, Abs) {
|
||||||
let sup_shift_up = if styles.get(EquationElem::cramped) {
|
let sup_shift_up = (if cramped {
|
||||||
scaled!(ctx, styles, superscript_shift_up_cramped)
|
value!(font, superscript_shift_up_cramped)
|
||||||
} else {
|
} else {
|
||||||
scaled!(ctx, styles, superscript_shift_up)
|
value!(font, superscript_shift_up)
|
||||||
};
|
})
|
||||||
|
.at(font_size);
|
||||||
|
|
||||||
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min);
|
let sup_bottom_min = value!(font, superscript_bottom_min).at(font_size);
|
||||||
let sup_bottom_max_with_sub =
|
let sup_bottom_max_with_sub =
|
||||||
scaled!(ctx, styles, superscript_bottom_max_with_subscript);
|
value!(font, superscript_bottom_max_with_subscript).at(font_size);
|
||||||
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max);
|
let sup_drop_max = value!(font, superscript_baseline_drop_max).at(font_size);
|
||||||
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min);
|
let gap_min = value!(font, sub_superscript_gap_min).at(font_size);
|
||||||
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down);
|
let sub_shift_down = value!(font, subscript_shift_down).at(font_size);
|
||||||
let sub_top_max = scaled!(ctx, styles, subscript_top_max);
|
let sub_top_max = value!(font, subscript_top_max).at(font_size);
|
||||||
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min);
|
let sub_drop_min = value!(font, subscript_baseline_drop_min).at(font_size);
|
||||||
|
|
||||||
let mut shift_up = Abs::zero();
|
let mut shift_up = Abs::zero();
|
||||||
let mut shift_down = Abs::zero();
|
let mut shift_down = Abs::zero();
|
||||||
|
@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
|
|||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment,
|
DELIM_SHORT_FALL, FrameFragment, MathContext, find_math_font, style_for_denominator,
|
||||||
MathContext, DELIM_SHORT_FALL,
|
style_for_numerator,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FRAC_AROUND: Em = Em::new(0.1);
|
const FRAC_AROUND: Em = Em::new(0.1);
|
||||||
@ -49,29 +49,33 @@ fn layout_frac_like(
|
|||||||
binom: bool,
|
binom: bool,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
let font = find_math_font(ctx.engine.world, styles, span)?;
|
||||||
let axis = scaled!(ctx, styles, axis_height);
|
let axis = value!(font, axis_height).resolve(styles);
|
||||||
let thickness = scaled!(ctx, styles, fraction_rule_thickness);
|
let thickness = value!(font, fraction_rule_thickness).resolve(styles);
|
||||||
let shift_up = scaled!(
|
let shift_up = value!(
|
||||||
ctx, styles,
|
font, styles,
|
||||||
text: fraction_numerator_shift_up,
|
text: fraction_numerator_shift_up,
|
||||||
display: fraction_numerator_display_style_shift_up,
|
display: fraction_numerator_display_style_shift_up,
|
||||||
);
|
)
|
||||||
let shift_down = scaled!(
|
.resolve(styles);
|
||||||
ctx, styles,
|
let shift_down = value!(
|
||||||
|
font, styles,
|
||||||
text: fraction_denominator_shift_down,
|
text: fraction_denominator_shift_down,
|
||||||
display: fraction_denominator_display_style_shift_down,
|
display: fraction_denominator_display_style_shift_down,
|
||||||
);
|
)
|
||||||
let num_min = scaled!(
|
.resolve(styles);
|
||||||
ctx, styles,
|
let num_min = value!(
|
||||||
|
font, styles,
|
||||||
text: fraction_numerator_gap_min,
|
text: fraction_numerator_gap_min,
|
||||||
display: fraction_num_display_style_gap_min,
|
display: fraction_num_display_style_gap_min,
|
||||||
);
|
)
|
||||||
let denom_min = scaled!(
|
.resolve(styles);
|
||||||
ctx, styles,
|
let denom_min = value!(
|
||||||
|
font, styles,
|
||||||
text: fraction_denominator_gap_min,
|
text: fraction_denominator_gap_min,
|
||||||
display: fraction_denom_display_style_gap_min,
|
display: fraction_denom_display_style_gap_min,
|
||||||
);
|
)
|
||||||
|
.resolve(styles);
|
||||||
|
|
||||||
let num_style = style_for_numerator(styles);
|
let num_style = style_for_numerator(styles);
|
||||||
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
|
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
|
||||||
@ -82,7 +86,7 @@ fn layout_frac_like(
|
|||||||
// Add a comma between each element.
|
// Add a comma between each element.
|
||||||
denom
|
denom
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
|
.flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()])
|
||||||
.skip(1),
|
.skip(1),
|
||||||
),
|
),
|
||||||
styles.chain(&denom_style),
|
styles.chain(&denom_style),
|
||||||
@ -109,12 +113,18 @@ fn layout_frac_like(
|
|||||||
frame.push_frame(denom_pos, denom);
|
frame.push_frame(denom_pos, denom);
|
||||||
|
|
||||||
if binom {
|
if binom {
|
||||||
let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?;
|
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||||
|
|
||||||
|
let mut left =
|
||||||
|
ctx.layout_into_fragment(&SymbolElem::packed('(').spanned(span), styles)?;
|
||||||
left.stretch_vertical(ctx, height - short_fall);
|
left.stretch_vertical(ctx, height - short_fall);
|
||||||
left.center_on_axis();
|
left.center_on_axis();
|
||||||
ctx.push(left);
|
ctx.push(left);
|
||||||
|
|
||||||
ctx.push(FrameFragment::new(styles, frame));
|
ctx.push(FrameFragment::new(styles, frame));
|
||||||
let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?;
|
|
||||||
|
let mut right =
|
||||||
|
ctx.layout_into_fragment(&SymbolElem::packed(')').spanned(span), styles)?;
|
||||||
right.stretch_vertical(ctx, height - short_fall);
|
right.stretch_vertical(ctx, height - short_fall);
|
||||||
right.center_on_axis();
|
right.center_on_axis();
|
||||||
ctx.push(right);
|
ctx.push(right);
|
||||||
|
@ -2,21 +2,25 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
|
|
||||||
use az::SaturatingAs;
|
use az::SaturatingAs;
|
||||||
use rustybuzz::{BufferFlags, UnicodeBuffer};
|
use rustybuzz::{BufferFlags, UnicodeBuffer};
|
||||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
|
||||||
use ttf_parser::GlyphId;
|
use ttf_parser::GlyphId;
|
||||||
use typst_library::diag::{bail, warning, SourceResult};
|
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_library::diag::{SourceResult, bail, warning};
|
||||||
use typst_library::foundations::StyleChain;
|
use typst_library::foundations::StyleChain;
|
||||||
use typst_library::introspection::Tag;
|
use typst_library::introspection::Tag;
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
||||||
};
|
};
|
||||||
use typst_library::math::{EquationElem, MathSize};
|
use typst_library::math::{EquationElem, MathSize};
|
||||||
use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem};
|
use typst_library::text::{
|
||||||
|
Font, Glyph, TextElem, TextItem, families, features, language, variant,
|
||||||
|
};
|
||||||
|
use typst_library::visualize::Paint;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{default_math_class, Get};
|
use typst_utils::{Get, default_math_class};
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use super::MathContext;
|
use super::{MathContext, find_math_font};
|
||||||
use crate::inline::create_shape_plan;
|
use crate::inline::create_shape_plan;
|
||||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
use crate::modifiers::{FrameModifiers, FrameModify};
|
||||||
|
|
||||||
@ -108,6 +112,21 @@ impl MathFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn font(
|
||||||
|
&self,
|
||||||
|
ctx: &MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<(Font, Abs)> {
|
||||||
|
Ok((
|
||||||
|
match self {
|
||||||
|
Self::Glyph(glyph) => glyph.item.font.clone(),
|
||||||
|
_ => find_math_font(ctx.engine.world, styles, span)?,
|
||||||
|
},
|
||||||
|
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn font_size(&self) -> Option<Abs> {
|
pub fn font_size(&self) -> Option<Abs> {
|
||||||
match self {
|
match self {
|
||||||
Self::Glyph(glyph) => Some(glyph.item.size),
|
Self::Glyph(glyph) => Some(glyph.item.size),
|
||||||
@ -192,6 +211,31 @@ impl MathFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fill(&self) -> Option<Paint> {
|
||||||
|
match self {
|
||||||
|
Self::Glyph(glyph) => Some(glyph.item.fill.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
|
||||||
|
if let Self::Glyph(glyph) = self {
|
||||||
|
glyph.stretch_vertical(ctx, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
|
||||||
|
if let Self::Glyph(glyph) = self {
|
||||||
|
glyph.stretch_horizontal(ctx, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn center_on_axis(&mut self) {
|
||||||
|
if let Self::Glyph(glyph) = self {
|
||||||
|
glyph.center_on_axis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// If no kern table is provided for a corner, a kerning amount of zero is
|
/// If no kern table is provided for a corner, a kerning amount of zero is
|
||||||
/// assumed.
|
/// assumed.
|
||||||
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
|
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
|
||||||
@ -261,23 +305,70 @@ pub struct GlyphFragment {
|
|||||||
impl GlyphFragment {
|
impl GlyphFragment {
|
||||||
/// Calls `new` with the given character.
|
/// Calls `new` with the given character.
|
||||||
pub fn new_char(
|
pub fn new_char(
|
||||||
font: &Font,
|
ctx: &MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
c: char,
|
c: char,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> SourceResult<Self> {
|
) -> SourceResult<Option<Self>> {
|
||||||
Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span)
|
Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects a font to use and then shapes text.
|
||||||
|
pub fn new(
|
||||||
|
ctx: &MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
text: &str,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Option<Self>> {
|
||||||
|
let families = families(styles);
|
||||||
|
let variant = variant(styles);
|
||||||
|
let fallback = styles.get(TextElem::fallback);
|
||||||
|
let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len());
|
||||||
|
|
||||||
|
// Find the next available family.
|
||||||
|
let world = ctx.engine.world;
|
||||||
|
let book = world.book();
|
||||||
|
let mut selection = None;
|
||||||
|
for family in families {
|
||||||
|
selection = book
|
||||||
|
.select(family.as_str(), variant)
|
||||||
|
.and_then(|id| world.font(id))
|
||||||
|
.filter(|font| {
|
||||||
|
font.ttf().tables().math.and_then(|math| math.constants).is_some()
|
||||||
|
})
|
||||||
|
.filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end])));
|
||||||
|
if selection.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do font fallback if the families are exhausted and fallback is enabled.
|
||||||
|
if selection.is_none() && fallback {
|
||||||
|
selection = book
|
||||||
|
.select_fallback(None, variant, text)
|
||||||
|
.and_then(|id| world.font(id))
|
||||||
|
.filter(|font| {
|
||||||
|
font.ttf().tables().math.and_then(|math| math.constants).is_some()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error out if no math font could be found at all.
|
||||||
|
let Some(font) = selection else {
|
||||||
|
bail!(span, "current font does not support math");
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::shape(&font, styles, text, span)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to create a new glyph out of the given string. Will bail if the
|
/// Try to create a new glyph out of the given string. Will bail if the
|
||||||
/// result from shaping the string is not a single glyph or is a tofu.
|
/// result from shaping the string is more than a single glyph.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
pub fn new(
|
pub fn shape(
|
||||||
font: &Font,
|
font: &Font,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
text: &str,
|
text: &str,
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> SourceResult<GlyphFragment> {
|
) -> SourceResult<Option<GlyphFragment>> {
|
||||||
let mut buffer = UnicodeBuffer::new();
|
let mut buffer = UnicodeBuffer::new();
|
||||||
buffer.push_str(text);
|
buffer.push_str(text);
|
||||||
buffer.set_language(language(styles));
|
buffer.set_language(language(styles));
|
||||||
@ -300,18 +391,15 @@ impl GlyphFragment {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
||||||
if buffer.len() != 1 {
|
match buffer.len() {
|
||||||
bail!(span, "did not get a single glyph after shaping {}", text);
|
0 => return Ok(None),
|
||||||
|
1 => {}
|
||||||
|
_ => bail!(span, "did not get a single glyph after shaping {}", text),
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = buffer.glyph_infos()[0];
|
let info = buffer.glyph_infos()[0];
|
||||||
let pos = buffer.glyph_positions()[0];
|
let pos = buffer.glyph_positions()[0];
|
||||||
|
|
||||||
// TODO: add support for coverage and fallback, like in normal text shaping.
|
|
||||||
if info.glyph_id == 0 {
|
|
||||||
bail!(span, "current font is missing a glyph for {}", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cluster = info.cluster as usize;
|
let cluster = info.cluster as usize;
|
||||||
let c = text[cluster..].chars().next().unwrap();
|
let c = text[cluster..].chars().next().unwrap();
|
||||||
let limits = Limits::for_char(c);
|
let limits = Limits::for_char(c);
|
||||||
@ -361,7 +449,7 @@ impl GlyphFragment {
|
|||||||
modifiers: FrameModifiers::get_in(styles),
|
modifiers: FrameModifiers::get_in(styles),
|
||||||
};
|
};
|
||||||
fragment.update_glyph();
|
fragment.update_glyph();
|
||||||
Ok(fragment)
|
Ok(Some(fragment))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets element id and boxes in appropriate way without changing other
|
/// Sets element id and boxes in appropriate way without changing other
|
||||||
@ -681,7 +769,11 @@ fn min_connector_overlap(font: &Font) -> Option<Em> {
|
|||||||
.map(|variants| font.to_em(variants.min_connector_overlap))
|
.map(|variants| font.to_em(variants.min_connector_overlap))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option<GlyphConstruction> {
|
fn glyph_construction(
|
||||||
|
font: &Font,
|
||||||
|
id: GlyphId,
|
||||||
|
axis: Axis,
|
||||||
|
) -> Option<GlyphConstruction<'_>> {
|
||||||
font.ttf()
|
font.ttf()
|
||||||
.tables()
|
.tables()
|
||||||
.math?
|
.math?
|
||||||
@ -810,7 +902,10 @@ fn assemble(
|
|||||||
|
|
||||||
/// Return an iterator over the assembly's parts with extenders repeated the
|
/// Return an iterator over the assembly's parts with extenders repeated the
|
||||||
/// specified number of times.
|
/// specified number of times.
|
||||||
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
|
fn parts(
|
||||||
|
assembly: GlyphAssembly<'_>,
|
||||||
|
repeat: usize,
|
||||||
|
) -> impl Iterator<Item = GlyphPart> + '_ {
|
||||||
assembly.parts.into_iter().flat_map(move |part| {
|
assembly.parts.into_iter().flat_map(move |part| {
|
||||||
let count = if part.part_flags.extender() { repeat } else { 1 };
|
let count = if part.part_flags.extender() { repeat } else { 1 };
|
||||||
std::iter::repeat_n(part, count)
|
std::iter::repeat_n(part, count)
|
||||||
|
@ -5,7 +5,7 @@ use typst_library::math::{EquationElem, LrElem, MidElem};
|
|||||||
use typst_utils::SliceExt;
|
use typst_utils::SliceExt;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
|
use super::{DELIM_SHORT_FALL, MathContext, MathFragment, stretch_fragment};
|
||||||
|
|
||||||
/// Lays out an [`LrElem`].
|
/// Lays out an [`LrElem`].
|
||||||
#[typst_macros::time(name = "math.lr", span = elem.span())]
|
#[typst_macros::time(name = "math.lr", span = elem.span())]
|
||||||
@ -21,11 +21,11 @@ pub fn layout_lr(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract implicit LrElem.
|
// Extract implicit LrElem.
|
||||||
if let Some(lr) = body.to_packed::<LrElem>() {
|
if let Some(lr) = body.to_packed::<LrElem>()
|
||||||
if lr.size.get(styles).is_one() {
|
&& lr.size.get(styles).is_one()
|
||||||
|
{
|
||||||
body = &lr.body;
|
body = &lr.body;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut fragments = ctx.layout_into_fragments(body, styles)?;
|
let mut fragments = ctx.layout_into_fragments(body, styles)?;
|
||||||
|
|
||||||
@ -33,12 +33,13 @@ pub fn layout_lr(
|
|||||||
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
|
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
|
||||||
let inner_fragments = &mut fragments[start_idx..end_idx];
|
let inner_fragments = &mut fragments[start_idx..end_idx];
|
||||||
|
|
||||||
let axis = scaled!(ctx, styles, axis_height);
|
let mut max_extent = Abs::zero();
|
||||||
let max_extent = inner_fragments
|
for fragment in inner_fragments.iter() {
|
||||||
.iter()
|
let (font, size) = fragment.font(ctx, styles, elem.span())?;
|
||||||
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
|
let axis = value!(font, axis_height).at(size);
|
||||||
.max()
|
let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
|
||||||
.unwrap_or_default();
|
max_extent = max_extent.max(extent);
|
||||||
|
}
|
||||||
|
|
||||||
let relative_to = 2.0 * max_extent;
|
let relative_to = 2.0 * max_extent;
|
||||||
let height = elem.size.resolve(styles);
|
let height = elem.size.resolve(styles);
|
||||||
@ -55,13 +56,13 @@ pub fn layout_lr(
|
|||||||
|
|
||||||
// Handle MathFragment::Glyph fragments that should be scaled up.
|
// Handle MathFragment::Glyph fragments that should be scaled up.
|
||||||
for fragment in inner_fragments.iter_mut() {
|
for fragment in inner_fragments.iter_mut() {
|
||||||
if let MathFragment::Glyph(ref mut glyph) = fragment {
|
if let MathFragment::Glyph(glyph) = fragment
|
||||||
if glyph.mid_stretched == Some(false) {
|
&& glyph.mid_stretched == Some(false)
|
||||||
|
{
|
||||||
glyph.mid_stretched = Some(true);
|
glyph.mid_stretched = Some(true);
|
||||||
scale(ctx, fragment, relative_to, height);
|
scale(ctx, fragment, relative_to, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Remove weak SpacingFragment immediately after the opening or immediately
|
// Remove weak SpacingFragment immediately after the opening or immediately
|
||||||
// before the closing.
|
// before the closing.
|
||||||
@ -95,7 +96,7 @@ pub fn layout_mid(
|
|||||||
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
|
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
|
||||||
|
|
||||||
for fragment in &mut fragments {
|
for fragment in &mut fragments {
|
||||||
if let MathFragment::Glyph(ref mut glyph) = fragment {
|
if let MathFragment::Glyph(glyph) = fragment {
|
||||||
glyph.mid_stretched = Some(false);
|
glyph.mid_stretched = Some(false);
|
||||||
glyph.class = MathClass::Relation;
|
glyph.class = MathClass::Relation;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use typst_library::diag::{bail, warning, SourceResult};
|
use typst_library::diag::{SourceResult, bail, warning};
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
||||||
};
|
};
|
||||||
@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
|
|||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment,
|
AlignmentResult, DELIM_SHORT_FALL, FrameFragment, GlyphFragment, LeftRightAlternator,
|
||||||
LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
|
MathContext, alignments, find_math_font, style_for_denominator,
|
||||||
};
|
};
|
||||||
|
|
||||||
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
||||||
@ -186,12 +186,10 @@ fn layout_body(
|
|||||||
// We pad ascent and descent with the ascent and descent of the paren
|
// We pad ascent and descent with the ascent and descent of the paren
|
||||||
// to ensure that normal matrices are aligned with others unless they are
|
// to ensure that normal matrices are aligned with others unless they are
|
||||||
// way too big.
|
// way too big.
|
||||||
let paren = GlyphFragment::new_char(
|
// This will never panic as a paren will never shape into nothing.
|
||||||
ctx.font,
|
let paren =
|
||||||
styles.chain(&denom_style),
|
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
|
||||||
'(',
|
.unwrap();
|
||||||
Span::detached(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
for (column, col) in columns.iter().zip(&mut cols) {
|
for (column, col) in columns.iter().zip(&mut cols) {
|
||||||
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
|
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
|
||||||
@ -314,13 +312,15 @@ fn layout_delimiters(
|
|||||||
span: Span,
|
span: Span,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||||
let axis = scaled!(ctx, styles, axis_height);
|
let font = find_math_font(ctx.engine.world, styles, span)?;
|
||||||
|
let axis = value!(font, axis_height).resolve(styles);
|
||||||
let height = frame.height();
|
let height = frame.height();
|
||||||
let target = height + VERTICAL_PADDING.of(height);
|
let target = height + VERTICAL_PADDING.of(height);
|
||||||
frame.set_baseline(height / 2.0 + axis);
|
frame.set_baseline(height / 2.0 + axis);
|
||||||
|
|
||||||
if let Some(left_c) = left {
|
if let Some(left_c) = left {
|
||||||
let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?;
|
let mut left =
|
||||||
|
ctx.layout_into_fragment(&SymbolElem::packed(left_c).spanned(span), styles)?;
|
||||||
left.stretch_vertical(ctx, target - short_fall);
|
left.stretch_vertical(ctx, target - short_fall);
|
||||||
left.center_on_axis();
|
left.center_on_axis();
|
||||||
ctx.push(left);
|
ctx.push(left);
|
||||||
@ -329,7 +329,8 @@ fn layout_delimiters(
|
|||||||
ctx.push(FrameFragment::new(styles, frame));
|
ctx.push(FrameFragment::new(styles, frame));
|
||||||
|
|
||||||
if let Some(right_c) = right {
|
if let Some(right_c) = right {
|
||||||
let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?;
|
let mut right =
|
||||||
|
ctx.layout_into_fragment(&SymbolElem::packed(right_c).spanned(span), styles)?;
|
||||||
right.stretch_vertical(ctx, target - short_fall);
|
right.stretch_vertical(ctx, target - short_fall);
|
||||||
right.center_on_axis();
|
right.center_on_axis();
|
||||||
ctx.push(right);
|
ctx.push(right);
|
||||||
|
@ -13,7 +13,7 @@ mod stretch;
|
|||||||
mod text;
|
mod text;
|
||||||
mod underover;
|
mod underover;
|
||||||
|
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
||||||
@ -27,16 +27,12 @@ use typst_library::layout::{
|
|||||||
use typst_library::math::*;
|
use typst_library::math::*;
|
||||||
use typst_library::model::ParElem;
|
use typst_library::model::ParElem;
|
||||||
use typst_library::routines::{Arenas, RealizationKind};
|
use typst_library::routines::{Arenas, RealizationKind};
|
||||||
use typst_library::text::{
|
use typst_library::text::{LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, TextElem};
|
||||||
families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
|
|
||||||
};
|
|
||||||
use typst_library::World;
|
|
||||||
use typst_syntax::Span;
|
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use self::fragment::{
|
use self::fragment::{
|
||||||
has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment,
|
FrameFragment, GlyphFragment, Limits, MathFragment, has_dtls_feat, stretch_axes,
|
||||||
};
|
};
|
||||||
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
|
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
|
||||||
use self::shared::*;
|
use self::shared::*;
|
||||||
@ -53,12 +49,11 @@ pub fn layout_equation_inline(
|
|||||||
) -> SourceResult<Vec<InlineItem>> {
|
) -> SourceResult<Vec<InlineItem>> {
|
||||||
assert!(!elem.block.get(styles));
|
assert!(!elem.block.get(styles));
|
||||||
|
|
||||||
let font = find_math_font(engine, styles, elem.span())?;
|
|
||||||
|
|
||||||
let mut locator = locator.split();
|
let mut locator = locator.split();
|
||||||
let mut ctx = MathContext::new(engine, &mut locator, region, &font);
|
let mut ctx = MathContext::new(engine, &mut locator, region);
|
||||||
|
|
||||||
let scale_style = style_for_script_scale(&ctx);
|
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
|
||||||
|
let scale_style = style_for_script_scale(&font);
|
||||||
let styles = styles.chain(&scale_style);
|
let styles = styles.chain(&scale_style);
|
||||||
|
|
||||||
let run = ctx.layout_into_run(&elem.body, styles)?;
|
let run = ctx.layout_into_run(&elem.body, styles)?;
|
||||||
@ -108,12 +103,12 @@ pub fn layout_equation_block(
|
|||||||
assert!(elem.block.get(styles));
|
assert!(elem.block.get(styles));
|
||||||
|
|
||||||
let span = elem.span();
|
let span = elem.span();
|
||||||
let font = find_math_font(engine, styles, span)?;
|
|
||||||
|
|
||||||
let mut locator = locator.split();
|
let mut locator = locator.split();
|
||||||
let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font);
|
let mut ctx = MathContext::new(engine, &mut locator, regions.base());
|
||||||
|
|
||||||
let scale_style = style_for_script_scale(&ctx);
|
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
|
||||||
|
let scale_style = style_for_script_scale(&font);
|
||||||
let styles = styles.chain(&scale_style);
|
let styles = styles.chain(&scale_style);
|
||||||
|
|
||||||
let full_equation_builder = ctx
|
let full_equation_builder = ctx
|
||||||
@ -234,24 +229,6 @@ pub fn layout_equation_block(
|
|||||||
Ok(Fragment::frames(frames))
|
Ok(Fragment::frames(frames))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_math_font(
|
|
||||||
engine: &mut Engine<'_>,
|
|
||||||
styles: StyleChain,
|
|
||||||
span: Span,
|
|
||||||
) -> SourceResult<Font> {
|
|
||||||
let variant = variant(styles);
|
|
||||||
let world = engine.world;
|
|
||||||
let Some(font) = families(styles).find_map(|family| {
|
|
||||||
let id = world.book().select(family.as_str(), variant)?;
|
|
||||||
let font = world.font(id)?;
|
|
||||||
let _ = font.ttf().tables().math?.constants?;
|
|
||||||
Some(font)
|
|
||||||
}) else {
|
|
||||||
bail!(span, "current font does not support math");
|
|
||||||
};
|
|
||||||
Ok(font)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_equation_number(
|
fn add_equation_number(
|
||||||
equation_builder: MathRunFrameBuilder,
|
equation_builder: MathRunFrameBuilder,
|
||||||
number: Frame,
|
number: Frame,
|
||||||
@ -370,9 +347,6 @@ struct MathContext<'a, 'v, 'e> {
|
|||||||
engine: &'v mut Engine<'e>,
|
engine: &'v mut Engine<'e>,
|
||||||
locator: &'v mut SplitLocator<'a>,
|
locator: &'v mut SplitLocator<'a>,
|
||||||
region: Region,
|
region: Region,
|
||||||
// Font-related.
|
|
||||||
font: &'a Font,
|
|
||||||
constants: ttf_parser::math::Constants<'a>,
|
|
||||||
// Mutable.
|
// Mutable.
|
||||||
fragments: Vec<MathFragment>,
|
fragments: Vec<MathFragment>,
|
||||||
}
|
}
|
||||||
@ -383,19 +357,11 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
|||||||
engine: &'v mut Engine<'e>,
|
engine: &'v mut Engine<'e>,
|
||||||
locator: &'v mut SplitLocator<'a>,
|
locator: &'v mut SplitLocator<'a>,
|
||||||
base: Size,
|
base: Size,
|
||||||
font: &'a Font,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// These unwraps are safe as the font given is one returned by the
|
|
||||||
// find_math_font function, which only returns fonts that have a math
|
|
||||||
// constants table.
|
|
||||||
let constants = font.ttf().tables().math.unwrap().constants.unwrap();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
engine,
|
engine,
|
||||||
locator,
|
locator,
|
||||||
region: Region::new(base, Axes::splat(false)),
|
region: Region::new(base, Axes::splat(false)),
|
||||||
font,
|
|
||||||
constants,
|
|
||||||
fragments: vec![],
|
fragments: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -469,17 +435,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
|||||||
styles,
|
styles,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let outer = styles;
|
|
||||||
for (elem, styles) in pairs {
|
for (elem, styles) in pairs {
|
||||||
// Hack because the font is fixed in math.
|
|
||||||
if styles != outer
|
|
||||||
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
|
|
||||||
{
|
|
||||||
let frame = layout_external(elem, self, styles)?;
|
|
||||||
self.push(FrameFragment::new(styles, frame).with_spaced(true));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
layout_realized(elem, self, styles)?;
|
layout_realized(elem, self, styles)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,7 +452,10 @@ fn layout_realized(
|
|||||||
if let Some(elem) = elem.to_packed::<TagElem>() {
|
if let Some(elem) = elem.to_packed::<TagElem>() {
|
||||||
ctx.push(MathFragment::Tag(elem.tag.clone()));
|
ctx.push(MathFragment::Tag(elem.tag.clone()));
|
||||||
} else if elem.is::<SpaceElem>() {
|
} else if elem.is::<SpaceElem>() {
|
||||||
let space_width = ctx.font.space_width().unwrap_or(THICK);
|
let space_width = find_math_font(ctx.engine.world, styles, elem.span())
|
||||||
|
.ok()
|
||||||
|
.and_then(|font| font.space_width())
|
||||||
|
.unwrap_or(THICK);
|
||||||
ctx.push(MathFragment::Space(space_width.resolve(styles)));
|
ctx.push(MathFragment::Space(space_width.resolve(styles)));
|
||||||
} else if elem.is::<LinebreakElem>() {
|
} else if elem.is::<LinebreakElem>() {
|
||||||
ctx.push(MathFragment::Linebreak);
|
ctx.push(MathFragment::Linebreak);
|
||||||
@ -566,10 +525,12 @@ fn layout_realized(
|
|||||||
self::underover::layout_overshell(elem, ctx, styles)?
|
self::underover::layout_overshell(elem, ctx, styles)?
|
||||||
} else {
|
} else {
|
||||||
let mut frame = layout_external(elem, ctx, styles)?;
|
let mut frame = layout_external(elem, ctx, styles)?;
|
||||||
if !frame.has_baseline() {
|
if !frame.has_baseline() && !elem.is::<RawElem>() {
|
||||||
let axis = scaled!(ctx, styles, axis_height);
|
if let Ok(font) = find_math_font(ctx.engine.world, styles, elem.span()) {
|
||||||
|
let axis = value!(font, axis_height).resolve(styles);
|
||||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ctx.push(
|
ctx.push(
|
||||||
FrameFragment::new(styles, frame)
|
FrameFragment::new(styles, frame)
|
||||||
.with_spaced(true)
|
.with_spaced(true)
|
||||||
@ -603,13 +564,10 @@ fn layout_h(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
if let Spacing::Rel(rel) = elem.amount {
|
if let Spacing::Rel(rel) = elem.amount
|
||||||
if rel.rel.is_zero() {
|
&& rel.rel.is_zero()
|
||||||
ctx.push(MathFragment::Spacing(
|
{
|
||||||
rel.abs.resolve(styles),
|
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak.get(styles)));
|
||||||
elem.weak.get(styles),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
|
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||||
use typst_library::math::{EquationElem, MathSize, RootElem};
|
use typst_library::math::{EquationElem, MathSize, RootElem};
|
||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_library::visualize::{FixedStroke, Geometry};
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
|
|
||||||
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext};
|
use super::{FrameFragment, MathContext, style_cramped};
|
||||||
|
|
||||||
/// Lays out a [`RootElem`].
|
/// Lays out a [`RootElem`].
|
||||||
///
|
///
|
||||||
@ -17,45 +17,62 @@ pub fn layout_root(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let index = elem.index.get_ref(styles);
|
|
||||||
let span = elem.span();
|
let span = elem.span();
|
||||||
|
|
||||||
let gap = scaled!(
|
|
||||||
ctx, styles,
|
|
||||||
text: radical_vertical_gap,
|
|
||||||
display: radical_display_style_vertical_gap,
|
|
||||||
);
|
|
||||||
let thickness = scaled!(ctx, styles, radical_rule_thickness);
|
|
||||||
let extra_ascender = scaled!(ctx, styles, radical_extra_ascender);
|
|
||||||
let kern_before = scaled!(ctx, styles, radical_kern_before_degree);
|
|
||||||
let kern_after = scaled!(ctx, styles, radical_kern_after_degree);
|
|
||||||
let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
|
|
||||||
|
|
||||||
// Layout radicand.
|
// Layout radicand.
|
||||||
let radicand = {
|
let radicand = {
|
||||||
let cramped = style_cramped();
|
let cramped = style_cramped();
|
||||||
let styles = styles.chain(&cramped);
|
let styles = styles.chain(&cramped);
|
||||||
let run = ctx.layout_into_run(&elem.radicand, styles)?;
|
let run = ctx.layout_into_run(&elem.radicand, styles)?;
|
||||||
let multiline = run.is_multiline();
|
let multiline = run.is_multiline();
|
||||||
let mut radicand = run.into_fragment(styles).into_frame();
|
let radicand = run.into_fragment(styles);
|
||||||
if multiline {
|
if multiline {
|
||||||
// Align the frame center line with the math axis.
|
// Align the frame center line with the math axis.
|
||||||
radicand.set_baseline(
|
let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?;
|
||||||
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height),
|
let axis = value!(font, axis_height).at(size);
|
||||||
);
|
let mut radicand = radicand.into_frame();
|
||||||
}
|
radicand.set_baseline(radicand.height() / 2.0 + axis);
|
||||||
radicand
|
radicand
|
||||||
|
} else {
|
||||||
|
radicand.into_frame()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Layout root symbol.
|
// Layout root symbol.
|
||||||
|
let mut sqrt =
|
||||||
|
ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?;
|
||||||
|
|
||||||
|
let (font, size) = sqrt.font(ctx, styles, span)?;
|
||||||
|
let thickness = value!(font, radical_rule_thickness).at(size);
|
||||||
|
let extra_ascender = value!(font, radical_extra_ascender).at(size);
|
||||||
|
let kern_before = value!(font, radical_kern_before_degree).at(size);
|
||||||
|
let kern_after = value!(font, radical_kern_after_degree).at(size);
|
||||||
|
let raise_factor = percent!(font, radical_degree_bottom_raise_percent);
|
||||||
|
let gap = value!(
|
||||||
|
font, styles,
|
||||||
|
text: radical_vertical_gap,
|
||||||
|
display: radical_display_style_vertical_gap,
|
||||||
|
)
|
||||||
|
.at(size);
|
||||||
|
|
||||||
|
let line = FrameItem::Shape(
|
||||||
|
Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair(
|
||||||
|
sqrt.fill()
|
||||||
|
.unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()),
|
||||||
|
thickness,
|
||||||
|
)),
|
||||||
|
span,
|
||||||
|
);
|
||||||
|
|
||||||
let target = radicand.height() + thickness + gap;
|
let target = radicand.height() + thickness + gap;
|
||||||
let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
|
|
||||||
sqrt.stretch_vertical(ctx, target);
|
sqrt.stretch_vertical(ctx, target);
|
||||||
let sqrt = sqrt.into_frame();
|
let sqrt = sqrt.into_frame();
|
||||||
|
|
||||||
// Layout the index.
|
// Layout the index.
|
||||||
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
|
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
|
||||||
let index = index
|
let index = elem
|
||||||
|
.index
|
||||||
|
.get_ref(styles)
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
|
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
@ -107,19 +124,7 @@ pub fn layout_root(
|
|||||||
}
|
}
|
||||||
|
|
||||||
frame.push_frame(sqrt_pos, sqrt);
|
frame.push_frame(sqrt_pos, sqrt);
|
||||||
frame.push(
|
frame.push(line_pos, line);
|
||||||
line_pos,
|
|
||||||
FrameItem::Shape(
|
|
||||||
Geometry::Line(Point::with_x(radicand.width())).stroked(
|
|
||||||
FixedStroke::from_pair(
|
|
||||||
styles.get_ref(TextElem::fill).as_decoration(),
|
|
||||||
thickness,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
span,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
frame.push_frame(radicand_pos, radicand);
|
frame.push_frame(radicand_pos, radicand);
|
||||||
ctx.push(FrameFragment::new(styles, frame));
|
ctx.push(FrameFragment::new(styles, frame));
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@ use std::iter::once;
|
|||||||
|
|
||||||
use typst_library::foundations::{Resolve, StyleChain};
|
use typst_library::foundations::{Resolve, StyleChain};
|
||||||
use typst_library::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
|
use typst_library::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
|
||||||
use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN};
|
use typst_library::math::{EquationElem, MEDIUM, MathSize, THICK, THIN};
|
||||||
use typst_library::model::ParElem;
|
use typst_library::model::ParElem;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use super::{alignments, FrameFragment, MathFragment};
|
use super::{FrameFragment, MathFragment, alignments};
|
||||||
|
|
||||||
const TIGHT_LEADING: Em = Em::new(0.25);
|
const TIGHT_LEADING: Em = Em::new(0.25);
|
||||||
|
|
||||||
@ -87,11 +87,11 @@ impl MathRun {
|
|||||||
|
|
||||||
// Insert spacing between the last and this non-ignorant item.
|
// Insert spacing between the last and this non-ignorant item.
|
||||||
if !fragment.is_ignorant() {
|
if !fragment.is_ignorant() {
|
||||||
if let Some(i) = last {
|
if let Some(i) = last
|
||||||
if let Some(s) = spacing(&resolved[i], space.take(), &fragment) {
|
&& let Some(s) = spacing(&resolved[i], space.take(), &fragment)
|
||||||
|
{
|
||||||
resolved.insert(i + 1, s);
|
resolved.insert(i + 1, s);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
last = Some(resolved.len());
|
last = Some(resolved.len());
|
||||||
}
|
}
|
||||||
@ -123,11 +123,11 @@ impl MathRun {
|
|||||||
1 + self.0.iter().filter(|f| matches!(f, MathFragment::Linebreak)).count();
|
1 + self.0.iter().filter(|f| matches!(f, MathFragment::Linebreak)).count();
|
||||||
|
|
||||||
// A linebreak at the very end does not introduce an extra row.
|
// A linebreak at the very end does not introduce an extra row.
|
||||||
if let Some(f) = self.0.last() {
|
if let Some(f) = self.0.last()
|
||||||
if matches!(f, MathFragment::Linebreak) {
|
&& matches!(f, MathFragment::Linebreak)
|
||||||
|
{
|
||||||
count -= 1
|
count -= 1
|
||||||
}
|
}
|
||||||
}
|
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,11 +344,11 @@ impl MathRun {
|
|||||||
descent = Abs::zero();
|
descent = Abs::zero();
|
||||||
|
|
||||||
space_is_visible = true;
|
space_is_visible = true;
|
||||||
if let Some(f_next) = iter.peek() {
|
if let Some(f_next) = iter.peek()
|
||||||
if !is_space(f_next) {
|
&& !is_space(f_next)
|
||||||
|
{
|
||||||
items.push(InlineItem::Space(Abs::zero(), true));
|
items.push(InlineItem::Space(Abs::zero(), true));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
space_is_visible = false;
|
space_is_visible = false;
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,47 @@
|
|||||||
use ttf_parser::math::MathValue;
|
use comemo::Tracked;
|
||||||
use ttf_parser::Tag;
|
use ttf_parser::Tag;
|
||||||
|
use ttf_parser::math::MathValue;
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::foundations::{Style, StyleChain};
|
use typst_library::foundations::{Style, StyleChain};
|
||||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
|
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
|
||||||
use typst_library::math::{EquationElem, MathSize};
|
use typst_library::math::{EquationElem, MathSize};
|
||||||
use typst_library::text::{FontFeatures, TextElem};
|
use typst_library::text::{Font, FontFeatures, TextElem, families, variant};
|
||||||
|
use typst_syntax::Span;
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
|
use super::{LeftRightAlternator, MathFragment, MathRun};
|
||||||
|
|
||||||
macro_rules! scaled {
|
macro_rules! value {
|
||||||
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
||||||
match $styles.get(typst_library::math::EquationElem::size) {
|
match $styles.get(typst_library::math::EquationElem::size) {
|
||||||
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
|
typst_library::math::MathSize::Display => value!($font, $display),
|
||||||
_ => scaled!($ctx, $styles, $text),
|
_ => value!($font, $text),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($ctx:expr, $styles:expr, $name:ident) => {
|
($font:expr, $name:ident) => {
|
||||||
$crate::math::Scaled::scaled(
|
$font
|
||||||
$ctx.constants.$name(),
|
.ttf()
|
||||||
$ctx,
|
.tables()
|
||||||
$styles.resolve(typst_library::text::TextElem::size),
|
.math
|
||||||
)
|
.and_then(|math| math.constants)
|
||||||
|
.map(|constants| {
|
||||||
|
crate::math::shared::Scaled::scaled(constants.$name(), &$font)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! percent {
|
macro_rules! percent {
|
||||||
($ctx:expr, $name:ident) => {
|
($font:expr, $name:ident) => {
|
||||||
$ctx.constants.$name() as f64 / 100.0
|
$font
|
||||||
|
.ttf()
|
||||||
|
.tables()
|
||||||
|
.math
|
||||||
|
.and_then(|math| math.constants)
|
||||||
|
.map(|constants| constants.$name())
|
||||||
|
.unwrap() as f64
|
||||||
|
/ 100.0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,27 +50,47 @@ pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
|||||||
|
|
||||||
/// Converts some unit to an absolute length with the current font & font size.
|
/// Converts some unit to an absolute length with the current font & font size.
|
||||||
pub trait Scaled {
|
pub trait Scaled {
|
||||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
|
fn scaled(self, font: &Font) -> Em;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scaled for i16 {
|
impl Scaled for i16 {
|
||||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
fn scaled(self, font: &Font) -> Em {
|
||||||
ctx.font.to_em(self).at(font_size)
|
font.to_em(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scaled for u16 {
|
impl Scaled for u16 {
|
||||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
fn scaled(self, font: &Font) -> Em {
|
||||||
ctx.font.to_em(self).at(font_size)
|
font.to_em(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scaled for MathValue<'_> {
|
impl Scaled for MathValue<'_> {
|
||||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
fn scaled(self, font: &Font) -> Em {
|
||||||
self.value.scaled(ctx, font_size)
|
self.value.scaled(font)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current math font.
|
||||||
|
#[comemo::memoize]
|
||||||
|
pub fn find_math_font(
|
||||||
|
world: Tracked<dyn World + '_>,
|
||||||
|
styles: StyleChain,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Font> {
|
||||||
|
let variant = variant(styles);
|
||||||
|
let Some(font) = families(styles).find_map(|family| {
|
||||||
|
let id = world.book().select(family.as_str(), variant)?;
|
||||||
|
let font = world.font(id)?;
|
||||||
|
let _ = font.ttf().tables().math?.constants?;
|
||||||
|
// Take the base font as the "main" math font.
|
||||||
|
family.covers().map_or(Some(font), |_| None)
|
||||||
|
}) else {
|
||||||
|
bail!(span, "current font does not support math");
|
||||||
|
};
|
||||||
|
Ok(font)
|
||||||
|
}
|
||||||
|
|
||||||
/// Styles something as cramped.
|
/// Styles something as cramped.
|
||||||
pub fn style_cramped() -> LazyHash<Style> {
|
pub fn style_cramped() -> LazyHash<Style> {
|
||||||
EquationElem::cramped.set(true).wrap()
|
EquationElem::cramped.set(true).wrap()
|
||||||
@ -107,11 +142,12 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Styles to add font constants to the style chain.
|
/// Styles to add font constants to the style chain.
|
||||||
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
|
pub fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
|
||||||
|
let constants = font.ttf().tables().math.and_then(|math| math.constants).unwrap();
|
||||||
EquationElem::script_scale
|
EquationElem::script_scale
|
||||||
.set((
|
.set((
|
||||||
ctx.constants.script_percent_scale_down(),
|
constants.script_percent_scale_down(),
|
||||||
ctx.constants.script_script_percent_scale_down(),
|
constants.script_script_percent_scale_down(),
|
||||||
))
|
))
|
||||||
.wrap()
|
.wrap()
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use typst_library::diag::{warning, SourceResult};
|
use typst_library::diag::{SourceResult, warning};
|
||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use typst_library::layout::{Abs, Axis, Rel};
|
use typst_library::layout::{Abs, Axis, Rel};
|
||||||
use typst_library::math::StretchElem;
|
use typst_library::math::StretchElem;
|
||||||
use typst_utils::Get;
|
use typst_utils::Get;
|
||||||
|
|
||||||
use super::{stretch_axes, MathContext, MathFragment};
|
use super::{MathContext, MathFragment, stretch_axes};
|
||||||
|
|
||||||
/// Lays out a [`StretchElem`].
|
/// Lays out a [`StretchElem`].
|
||||||
#[typst_macros::time(name = "math.stretch", span = elem.span())]
|
#[typst_macros::time(name = "math.stretch", span = elem.span())]
|
||||||
@ -37,7 +37,7 @@ pub fn stretch_fragment(
|
|||||||
) {
|
) {
|
||||||
let size = fragment.size();
|
let size = fragment.size();
|
||||||
|
|
||||||
let MathFragment::Glyph(ref mut glyph) = fragment else { return };
|
let MathFragment::Glyph(glyph) = fragment else { return };
|
||||||
|
|
||||||
// Return if we attempt to stretch along an axis which isn't stretchable,
|
// Return if we attempt to stretch along an axis which isn't stretchable,
|
||||||
// so that the original fragment isn't modified.
|
// so that the original fragment isn't modified.
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
use std::f64::consts::SQRT_2;
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
use codex::styling::{to_style, MathStyle};
|
use codex::styling::{MathStyle, to_style};
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
use typst_library::foundations::{Packed, Resolve, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{Abs, Size};
|
use typst_library::layout::{Abs, Size};
|
||||||
use typst_library::math::{EquationElem, MathSize};
|
use typst_library::math::{EquationElem, MathSize};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
|
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
|
||||||
};
|
};
|
||||||
use typst_syntax::{is_newline, Span};
|
use typst_syntax::{Span, is_newline};
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext, MathFragment,
|
FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun, find_math_font,
|
||||||
MathRun,
|
has_dtls_feat, style_dtls,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Lays out a [`TextElem`].
|
/// Lays out a [`TextElem`].
|
||||||
@ -52,7 +52,8 @@ fn layout_text_lines<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut frame = MathRun::new(fragments).into_frame(styles);
|
let mut frame = MathRun::new(fragments).into_frame(styles);
|
||||||
let axis = scaled!(ctx, styles, axis_height);
|
let font = find_math_font(ctx.engine.world, styles, span)?;
|
||||||
|
let axis = value!(font, axis_height).resolve(styles);
|
||||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||||
Ok(FrameFragment::new(styles, frame))
|
Ok(FrameFragment::new(styles, frame))
|
||||||
}
|
}
|
||||||
@ -80,7 +81,9 @@ fn layout_inline_text(
|
|||||||
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||||
let c = to_style(unstyled_c, style).next().unwrap();
|
let c = to_style(unstyled_c, style).next().unwrap();
|
||||||
|
|
||||||
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
|
// This won't panic as ASCII digits and '.' will never end up as
|
||||||
|
// nothing after shaping.
|
||||||
|
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
|
||||||
fragments.push(glyph.into());
|
fragments.push(glyph.into());
|
||||||
}
|
}
|
||||||
let frame = MathRun::new(fragments).into_frame(styles);
|
let frame = MathRun::new(fragments).into_frame(styles);
|
||||||
@ -132,8 +135,11 @@ pub fn layout_symbol(
|
|||||||
// Switch dotless char to normal when we have the dtls OpenType feature.
|
// Switch dotless char to normal when we have the dtls OpenType feature.
|
||||||
// This should happen before the main styling pass.
|
// This should happen before the main styling pass.
|
||||||
let dtls = style_dtls();
|
let dtls = style_dtls();
|
||||||
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
|
let (unstyled_c, symbol_styles) = match (
|
||||||
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
|
try_dotless(elem.text),
|
||||||
|
find_math_font(ctx.engine.world, styles, elem.span()),
|
||||||
|
) {
|
||||||
|
(Some(c), Ok(font)) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
|
||||||
_ => (elem.text, styles),
|
_ => (elem.text, styles),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -144,39 +150,22 @@ pub fn layout_symbol(
|
|||||||
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||||
let text: EcoString = to_style(unstyled_c, style).collect();
|
let text: EcoString = to_style(unstyled_c, style).collect();
|
||||||
|
|
||||||
let fragment: MathFragment =
|
if let Some(mut glyph) = GlyphFragment::new(ctx, symbol_styles, &text, elem.span())? {
|
||||||
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) {
|
|
||||||
Ok(mut glyph) => {
|
|
||||||
adjust_glyph_layout(&mut glyph, ctx, styles);
|
|
||||||
glyph.into()
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Not in the math font, fallback to normal inline text layout.
|
|
||||||
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
|
|
||||||
layout_inline_text(&text, elem.span(), ctx, styles)?.into()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ctx.push(fragment);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Centers large glyphs vertically on the axis, scaling them if in display
|
|
||||||
/// style.
|
|
||||||
fn adjust_glyph_layout(
|
|
||||||
glyph: &mut GlyphFragment,
|
|
||||||
ctx: &mut MathContext,
|
|
||||||
styles: StyleChain,
|
|
||||||
) {
|
|
||||||
if glyph.class == MathClass::Large {
|
if glyph.class == MathClass::Large {
|
||||||
if styles.get(EquationElem::size) == MathSize::Display {
|
if styles.get(EquationElem::size) == MathSize::Display {
|
||||||
let height = scaled!(ctx, styles, display_operator_min_height)
|
let height = value!(glyph.item.font, display_operator_min_height)
|
||||||
|
.at(glyph.item.size)
|
||||||
.max(SQRT_2 * glyph.size.y);
|
.max(SQRT_2 * glyph.size.y);
|
||||||
glyph.stretch_vertical(ctx, height);
|
glyph.stretch_vertical(ctx, height);
|
||||||
};
|
};
|
||||||
// TeXbook p 155. Large operators are always vertically centered on the
|
// TeXbook p 155. Large operators are always vertically centered on
|
||||||
// axis.
|
// the axis.
|
||||||
glyph.center_on_axis();
|
glyph.center_on_axis();
|
||||||
}
|
}
|
||||||
|
ctx.push(glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The non-dotless version of a dotless character that can be used with the
|
/// The non-dotless version of a dotless character that can be used with the
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
|
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
|
||||||
use typst_library::math::{
|
use typst_library::math::{
|
||||||
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
|
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
|
||||||
@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
|
|||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
|
FrameFragment, LeftRightAlternator, MathContext, MathRun, stack, style_cramped,
|
||||||
GlyphFragment, LeftRightAlternator, MathContext, MathRun,
|
style_for_subscript, style_for_superscript,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BRACE_GAP: Em = Em::new(0.25);
|
const BRACE_GAP: Em = Em::new(0.25);
|
||||||
@ -208,26 +208,29 @@ fn layout_underoverline(
|
|||||||
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
|
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
|
||||||
match position {
|
match position {
|
||||||
Position::Under => {
|
Position::Under => {
|
||||||
let sep = scaled!(ctx, styles, underbar_extra_descender);
|
|
||||||
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
|
|
||||||
let gap = scaled!(ctx, styles, underbar_vertical_gap);
|
|
||||||
extra_height = sep + bar_height + gap;
|
|
||||||
|
|
||||||
content = ctx.layout_into_fragment(body, styles)?;
|
content = ctx.layout_into_fragment(body, styles)?;
|
||||||
|
|
||||||
|
let (font, size) = content.font(ctx, styles, span)?;
|
||||||
|
let sep = value!(font, underbar_extra_descender).at(size);
|
||||||
|
bar_height = value!(font, underbar_rule_thickness).at(size);
|
||||||
|
let gap = value!(font, underbar_vertical_gap).at(size);
|
||||||
|
extra_height = sep + bar_height + gap;
|
||||||
|
|
||||||
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
|
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
|
||||||
content_pos = Point::zero();
|
content_pos = Point::zero();
|
||||||
baseline = content.ascent();
|
baseline = content.ascent();
|
||||||
line_adjust = -content.italics_correction();
|
line_adjust = -content.italics_correction();
|
||||||
}
|
}
|
||||||
Position::Over => {
|
Position::Over => {
|
||||||
let sep = scaled!(ctx, styles, overbar_extra_ascender);
|
|
||||||
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
|
|
||||||
let gap = scaled!(ctx, styles, overbar_vertical_gap);
|
|
||||||
extra_height = sep + bar_height + gap;
|
|
||||||
|
|
||||||
let cramped = style_cramped();
|
let cramped = style_cramped();
|
||||||
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;
|
let styles = styles.chain(&cramped);
|
||||||
|
content = ctx.layout_into_fragment(body, styles)?;
|
||||||
|
|
||||||
|
let (font, size) = content.font(ctx, styles, span)?;
|
||||||
|
let sep = value!(font, overbar_extra_ascender).at(size);
|
||||||
|
bar_height = value!(font, overbar_rule_thickness).at(size);
|
||||||
|
let gap = value!(font, overbar_vertical_gap).at(size);
|
||||||
|
extra_height = sep + bar_height + gap;
|
||||||
|
|
||||||
line_pos = Point::with_y(sep + bar_height / 2.0);
|
line_pos = Point::with_y(sep + bar_height / 2.0);
|
||||||
content_pos = Point::with_y(extra_height);
|
content_pos = Point::with_y(extra_height);
|
||||||
@ -285,7 +288,8 @@ fn layout_underoverspreader(
|
|||||||
let body = ctx.layout_into_run(body, styles)?;
|
let body = ctx.layout_into_run(body, styles)?;
|
||||||
let body_class = body.class();
|
let body_class = body.class();
|
||||||
let body = body.into_fragment(styles);
|
let body = body.into_fragment(styles);
|
||||||
let mut glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
|
let mut glyph =
|
||||||
|
ctx.layout_into_fragment(&SymbolElem::packed(c).spanned(span), styles)?;
|
||||||
glyph.stretch_horizontal(ctx, body.width());
|
glyph.stretch_horizontal(ctx, body.width());
|
||||||
|
|
||||||
let mut rows = vec![];
|
let mut rows = vec![];
|
||||||
|
@ -4,21 +4,23 @@ mod collect;
|
|||||||
mod finalize;
|
mod finalize;
|
||||||
mod run;
|
mod run;
|
||||||
|
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Content, StyleChain};
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
use typst_library::introspection::{
|
use typst_library::introspection::{
|
||||||
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
|
Introspector, IntrospectorBuilder, Locator, ManualPageCounter, SplitLocator, TagElem,
|
||||||
};
|
};
|
||||||
use typst_library::layout::{FrameItem, Page, PagedDocument, Point};
|
use typst_library::layout::{FrameItem, Page, PagedDocument, Point, Transform};
|
||||||
use typst_library::model::DocumentInfo;
|
use typst_library::model::DocumentInfo;
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
use typst_library::World;
|
|
||||||
|
|
||||||
use self::collect::{collect, Item};
|
use self::collect::{Item, collect};
|
||||||
use self::finalize::finalize;
|
use self::finalize::finalize;
|
||||||
use self::run::{layout_blank_page, layout_page_run, LayoutedPage};
|
use self::run::{LayoutedPage, layout_blank_page, layout_page_run};
|
||||||
|
|
||||||
/// Layout content into a document.
|
/// Layout content into a document.
|
||||||
///
|
///
|
||||||
@ -75,7 +77,7 @@ fn layout_document_impl(
|
|||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let mut info = DocumentInfo::default();
|
let mut info = DocumentInfo::default();
|
||||||
let mut children = (engine.routines.realize)(
|
let mut children = (engine.routines.realize)(
|
||||||
RealizationKind::LayoutDocument(&mut info),
|
RealizationKind::LayoutDocument { info: &mut info },
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
@ -84,7 +86,7 @@ fn layout_document_impl(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
|
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
|
||||||
let introspector = Introspector::paged(&pages);
|
let introspector = introspect_pages(&pages);
|
||||||
|
|
||||||
Ok(PagedDocument { pages, info, introspector })
|
Ok(PagedDocument { pages, info, introspector })
|
||||||
}
|
}
|
||||||
@ -157,3 +159,27 @@ fn layout_pages<'a>(
|
|||||||
|
|
||||||
Ok(pages)
|
Ok(pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Introspects pages.
|
||||||
|
#[typst_macros::time(name = "introspect pages")]
|
||||||
|
fn introspect_pages(pages: &[Page]) -> Introspector {
|
||||||
|
let mut builder = IntrospectorBuilder::new();
|
||||||
|
builder.pages = pages.len();
|
||||||
|
builder.page_numberings.reserve(pages.len());
|
||||||
|
builder.page_supplements.reserve(pages.len());
|
||||||
|
|
||||||
|
// Discover all elements.
|
||||||
|
let mut elems = Vec::new();
|
||||||
|
for (i, page) in pages.iter().enumerate() {
|
||||||
|
builder.page_numberings.push(page.numbering.clone());
|
||||||
|
builder.page_supplements.push(page.supplement.clone());
|
||||||
|
builder.discover_in_frame(
|
||||||
|
&mut elems,
|
||||||
|
&page.frame,
|
||||||
|
NonZeroUsize::new(1 + i).unwrap(),
|
||||||
|
Transform::identity(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.finalize(elems)
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
|
use typst_library::World;
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
@ -16,10 +17,9 @@ use typst_library::model::Numbering;
|
|||||||
use typst_library::routines::{Pair, Routines};
|
use typst_library::routines::{Pair, Routines};
|
||||||
use typst_library::text::{LocalName, TextElem};
|
use typst_library::text::{LocalName, TextElem};
|
||||||
use typst_library::visualize::Paint;
|
use typst_library::visualize::Paint;
|
||||||
use typst_library::World;
|
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use crate::flow::{layout_flow, FlowMode};
|
use crate::flow::{FlowMode, layout_flow};
|
||||||
|
|
||||||
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
||||||
/// page number to be finalized into a `Page`. (Because the margins can depend
|
/// page number to be finalized into a `Page`. (Because the margins can depend
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Packed, Resolve, StyleChain};
|
use typst_library::foundations::{Packed, Resolve, StyleChain};
|
||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use comemo::Track;
|
use comemo::Track;
|
||||||
use ecow::{eco_format, EcoVec};
|
use ecow::{EcoVec, eco_format};
|
||||||
use smallvec::smallvec;
|
use smallvec::smallvec;
|
||||||
use typst_library::diag::{bail, At, SourceResult};
|
use typst_library::diag::{At, SourceResult, bail};
|
||||||
use typst_library::foundations::{
|
use typst_library::foundations::{
|
||||||
dict, Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart,
|
Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart,
|
||||||
StyleChain, Target,
|
StyleChain, Target, dict,
|
||||||
};
|
};
|
||||||
use typst_library::introspection::{Counter, Locator, LocatorLink};
|
use typst_library::introspection::{Counter, Locator, LocatorLink};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
@ -20,8 +20,8 @@ use typst_library::math::EquationElem;
|
|||||||
use typst_library::model::{
|
use typst_library::model::{
|
||||||
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
|
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
|
||||||
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
|
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
|
||||||
LinkElem, LinkTarget, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem,
|
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
|
||||||
ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
|
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
|
||||||
};
|
};
|
||||||
use typst_library::pdf::EmbedElem;
|
use typst_library::pdf::EmbedElem;
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
@ -161,11 +161,7 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
|||||||
let indent = elem.indent.get(styles);
|
let indent = elem.indent.get(styles);
|
||||||
let hanging_indent = elem.hanging_indent.get(styles);
|
let hanging_indent = elem.hanging_indent.get(styles);
|
||||||
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
|
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
|
||||||
if tight {
|
if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
|
||||||
styles.get(ParElem::leading)
|
|
||||||
} else {
|
|
||||||
styles.get(ParElem::spacing)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let pad = hanging_indent + indent;
|
let pad = hanging_indent + indent;
|
||||||
@ -216,14 +212,8 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
|
|||||||
|
|
||||||
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
|
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
|
||||||
let body = elem.body.clone();
|
let body = elem.body.clone();
|
||||||
Ok(match &elem.dest {
|
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
|
||||||
LinkTarget::Dest(dest) => body.linked(dest.clone()),
|
Ok(body.linked(dest))
|
||||||
LinkTarget::Label(label) => {
|
|
||||||
let elem = engine.introspector.query_label(*label).at(elem.span())?;
|
|
||||||
let dest = Destination::Location(elem.location().unwrap());
|
|
||||||
body.linked(dest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
||||||
@ -278,7 +268,7 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
|
|||||||
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
|
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(block.pack().spanned(span))
|
Ok(block.pack())
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
||||||
@ -332,8 +322,7 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
|
|||||||
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
|
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
|
||||||
Ok(BlockElem::new()
|
Ok(BlockElem::new()
|
||||||
.with_body(Some(BlockBody::Content(elem.realize(engine, styles)?)))
|
.with_body(Some(BlockBody::Content(elem.realize(engine, styles)?)))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
|
||||||
@ -556,9 +545,7 @@ const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
|
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table)
|
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
|
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
|
||||||
@ -709,27 +696,19 @@ const ALIGN_RULE: ShowFn<AlignElem> =
|
|||||||
|elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles)));
|
|elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles)));
|
||||||
|
|
||||||
const PAD_RULE: ShowFn<PadElem> = |elem, _, _| {
|
const PAD_RULE: ShowFn<PadElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad)
|
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
|
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns)
|
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
|
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack)
|
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
|
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid)
|
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
|
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
|
||||||
@ -759,33 +738,23 @@ fn show_cell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
|
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
|
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
|
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
|
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
|
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const HIDE_RULE: ShowFn<HideElem> =
|
const HIDE_RULE: ShowFn<HideElem> =
|
||||||
@ -807,83 +776,66 @@ const LAYOUT_RULE: ShowFn<LayoutElem> = |elem, _, _| {
|
|||||||
crate::flow::layout_fragment(engine, &result, locator, styles, regions)
|
crate::flow::layout_fragment(engine, &result, locator, styles, regions)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| {
|
const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image)
|
||||||
.with_width(elem.width.get(styles))
|
.with_width(elem.width.get(styles))
|
||||||
.with_height(elem.height.get(styles))
|
.with_height(elem.height.get(styles))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
|
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| {
|
const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect)
|
||||||
.with_width(elem.width.get(styles))
|
.with_width(elem.width.get(styles))
|
||||||
.with_height(elem.height.get(styles))
|
.with_height(elem.height.get(styles))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| {
|
const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square)
|
||||||
.with_width(elem.width.get(styles))
|
.with_width(elem.width.get(styles))
|
||||||
.with_height(elem.height.get(styles))
|
.with_height(elem.height.get(styles))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| {
|
const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse)
|
||||||
.with_width(elem.width.get(styles))
|
.with_width(elem.width.get(styles))
|
||||||
.with_height(elem.height.get(styles))
|
.with_height(elem.height.get(styles))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| {
|
const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle)
|
||||||
.with_width(elem.width.get(styles))
|
.with_width(elem.width.get(styles))
|
||||||
.with_height(elem.height.get(styles))
|
.with_height(elem.height.get(styles))
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
|
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
|
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
|
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
|
||||||
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path)
|
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
|
const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
|
||||||
if elem.block.get(styles) {
|
if elem.block.get(styles) {
|
||||||
Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block)
|
Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block)
|
||||||
.pack()
|
.pack())
|
||||||
.spanned(elem.span()))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline)
|
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline).pack())
|
||||||
.pack()
|
|
||||||
.spanned(elem.span()))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::f64::consts::SQRT_2;
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
use kurbo::{CubicBez, ParamCurveExtrema};
|
use kurbo::{CubicBez, ParamCurveExtrema};
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, StyledElem};
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, StyledElem};
|
||||||
use typst_library::introspection::{Locator, SplitLocator};
|
use typst_library::introspection::{Locator, SplitLocator};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::cell::LazyCell;
|
use std::cell::LazyCell;
|
||||||
|
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{SourceResult, bail};
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
||||||
use typst_library::introspection::Locator;
|
use typst_library::introspection::Locator;
|
||||||
|
@ -31,6 +31,7 @@ flate2 = { workspace = true }
|
|||||||
fontdb = { workspace = true }
|
fontdb = { workspace = true }
|
||||||
glidesort = { workspace = true }
|
glidesort = { workspace = true }
|
||||||
hayagriva = { workspace = true }
|
hayagriva = { workspace = true }
|
||||||
|
hayro-syntax = { workspace = true }
|
||||||
icu_properties = { workspace = true }
|
icu_properties = { workspace = true }
|
||||||
icu_provider = { workspace = true }
|
icu_provider = { workspace = true }
|
||||||
icu_provider_blob = { workspace = true }
|
icu_provider_blob = { workspace = true }
|
||||||
|
@ -8,7 +8,7 @@ use std::string::FromUtf8Error;
|
|||||||
|
|
||||||
use az::SaturatingAs;
|
use az::SaturatingAs;
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::{eco_vec, EcoVec};
|
use ecow::{EcoVec, eco_vec};
|
||||||
use typst_syntax::package::{PackageSpec, PackageVersion};
|
use typst_syntax::package::{PackageSpec, PackageVersion};
|
||||||
use typst_syntax::{Lines, Span, Spanned, SyntaxError};
|
use typst_syntax::{Lines, Span, Spanned, SyntaxError};
|
||||||
use utf8_iter::ErrorReportingUtf8Chars;
|
use utf8_iter::ErrorReportingUtf8Chars;
|
||||||
@ -296,14 +296,13 @@ impl<T> Trace<T> for SourceResult<T> {
|
|||||||
let Some(trace_range) = world.range(span) else { return errors };
|
let Some(trace_range) = world.range(span) else { return errors };
|
||||||
for error in errors.make_mut().iter_mut() {
|
for error in errors.make_mut().iter_mut() {
|
||||||
// Skip traces that surround the error.
|
// Skip traces that surround the error.
|
||||||
if let Some(error_range) = world.range(error.span) {
|
if let Some(error_range) = world.range(error.span)
|
||||||
if error.span.id() == span.id()
|
&& error.span.id() == span.id()
|
||||||
&& trace_range.start <= error_range.start
|
&& trace_range.start <= error_range.start
|
||||||
&& trace_range.end >= error_range.end
|
&& trace_range.end >= error_range.end
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
error.trace.push(Spanned::new(make_point(), span));
|
error.trace.push(Spanned::new(make_point(), span));
|
||||||
}
|
}
|
||||||
@ -839,7 +838,9 @@ pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError
|
|||||||
let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
|
let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
|
||||||
let message = match error {
|
let message = match error {
|
||||||
roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
|
roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
|
||||||
eco_format!("failed to parse {format} (found closing tag '{actual}' instead of '{expected}')")
|
eco_format!(
|
||||||
|
"failed to parse {format} (found closing tag '{actual}' instead of '{expected}')"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
roxmltree::Error::UnknownEntityReference(entity, _) => {
|
roxmltree::Error::UnknownEntityReference(entity, _) => {
|
||||||
eco_format!("failed to parse {format} (unknown entity '{entity}')")
|
eco_format!("failed to parse {format} (unknown entity '{entity}')")
|
||||||
|
@ -8,11 +8,11 @@ use ecow::EcoVec;
|
|||||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||||
use typst_syntax::{FileId, Span};
|
use typst_syntax::{FileId, Span};
|
||||||
|
|
||||||
use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
use crate::World;
|
||||||
|
use crate::diag::{HintedStrResult, SourceDiagnostic, SourceResult, StrResult, bail};
|
||||||
use crate::foundations::{Styles, Value};
|
use crate::foundations::{Styles, Value};
|
||||||
use crate::introspection::Introspector;
|
use crate::introspection::Introspector;
|
||||||
use crate::routines::Routines;
|
use crate::routines::Routines;
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// Holds all data needed during compilation.
|
/// Holds all data needed during compilation.
|
||||||
pub struct Engine<'a> {
|
pub struct Engine<'a> {
|
||||||
@ -47,7 +47,11 @@ impl Engine<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Runs tasks on the engine in parallel.
|
/// Runs tasks on the engine in parallel.
|
||||||
pub fn parallelize<P, I, T, U, F>(&mut self, iter: P, f: F) -> impl Iterator<Item = U>
|
pub fn parallelize<P, I, T, U, F>(
|
||||||
|
&mut self,
|
||||||
|
iter: P,
|
||||||
|
f: F,
|
||||||
|
) -> impl Iterator<Item = U> + use<P, I, T, U, F>
|
||||||
where
|
where
|
||||||
P: IntoIterator<IntoIter = I>,
|
P: IntoIterator<IntoIter = I>,
|
||||||
I: Iterator<Item = T>,
|
I: Iterator<Item = T>,
|
||||||
@ -111,11 +115,7 @@ impl Traced {
|
|||||||
/// We hide the span if it isn't in the given file so that only results for
|
/// We hide the span if it isn't in the given file so that only results for
|
||||||
/// the file with the traced span are invalidated.
|
/// the file with the traced span are invalidated.
|
||||||
pub fn get(&self, id: FileId) -> Option<Span> {
|
pub fn get(&self, id: FileId) -> Option<Span> {
|
||||||
if self.0.and_then(Span::id) == Some(id) {
|
if self.0.and_then(Span::id) == Some(id) { self.0 } else { None }
|
||||||
self.0
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
|
|
||||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
use ecow::{EcoString, EcoVec, eco_format, eco_vec};
|
||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
|
|
||||||
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult};
|
use crate::diag::{At, SourceDiagnostic, SourceResult, StrResult, bail, error};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value,
|
Array, Dict, FromValue, IntoValue, Repr, Str, Value, cast, func, repr, scope, ty,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Captured arguments to a function.
|
/// Captured arguments to a function.
|
||||||
|
@ -4,16 +4,16 @@ use std::num::{NonZeroI64, NonZeroUsize};
|
|||||||
use std::ops::{Add, AddAssign};
|
use std::ops::{Add, AddAssign};
|
||||||
|
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::{eco_format, EcoString, EcoVec};
|
use ecow::{EcoString, EcoVec, eco_format};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
|
|
||||||
use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
use crate::diag::{At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult, bail};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue,
|
Args, Bytes, CastInfo, Context, Dict, FromValue, Func, IntoValue, Reflect, Repr, Str,
|
||||||
Func, IntoValue, Reflect, Repr, Str, Value, Version,
|
Value, Version, cast, func, ops, repr, scope, ty,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Create a new [`Array`] from values.
|
/// Create a new [`Array`] from values.
|
||||||
|
@ -4,8 +4,8 @@ use ecow::EcoString;
|
|||||||
|
|
||||||
use crate::diag::HintedStrResult;
|
use crate::diag::HintedStrResult;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type,
|
CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type,
|
||||||
Value,
|
Value, ty,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A value that indicates a smart default.
|
/// A value that indicates a smart default.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
|
|
||||||
use crate::foundations::{ty, Repr};
|
use crate::foundations::{Repr, ty};
|
||||||
|
|
||||||
/// A type with two states.
|
/// A type with two states.
|
||||||
///
|
///
|
||||||
|
@ -5,13 +5,13 @@ use std::ops::{Add, AddAssign, Deref};
|
|||||||
use std::str::Utf8Error;
|
use std::str::Utf8Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use typst_syntax::Lines;
|
use typst_syntax::Lines;
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use crate::diag::{bail, StrResult};
|
use crate::diag::{StrResult, bail};
|
||||||
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
|
use crate::foundations::{Array, Reflect, Repr, Str, Value, cast, func, scope, ty};
|
||||||
|
|
||||||
/// A sequence of bytes.
|
/// A sequence of bytes.
|
||||||
///
|
///
|
||||||
|
@ -7,8 +7,8 @@ use az::SaturatingAs;
|
|||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
use typst_utils::{round_int_with_precision, round_with_precision};
|
use typst_utils::{round_int_with_precision, round_with_precision};
|
||||||
|
|
||||||
use crate::diag::{bail, At, HintedString, SourceResult, StrResult};
|
use crate::diag::{At, HintedString, SourceResult, StrResult, bail};
|
||||||
use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value};
|
use crate::foundations::{Decimal, IntoValue, Module, Scope, Value, cast, func, ops};
|
||||||
use crate::layout::{Angle, Fr, Length, Ratio};
|
use crate::layout::{Angle, Fr, Length, Ratio};
|
||||||
|
|
||||||
/// A module with calculation definitions.
|
/// A module with calculation definitions.
|
||||||
|
@ -14,7 +14,7 @@ use unicode_math_class::MathClass;
|
|||||||
|
|
||||||
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
|
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value,
|
Fold, NativeElement, Packed, Repr, Str, Type, Value, array, repr,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Determine details of a type.
|
/// Determine details of a type.
|
||||||
@ -347,14 +347,15 @@ impl CastInfo {
|
|||||||
msg.hint(eco_format!("use `label({})` to create a label", s.repr()));
|
msg.hint(eco_format!("use `label({})` to create a label", s.repr()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Value::Decimal(_) = found {
|
} else if let Value::Decimal(_) = found
|
||||||
if !matching_type && parts.iter().any(|p| p == "float") {
|
&& !matching_type
|
||||||
|
&& parts.iter().any(|p| p == "float")
|
||||||
|
{
|
||||||
msg.hint(eco_format!(
|
msg.hint(eco_format!(
|
||||||
"if loss of precision is acceptable, explicitly cast the \
|
"if loss of precision is acceptable, explicitly cast the \
|
||||||
decimal to a float with `float(value)`"
|
decimal to a float with `float(value)`"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
msg
|
msg
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,8 @@ use typst_utils::Static;
|
|||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, Args, Content, ContentVtable, FieldAccessError, Func, ParamInfo, Repr, Scope,
|
Args, Content, ContentVtable, FieldAccessError, Func, ParamInfo, Repr, Scope,
|
||||||
Selector, StyleChain, Styles, Value,
|
Selector, StyleChain, Styles, Value, cast,
|
||||||
};
|
};
|
||||||
use crate::text::{Lang, Region};
|
use crate::text::{Lang, Region};
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ use std::hash::Hash;
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
|
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
Container, Content, FieldVtable, Fold, FoldFn, IntoValue, NativeElement, Packed,
|
Container, Content, FieldVtable, Fold, FoldFn, IntoValue, NativeElement, Packed,
|
||||||
|
@ -17,7 +17,7 @@ use std::iter::{self, Sum};
|
|||||||
use std::ops::{Add, AddAssign, ControlFlow};
|
use std::ops::{Add, AddAssign, ControlFlow};
|
||||||
|
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{EcoString, eco_format};
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
@ -26,8 +26,8 @@ use typst_utils::singleton;
|
|||||||
use crate::diag::{SourceResult, StrResult};
|
use crate::diag::{SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
func, repr, scope, ty, Context, Dict, IntoValue, Label, Property, Recipe,
|
Context, Dict, IntoValue, Label, Property, Recipe, RecipeIndex, Repr, Selector, Str,
|
||||||
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
|
Style, StyleChain, Styles, Value, func, repr, scope, ty,
|
||||||
};
|
};
|
||||||
use crate::introspection::Location;
|
use crate::introspection::Location;
|
||||||
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
|
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
|
||||||
@ -174,11 +174,11 @@ impl Content {
|
|||||||
id: u8,
|
id: u8,
|
||||||
styles: Option<StyleChain>,
|
styles: Option<StyleChain>,
|
||||||
) -> Result<Value, FieldAccessError> {
|
) -> Result<Value, FieldAccessError> {
|
||||||
if id == 255 {
|
if id == 255
|
||||||
if let Some(label) = self.label() {
|
&& let Some(label) = self.label()
|
||||||
|
{
|
||||||
return Ok(label.into_value());
|
return Ok(label.into_value());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match self.0.handle().field(id) {
|
match self.0.handle().field(id) {
|
||||||
Some(handle) => match styles {
|
Some(handle) => match styles {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user