Compare commits

...

45 Commits

Author SHA1 Message Date
mkorje
8c811110f3
Add delim-size parameter to mat, vec, and cases
Takes either a function or a relative length, just like with `lr`,
`stretch`, and `accent` which was changed in the previous two commits.
The default is now much clearer to the user: `x => x * 1.1 - 0.1em`.
2025-07-23 17:55:19 +10:00
mkorje
5738c98ff5
Allow a function as an argument to size in accent
The short fall is now only applied in the default for `accent`
(`x => x - 0.5em`).
2025-07-23 17:53:13 +10:00
mkorje
24bac30c84
Allow a function as an argument to size in stretch and lr
Previously there was always a short fall when scaling delimiters, even if
the user requested a specific size. This is no longer the case; the short
fall is only present in the default for `lr` (`x => x - 0.1em`) - the
size of the delimiters is now actually what was specified in the size
argument. This also makes the default for `lr` much clearer to the user.

A slight hack was used by exploiting the `name` property in the `func`
attribute macro so that the default value in the docs for `lr.size` would
clearly show what the default function was (instead of just its name
`default_lr_size` which is meaningless and inaccessible to the user).
2025-07-23 17:50:58 +10:00
Laurenz
78355421ad
Add pdf extension to image autocompletions (#6643) 2025-07-22 12:07:29 +00:00
Laurenz Stampfl
af2253ba16
Add support for PDF embedding (#6623)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-22 11:06:44 +00:00
Laurenz
b036fd97ab Reformat with 2024 edition 2025-07-21 15:22:29 +02:00
Laurenz
e81a5a6ef2 Yeet if_chain macro 2025-07-21 15:22:29 +02:00
Laurenz
c9c2315ad3 Fix 2024 clippy warnings 2025-07-21 15:22:29 +02:00
Laurenz
4bbd4e195b Migrate to 2024 edition 2025-07-21 15:22:28 +02:00
Laurenz
eed75ca4d6 Bump MSRV to 1.88 2025-07-21 15:22:28 +02:00
Laurenz
a43b7e785c Bump CI Rust to 1.88 2025-07-21 13:48:48 +02:00
pog102
55dad02887
Add Lithuanian translations (#6587) 2025-07-21 10:57:40 +00:00
Erik
b790c6d59c
Add rust-analyzer to flake devShell (#6618) 2025-07-18 14:36:10 +00:00
Malo
b1c79b50d4
Fix documentation oneliners (#6608) 2025-07-18 13:25:17 +00:00
Patrick Massot
4629ede020
Mention Tinymist in README.md (#6601) 2025-07-18 13:21:36 +00:00
Lachlan Kermode
627f5b9d4f
Add show rule for smallcaps in HTML (#6600) 2025-07-17 16:09:13 +00:00
Robin
5661c20580
Slightly improve selector docs (#6544) 2025-07-16 16:15:49 +00:00
Laurenz
7897e86bcc
Restore timing scopes for native show rules (#6616) 2025-07-16 09:54:43 +00:00
Laurenz
8e0e0f1a3b
Bump zip dependency (#6615) 2025-07-16 09:12:38 +00:00
Laurenz
0a4b72f8f6
Partially automate span assignment in native show rule (#6613) 2025-07-16 08:55:06 +00:00
Laurenz
c58766440c
Support intra-doc links in HTML (#6602) 2025-07-16 08:17:42 +00:00
Y.D.X.
ea5272bb2b
Support setting fonts repeatedly with different covers (#6604) 2025-07-16 08:10:21 +00:00
Malo
cdbf60e883
Change enum.item.number to Smart instead of Option (#6609) 2025-07-16 08:05:52 +00:00
Laurenz
9a6268050f
HTML frame improvements (#6605) 2025-07-15 14:48:31 +00:00
Robin
f51cb4b03e
Rephrase docs for truncation of float/decimal to integer (#6543) 2025-07-14 13:31:27 +00:00
Robin
0c12828c9a
Fix minor typo in text docs (#6589) 2025-07-14 13:26:25 +00:00
Robin
b1a091a236
Use "whitespace" instead of "space" to denote block-level equation in docs (#6591) 2025-07-14 13:26:10 +00:00
Said A.
0264534928
Fix regression in reference autocomplete (#6586) 2025-07-10 15:02:23 +00:00
Said A.
70710deb2b
Deduplicate labels for code completion (#6516) 2025-07-10 13:15:19 +00:00
Laurenz
275012d7c6
Handle lower and upper in HTML export (#6585) 2025-07-10 10:54:06 +00:00
Laurenz
98802dde7e
Complete movement of HTML export code to typst-html (#6584) 2025-07-10 10:42:34 +00:00
Laurenz
ac77fdbb6e
Fix tooltip for figure reference (#6580) 2025-07-09 13:50:54 +00:00
Laurenz
3aa7e861e7
Support images in HTML export (#6578) 2025-07-09 13:48:43 +00:00
Laurenz
a45c3388a6
More consistent Packed<T> to Content conversion methods (#6579) 2025-07-09 13:40:22 +00:00
Max
f9b01f595d
Move math styling to codex and add math.scr (#6309) 2025-07-09 13:08:49 +00:00
Robin
eed3407051
Update Swedish translations based on defaults used for LaTeX and cleveref (#6519) 2025-07-09 12:44:42 +00:00
Jassiel Ovando
1bbb58c43f
Add completions subcommand (#6568) 2025-07-09 12:41:40 +00:00
Malo
1dc4c248d1
Add default argument for str.first and str.last (#6554)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-09 12:10:24 +00:00
frozolotl
9e6adb6f45
Ignore spans when checking for RawElem equality (#6560) 2025-07-09 12:04:22 +00:00
Robin
4534167656
Use "displayed" instead of "repeated" to avoid ambiguity in numbering docs (#6565) 2025-07-09 12:02:50 +00:00
Laurenz
9ad1879e9d
Anti-alias clip paths (#6570) 2025-07-09 12:02:13 +00:00
Robin
e5e813219e
Fix typo of Typst domain in quote docs (#6573) 2025-07-09 12:01:57 +00:00
Laurenz
52a708b988
Move html module to typst-html crate (#6577) 2025-07-09 09:46:40 +00:00
Laurenz
e71674f6b3
Construct library via extension trait instead of default & inherent impl (#6576) 2025-07-09 09:28:26 +00:00
Laurenz
e5e1dcd9c0
Target-specific native show rules (#6569) 2025-07-09 08:16:36 +00:00
361 changed files with 6928 additions and 5552 deletions

View File

@ -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

View File

@ -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 }}

156
Cargo.lock generated
View File

@ -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",
] ]
@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -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=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a" 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",
@ -2971,8 +3029,12 @@ dependencies = [
name = "typst-html" name = "typst-html"
version = "0.13.1" version = "0.13.1"
dependencies = [ dependencies = [
"bumpalo",
"comemo", "comemo",
"ecow", "ecow",
"palette",
"time",
"typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-svg", "typst-svg",
@ -2987,7 +3049,6 @@ version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"if_chain",
"once_cell", "once_cell",
"pathdiff", "pathdiff",
"serde", "serde",
@ -3028,6 +3089,7 @@ version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bumpalo", "bumpalo",
"codex",
"comemo", "comemo",
"ecow", "ecow",
"hypher", "hypher",
@ -3058,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",
@ -3070,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",
@ -3168,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",
@ -3186,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",
@ -3584,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",
] ]
@ -3719,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]]
@ -3927,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",
@ -3942,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"

View File

@ -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 = "c1089b4" } 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"
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "a5428cb" } codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"
@ -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

View File

@ -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

View File

@ -29,6 +29,7 @@ typst-svg = { workspace = true }
typst-timing = { workspace = true } typst-timing = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
clap_complete = { workspace = true }
codespan-reporting = { workspace = true } codespan-reporting = { workspace = true }
color-print = { workspace = true } color-print = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }

View File

@ -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"]

View File

@ -7,6 +7,7 @@ use std::str::FromStr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::builder::{TypedValueParser, ValueParser}; use clap::builder::{TypedValueParser, ValueParser};
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint}; use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint};
use clap_complete::Shell;
use semver::Version; use semver::Version;
/// The character typically used to separate path components /// The character typically used to separate path components
@ -81,6 +82,9 @@ pub enum Command {
/// Self update the Typst CLI. /// Self update the Typst CLI.
#[cfg_attr(not(feature = "self-update"), clap(hide = true))] #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
Update(UpdateCommand), Update(UpdateCommand),
/// Generates shell completion scripts.
Completions(CompletionsCommand),
} }
/// Compiles an input file into a supported output format. /// Compiles an input file into a supported output format.
@ -198,6 +202,14 @@ pub struct UpdateCommand {
pub backup_path: Option<PathBuf>, pub backup_path: Option<PathBuf>,
} }
/// Generates shell completion scripts.
#[derive(Debug, Clone, Parser)]
pub struct CompletionsCommand {
/// The shell to generate completions for.
#[arg(value_enum)]
pub shell: Shell,
}
/// Arguments for compilation and watching. /// Arguments for compilation and watching.
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct CompileArgs { pub struct CompileArgs {

View File

@ -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")

View File

@ -0,0 +1,13 @@
use std::io::stdout;
use clap::CommandFactory;
use clap_complete::generate;
use crate::args::{CliArguments, CompletionsCommand};
/// Execute the completions command.
pub fn completions(command: &CompletionsCommand) {
let mut cmd = CliArguments::command();
let bin_name = cmd.get_name().to_string();
generate(command.shell, &mut cmd, bin_name, &mut stdout());
}

View File

@ -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.

View File

@ -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,
}; };

View File

@ -1,5 +1,6 @@
mod args; mod args;
mod compile; mod compile;
mod completions;
mod download; mod download;
mod fonts; mod fonts;
mod greet; mod greet;
@ -20,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;
@ -71,6 +72,7 @@ fn dispatch() -> HintedStrResult<()> {
Command::Query(command) => crate::query::query(command)?, Command::Query(command) => crate::query::query(command)?,
Command::Fonts(command) => crate::fonts::fonts(command), Command::Fonts(command) => crate::fonts::fonts(command),
Command::Update(command) => crate::update::update(command)?, Command::Update(command) => crate::update::update(command)?,
Command::Completions(command) => crate::completions::completions(command),
} }
Ok(()) Ok(())
@ -100,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;

View File

@ -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};

View File

@ -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()
} }

View File

@ -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;

View File

@ -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;

View File

@ -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};

View File

@ -5,14 +5,14 @@ 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};
use typst::syntax::{FileId, Lines, Source, VirtualPath}; use typst::syntax::{FileId, Lines, Source, VirtualPath};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
use typst::{Library, World}; use typst::{Library, LibraryExt, World};
use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage; use typst_kit::package::PackageStorage;
use typst_timing::timed; use typst_timing::timed;
@ -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));

View File

@ -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 }

View File

@ -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))

View File

@ -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};

View File

@ -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;

View File

@ -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)
} }

View File

@ -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;

View File

@ -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());
} }

View File

@ -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]

View File

@ -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())
} }

View File

@ -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);

View File

@ -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"
)) ))
} }
}
} }

