Compare commits

...

29 Commits

Author SHA1 Message Date
mkorje
13a9549d05
Support multiple fonts in math 2025-07-23 17:32:23 +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
340 changed files with 4817 additions and 3383 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 }}

149
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",
] ]
@ -748,9 +748,9 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys", "libz-rs-sys",
@ -964,6 +964,69 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "hayro"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"bytemuck",
"hayro-interpret",
"image",
"kurbo",
"rustc-hash",
"smallvec",
]
[[package]]
name = "hayro-font"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"log",
"phf",
]
[[package]]
name = "hayro-interpret"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"bitflags 2.9.1",
"hayro-font",
"hayro-syntax",
"kurbo",
"log",
"phf",
"qcms",
"skrifa",
"smallvec",
"yoke 0.8.0",
]
[[package]]
name = "hayro-syntax"
version = "0.0.1"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"flate2",
"kurbo",
"log",
"rustc-hash",
"smallvec",
"zune-jpeg",
]
[[package]]
name = "hayro-write"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"flate2",
"hayro-syntax",
"log",
"pdf-writer",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -1198,17 +1261,11 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "if_chain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.5" version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
@ -1271,7 +1328,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"inotify-sys", "inotify-sys",
"libc", "libc",
] ]
@ -1367,7 +1424,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",
@ -1376,6 +1433,7 @@ dependencies = [
"float-cmp 0.10.0", "float-cmp 0.10.0",
"fxhash", "fxhash",
"gif", "gif",
"hayro-write",
"image-webp", "image-webp",
"imagesize", "imagesize",
"once_cell", "once_cell",
@ -1385,6 +1443,7 @@ dependencies = [
"rustybuzz", "rustybuzz",
"siphasher", "siphasher",
"skrifa", "skrifa",
"smallvec",
"subsetter", "subsetter",
"tiny-skia-path", "tiny-skia-path",
"xmp-writer", "xmp-writer",
@ -1395,7 +1454,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla-svg" name = "krilla-svg"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
dependencies = [ dependencies = [
"flate2", "flate2",
"fontdb", "fontdb",
@ -1408,9 +1467,9 @@ dependencies = [
[[package]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"smallvec", "smallvec",
@ -1462,16 +1521,16 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall", "redox_syscall",
] ]
[[package]] [[package]]
name = "libz-rs-sys" name = "libz-rs-sys"
version = "0.4.2" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d" checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
dependencies = [ dependencies = [
"zlib-rs", "zlib-rs",
] ]
@ -1628,7 +1687,7 @@ version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"filetime", "filetime",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
@ -1710,7 +1769,7 @@ version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"cfg-if", "cfg-if",
"foreign-types", "foreign-types",
"libc", "libc",
@ -1847,7 +1906,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc" checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@ -2005,7 +2064,7 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"getopts", "getopts",
"memchr", "memchr",
"unicase", "unicase",
@ -2118,7 +2177,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]
@ -2221,7 +2280,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -2240,7 +2299,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"bytemuck", "bytemuck",
"core_maths", "core_maths",
"log", "log",
@ -2288,7 +2347,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -2451,9 +2510,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
@ -2861,7 +2920,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1" source = "git+https://github.com/typst/typst-assets?rev=fbf00f9#fbf00f9539fdb0825bef4d39fb57d5986c51b756"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -2911,7 +2970,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648" source = "git+https://github.com/typst/typst-dev-assets?rev=c6c2acf#c6c2acf6cdc31f99a23a478d3d614f8bf806a4f5"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"
@ -2943,7 +3002,6 @@ version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"if_chain",
"indexmap 2.7.1", "indexmap 2.7.1",
"stacker", "stacker",
"toml", "toml",
@ -2991,7 +3049,6 @@ version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"if_chain",
"once_cell", "once_cell",
"pathdiff", "pathdiff",
"serde", "serde",
@ -3063,7 +3120,7 @@ name = "typst-library"
version = "0.13.1" version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bitflags 2.8.0", "bitflags 2.9.1",
"bumpalo", "bumpalo",
"chinese-number", "chinese-number",
"ciborium", "ciborium",
@ -3075,6 +3132,7 @@ dependencies = [
"fontdb", "fontdb",
"glidesort", "glidesort",
"hayagriva", "hayagriva",
"hayro-syntax",
"icu_properties", "icu_properties",
"icu_provider", "icu_provider",
"icu_provider_blob", "icu_provider_blob",
@ -3173,11 +3231,13 @@ version = "0.13.1"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"comemo", "comemo",
"hayro",
"image", "image",
"pixglyph", "pixglyph",
"resvg", "resvg",
"tiny-skia", "tiny-skia",
"ttf-parser", "ttf-parser",
"typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-timing", "typst-timing",
@ -3191,8 +3251,10 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"flate2", "flate2",
"hayro",
"image", "image",
"ttf-parser", "ttf-parser",
"typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-timing", "typst-timing",
@ -3589,7 +3651,7 @@ version = "0.221.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"indexmap 2.7.1", "indexmap 2.7.1",
] ]
@ -3724,7 +3786,7 @@ version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]
@ -3932,13 +3994,12 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "2.5.0" version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88" checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",
"crossbeam-utils",
"flate2", "flate2",
"indexmap 2.7.1", "indexmap 2.7.1",
"memchr", "memchr",
@ -3947,9 +4008,9 @@ dependencies = [
[[package]] [[package]]
name = "zlib-rs" name = "zlib-rs"
version = "0.4.2" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05" checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
[[package]] [[package]]
name = "zopfli" name = "zopfli"

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 = "edf0d64" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fbf00f9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "c6c2acf" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -61,6 +61,8 @@ fontdb = { version = "0.23", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"
glidesort = "0.1.2" glidesort = "0.1.2"
hayagriva = "0.8.1" hayagriva = "0.8.1"
hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
hayro = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
heck = "0.5" heck = "0.5"
hypher = "0.1.4" hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] } icu_properties = { version = "1.4", features = ["serde"] }
@ -68,13 +70,12 @@ icu_provider = { version = "1.4", features = ["sync"] }
icu_provider_adapters = "1.4" icu_provider_adapters = "1.4"
icu_provider_blob = "1.4" icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] } icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] } image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false } infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] } krilla = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] }
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" } krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00"}
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"
@ -143,7 +144,7 @@ xmlparser = "0.13.5"
xmlwriter = "0.1.0" xmlwriter = "0.1.0"
xz2 = { version = "0.1", features = ["static"] } xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1" yaml-front-matter = "0.1"
zip = { version = "2.5", default-features = false, features = ["deflate"] } zip = { version = "4.3", default-features = false, features = ["deflate"] }
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 2 opt-level = 2

View File

@ -173,8 +173,11 @@ typst help
typst help watch 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

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

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

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