View File

@ -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;

View File

@ -13,14 +13,18 @@ keywords = { workspace = true }
readme = { workspace = true } readme = { workspace = true }
[dependencies] [dependencies]
typst-assets = { workspace = true }
typst-library = { workspace = true } typst-library = { workspace = true }
typst-macros = { workspace = true } typst-macros = { workspace = true }
typst-syntax = { workspace = true } typst-syntax = { workspace = true }
typst-timing = { workspace = true } typst-timing = { workspace = true }
typst-utils = { workspace = true } typst-utils = { workspace = true }
typst-svg = { workspace = true } typst-svg = { workspace = true }
bumpalo = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
palette = { workspace = true }
time = { workspace = true }
[lints] [lints]
workspace = true workspace = true

View 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");

View 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,
}
}

View 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))
}

View File

@ -0,0 +1,178 @@
//! Conversion from Typst data types into CSS data types.
use std::fmt::{self, Display, Write};
use ecow::EcoString;
use typst_library::layout::{Length, Rel};
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
use typst_utils::Numeric;
/// A list of CSS properties with values.
#[derive(Debug, Default)]
pub struct Properties(EcoString);
impl Properties {
/// Creates an empty list.
pub fn new() -> Self {
Self::default()
}
/// Adds a new property to the list.
pub fn push(&mut self, property: &str, value: impl Display) {
if !self.0.is_empty() {
self.0.push_str("; ");
}
write!(&mut self.0, "{property}: {value}").unwrap();
}
/// Adds a new property in builder-style.
#[expect(unused)]
pub fn with(mut self, property: &str, value: impl Display) -> Self {
self.push(property, value);
self
}
/// Turns this into a string suitable for use as an inline `style`
/// attribute.
pub fn into_inline_styles(self) -> Option<EcoString> {
(!self.0.is_empty()).then_some(self.0)
}
}
pub fn rel(rel: Rel) -> impl Display {
typst_utils::display(move |f| match (rel.abs.is_zero(), rel.rel.is_zero()) {
(false, false) => {
write!(f, "calc({}% + {})", rel.rel.get(), length(rel.abs))
}
(true, false) => write!(f, "{}%", rel.rel.get()),
(_, true) => write!(f, "{}", length(rel.abs)),
})
}
pub fn length(length: Length) -> impl Display {
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
(false, false) => {
write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get())
}
(true, false) => write!(f, "{}em", length.em.get()),
(_, true) => write!(f, "{}pt", length.abs.to_pt()),
})
}
pub fn color(color: Color) -> impl Display {
typst_utils::display(move |f| match color {
Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()),
Color::Oklab(v) => oklab(f, v),
Color::Oklch(v) => oklch(f, v),
Color::LinearRgb(v) => linear_rgb(f, v),
Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()),
})
}
fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result {
write!(f, "oklab({} {} {}{})", percent(v.l), number(v.a), number(v.b), alpha(v.alpha))
}
fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result {
write!(
f,
"oklch({} {} {}deg{})",
percent(v.l),
number(v.chroma),
number(v.hue.into_degrees()),
alpha(v.alpha)
)
}
fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result {
if let Some(v) = rgb_to_8_bit_lossless(v) {
let (r, g, b, a) = v.into_components();
write!(f, "#{r:02x}{g:02x}{b:02x}")?;
if a != u8::MAX {
write!(f, "{a:02x}")?;
}
Ok(())
} else {
write!(
f,
"rgb({} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha)
)
}
}
/// Converts an f32 RGBA color to its 8-bit representation if the result is
/// [very close](is_very_close) to the original.
fn rgb_to_8_bit_lossless(
v: Rgb,
) -> Option<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
let l = v.into_format::<u8, u8>();
let h = l.into_format::<f32, f32>();
(is_very_close(v.red, h.red)
&& is_very_close(v.blue, h.blue)
&& is_very_close(v.green, h.green)
&& is_very_close(v.alpha, h.alpha))
.then_some(l)
}
fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result {
write!(
f,
"color(srgb-linear {} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha),
)
}
fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result {
write!(
f,
"hsl({}deg {} {}{})",
number(v.hue.into_degrees()),
percent(v.saturation),
percent(v.lightness),
alpha(v.alpha),
)
}
/// Displays an alpha component if it not 1.
fn alpha(value: f32) -> impl Display {
typst_utils::display(move |f| {
if !is_very_close(value, 1.0) {
write!(f, " / {}", percent(value))?;
}
Ok(())
})
}
/// Displays a rounded percentage.
///
/// For a percentage, two significant digits after the comma gives us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn percent(ratio: f32) -> impl Display {
typst_utils::display(move |f| {
write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2))
})
}
/// Rounds a number for display.
///
/// For a number between 0 and 1, four significant digits give us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn number(value: f32) -> impl Display {
typst_utils::round_with_precision(value as f64, 4)
}
/// Whether two component values are close enough that there is no
/// difference when encoding them with 12-bit. 12 bit is the highest
/// reasonable color bit depth found in the industry.
fn is_very_close(a: f32, b: f32) -> bool {
const MAX_BIT_DEPTH: u32 = 12;
const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32;
(a - b).abs() < EPS
}

View 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>),
}

View 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![],
}
}
}

View File

@ -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);
} }

View 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())
}

View File

@ -1,357 +1,118 @@
//! Typst's HTML exporter. //! Typst's HTML exporter.
mod attr;
mod charsets;
mod convert;
mod css;
mod document;
mod dom;
mod encode; mod encode;
mod fragment;
mod link;
mod rules;
mod tag;
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;
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::{Content, StyleChain, Target, TargetElem}; use typst_macros::elem;
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::World;
use typst_syntax::Span;
/// Produce an HTML document from content. /// Creates the module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.start_category(Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
crate::typed::define(&mut html);
Module::new("html", html)
}
/// 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>),
} }

View 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;
}
}
}

View File