@ -21,8 +21,8 @@ use std::io::{self, Write};
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::LazyLock; use std::sync::LazyLock;
use clap::error::ErrorKind;
use clap::Parser; use clap::Parser;
use clap::error::ErrorKind;
use codespan_reporting::term; use codespan_reporting::term;
use codespan_reporting::term::termcolor::WriteColor; use codespan_reporting::term::termcolor::WriteColor;
use typst::diag::HintedStrResult; use typst::diag::HintedStrResult;
@ -102,7 +102,7 @@ fn print_error(msg: &str) -> io::Result<()> {
#[cfg(not(feature = "self-update"))] #[cfg(not(feature = "self-update"))]
mod update { mod update {
use typst::diag::{bail, StrResult}; use typst::diag::{StrResult, bail};
use crate::args::UpdateCommand; use crate::args::UpdateCommand;

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,7 +5,7 @@ use std::sync::{LazyLock, OnceLock};
use std::{fmt, fs, io, mem}; use std::{fmt, fs, io, mem};
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc}; use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use parking_lot::Mutex; use parking_lot::Mutex;
use typst::diag::{FileError, FileResult}; use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
@ -361,10 +361,10 @@ 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.
@ -372,10 +372,10 @@ impl<T: Clone> SlotCell<T> {
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);

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,10 +29,10 @@ 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)

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,21 +324,17 @@ 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(), // Only validate the context once we know that this is indeed
); // a field from the style chain.
then { let _ = styles?;
// Only validate the context once we know that this is indeed return Ok(value);
// a field from the style chain.
let _ = styles?;
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,14 +46,14 @@ 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` {
vm.engine.sink.warn(warning!( // Warn on `import x as x`
new_name.span(), vm.engine.sink.warn(warning!(
"unnecessary import rename to same name", new_name.span(),
)); "unnecessary import rename to same name",
} ));
} }
// Define renamed module on the scope. // Define renamed module on the scope.
@ -142,15 +142,14 @@ 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!(
renamed_item.new_name().span(), renamed_item.new_name().span(),
"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,12 +76,12 @@ 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)?; {
dict.insert(access.field().get().clone().into(), rhs); let dict = access_dict(vm, access)?;
return Ok(Value::None); dict.insert(access.field().get().clone().into(), rhs);
} return Ok(Value::None);
} }
let location = binary.lhs().access(vm)?; let location = binary.lhs().access(vm)?;

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,10 +12,10 @@ 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();
@ -58,18 +58,16 @@ 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";
hint: "write `set par(spacing: ..)` instead"; hint: "write `set par(spacing: ..)` instead";
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

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

@ -3,28 +3,10 @@
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use ecow::EcoString; use ecow::EcoString;
use typst_library::html::{attr, HtmlElem};
use typst_library::layout::{Length, Rel}; use typst_library::layout::{Length, Rel};
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
use typst_utils::Numeric; use typst_utils::Numeric;
/// Additional methods for [`HtmlElem`].
pub trait HtmlElemExt {
/// Adds the styles to an element if the property list is non-empty.
fn with_styles(self, properties: Properties) -> Self;
}
impl HtmlElemExt for HtmlElem {
/// Adds CSS styles to an element.
fn with_styles(self, properties: Properties) -> Self {
if let Some(value) = properties.into_inline_styles() {
self.with_attr(attr::style, value)
} else {
self
}
}
}
/// A list of CSS properties with values. /// A list of CSS properties with values.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Properties(EcoString); pub struct Properties(EcoString);

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,33 +1,29 @@
//! Typst's HTML exporter. //! Typst's HTML exporter.
mod attr;
mod charsets;
mod convert;
mod css; mod css;
mod document;
mod dom;
mod encode; mod encode;
mod fragment;
mod link;
mod rules; mod rules;
mod tag;
mod typed; mod typed;
pub use self::document::html_document;
pub use self::dom::*;
pub use self::encode::html; pub use self::encode::html;
pub use self::rules::register; pub use self::rules::register;
use comemo::{Track, Tracked, TrackedMut}; use ecow::EcoString;
use typst_library::diag::{bail, warning, At, SourceResult}; use typst_library::Category;
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{Content, Module, Scope};
use typst_library::foundations::{ use typst_macros::elem;
Content, Module, Scope, StyleChain, Target, TargetElem,
};
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
};
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::{Category, World};
use typst_syntax::Span;
/// Create a module with all HTML definitions. /// Creates the module with all HTML definitions.
pub fn module() -> Module { pub fn module() -> Module {
let mut html = Scope::deduplicating(); let mut html = Scope::deduplicating();
html.start_category(Category::Html); html.start_category(Category::Html);
@ -37,337 +33,86 @@ pub fn module() -> Module {
Module::new("html", html) Module::new("html", html)
} }
/// Produce an HTML document from content. /// An HTML element that can contain Typst content.
/// ///
/// This first performs root-level realization and then turns the resulting /// Typst's HTML export automatically generates the appropriate tags for most
/// elements into HTML. /// elements. However, sometimes, it is desirable to retain more control. For
#[typst_macros::time(name = "html document")] /// example, when using Typst to generate your blog, you could use this function
pub fn html_document( /// to wrap each article in an `<article>` tag.
engine: &mut Engine, ///
content: &Content, /// Typst is aware of what is valid HTML. A tag and its attributes must form
styles: StyleChain, /// syntactically valid HTML. Some tags, like `meta` do not accept content.
) -> SourceResult<HtmlDocument> { /// Hence, you must not provide a body for them. We may add more checks in the
html_document_impl( /// future, so be sure that you are generating valid HTML when using this
engine.routines, /// function.
engine.world, ///
engine.introspector, /// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
engine.traced, /// you instead create them with this function, Typst will omit its own tags.
TrackedMut::reborrow_mut(&mut engine.sink), ///
engine.route.track(), /// ```typ
content, /// #html.elem("div", attrs: (style: "background: aqua"))[
styles, /// A div with _Typst content_ inside!
) /// ]
/// ```
#[elem(name = "elem")]
pub struct HtmlElem {
/// The element's tag.
#[required]
pub tag: HtmlTag,
/// The element's HTML attributes.
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
pub body: Option<Content>,
} }
/// The internal implementation of `html_document`. impl HtmlElem {
#[comemo::memoize] /// Add an attribute to the element.
#[allow(clippy::too_many_arguments)] pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
fn html_document_impl( self.attrs
routines: &Routines, .as_option_mut()
world: Tracked<dyn World + '_>, .get_or_insert_with(Default::default)
introspector: Tracked<Introspector>, .push(attr, value);
traced: Tracked<Traced>, self
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument(&mut info),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?;
Ok(HtmlDocument { info, root, introspector })
}
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
// No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
handle_list(&mut engine, &mut locator, children.iter().copied())
}
/// Convert children into HTML nodes.
fn handle_list<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert a child into HTML node(s).
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>() {
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 {
engine.sink.warn(warning!(
child.span(),
"{} was ignored during HTML export",
child.elem().name()
));
}
Ok(())
}
/// 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 { /// Adds the attribute to the element if value is not `None`.
children.push( pub fn with_optional_attr(
HtmlElement::new(tag::meta) self,
.with_attr(attr::name, "description") attr: HtmlAttr,
.with_attr(attr::content, description.clone()) value: Option<impl Into<EcoString>>,
.into(), ) -> Self {
); if let Some(value) = value { self.with_attr(attr, value) } else { self }
} }
if !info.author.is_empty() { /// Adds CSS styles to an element.
children.push( fn with_styles(self, properties: css::Properties) -> Self {
HtmlElement::new(tag::meta) if let Some(value) = properties.into_inline_styles() {
.with_attr(attr::name, "authors") self.with_attr(attr::style, value)
.with_attr(attr::content, info.author.join(", ")) } else {
.into(), self
)
}
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. /// An element that lays out its content as an inline SVG.
enum OutputKind { ///
/// The user generated their own `<html>` element. We do not need to supply /// Sometimes, converting Typst content to HTML is not desirable. This can be
/// one. /// the case for plots and other content that relies on positioning and styling
Html(HtmlElement), /// to convey its message.
/// The user generate their own `<body>` element. We do not need to supply ///
/// one, but need supply the `<html>` element. /// This function allows you to use the Typst layout engine that would also be
Body(HtmlElement), /// used for PDF, SVG, and PNG export to render a part of your document exactly
/// The user generated leafs which we wrap in a `<body>` and `<html>`. /// how it would appear when exported in one of these formats. It embeds the
Leafs(Vec<HtmlNode>), /// content as an inline SVG.
#[elem]
pub struct FrameElem {
/// The content that shall be laid out.
#[positional]
#[required]
pub body: Content,
} }

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

@ -1,13 +1,12 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use ecow::{eco_format, EcoVec}; use ecow::{EcoVec, eco_format};
use typst_library::diag::warning; use typst_library::diag::{At, warning};
use typst_library::foundations::{ use typst_library::foundations::{
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target, Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
}; };
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::introspection::{Counter, Locator}; use typst_library::introspection::{Counter, Locator};
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use typst_library::layout::resolve::{Cell, CellGrid, Entry, table_to_cellgrid};
use typst_library::layout::{OuterVAlignment, Sizing}; use typst_library::layout::{OuterVAlignment, Sizing};
use typst_library::model::{ use typst_library::model::{
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption, Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
@ -15,16 +14,16 @@ use typst_library::model::{
RefElem, StrongElem, TableCell, TableElem, TermsElem, RefElem, StrongElem, TableCell, TableElem, TermsElem,
}; };
use typst_library::text::{ use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
SubElem, SuperElem, UnderlineElem, SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
}; };
use typst_library::visualize::ImageElem; use typst_library::visualize::ImageElem;
use crate::css::{self, HtmlElemExt}; use crate::{FrameElem, HtmlAttrs, HtmlElem, HtmlTag, attr, css, tag};
/// Register show rules for the [HTML target](Target::Html). /// Registers show rules for the [HTML target](Target::Html).
pub fn register(rules: &mut NativeRuleMap) { pub fn register(rules: &mut NativeRuleMap) {
use Target::Html; use Target::{Html, Paged};
// Model. // Model.
rules.register(Html, STRONG_RULE); rules.register(Html, STRONG_RULE);
@ -48,26 +47,24 @@ pub fn register(rules: &mut NativeRuleMap) {
rules.register(Html, OVERLINE_RULE); rules.register(Html, OVERLINE_RULE);
rules.register(Html, STRIKE_RULE); rules.register(Html, STRIKE_RULE);
rules.register(Html, HIGHLIGHT_RULE); rules.register(Html, HIGHLIGHT_RULE);
rules.register(Html, SMALLCAPS_RULE);
rules.register(Html, RAW_RULE); rules.register(Html, RAW_RULE);
rules.register(Html, RAW_LINE_RULE); rules.register(Html, RAW_LINE_RULE);
// Visualize. // Visualize.
rules.register(Html, IMAGE_RULE); rules.register(Html, IMAGE_RULE);
// For the HTML target, `html.frame` is a primitive. In the laid-out target,
// it should be a no-op so that nested frames don't break (things like `show
// math.equation: html.frame` can result in nested ones).
rules.register::<FrameElem>(Paged, |elem, _, _| Ok(elem.body.clone()));
} }
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| { const STRONG_RULE: ShowFn<StrongElem> =
Ok(HtmlElem::new(tag::strong) |elem, _, _| Ok(HtmlElem::new(tag::strong).with_body(Some(elem.body.clone())).pack());
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const EMPH_RULE: ShowFn<EmphElem> = |elem, _, _| { const EMPH_RULE: ShowFn<EmphElem> =
Ok(HtmlElem::new(tag::em) |elem, _, _| Ok(HtmlElem::new(tag::em).with_body(Some(elem.body.clone())).pack());
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| { const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::ul) Ok(HtmlElem::new(tag::ul)
@ -82,8 +79,7 @@ const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
.pack() .pack()
.spanned(item.span()) .spanned(item.span())
})))) }))))
.pack() .pack())
.spanned(elem.span()))
}; };
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| { const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
@ -99,7 +95,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
let body = Content::sequence(elem.children.iter().map(|item| { let body = Content::sequence(elem.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li); let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number.get(styles) { if let Smart::Custom(nr) = item.number.get(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}")); li = li.with_attr(attr::value, eco_format!("{nr}"));
} }
// Text in wide enums shall always turn into paragraphs. // Text in wide enums shall always turn into paragraphs.
@ -110,7 +106,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
li.with_body(Some(body)).pack().spanned(item.span()) li.with_body(Some(body)).pack().spanned(item.span())
})); }));
Ok(ol.with_body(Some(body)).pack().spanned(elem.span())) Ok(ol.with_body(Some(body)).pack())
}; };
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| { const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
@ -137,20 +133,32 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
}; };
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| { const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
let body = elem.body.clone(); let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
Ok(if let LinkTarget::Dest(Destination::Url(url)) = &elem.dest {
HtmlElem::new(tag::a) let href = match dest {
.with_attr(attr::href, url.clone().into_inner()) Destination::Url(url) => Some(url.clone().into_inner()),
.with_body(Some(body)) Destination::Location(location) => {
.pack() let id = engine
.spanned(elem.span()) .introspector
} else { .html_id(location)
engine.sink.warn(warning!( .cloned()
elem.span(), .ok_or("failed to determine link anchor")
"non-URL links are not yet supported by HTML export" .at(elem.span())?;
)); Some(eco_format!("#{id}"))
body }
}) 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| { const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
@ -186,10 +194,9 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
.with_attr(attr::role, "heading") .with_attr(attr::role, "heading")
.with_attr(attr::aria_level, eco_format!("{}", level + 1)) .with_attr(attr::aria_level, eco_format!("{}", level + 1))
.pack() .pack()
.spanned(span)
} else { } else {
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1]; let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) HtmlElem::new(t).with_body(Some(realized)).pack()
}) })
}; };
@ -208,17 +215,13 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
// Ensure that the body is considered a paragraph. // Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span); realized += ParbreakElem::shared().clone().spanned(span);
Ok(HtmlElem::new(tag::figure) Ok(HtmlElem::new(tag::figure).with_body(Some(realized)).pack())
.with_body(Some(realized))
.pack()
.spanned(span))
}; };
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| { const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
Ok(HtmlElem::new(tag::figcaption) Ok(HtmlElem::new(tag::figcaption)
.with_body(Some(elem.realize(engine, styles)?)) .with_body(Some(elem.realize(engine, styles)?))
.pack() .pack())
.spanned(elem.span()))
}; };
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| { const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
@ -235,13 +238,11 @@ const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
if block { if block {
let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized)); let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized));
if let Some(Attribution::Content(attribution)) = attribution { if let Some(Attribution::Content(attribution)) = attribution
if let Some(link) = attribution.to_packed::<LinkElem>() { && let Some(link) = attribution.to_packed::<LinkElem>()
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { && let LinkTarget::Dest(Destination::Url(url)) = &link.dest
blockquote = {
blockquote.with_attr(attr::cite, url.clone().into_inner()); blockquote = blockquote.with_attr(attr::cite, url.clone().into_inner());
}
}
} }
realized = blockquote.pack().spanned(span); realized = blockquote.pack().spanned(span);
@ -359,19 +360,11 @@ fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
.spanned(cell.span()) .spanned(cell.span())
} }
const SUB_RULE: ShowFn<SubElem> = |elem, _, _| { const SUB_RULE: ShowFn<SubElem> =
Ok(HtmlElem::new(tag::sub) |elem, _, _| Ok(HtmlElem::new(tag::sub).with_body(Some(elem.body.clone())).pack());
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, _| { const SUPER_RULE: ShowFn<SuperElem> =
Ok(HtmlElem::new(tag::sup) |elem, _, _| Ok(HtmlElem::new(tag::sup).with_body(Some(elem.body.clone())).pack());
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| { const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
// Note: In modern HTML, `<u>` is not the underline element, but // Note: In modern HTML, `<u>` is not the underline element, but
@ -396,6 +389,20 @@ const STRIKE_RULE: ShowFn<StrikeElem> =
const HIGHLIGHT_RULE: ShowFn<HighlightElem> = const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack()); |elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::span)
.with_attr(
attr::style,
if elem.all.get(styles) {
"font-variant-caps: all-small-caps"
} else {
"font-variant-caps: small-caps"
},
)
.with_body(Some(elem.body.clone()))
.pack())
};
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| { const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
let lines = elem.lines.as_deref().unwrap_or_default(); let lines = elem.lines.as_deref().unwrap_or_default();
@ -410,8 +417,7 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code }) Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
.with_body(Some(Content::sequence(seq))) .with_body(Some(Content::sequence(seq)))
.pack() .pack())
.spanned(elem.span()))
}; };
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone()); const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());

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,22 +9,20 @@ use std::sync::LazyLock;
use bumpalo::Bump; use bumpalo::Bump;
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, eco_vec, EcoString}; use ecow::{EcoString, eco_format, eco_vec};
use typst_assets::html as data; use typst_assets::html as data;
use typst_library::diag::{bail, At, Hint, HintedStrResult, SourceResult}; use typst_library::diag::{At, Hint, HintedStrResult, SourceResult, bail};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{ use typst_library::foundations::{
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration, Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo, FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
PositiveF64, Reflect, Scope, Str, Type, Value, PositiveF64, Reflect, Scope, Str, Type, Value,
}; };
use typst_library::html::tag;
use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::layout::{Axes, Axis, Dir, Length}; use typst_library::layout::{Axes, Axis, Dir, Length};
use typst_library::visualize::Color; use typst_library::visualize::Color;
use typst_macros::cast; use typst_macros::cast;
use crate::css; use crate::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag, css, tag};
/// Hook up all typed HTML definitions. /// Hook up all typed HTML definitions.
pub(super) fn define(html: &mut Scope) { pub(super) fn define(html: &mut Scope) {

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, FigureElem}; use typst::model::{BibliographyElem, FigureElem};
use typst::syntax::{ast, LinkedNode, SyntaxKind}; use typst::syntax::{LinkedNode, SyntaxKind, ast};
use crate::IdeWorld; use crate::IdeWorld;
@ -25,16 +27,17 @@ 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
return analyze_expr(world, parent); && node.index() > 0
} {
return analyze_expr(world, parent);
} }
return typst::trace::<PagedDocument>(world.upcast(), node.span()); return typst::trace::<PagedDocument>(world.upcast(), node.span());
@ -66,14 +69,22 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value
/// - All labels and descriptions for them, if available /// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after /// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography. /// belong to a bibliography.
///
/// Note: When multiple labels in the document have the same identifier,
/// this only returns the first one.
pub fn analyze_labels( pub fn analyze_labels(
document: &PagedDocument, document: &PagedDocument,
) -> (Vec<(Label, Option<EcoString>)>, usize) { ) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![]; let mut output = vec![];
let mut seen_labels = HashSet::new();
// Labels in the document. // Labels in the document.
for elem in document.introspector.all() { for elem in document.introspector.all() {
let Some(label) = elem.label() else { continue }; let Some(label) = elem.label() else { continue };
if !seen_labels.insert(label) {
continue;
}
let details = elem let details = elem
.to_packed::<FigureElem>() .to_packed::<FigureElem>()
.and_then(|figure| match figure.caption.as_option() { .and_then(|figure| match figure.caption.as_option() {

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,26 +144,22 @@ 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.
@ -366,37 +368,34 @@ 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() == "."); && ctx.leaf.range().end == ctx.cursor
if ctx.leaf.range().end == ctx.cursor; && let Some(prev) = ctx.leaf.prev_sibling()
if let Some(prev) = ctx.leaf.prev_sibling(); && (!in_markup || prev.range().end == ctx.leaf.range().start)
if !in_markup || prev.range().end == ctx.leaf.range().start; && prev.is::<ast::Expr>()
if prev.is::<ast::Expr>(); && (prev.parent_kind() != Some(SyntaxKind::Markup)
if prev.parent_kind() != Some(SyntaxKind::Markup) || || prev.prev_sibling_kind() == Some(SyntaxKind::Hash))
prev.prev_sibling_kind() == Some(SyntaxKind::Hash); && let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next()
if 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,57 +499,49 @@ 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(':'); ctx.package_completions(all_versions);
ctx.package_completions(all_versions); } else {
} else { ctx.file_completions_with_extensions(&["typ"]);
ctx.file_completions_with_extensions(&["typ"]);
}
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,15 +591,13 @@ 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,32 +697,28 @@ 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());
}
named_param_value_completions(ctx, callee, &param);
return true;
} }
named_param_value_completions(ctx, callee, &param);
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());
}
param_completions(ctx, callee, set, args, args_linked);
return true;
} }
param_completions(ctx, callee, set, args, args_linked);
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,14 +1079,12 @@ 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;
} }
@ -1375,10 +1355,11 @@ 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('"')
apply = Some(trimmed.into()); && let Some(trimmed) = label.strip_suffix('"')
} {
apply = Some(trimmed.into());
} }
self.completions.push(Completion { self.completions.push(Completion {
@ -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.
@ -1582,7 +1563,7 @@ mod tests {
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self; fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self; fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_apply<'a>(&self, label: &str, apply: impl Into<Option<&'a str>>) fn must_apply<'a>(&self, label: &str, apply: impl Into<Option<&'a str>>)
-> &Self; -> &Self;
} }
impl ResponseExt for Response { impl ResponseExt for Response {
@ -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,28 +36,28 @@ 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 { {
Destination::Url(url) => Jump::Url(url.clone()), return Some(match dest {
Destination::Position(pos) => Jump::Position(*pos), Destination::Url(url) => Jump::Url(url.clone()),
Destination::Location(loc) => { Destination::Position(pos) => Jump::Position(*pos),
Jump::Position(document.introspector.position(*loc)) Destination::Location(loc) => {
} Jump::Position(document.introspector.position(*loc))
}); }
} });
} }
} }
// 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
@ -177,11 +177,11 @@ 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 {
@ -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,10 +59,10 @@ 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.
@ -124,13 +124,13 @@ 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(); {
for ident in pattern.bindings() { let pattern = v.pattern();
if let Some(res) = recv(NamedItem::Var(ident)) { for ident in pattern.bindings() {
return Some(res); if let Some(res) = recv(NamedItem::Var(ident)) {
} return Some(res);
} }
} }
} }
@ -155,10 +155,10 @@ 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);
} }
} }
} }
@ -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,7 +9,7 @@ use typst::layout::{Abs, Margin, PageElem};
use typst::syntax::package::{PackageSpec, PackageVersion}; use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash}; use typst::utils::{LazyHash, singleton};
use typst::{Feature, Library, LibraryExt, World}; use typst::{Feature, Library, LibraryExt, World};
use crate::IdeWorld; use crate::IdeWorld;

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,10 +65,10 @@ 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);
} }
} }
@ -93,10 +92,10 @@ 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() {
@ -109,19 +108,17 @@ 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,50 +187,45 @@ 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>;

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

@ -407,10 +407,10 @@ 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

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,10 +684,10 @@ 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);

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,39 +274,39 @@ 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 {
// possible presence of the footer. // Ensure rows in the first region will be aware of the
self.prepare_footer(footer, engine, 0)?; // possible presence of the footer.
self.regions.size.y -= self.current.footer_height; self.prepare_footer(footer, engine, 0)?;
self.current.initial_after_repeats = self.regions.size.y; self.regions.size.y -= self.current.footer_height;
} 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
if y == footer.start { && y >= footer.start
self.layout_footer(footer, engine, self.finished.len())?; {
self.flush_orphans(); if y == footer.start {
} self.layout_footer(footer, engine, self.finished.len())?;
y = footer.end; self.flush_orphans();
continue;
} }
y = footer.end;
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,14 +1283,12 @@ 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
&& breakable
&& first.is_empty()
&& rest.iter().any(|frame| !frame.is_empty())
{ {
if can_skip return Ok(None);
&& breakable
&& first.is_empty()
&& rest.iter().any(|frame| !frame.is_empty())
{
return Ok(None);
}
} }
// Skip frames from previous regions if applicable. // Skip frames from previous regions if applicable.
@ -1529,16 +1527,16 @@ 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.repeated_header_rows = self.current.lrows.truncate(orphan_snapshot);
self.current.repeated_header_rows.min(orphan_snapshot); self.current.repeated_header_rows =
self.current.repeated_header_rows.min(orphan_snapshot);
if orphan_snapshot == 0 { if orphan_snapshot == 0 {
// Removed all repeated headers. // Removed all repeated headers.
self.current.last_repeated_header_end = 0; self.current.last_repeated_header_end = 0;
}
} }
} }
@ -1571,21 +1569,19 @@ 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). //
// // TODO(subfooters): this check can be replaced by a vector of
// TODO(subfooters): this check can be replaced by a vector of // repeating footers in the future, and/or some "pending
// repeating footers in the future, and/or some "pending // footers" vector for footers we're about to place.
// footers" vector for footers we're about to place. if footer.repeated
if footer.repeated && self.current.lrows.iter().all(|row| row.index() < footer.start)
&& self.current.lrows.iter().all(|row| row.index() < footer.start) {
{ laid_out_footer_start = Some(footer.start);
laid_out_footer_start = Some(footer.start); self.layout_footer(footer, engine, self.finished.len())?;
self.layout_footer(footer, engine, self.finished.len())?;
}
} }
} }

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,12 +291,12 @@ 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, {
// so don't draw it here. // There is a colspan cell going through this vline's position,
return None; // so don't draw it here.
} return None;
} }
} }
@ -416,26 +416,26 @@ 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. {
// The 'parent.y' row and any other spanned rows above 'y' could be // Get the first 'y' spanned by the possible rowspan in this region.
// missing from this region, which could have lead the check above // The 'parent.y' row and any other spanned rows above 'y' could be
// to be triggered, even though there is no spanned row above the // missing from this region, which could have lead the check above
// hline in the final layout of this region, and thus no overlap // to be triggered, even though there is no spanned row above the
// with the hline, allowing it to be drawn regardless of the // hline in the final layout of this region, and thus no overlap
// theoretical presence of a rowspan going across its position. // with the hline, allowing it to be drawn regardless of the
let local_parent_y = rows // theoretical presence of a rowspan going across its position.
.iter() let local_parent_y = rows
.find(|row| row.y >= parent.y) .iter()
.map(|row| row.y) .find(|row| row.y >= parent.y)
.unwrap_or(y); .map(|row| row.y)
.unwrap_or(y);
if local_parent_y < y { if local_parent_y < y {
// There is a rowspan cell going through this hline's // There is a rowspan cell going through this hline's
// position, so don't draw it here. // position, so don't draw it here.
return None; return None;
}
} }
} }
} }

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,16 +240,17 @@ 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
// Simulate the footer again; the region's 'full' might have && skipped_region
// changed. {
self.regions.size.y += self.current.footer_height; // Simulate the footer again; the region's 'full' might have
self.current.footer_height = self // changed.
.simulate_footer(footer, &self.regions, engine, disambiguator)? self.regions.size.y += self.current.footer_height;
.height; self.current.footer_height = self
self.regions.size.y -= self.current.footer_height; .simulate_footer(footer, &self.regions, engine, disambiguator)?
} .height;
self.regions.size.y -= self.current.footer_height;
} }
let repeating_header_rows = let repeating_header_rows =

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,15 +238,16 @@ 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
// Non-repeated footer, so keep it unbreakable. && current_row >= footer.start
// {
// TODO(subfooters): This will become unnecessary // Non-repeated footer, so keep it unbreakable.
// once non-repeated footers are treated differently and //
// have widow prevention. // TODO(subfooters): This will become unnecessary
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start); // once non-repeated footers are treated differently and
} // have widow prevention.
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(
@ -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

@ -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,11 +274,11 @@ 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; {
return; *last_len += segment_len;
} 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,17 +155,17 @@ 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.
@ -218,10 +218,10 @@ 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,30 +461,26 @@ 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)
{ {
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size); let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size);
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

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,12 +136,12 @@ 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); {
start = last_end; lines.push(last_attempt);
attempt = line(engine, p, start..end, breakpoint, lines.last()); start = last_end;
} 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
@ -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;
}; };
ctx.used.push(font.clone()); // This font has been exhausted and will not be used again.
if covers.is_none() {
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

@ -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,11 +1,10 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::AccentElem; use typst_library::math::AccentElem;
use super::{ use super::{
style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext, FrameFragment, MathContext, MathFragment, style_cramped, style_dtls, style_flac,
MathFragment,
}; };
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
@ -27,14 +26,17 @@ pub fn layout_accent(
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles }; if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
let cramped = style_cramped(); let cramped = style_cramped();
let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?; let base_styles = base_styles.chain(&cramped);
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
let (font, size) = base.font(ctx, base_styles, elem.base.span())?;
// Preserve class to preserve automatic spacing. // Preserve class to preserve automatic spacing.
let base_class = base.class(); let base_class = base.class();
let base_attach = base.accent_attach(); let base_attach = base.accent_attach();
// Try to replace the accent glyph with its flattened variant. // Try to replace the accent glyph with its flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flattened_base_height = value!(font, flattened_accent_base_height).at(size);
let flac = style_flac(); let flac = style_flac();
let accent_styles = if top_accent && base.ascent() > flattened_base_height { let accent_styles = if top_accent && base.ascent() > flattened_base_height {
styles.chain(&flac) styles.chain(&flac)
@ -42,23 +44,25 @@ pub fn layout_accent(
styles styles
}; };
let mut glyph = let mut accent = ctx.layout_into_fragment(
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; &SymbolElem::packed(accent.0).spanned(elem.span()),
accent_styles,
)?;
// Forcing the accent to be at least as large as the base makes it too wide // Forcing the accent to be at least as large as the base makes it too wide
// in many cases. // in many cases.
let width = elem.size.resolve(styles).relative_to(base.width()); let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size); let short_fall = ACCENT_SHORT_FALL.at(size);
glyph.stretch_horizontal(ctx, width - short_fall); accent.stretch_horizontal(ctx, width - short_fall);
let accent_attach = glyph.accent_attach.0; let accent_attach = accent.accent_attach().0;
let accent = glyph.into_frame(); let accent = accent.into_frame();
let (gap, accent_pos, base_pos) = if top_accent { let (gap, accent_pos, base_pos) = if top_accent {
// Descent is negative because the accent's ink bottom is above the // Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent // baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we // minus the accent base height. Only if the base is very small, we
// need a larger gap so that the accent doesn't move too low. // need a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height); let accent_base_height = value!(font, accent_base_height).at(size);
let gap = -accent.descent() - base.ascent().min(accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height);
let accent_pos = Point::with_x(base_attach.0 - accent_attach); let accent_pos = Point::with_x(base_attach.0 - accent_attach);
let base_pos = Point::with_y(accent.height() + gap); let base_pos = Point::with_y(accent.height() + gap);

View File

@ -4,11 +4,13 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{ use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
}; };
use typst_library::text::Font;
use typst_syntax::Span;
use typst_utils::OptionExt; use typst_utils::OptionExt;
use super::{ use super::{
stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits, FrameFragment, Limits, MathContext, MathFragment, stretch_fragment,
MathContext, MathFragment, style_for_subscript, style_for_superscript,
}; };
macro_rules! measure { macro_rules! measure {
@ -83,7 +85,7 @@ pub fn layout_attach(
layout!(br, sub_style_chain)?, layout!(br, sub_style_chain)?,
]; ];
layout_attachments(ctx, styles, base, fragments) layout_attachments(ctx, styles, base, elem.base.span(), fragments)
} }
/// Lays out a [`PrimeElem`]. /// Lays out a [`PrimeElem`].
@ -102,13 +104,19 @@ pub fn layout_primes(
4 => '⁗', 4 => '⁗',
_ => unreachable!(), _ => unreachable!(),
}; };
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; let f = ctx.layout_into_fragment(
&SymbolElem::packed(c).spanned(elem.span()),
styles,
)?;
ctx.push(f); ctx.push(f);
} }
count => { count => {
// Custom amount of primes // Custom amount of primes
let prime = ctx let prime = ctx
.layout_into_fragment(&SymbolElem::packed(''), styles)? .layout_into_fragment(
&SymbolElem::packed('').spanned(elem.span()),
styles,
)?
.into_frame(); .into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0; let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height())); let mut frame = Frame::soft(Size::new(width, prime.height()));
@ -170,22 +178,25 @@ fn layout_attachments(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
base: MathFragment, base: MathFragment,
span: Span,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> { ) -> SourceResult<()> {
let base_class = base.class(); let class = base.class();
let (font, size) = base.font(ctx, styles, span)?;
let cramped = styles.get(EquationElem::cramped);
// Calculate the distance from the base's baseline to the superscripts' and // Calculate the distance from the base's baseline to the superscripts' and
// subscripts' baseline. // subscripts' baseline.
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) { let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
(Abs::zero(), Abs::zero()) (Abs::zero(), Abs::zero())
} else { } else {
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br]) compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
}; };
// Calculate the distance from the base's baseline to the top attachment's // Calculate the distance from the base's baseline to the top attachment's
// and bottom attachment's baseline. // and bottom attachment's baseline.
let (t_shift, b_shift) = let (t_shift, b_shift) =
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]); compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
// Calculate the final frame height. // Calculate the final frame height.
let ascent = base let ascent = base
@ -215,7 +226,7 @@ fn layout_attachments(
// `space_after_script` is extra spacing that is at the start before each // `space_after_script` is extra spacing that is at the start before each
// pre-script, and at the end after each post-script (see the MathConstants // pre-script, and at the end after each post-script (see the MathConstants
// table in the OpenType MATH spec). // table in the OpenType MATH spec).
let space_after_script = scaled!(ctx, styles, space_after_script); let space_after_script = value!(font, space_after_script).at(size);
// Calculate the distance each pre-script extends to the left of the base's // Calculate the distance each pre-script extends to the left of the base's
// width. // width.
@ -272,7 +283,7 @@ fn layout_attachments(
layout!(b, b_x, b_y); // lower-limit layout!(b, b_x, b_y); // lower-limit
// Done! Note that we retain the class of the base. // Done! Note that we retain the class of the base.
ctx.push(FrameFragment::new(styles, frame).with_class(base_class)); ctx.push(FrameFragment::new(styles, frame).with_class(class));
Ok(()) Ok(())
} }
@ -364,8 +375,8 @@ fn compute_limit_widths(
/// Returns two lengths, the first being the distance to the upper-limit's /// Returns two lengths, the first being the distance to the upper-limit's
/// baseline and the second being the distance to the lower-limit's baseline. /// baseline and the second being the distance to the lower-limit's baseline.
fn compute_limit_shifts( fn compute_limit_shifts(
ctx: &MathContext, font: &Font,
styles: StyleChain, font_size: Abs,
base: &MathFragment, base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2], [t, b]: [Option<&MathFragment>; 2],
) -> (Abs, Abs) { ) -> (Abs, Abs) {
@ -373,16 +384,15 @@ fn compute_limit_shifts(
// ascender of the limits respectively, whereas `upper_rise_min` and // ascender of the limits respectively, whereas `upper_rise_min` and
// `lower_drop_min` give gaps to each limit's baseline (see the // `lower_drop_min` give gaps to each limit's baseline (see the
// MathConstants table in the OpenType MATH spec). // MathConstants table in the OpenType MATH spec).
let t_shift = t.map_or_default(|t| { let t_shift = t.map_or_default(|t| {
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min); let upper_gap_min = value!(font, upper_limit_gap_min).at(font_size);
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min); let upper_rise_min = value!(font, upper_limit_baseline_rise_min).at(font_size);
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent()) base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
}); });
let b_shift = b.map_or_default(|b| { let b_shift = b.map_or_default(|b| {
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min); let lower_gap_min = value!(font, lower_limit_gap_min).at(font_size);
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min); let lower_drop_min = value!(font, lower_limit_baseline_drop_min).at(font_size);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent()) base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
}); });
@ -393,25 +403,27 @@ fn compute_limit_shifts(
/// Returns two lengths, the first being the distance to the superscripts' /// Returns two lengths, the first being the distance to the superscripts'
/// baseline and the second being the distance to the subscripts' baseline. /// baseline and the second being the distance to the subscripts' baseline.
fn compute_script_shifts( fn compute_script_shifts(
ctx: &MathContext, font: &Font,
styles: StyleChain, font_size: Abs,
cramped: bool,
base: &MathFragment, base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4], [tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) { ) -> (Abs, Abs) {
let sup_shift_up = if styles.get(EquationElem::cramped) { let sup_shift_up = (if cramped {
scaled!(ctx, styles, superscript_shift_up_cramped) value!(font, superscript_shift_up_cramped)
} else { } else {
scaled!(ctx, styles, superscript_shift_up) value!(font, superscript_shift_up)
}; })
.at(font_size);
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min); let sup_bottom_min = value!(font, superscript_bottom_min).at(font_size);
let sup_bottom_max_with_sub = let sup_bottom_max_with_sub =
scaled!(ctx, styles, superscript_bottom_max_with_subscript); value!(font, superscript_bottom_max_with_subscript).at(font_size);
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max); let sup_drop_max = value!(font, superscript_baseline_drop_max).at(font_size);
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min); let gap_min = value!(font, sub_superscript_gap_min).at(font_size);
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down); let sub_shift_down = value!(font, subscript_shift_down).at(font_size);
let sub_top_max = scaled!(ctx, styles, subscript_top_max); let sub_top_max = value!(font, subscript_top_max).at(font_size);
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min); let sub_drop_min = value!(font, subscript_baseline_drop_min).at(font_size);
let mut shift_up = Abs::zero(); let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero(); let mut shift_down = Abs::zero();

View File

@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment, DELIM_SHORT_FALL, FrameFragment, MathContext, find_math_font, style_for_denominator,
MathContext, DELIM_SHORT_FALL, style_for_numerator,
}; };
const FRAC_AROUND: Em = Em::new(0.1); const FRAC_AROUND: Em = Em::new(0.1);
@ -49,29 +49,33 @@ fn layout_frac_like(
binom: bool, binom: bool,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles); let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = scaled!(ctx, styles, axis_height); let axis = value!(font, axis_height).resolve(styles);
let thickness = scaled!(ctx, styles, fraction_rule_thickness); let thickness = value!(font, fraction_rule_thickness).resolve(styles);
let shift_up = scaled!( let shift_up = value!(
ctx, styles, font, styles,
text: fraction_numerator_shift_up, text: fraction_numerator_shift_up,
display: fraction_numerator_display_style_shift_up, display: fraction_numerator_display_style_shift_up,
); )
let shift_down = scaled!( .resolve(styles);
ctx, styles, let shift_down = value!(
font, styles,
text: fraction_denominator_shift_down, text: fraction_denominator_shift_down,
display: fraction_denominator_display_style_shift_down, display: fraction_denominator_display_style_shift_down,
); )
let num_min = scaled!( .resolve(styles);
ctx, styles, let num_min = value!(
font, styles,
text: fraction_numerator_gap_min, text: fraction_numerator_gap_min,
display: fraction_num_display_style_gap_min, display: fraction_num_display_style_gap_min,
); )
let denom_min = scaled!( .resolve(styles);
ctx, styles, let denom_min = value!(
font, styles,
text: fraction_denominator_gap_min, text: fraction_denominator_gap_min,
display: fraction_denom_display_style_gap_min, display: fraction_denom_display_style_gap_min,
); )
.resolve(styles);
let num_style = style_for_numerator(styles); let num_style = style_for_numerator(styles);
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?; let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
@ -82,7 +86,7 @@ fn layout_frac_like(
// Add a comma between each element. // Add a comma between each element.
denom denom
.iter() .iter()
.flat_map(|a| [SymbolElem::packed(','), a.clone()]) .flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()])
.skip(1), .skip(1),
), ),
styles.chain(&denom_style), styles.chain(&denom_style),
@ -109,12 +113,18 @@ fn layout_frac_like(
frame.push_frame(denom_pos, denom); frame.push_frame(denom_pos, denom);
if binom { if binom {
let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?; let short_fall = DELIM_SHORT_FALL.resolve(styles);
let mut left =
ctx.layout_into_fragment(&SymbolElem::packed('(').spanned(span), styles)?;
left.stretch_vertical(ctx, height - short_fall); left.stretch_vertical(ctx, height - short_fall);
left.center_on_axis(); left.center_on_axis();
ctx.push(left); ctx.push(left);
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));
let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?;
let mut right =
ctx.layout_into_fragment(&SymbolElem::packed(')').spanned(span), styles)?;
right.stretch_vertical(ctx, height - short_fall); right.stretch_vertical(ctx, height - short_fall);
right.center_on_axis(); right.center_on_axis();
ctx.push(right); ctx.push(right);

View File

@ -2,21 +2,25 @@ use std::fmt::{self, Debug, Formatter};
use az::SaturatingAs; use az::SaturatingAs;
use rustybuzz::{BufferFlags, UnicodeBuffer}; use rustybuzz::{BufferFlags, UnicodeBuffer};
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
use typst_library::diag::{bail, warning, SourceResult}; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use typst_library::World;
use typst_library::diag::{SourceResult, bail, warning};
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag; use typst_library::introspection::Tag;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
}; };
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem}; use typst_library::text::{
Font, Glyph, TextElem, TextItem, families, features, language, variant,
};
use typst_library::visualize::Paint;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{default_math_class, Get}; use typst_utils::{Get, default_math_class};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::MathContext; use super::{MathContext, find_math_font};
use crate::inline::create_shape_plan; use crate::inline::create_shape_plan;
use crate::modifiers::{FrameModifiers, FrameModify}; use crate::modifiers::{FrameModifiers, FrameModify};
@ -108,6 +112,21 @@ impl MathFragment {
} }
} }
pub fn font(
&self,
ctx: &MathContext,
styles: StyleChain,
span: Span,
) -> SourceResult<(Font, Abs)> {
Ok((
match self {
Self::Glyph(glyph) => glyph.item.font.clone(),
_ => find_math_font(ctx.engine.world, styles, span)?,
},
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
))
}
pub fn font_size(&self) -> Option<Abs> { pub fn font_size(&self) -> Option<Abs> {
match self { match self {
Self::Glyph(glyph) => Some(glyph.item.size), Self::Glyph(glyph) => Some(glyph.item.size),
@ -192,6 +211,31 @@ impl MathFragment {
} }
} }
pub fn fill(&self) -> Option<Paint> {
match self {
Self::Glyph(glyph) => Some(glyph.item.fill.clone()),
_ => None,
}
}
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
if let Self::Glyph(glyph) = self {
glyph.stretch_vertical(ctx, height)
}
}
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
if let Self::Glyph(glyph) = self {
glyph.stretch_horizontal(ctx, width)
}
}
pub fn center_on_axis(&mut self) {
if let Self::Glyph(glyph) = self {
glyph.center_on_axis()
}
}
/// If no kern table is provided for a corner, a kerning amount of zero is /// If no kern table is provided for a corner, a kerning amount of zero is
/// assumed. /// assumed.
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs { pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
@ -261,23 +305,70 @@ pub struct GlyphFragment {
impl GlyphFragment { impl GlyphFragment {
/// Calls `new` with the given character. /// Calls `new` with the given character.
pub fn new_char( pub fn new_char(
font: &Font, ctx: &MathContext,
styles: StyleChain, styles: StyleChain,
c: char, c: char,
span: Span, span: Span,
) -> SourceResult<Self> { ) -> SourceResult<Option<Self>> {
Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span) Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)
}
/// Selects a font to use and then shapes text.
pub fn new(
ctx: &MathContext,
styles: StyleChain,
text: &str,
span: Span,
) -> SourceResult<Option<Self>> {
let families = families(styles);
let variant = variant(styles);
let fallback = styles.get(TextElem::fallback);
let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len());
// Find the next available family.
let world = ctx.engine.world;
let book = world.book();
let mut selection = None;
for family in families {
selection = book
.select(family.as_str(), variant)
.and_then(|id| world.font(id))
.filter(|font| {
font.ttf().tables().math.and_then(|math| math.constants).is_some()
})
.filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end])));
if selection.is_some() {
break;
}
}
// Do font fallback if the families are exhausted and fallback is enabled.
if selection.is_none() && fallback {
selection = book
.select_fallback(None, variant, text)
.and_then(|id| world.font(id))
.filter(|font| {
font.ttf().tables().math.and_then(|math| math.constants).is_some()
});
}
// Error out if no math font could be found at all.
let Some(font) = selection else {
bail!(span, "current font does not support math");
};
Self::shape(&font, styles, text, span)
} }
/// Try to create a new glyph out of the given string. Will bail if the /// Try to create a new glyph out of the given string. Will bail if the
/// result from shaping the string is not a single glyph or is a tofu. /// result from shaping the string is more than a single glyph.
#[comemo::memoize] #[comemo::memoize]
pub fn new( pub fn shape(
font: &Font, font: &Font,
styles: StyleChain, styles: StyleChain,
text: &str, text: &str,
span: Span, span: Span,
) -> SourceResult<GlyphFragment> { ) -> SourceResult<Option<GlyphFragment>> {
let mut buffer = UnicodeBuffer::new(); let mut buffer = UnicodeBuffer::new();
buffer.push_str(text); buffer.push_str(text);
buffer.set_language(language(styles)); buffer.set_language(language(styles));
@ -300,18 +391,15 @@ impl GlyphFragment {
); );
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
if buffer.len() != 1 { match buffer.len() {
bail!(span, "did not get a single glyph after shaping {}", text); 0 => return Ok(None),
1 => {}
_ => bail!(span, "did not get a single glyph after shaping {}", text),
} }
let info = buffer.glyph_infos()[0]; let info = buffer.glyph_infos()[0];
let pos = buffer.glyph_positions()[0]; let pos = buffer.glyph_positions()[0];
// TODO: add support for coverage and fallback, like in normal text shaping.
if info.glyph_id == 0 {
bail!(span, "current font is missing a glyph for {}", text);
}
let cluster = info.cluster as usize; let cluster = info.cluster as usize;
let c = text[cluster..].chars().next().unwrap(); let c = text[cluster..].chars().next().unwrap();
let limits = Limits::for_char(c); let limits = Limits::for_char(c);
@ -361,7 +449,7 @@ impl GlyphFragment {
modifiers: FrameModifiers::get_in(styles), modifiers: FrameModifiers::get_in(styles),
}; };
fragment.update_glyph(); fragment.update_glyph();
Ok(fragment) Ok(Some(fragment))
} }
/// Sets element id and boxes in appropriate way without changing other /// Sets element id and boxes in appropriate way without changing other
@ -681,7 +769,11 @@ fn min_connector_overlap(font: &Font) -> Option<Em> {
.map(|variants| font.to_em(variants.min_connector_overlap)) .map(|variants| font.to_em(variants.min_connector_overlap))
} }
fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option<GlyphConstruction> { fn glyph_construction(
font: &Font,
id: GlyphId,
axis: Axis,
) -> Option<GlyphConstruction<'_>> {
font.ttf() font.ttf()
.tables() .tables()
.math? .math?
@ -810,7 +902,10 @@ fn assemble(
/// Return an iterator over the assembly's parts with extenders repeated the /// Return an iterator over the assembly's parts with extenders repeated the
/// specified number of times. /// specified number of times.
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { fn parts(
assembly: GlyphAssembly<'_>,
repeat: usize,
) -> impl Iterator<Item = GlyphPart> + '_ {
assembly.parts.into_iter().flat_map(move |part| { assembly.parts.into_iter().flat_map(move |part| {
let count = if part.part_flags.extender() { repeat } else { 1 }; let count = if part.part_flags.extender() { repeat } else { 1 };
std::iter::repeat_n(part, count) std::iter::repeat_n(part, count)

View File

@ -5,7 +5,7 @@ use typst_library::math::{EquationElem, LrElem, MidElem};
use typst_utils::SliceExt; use typst_utils::SliceExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; use super::{DELIM_SHORT_FALL, MathContext, MathFragment, stretch_fragment};
/// Lays out an [`LrElem`]. /// Lays out an [`LrElem`].
#[typst_macros::time(name = "math.lr", span = elem.span())] #[typst_macros::time(name = "math.lr", span = elem.span())]
@ -21,10 +21,10 @@ pub fn layout_lr(
} }
// Extract implicit LrElem. // Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() { if let Some(lr) = body.to_packed::<LrElem>()
if lr.size.get(styles).is_one() { && lr.size.get(styles).is_one()
body = &lr.body; {
} body = &lr.body;
} }
let mut fragments = ctx.layout_into_fragments(body, styles)?; let mut fragments = ctx.layout_into_fragments(body, styles)?;
@ -33,12 +33,13 @@ pub fn layout_lr(
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
let inner_fragments = &mut fragments[start_idx..end_idx]; let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height); let mut max_extent = Abs::zero();
let max_extent = inner_fragments for fragment in inner_fragments.iter() {
.iter() let (font, size) = fragment.font(ctx, styles, elem.span())?;
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) let axis = value!(font, axis_height).at(size);
.max() let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
.unwrap_or_default(); max_extent = max_extent.max(extent);
}
let relative_to = 2.0 * max_extent; let relative_to = 2.0 * max_extent;
let height = elem.size.resolve(styles); let height = elem.size.resolve(styles);
@ -55,11 +56,11 @@ pub fn layout_lr(
// Handle MathFragment::Glyph fragments that should be scaled up. // Handle MathFragment::Glyph fragments that should be scaled up.
for fragment in inner_fragments.iter_mut() { for fragment in inner_fragments.iter_mut() {
if let MathFragment::Glyph(ref mut glyph) = fragment { if let MathFragment::Glyph(glyph) = fragment
if glyph.mid_stretched == Some(false) { && glyph.mid_stretched == Some(false)
glyph.mid_stretched = Some(true); {
scale(ctx, fragment, relative_to, height); glyph.mid_stretched = Some(true);
} scale(ctx, fragment, relative_to, height);
} }
} }
@ -95,7 +96,7 @@ pub fn layout_mid(
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments { for fragment in &mut fragments {
if let MathFragment::Glyph(ref mut glyph) = fragment { if let MathFragment::Glyph(glyph) = fragment {
glyph.mid_stretched = Some(false); glyph.mid_stretched = Some(false);
glyph.class = MathClass::Relation; glyph.class = MathClass::Relation;
} }

View File

@ -1,5 +1,5 @@
use typst_library::diag::{bail, warning, SourceResult}; use typst_library::diag::{SourceResult, bail, warning};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
}; };
@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, AlignmentResult, DELIM_SHORT_FALL, FrameFragment, GlyphFragment, LeftRightAlternator,
LeftRightAlternator, MathContext, DELIM_SHORT_FALL, MathContext, alignments, find_math_font, style_for_denominator,
}; };
const VERTICAL_PADDING: Ratio = Ratio::new(0.1); const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -186,12 +186,10 @@ fn layout_body(
// We pad ascent and descent with the ascent and descent of the paren // We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are // to ensure that normal matrices are aligned with others unless they are
// way too big. // way too big.
let paren = GlyphFragment::new_char( // This will never panic as a paren will never shape into nothing.
ctx.font, let paren =
styles.chain(&denom_style), GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
'(', .unwrap();
Span::detached(),
)?;
for (column, col) in columns.iter().zip(&mut cols) { for (column, col) in columns.iter().zip(&mut cols) {
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
@ -314,13 +312,15 @@ fn layout_delimiters(
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles); let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height); let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
let height = frame.height(); let height = frame.height();
let target = height + VERTICAL_PADDING.of(height); let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis); frame.set_baseline(height / 2.0 + axis);
if let Some(left_c) = left { if let Some(left_c) = left {
let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?; let mut left =
ctx.layout_into_fragment(&SymbolElem::packed(left_c).spanned(span), styles)?;
left.stretch_vertical(ctx, target - short_fall); left.stretch_vertical(ctx, target - short_fall);
left.center_on_axis(); left.center_on_axis();
ctx.push(left); ctx.push(left);
@ -329,7 +329,8 @@ fn layout_delimiters(
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));
if let Some(right_c) = right { if let Some(right_c) = right {
let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?; let mut right =
ctx.layout_into_fragment(&SymbolElem::packed(right_c).spanned(span), styles)?;
right.stretch_vertical(ctx, target - short_fall); right.stretch_vertical(ctx, target - short_fall);
right.center_on_axis(); right.center_on_axis();
ctx.push(right); ctx.push(right);

View File

@ -13,7 +13,7 @@ mod stretch;
mod text; mod text;
mod underover; mod underover;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{ use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
@ -27,16 +27,12 @@ use typst_library::layout::{
use typst_library::math::*; use typst_library::math::*;
use typst_library::model::ParElem; use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind}; use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{ use typst_library::text::{LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, TextElem};
families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
};
use typst_library::World;
use typst_syntax::Span;
use typst_utils::Numeric; use typst_utils::Numeric;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use self::fragment::{ use self::fragment::{
has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment, FrameFragment, GlyphFragment, Limits, MathFragment, has_dtls_feat, stretch_axes,
}; };
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder}; use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
use self::shared::*; use self::shared::*;
@ -53,12 +49,11 @@ pub fn layout_equation_inline(
) -> SourceResult<Vec<InlineItem>> { ) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block.get(styles)); assert!(!elem.block.get(styles));
let font = find_math_font(engine, styles, elem.span())?;
let mut locator = locator.split(); let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, region, &font); let mut ctx = MathContext::new(engine, &mut locator, region);
let scale_style = style_for_script_scale(&ctx); let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style); let styles = styles.chain(&scale_style);
let run = ctx.layout_into_run(&elem.body, styles)?; let run = ctx.layout_into_run(&elem.body, styles)?;
@ -108,12 +103,12 @@ pub fn layout_equation_block(
assert!(elem.block.get(styles)); assert!(elem.block.get(styles));
let span = elem.span(); let span = elem.span();
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split(); let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font); let mut ctx = MathContext::new(engine, &mut locator, regions.base());
let scale_style = style_for_script_scale(&ctx); let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style); let styles = styles.chain(&scale_style);
let full_equation_builder = ctx let full_equation_builder = ctx
@ -234,24 +229,6 @@ pub fn layout_equation_block(
Ok(Fragment::frames(frames)) Ok(Fragment::frames(frames))
} }
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let world = engine.world;
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
Some(font)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
fn add_equation_number( fn add_equation_number(
equation_builder: MathRunFrameBuilder, equation_builder: MathRunFrameBuilder,
number: Frame, number: Frame,
@ -370,9 +347,6 @@ struct MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>, engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>, locator: &'v mut SplitLocator<'a>,
region: Region, region: Region,
// Font-related.
font: &'a Font,
constants: ttf_parser::math::Constants<'a>,
// Mutable. // Mutable.
fragments: Vec<MathFragment>, fragments: Vec<MathFragment>,
} }
@ -383,19 +357,11 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>, engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>, locator: &'v mut SplitLocator<'a>,
base: Size, base: Size,
font: &'a Font,
) -> Self { ) -> Self {
// These unwraps are safe as the font given is one returned by the
// find_math_font function, which only returns fonts that have a math
// constants table.
let constants = font.ttf().tables().math.unwrap().constants.unwrap();
Self { Self {
engine, engine,
locator, locator,
region: Region::new(base, Axes::splat(false)), region: Region::new(base, Axes::splat(false)),
font,
constants,
fragments: vec![], fragments: vec![],
} }
} }
@ -469,17 +435,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
styles, styles,
)?; )?;
let outer = styles;
for (elem, styles) in pairs { for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
{
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue;
}
layout_realized(elem, self, styles)?; layout_realized(elem, self, styles)?;
} }
@ -496,7 +452,10 @@ fn layout_realized(
if let Some(elem) = elem.to_packed::<TagElem>() { if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone())); ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() { } else if elem.is::<SpaceElem>() {
let space_width = ctx.font.space_width().unwrap_or(THICK); let space_width = find_math_font(ctx.engine.world, styles, elem.span())
.ok()
.and_then(|font| font.space_width())
.unwrap_or(THICK);
ctx.push(MathFragment::Space(space_width.resolve(styles))); ctx.push(MathFragment::Space(space_width.resolve(styles)));
} else if elem.is::<LinebreakElem>() { } else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak); ctx.push(MathFragment::Linebreak);
@ -566,9 +525,11 @@ fn layout_realized(
self::underover::layout_overshell(elem, ctx, styles)? self::underover::layout_overshell(elem, ctx, styles)?
} else { } else {
let mut frame = layout_external(elem, ctx, styles)?; let mut frame = layout_external(elem, ctx, styles)?;
if !frame.has_baseline() { if !frame.has_baseline() && !elem.is::<RawElem>() {
let axis = scaled!(ctx, styles, axis_height); if let Ok(font) = find_math_font(ctx.engine.world, styles, elem.span()) {
frame.set_baseline(frame.height() / 2.0 + axis); let axis = value!(font, axis_height).resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis);
}
} }
ctx.push( ctx.push(
FrameFragment::new(styles, frame) FrameFragment::new(styles, frame)
@ -603,13 +564,10 @@ fn layout_h(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount { if let Spacing::Rel(rel) = elem.amount
if rel.rel.is_zero() { && rel.rel.is_zero()
ctx.push(MathFragment::Spacing( {
rel.abs.resolve(styles), ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak.get(styles)));
elem.weak.get(styles),
));
}
} }
Ok(()) Ok(())
} }

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, SymbolElem};
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
use typst_library::math::{EquationElem, MathSize, RootElem}; use typst_library::math::{EquationElem, MathSize, RootElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry}; use typst_library::visualize::{FixedStroke, Geometry};
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext}; use super::{FrameFragment, MathContext, style_cramped};
/// Lays out a [`RootElem`]. /// Lays out a [`RootElem`].
/// ///
@ -17,45 +17,62 @@ pub fn layout_root(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = elem.index.get_ref(styles);
let span = elem.span(); let span = elem.span();
let gap = scaled!(
ctx, styles,
text: radical_vertical_gap,
display: radical_display_style_vertical_gap,
);
let thickness = scaled!(ctx, styles, radical_rule_thickness);
let extra_ascender = scaled!(ctx, styles, radical_extra_ascender);
let kern_before = scaled!(ctx, styles, radical_kern_before_degree);
let kern_after = scaled!(ctx, styles, radical_kern_after_degree);
let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
// Layout radicand. // Layout radicand.
let radicand = { let radicand = {
let cramped = style_cramped(); let cramped = style_cramped();
let styles = styles.chain(&cramped); let styles = styles.chain(&cramped);
let run = ctx.layout_into_run(&elem.radicand, styles)?; let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline(); let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame(); let radicand = run.into_fragment(styles);
if multiline { if multiline {
// Align the frame center line with the math axis. // Align the frame center line with the math axis.
radicand.set_baseline( let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?;
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height), let axis = value!(font, axis_height).at(size);
); let mut radicand = radicand.into_frame();
radicand.set_baseline(radicand.height() / 2.0 + axis);
radicand
} else {
radicand.into_frame()
} }
radicand
}; };
// Layout root symbol. // Layout root symbol.
let mut sqrt =
ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?;
let (font, size) = sqrt.font(ctx, styles, span)?;
let thickness = value!(font, radical_rule_thickness).at(size);
let extra_ascender = value!(font, radical_extra_ascender).at(size);
let kern_before = value!(font, radical_kern_before_degree).at(size);
let kern_after = value!(font, radical_kern_after_degree).at(size);
let raise_factor = percent!(font, radical_degree_bottom_raise_percent);
let gap = value!(
font, styles,
text: radical_vertical_gap,
display: radical_display_style_vertical_gap,
)
.at(size);
let line = FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair(
sqrt.fill()
.unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()),
thickness,
)),
span,
);
let target = radicand.height() + thickness + gap; let target = radicand.height() + thickness + gap;
let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
sqrt.stretch_vertical(ctx, target); sqrt.stretch_vertical(ctx, target);
let sqrt = sqrt.into_frame(); let sqrt = sqrt.into_frame();
// Layout the index. // Layout the index.
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap(); let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
let index = index let index = elem
.index
.get_ref(styles)
.as_ref() .as_ref()
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
.transpose()?; .transpose()?;
@ -107,19 +124,7 @@ pub fn layout_root(
} }
frame.push_frame(sqrt_pos, sqrt); frame.push_frame(sqrt_pos, sqrt);
frame.push( frame.push(line_pos, line);
line_pos,
FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(
FixedStroke::from_pair(
styles.get_ref(TextElem::fill).as_decoration(),
thickness,
),
),
span,
),
);
frame.push_frame(radicand_pos, radicand); frame.push_frame(radicand_pos, radicand);
ctx.push(FrameFragment::new(styles, frame)); ctx.push(FrameFragment::new(styles, frame));

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,10 +87,10 @@ 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,10 +123,10 @@ 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,10 +344,10 @@ 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,32 +1,47 @@
use ttf_parser::math::MathValue; use comemo::Tracked;
use ttf_parser::Tag; use ttf_parser::Tag;
use ttf_parser::math::MathValue;
use typst_library::World;
use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::{Style, StyleChain}; use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{FontFeatures, TextElem}; use typst_library::text::{Font, FontFeatures, TextElem, families, variant};
use typst_syntax::Span;
use typst_utils::LazyHash; use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; use super::{LeftRightAlternator, MathFragment, MathRun};
macro_rules! scaled { macro_rules! value {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { ($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match $styles.get(typst_library::math::EquationElem::size) { match $styles.get(typst_library::math::EquationElem::size) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), typst_library::math::MathSize::Display => value!($font, $display),
_ => scaled!($ctx, $styles, $text), _ => value!($font, $text),
} }
}; };
($ctx:expr, $styles:expr, $name:ident) => { ($font:expr, $name:ident) => {
$crate::math::Scaled::scaled( $font
$ctx.constants.$name(), .ttf()
$ctx, .tables()
$styles.resolve(typst_library::text::TextElem::size), .math
) .and_then(|math| math.constants)
.map(|constants| {
crate::math::shared::Scaled::scaled(constants.$name(), &$font)
})
.unwrap()
}; };
} }
macro_rules! percent { macro_rules! percent {
($ctx:expr, $name:ident) => { ($font:expr, $name:ident) => {
$ctx.constants.$name() as f64 / 100.0 $font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| constants.$name())
.unwrap() as f64
/ 100.0
}; };
} }
@ -35,27 +50,47 @@ pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// Converts some unit to an absolute length with the current font & font size. /// Converts some unit to an absolute length with the current font & font size.
pub trait Scaled { pub trait Scaled {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; fn scaled(self, font: &Font) -> Em;
} }
impl Scaled for i16 { impl Scaled for i16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { fn scaled(self, font: &Font) -> Em {
ctx.font.to_em(self).at(font_size) font.to_em(self)
} }
} }
impl Scaled for u16 { impl Scaled for u16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { fn scaled(self, font: &Font) -> Em {
ctx.font.to_em(self).at(font_size) font.to_em(self)
} }
} }
impl Scaled for MathValue<'_> { impl Scaled for MathValue<'_> {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { fn scaled(self, font: &Font) -> Em {
self.value.scaled(ctx, font_size) self.value.scaled(font)
} }
} }
/// Get the current math font.
#[comemo::memoize]
pub fn find_math_font(
world: Tracked<dyn World + '_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
// Take the base font as the "main" math font.
family.covers().map_or(Some(font), |_| None)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
/// Styles something as cramped. /// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> { pub fn style_cramped() -> LazyHash<Style> {
EquationElem::cramped.set(true).wrap() EquationElem::cramped.set(true).wrap()
@ -107,11 +142,12 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
} }
/// Styles to add font constants to the style chain. /// Styles to add font constants to the style chain.
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> { pub fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
let constants = font.ttf().tables().math.and_then(|math| math.constants).unwrap();
EquationElem::script_scale EquationElem::script_scale
.set(( .set((
ctx.constants.script_percent_scale_down(), constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(), constants.script_script_percent_scale_down(),
)) ))
.wrap() .wrap()
} }

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, Rel};
use typst_library::math::StretchElem; use typst_library::math::StretchElem;
use typst_utils::Get; use typst_utils::Get;
use super::{stretch_axes, MathContext, MathFragment}; use super::{MathContext, MathFragment, stretch_axes};
/// Lays out a [`StretchElem`]. /// Lays out a [`StretchElem`].
#[typst_macros::time(name = "math.stretch", span = elem.span())] #[typst_macros::time(name = "math.stretch", span = elem.span())]
@ -37,7 +37,7 @@ pub fn stretch_fragment(
) { ) {
let size = fragment.size(); let size = fragment.size();
let MathFragment::Glyph(ref mut glyph) = fragment else { return }; let MathFragment::Glyph(glyph) = fragment else { return };
// Return if we attempt to stretch along an axis which isn't stretchable, // Return if we attempt to stretch along an axis which isn't stretchable,
// so that the original fragment isn't modified. // so that the original fragment isn't modified.

View File

@ -1,21 +1,21 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use codex::styling::{to_style, MathStyle}; use codex::styling::{MathStyle, to_style};
use ecow::EcoString; use ecow::EcoString;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::foundations::{Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size}; use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{ use typst_library::text::{
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric, BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
}; };
use typst_syntax::{is_newline, Span}; use typst_syntax::{Span, is_newline};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{ use super::{
has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext, MathFragment, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun, find_math_font,
MathRun, has_dtls_feat, style_dtls,
}; };
/// Lays out a [`TextElem`]. /// Lays out a [`TextElem`].
@ -52,7 +52,8 @@ fn layout_text_lines<'a>(
} }
} }
let mut frame = MathRun::new(fragments).into_frame(styles); let mut frame = MathRun::new(fragments).into_frame(styles);
let axis = scaled!(ctx, styles, axis_height); let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis); frame.set_baseline(frame.height() / 2.0 + axis);
Ok(FrameFragment::new(styles, frame)) Ok(FrameFragment::new(styles, frame))
} }
@ -80,7 +81,9 @@ fn layout_inline_text(
let style = MathStyle::select(unstyled_c, variant, bold, italic); let style = MathStyle::select(unstyled_c, variant, bold, italic);
let c = to_style(unstyled_c, style).next().unwrap(); let c = to_style(unstyled_c, style).next().unwrap();
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; // This won't panic as ASCII digits and '.' will never end up as
// nothing after shaping.
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
fragments.push(glyph.into()); fragments.push(glyph.into());
} }
let frame = MathRun::new(fragments).into_frame(styles); let frame = MathRun::new(fragments).into_frame(styles);
@ -132,8 +135,11 @@ pub fn layout_symbol(
// Switch dotless char to normal when we have the dtls OpenType feature. // Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass. // This should happen before the main styling pass.
let dtls = style_dtls(); let dtls = style_dtls();
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) { let (unstyled_c, symbol_styles) = match (
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), try_dotless(elem.text),
find_math_font(ctx.engine.world, styles, elem.span()),
) {
(Some(c), Ok(font)) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
_ => (elem.text, styles), _ => (elem.text, styles),
}; };
@ -144,39 +150,22 @@ pub fn layout_symbol(
let style = MathStyle::select(unstyled_c, variant, bold, italic); let style = MathStyle::select(unstyled_c, variant, bold, italic);
let text: EcoString = to_style(unstyled_c, style).collect(); let text: EcoString = to_style(unstyled_c, style).collect();
let fragment: MathFragment = if let Some(mut glyph) = GlyphFragment::new(ctx, symbol_styles, &text, elem.span())? {
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) { if glyph.class == MathClass::Large {
Ok(mut glyph) => { if styles.get(EquationElem::size) == MathSize::Display {
adjust_glyph_layout(&mut glyph, ctx, styles); let height = value!(glyph.item.font, display_operator_min_height)
glyph.into() .at(glyph.item.size)
} .max(SQRT_2 * glyph.size.y);
Err(_) => { glyph.stretch_vertical(ctx, height);
// Not in the math font, fallback to normal inline text layout. };
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. // TeXbook p 155. Large operators are always vertically centered on
layout_inline_text(&text, elem.span(), ctx, styles)?.into() // the axis.
} glyph.center_on_axis();
}; }
ctx.push(fragment); ctx.push(glyph);
Ok(())
}
/// Centers large glyphs vertically on the axis, scaling them if in display
/// style.
fn adjust_glyph_layout(
glyph: &mut GlyphFragment,
ctx: &mut MathContext,
styles: StyleChain,
) {
if glyph.class == MathClass::Large {
if styles.get(EquationElem::size) == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height);
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
glyph.center_on_axis();
} }
Ok(())
} }
/// The non-dotless version of a dotless character that can be used with the /// The non-dotless version of a dotless character that can be used with the

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{ use typst_library::math::{
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem, OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment, FrameFragment, LeftRightAlternator, MathContext, MathRun, stack, style_cramped,
GlyphFragment, LeftRightAlternator, MathContext, MathRun, style_for_subscript, style_for_superscript,
}; };
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);
@ -208,26 +208,29 @@ fn layout_underoverline(
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust); let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
match position { match position {
Position::Under => { Position::Under => {
let sep = scaled!(ctx, styles, underbar_extra_descender);
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
let gap = scaled!(ctx, styles, underbar_vertical_gap);
extra_height = sep + bar_height + gap;
content = ctx.layout_into_fragment(body, styles)?; content = ctx.layout_into_fragment(body, styles)?;
let (font, size) = content.font(ctx, styles, span)?;
let sep = value!(font, underbar_extra_descender).at(size);
bar_height = value!(font, underbar_rule_thickness).at(size);
let gap = value!(font, underbar_vertical_gap).at(size);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
content_pos = Point::zero(); content_pos = Point::zero();
baseline = content.ascent(); baseline = content.ascent();
line_adjust = -content.italics_correction(); line_adjust = -content.italics_correction();
} }
Position::Over => { Position::Over => {
let sep = scaled!(ctx, styles, overbar_extra_ascender);
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
let gap = scaled!(ctx, styles, overbar_vertical_gap);
extra_height = sep + bar_height + gap;
let cramped = style_cramped(); let cramped = style_cramped();
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?; let styles = styles.chain(&cramped);
content = ctx.layout_into_fragment(body, styles)?;
let (font, size) = content.font(ctx, styles, span)?;
let sep = value!(font, overbar_extra_ascender).at(size);
bar_height = value!(font, overbar_rule_thickness).at(size);
let gap = value!(font, overbar_vertical_gap).at(size);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(sep + bar_height / 2.0); line_pos = Point::with_y(sep + bar_height / 2.0);
content_pos = Point::with_y(extra_height); content_pos = Point::with_y(extra_height);
@ -285,7 +288,8 @@ fn layout_underoverspreader(
let body = ctx.layout_into_run(body, styles)?; let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class(); let body_class = body.class();
let body = body.into_fragment(styles); let body = body.into_fragment(styles);
let mut glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; let mut glyph =
ctx.layout_into_fragment(&SymbolElem::packed(c).spanned(span), styles)?;
glyph.stretch_horizontal(ctx, body.width()); glyph.stretch_horizontal(ctx, body.width());
let mut rows = vec![]; let mut rows = vec![];

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

@ -1,12 +1,12 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use comemo::Track; use comemo::Track;
use ecow::{eco_format, EcoVec}; use ecow::{EcoVec, eco_format};
use smallvec::smallvec; use smallvec::smallvec;
use typst_library::diag::{bail, At, SourceResult}; use typst_library::diag::{At, SourceResult, bail};
use typst_library::foundations::{ use typst_library::foundations::{
dict, Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart, Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart,
StyleChain, Target, StyleChain, Target, dict,
}; };
use typst_library::introspection::{Counter, Locator, LocatorLink}; use typst_library::introspection::{Counter, Locator, LocatorLink};
use typst_library::layout::{ use typst_library::layout::{
@ -20,8 +20,8 @@ use typst_library::math::EquationElem;
use typst_library::model::{ use typst_library::model::{
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem, Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem, EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
LinkElem, LinkTarget, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
}; };
use typst_library::pdf::EmbedElem; use typst_library::pdf::EmbedElem;
use typst_library::text::{ use typst_library::text::{
@ -161,11 +161,7 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
let indent = elem.indent.get(styles); let indent = elem.indent.get(styles);
let hanging_indent = elem.hanging_indent.get(styles); let hanging_indent = elem.hanging_indent.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| { let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight { if tight { styles.get(ParElem::leading) } else { styles.get(ParElem::spacing) }
styles.get(ParElem::leading)
} else {
styles.get(ParElem::spacing)
}
}); });
let pad = hanging_indent + indent; let pad = hanging_indent + indent;
@ -216,14 +212,8 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| { const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
let body = elem.body.clone(); let body = elem.body.clone();
Ok(match &elem.dest { let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
LinkTarget::Dest(dest) => body.linked(dest.clone()), Ok(body.linked(dest))
LinkTarget::Label(label) => {
let elem = engine.introspector.query_label(*label).at(elem.span())?;
let dest = Destination::Location(elem.location().unwrap());
body.linked(dest)
}
})
}; };
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| { const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
@ -278,7 +268,7 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
BlockElem::new().with_body(Some(BlockBody::Content(realized))) BlockElem::new().with_body(Some(BlockBody::Content(realized)))
}; };
Ok(block.pack().spanned(span)) Ok(block.pack())
}; };
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| { const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
@ -332,8 +322,7 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| { const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
Ok(BlockElem::new() Ok(BlockElem::new()
.with_body(Some(BlockBody::Content(elem.realize(engine, styles)?))) .with_body(Some(BlockBody::Content(elem.realize(engine, styles)?)))
.pack() .pack())
.spanned(elem.span()))
}; };
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| { const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
@ -556,9 +545,7 @@ const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
}; };
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| { const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table) Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table).pack())
.pack()
.spanned(elem.span()))
}; };
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| { const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
@ -709,27 +696,19 @@ const ALIGN_RULE: ShowFn<AlignElem> =
|elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles))); |elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles)));
const PAD_RULE: ShowFn<PadElem> = |elem, _, _| { const PAD_RULE: ShowFn<PadElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad) Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad).pack())
.pack()
.spanned(elem.span()))
}; };
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| { const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns) Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns).pack())
.pack()
.spanned(elem.span()))
}; };
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| { const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack) Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack).pack())
.pack()
.spanned(elem.span()))
}; };
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| { const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid) Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid).pack())
.pack()
.spanned(elem.span()))
}; };
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| { const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
@ -759,33 +738,23 @@ fn show_cell(
} }
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| { const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move) Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move).pack())
.pack()
.spanned(elem.span()))
}; };
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| { const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale) Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale).pack())
.pack()
.spanned(elem.span()))
}; };
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| { const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate) Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate).pack())
.pack()
.spanned(elem.span()))
}; };
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| { const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew) Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew).pack())
.pack()
.spanned(elem.span()))
}; };
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| { const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat) Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat).pack())
.pack()
.spanned(elem.span()))
}; };
const HIDE_RULE: ShowFn<HideElem> = const HIDE_RULE: ShowFn<HideElem> =
@ -807,83 +776,66 @@ const LAYOUT_RULE: ShowFn<LayoutElem> = |elem, _, _| {
crate::flow::layout_fragment(engine, &result, locator, styles, regions) crate::flow::layout_fragment(engine, &result, locator, styles, regions)
}, },
) )
.pack() .pack())
.spanned(elem.span()))
}; };
const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| { const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image) Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image)
.with_width(elem.width.get(styles)) .with_width(elem.width.get(styles))
.with_height(elem.height.get(styles)) .with_height(elem.height.get(styles))
.pack() .pack())
.spanned(elem.span()))
}; };
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| { const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line).pack())
.pack()
.spanned(elem.span()))
}; };
const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| { const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect)
.with_width(elem.width.get(styles)) .with_width(elem.width.get(styles))
.with_height(elem.height.get(styles)) .with_height(elem.height.get(styles))
.pack() .pack())
.spanned(elem.span()))
}; };
const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| { const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square)
.with_width(elem.width.get(styles)) .with_width(elem.width.get(styles))
.with_height(elem.height.get(styles)) .with_height(elem.height.get(styles))
.pack() .pack())
.spanned(elem.span()))
}; };
const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| { const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse)
.with_width(elem.width.get(styles)) .with_width(elem.width.get(styles))
.with_height(elem.height.get(styles)) .with_height(elem.height.get(styles))
.pack() .pack())
.spanned(elem.span()))
}; };
const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| { const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle)
.with_width(elem.width.get(styles)) .with_width(elem.width.get(styles))
.with_height(elem.height.get(styles)) .with_height(elem.height.get(styles))
.pack() .pack())
.spanned(elem.span()))
}; };
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| { const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon).pack())
.pack()
.spanned(elem.span()))
}; };
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| { const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve).pack())
.pack()
.spanned(elem.span()))
}; };
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| { const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path) Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path).pack())
.pack()
.spanned(elem.span()))
}; };
const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| { const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
if elem.block.get(styles) { if elem.block.get(styles) {
Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block) Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block)
.pack() .pack())
.spanned(elem.span()))
} else { } else {
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline) Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline).pack())
.pack()
.spanned(elem.span()))
} }
}; };

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,13 +296,12 @@ 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.