@ -0,0 +1,456 @@
use std::num::NonZeroUsize;
use ecow::{EcoVec, eco_format};
use typst_library::diag::{At, warning};
use typst_library::foundations::{
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
};
use typst_library::introspection::{Counter, Locator};
use typst_library::layout::resolve::{Cell, CellGrid, Entry, table_to_cellgrid};
use typst_library::layout::{OuterVAlignment, Sizing};
use typst_library::model::{
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
RefElem, StrongElem, TableCell, TableElem, TermsElem,
};
use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
};
use typst_library::visualize::ImageElem;
use crate::{FrameElem, HtmlAttrs, HtmlElem, HtmlTag, attr, css, tag};
/// Registers show rules for the [HTML target](Target::Html).
pub fn register(rules: &mut NativeRuleMap) {
use Target::{Html, Paged};
// Model.
rules.register(Html, STRONG_RULE);
rules.register(Html, EMPH_RULE);
rules.register(Html, LIST_RULE);
rules.register(Html, ENUM_RULE);
rules.register(Html, TERMS_RULE);
rules.register(Html, LINK_RULE);
rules.register(Html, HEADING_RULE);
rules.register(Html, FIGURE_RULE);
rules.register(Html, FIGURE_CAPTION_RULE);
rules.register(Html, QUOTE_RULE);
rules.register(Html, REF_RULE);
rules.register(Html, CITE_GROUP_RULE);
rules.register(Html, TABLE_RULE);
// Text.
rules.register(Html, SUB_RULE);
rules.register(Html, SUPER_RULE);
rules.register(Html, UNDERLINE_RULE);
rules.register(Html, OVERLINE_RULE);
rules.register(Html, STRIKE_RULE);
rules.register(Html, HIGHLIGHT_RULE);
rules.register(Html, SMALLCAPS_RULE);
rules.register(Html, RAW_RULE);
rules.register(Html, RAW_LINE_RULE);
// Visualize.
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, _, _| Ok(HtmlElem::new(tag::strong).with_body(Some(elem.body.clone())).pack());
const EMPH_RULE: ShowFn<EmphElem> =
|elem, _, _| Ok(HtmlElem::new(tag::em).with_body(Some(elem.body.clone())).pack());
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(elem.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !elem.tight.get(styles) {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li)
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
.pack())
};
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
let mut ol = HtmlElem::new(tag::ol);
if elem.reversed.get(styles) {
ol = ol.with_attr(attr::reversed, "reversed");
}
if let Some(n) = elem.start.get(styles).custom() {
ol = ol.with_attr(attr::start, eco_format!("{n}"));
}
let body = Content::sequence(elem.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Smart::Custom(nr) = item.number.get(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !elem.tight.get(styles) {
body += ParbreakElem::shared();
}
li.with_body(Some(body)).pack().spanned(item.span())
}));
Ok(ol.with_body(Some(body)).pack())
};
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(elem.children.iter().flat_map(|item| {
// Text in wide term lists shall always turn into paragraphs.
let mut description = item.description.clone();
if !elem.tight.get(styles) {
description += ParbreakElem::shared();
}
[
HtmlElem::new(tag::dt)
.with_body(Some(item.term.clone()))
.pack()
.spanned(item.term.span()),
HtmlElem::new(tag::dd)
.with_body(Some(description))
.pack()
.spanned(item.description.span()),
]
}))))
.pack())
};
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
let href = match dest {
Destination::Url(url) => Some(url.clone().into_inner()),
Destination::Location(location) => {
let id = engine
.introspector
.html_id(location)
.cloned()
.ok_or("failed to determine link anchor")
.at(elem.span())?;
Some(eco_format!("#{id}"))
}
Destination::Position(_) => {
engine.sink.warn(warning!(
elem.span(),
"positional link was ignored during HTML export"
));
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| {
let span = elem.span();
let mut realized = elem.body.clone();
if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() {
let location = elem.location().unwrap();
let numbering = Counter::of(HeadingElem::ELEM)
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
realized = numbering + SpaceElem::shared().clone() + realized;
}
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
// reason, levels are offset by one: A Typst level 1 heading becomes
// a `<h2>`.
let level = elem.resolve_level(styles).get();
Ok(if level >= 6 {
engine.sink.warn(warning!(
span,
"heading of level {} was transformed to \
<div role=\"heading\" aria-level=\"{}\">, which is not \
supported by all assistive technology",
level, level + 1;
hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
hint: "you may want to restructure your document so that \
it doesn't contain deep headings"
));
HtmlElem::new(tag::div)
.with_body(Some(realized))
.with_attr(attr::role, "heading")
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
.pack()
} else {
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
HtmlElem::new(t).with_body(Some(realized)).pack()
})
};
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
let span = elem.span();
let mut realized = elem.body.clone();
// Build the caption, if any.
if let Some(caption) = elem.caption.get_cloned(styles) {
realized = match caption.position.get(styles) {
OuterVAlignment::Top => caption.pack() + realized,
OuterVAlignment::Bottom => realized + caption.pack(),
};
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
Ok(HtmlElem::new(tag::figure).with_body(Some(realized)).pack())
};
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
Ok(HtmlElem::new(tag::figcaption)
.with_body(Some(elem.realize(engine, styles)?))
.pack())
};
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
let span = elem.span();
let block = elem.block.get(styles);
let mut realized = elem.body.clone();
if elem.quotes.get(styles).unwrap_or(!block) {
realized = QuoteElem::quoted(realized, styles);
}
let attribution = elem.attribution.get_ref(styles);
if block {
let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized));
if let Some(Attribution::Content(attribution)) = attribution
&& let Some(link) = attribution.to_packed::<LinkElem>()
&& let LinkTarget::Dest(Destination::Url(url)) = &link.dest
{
blockquote = blockquote.with_attr(attr::cite, url.clone().into_inner());
}
realized = blockquote.pack().spanned(span);
if let Some(attribution) = attribution.as_ref() {
realized += attribution.realize(span);
}
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone();
realized += CiteElem::new(*label).pack().spanned(span);
}
Ok(realized)
};
const REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
const TABLE_RULE: ShowFn<TableElem> = |elem, engine, styles| {
// The locator is not used by HTML export, so we can just fabricate one.
let locator = Locator::root();
Ok(show_cellgrid(table_to_cellgrid(elem, engine, locator, styles)?, styles))
};
fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
let tr = |tag, row: &[Entry]| {
let row = row
.iter()
.flat_map(|entry| entry.as_cell())
.map(|cell| show_cell(tag, cell, styles));
elem(tag::tr, Content::sequence(row))
};
// TODO(subfooters): similarly to headers, take consecutive footers from
// the end for 'tfoot'.
let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
});
// Store all consecutive headers at the start in 'thead'. All remaining
// headers are just 'th' rows across the table body.
let mut consecutive_header_end = 0;
let first_mid_table_header = grid
.headers
.iter()
.take_while(|hd| {
let is_consecutive = hd.range.start == consecutive_header_end;
consecutive_header_end = hd.range.end;
is_consecutive
})
.count();
let (y_offset, header) = if first_mid_table_header > 0 {
let removed_header_rows =
grid.headers.get(first_mid_table_header - 1).unwrap().range.end;
let rows = rows.drain(..removed_header_rows);
(
removed_header_rows,
Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))),
)
} else {
(0, None)
};
// TODO: Consider improving accessibility properties of multi-level headers
// inside tables in the future, e.g. indicating which columns they are
// relative to and so on. See also:
// https://www.w3.org/WAI/tutorials/tables/multi-level/
let mut next_header = first_mid_table_header;
let mut body =
Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| {
let y = relative_y + y_offset;
if let Some(current_header) =
grid.headers.get(next_header).filter(|h| h.range.contains(&y))
{
if y + 1 == current_header.range.end {
next_header += 1;
}
tr(tag::th, row)
} else {
tr(tag::td, row)
}
}));
if header.is_some() || footer.is_some() {
body = elem(tag::tbody, body);
}
let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
elem(tag::table, Content::sequence(content))
}
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
let cell = cell.body.clone();
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
let mut attrs = HtmlAttrs::new();
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
if let Some(colspan) = span(cell.colspan.get(styles)) {
attrs.push(attr::colspan, colspan);
}
if let Some(rowspan) = span(cell.rowspan.get(styles)) {
attrs.push(attr::rowspan, rowspan);
}
HtmlElem::new(tag)
.with_body(Some(cell.body.clone()))
.with_attrs(attrs)
.pack()
.spanned(cell.span())
}
const SUB_RULE: ShowFn<SubElem> =
|elem, _, _| Ok(HtmlElem::new(tag::sub).with_body(Some(elem.body.clone())).pack());
const SUPER_RULE: ShowFn<SuperElem> =
|elem, _, _| Ok(HtmlElem::new(tag::sup).with_body(Some(elem.body.clone())).pack());
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
// Note: In modern HTML, `<u>` is not the underline element, but
// rather an "Unarticulated Annotation" element (see HTML spec
// 4.5.22). Using `text-decoration` instead is recommended by MDN.
Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: underline")
.with_body(Some(elem.body.clone()))
.pack())
};
const OVERLINE_RULE: ShowFn<OverlineElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: overline")
.with_body(Some(elem.body.clone()))
.pack())
};
const STRIKE_RULE: ShowFn<StrikeElem> =
|elem, _, _| Ok(HtmlElem::new(tag::s).with_body(Some(elem.body.clone())).pack());
const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|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| {
let lines = elem.lines.as_deref().unwrap_or_default();
let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1));
for (i, line) in lines.iter().enumerate() {
if i != 0 {
seq.push(LinebreakElem::shared().clone());
}
seq.push(line.clone().pack());
}
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
.with_body(Some(Content::sequence(seq)))
.pack())
};
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
const IMAGE_RULE: ShowFn<ImageElem> = |elem, engine, styles| {
let image = elem.decode(engine, styles)?;
let mut attrs = HtmlAttrs::new();
attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image));
if let Some(alt) = elem.alt.get_cloned(styles) {
attrs.push(attr::alt, alt);
}
let mut inline = css::Properties::new();
// TODO: Exclude in semantic profile.
if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) {
inline.push("image-rendering", value);
}
// TODO: Exclude in semantic profile?
match elem.width.get(styles) {
Smart::Auto => {}
Smart::Custom(rel) => inline.push("width", css::rel(rel)),
}
// TODO: Exclude in semantic profile?
match elem.height.get(styles) {
Sizing::Auto => {}
Sizing::Rel(rel) => inline.push("height", css::rel(rel)),
Sizing::Fr(_) => {}
}
Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack())
};

View 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
)
}

View File

@ -9,21 +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_macros::cast; use typst_library::diag::{At, Hint, HintedStrResult, SourceResult, bail};
use typst_library::engine::Engine;
use crate::diag::{bail, At, Hint, HintedStrResult, SourceResult}; use typst_library::foundations::{
use crate::engine::Engine;
use crate::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 crate::html::tag; use typst_library::layout::{Axes, Axis, Dir, Length};
use crate::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; use typst_library::visualize::Color;
use crate::layout::{Axes, Axis, Dir, Length}; use typst_macros::cast;
use crate::visualize::Color;
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) {
@ -705,153 +704,6 @@ impl IntoAttr for SourceSize {
} }
} }
/// Conversion from Typst data types into CSS data types.
///
/// This can be moved elsewhere once we start supporting more CSS stuff.
mod css {
use std::fmt::{self, Display};
use typst_utils::Numeric;
use crate::layout::Length;
use crate::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
pub fn length(length: Length) -> impl Display {
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
(false, false) => {
write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get())
}
(true, false) => write!(f, "{}em", length.em.get()),
(_, true) => write!(f, "{}pt", length.abs.to_pt()),
})
}
pub fn color(color: Color) -> impl Display {
typst_utils::display(move |f| match color {
Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()),
Color::Oklab(v) => oklab(f, v),
Color::Oklch(v) => oklch(f, v),
Color::LinearRgb(v) => linear_rgb(f, v),
Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()),
})
}
fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result {
write!(
f,
"oklab({} {} {}{})",
percent(v.l),
number(v.a),
number(v.b),
alpha(v.alpha)
)
}
fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result {
write!(
f,
"oklch({} {} {}deg{})",
percent(v.l),
number(v.chroma),
number(v.hue.into_degrees()),
alpha(v.alpha)
)
}
fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result {
if let Some(v) = rgb_to_8_bit_lossless(v) {
let (r, g, b, a) = v.into_components();
write!(f, "#{r:02x}{g:02x}{b:02x}")?;
if a != u8::MAX {
write!(f, "{a:02x}")?;
}
Ok(())
} else {
write!(
f,
"rgb({} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha)
)
}
}
/// Converts an f32 RGBA color to its 8-bit representation if the result is
/// [very close](is_very_close) to the original.
fn rgb_to_8_bit_lossless(
v: Rgb,
) -> Option<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
let l = v.into_format::<u8, u8>();
let h = l.into_format::<f32, f32>();
(is_very_close(v.red, h.red)
&& is_very_close(v.blue, h.blue)
&& is_very_close(v.green, h.green)
&& is_very_close(v.alpha, h.alpha))
.then_some(l)
}
fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result {
write!(
f,
"color(srgb-linear {} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha),
)
}
fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result {
write!(
f,
"hsl({}deg {} {}{})",
number(v.hue.into_degrees()),
percent(v.saturation),
percent(v.lightness),
alpha(v.alpha),
)
}
/// Displays an alpha component if it not 1.
fn alpha(value: f32) -> impl Display {
typst_utils::display(move |f| {
if !is_very_close(value, 1.0) {
write!(f, " / {}", percent(value))?;
}
Ok(())
})
}
/// Displays a rounded percentage.
///
/// For a percentage, two significant digits after the comma gives us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn percent(ratio: f32) -> impl Display {
typst_utils::display(move |f| {
write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2))
})
}
/// Rounds a number for display.
///
/// For a number between 0 and 1, four significant digits give us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn number(value: f32) -> impl Display {
typst_utils::round_with_precision(value as f64, 4)
}
/// Whether two component values are close enough that there is no
/// difference when encoding them with 12-bit. 12 bit is the highest
/// reasonable color bit depth found in the industry.
fn is_very_close(a: f32, b: f32) -> bool {
const MAX_BIT_DEPTH: u32 = 12;
const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32;
(a - b).abs() < EPS
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -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 }

View File

@ -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; 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,17 +69,30 @@ 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
.get_by_name("caption") .to_packed::<FigureElem>()
.or_else(|_| elem.get_by_name("body")) .and_then(|figure| match figure.caption.as_option() {
Some(Some(caption)) => Some(caption.pack_ref()),
_ => None,
})
.unwrap_or(elem)
.get_by_name("body")
.ok() .ok()
.and_then(|field| match field { .and_then(|field| match field {
Value::Content(content) => Some(content), Value::Content(content) => Some(content),

View File

@ -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, &param); named_param_value_completions(ctx, callee, &param);
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]

View File

@ -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>);

View File

@ -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 {

View File

@ -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 {

View File

@ -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>() {

View File

@ -9,8 +9,8 @@ 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, World}; use typst::{Feature, Library, LibraryExt, World};
use crate::IdeWorld; use crate::IdeWorld;

View File

@ -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(&param.input, &string.get())
if let Some(docs) = find_string_doc(&param.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>;
@ -378,4 +367,9 @@ mod tests {
.with_source("other.typ", "#let f = (x) => 1"); .with_source("other.typ", "#let f = (x) => 1");
test(&world, -4, Side::After).must_be_code("(..) => .."); test(&world, -4, Side::After).must_be_code("(..) => ..");
} }
#[test]
fn test_tooltip_reference() {
test("#figure(caption: [Hi])[]<f> @f", -1, Side::Before).must_be_text("Hi");
}
} }

View File

@ -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

View File

@ -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}"))));
} }
}; };

View File

@ -21,6 +21,7 @@ typst-timing = { workspace = true }
typst-utils = { workspace = true } typst-utils = { workspace = true }
az = { workspace = true } az = { workspace = true }
bumpalo = { workspace = true } bumpalo = { workspace = true }
codex = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
hypher = { workspace = true } hypher = { workspace = true }

View File

@ -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

View File

@ -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()));

View File

@ -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.

View File

@ -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,

View File

@ -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();

View File

@ -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,

View File

@ -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};

View File

@ -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());

View File

@ -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;
} }
}
} }

View File

@ -1,18 +1,11 @@
use std::ffi::OsStr; use typst_library::diag::SourceResult;
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
}; };
use typst_library::loading::DataSource; use typst_library::visualize::{Curve, Image, ImageElem, ImageFit};
use typst_library::text::families;
use typst_library::visualize::{
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
RasterImage, SvgImage, VectorFormat,
};
/// Layout the image. /// Layout the image.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
@ -23,53 +16,7 @@ pub fn layout_image(
styles: StyleChain, styles: StyleChain,
region: Region, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let span = elem.span(); let image = elem.decode(engine, styles)?;
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let Derived { source, derived: loaded } = &elem.source;
let format = match elem.format.get(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(source, &loaded.data).at(span)?,
};
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
if has_foreign_object {
engine.sink.warn(warning!(
span,
"image contains foreign object";
hint: "SVG images with foreign objects might render incorrectly in typst";
hint: "see https://github.com/typst/typst/issues/1421 for more information"
));
}
}
// Construct the image itself.
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
loaded.data.clone(),
format,
elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
loaded.data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.within(loaded)?,
),
};
let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles));
// Determine the image's pixel aspect ratio. // Determine the image's pixel aspect ratio.
let pxw = image.width(); let pxw = image.width();
@ -122,7 +69,7 @@ pub fn layout_image(
// the frame to the target size, center aligning the image in the // the frame to the target size, center aligning the image in the
// process. // process.
let mut frame = Frame::soft(fitted); let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); frame.push(Point::zero(), FrameItem::Image(image, fitted, elem.span()));
frame.resize(target, Axes::splat(FixedAlignment::Center)); frame.resize(target, Axes::splat(FixedAlignment::Center));
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
@ -132,25 +79,3 @@ pub fn layout_image(
Ok(frame) Ok(frame)
} }
/// Try to determine the image format based on the data.
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
if let DataSource::Path(path) = source {
let ext = std::path::Path::new(path.as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
match ext.as_str() {
"png" => return Ok(ExchangeFormat::Png.into()),
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
"webp" => return Ok(ExchangeFormat::Webp.into()),
_ => {}
}
}
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
}

View File

@ -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));
} }

View File

@ -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

View File

@ -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
}
} }
} }

View File

@ -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;

View File

@ -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();

View File

@ -10,21 +10,11 @@ mod modifiers;
mod pad; mod pad;
mod pages; mod pages;
mod repeat; mod repeat;
mod rules;
mod shapes; mod shapes;
mod stack; mod stack;
mod transforms; mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::flow::{layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image;
pub use self::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad;
pub use self::pages::layout_document; pub use self::pages::layout_document;
pub use self::repeat::layout_repeat; pub use self::rules::register;
pub use self::shapes::{
layout_circle, layout_curve, layout_ellipse, layout_line, layout_path,
layout_polygon, layout_rect, layout_square,
};
pub use self::stack::layout_stack;
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};

View File

@ -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);

View File

@ -1,16 +1,13 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::layout::{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, GlyphFragment, MathContext, MathFragment, style_cramped, style_dtls,
MathFragment, style_flac,
}; };
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// Lays out an [`AccentElem`]. /// Lays out an [`AccentElem`].
#[typst_macros::time(name = "math.accent", span = elem.span())] #[typst_macros::time(name = "math.accent", span = elem.span())]
pub fn layout_accent( pub fn layout_accent(
@ -45,11 +42,8 @@ pub fn layout_accent(
let mut glyph = let mut glyph =
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;
// Forcing the accent to be at least as large as the base makes it too wide let width = elem.size.get_ref(styles).resolve(ctx.engine, styles, base.width())?;
// in many cases. glyph.stretch_horizontal(ctx, width);
let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
glyph.stretch_horizontal(ctx, width - short_fall);
let accent_attach = glyph.accent_attach.0; let accent_attach = glyph.accent_attach.0;
let accent = glyph.into_frame(); let accent = glyph.into_frame();

View File

@ -1,14 +1,15 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Size};
use typst_library::math::{ use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
StretchSize,
}; };
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 {
@ -66,12 +67,12 @@ pub fn layout_attach(
let relative_to_width = measure!(t, width).max(measure!(b, width)); let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment( stretch_fragment(
ctx, ctx,
styles,
&mut base, &mut base,
Some(Axis::X), Some(Axis::X),
Some(relative_to_width), Some(relative_to_width),
stretch, &stretch,
Abs::zero(), )?;
);
} }
let fragments = [ let fragments = [
@ -154,7 +155,7 @@ pub fn layout_limits(
} }
/// Get the size to stretch the base to. /// Get the size to stretch the base to.
fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> { fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<StretchSize> {
// Extract from an EquationElem. // Extract from an EquationElem.
let mut base = &elem.base; let mut base = &elem.base;
while let Some(equation) = base.to_packed::<EquationElem>() { while let Some(equation) = base.to_packed::<EquationElem>() {
@ -162,7 +163,7 @@ fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs
} }
base.to_packed::<StretchElem>() base.to_packed::<StretchElem>()
.map(|stretch| stretch.size.resolve(styles)) .map(|stretch| stretch.size.get_cloned(styles))
} }
/// Lay out the attachments. /// Lay out the attachments.

View File

@ -1,14 +1,13 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
use typst_library::math::{BinomElem, FracElem}; use typst_library::math::{BinomElem, DELIM_SHORT_FALL, FracElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry}; 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, FrameFragment, GlyphFragment, MathContext, style_for_denominator, style_for_numerator,
MathContext, DELIM_SHORT_FALL,
}; };
const FRAC_AROUND: Em = Em::new(0.1); const FRAC_AROUND: Em = Em::new(0.1);
@ -49,7 +48,7 @@ fn layout_frac_like(
binom: bool, binom: bool,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles); let short_fall = DELIM_SHORT_FALL.abs().resolve(styles);
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
let thickness = scaled!(ctx, styles, fraction_rule_thickness); let thickness = scaled!(ctx, styles, fraction_rule_thickness);
let shift_up = scaled!( let shift_up = scaled!(

View File

@ -2,18 +2,18 @@ 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::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, features, language};
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;
@ -681,7 +681,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 +814,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)