View File

@ -1,6 +1,6 @@
use ecow::EcoString; use ecow::EcoString;
use crate::foundations::{ty, Repr}; use crate::foundations::{Repr, ty};
/// A type with two states. /// A type with two states.
/// ///

View File

@ -5,13 +5,13 @@ use std::ops::{Add, AddAssign, Deref};
use std::str::Utf8Error; use std::str::Utf8Error;
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::Lines; use typst_syntax::Lines;
use typst_utils::LazyHash; use typst_utils::LazyHash;
use crate::diag::{bail, StrResult}; use crate::diag::{StrResult, bail};
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value}; use crate::foundations::{Array, Reflect, Repr, Str, Value, cast, func, scope, ty};
/// A sequence of bytes. /// A sequence of bytes.
/// ///

View File

@ -7,8 +7,8 @@ use az::SaturatingAs;
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use typst_utils::{round_int_with_precision, round_with_precision}; use typst_utils::{round_int_with_precision, round_with_precision};
use crate::diag::{bail, At, HintedString, SourceResult, StrResult}; use crate::diag::{At, HintedString, SourceResult, StrResult, bail};
use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value}; use crate::foundations::{Decimal, IntoValue, Module, Scope, Value, cast, func, ops};
use crate::layout::{Angle, Fr, Length, Ratio}; use crate::layout::{Angle, Fr, Length, Ratio};
/// A module with calculation definitions. /// A module with calculation definitions.