View File

@ -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};
use typst_library::layout::{Abs, Axis, Rel}; use typst_library::layout::{Abs, Axis};
use typst_library::math::{EquationElem, LrElem, MidElem}; use typst_library::math::{EquationElem, LrElem, MidElem, StretchSize};
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::{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_ref(styles).is_lr_default()
{
body = &lr.body; body = &lr.body;
} }
}
let mut fragments = ctx.layout_into_fragments(body, styles)?; let mut fragments = ctx.layout_into_fragments(body, styles)?;
@ -41,25 +41,39 @@ pub fn layout_lr(
.unwrap_or_default(); .unwrap_or_default();
let relative_to = 2.0 * max_extent; let relative_to = 2.0 * max_extent;
let height = elem.size.resolve(styles); let height = elem.size.get_ref(styles);
// Scale up fragments at both ends. // Scale up fragments at both ends.
match inner_fragments { match inner_fragments {
[one] => scale_if_delimiter(ctx, one, relative_to, height, None), [one] => scale_if_delimiter(ctx, styles, one, relative_to, height, None)?,
[first, .., last] => { [first, .., last] => {
scale_if_delimiter(ctx, first, relative_to, height, Some(MathClass::Opening)); scale_if_delimiter(
scale_if_delimiter(ctx, last, relative_to, height, Some(MathClass::Closing)); ctx,
styles,
first,
relative_to,
height,
Some(MathClass::Opening),
)?;
scale_if_delimiter(
ctx,
styles,
last,
relative_to,
height,
Some(MathClass::Closing),
)?;
} }
[] => {} [] => {}
} }
// 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, styles, fragment, relative_to, height)?;
}
} }
} }
@ -95,7 +109,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;
} }
@ -112,32 +126,32 @@ pub fn layout_mid(
/// it is a delimiter, in a way that cannot be overridden by the user. /// it is a delimiter, in a way that cannot be overridden by the user.
fn scale_if_delimiter( fn scale_if_delimiter(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment, fragment: &mut MathFragment,
relative_to: Abs, relative_to: Abs,
height: Rel<Abs>, height: &StretchSize,
apply: Option<MathClass>, apply: Option<MathClass>,
) { ) -> SourceResult<()> {
if matches!( if matches!(
fragment.class(), fragment.class(),
MathClass::Opening | MathClass::Closing | MathClass::Fence MathClass::Opening | MathClass::Closing | MathClass::Fence
) { ) {
scale(ctx, fragment, relative_to, height); scale(ctx, styles, fragment, relative_to, height)?;
if let Some(class) = apply { if let Some(class) = apply {
fragment.set_class(class); fragment.set_class(class);
} }
} }
Ok(())
} }
/// Scales a math fragment to a height. /// Scales a math fragment to a height.
fn scale( fn scale(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment, fragment: &mut MathFragment,
relative_to: Abs, relative_to: Abs,
height: Rel<Abs>, height: &StretchSize,
) { ) -> SourceResult<()> {
// This unwrap doesn't really matter. If it is None, then the fragment stretch_fragment(ctx, styles, fragment, Some(Axis::Y), Some(relative_to), height)
// won't be stretchable anyways.
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(ctx, fragment, Some(Axis::Y), Some(relative_to), height, short_fall);
} }

View File

@ -1,19 +1,20 @@
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};
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, Rel, Size,
};
use typst_library::math::{
Augment, AugmentOffsets, CasesElem, MatElem, StretchSize, VecElem,
}; };
use typst_library::math::{Augment, AugmentOffsets, CasesElem, MatElem, VecElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; 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, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext,
LeftRightAlternator, MathContext, DELIM_SHORT_FALL, alignments, style_for_denominator,
}; };
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05);
/// Lays out a [`VecElem`]. /// Lays out a [`VecElem`].
@ -39,7 +40,15 @@ pub fn layout_vec(
)?; )?;
let delim = elem.delim.get(styles); let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) layout_delimiters(
ctx,
styles,
frame,
elem.delim_size.get_ref(styles),
delim.open(),
delim.close(),
span,
)
} }
/// Lays out a [`CasesElem`]. /// Lays out a [`CasesElem`].
@ -70,7 +79,15 @@ pub fn layout_cases(
} else { } else {
(delim.open(), None) (delim.open(), None)
}; };
layout_delimiters(ctx, styles, frame, open, close, span) layout_delimiters(
ctx,
styles,
frame,
elem.delim_size.get_ref(styles),
open,
close,
span,
)
} }
/// Lays out a [`MatElem`]. /// Lays out a [`MatElem`].
@ -128,7 +145,15 @@ pub fn layout_mat(
)?; )?;
let delim = elem.delim.get(styles); let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) layout_delimiters(
ctx,
styles,
frame,
elem.delim_size.get_ref(styles),
delim.open(),
delim.close(),
span,
)
} }
/// Layout the inner contents of a matrix, vector, or cases. /// Layout the inner contents of a matrix, vector, or cases.
@ -309,19 +334,20 @@ fn layout_delimiters(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
mut frame: Frame, mut frame: Frame,
size: &StretchSize,
left: Option<char>, left: Option<char>,
right: Option<char>, right: Option<char>,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
let height = frame.height(); let height = frame.height();
let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis); frame.set_baseline(height / 2.0 + axis);
let target = size.resolve(ctx.engine, styles, height)?;
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 = GlyphFragment::new_char(ctx.font, styles, left_c, span)?;
left.stretch_vertical(ctx, target - short_fall); left.stretch_vertical(ctx, target);
left.center_on_axis(); left.center_on_axis();
ctx.push(left); ctx.push(left);
} }
@ -330,7 +356,7 @@ fn layout_delimiters(
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 = GlyphFragment::new_char(ctx.font, styles, right_c, span)?;
right.stretch_vertical(ctx, target - short_fall); right.stretch_vertical(ctx, target);
right.center_on_axis(); right.center_on_axis();
ctx.push(right); ctx.push(right);
} }

View File

@ -13,7 +13,8 @@ mod stretch;
mod text; mod text;
mod underover; mod underover;
use typst_library::diag::{bail, SourceResult}; use typst_library::World;
use typst_library::diag::{SourceResult, bail};
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,
@ -28,15 +29,14 @@ 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::{
families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, families, variant,
}; };
use typst_library::World;
use typst_syntax::Span; 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::*;
@ -603,13 +603,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(())
} }

View File

@ -5,7 +5,7 @@ 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, GlyphFragment, MathContext, style_cramped};
/// Lays out a [`RootElem`]. /// Lays out a [`RootElem`].
/// ///

View File

@ -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;
} }

View File

@ -1,7 +1,7 @@
use ttf_parser::math::MathValue;
use ttf_parser::Tag; use ttf_parser::Tag;
use ttf_parser::math::MathValue;
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, 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::{FontFeatures, TextElem};
use typst_utils::LazyHash; use typst_utils::LazyHash;
@ -30,9 +30,6 @@ macro_rules! percent {
}; };
} }
/// How much less high scaled delimiters can be than what they wrap.
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, ctx: &MathContext, font_size: Abs) -> Abs;

View File

@ -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};
use typst_library::math::StretchElem; use typst_library::math::{StretchElem, StretchSize};
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())]
@ -14,14 +14,7 @@ pub fn layout_stretch(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment( stretch_fragment(ctx, styles, &mut fragment, None, None, elem.size.get_ref(styles))?;
ctx,
&mut fragment,
None,
None,
elem.size.resolve(styles),
Abs::zero(),
);
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
} }
@ -29,29 +22,29 @@ pub fn layout_stretch(
/// Attempts to stretch the given fragment by/to the amount given in stretch. /// Attempts to stretch the given fragment by/to the amount given in stretch.
pub fn stretch_fragment( pub fn stretch_fragment(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment, fragment: &mut MathFragment,
axis: Option<Axis>, axis: Option<Axis>,
relative_to: Option<Abs>, relative_to: Option<Abs>,
stretch: Rel<Abs>, stretch: &StretchSize,
short_fall: Abs, ) -> SourceResult<()> {
) {
let size = fragment.size(); let size = fragment.size();
let MathFragment::Glyph(ref mut glyph) = fragment else { return }; let MathFragment::Glyph(glyph) = fragment else { return Ok(()) };
// 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.
let axes = stretch_axes(&glyph.item.font, glyph.base_glyph.id); let axes = stretch_axes(&glyph.item.font, glyph.base_glyph.id);
let stretch_axis = if let Some(axis) = axis { let stretch_axis = if let Some(axis) = axis {
if !axes.get(axis) { if !axes.get(axis) {
return; return Ok(());
} }
axis axis
} else { } else {
match (axes.x, axes.y) { match (axes.x, axes.y) {
(true, false) => Axis::X, (true, false) => Axis::X,
(false, true) => Axis::Y, (false, true) => Axis::Y,
(false, false) => return, (false, false) => return Ok(()),
(true, true) => { (true, true) => {
// As far as we know, there aren't any glyphs that have both // As far as we know, there aren't any glyphs that have both
// vertical and horizontal constructions. So for the time being, we // vertical and horizontal constructions. So for the time being, we
@ -62,16 +55,19 @@ pub fn stretch_fragment(
hint: "this is probably a font bug"; hint: "this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues" hint: "please file an issue at https://github.com/typst/typst/issues"
)); ));
return; return Ok(());
} }
} }
}; };
let relative_to_size = relative_to.unwrap_or_else(|| size.get(stretch_axis)); let relative_to_size = relative_to.unwrap_or_else(|| size.get(stretch_axis));
let target = stretch.resolve(ctx.engine, styles, relative_to_size)?;
glyph.stretch(ctx, stretch.relative_to(relative_to_size) - short_fall, stretch_axis); glyph.stretch(ctx, target, stretch_axis);
if stretch_axis == Axis::Y { if stretch_axis == Axis::Y {
glyph.center_on_axis(); glyph.center_on_axis();
} }
Ok(())
} }

View File

@ -1,20 +1,21 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
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, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size}; use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant}; 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, has_dtls_feat,
MathRun, style_dtls,
}; };
/// Lays out a [`TextElem`]. /// Lays out a [`TextElem`].
@ -64,12 +65,21 @@ fn layout_inline_text(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<FrameFragment> { ) -> SourceResult<FrameFragment> {
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
// Disable auto-italic.
let italic = styles.get(EquationElem::italic).or(Some(false));
if text.chars().all(|c| c.is_ascii_digit() || c == '.') { if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Small optimization for numbers. Note that this lays out slightly // Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future. // differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![]; let mut fragments = vec![];
for unstyled_c in text.chars() { for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false); // This is fine as ascii digits and '.' can never end up as more
// than a single char after styling.
let style = MathStyle::select(unstyled_c, variant, bold, italic);
let c = to_style(unstyled_c, style).next().unwrap();
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
fragments.push(glyph.into()); fragments.push(glyph.into());
} }
@ -83,8 +93,10 @@ fn layout_inline_text(
.map(|p| p.wrap()); .map(|p| p.wrap());
let styles = styles.chain(&local); let styles = styles.chain(&local);
let styled_text: EcoString = let styled_text: EcoString = text
text.chars().map(|c| styled_char(styles, c, false)).collect(); .chars()
.flat_map(|c| to_style(c, MathStyle::select(c, variant, bold, italic)))
.collect();
let spaced = styled_text.graphemes(true).nth(1).is_some(); let spaced = styled_text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(styled_text).spanned(span); let elem = TextElem::packed(styled_text).spanned(span);
@ -124,9 +136,16 @@ pub fn layout_symbol(
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
_ => (elem.text, styles), _ => (elem.text, styles),
}; };
let c = styled_char(styles, unstyled_c, true);
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic);
let style = MathStyle::select(unstyled_c, variant, bold, italic);
let text: EcoString = to_style(unstyled_c, style).collect();
let fragment: MathFragment = let fragment: MathFragment =
match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) { match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) {
Ok(mut glyph) => { Ok(mut glyph) => {
adjust_glyph_layout(&mut glyph, ctx, styles); adjust_glyph_layout(&mut glyph, ctx, styles);
glyph.into() glyph.into()
@ -134,8 +153,7 @@ pub fn layout_symbol(
Err(_) => { Err(_) => {
// Not in the math font, fallback to normal inline text layout. // Not in the math font, fallback to normal inline text layout.
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. // TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? layout_inline_text(&text, elem.span(), ctx, styles)?.into()
.into()
} }
}; };
ctx.push(fragment); ctx.push(fragment);
@ -161,226 +179,6 @@ fn adjust_glyph_layout(
} }
} }
/// Style the character by selecting the unicode codepoint for italic, bold,
/// caligraphic, etc.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
use MathVariant::*;
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic).unwrap_or(
auto_italic
&& matches!(
c,
'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' |
'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
)
&& matches!(variant, Sans | Serif),
);
if let Some(c) = basic_exception(c) {
return c;
}
if let Some(c) = latin_exception(c, variant, bold, italic) {
return c;
}
if let Some(c) = greek_exception(c, variant, bold, italic) {
return c;
}
let base = match c {
'A'..='Z' => 'A',
'a'..='z' => 'a',
'Α'..='Ω' => 'Α',
'α'..='ω' => 'α',
'0'..='9' => '0',
// Hebrew Alef -> Dalet.
'\u{05D0}'..='\u{05D3}' => '\u{05D0}',
_ => return c,
};
let tuple = (variant, bold, italic);
let start = match c {
// Latin upper.
'A'..='Z' => match tuple {
(Serif, false, false) => 0x0041,
(Serif, true, false) => 0x1D400,
(Serif, false, true) => 0x1D434,
(Serif, true, true) => 0x1D468,
(Sans, false, false) => 0x1D5A0,
(Sans, true, false) => 0x1D5D4,
(Sans, false, true) => 0x1D608,
(Sans, true, true) => 0x1D63C,
(Cal, false, _) => 0x1D49C,
(Cal, true, _) => 0x1D4D0,
(Frak, false, _) => 0x1D504,
(Frak, true, _) => 0x1D56C,
(Mono, _, _) => 0x1D670,
(Bb, _, _) => 0x1D538,
},
// Latin lower.
'a'..='z' => match tuple {
(Serif, false, false) => 0x0061,
(Serif, true, false) => 0x1D41A,
(Serif, false, true) => 0x1D44E,
(Serif, true, true) => 0x1D482,
(Sans, false, false) => 0x1D5BA,
(Sans, true, false) => 0x1D5EE,
(Sans, false, true) => 0x1D622,
(Sans, true, true) => 0x1D656,
(Cal, false, _) => 0x1D4B6,
(Cal, true, _) => 0x1D4EA,
(Frak, false, _) => 0x1D51E,
(Frak, true, _) => 0x1D586,
(Mono, _, _) => 0x1D68A,
(Bb, _, _) => 0x1D552,
},
// Greek upper.
'Α'..='Ω' => match tuple {
(Serif, false, false) => 0x0391,
(Serif, true, false) => 0x1D6A8,
(Serif, false, true) => 0x1D6E2,
(Serif, true, true) => 0x1D71C,
(Sans, _, false) => 0x1D756,
(Sans, _, true) => 0x1D790,
(Cal | Frak | Mono | Bb, _, _) => return c,
},
// Greek lower.
'α'..='ω' => match tuple {
(Serif, false, false) => 0x03B1,
(Serif, true, false) => 0x1D6C2,
(Serif, false, true) => 0x1D6FC,
(Serif, true, true) => 0x1D736,
(Sans, _, false) => 0x1D770,
(Sans, _, true) => 0x1D7AA,
(Cal | Frak | Mono | Bb, _, _) => return c,
},
// Hebrew Alef -> Dalet.
'\u{05D0}'..='\u{05D3}' => 0x2135,
// Numbers.
'0'..='9' => match tuple {
(Serif, false, _) => 0x0030,
(Serif, true, _) => 0x1D7CE,
(Bb, _, _) => 0x1D7D8,
(Sans, false, _) => 0x1D7E2,
(Sans, true, _) => 0x1D7EC,
(Mono, _, _) => 0x1D7F6,
(Cal | Frak, _, _) => return c,
},
_ => unreachable!(),
};
std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
}
fn basic_exception(c: char) -> Option<char> {
Some(match c {
'〈' => '⟨',
'〉' => '⟩',
'《' => '⟪',
'》' => '⟫',
_ => return None,
})
}
fn latin_exception(
c: char,
variant: MathVariant,
bold: bool,
italic: bool,
) -> Option<char> {
use MathVariant::*;
Some(match (c, variant, bold, italic) {
('B', Cal, false, _) => '',
('E', Cal, false, _) => '',
('F', Cal, false, _) => '',
('H', Cal, false, _) => '',
('I', Cal, false, _) => '',
('L', Cal, false, _) => '',
('M', Cal, false, _) => '',
('R', Cal, false, _) => '',
('C', Frak, false, _) => '',
('H', Frak, false, _) => '',
('I', Frak, false, _) => '',
('R', Frak, false, _) => '',
('Z', Frak, false, _) => '',
('C', Bb, ..) => '',
('H', Bb, ..) => '',
('N', Bb, ..) => '',
('P', Bb, ..) => '',
('Q', Bb, ..) => '',
('R', Bb, ..) => '',
('Z', Bb, ..) => '',
('D', Bb, _, true) => '',
('d', Bb, _, true) => '',
('e', Bb, _, true) => '',
('i', Bb, _, true) => '',
('j', Bb, _, true) => '',
('h', Serif, false, true) => '',
('e', Cal, false, _) => '',
('g', Cal, false, _) => '',
('o', Cal, false, _) => '',
('ħ', Serif, .., true) => 'ℏ',
('ı', Serif, .., true) => '𝚤',
('ȷ', Serif, .., true) => '𝚥',
_ => return None,
})
}
fn greek_exception(
c: char,
variant: MathVariant,
bold: bool,
italic: bool,
) -> Option<char> {
use MathVariant::*;
if c == 'Ϝ' && variant == Serif && bold {
return Some('𝟊');
}
if c == 'ϝ' && variant == Serif && bold {
return Some('𝟋');
}
let list = match c {
'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'],
'∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'],
'∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'],
'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'],
'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'],
'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'],
'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'],
'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'],
'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'],
'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'],
'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', ''],
'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'],
'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'],
'∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'],
_ => return None,
};
Some(match (variant, bold, italic) {
(Serif, true, false) => list[0],
(Serif, false, true) => list[1],
(Serif, true, true) => list[2],
(Sans, _, false) => list[3],
(Sans, _, true) => list[4],
(Bb, ..) => list[5],
_ => return None,
})
}
/// 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
/// `dtls` OpenType feature. /// `dtls` OpenType feature.
pub fn try_dotless(c: char) -> Option<char> { pub fn try_dotless(c: char) -> Option<char> {

View File

@ -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, GlyphFragment, LeftRightAlternator, MathContext, MathRun, stack,
GlyphFragment, LeftRightAlternator, MathContext, MathRun, style_cramped, style_for_subscript, style_for_superscript,
}; };
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);