View File

@ -14,7 +14,7 @@ use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
use crate::foundations::{ use crate::foundations::{
array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value, Fold, NativeElement, Packed, Repr, Str, Type, Value, array, repr,
}; };
/// Determine details of a type. /// Determine details of a type.
@ -347,13 +347,14 @@ impl CastInfo {
msg.hint(eco_format!("use `label({})` to create a label", s.repr())); msg.hint(eco_format!("use `label({})` to create a label", s.repr()));
} }
} }
} else if let Value::Decimal(_) = found { } else if let Value::Decimal(_) = found
if !matching_type && parts.iter().any(|p| p == "float") { && !matching_type
msg.hint(eco_format!( && parts.iter().any(|p| p == "float")
"if loss of precision is acceptable, explicitly cast the \ {
msg.hint(eco_format!(
"if loss of precision is acceptable, explicitly cast the \
decimal to a float with `float(value)`" decimal to a float with `float(value)`"
)); ));
}
} }
msg msg

View File

@ -11,8 +11,8 @@ use typst_utils::Static;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, Args, Content, ContentVtable, FieldAccessError, Func, ParamInfo, Repr, Scope, Args, Content, ContentVtable, FieldAccessError, Func, ParamInfo, Repr, Scope,
Selector, StyleChain, Styles, Value, Selector, StyleChain, Styles, Value, cast,
}; };
use crate::text::{Lang, Region}; use crate::text::{Lang, Region};
@ -243,7 +243,7 @@ pub trait Set {
pub trait Synthesize { pub trait Synthesize {
/// Prepare the element for show rule application. /// Prepare the element for show rule application.
fn synthesize(&mut self, engine: &mut Engine, styles: StyleChain) fn synthesize(&mut self, engine: &mut Engine, styles: StyleChain)
-> SourceResult<()>; -> SourceResult<()>;
} }
/// Defines built-in show set rules for an element. /// Defines built-in show set rules for an element.

View File

@ -3,7 +3,7 @@ use std::hash::Hash;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::sync::OnceLock; use std::sync::OnceLock;
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use crate::foundations::{ use crate::foundations::{
Container, Content, FieldVtable, Fold, FoldFn, IntoValue, NativeElement, Packed, Container, Content, FieldVtable, Fold, FoldFn, IntoValue, NativeElement, Packed,

View File

@ -17,7 +17,7 @@ use std::iter::{self, Sum};
use std::ops::{Add, AddAssign, ControlFlow}; use std::ops::{Add, AddAssign, ControlFlow};
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::Span; use typst_syntax::Span;
@ -26,8 +26,8 @@ use typst_utils::singleton;
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
func, repr, scope, ty, Context, Dict, IntoValue, Label, Property, Recipe, Context, Dict, IntoValue, Label, Property, Recipe, RecipeIndex, Repr, Selector, Str,
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value, Style, StyleChain, Styles, Value, func, repr, scope, ty,
}; };
use crate::introspection::Location; use crate::introspection::Location;
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
@ -174,10 +174,10 @@ impl Content {
id: u8, id: u8,
styles: Option<StyleChain>, styles: Option<StyleChain>,
) -> Result<Value, FieldAccessError> { ) -> Result<Value, FieldAccessError> {
if id == 255 { if id == 255
if let Some(label) = self.label() { && let Some(label) = self.label()
return Ok(label.into_value()); {
} return Ok(label.into_value());
} }
match self.0.handle().field(id) { match self.0.handle().field(id) {

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