View File

@ -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)
}

View File

@ -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

View File

@ -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;

View File

@ -0,0 +1,842 @@
use std::num::NonZeroUsize;
use comemo::Track;
use ecow::{EcoVec, eco_format};
use smallvec::smallvec;
use typst_library::diag::{At, SourceResult, bail};
use typst_library::foundations::{
Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart,
StyleChain, Target, dict,
};
use typst_library::introspection::{Counter, Locator, LocatorLink};
use typst_library::layout::{
Abs, AlignElem, Alignment, Axes, BlockBody, BlockElem, ColumnsElem, Em, GridCell,
GridChild, GridElem, GridItem, HAlignment, HElem, HideElem, InlineElem, LayoutElem,
Length, MoveElem, OuterVAlignment, PadElem, PlaceElem, PlacementScope, Region, Rel,
RepeatElem, RotateElem, ScaleElem, Sides, Size, Sizing, SkewElem, Spacing,
StackChild, StackElem, TrackSizings, VElem,
};
use typst_library::math::EquationElem;
use typst_library::model::{
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
};
use typst_library::pdf::EmbedElem;
use typst_library::text::{
DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName,
OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem,
SpaceElem, StrikeElem, SubElem, SuperElem, TextElem, TextSize, UnderlineElem,
WeightDelta,
};
use typst_library::visualize::{
CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem,
RectElem, SquareElem, Stroke,
};
use typst_utils::{Get, NonZeroExt, Numeric};
/// Register show rules for the [paged target](Target::Paged).
pub fn register(rules: &mut NativeRuleMap) {
use Target::Paged;
// Model.
rules.register(Paged, STRONG_RULE);
rules.register(Paged, EMPH_RULE);
rules.register(Paged, LIST_RULE);
rules.register(Paged, ENUM_RULE);
rules.register(Paged, TERMS_RULE);
rules.register(Paged, LINK_RULE);
rules.register(Paged, HEADING_RULE);
rules.register(Paged, FIGURE_RULE);
rules.register(Paged, FIGURE_CAPTION_RULE);
rules.register(Paged, QUOTE_RULE);
rules.register(Paged, FOOTNOTE_RULE);
rules.register(Paged, FOOTNOTE_ENTRY_RULE);
rules.register(Paged, OUTLINE_RULE);
rules.register(Paged, OUTLINE_ENTRY_RULE);
rules.register(Paged, REF_RULE);
rules.register(Paged, CITE_GROUP_RULE);
rules.register(Paged, BIBLIOGRAPHY_RULE);
rules.register(Paged, TABLE_RULE);
rules.register(Paged, TABLE_CELL_RULE);
// Text.
rules.register(Paged, SUB_RULE);
rules.register(Paged, SUPER_RULE);
rules.register(Paged, UNDERLINE_RULE);
rules.register(Paged, OVERLINE_RULE);
rules.register(Paged, STRIKE_RULE);
rules.register(Paged, HIGHLIGHT_RULE);
rules.register(Paged, SMALLCAPS_RULE);
rules.register(Paged, RAW_RULE);
rules.register(Paged, RAW_LINE_RULE);
// Layout.
rules.register(Paged, ALIGN_RULE);
rules.register(Paged, PAD_RULE);
rules.register(Paged, COLUMNS_RULE);
rules.register(Paged, STACK_RULE);
rules.register(Paged, GRID_RULE);
rules.register(Paged, GRID_CELL_RULE);
rules.register(Paged, MOVE_RULE);
rules.register(Paged, SCALE_RULE);
rules.register(Paged, ROTATE_RULE);
rules.register(Paged, SKEW_RULE);
rules.register(Paged, REPEAT_RULE);
rules.register(Paged, HIDE_RULE);
rules.register(Paged, LAYOUT_RULE);
// Visualize.
rules.register(Paged, IMAGE_RULE);
rules.register(Paged, LINE_RULE);
rules.register(Paged, RECT_RULE);
rules.register(Paged, SQUARE_RULE);
rules.register(Paged, ELLIPSE_RULE);
rules.register(Paged, CIRCLE_RULE);
rules.register(Paged, POLYGON_RULE);
rules.register(Paged, CURVE_RULE);
rules.register(Paged, PATH_RULE);
// Math.
rules.register(Paged, EQUATION_RULE);
// PDF.
rules.register(Paged, EMBED_RULE);
}
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, styles| {
Ok(elem
.body
.clone()
.set(TextElem::delta, WeightDelta(elem.delta.get(styles))))
};
const EMPH_RULE: ShowFn<EmphElem> =
|elem, _, _| Ok(elem.body.clone().set(TextElem::emph, ItalicToggle(true)));
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
let tight = elem.tight.get(styles);
let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_list)
.pack()
.spanned(elem.span());
if tight {
let spacing = elem
.spacing
.get(styles)
.unwrap_or_else(|| styles.get(ParElem::leading));
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
}
Ok(realized)
};
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
let tight = elem.tight.get(styles);
let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_enum)
.pack()
.spanned(elem.span());
if tight {
let spacing = elem
.spacing
.get(styles)
.unwrap_or_else(|| styles.get(ParElem::leading));
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
}
Ok(realized)
};
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
let span = elem.span();
let tight = elem.tight.get(styles);
let separator = elem.separator.get_ref(styles);
let indent = elem.indent.get(styles);
let hanging_indent = elem.hanging_indent.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
});
let pad = hanging_indent + indent;
let unpad = (!hanging_indent.is_zero())
.then(|| HElem::new((-hanging_indent).into()).pack().spanned(span));
let mut children = vec![];
for child in elem.children.iter() {
let mut seq = vec![];
seq.extend(unpad.clone());
seq.push(child.term.clone().strong());
seq.push(separator.clone());
seq.push(child.description.clone());
// Text in wide term lists shall always turn into paragraphs.
if !tight {
seq.push(ParbreakElem::shared().clone());
}
children.push(StackChild::Block(Content::sequence(seq)));
}
let padding =
Sides::default().with(styles.resolve(TextElem::dir).start(), pad.into());
let mut realized = StackElem::new(children)
.with_spacing(Some(gutter.into()))
.pack()
.spanned(span)
.padded(padding)
.set(TermsElem::within, true);
if tight {
let spacing = elem
.spacing
.get(styles)
.unwrap_or_else(|| styles.get(ParElem::leading));
let v = VElem::new(spacing.into())
.with_weak(true)
.with_attach(true)
.pack()
.spanned(span);
realized = v + realized;
}
Ok(realized)
};
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
let body = elem.body.clone();
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
Ok(body.linked(dest))
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
const SPACING_TO_NUMBERING: Em = Em::new(0.3);
let span = elem.span();
let mut realized = elem.body.clone();
let hanging_indent = elem.hanging_indent.get(styles);
let mut indent = match hanging_indent {
Smart::Custom(length) => length.resolve(styles),
Smart::Auto => Abs::zero(),
};
if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() {
let location = elem.location().unwrap();
let numbering = Counter::of(HeadingElem::ELEM)
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
if hanging_indent.is_auto() {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
// We don't have a locator for the numbering here, so we just
// use the measurement infrastructure for now.
let link = LocatorLink::measure(location);
let size = (engine.routines.layout_frame)(
engine,
&numbering,
Locator::link(&link),
styles,
pod,
)?
.size();
indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
}
let spacing = HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack();
realized = numbering + spacing + realized;
}
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(styles.resolve(TextElem::dir).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
Ok(block.pack())
};
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
let span = elem.span();
let mut realized = elem.body.clone();
// Build the caption, if any.
if let Some(caption) = elem.caption.get_cloned(styles) {
let (first, second) = match caption.position.get(styles) {
OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Bottom => (realized, caption.pack()),
};
realized = Content::sequence(vec![
first,
VElem::new(elem.gap.get(styles).into())
.with_weak(true)
.pack()
.spanned(span),
second,
]);
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
// Wrap the contents in a block.
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(span);
// Wrap in a float.
if let Some(align) = elem.placement.get(styles) {
realized = PlaceElem::new(realized)
.with_alignment(align.map(|align| HAlignment::Center + align))
.with_scope(elem.scope.get(styles))
.with_float(true)
.pack()
.spanned(span);
} else if elem.scope.get(styles) == PlacementScope::Parent {
bail!(
span,
"parent-scoped placement is only available for floating figures";
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
);
}
Ok(realized)
};
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
Ok(BlockElem::new()
.with_body(Some(BlockBody::Content(elem.realize(engine, styles)?)))
.pack())
};
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
let span = elem.span();
let block = elem.block.get(styles);
let mut realized = elem.body.clone();
if elem.quotes.get(styles).unwrap_or(!block) {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
let sticky = Content::sequence([hole.clone(), realized, hole]);
realized = QuoteElem::quoted(sticky, styles);
}
let attribution = elem.attribution.get_ref(styles);
if block {
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(span);
if let Some(attribution) = attribution.as_ref() {
// Bring the attribution a bit closer to the quote.
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v;
realized += BlockElem::new()
.with_body(Some(BlockBody::Content(attribution.realize(span))))
.pack()
.aligned(Alignment::END);
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone();
realized += CiteElem::new(*label).pack().spanned(span);
}
Ok(realized)
};
const FOOTNOTE_RULE: ShowFn<FootnoteElem> = |elem, engine, styles| {
let span = elem.span();
let loc = elem.declaration_location(engine).at(span)?;
let numbering = elem.numbering.get_ref(styles);
let counter = Counter::of(FootnoteElem::ELEM);
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num).pack().spanned(span);
let loc = loc.variant(1);
// Add zero-width weak spacing to make the footnote "sticky".
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
};
const FOOTNOTE_ENTRY_RULE: ShowFn<FootnoteEntry> = |elem, engine, styles| {
let span = elem.span();
let number_gap = Em::new(0.05);
let default = StyleChain::default();
let numbering = elem.note.numbering.get_ref(default);
let counter = Counter::of(FootnoteElem::ELEM);
let Some(loc) = elem.note.location() else {
bail!(
span, "footnote entry must have a location";
hint: "try using a query or a show rule to customize the footnote instead"
);
};
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num)
.pack()
.spanned(span)
.linked(Destination::Location(loc))
.located(loc.variant(1));
Ok(Content::sequence([
HElem::new(elem.indent.get(styles).into()).pack(),
sup,
HElem::new(number_gap.into()).with_weak(true).pack(),
elem.note.body_content().unwrap().clone(),
]))
};
const OUTLINE_RULE: ShowFn<OutlineElem> = |elem, engine, styles| {
let span = elem.span();
// Build the outline title.
let mut seq = vec![];
if let Some(title) = elem.title.get_cloned(styles).unwrap_or_else(|| {
Some(TextElem::packed(Packed::<OutlineElem>::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span),
);
}
let elems = engine.introspector.query(&elem.target.get_ref(styles).0);
let depth = elem.depth.get(styles).unwrap_or(NonZeroUsize::MAX);
// Build the outline entries.
for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let level = outlinable.level();
if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span));
}
}
Ok(Content::sequence(seq))
};
const OUTLINE_ENTRY_RULE: ShowFn<OutlineEntry> = |elem, engine, styles| {
let span = elem.span();
let context = Context::new(None, Some(styles));
let context = context.track();
let prefix = elem.prefix(engine, context, span)?;
let inner = elem.inner(engine, context, span)?;
let block = if elem.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
elem.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = elem.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
};
const REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5);
let span = elem.span();
let mut seq = vec![];
if let Some(title) = elem.title.get_ref(styles).clone().unwrap_or_else(|| {
Some(
TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles))
.spanned(span),
)
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span),
);
}
let works = Works::generate(engine).at(span)?;
let references = works
.references
.as_ref()
.ok_or_else(|| match elem.style.get_ref(styles).source {
CslSource::Named(style) => eco_format!(
"CSL style \"{}\" is not suitable for bibliographies",
style.display_name()
),
CslSource::Normal(..) => {
"CSL style is not suitable for bibliographies".into()
}
})
.at(span)?;
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let row_gutter = styles.get(ParElem::spacing);
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
.spanned(span),
)));
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(reference.clone())).spanned(span),
)));
}
seq.push(
GridElem::new(cells)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
.pack()
.spanned(span),
);
} else {
for (_, reference) in references {
let realized = reference.clone();
let block = if works.hanging_indent {
let body = HElem::new((-INDENT).into()).pack() + realized;
let inset = Sides::default()
.with(styles.resolve(TextElem::dir).start(), Some(INDENT.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
seq.push(block.pack().spanned(span));
}
}
Ok(Content::sequence(seq))
};
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table).pack())
};
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles))
};
const SUB_RULE: ShowFn<SubElem> = |elem, _, styles| {
show_script(
styles,
elem.body.clone(),
elem.typographic.get(styles),
elem.baseline.get(styles),
elem.size.get(styles),
ScriptKind::Sub,
)
};
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, styles| {
show_script(
styles,
elem.body.clone(),
elem.typographic.get(styles),
elem.baseline.get(styles),
elem.size.get(styles),
ScriptKind::Super,
)
};
fn show_script(
styles: StyleChain,
body: Content,
typographic: bool,
baseline: Smart<Length>,
size: Smart<TextSize>,
kind: ScriptKind,
) -> SourceResult<Content> {
let font_size = styles.resolve(TextElem::size);
Ok(body.set(
TextElem::shift_settings,
Some(ShiftSettings {
typographic,
shift: baseline.map(|l| -Em::from_length(l, font_size)),
size: size.map(|t| Em::from_length(t.0, font_size)),
kind,
}),
))
}
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Underline {
stroke: elem.stroke.resolve(styles).unwrap_or_default(),
offset: elem.offset.resolve(styles),
evade: elem.evade.get(styles),
background: elem.background.get(styles),
},
extent: elem.extent.resolve(styles),
}],
))
};
const OVERLINE_RULE: ShowFn<OverlineElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Overline {
stroke: elem.stroke.resolve(styles).unwrap_or_default(),
offset: elem.offset.resolve(styles),
evade: elem.evade.get(styles),
background: elem.background.get(styles),
},
extent: elem.extent.resolve(styles),
}],
))
};
const STRIKE_RULE: ShowFn<StrikeElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
// Note that we do not support evade option for strikethrough.
line: DecoLine::Strikethrough {
stroke: elem.stroke.resolve(styles).unwrap_or_default(),
offset: elem.offset.resolve(styles),
background: elem.background.get(styles),
},
extent: elem.extent.resolve(styles),
}],
))
};
const HIGHLIGHT_RULE: ShowFn<HighlightElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Highlight {
fill: elem.fill.get_cloned(styles),
stroke: elem
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|stroke| stroke.map(Stroke::unwrap_or_default)),
top_edge: elem.top_edge.get(styles),
bottom_edge: elem.bottom_edge.get(styles),
radius: elem.radius.resolve(styles).unwrap_or_default(),
},
extent: elem.extent.resolve(styles),
}],
))
};
const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
let sc = if elem.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules };
Ok(elem.body.clone().set(TextElem::smallcaps, Some(sc)))
};
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
let lines = elem.lines.as_deref().unwrap_or_default();
let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1));
for (i, line) in lines.iter().enumerate() {
if i != 0 {
seq.push(LinebreakElem::shared().clone());
}
seq.push(line.clone().pack());
}
let mut realized = Content::sequence(seq);
if elem.block.get(styles) {
// Align the text before inserting it into the block.
realized = realized.aligned(elem.align.get(styles).into());
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(elem.span());
}
Ok(realized)
};
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
const ALIGN_RULE: ShowFn<AlignElem> =
|elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles)));
const PAD_RULE: ShowFn<PadElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad).pack())
};
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns).pack())
};
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack).pack())
};
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid).pack())
};
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles))
};
/// Function with common code to display a grid cell or table cell.
fn show_cell(
mut body: Content,
inset: Smart<Sides<Option<Rel<Length>>>>,
align: Smart<Alignment>,
) -> SourceResult<Content> {
let inset = inset.unwrap_or_default().map(Option::unwrap_or_default);
if inset != Sides::default() {
// Only pad if some inset is not 0pt.
// Avoids a bug where using .padded() in any way inside Show causes
// alignment in align(...) to break.
body = body.padded(inset);
}
if let Smart::Custom(alignment) = align {
body = body.aligned(alignment);
}
Ok(body)
}
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move).pack())
};
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale).pack())
};
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate).pack())
};
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew).pack())
};
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat).pack())
};
const HIDE_RULE: ShowFn<HideElem> =
|elem, _, _| Ok(elem.body.clone().set(HideElem::hidden, true));
const LAYOUT_RULE: ShowFn<LayoutElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(
elem.clone(),
|elem, engine, locator, styles, regions| {
// Gets the current region's base size, which will be the size of the
// outer container, or of the page if there is no such container.
let Size { x, y } = regions.base();
let loc = elem.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
let result = elem
.func
.call(engine, context.track(), [dict! { "width" => x, "height" => y }])?
.display();
crate::flow::layout_fragment(engine, &result, locator, styles, regions)
},
)
.pack())
};
const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack())
};
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line).pack())
};
const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack())
};
const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack())
};
const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack())
};
const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack())
};
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon).pack())
};
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve).pack())
};
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path).pack())
};
const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
if elem.block.get(styles) {
Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block)
.pack())
} else {
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline).pack())
}
};
const EMBED_RULE: ShowFn<EmbedElem> = |_, _, _| Ok(Content::empty());

View File

@ -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;

View File

@ -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};

View File

@ -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;

View File

@ -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 }

View File

@ -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}')")

View File

@ -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
}
} }
} }

View File

@ -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.

View File

@ -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.

View File

@ -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.

Some files were not shown because too many files have changed in this diff Show More