Compare commits

...

111 Commits

Author SHA1 Message Date
Neven Villani
5d792c46ca Fix local issue with Rust version 2025-07-24 10:10:39 +02:00
Neven Villani
1617578a06
Merge branch 'main' into main 2025-07-24 10:02:30 +02:00
Neven Villani
26f0a1358c Move parts of the messages to hints 2025-07-24 09:30:00 +02:00
Neven Villani
3b84ce91e3 One comment out of date 2025-07-24 09:28:22 +02:00
Neven Villani
ddaec8aa85 Update in tests/ too the PathBuf in FileError 2025-07-24 09:28:22 +02:00
Neven Villani
3846955ce6 Report path on a FileError 2025-07-24 09:28:22 +02:00
Neven Villani
d148f3ebe6 those were temporary files 2025-07-24 09:28:16 +02:00
Neven Villani
3292aeaaa1 What's with the namespacing on the unit tests ? 2025-07-24 09:26:50 +02:00
Neven Villani
407b006dc6 first attempt 2025-07-24 09:26:50 +02:00
Neven Villani
7089a79b1e I want to get all of this working 2025-07-24 09:26:50 +02:00
Tobias Schmitz
7278d887cf
Fix bounding box computation for lines in curves (#6647)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-23 14:17:03 +00:00
Laurenz
78355421ad
Add pdf extension to image autocompletions (#6643) 2025-07-22 12:07:29 +00:00
Laurenz Stampfl
af2253ba16
Add support for PDF embedding (#6623)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-22 11:06:44 +00:00
Laurenz
b036fd97ab Reformat with 2024 edition 2025-07-21 15:22:29 +02:00
Laurenz
e81a5a6ef2 Yeet if_chain macro 2025-07-21 15:22:29 +02:00
Laurenz
c9c2315ad3 Fix 2024 clippy warnings 2025-07-21 15:22:29 +02:00
Laurenz
4bbd4e195b Migrate to 2024 edition 2025-07-21 15:22:28 +02:00
Laurenz
eed75ca4d6 Bump MSRV to 1.88 2025-07-21 15:22:28 +02:00
Laurenz
a43b7e785c Bump CI Rust to 1.88 2025-07-21 13:48:48 +02:00
pog102
55dad02887
Add Lithuanian translations (#6587) 2025-07-21 10:57:40 +00:00
Erik
b790c6d59c
Add rust-analyzer to flake devShell (#6618) 2025-07-18 14:36:10 +00:00
Malo
b1c79b50d4
Fix documentation oneliners (#6608) 2025-07-18 13:25:17 +00:00
Patrick Massot
4629ede020
Mention Tinymist in README.md (#6601) 2025-07-18 13:21:36 +00:00
Lachlan Kermode
627f5b9d4f
Add show rule for smallcaps in HTML (#6600) 2025-07-17 16:09:13 +00:00
Robin
5661c20580
Slightly improve selector docs (#6544) 2025-07-16 16:15:49 +00:00
Laurenz
7897e86bcc
Restore timing scopes for native show rules (#6616) 2025-07-16 09:54:43 +00:00
Laurenz
8e0e0f1a3b
Bump zip dependency (#6615) 2025-07-16 09:12:38 +00:00
Laurenz
0a4b72f8f6
Partially automate span assignment in native show rule (#6613) 2025-07-16 08:55:06 +00:00
Laurenz
c58766440c
Support intra-doc links in HTML (#6602) 2025-07-16 08:17:42 +00:00
Y.D.X.
ea5272bb2b
Support setting fonts repeatedly with different covers (#6604) 2025-07-16 08:10:21 +00:00
Malo
cdbf60e883
Change enum.item.number to Smart instead of Option (#6609) 2025-07-16 08:05:52 +00:00
Laurenz
9a6268050f
HTML frame improvements (#6605) 2025-07-15 14:48:31 +00:00
Robin
f51cb4b03e
Rephrase docs for truncation of float/decimal to integer (#6543) 2025-07-14 13:31:27 +00:00
Robin
0c12828c9a
Fix minor typo in text docs (#6589) 2025-07-14 13:26:25 +00:00
Robin
b1a091a236
Use "whitespace" instead of "space" to denote block-level equation in docs (#6591) 2025-07-14 13:26:10 +00:00
Said A.
0264534928
Fix regression in reference autocomplete (#6586) 2025-07-10 15:02:23 +00:00
Said A.
70710deb2b
Deduplicate labels for code completion (#6516) 2025-07-10 13:15:19 +00:00
Laurenz
275012d7c6
Handle lower and upper in HTML export (#6585) 2025-07-10 10:54:06 +00:00
Laurenz
98802dde7e
Complete movement of HTML export code to typst-html (#6584) 2025-07-10 10:42:34 +00:00
Laurenz
ac77fdbb6e
Fix tooltip for figure reference (#6580) 2025-07-09 13:50:54 +00:00
Laurenz
3aa7e861e7
Support images in HTML export (#6578) 2025-07-09 13:48:43 +00:00
Laurenz
a45c3388a6
More consistent Packed<T> to Content conversion methods (#6579) 2025-07-09 13:40:22 +00:00
Max
f9b01f595d
Move math styling to codex and add math.scr (#6309) 2025-07-09 13:08:49 +00:00
Robin
eed3407051
Update Swedish translations based on defaults used for LaTeX and cleveref (#6519) 2025-07-09 12:44:42 +00:00
Jassiel Ovando
1bbb58c43f
Add completions subcommand (#6568) 2025-07-09 12:41:40 +00:00
Malo
1dc4c248d1
Add default argument for str.first and str.last (#6554)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-09 12:10:24 +00:00
frozolotl
9e6adb6f45
Ignore spans when checking for RawElem equality (#6560) 2025-07-09 12:04:22 +00:00
Robin
4534167656
Use "displayed" instead of "repeated" to avoid ambiguity in numbering docs (#6565) 2025-07-09 12:02:50 +00:00
Laurenz
9ad1879e9d
Anti-alias clip paths (#6570) 2025-07-09 12:02:13 +00:00
Robin
e5e813219e
Fix typo of Typst domain in quote docs (#6573) 2025-07-09 12:01:57 +00:00
Laurenz
52a708b988
Move html module to typst-html crate (#6577) 2025-07-09 09:46:40 +00:00
Laurenz
e71674f6b3
Construct library via extension trait instead of default & inherent impl (#6576) 2025-07-09 09:28:26 +00:00
Laurenz
e5e1dcd9c0
Target-specific native show rules (#6569) 2025-07-09 08:16:36 +00:00
Laurenz
0a3c6939dd
Rewrite foundations of native elements (#6547) 2025-07-08 08:52:43 +00:00
Robin
36ecbb2c8d
Refer to json function instead of deprecated json.decode in groups docs (#6552) 2025-07-07 14:15:32 +00:00
Robin
51ab5b815c
Fix minor typo in function docs (#6542) 2025-07-07 14:15:10 +00:00
Laurenz
d1deb80bb8
Fix nightly warnings (#6558) 2025-07-05 12:23:48 +00:00
Andrew Voynov
88e451b3dc
Fix typo in PackageStorage (#6556) 2025-07-04 17:02:02 +00:00
Tobias Schmitz
cc3a68ecb1
Remove duplicate language computation (#6557) 2025-07-04 17:00:45 +00:00
Max
22a57fcf5c
Use punctuation math class for Arabic comma (#6537) 2025-07-02 08:01:44 +00:00
Malo
09c831d3b3
Use "subs" and "sups" font features for typographic scripts (#5777) 2025-07-02 08:00:45 +00:00
Robin
30ddc4a7ca
Fix typos in calc module docs (#6535) 2025-07-01 11:04:31 +00:00
Robin
d978f8c33a
Fix minor typo in array.product docs (#6532) 2025-07-01 11:04:11 +00:00
Adrián Delgado
c99f3ffc7d
Fix typo in PDF standard CLI help part 2 (#6531) 2025-06-30 16:51:36 +00:00
Adrián Delgado
880f56c90d
Fix typo in PDF standard CLI help (#6518) 2025-06-30 08:27:42 +00:00
Robin
a6cf0247b2
Fix typo in Advanced Styling docs tutorial (#6517) 2025-06-30 08:27:22 +00:00
Laurenz
c4bcfb18c1
Support HTML tests in test-helper extension (#6504) 2025-06-30 08:27:02 +00:00
Laurenz
e8f9877fc5
Acknowledgements (#6528) 2025-06-30 08:23:15 +00:00
Max
74b1b10986
Bump typst-dev-assets (#6514) 2025-06-27 10:35:05 +00:00
+merlan #flirora
584dd5fec6
Fix panic when sampling across two coincident gradient stops (#6166) 2025-06-27 09:26:15 +00:00
+merlan #flirora
b9f3a95e03
Sort line items by logical order when constructing frame (#5887)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-27 08:36:46 +00:00
Florian Bohlken
e8ce894ee7
Improve sentence in guide for LaTeX users (#6511) 2025-06-26 15:24:55 +00:00
Laurenz
9311f6f08e
Basic support for text decoration functions in HTML (#6510) 2025-06-26 13:44:45 +00:00
Laurenz
7420ec972f
Fix nested HTML frames (#6509) 2025-06-26 13:20:22 +00:00
Said A.
5dd5771df0
Disallow empty labels and references (#5776) (#6332)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-26 09:18:51 +00:00
Malo
04fd0acaca
Allow deprecating symbol variants (#6441) 2025-06-26 08:24:21 +00:00
Laurenz
6a1d6c08e2
Consistent sizing for html.frame (#6505) 2025-06-26 08:07:41 +00:00
Laurenz
35809387f8
Support in operator on strings and modules (#6498) 2025-06-26 08:06:22 +00:00
Connor K
d3caedd813
Fix typos in page-setup.md (#6499) 2025-06-25 16:59:19 +00:00
+merlan #flirora
d54544297b
Minor fixes to doc comments (#6500) 2025-06-25 16:58:40 +00:00
Laurenz
f2f527c451
Also fix encoding of <textarea> (#6497) 2025-06-24 15:52:15 +00:00
Laurenz
9e3c1199ed
Check that git tree is clean after build (#6495) 2025-06-24 15:05:02 +00:00
Tobias Schmitz
70399a94fd
Bump krilla to current Git version (#6488)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 13:23:37 +00:00
Andrew Voynov
d4be7c4ca5
Add page reference customization example (#6480)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 13:00:51 +00:00
Andrew Voynov
f162c37101
Improve equation reference example (#6481) 2025-06-24 12:49:28 +00:00
Andrew Voynov
87c5686560
Add docs for std module (#6407)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 10:22:55 +00:00
Max
899de6d5d5
Use ICU data to check if accent is bottom (#6393)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 10:03:10 +00:00
Andrew Voynov
24293a6c12
Rewrite outline.indent example (#6383)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 09:56:58 +00:00
Ivica Nakić
87cb8f5094
Adding Croatian translations entries (#6413) 2025-06-23 15:09:03 +00:00
Wannes Malfait
38dd6da237
Fix stroke cap of shapes with partial stroke (#5688) 2025-06-23 14:58:04 +00:00
Laurenz
bf8ef2a4a5 Properly handle raw text elements 2025-06-23 15:59:22 +02:00
Laurenz
c2e2fd99f6 Extract write_children function 2025-06-23 15:56:01 +02:00
Laurenz
f8dc1ad3bd Handle pre elements that start with a newline 2025-06-23 15:56:01 +02:00
Laurenz
9050ee1639 Turn non-empty void element into export error 2025-06-23 14:22:09 +02:00
Laurenz
c1b2aee1a9 Test runner support for HTML export errors 2025-06-23 14:21:35 +02:00
Laurenz
fbb02f40d9 Consistent codepoint formatting in HTML and PDF error messages 2025-06-23 14:18:41 +02:00
Laurenz
e9dc4bb204
Typed HTML API (#6476) 2025-06-23 09:12:58 +00:00
Laurenz
3602d06a15 Support for generating native functions at runtime 2025-06-20 17:32:37 +02:00
Laurenz
15302dbe7a Add typst_utils::display 2025-06-20 17:32:37 +02:00
Laurenz
4580daf307 More type-safe color conversions 2025-06-20 17:32:37 +02:00
Laurenz
d821633f50 Generic casting for Axes<T> 2025-06-20 17:32:37 +02:00
Laurenz
3b35f0cecf Add Duration::decompose 2025-06-20 17:32:37 +02:00
Laurenz
fee6844045 Encode empty attributes with shorthand syntax 2025-06-20 17:32:37 +02:00
Laurenz
f364b3c323
Fix param autocompletion false positive (#6475) 2025-06-20 12:32:04 +00:00
Noam Zaks
f1c761e88b
Fix align link in layout documentation (#6451) 2025-06-19 21:24:02 +00:00
Andrew Voynov
4588595792
Prefer .yaml over .yml in the docs (#6436) 2025-06-19 19:20:15 +00:00
Laurenz
0bc68df2a9
Hint for label in both document and bibliography (#6457) 2025-06-19 07:29:38 +00:00
Laurenz
f32cd5b3e1
Ensure that label repr is syntactically valid (#6456) 2025-06-19 07:29:35 +00:00
Laurenz
64d0a564bf
Better error message for compile time string interning failure (#6439) 2025-06-12 14:11:18 +00:00
cAttte
4a638f41cd
Consume data argument in pdf.embed() (#6435) 2025-06-12 14:10:04 +00:00
cAttte
f9897479d2
Unify EvalMode and LexMode into SyntaxMode (#6432) 2025-06-12 14:09:37 +00:00
415 changed files with 12550 additions and 7906 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
@ -81,13 +81,14 @@ jobs:
- run: cargo clippy --workspace --all-targets --no-default-features - run: cargo clippy --workspace --all-targets --no-default-features
- run: cargo fmt --check --all - run: cargo fmt --check --all
- run: cargo doc --workspace --no-deps - run: cargo doc --workspace --no-deps
- run: git diff --exit-code
min-version: min-version:
name: Check minimum Rust version name: Check minimum Rust version
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
@ -98,7 +99,19 @@ 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
miri:
name: Check unsafe code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
components: miri
toolchain: nightly-2025-05-10
- uses: Swatinem/rust-cache@v2
- 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 }}

170
Cargo.lock generated
View File

@ -181,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.8.0" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -214,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.21.0" version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe" source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -748,9 +748,9 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.0" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys", "libz-rs-sys",
@ -786,9 +786,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]] [[package]]
name = "font-types" name = "font-types"
version = "0.8.4" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@ -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,8 +1424,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",
@ -1377,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",
@ -1386,6 +1443,7 @@ dependencies = [
"rustybuzz", "rustybuzz",
"siphasher", "siphasher",
"skrifa", "skrifa",
"smallvec",
"subsetter", "subsetter",
"tiny-skia-path", "tiny-skia-path",
"xmp-writer", "xmp-writer",
@ -1396,8 +1454,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla-svg" name = "krilla-svg"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
dependencies = [ dependencies = [
"flate2", "flate2",
"fontdb", "fontdb",
@ -1410,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",
@ -1464,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",
] ]
@ -1630,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",
@ -1712,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",
@ -1849,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",
@ -2007,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",
@ -2106,9 +2163,9 @@ dependencies = [
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.28.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288" checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"font-types", "font-types",
@ -2120,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]]
@ -2223,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",
@ -2242,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",
@ -2290,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",
@ -2434,9 +2491,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.30.0" version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8" checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"read-fonts", "read-fonts",
@ -2453,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"
@ -2863,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=c74e539#c74e539b090070a0c66fd007c550f5b6d3b724bd" source = "git+https://github.com/typst/typst-assets?rev=fbf00f9#fbf00f9539fdb0825bef4d39fb57d5986c51b756"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -2913,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=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92" source = "git+https://github.com/typst/typst-dev-assets?rev=c6c2acf#c6c2acf6cdc31f99a23a478d3d614f8bf806a4f5"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"
@ -2945,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",
@ -2973,8 +3029,12 @@ dependencies = [
name = "typst-html" name = "typst-html"
version = "0.13.1" version = "0.13.1"
dependencies = [ dependencies = [
"bumpalo",
"comemo", "comemo",
"ecow", "ecow",
"palette",
"time",
"typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-svg", "typst-svg",
@ -2989,7 +3049,6 @@ version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"if_chain",
"once_cell", "once_cell",
"pathdiff", "pathdiff",
"serde", "serde",
@ -3030,6 +3089,7 @@ version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bumpalo", "bumpalo",
"codex",
"comemo", "comemo",
"ecow", "ecow",
"hypher", "hypher",
@ -3060,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",
@ -3072,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",
@ -3170,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",
@ -3188,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",
@ -3586,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",
] ]
@ -3721,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]]
@ -3929,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",
@ -3944,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 = "c74e539" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fbf00f9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "c6c2acf" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "56eb217" } codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"
@ -61,6 +61,8 @@ fontdb = { version = "0.23", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"
glidesort = "0.1.2" glidesort = "0.1.2"
hayagriva = "0.8.1" hayagriva = "0.8.1"
hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
hayro = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
heck = "0.5" heck = "0.5"
hypher = "0.1.4" hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] } icu_properties = { version = "1.4", features = ["serde"] }
@ -68,13 +70,12 @@ icu_provider = { version = "1.4", features = ["sync"] }
icu_provider_adapters = "1.4" icu_provider_adapters = "1.4"
icu_provider_blob = "1.4" icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] } icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] } image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false } infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { version = "0.4.0", 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 = "0.1.0" krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00"}
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"
@ -143,7 +144,7 @@ xmlparser = "0.13.5"
xmlwriter = "0.1.0" xmlwriter = "0.1.0"
xz2 = { version = "0.1", features = ["static"] } xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1" yaml-front-matter = "0.1"
zip = { version = "2.5", default-features = false, features = ["deflate"] } zip = { version = "4.3", default-features = false, features = ["deflate"] }
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 2 opt-level = 2

View File

@ -174,7 +174,10 @@ typst help watch
``` ```
If you prefer an integrated IDE-like experience with autocompletion and instant If you prefer an integrated IDE-like experience with autocompletion and instant
preview, you can also check out [Typst's free web app][app]. preview, you can also check out our [free web app][app]. Alternatively, there is
a community-created language server called
[Tinymist](https://myriad-dreamin.github.io/tinymist/) which is integrated into
various editor extensions.
## Community ## Community
The main places where the community gathers are our [Forum][forum] and our The main places where the community gathers are our [Forum][forum] and our
@ -240,6 +243,26 @@ instant preview. To achieve these goals, we follow three core design principles:
Luckily we have [`comemo`], a system for incremental compilation which does Luckily we have [`comemo`], a system for incremental compilation which does
most of the hard work in the background. most of the hard work in the background.
## Acknowledgements
We'd like to thank everyone who is supporting Typst's development, be it via
[GitHub sponsors] or elsewhere. In particular, special thanks[^1] go to:
- [Posit](https://posit.co/blog/posit-and-typst/) for financing a full-time
compiler engineer
- [NLnet](https://nlnet.nl/) for supporting work on Typst via multiple grants
through the [NGI Zero Core](https://nlnet.nl/core) fund:
- Work on [HTML export](https://nlnet.nl/project/Typst-HTML/)
- Work on [PDF accessibility](https://nlnet.nl/project/Typst-Accessibility/)
- [Science & Startups](https://www.science-startups.berlin/) for having financed
Typst development from January through June 2023 via the Berlin Startup
Scholarship
- [Zerodha](https://zerodha.tech/blog/1-5-million-pdfs-in-25-minutes/) for their
generous one-time sponsorship
[^1]: This list only includes contributions for our open-source work that exceed
or are expected to exceed €10K.
[docs]: https://typst.app/docs/ [docs]: https://typst.app/docs/
[app]: https://typst.app/ [app]: https://typst.app/
[discord]: https://discord.gg/2uDybryKPe [discord]: https://discord.gg/2uDybryKPe
@ -259,3 +282,4 @@ instant preview. To achieve these goals, we follow three core design principles:
[packages]: https://github.com/typst/packages/ [packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/ [`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst [snap]: https://snapcraft.io/typst
[GitHub sponsors]: https://github.com/sponsors/typst/

View File

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

View File

@ -1,10 +1,10 @@
use std::env; use std::env;
use std::fs::{create_dir_all, File}; use std::fs::{File, create_dir_all};
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use clap::{CommandFactory, ValueEnum}; use clap::{CommandFactory, ValueEnum};
use clap_complete::{generate_to, Shell}; use clap_complete::{Shell, generate_to};
use clap_mangen::Man; use clap_mangen::Man;
#[path = "src/args.rs"] #[path = "src/args.rs"]

View File

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

View File

@ -10,14 +10,14 @@ use ecow::eco_format;
use parking_lot::RwLock; use parking_lot::RwLock;
use pathdiff::diff_paths; use pathdiff::diff_paths;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::WorldExt;
use typst::diag::{ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail,
}; };
use typst::foundations::{Datetime, Smart}; use typst::foundations::{Datetime, Smart};
use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Lines, Span}; use typst::syntax::{FileId, Lines, Span};
use typst::WorldExt; use typst_html::HtmlDocument;
use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
use crate::args::{ use crate::args::{
@ -513,7 +513,9 @@ fn write_make_deps(
}) })
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
else { else {
bail!("failed to create make dependencies file because output path was not valid unicode") bail!(
"failed to create make dependencies file because output path was not valid unicode"
)
}; };
if output_paths.is_empty() { if output_paths.is_empty() {
bail!("failed to create make dependencies file because output was stdout") bail!("failed to create make dependencies file because output was stdout")

View File

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

View File

@ -8,8 +8,8 @@ use codespan_reporting::term::termcolor::WriteColor;
use typst::utils::format_duration; use typst::utils::format_duration;
use typst_kit::download::{DownloadState, Downloader, Progress}; use typst_kit::download::{DownloadState, Downloader, Progress};
use crate::terminal::{self, TermOut};
use crate::ARGS; use crate::ARGS;
use crate::terminal::{self, TermOut};
/// Prints download progress by writing `downloading {0}` followed by repeatedly /// Prints download progress by writing `downloading {0}` followed by repeatedly
/// updating the last terminal line. /// updating the last terminal line.

View File

@ -4,7 +4,7 @@ use std::path::Path;
use codespan_reporting::term::termcolor::{Color, ColorSpec, WriteColor}; use codespan_reporting::term::termcolor::{Color, ColorSpec, WriteColor};
use ecow::eco_format; use ecow::eco_format;
use fs_extra::dir::CopyOptions; use fs_extra::dir::CopyOptions;
use typst::diag::{bail, FileError, StrResult}; use typst::diag::{FileError, StrResult, bail};
use typst::syntax::package::{ use typst::syntax::package::{
PackageManifest, PackageSpec, TemplateInfo, VersionlessPackageSpec, PackageManifest, PackageSpec, TemplateInfo, VersionlessPackageSpec,
}; };
@ -29,8 +29,9 @@ pub fn init(command: &InitCommand) -> StrResult<()> {
})?; })?;
// Find or download the package. // Find or download the package.
let package_path = let package_path = package_storage
package_storage.prepare_package(&spec, &mut PrintDownload(&spec))?; .prepare_package(&spec, &mut PrintDownload(&spec))
.map_err(|e| eco_format!("{e}"))?;
// Parse the manifest. // Parse the manifest.
let manifest = parse_manifest(&package_path)?; let manifest = parse_manifest(&package_path)?;

View File

@ -1,5 +1,6 @@
mod args; mod args;
mod compile; mod compile;
mod completions;
mod download; mod download;
mod fonts; mod fonts;
mod greet; mod greet;
@ -20,8 +21,8 @@ use std::io::{self, Write};
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::LazyLock; use std::sync::LazyLock;
use clap::error::ErrorKind;
use clap::Parser; use clap::Parser;
use clap::error::ErrorKind;
use codespan_reporting::term; use codespan_reporting::term;
use codespan_reporting::term::termcolor::WriteColor; use codespan_reporting::term::termcolor::WriteColor;
use typst::diag::HintedStrResult; use typst::diag::HintedStrResult;
@ -71,6 +72,7 @@ fn dispatch() -> HintedStrResult<()> {
Command::Query(command) => crate::query::query(command)?, Command::Query(command) => crate::query::query(command)?,
Command::Fonts(command) => crate::fonts::fonts(command), Command::Fonts(command) => crate::fonts::fonts(command),
Command::Update(command) => crate::update::update(command)?, Command::Update(command) => crate::update::update(command)?,
Command::Completions(command) => crate::completions::completions(command),
} }
Ok(()) Ok(())
@ -100,7 +102,7 @@ fn print_error(msg: &str) -> io::Result<()> {
#[cfg(not(feature = "self-update"))] #[cfg(not(feature = "self-update"))]
mod update { mod update {
use typst::diag::{bail, StrResult}; use typst::diag::{StrResult, bail};
use crate::args::UpdateCommand; use crate::args::UpdateCommand;

View File

@ -1,13 +1,13 @@
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; use typst::syntax::{Span, SyntaxMode};
use typst::World; use typst_eval::eval_string;
use typst_eval::{eval_string, EvalMode};
use crate::args::{QueryCommand, SerializationFormat}; use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics; use crate::compile::print_diagnostics;
@ -63,7 +63,7 @@ fn retrieve(
Sink::new().track_mut(), Sink::new().track_mut(),
&command.selector, &command.selector,
Span::detached(), Span::detached(),
EvalMode::Code, SyntaxMode::Code,
Scope::default(), Scope::default(),
) )
.map_err(|errors| { .map_err(|errors| {

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use ecow::eco_format; use ecow::eco_format;
use parking_lot::{Condvar, Mutex, MutexGuard}; use parking_lot::{Condvar, Mutex, MutexGuard};
use tiny_http::{Header, Request, Response, StatusCode}; use tiny_http::{Header, Request, Response, StatusCode};
use typst::diag::{bail, StrResult}; use typst::diag::{StrResult, bail};
use crate::args::{Input, ServerArgs}; use crate::args::{Input, ServerArgs};
@ -162,7 +162,7 @@ impl<T> Bucket<T> {
} }
/// Retrieves the current data in the bucket. /// Retrieves the current data in the bucket.
fn get(&self) -> MutexGuard<T> { fn get(&self) -> MutexGuard<'_, T> {
self.mutex.lock() self.mutex.lock()
} }

View File

@ -2,9 +2,9 @@ use std::fs::File;
use std::io::BufWriter; use std::io::BufWriter;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use typst::diag::{bail, StrResult};
use typst::syntax::Span;
use typst::World; use typst::World;
use typst::diag::{StrResult, bail};
use typst::syntax::Span;
use crate::args::{CliArguments, Command}; use crate::args::{CliArguments, Command};
use crate::world::SystemWorld; use crate::world::SystemWorld;

View File

@ -6,7 +6,7 @@ use ecow::eco_format;
use semver::Version; use semver::Version;
use serde::Deserialize; use serde::Deserialize;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use typst::diag::{bail, StrResult}; use typst::diag::{StrResult, bail};
use typst_kit::download::Downloader; use typst_kit::download::Downloader;
use xz2::bufread::XzDecoder; use xz2::bufread::XzDecoder;
use zip::ZipArchive; use zip::ZipArchive;

View File

@ -10,12 +10,12 @@ use codespan_reporting::term::{self, termcolor};
use ecow::eco_format; use ecow::eco_format;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _}; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
use same_file::is_same_file; use same_file::is_same_file;
use typst::diag::{bail, warning, StrResult}; use typst::diag::{StrResult, bail, warning};
use typst::syntax::Span; use typst::syntax::Span;
use typst::utils::format_duration; use typst::utils::format_duration;
use crate::args::{Input, Output, WatchCommand}; use crate::args::{Input, Output, WatchCommand};
use crate::compile::{compile_once, print_diagnostics, CompileConfig}; use crate::compile::{CompileConfig, compile_once, print_diagnostics};
use crate::timings::Timer; use crate::timings::Timer;
use crate::world::{SystemWorld, WorldCreationError}; use crate::world::{SystemWorld, WorldCreationError};
use crate::{print_error, terminal}; use crate::{print_error, terminal};

View File

@ -5,14 +5,14 @@ use std::sync::{LazyLock, OnceLock};
use std::{fmt, fs, io, mem}; use std::{fmt, fs, io, mem};
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc}; use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use parking_lot::Mutex; use parking_lot::Mutex;
use typst::diag::{FileError, FileResult}; use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
use typst::syntax::{FileId, Lines, Source, VirtualPath}; use typst::syntax::{FileId, Lines, Source, VirtualPath};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
use typst::{Library, World}; use typst::{Library, LibraryExt, World};
use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage; use typst_kit::package::PackageStorage;
use typst_timing::timed; use typst_timing::timed;
@ -361,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);
@ -404,7 +404,9 @@ fn system_path(
// Join the path to the root. If it tries to escape, deny // Join the path to the root. If it tries to escape, deny
// access. Note: It can still escape via symlinks. // access. Note: It can still escape via symlinks.
id.vpath().resolve(root).ok_or(FileError::AccessDenied) id.vpath()
.resolve(root)
.ok_or_else(|| FileError::AccessDenied(id.vpath().as_rootless_path().into()))
} }
/// Reads a file from a `FileId`. /// Reads a file from a `FileId`.
@ -427,7 +429,7 @@ fn read(
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> { fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
let f = |e| FileError::from_io(e, path); let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() { if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory) Err(FileError::IsDirectory(path.into()))
} else { } else {
fs::read(path).map_err(f) fs::read(path).map_err(f)
} }

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,25 +14,24 @@ 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;
pub use typst_library::routines::EvalMode;
use self::access::*; use self::access::*;
use self::binding::*; 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};
/// Evaluate a source file and return the resulting module. /// Evaluate a source file and return the resulting module.
#[comemo::memoize] #[comemo::memoize]
@ -104,13 +103,13 @@ pub fn eval_string(
sink: TrackedMut<Sink>, sink: TrackedMut<Sink>,
string: &str, string: &str,
span: Span, span: Span,
mode: EvalMode, mode: SyntaxMode,
scope: Scope, scope: Scope,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let mut root = match mode { let mut root = match mode {
EvalMode::Code => parse_code(string), SyntaxMode::Code => parse_code(string),
EvalMode::Markup => parse(string), SyntaxMode::Markup => parse(string),
EvalMode::Math => parse_math(string), SyntaxMode::Math => parse_math(string),
}; };
root.synthesize(span); root.synthesize(span);
@ -141,11 +140,11 @@ pub fn eval_string(
// Evaluate the code. // Evaluate the code.
let output = match mode { let output = match mode {
EvalMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?, SyntaxMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?,
EvalMode::Markup => { SyntaxMode::Markup => {
Value::Content(root.cast::<ast::Markup>().unwrap().eval(&mut vm)?) Value::Content(root.cast::<ast::Markup>().unwrap().eval(&mut vm)?)
} }
EvalMode::Math => Value::Content( SyntaxMode::Math => Value::Content(
EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?) EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?)
.with_block(false) .with_block(false)
.pack() .pack()

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,
}; };
@ -186,7 +186,7 @@ impl Eval for ast::Raw<'_> {
let lines = self.lines().map(|line| (line.get().clone(), line.span())).collect(); let lines = self.lines().map(|line| (line.get().clone(), line.span())).collect();
let mut elem = RawElem::new(RawContent::Lines(lines)).with_block(self.block()); let mut elem = RawElem::new(RawContent::Lines(lines)).with_block(self.block());
if let Some(lang) = self.lang() { if let Some(lang) = self.lang() {
elem.push_lang(Some(lang.get().clone())); elem.lang.set(Some(lang.get().clone()));
} }
Ok(elem.pack()) Ok(elem.pack())
} }
@ -205,7 +205,9 @@ impl Eval for ast::Label<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Label(Label::new(PicoStr::intern(self.get())))) Ok(Value::Label(
Label::new(PicoStr::intern(self.get())).expect("unexpected empty label"),
))
} }
} }
@ -213,12 +215,12 @@ impl Eval for ast::Ref<'_> {
type Output = Content; type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let target = Label::new(PicoStr::intern(self.target())); let target = Label::new(PicoStr::intern(self.target()))
.expect("unexpected empty reference");
let mut elem = RefElem::new(target); let mut elem = RefElem::new(target);
if let Some(supplement) = self.supplement() { if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content( elem.supplement
supplement.eval(vm)?, .set(Smart::Custom(Some(Supplement::Content(supplement.eval(vm)?))));
))));
} }
Ok(elem.pack()) Ok(elem.pack())
} }
@ -249,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.push_number(Some(number)); elem.number.set(Smart::Custom(number));
} }
Ok(elem.pack()) Ok(elem.pack())
} }

View File

@ -80,17 +80,17 @@ impl Eval for ast::MathAttach<'_> {
let mut elem = AttachElem::new(base); let mut elem = AttachElem::new(base);
if let Some(expr) = self.top() { if let Some(expr) = self.top() {
elem.push_t(Some(expr.eval_display(vm)?)); elem.t.set(Some(expr.eval_display(vm)?));
} }
// Always attach primes in scripts style (not limits style), // Always attach primes in scripts style (not limits style),
// i.e. at the top-right corner. // i.e. at the top-right corner.
if let Some(primes) = self.primes() { if let Some(primes) = self.primes() {
elem.push_tr(Some(primes.eval(vm)?)); elem.tr.set(Some(primes.eval(vm)?));
} }
if let Some(expr) = self.bottom() { if let Some(expr) = self.bottom() {
elem.push_b(Some(expr.eval_display(vm)?)); elem.b.set(Some(expr.eval_display(vm)?));
} }
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,6 +1,6 @@
use typst_library::diag::{warning, At, SourceResult}; use typst_library::diag::{At, SourceResult, warning};
use typst_library::foundations::{ use typst_library::foundations::{
Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation, Element, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
}; };
use typst_library::layout::BlockElem; use typst_library::layout::BlockElem;
use typst_library::model::ParElem; use typst_library::model::ParElem;
@ -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,19 +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>(<BlockElem as Fields>::Enum::Above as _) || {
styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Below as _); vm.engine.sink.warn(warning!(
then {
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

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

View File

@ -0,0 +1,195 @@
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use crate::HtmlAttr;
pub const abbr: HtmlAttr = HtmlAttr::constant("abbr");
pub const accept: HtmlAttr = HtmlAttr::constant("accept");
pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset");
pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey");
pub const action: HtmlAttr = HtmlAttr::constant("action");
pub const allow: HtmlAttr = HtmlAttr::constant("allow");
pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen");
pub const alpha: HtmlAttr = HtmlAttr::constant("alpha");
pub const alt: HtmlAttr = HtmlAttr::constant("alt");
pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant");
pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic");
pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete");
pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy");
pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked");
pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount");
pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex");
pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan");
pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls");
pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current");
pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby");
pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details");
pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled");
pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage");
pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded");
pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto");
pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup");
pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden");
pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid");
pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts");
pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label");
pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby");
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live");
pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal");
pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline");
pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable");
pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation");
pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns");
pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder");
pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset");
pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed");
pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly");
pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant");
pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required");
pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription");
pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount");
pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex");
pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan");
pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected");
pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize");
pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort");
pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax");
pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin");
pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow");
pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext");
pub const r#as: HtmlAttr = HtmlAttr::constant("as");
pub const r#async: HtmlAttr = HtmlAttr::constant("async");
pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize");
pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete");
pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect");
pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus");
pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay");
pub const blocking: HtmlAttr = HtmlAttr::constant("blocking");
pub const charset: HtmlAttr = HtmlAttr::constant("charset");
pub const checked: HtmlAttr = HtmlAttr::constant("checked");
pub const cite: HtmlAttr = HtmlAttr::constant("cite");
pub const class: HtmlAttr = HtmlAttr::constant("class");
pub const closedby: HtmlAttr = HtmlAttr::constant("closedby");
pub const color: HtmlAttr = HtmlAttr::constant("color");
pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace");
pub const cols: HtmlAttr = HtmlAttr::constant("cols");
pub const colspan: HtmlAttr = HtmlAttr::constant("colspan");
pub const command: HtmlAttr = HtmlAttr::constant("command");
pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor");
pub const content: HtmlAttr = HtmlAttr::constant("content");
pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable");
pub const controls: HtmlAttr = HtmlAttr::constant("controls");
pub const coords: HtmlAttr = HtmlAttr::constant("coords");
pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin");
pub const data: HtmlAttr = HtmlAttr::constant("data");
pub const datetime: HtmlAttr = HtmlAttr::constant("datetime");
pub const decoding: HtmlAttr = HtmlAttr::constant("decoding");
pub const default: HtmlAttr = HtmlAttr::constant("default");
pub const defer: HtmlAttr = HtmlAttr::constant("defer");
pub const dir: HtmlAttr = HtmlAttr::constant("dir");
pub const dirname: HtmlAttr = HtmlAttr::constant("dirname");
pub const disabled: HtmlAttr = HtmlAttr::constant("disabled");
pub const download: HtmlAttr = HtmlAttr::constant("download");
pub const draggable: HtmlAttr = HtmlAttr::constant("draggable");
pub const enctype: HtmlAttr = HtmlAttr::constant("enctype");
pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint");
pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority");
pub const r#for: HtmlAttr = HtmlAttr::constant("for");
pub const form: HtmlAttr = HtmlAttr::constant("form");
pub const formaction: HtmlAttr = HtmlAttr::constant("formaction");
pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype");
pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod");
pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate");
pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget");
pub const headers: HtmlAttr = HtmlAttr::constant("headers");
pub const height: HtmlAttr = HtmlAttr::constant("height");
pub const hidden: HtmlAttr = HtmlAttr::constant("hidden");
pub const high: HtmlAttr = HtmlAttr::constant("high");
pub const href: HtmlAttr = HtmlAttr::constant("href");
pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang");
pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv");
pub const id: HtmlAttr = HtmlAttr::constant("id");
pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes");
pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset");
pub const inert: HtmlAttr = HtmlAttr::constant("inert");
pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode");
pub const integrity: HtmlAttr = HtmlAttr::constant("integrity");
pub const is: HtmlAttr = HtmlAttr::constant("is");
pub const ismap: HtmlAttr = HtmlAttr::constant("ismap");
pub const itemid: HtmlAttr = HtmlAttr::constant("itemid");
pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop");
pub const itemref: HtmlAttr = HtmlAttr::constant("itemref");
pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope");
pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype");
pub const kind: HtmlAttr = HtmlAttr::constant("kind");
pub const label: HtmlAttr = HtmlAttr::constant("label");
pub const lang: HtmlAttr = HtmlAttr::constant("lang");
pub const list: HtmlAttr = HtmlAttr::constant("list");
pub const loading: HtmlAttr = HtmlAttr::constant("loading");
pub const r#loop: HtmlAttr = HtmlAttr::constant("loop");
pub const low: HtmlAttr = HtmlAttr::constant("low");
pub const max: HtmlAttr = HtmlAttr::constant("max");
pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength");
pub const media: HtmlAttr = HtmlAttr::constant("media");
pub const method: HtmlAttr = HtmlAttr::constant("method");
pub const min: HtmlAttr = HtmlAttr::constant("min");
pub const minlength: HtmlAttr = HtmlAttr::constant("minlength");
pub const multiple: HtmlAttr = HtmlAttr::constant("multiple");
pub const muted: HtmlAttr = HtmlAttr::constant("muted");
pub const name: HtmlAttr = HtmlAttr::constant("name");
pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule");
pub const nonce: HtmlAttr = HtmlAttr::constant("nonce");
pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate");
pub const open: HtmlAttr = HtmlAttr::constant("open");
pub const optimum: HtmlAttr = HtmlAttr::constant("optimum");
pub const pattern: HtmlAttr = HtmlAttr::constant("pattern");
pub const ping: HtmlAttr = HtmlAttr::constant("ping");
pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder");
pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline");
pub const popover: HtmlAttr = HtmlAttr::constant("popover");
pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget");
pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction");
pub const poster: HtmlAttr = HtmlAttr::constant("poster");
pub const preload: HtmlAttr = HtmlAttr::constant("preload");
pub const readonly: HtmlAttr = HtmlAttr::constant("readonly");
pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy");
pub const rel: HtmlAttr = HtmlAttr::constant("rel");
pub const required: HtmlAttr = HtmlAttr::constant("required");
pub const reversed: HtmlAttr = HtmlAttr::constant("reversed");
pub const role: HtmlAttr = HtmlAttr::constant("role");
pub const rows: HtmlAttr = HtmlAttr::constant("rows");
pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan");
pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox");
pub const scope: HtmlAttr = HtmlAttr::constant("scope");
pub const selected: HtmlAttr = HtmlAttr::constant("selected");
pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable");
pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry");
pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus");
pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode");
pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable");
pub const shape: HtmlAttr = HtmlAttr::constant("shape");
pub const size: HtmlAttr = HtmlAttr::constant("size");
pub const sizes: HtmlAttr = HtmlAttr::constant("sizes");
pub const slot: HtmlAttr = HtmlAttr::constant("slot");
pub const span: HtmlAttr = HtmlAttr::constant("span");
pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck");
pub const src: HtmlAttr = HtmlAttr::constant("src");
pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc");
pub const srclang: HtmlAttr = HtmlAttr::constant("srclang");
pub const srcset: HtmlAttr = HtmlAttr::constant("srcset");
pub const start: HtmlAttr = HtmlAttr::constant("start");
pub const step: HtmlAttr = HtmlAttr::constant("step");
pub const style: HtmlAttr = HtmlAttr::constant("style");
pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex");
pub const target: HtmlAttr = HtmlAttr::constant("target");
pub const title: HtmlAttr = HtmlAttr::constant("title");
pub const translate: HtmlAttr = HtmlAttr::constant("translate");
pub const r#type: HtmlAttr = HtmlAttr::constant("type");
pub const usemap: HtmlAttr = HtmlAttr::constant("usemap");
pub const value: HtmlAttr = HtmlAttr::constant("value");
pub const width: HtmlAttr = HtmlAttr::constant("width");
pub const wrap: HtmlAttr = HtmlAttr::constant("wrap");
pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions");

View File

@ -0,0 +1,81 @@
//! Defines syntactical properties of HTML tags, attributes, and text.
/// Check whether a character is in a tag name.
pub const fn is_valid_in_tag_name(c: char) -> bool {
c.is_ascii_alphanumeric()
}
/// Check whether a character is valid in an attribute name.
pub const fn is_valid_in_attribute_name(c: char) -> bool {
match c {
// These are forbidden.
'\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false,
c if is_whatwg_control_char(c) => false,
c if is_whatwg_non_char(c) => false,
// _Everything_ else is allowed, including U+2029 paragraph
// separator. Go wild.
_ => true,
}
}
/// Check whether a character can be an used in an attribute value without
/// escaping.
///
/// See <https://html.spec.whatwg.org/multipage/syntax.html#attributes-2>
pub const fn is_valid_in_attribute_value(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Quotation marks are not allowed in double-quote-delimited attribute
// values.
'"' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check whether a character can be an used in normal text without
/// escaping.
pub const fn is_valid_in_normal_element_text(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Less-than signs are not allowed in text.
'<' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check if something is valid text in HTML.
pub const fn is_w3c_text_char(c: char) -> bool {
match c {
// Non-characters are obviously not text characters.
c if is_whatwg_non_char(c) => false,
// Control characters are disallowed, except for whitespace.
c if is_whatwg_control_char(c) => c.is_ascii_whitespace(),
// Everything else is allowed.
_ => true,
}
}
const fn is_whatwg_non_char(c: char) -> bool {
match c {
'\u{fdd0}'..='\u{fdef}' => true,
// Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive).
c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true,
_ => false,
}
}
const fn is_whatwg_control_char(c: char) -> bool {
match c {
// C0 control characters.
'\u{00}'..='\u{1f}' => true,
// Other control characters.
'\u{7f}'..='\u{9f}' => true,
_ => false,
}
}

View File

@ -0,0 +1,127 @@
use typst_library::diag::{SourceResult, warning};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::introspection::{SplitLocator, TagElem};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::ParElem;
use typst_library::routines::Pair;
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use crate::fragment::html_fragment;
use crate::{FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, attr, tag};
/// Converts realized content into HTML nodes.
pub fn convert_to_nodes<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert one element into HTML node(s).
fn handle(
engine: &mut Engine,
child: &Content,
locator: &mut SplitLocator,
styles: StyleChain,
output: &mut Vec<HtmlNode>,
) -> SourceResult<()> {
if let Some(elem) = child.to_packed::<TagElem>() {
output.push(HtmlNode::Tag(elem.tag.clone()));
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
let mut children = vec![];
if let Some(body) = elem.body.get_ref(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs.get_cloned(styles),
children,
span: elem.span(),
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() {
let children =
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// TODO: This is rather incomplete.
if let Some(body) = elem.body.get_ref(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::span)
.with_attr(attr::style, "display: inline-block;")
.with_children(children)
.spanned(elem.span())
.into(),
)
}
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body.get_ref(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
{
// TODO: This is rather incomplete.
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::div)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {
let text = if let Some(case) = styles.get(TextElem::case) {
case.apply(&elem.text).into()
} else {
elem.text.clone()
};
output.push(HtmlNode::text(text, elem.span()));
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
output.push(HtmlNode::text(
if elem.double.get(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::target.set(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
locator,
styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?;
output.push(HtmlNode::Frame(HtmlFrame::new(frame, styles)));
} else {
engine.sink.warn(warning!(
child.span(),
"{} was ignored during HTML export",
child.elem().name()
));
}
Ok(())
}
/// Checks whether the given element is an inline-level HTML element.
pub fn is_inline(elem: &Content) -> bool {
elem.to_packed::<HtmlElem>()
.is_some_and(|elem| tag::is_inline_by_default(elem.tag))
}

View File

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

View File

@ -0,0 +1,236 @@
use std::collections::HashSet;
use std::num::NonZeroUsize;
use comemo::{Tracked, TrackedMut};
use typst_library::World;
use typst_library::diag::{SourceResult, bail};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{
Introspector, IntrospectorBuilder, Location, Locator,
};
use typst_library::layout::{Point, Position, Transform};
use typst_library::model::DocumentInfo;
use typst_library::routines::{Arenas, RealizationKind, Routines};
use typst_syntax::Span;
use typst_utils::NonZeroExt;
use crate::{HtmlDocument, HtmlElement, HtmlNode, attr, tag};
/// Produce an HTML document from content.
///
/// This first performs root-level realization and then turns the resulting
/// elements into HTML.
#[typst_macros::time(name = "html document")]
pub fn html_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
html_document_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
styles,
)
}
/// The internal implementation of `html_document`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_document_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument {
info: &mut info,
is_inline: crate::convert::is_inline,
},
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = crate::convert::convert_to_nodes(
&mut engine,
&mut locator,
children.iter().copied(),
)?;
let mut link_targets = HashSet::new();
let mut introspector = introspect_html(&output, &mut link_targets);
let mut root = root_element(output, &info)?;
crate::link::identify_link_targets(&mut root, &mut introspector, link_targets);
Ok(HtmlDocument { info, root, introspector })
}
/// Introspects HTML nodes.
#[typst_macros::time(name = "introspect html")]
fn introspect_html(
output: &[HtmlNode],
link_targets: &mut HashSet<Location>,
) -> Introspector {
fn discover(
builder: &mut IntrospectorBuilder,
sink: &mut Vec<(Content, Position)>,
link_targets: &mut HashSet<Location>,
nodes: &[HtmlNode],
) {
for node in nodes {
match node {
HtmlNode::Tag(tag) => {
builder.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
);
}
HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => {
discover(builder, sink, link_targets, &elem.children)
}
HtmlNode::Frame(frame) => {
builder.discover_in_frame(
sink,
&frame.inner,
NonZeroUsize::ONE,
Transform::identity(),
);
crate::link::introspect_frame_links(&frame.inner, link_targets);
}
}
}
}
let mut elems = Vec::new();
let mut builder = IntrospectorBuilder::new();
discover(&mut builder, &mut elems, link_targets, output);
builder.finalize(elems)
}
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
/// supplying a suitable `<head>`.
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
let head = head_element(info);
let body = match classify_output(output)? {
OutputKind::Html(element) => return Ok(element),
OutputKind::Body(body) => body,
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
};
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
}
/// Generate a `<head>` element.
fn head_element(info: &DocumentInfo) -> HtmlElement {
let mut children = vec![];
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "viewport")
.with_attr(attr::content, "width=device-width, initial-scale=1")
.into(),
);
if let Some(title) = &info.title {
children.push(
HtmlElement::new(tag::title)
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
.into(),
);
}
if let Some(description) = &info.description {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "description")
.with_attr(attr::content, description.clone())
.into(),
);
}
if !info.author.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "authors")
.with_attr(attr::content, info.author.join(", "))
.into(),
)
}
if !info.keywords.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "keywords")
.with_attr(attr::content, info.keywords.join(", "))
.into(),
)
}
HtmlElement::new(tag::head).with_children(children)
}
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag,
),
_ => {}
}
}
Ok(OutputKind::Leafs(output))
}
/// What kinds of output the user generated.
enum OutputKind {
/// The user generated their own `<html>` element. We do not need to supply
/// one.
Html(HtmlElement),
/// The user generate their own `<body>` element. We do not need to supply
/// one, but need supply the `<html>` element.
Body(HtmlElement),
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
Leafs(Vec<HtmlNode>),
}

View File

@ -0,0 +1,308 @@
use std::fmt::{self, Debug, Display, Formatter};
use ecow::{EcoString, EcoVec};
use typst_library::diag::{HintedStrResult, StrResult, bail};
use typst_library::foundations::{Dict, Repr, Str, StyleChain, cast};
use typst_library::introspection::{Introspector, Tag};
use typst_library::layout::{Abs, Frame, Point};
use typst_library::model::DocumentInfo;
use typst_library::text::TextElem;
use typst_syntax::Span;
use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::charsets;
/// An HTML document.
#[derive(Debug, Clone)]
pub struct HtmlDocument {
/// The document's root HTML element.
pub root: HtmlElement,
/// Details about the document.
pub info: DocumentInfo,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
}
/// A child of an HTML element.
#[derive(Debug, Clone, Hash)]
pub enum HtmlNode {
/// An introspectable element that produced something within this node.
Tag(Tag),
/// Plain text.
Text(EcoString, Span),
/// Another element.
Element(HtmlElement),
/// Layouted content that will be embedded into HTML as an SVG.
Frame(HtmlFrame),
}
impl HtmlNode {
/// Create a plain text node.
pub fn text(text: impl Into<EcoString>, span: Span) -> Self {
Self::Text(text.into(), span)
}
}
impl From<HtmlElement> for HtmlNode {
fn from(element: HtmlElement) -> Self {
Self::Element(element)
}
}
/// An HTML element.
#[derive(Debug, Clone, Hash)]
pub struct HtmlElement {
/// The HTML tag.
pub tag: HtmlTag,
/// The element's attributes.
pub attrs: HtmlAttrs,
/// The element's children.
pub children: Vec<HtmlNode>,
/// The span from which the element originated, if any.
pub span: Span,
}
impl HtmlElement {
/// Create a new, blank element without attributes or children.
pub fn new(tag: HtmlTag) -> Self {
Self {
tag,
attrs: HtmlAttrs::default(),
children: vec![],
span: Span::detached(),
}
}
/// Attach children to the element.
///
/// Note: This overwrites potential previous children.
pub fn with_children(mut self, children: Vec<HtmlNode>) -> Self {
self.children = children;
self
}
/// Add an atribute to the element.
pub fn with_attr(mut self, key: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.push(key, value);
self
}
/// Attach a span to the element.
pub fn spanned(mut self, span: Span) -> Self {
self.span = span;
self
}
}
/// The tag of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlTag(PicoStr);
impl HtmlTag {
/// Intern an HTML tag string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("tag name must not be empty");
}
if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) {
bail!("the character {} is not valid in a tag name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlTag`.
///
/// Should only be used in const contexts because it can panic.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("tag name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) {
panic!("not all characters are valid in a tag name");
}
i += 1;
}
Self(PicoStr::constant(string))
}
/// Resolves the tag to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the tag into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.resolve())
}
}
cast! {
HtmlTag,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Attributes of an HTML element.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
impl HtmlAttrs {
/// Creates an empty attribute list.
pub fn new() -> Self {
Self::default()
}
/// Adds an attribute.
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.push((attr, value.into()));
}
/// Adds an attribute to the start of the list.
pub fn push_front(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.insert(0, (attr, value.into()));
}
/// Finds an attribute value.
pub fn get(&self, attr: HtmlAttr) -> Option<&EcoString> {
self.0.iter().find(|&&(k, _)| k == attr).map(|(_, v)| v)
}
}
cast! {
HtmlAttrs,
self => self.0
.into_iter()
.map(|(key, value)| (key.resolve().as_str().into(), value.into_value()))
.collect::<Dict>()
.into_value(),
values: Dict => Self(values
.into_iter()
.map(|(k, v)| {
let attr = HtmlAttr::intern(&k)?;
let value = v.cast::<EcoString>()?;
Ok((attr, value))
})
.collect::<HintedStrResult<_>>()?),
}
/// An attribute of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr);
impl HtmlAttr {
/// Intern an HTML attribute string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("attribute name must not be empty");
}
if let Some(c) =
string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c))
{
bail!("the character {} is not valid in an attribute name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlAttr`.
///
/// Must only be used in const contexts (in a constant definition or
/// explicit `const { .. }` block) because otherwise a panic for a malformed
/// attribute or not auto-internible constant will only be caught at
/// runtime.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("attribute name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
if !bytes[i].is_ascii()
|| !charsets::is_valid_in_attribute_name(bytes[i] as char)
{
panic!("not all characters are valid in an attribute name");
}
i += 1;
}
Self(PicoStr::constant(string))
}
/// Resolves the attribute to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the attribute into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.resolve())
}
}
cast! {
HtmlAttr,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Layouted content that will be embedded into HTML as an SVG.
#[derive(Debug, Clone, Hash)]
pub struct HtmlFrame {
/// The frame that will be displayed as an SVG.
pub inner: Frame,
/// The text size where the frame was defined. This is used to size the
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
/// An ID to assign to the SVG itself.
pub id: Option<EcoString>,
/// IDs to assign to destination jump points within the SVG.
pub link_points: Vec<(Point, EcoString)>,
}
impl HtmlFrame {
/// Wraps a laid-out frame.
pub fn new(inner: Frame, styles: StyleChain) -> Self {
Self {
inner,
text_size: styles.resolve(TextElem::size),
id: None,
link_points: vec![],
}
}
}

View File

@ -1,14 +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::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag}; use typst_library::introspection::Introspector;
use typst_library::layout::Frame;
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)?;
@ -18,17 +21,26 @@ 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,
} }
/// Write a newline and indent, if pretty printing is enabled. 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.
fn write_indent(w: &mut Writer) { fn write_indent(w: &mut Writer) {
if w.pretty { if w.pretty {
w.buf.push('\n'); w.buf.push('\n');
@ -38,7 +50,7 @@ fn write_indent(w: &mut Writer) {
} }
} }
/// Encode an HTML node into the writer. /// Encodes an HTML node into the writer.
fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> { fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
match node { match node {
HtmlNode::Tag(_) => {} HtmlNode::Tag(_) => {}
@ -49,7 +61,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Encode plain text into the writer. /// Encodes plain text into the writer.
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> { fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
for c in text.chars() { for c in text.chars() {
if charsets::is_valid_in_normal_element_text(c) { if charsets::is_valid_in_normal_element_text(c) {
@ -61,7 +73,7 @@ fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Encode one element into the write. /// Encodes one element into the writer.
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.buf.push('<'); w.buf.push('<');
w.buf.push_str(&element.tag.resolve()); w.buf.push_str(&element.tag.resolve());
@ -69,54 +81,37 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
for (attr, value) in &element.attrs.0 { for (attr, value) in &element.attrs.0 {
w.buf.push(' '); w.buf.push(' ');
w.buf.push_str(&attr.resolve()); w.buf.push_str(&attr.resolve());
w.buf.push('=');
w.buf.push('"'); // If the string is empty, we can use shorthand syntax.
for c in value.chars() { // `<elem attr="">..</div` is equivalent to `<elem attr>..</div>`
if charsets::is_valid_in_attribute_value(c) { if !value.is_empty() {
w.buf.push(c); w.buf.push('=');
} else { w.buf.push('"');
write_escape(w, c).at(element.span)?; for c in value.chars() {
if charsets::is_valid_in_attribute_value(c) {
w.buf.push(c);
} else {
write_escape(w, c).at(element.span)?;
}
} }
w.buf.push('"');
} }
w.buf.push('"');
} }
w.buf.push('>'); w.buf.push('>');
if tag::is_void(element.tag) { if tag::is_void(element.tag) {
if !element.children.is_empty() {
bail!(element.span, "HTML void elements must not have children");
}
return Ok(()); return Ok(());
} }
let pretty = w.pretty; if tag::is_raw(element.tag) {
if !element.children.is_empty() { write_raw(w, element)?;
let pretty_inside = allows_pretty_inside(element.tag) } else if !element.children.is_empty() {
&& element.children.iter().any(|node| match node { write_children(w, element)?;
HtmlNode::Element(child) => wants_pretty_around(child.tag),
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_around = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_around {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_around;
}
w.level -= 1;
write_indent(w);
} }
w.pretty = pretty;
w.buf.push_str("</"); w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve()); w.buf.push_str(&element.tag.resolve());
@ -125,6 +120,156 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Encodes the children of an element.
fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
// See HTML spec § 13.1.2.5.
if matches!(element.tag, tag::pre | tag::textarea) && starts_with_newline(element) {
w.buf.push('\n');
}
let pretty = w.pretty;
let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag),
HtmlNode::Frame(_) => true,
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_around = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_around {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_around;
}
w.level -= 1;
write_indent(w);
w.pretty = pretty;
Ok(())
}
/// Whether the first character in the element is a newline.
fn starts_with_newline(element: &HtmlElement) -> bool {
for child in &element.children {
match child {
HtmlNode::Tag(_) => {}
HtmlNode::Text(text, _) => return text.starts_with(['\n', '\r']),
_ => return false,
}
}
false
}
/// Encodes the contents of a raw text element.
fn write_raw(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
let text = collect_raw_text(element)?;
if let Some(closing) = find_closing_tag(&text, element.tag) {
bail!(
element.span,
"HTML raw text element cannot contain its own closing tag";
hint: "the sequence `{closing}` appears in the raw text",
)
}
let mode = if w.pretty { RawMode::of(element, &text) } else { RawMode::Keep };
match mode {
RawMode::Keep => {
w.buf.push_str(&text);
}
RawMode::Wrap => {
w.buf.push('\n');
w.buf.push_str(&text);
write_indent(w);
}
RawMode::Indent => {
w.level += 1;
for line in text.lines() {
write_indent(w);
w.buf.push_str(line);
}
w.level -= 1;
write_indent(w);
}
}
Ok(())
}
/// Collects the textual contents of a raw text element.
fn collect_raw_text(element: &HtmlElement) -> SourceResult<String> {
let mut output = String::new();
for c in &element.children {
match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Text(text, _) => output.push_str(text),
HtmlNode::Element(_) | HtmlNode::Frame(_) => {
let span = match c {
HtmlNode::Element(child) => child.span,
_ => element.span,
};
bail!(span, "HTML raw text element cannot have non-text children")
}
};
}
Ok(output)
}
/// Finds a closing sequence for the given tag in the text, if it exists.
///
/// See HTML spec § 13.1.2.6.
fn find_closing_tag(text: &str, tag: HtmlTag) -> Option<&str> {
let s = tag.resolve();
let len = s.len();
text.match_indices("</").find_map(|(i, _)| {
let rest = &text[i + 2..];
let disallowed = rest.len() >= len
&& rest[..len].eq_ignore_ascii_case(&s)
&& rest[len..].starts_with(['\t', '\n', '\u{c}', '\r', ' ', '>', '/']);
disallowed.then(|| &text[i..i + 2 + len])
})
}
/// How to format the contents of a raw text element.
enum RawMode {
/// Just don't touch it.
Keep,
/// Newline after the opening and newline + indent before the closing tag.
Wrap,
/// Newlines after opening and before closing tag and each line indented.
Indent,
}
impl RawMode {
fn of(element: &HtmlElement, text: &str) -> Self {
match element.tag {
tag::script
if !element.attrs.0.iter().any(|(attr, value)| {
*attr == attr::r#type && value != "text/javascript"
}) =>
{
// Template literals can be multi-line, so indent may change
// the semantics of the JavaScript.
if text.contains('`') { Self::Wrap } else { Self::Indent }
}
tag::style => Self::Indent,
_ => Self::Keep,
}
}
}
/// Whether we are allowed to add an extra newline at the start and end of the /// Whether we are allowed to add an extra newline at the start and end of the
/// element's contents. /// element's contents.
/// ///
@ -160,15 +305,19 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
c if charsets::is_w3c_text_char(c) && c != '\r' => { c if charsets::is_w3c_text_char(c) && c != '\r' => {
write!(w.buf, "&#x{:x};", c as u32).unwrap() write!(w.buf, "&#x{:x};", c as u32).unwrap()
} }
_ => bail!("the character {} cannot be encoded in HTML", c.repr()), _ => bail!("the character `{}` cannot be encoded in HTML", c.repr()),
} }
Ok(()) Ok(())
} }
/// Encode a laid out frame into the writer. /// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &Frame) { 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) &frame.inner,
.replace("<svg class", "<svg style=\"overflow: visible;\" class"); frame.text_size,
frame.id.as_deref(),
&frame.link_points,
w.introspector,
);
w.buf.push_str(&svg); w.buf.push_str(&svg);
} }

View File

@ -0,0 +1,76 @@
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink};
use typst_library::World;
use typst_library::routines::{Arenas, FragmentKind, RealizationKind, Routines};
use crate::HtmlNode;
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
// No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment {
kind: &mut FragmentKind::Block,
is_inline: crate::convert::is_inline,
},
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
crate::convert::convert_to_nodes(&mut engine, &mut locator, children.iter().copied())
}

View File

@ -1,357 +1,118 @@
//! Typst's HTML exporter. //! Typst's HTML exporter.
mod attr;
mod charsets;
mod convert;
mod css;
mod document;
mod dom;
mod encode; mod encode;
mod fragment;
mod link;
mod rules;
mod tag;
mod typed;
pub use self::document::html_document;
pub use self::dom::*;
pub use self::encode::html; pub use self::encode::html;
pub use self::rules::register;
use comemo::{Track, Tracked, TrackedMut}; use ecow::EcoString;
use typst_library::diag::{bail, warning, At, SourceResult}; use typst_library::Category;
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{Content, Module, Scope};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; use typst_macros::elem;
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode,
};
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World;
use typst_syntax::Span;
/// Produce an HTML document from content. /// Creates the module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.start_category(Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
crate::typed::define(&mut html);
Module::new("html", html)
}
/// An HTML element that can contain Typst content.
/// ///
/// This first performs root-level realization and then turns the resulting /// Typst's HTML export automatically generates the appropriate tags for most
/// elements into HTML. /// elements. However, sometimes, it is desirable to retain more control. For
#[typst_macros::time(name = "html document")] /// example, when using Typst to generate your blog, you could use this function
pub fn html_document( /// to wrap each article in an `<article>` tag.
engine: &mut Engine, ///
content: &Content, /// Typst is aware of what is valid HTML. A tag and its attributes must form
styles: StyleChain, /// syntactically valid HTML. Some tags, like `meta` do not accept content.
) -> SourceResult<HtmlDocument> { /// Hence, you must not provide a body for them. We may add more checks in the
html_document_impl( /// future, so be sure that you are generating valid HTML when using this
engine.routines, /// function.
engine.world, ///
engine.introspector, /// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
engine.traced, /// you instead create them with this function, Typst will omit its own tags.
TrackedMut::reborrow_mut(&mut engine.sink), ///
engine.route.track(), /// ```typ
content, /// #html.elem("div", attrs: (style: "background: aqua"))[
styles, /// A div with _Typst content_ inside!
) /// ]
/// ```
#[elem(name = "elem")]
pub struct HtmlElem {
/// The element's tag.
#[required]
pub tag: HtmlTag,
/// The element's HTML attributes.
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
pub body: Option<Content>,
} }
/// The internal implementation of `html_document`. impl HtmlElem {
#[comemo::memoize] /// Add an attribute to the element.
#[allow(clippy::too_many_arguments)] pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
fn html_document_impl( self.attrs
routines: &Routines, .as_option_mut()
world: Tracked<dyn World + '_>, .get_or_insert_with(Default::default)
introspector: Tracked<Introspector>, .push(attr, value);
traced: Tracked<Traced>, self
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument(&mut info),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?;
Ok(HtmlDocument { info, root, introspector })
}
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
// No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
handle_list(&mut engine, &mut locator, children.iter().copied())
}
/// Convert children into HTML nodes.
fn handle_list<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert a child into HTML node(s).
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(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
if tag::is_void(elem.tag) && !children.is_empty() {
bail!(elem.span(), "HTML void elements may not have children");
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs(styles).clone(),
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(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(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(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::set_target(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(frame));
} 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

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

View File

@ -0,0 +1,271 @@
//! Predefined constants for HTML tags.
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use crate::HtmlTag;
pub const a: HtmlTag = HtmlTag::constant("a");
pub const abbr: HtmlTag = HtmlTag::constant("abbr");
pub const address: HtmlTag = HtmlTag::constant("address");
pub const area: HtmlTag = HtmlTag::constant("area");
pub const article: HtmlTag = HtmlTag::constant("article");
pub const aside: HtmlTag = HtmlTag::constant("aside");
pub const audio: HtmlTag = HtmlTag::constant("audio");
pub const b: HtmlTag = HtmlTag::constant("b");
pub const base: HtmlTag = HtmlTag::constant("base");
pub const bdi: HtmlTag = HtmlTag::constant("bdi");
pub const bdo: HtmlTag = HtmlTag::constant("bdo");
pub const blockquote: HtmlTag = HtmlTag::constant("blockquote");
pub const body: HtmlTag = HtmlTag::constant("body");
pub const br: HtmlTag = HtmlTag::constant("br");
pub const button: HtmlTag = HtmlTag::constant("button");
pub const canvas: HtmlTag = HtmlTag::constant("canvas");
pub const caption: HtmlTag = HtmlTag::constant("caption");
pub const cite: HtmlTag = HtmlTag::constant("cite");
pub const code: HtmlTag = HtmlTag::constant("code");
pub const col: HtmlTag = HtmlTag::constant("col");
pub const colgroup: HtmlTag = HtmlTag::constant("colgroup");
pub const data: HtmlTag = HtmlTag::constant("data");
pub const datalist: HtmlTag = HtmlTag::constant("datalist");
pub const dd: HtmlTag = HtmlTag::constant("dd");
pub const del: HtmlTag = HtmlTag::constant("del");
pub const details: HtmlTag = HtmlTag::constant("details");
pub const dfn: HtmlTag = HtmlTag::constant("dfn");
pub const dialog: HtmlTag = HtmlTag::constant("dialog");
pub const div: HtmlTag = HtmlTag::constant("div");
pub const dl: HtmlTag = HtmlTag::constant("dl");
pub const dt: HtmlTag = HtmlTag::constant("dt");
pub const em: HtmlTag = HtmlTag::constant("em");
pub const embed: HtmlTag = HtmlTag::constant("embed");
pub const fieldset: HtmlTag = HtmlTag::constant("fieldset");
pub const figcaption: HtmlTag = HtmlTag::constant("figcaption");
pub const figure: HtmlTag = HtmlTag::constant("figure");
pub const footer: HtmlTag = HtmlTag::constant("footer");
pub const form: HtmlTag = HtmlTag::constant("form");
pub const h1: HtmlTag = HtmlTag::constant("h1");
pub const h2: HtmlTag = HtmlTag::constant("h2");
pub const h3: HtmlTag = HtmlTag::constant("h3");
pub const h4: HtmlTag = HtmlTag::constant("h4");
pub const h5: HtmlTag = HtmlTag::constant("h5");
pub const h6: HtmlTag = HtmlTag::constant("h6");
pub const head: HtmlTag = HtmlTag::constant("head");
pub const header: HtmlTag = HtmlTag::constant("header");
pub const hgroup: HtmlTag = HtmlTag::constant("hgroup");
pub const hr: HtmlTag = HtmlTag::constant("hr");
pub const html: HtmlTag = HtmlTag::constant("html");
pub const i: HtmlTag = HtmlTag::constant("i");
pub const iframe: HtmlTag = HtmlTag::constant("iframe");
pub const img: HtmlTag = HtmlTag::constant("img");
pub const input: HtmlTag = HtmlTag::constant("input");
pub const ins: HtmlTag = HtmlTag::constant("ins");
pub const kbd: HtmlTag = HtmlTag::constant("kbd");
pub const label: HtmlTag = HtmlTag::constant("label");
pub const legend: HtmlTag = HtmlTag::constant("legend");
pub const li: HtmlTag = HtmlTag::constant("li");
pub const link: HtmlTag = HtmlTag::constant("link");
pub const main: HtmlTag = HtmlTag::constant("main");
pub const map: HtmlTag = HtmlTag::constant("map");
pub const mark: HtmlTag = HtmlTag::constant("mark");
pub const menu: HtmlTag = HtmlTag::constant("menu");
pub const meta: HtmlTag = HtmlTag::constant("meta");
pub const meter: HtmlTag = HtmlTag::constant("meter");
pub const nav: HtmlTag = HtmlTag::constant("nav");
pub const noscript: HtmlTag = HtmlTag::constant("noscript");
pub const object: HtmlTag = HtmlTag::constant("object");
pub const ol: HtmlTag = HtmlTag::constant("ol");
pub const optgroup: HtmlTag = HtmlTag::constant("optgroup");
pub const option: HtmlTag = HtmlTag::constant("option");
pub const output: HtmlTag = HtmlTag::constant("output");
pub const p: HtmlTag = HtmlTag::constant("p");
pub const picture: HtmlTag = HtmlTag::constant("picture");
pub const pre: HtmlTag = HtmlTag::constant("pre");
pub const progress: HtmlTag = HtmlTag::constant("progress");
pub const q: HtmlTag = HtmlTag::constant("q");
pub const rp: HtmlTag = HtmlTag::constant("rp");
pub const rt: HtmlTag = HtmlTag::constant("rt");
pub const ruby: HtmlTag = HtmlTag::constant("ruby");
pub const s: HtmlTag = HtmlTag::constant("s");
pub const samp: HtmlTag = HtmlTag::constant("samp");
pub const script: HtmlTag = HtmlTag::constant("script");
pub const search: HtmlTag = HtmlTag::constant("search");
pub const section: HtmlTag = HtmlTag::constant("section");
pub const select: HtmlTag = HtmlTag::constant("select");
pub const slot: HtmlTag = HtmlTag::constant("slot");
pub const small: HtmlTag = HtmlTag::constant("small");
pub const source: HtmlTag = HtmlTag::constant("source");
pub const span: HtmlTag = HtmlTag::constant("span");
pub const strong: HtmlTag = HtmlTag::constant("strong");
pub const style: HtmlTag = HtmlTag::constant("style");
pub const sub: HtmlTag = HtmlTag::constant("sub");
pub const summary: HtmlTag = HtmlTag::constant("summary");
pub const sup: HtmlTag = HtmlTag::constant("sup");
pub const table: HtmlTag = HtmlTag::constant("table");
pub const tbody: HtmlTag = HtmlTag::constant("tbody");
pub const td: HtmlTag = HtmlTag::constant("td");
pub const template: HtmlTag = HtmlTag::constant("template");
pub const textarea: HtmlTag = HtmlTag::constant("textarea");
pub const tfoot: HtmlTag = HtmlTag::constant("tfoot");
pub const th: HtmlTag = HtmlTag::constant("th");
pub const thead: HtmlTag = HtmlTag::constant("thead");
pub const time: HtmlTag = HtmlTag::constant("time");
pub const title: HtmlTag = HtmlTag::constant("title");
pub const tr: HtmlTag = HtmlTag::constant("tr");
pub const track: HtmlTag = HtmlTag::constant("track");
pub const u: HtmlTag = HtmlTag::constant("u");
pub const ul: HtmlTag = HtmlTag::constant("ul");
pub const var: HtmlTag = HtmlTag::constant("var");
pub const video: HtmlTag = HtmlTag::constant("video");
pub const wbr: HtmlTag = HtmlTag::constant("wbr");
/// Whether this is a void tag whose associated element may not have
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
/// Whether an element is considered metadata.
pub fn is_metadata(tag: HtmlTag) -> bool {
matches!(
tag,
self::base
| self::link
| self::meta
| self::noscript
| self::script
| self::style
| self::template
| self::title
)
}
/// Whether nodes with the tag have the CSS property `display: block` by
/// default.
pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::html
| self::head
| self::body
| self::article
| self::aside
| self::h1
| self::h2
| self::h3
| self::h4
| self::h5
| self::h6
| self::hgroup
| self::nav
| self::section
| self::dd
| self::dl
| self::dt
| self::menu
| self::ol
| self::ul
| self::address
| self::blockquote
| self::dialog
| self::div
| self::fieldset
| self::figure
| self::figcaption
| self::footer
| self::form
| self::header
| self::hr
| self::legend
| self::main
| self::p
| self::pre
| self::search
)
}
/// Whether the element is inline-level as opposed to being block-level.
///
/// Not sure whether this distinction really makes sense. But we somehow
/// need to decide what to put into automatic paragraphs. A `<strong>`
/// should merged into a paragraph created by realization, but a `<div>`
/// shouldn't.
///
/// <https://www.w3.org/TR/html401/struct/global.html#block-inline>
/// <https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content>
/// <https://github.com/orgs/mdn/discussions/353>
pub fn is_inline_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::abbr
| self::a
| self::bdi
| self::b
| self::br
| self::bdo
| self::code
| self::cite
| self::dfn
| self::data
| self::i
| self::em
| self::mark
| self::kbd
| self::rp
| self::q
| self::ruby
| self::rt
| self::samp
| self::s
| self::span
| self::small
| self::sub
| self::strong
| self::time
| self::sup
| self::var
| self::u
)
}
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// by default.
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::table
| self::thead
| self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col
| self::colgroup
)
}

View File

@ -0,0 +1,720 @@
//! The typed HTML element API (e.g. `html.div`).
//!
//! The typed API is backed by generated data derived from the HTML
//! specification. See [generated] and `tools/codegen`.
use std::fmt::Write;
use std::num::{NonZeroI64, NonZeroU64};
use std::sync::LazyLock;
use bumpalo::Bump;
use comemo::Tracked;
use ecow::{EcoString, eco_format, eco_vec};
use typst_assets::html as data;
use typst_library::diag::{At, Hint, HintedStrResult, SourceResult, bail};
use typst_library::engine::Engine;
use typst_library::foundations::{
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
PositiveF64, Reflect, Scope, Str, Type, Value,
};
use typst_library::layout::{Axes, Axis, Dir, Length};
use typst_library::visualize::Color;
use typst_macros::cast;
use crate::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag, css, tag};
/// Hook up all typed HTML definitions.
pub(super) fn define(html: &mut Scope) {
for data in FUNCS.iter() {
html.define_func_with_data(data);
}
}
/// Lazily created functions for all typed HTML constructors.
static FUNCS: LazyLock<Vec<NativeFuncData>> = LazyLock::new(|| {
// Leaking is okay here. It's not meaningfully different from having
// memory-managed values as `FUNCS` is a static.
let bump = Box::leak(Box::new(Bump::new()));
data::ELEMS.iter().map(|info| create_func_data(info, bump)).collect()
});
/// Creates metadata for a native HTML element constructor function.
fn create_func_data(
element: &'static data::ElemInfo,
bump: &'static Bump,
) -> NativeFuncData {
NativeFuncData {
function: NativeFuncPtr(bump.alloc(
move |_: &mut Engine, _: Tracked<Context>, args: &mut Args| {
construct(element, args)
},
)),
name: element.name,
title: {
let title = bump.alloc_str(element.name);
title[0..1].make_ascii_uppercase();
title
},
docs: element.docs,
keywords: &[],
contextual: false,
scope: LazyLock::new(&|| Scope::new()),
params: LazyLock::new(bump.alloc(move || create_param_info(element))),
returns: LazyLock::new(&|| CastInfo::Type(Type::of::<Content>())),
}
}
/// Creates parameter signature metadata for an element.
fn create_param_info(element: &'static data::ElemInfo) -> Vec<ParamInfo> {
let mut params = vec![];
for attr in element.attributes() {
params.push(ParamInfo {
name: attr.name,
docs: attr.docs,
input: AttrType::convert(attr.ty).input(),
default: None,
positional: false,
named: true,
variadic: false,
required: false,
settable: false,
});
}
let tag = HtmlTag::constant(element.name);
if !tag::is_void(tag) {
params.push(ParamInfo {
name: "body",
docs: "The contents of the HTML element.",
input: CastInfo::Type(Type::of::<Content>()),
default: None,
positional: true,
named: false,
variadic: false,
required: false,
settable: false,
});
}
params
}
/// The native constructor function shared by all HTML elements.
fn construct(element: &'static data::ElemInfo, args: &mut Args) -> SourceResult<Value> {
let mut attrs = HtmlAttrs::default();
let mut errors = eco_vec![];
args.items.retain(|item| {
let Some(name) = &item.name else { return true };
let Some(attr) = element.get_attr(name) else { return true };
let span = item.value.span;
let value = std::mem::take(&mut item.value.v);
let ty = AttrType::convert(attr.ty);
match ty.cast(value).at(span) {
Ok(Some(string)) => attrs.push(HtmlAttr::constant(attr.name), string),
Ok(None) => {}
Err(diags) => errors.extend(diags),
}
false
});
if !errors.is_empty() {
return Err(errors);
}
let tag = HtmlTag::constant(element.name);
let mut elem = HtmlElem::new(tag);
if !attrs.0.is_empty() {
elem.attrs.set(attrs);
}
if !tag::is_void(tag) {
let body = args.eat::<Content>()?;
elem.body.set(body);
}
Ok(elem.into_value())
}
/// A dynamic representation of an attribute's type.
///
/// See the documentation of [`data::Type`] for more details on variants.
enum AttrType {
Presence,
Native(NativeType),
Strings(StringsType),
Union(UnionType),
List(ListType),
}
impl AttrType {
/// Converts the type definition into a representation suitable for casting
/// and reflection.
const fn convert(ty: data::Type) -> AttrType {
use data::Type;
match ty {
Type::Presence => Self::Presence,
Type::None => Self::of::<NoneValue>(),
Type::NoneEmpty => Self::of::<NoneEmpty>(),
Type::NoneUndefined => Self::of::<NoneUndefined>(),
Type::Auto => Self::of::<AutoValue>(),
Type::TrueFalse => Self::of::<TrueFalseBool>(),
Type::YesNo => Self::of::<YesNoBool>(),
Type::OnOff => Self::of::<OnOffBool>(),
Type::Int => Self::of::<i64>(),
Type::NonNegativeInt => Self::of::<u64>(),
Type::PositiveInt => Self::of::<NonZeroU64>(),
Type::Float => Self::of::<f64>(),
Type::PositiveFloat => Self::of::<PositiveF64>(),
Type::Str => Self::of::<Str>(),
Type::Char => Self::of::<char>(),
Type::Datetime => Self::of::<Datetime>(),
Type::Duration => Self::of::<Duration>(),
Type::Color => Self::of::<Color>(),
Type::HorizontalDir => Self::of::<HorizontalDir>(),
Type::IconSize => Self::of::<IconSize>(),
Type::ImageCandidate => Self::of::<ImageCandidate>(),
Type::SourceSize => Self::of::<SourceSize>(),
Type::Strings(start, end) => Self::Strings(StringsType { start, end }),
Type::Union(variants) => Self::Union(UnionType(variants)),
Type::List(inner, separator, shorthand) => {
Self::List(ListType { inner, separator, shorthand })
}
}
}
/// Produces the dynamic representation of an attribute type backed by a
/// native Rust type.
const fn of<T: IntoAttr>() -> Self {
Self::Native(NativeType::of::<T>())
}
/// See [`Reflect::input`].
fn input(&self) -> CastInfo {
match self {
Self::Presence => bool::input(),
Self::Native(ty) => (ty.input)(),
Self::Union(ty) => ty.input(),
Self::Strings(ty) => ty.input(),
Self::List(ty) => ty.input(),
}
}
/// See [`Reflect::castable`].
fn castable(&self, value: &Value) -> bool {
match self {
Self::Presence => bool::castable(value),
Self::Native(ty) => (ty.castable)(value),
Self::Union(ty) => ty.castable(value),
Self::Strings(ty) => ty.castable(value),
Self::List(ty) => ty.castable(value),
}
}
/// Tries to cast the value into this attribute's type and serialize it into
/// an HTML attribute string.
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
match self {
Self::Presence => value.cast::<bool>().map(|b| b.then(EcoString::new)),
Self::Native(ty) => (ty.cast)(value),
Self::Union(ty) => ty.cast(value),
Self::Strings(ty) => ty.cast(value),
Self::List(ty) => ty.cast(value),
}
}
}
/// An enumeration with generated string variants.
///
/// `start` and `end` are used to index into `data::ATTR_STRINGS`.
struct StringsType {
start: usize,
end: usize,
}
impl StringsType {
fn input(&self) -> CastInfo {
CastInfo::Union(
self.strings()
.iter()
.map(|(val, desc)| CastInfo::Value(val.into_value(), desc))
.collect(),
)
}
fn castable(&self, value: &Value) -> bool {
match value {
Value::Str(s) => self.strings().iter().any(|&(v, _)| v == s.as_str()),
_ => false,
}
}
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
if self.castable(&value) {
value.cast().map(Some)
} else {
Err(self.input().error(&value))
}
}
fn strings(&self) -> &'static [(&'static str, &'static str)] {
&data::ATTR_STRINGS[self.start..self.end]
}
}
/// A type that accepts any of the contained types.
struct UnionType(&'static [data::Type]);
impl UnionType {
fn input(&self) -> CastInfo {
CastInfo::Union(self.iter().map(|ty| ty.input()).collect())
}
fn castable(&self, value: &Value) -> bool {
self.iter().any(|ty| ty.castable(value))
}
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
for item in self.iter() {
if item.castable(&value) {
return item.cast(value);
}
}
Err(self.input().error(&value))
}
fn iter(&self) -> impl Iterator<Item = AttrType> {
self.0.iter().map(|&ty| AttrType::convert(ty))
}
}
/// A list of items separated by a specific separator char.
///
/// - <https://html.spec.whatwg.org/#space-separated-tokens>
/// - <https://html.spec.whatwg.org/#comma-separated-tokens>
struct ListType {
inner: &'static data::Type,
separator: char,
shorthand: bool,
}
impl ListType {
fn input(&self) -> CastInfo {
if self.shorthand {
Array::input() + self.inner().input()
} else {
Array::input()
}
}
fn castable(&self, value: &Value) -> bool {
Array::castable(value) || (self.shorthand && self.inner().castable(value))
}
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
let ty = self.inner();
if Array::castable(&value) {
let array = value.cast::<Array>()?;
let mut out = EcoString::new();
for (i, item) in array.into_iter().enumerate() {
let item = ty.cast(item)?.unwrap();
if item.as_str().contains(self.separator) {
let buf;
let name = match self.separator {
' ' => "space",
',' => "comma",
_ => {
buf = eco_format!("'{}'", self.separator);
buf.as_str()
}
};
bail!(
"array item may not contain a {name}";
hint: "the array attribute will be encoded as a \
{name}-separated string"
);
}
if i > 0 {
out.push(self.separator);
if self.separator == ',' {
out.push(' ');
}
}
out.push_str(&item);
}
Ok(Some(out))
} else if self.shorthand && ty.castable(&value) {
let item = ty.cast(value)?.unwrap();
Ok(Some(item))
} else {
Err(self.input().error(&value))
}
}
fn inner(&self) -> AttrType {
AttrType::convert(*self.inner)
}
}
/// A dynamic representation of attribute backed by a native type implementing
/// - the standard `Reflect` and `FromValue` traits for casting from a value,
/// - the special `IntoAttr` trait for conversion into an attribute string.
#[derive(Copy, Clone)]
struct NativeType {
input: fn() -> CastInfo,
cast: fn(Value) -> HintedStrResult<Option<EcoString>>,
castable: fn(&Value) -> bool,
}
impl NativeType {
/// Creates a dynamic native type from a native Rust type.
const fn of<T: IntoAttr>() -> Self {
Self {
cast: |value| {
let this = value.cast::<T>()?;
Ok(Some(this.into_attr()))
},
input: T::input,
castable: T::castable,
}
}
}
/// Casts a native type into an HTML attribute.
pub trait IntoAttr: FromValue {
/// Turn the value into an attribute string.
fn into_attr(self) -> EcoString;
}
impl IntoAttr for Str {
fn into_attr(self) -> EcoString {
self.into()
}
}
/// A boolean that is encoded as a string:
/// - `false` is encoded as `"false"`
/// - `true` is encoded as `"true"`
pub struct TrueFalseBool(pub bool);
cast! {
TrueFalseBool,
v: bool => Self(v),
}
impl IntoAttr for TrueFalseBool {
fn into_attr(self) -> EcoString {
if self.0 { "true" } else { "false" }.into()
}
}
/// A boolean that is encoded as a string:
/// - `false` is encoded as `"no"`
/// - `true` is encoded as `"yes"`
pub struct YesNoBool(pub bool);
cast! {
YesNoBool,
v: bool => Self(v),
}
impl IntoAttr for YesNoBool {
fn into_attr(self) -> EcoString {
if self.0 { "yes" } else { "no" }.into()
}
}
/// A boolean that is encoded as a string:
/// - `false` is encoded as `"off"`
/// - `true` is encoded as `"on"`
pub struct OnOffBool(pub bool);
cast! {
OnOffBool,
v: bool => Self(v),
}
impl IntoAttr for OnOffBool {
fn into_attr(self) -> EcoString {
if self.0 { "on" } else { "off" }.into()
}
}
impl IntoAttr for AutoValue {
fn into_attr(self) -> EcoString {
"auto".into()
}
}
impl IntoAttr for NoneValue {
fn into_attr(self) -> EcoString {
"none".into()
}
}
/// A `none` value that turns into an empty string attribute.
struct NoneEmpty;
cast! {
NoneEmpty,
_: NoneValue => NoneEmpty,
}
impl IntoAttr for NoneEmpty {
fn into_attr(self) -> EcoString {
"".into()
}
}
/// A `none` value that turns into the string `"undefined"`.
struct NoneUndefined;
cast! {
NoneUndefined,
_: NoneValue => NoneUndefined,
}
impl IntoAttr for NoneUndefined {
fn into_attr(self) -> EcoString {
"undefined".into()
}
}
impl IntoAttr for char {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for i64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for u64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for NonZeroI64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for NonZeroU64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for f64 {
fn into_attr(self) -> EcoString {
// HTML float literal allows all the things that Rust's float `Display`
// impl produces.
eco_format!("{self}")
}
}
impl IntoAttr for PositiveF64 {
fn into_attr(self) -> EcoString {
self.get().into_attr()
}
}
impl IntoAttr for Color {
fn into_attr(self) -> EcoString {
eco_format!("{}", css::color(self))
}
}
impl IntoAttr for Duration {
fn into_attr(self) -> EcoString {
// https://html.spec.whatwg.org/#valid-duration-string
let mut out = EcoString::new();
macro_rules! part {
($s:literal) => {
if !out.is_empty() {
out.push(' ');
}
write!(out, $s).unwrap();
};
}
let [weeks, days, hours, minutes, seconds] = self.decompose();
if weeks > 0 {
part!("{weeks}w");
}
if days > 0 {
part!("{days}d");
}
if hours > 0 {
part!("{hours}h");
}
if minutes > 0 {
part!("{minutes}m");
}
if seconds > 0 || out.is_empty() {
part!("{seconds}s");
}
out
}
}
impl IntoAttr for Datetime {
fn into_attr(self) -> EcoString {
let fmt = typst_utils::display(|f| match self {
Self::Date(date) => datetime::date(f, date),
Self::Time(time) => datetime::time(f, time),
Self::Datetime(datetime) => datetime::datetime(f, datetime),
});
eco_format!("{fmt}")
}
}
mod datetime {
use std::fmt::{self, Formatter, Write};
pub fn datetime(f: &mut Formatter, datetime: time::PrimitiveDateTime) -> fmt::Result {
// https://html.spec.whatwg.org/#valid-global-date-and-time-string
date(f, datetime.date())?;
f.write_char('T')?;
time(f, datetime.time())
}
pub fn date(f: &mut Formatter, date: time::Date) -> fmt::Result {
// https://html.spec.whatwg.org/#valid-date-string
write!(f, "{:04}-{:02}-{:02}", date.year(), date.month() as u8, date.day())
}
pub fn time(f: &mut Formatter, time: time::Time) -> fmt::Result {
// https://html.spec.whatwg.org/#valid-time-string
write!(f, "{:02}:{:02}", time.hour(), time.minute())?;
if time.second() > 0 {
write!(f, ":{:02}", time.second())?;
}
Ok(())
}
}
/// A direction on the X axis: `ltr` or `rtl`.
pub struct HorizontalDir(Dir);
cast! {
HorizontalDir,
v: Dir => {
if v.axis() == Axis::Y {
bail!("direction must be horizontal");
}
Self(v)
},
}
impl IntoAttr for HorizontalDir {
fn into_attr(self) -> EcoString {
self.0.into_attr()
}
}
impl IntoAttr for Dir {
fn into_attr(self) -> EcoString {
match self {
Self::LTR => "ltr".into(),
Self::RTL => "rtl".into(),
Self::TTB => "ttb".into(),
Self::BTT => "btt".into(),
}
}
}
/// A width/height pair for `<link rel="icon" sizes="..." />`.
pub struct IconSize(Axes<u64>);
cast! {
IconSize,
v: Axes<u64> => Self(v),
}
impl IntoAttr for IconSize {
fn into_attr(self) -> EcoString {
eco_format!("{}x{}", self.0.x, self.0.y)
}
}
/// <https://html.spec.whatwg.org/#image-candidate-string>
pub struct ImageCandidate(EcoString);
cast! {
ImageCandidate,
mut v: Dict => {
let src = v.take("src")?.cast::<EcoString>()?;
let width: Option<NonZeroU64> =
v.take("width").ok().map(Value::cast).transpose()?;
let density: Option<PositiveF64> =
v.take("density").ok().map(Value::cast).transpose()?;
v.finish(&["src", "width", "density"])?;
if src.is_empty() {
bail!("`src` must not be empty");
} else if src.starts_with(',') || src.ends_with(',') {
bail!("`src` must not start or end with a comma");
}
let mut out = src;
match (width, density) {
(None, None) => {}
(Some(width), None) => write!(out, " {width}w").unwrap(),
(None, Some(density)) => write!(out, " {}d", density.get()).unwrap(),
(Some(_), Some(_)) => bail!("cannot specify both `width` and `density`"),
}
Self(out)
},
}
impl IntoAttr for ImageCandidate {
fn into_attr(self) -> EcoString {
self.0
}
}
/// <https://html.spec.whatwg.org/multipage/images.html#valid-source-size-list>
pub struct SourceSize(EcoString);
cast! {
SourceSize,
mut v: Dict => {
let condition = v.take("condition")?.cast::<EcoString>()?;
let size = v
.take("size")?
.cast::<Length>()
.hint("CSS lengths that are not expressible as Typst lengths are not yet supported")
.hint("you can use `html.elem` to create a raw attribute")?;
Self(eco_format!("({condition}) {}", css::length(size)))
},
}
impl IntoAttr for SourceSize {
fn into_attr(self) -> EcoString {
self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tags_and_attr_const_internible() {
for elem in data::ELEMS {
let _ = HtmlTag::constant(elem.name);
}
for attr in data::ATTRS {
let _ = HtmlAttr::constant(attr.name);
}
}
}

View File

@ -17,7 +17,6 @@ typst = { workspace = true }
typst-eval = { workspace = true } typst-eval = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
if_chain = { workspace = true }
pathdiff = { workspace = true } pathdiff = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
unscanny = { workspace = true } unscanny = { workspace = true }

View File

@ -1,9 +1,11 @@
use std::collections::HashSet;
use comemo::Track; use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec}; use ecow::{EcoString, EcoVec, eco_vec};
use typst::foundations::{Label, Styles, Value}; use typst::foundations::{Label, Styles, Value};
use typst::layout::PagedDocument; use typst::layout::PagedDocument;
use typst::model::BibliographyElem; use typst::model::{BibliographyElem, FigureElem};
use typst::syntax::{ast, LinkedNode, SyntaxKind}; use typst::syntax::{LinkedNode, SyntaxKind, ast};
use crate::IdeWorld; use crate::IdeWorld;
@ -25,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,17 +69,30 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value
/// - All labels and descriptions for them, if available /// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after /// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography. /// belong to a bibliography.
///
/// Note: When multiple labels in the document have the same identifier,
/// this only returns the first one.
pub fn analyze_labels( pub fn analyze_labels(
document: &PagedDocument, document: &PagedDocument,
) -> (Vec<(Label, Option<EcoString>)>, usize) { ) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![]; let mut output = vec![];
let mut seen_labels = HashSet::new();
// Labels in the document. // Labels in the document.
for elem in document.introspector.all() { for elem in document.introspector.all() {
let Some(label) = elem.label() else { continue }; let Some(label) = elem.label() else { continue };
if !seen_labels.insert(label) {
continue;
}
let details = elem let details = elem
.get_by_name("caption") .to_packed::<FigureElem>()
.or_else(|_| elem.get_by_name("body")) .and_then(|figure| match figure.caption.as_option() {
Some(Some(caption)) => Some(caption.pack_ref()),
_ => None,
})
.unwrap_or(elem)
.get_by_name("body")
.ok() .ok()
.and_then(|field| match field { .and_then(|field| match field {
Value::Content(content) => Some(content), Value::Content(content) => Some(content),

View File

@ -2,18 +2,17 @@ use std::cmp::Reverse;
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::ffi::OsStr; use std::ffi::OsStr;
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use if_chain::if_chain;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst::foundations::{ use typst::foundations::{
fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, StyleChain, Styles,
StyleChain, Styles, Type, Value, Type, Value, fields_on, repr,
}; };
use typst::layout::{Alignment, Dir, PagedDocument}; use typst::layout::{Alignment, Dir, PagedDocument};
use typst::syntax::ast::AstNode; use typst::syntax::ast::AstNode;
use typst::syntax::{ use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source, FileId, LinkedNode, Side, Source, SyntaxKind, ast, is_id_continue, is_id_start,
SyntaxKind, is_ident,
}; };
use typst::text::{FontFlags, RawElem}; use typst::text::{FontFlags, RawElem};
use typst::visualize::Color; use typst::visualize::Color;
@ -22,7 +21,7 @@ use unscanny::Scanner;
use crate::utils::{ use crate::utils::{
check_value_recursively, globals, plain_docs_sentence, summarize_font_family, check_value_recursively, globals, plain_docs_sentence, summarize_font_family,
}; };
use crate::{analyze_expr, analyze_import, analyze_labels, named_items, IdeWorld}; use crate::{IdeWorld, analyze_expr, analyze_import, analyze_labels, named_items};
/// Autocomplete a cursor position in a source file. /// Autocomplete a cursor position in a source file.
/// ///
@ -76,7 +75,7 @@ pub struct Completion {
} }
/// A kind of item that can be completed. /// A kind of item that can be completed.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum CompletionKind { pub enum CompletionKind {
/// A syntactical structure. /// A syntactical structure.
@ -130,7 +129,14 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool {
return true; return true;
} }
// Start of a reference: "@|" or "@he|". // Start of a reference: "@|".
if ctx.leaf.kind() == SyntaxKind::Text && ctx.before.ends_with("@") {
ctx.from = ctx.cursor;
ctx.label_completions();
return true;
}
// An existing reference: "@he|".
if ctx.leaf.kind() == SyntaxKind::RefMarker { if ctx.leaf.kind() == SyntaxKind::RefMarker {
ctx.from = ctx.leaf.offset() + 1; ctx.from = ctx.leaf.offset() + 1;
ctx.label_completions(); ctx.label_completions();
@ -138,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
@ -448,7 +447,7 @@ fn field_access_completions(
match value { match value {
Value::Symbol(symbol) => { Value::Symbol(symbol) => {
for modifier in symbol.modifiers() { for modifier in symbol.modifiers() {
if let Ok(modified) = symbol.clone().modified(modifier) { if let Ok(modified) = symbol.clone().modified((), modifier) {
ctx.completions.push(Completion { ctx.completions.push(Completion {
kind: CompletionKind::Symbol(modified.get()), kind: CompletionKind::Symbol(modified.get()),
label: modifier.into(), label: modifier.into(),
@ -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,65 +664,61 @@ 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.
let mut deciding = ctx.leaf.clone(); let mut deciding = ctx.leaf.clone();
while !matches!( while !matches!(
deciding.kind(), deciding.kind(),
SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon SyntaxKind::LeftParen
| SyntaxKind::RightParen
| SyntaxKind::Comma
| SyntaxKind::Colon
) { ) {
let Some(prev) = deciding.prev_leaf() else { break }; let Some(prev) = deciding.prev_leaf() else { break };
deciding = prev; deciding = prev;
} }
// 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
@ -849,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"],
@ -1094,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;
} }
@ -1372,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 {
@ -1561,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.
@ -1579,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 {
@ -1641,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,
@ -1706,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]
@ -1734,6 +1755,8 @@ mod tests {
test("#numbering(\"foo\", 1, )", -2) test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"]) .must_include(["integer"])
.must_exclude(["string"]); .must_exclude(["string"]);
// After argument list no completions.
test("#numbering()", -1).must_exclude(["string"]);
} }
/// Test that autocompletion for values of known type picks up nested /// Test that autocompletion for values of known type picks up nested
@ -1829,18 +1852,27 @@ mod tests {
#[test] #[test]
fn test_autocomplete_fonts() { fn test_autocomplete_fonts() {
test("#text(font:)", -1) test("#text(font:)", -2)
.must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]);
test("#show link: set text(font: )", -1) test("#show link: set text(font: )", -2)
.must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]);
test("#show math.equation: set text(font: )", -1) test("#show math.equation: set text(font: )", -2)
.must_include(["\"New Computer Modern Math\""]) .must_include(["\"New Computer Modern Math\""])
.must_exclude(["\"Libertinus Serif\""]); .must_exclude(["\"Libertinus Serif\""]);
test("#show math.equation: it => { set text(font: )\nit }", -6) test("#show math.equation: it => { set text(font: )\nit }", -7)
.must_include(["\"New Computer Modern Math\""]) .must_include(["\"New Computer Modern Math\""])
.must_exclude(["\"Libertinus Serif\""]); .must_exclude(["\"Libertinus Serif\""]);
} }
#[test]
fn test_autocomplete_typed_html() {
test("#html.div(translate: )", -2)
.must_include(["true", "false"])
.must_exclude(["\"yes\"", "\"no\""]);
test("#html.input(value: )", -2).must_include(["float", "string", "red", "blue"]);
test("#html.div(role: )", -2).must_include(["\"alertdialog\""]);
}
} }

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.
@ -72,7 +72,8 @@ pub fn definition(
// Try to jump to the referenced content. // Try to jump to the referenced content.
DerefTarget::Ref(node) => { DerefTarget::Ref(node) => {
let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target())); let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target()))
.expect("unexpected empty reference");
let selector = Selector::Label(label); let selector = Selector::Label(label);
let elem = document?.introspector.query_first(&selector)?; let elem = document?.introspector.query_first(&selector)?;
return Some(Definition::Span(elem.span())); return Some(Definition::Span(elem.span()));
@ -89,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>);
@ -186,6 +187,6 @@ mod tests {
#[test] #[test]
fn test_definition_std() { fn test_definition_std() {
test("#table", 1, Side::After).must_be_value(typst::model::TableElem::elem()); test("#table", 1, Side::After).must_be_value(typst::model::TableElem::ELEM);
} }
} }

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,8 +9,8 @@ use typst::layout::{Abs, Margin, PageElem};
use typst::syntax::package::{PackageSpec, PackageVersion}; use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash}; use typst::utils::{LazyHash, singleton};
use typst::{Library, World}; use typst::{Feature, Library, LibraryExt, World};
use crate::IdeWorld; use crate::IdeWorld;
@ -168,14 +168,14 @@ fn library() -> Library {
// Set page width to 120pt with 10pt margins, so that the inner page is // Set page width to 120pt with 10pt margins, so that the inner page is
// exactly 100pt wide. Page height is unbounded and font size is 10pt so // exactly 100pt wide. Page height is unbounded and font size is 10pt so
// that it multiplies to nice round numbers. // that it multiplies to nice round numbers.
let mut lib = typst::Library::default(); let mut lib = typst::Library::builder()
.with_features([Feature::Html].into_iter().collect())
.build();
lib.styles.set(PageElem::width, Smart::Custom(Abs::pt(120.0).into()));
lib.styles.set(PageElem::height, Smart::Auto);
lib.styles lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); .set(PageElem::margin, Margin::splat(Some(Smart::Custom(Abs::pt(10.0).into()))));
lib.styles.set(PageElem::set_height(Smart::Auto)); lib.styles.set(TextElem::size, TextSize(Abs::pt(10.0).into()));
lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
Abs::pt(10.0).into(),
)))));
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
lib lib
} }
@ -202,7 +202,8 @@ impl WorldLike for &str {
} }
} }
/// Specifies a position in a file for a test. /// Specifies a position in a file for a test. Negative numbers index from the
/// back. `-1` is at the very back.
pub trait FilePos { pub trait FilePos {
fn resolve(self, world: &TestWorld) -> (Source, usize); fn resolve(self, world: &TestWorld) -> (Source, usize);
} }

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

View File

@ -2,7 +2,7 @@ use std::fmt::Write;
use std::ops::ControlFlow; use std::ops::ControlFlow;
use comemo::Track; use comemo::Track;
use ecow::{eco_format, EcoString}; use ecow::{EcoString, eco_format};
use typst::engine::{Engine, Route, Sink, Traced}; use typst::engine::{Engine, Route, Sink, Traced};
use typst::foundations::{Scope, Value}; use typst::foundations::{Scope, Value};
use typst::introspection::Introspector; use typst::introspection::Introspector;
@ -119,11 +119,7 @@ pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope {
.is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash)); .is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash));
let library = world.library(); let library = world.library();
if in_math { if in_math { library.math.scope() } else { library.global.scope() }
library.math.scope()
} else {
library.global.scope()
}
} }
/// Checks whether the given value or any of its constituent parts satisfy the /// Checks whether the given value or any of its constituent parts satisfy the

View File

@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use ecow::eco_format; use ecow::eco_format;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::Deserialize; use serde::Deserialize;
use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; use typst_library::diag::{PackageError, PackageResult, StrResult, bail};
use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec}; use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
use crate::download::{Downloader, Progress}; use crate::download::{Downloader, Progress};
@ -87,6 +87,7 @@ impl PackageStorage {
) -> PackageResult<PathBuf> { ) -> PackageResult<PathBuf> {
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version); let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
// By default, search for the package locally
if let Some(packages_dir) = &self.package_path { if let Some(packages_dir) = &self.package_path {
let dir = packages_dir.join(&subdir); let dir = packages_dir.join(&subdir);
if dir.exists() { if dir.exists() {
@ -94,6 +95,7 @@ impl PackageStorage {
} }
} }
// As a fallback, look into the cache and possibly download from network.
if let Some(cache_dir) = &self.package_cache_path { if let Some(cache_dir) = &self.package_cache_path {
let dir = cache_dir.join(&subdir); let dir = cache_dir.join(&subdir);
if dir.exists() { if dir.exists() {
@ -102,14 +104,42 @@ impl PackageStorage {
// Download from network if it doesn't exist yet. // Download from network if it doesn't exist yet.
if spec.namespace == DEFAULT_NAMESPACE { if spec.namespace == DEFAULT_NAMESPACE {
self.download_package(spec, cache_dir, progress)?; return self.download_package(spec, cache_dir, progress);
if dir.exists() {
return Ok(dir);
}
} }
} }
Err(PackageError::NotFound(spec.clone())) // None of the strategies above found the package, so all code paths
// from now on fail. The rest of the function is only to determine the
// cause of the failure.
// We try `namespace/` then `namespace/name/` then `namespace/name/version/`
// and see where the first error occurs.
let not_found = |msg| Err(PackageError::NotFound(spec.clone(), msg));
let Some(packages_dir) = &self.package_path else {
return not_found(eco_format!("cannot access local package storage"));
};
let namespace_dir = packages_dir.join(format!("{}", spec.namespace));
if !namespace_dir.exists() {
return not_found(eco_format!(
"the namespace @{} should be located at {}",
spec.namespace,
namespace_dir.display()
));
}
let package_dir = namespace_dir.join(format!("{}", spec.name));
if !package_dir.exists() {
return not_found(eco_format!(
"the registry at {} does not have package '{}'",
namespace_dir.display(),
spec.name
));
}
let latest = self.determine_latest_version(&spec.versionless()).ok();
Err(PackageError::VersionNotFound(
spec.clone(),
latest,
eco_format!("{}", namespace_dir.display()),
))
} }
/// Tries to determine the latest version of a package. /// Tries to determine the latest version of a package.
@ -171,25 +201,33 @@ impl PackageStorage {
spec: &PackageSpec, spec: &PackageSpec,
cache_dir: &Path, cache_dir: &Path,
progress: &mut dyn Progress, progress: &mut dyn Progress,
) -> PackageResult<()> { ) -> PackageResult<PathBuf> {
assert_eq!(spec.namespace, DEFAULT_NAMESPACE); assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
let url = format!( let namespace_url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}");
"{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/{}-{}.tar.gz", let url = format!("{namespace_url}/{}-{}.tar.gz", spec.name, spec.version);
spec.name, spec.version
);
let data = match self.downloader.download_with_progress(&url, progress) { let data = match self.downloader.download_with_progress(&url, progress) {
Ok(data) => data, Ok(data) => data,
Err(ureq::Error::Status(404, _)) => { Err(ureq::Error::Status(404, _)) => {
if let Ok(version) = self.determine_latest_version(&spec.versionless()) { if let Ok(version) = self.determine_latest_version(&spec.versionless()) {
return Err(PackageError::VersionNotFound(spec.clone(), version)); return Err(PackageError::VersionNotFound(
spec.clone(),
Some(version),
eco_format!("{namespace_url}"),
));
} else { } else {
return Err(PackageError::NotFound(spec.clone())); return Err(PackageError::NotFound(
spec.clone(),
eco_format!(
"the registry at {namespace_url} does not have package '{}'",
spec.name
),
));
} }
} }
Err(err) => { Err(err) => {
return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))) return Err(PackageError::NetworkFailed(Some(eco_format!("{err}"))));
} }
}; };
@ -199,7 +237,7 @@ impl PackageStorage {
// The place at which the specific package version will live in the end. // The place at which the specific package version will live in the end.
let package_dir = base_dir.join(format!("{}", spec.version)); let package_dir = base_dir.join(format!("{}", spec.version));
// To prevent multiple Typst instances from interferring, we download // To prevent multiple Typst instances from interfering, we download
// into a temporary directory first and then move this directory to // into a temporary directory first and then move this directory to
// its final destination. // its final destination.
// //
@ -235,8 +273,8 @@ impl PackageStorage {
// broken packages still occur even with the rename safeguard, we might // broken packages still occur even with the rename safeguard, we might
// consider more complex solutions like file locking or checksums. // consider more complex solutions like file locking or checksums.
match fs::rename(&tempdir, &package_dir) { match fs::rename(&tempdir, &package_dir) {
Ok(()) => Ok(()), Ok(()) => Ok(package_dir),
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()), Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(package_dir),
Err(err) => Err(error("failed to move downloaded package directory", err)), Err(err) => Err(error("failed to move downloaded package directory", err)),
} }
} }

View File

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

View File

@ -24,15 +24,15 @@ pub fn layout_single_block(
region: Region, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
// Fetch sizing properties. // Fetch sizing properties.
let width = elem.width(styles); let width = elem.width.get(styles);
let height = elem.height(styles); let height = elem.height.get(styles);
let inset = elem.inset(styles).unwrap_or_default(); let inset = elem.inset.resolve(styles).unwrap_or_default();
// Build the pod regions. // Build the pod regions.
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size); let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size);
// Layout the body. // Layout the body.
let body = elem.body(styles); let body = elem.body.get_ref(styles);
let mut frame = match body { let mut frame = match body {
// If we have no body, just create one frame. Its size will be // If we have no body, just create one frame. Its size will be
// adjusted below. // adjusted below.
@ -73,18 +73,19 @@ pub fn layout_single_block(
} }
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill.get_cloned(styles);
let stroke = elem let stroke = elem
.stroke(styles) .stroke
.resolve(styles)
.unwrap_or_default() .unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default)); .map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking). // Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default()); let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default()); let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Clip the contents, if requested. // Clip the contents, if requested.
if elem.clip(styles) { if elem.clip.get(styles) {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset)); frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
} }
@ -111,9 +112,9 @@ pub fn layout_multi_block(
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Fetch sizing properties. // Fetch sizing properties.
let width = elem.width(styles); let width = elem.width.get(styles);
let height = elem.height(styles); let height = elem.height.get(styles);
let inset = elem.inset(styles).unwrap_or_default(); let inset = elem.inset.resolve(styles).unwrap_or_default();
// Allocate a small vector for backlogs. // Allocate a small vector for backlogs.
let mut buf = SmallVec::<[Abs; 2]>::new(); let mut buf = SmallVec::<[Abs; 2]>::new();
@ -122,7 +123,7 @@ pub fn layout_multi_block(
let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf); let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
// Layout the body. // Layout the body.
let body = elem.body(styles); let body = elem.body.get_ref(styles);
let mut fragment = match body { let mut fragment = match body {
// If we have no body, just create one frame plus one per backlog // If we have no body, just create one frame plus one per backlog
// region. We create them zero-sized; if necessary, their size will // region. We create them zero-sized; if necessary, their size will
@ -188,18 +189,19 @@ pub fn layout_multi_block(
}; };
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill.get_ref(styles);
let stroke = elem let stroke = elem
.stroke(styles) .stroke
.resolve(styles)
.unwrap_or_default() .unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default)); .map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking). // Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default()); let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default()); let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Fetch/compute these outside of the loop. // Fetch/compute these outside of the loop.
let clip = elem.clip(styles); let clip = elem.clip.get(styles);
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some); let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
let has_inset = !inset.is_zero(); let has_inset = !inset.is_zero();
let is_explicit = matches!(body, None | Some(BlockBody::Content(_))); let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));
@ -405,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;
@ -89,7 +89,7 @@ impl<'a> Collector<'a, '_, '_> {
} else if child.is::<FlushElem>() { } else if child.is::<FlushElem>() {
self.output.push(Child::Flush); self.output.push(Child::Flush);
} else if let Some(elem) = child.to_packed::<ColbreakElem>() { } else if let Some(elem) = child.to_packed::<ColbreakElem>() {
self.output.push(Child::Break(elem.weak(styles))); self.output.push(Child::Break(elem.weak.get(styles)));
} else if child.is::<PagebreakElem>() { } else if child.is::<PagebreakElem>() {
bail!( bail!(
child.span(), "pagebreaks are not allowed inside of containers"; child.span(), "pagebreaks are not allowed inside of containers";
@ -132,7 +132,7 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag)); self.output.push(Child::Tag(&elem.tag));
} }
let leading = ParElem::leading_in(styles); let leading = styles.resolve(ParElem::leading);
self.lines(lines, leading, styles); self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] { for (c, _) in &self.children[end..] {
@ -146,7 +146,9 @@ impl<'a> Collector<'a, '_, '_> {
/// Collect vertical spacing into a relative or fractional child. /// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) { fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount { self.output.push(match elem.amount {
Spacing::Rel(rel) => Child::Rel(rel.resolve(styles), elem.weak(styles) as u8), Spacing::Rel(rel) => {
Child::Rel(rel.resolve(styles), elem.weak.get(styles) as u8)
}
Spacing::Fr(fr) => Child::Fr(fr), Spacing::Fr(fr) => Child::Fr(fr),
}); });
} }
@ -169,8 +171,8 @@ impl<'a> Collector<'a, '_, '_> {
)? )?
.into_frames(); .into_frames();
let spacing = elem.spacing(styles); let spacing = elem.spacing.resolve(styles);
let leading = elem.leading(styles); let leading = elem.leading.resolve(styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
@ -184,8 +186,8 @@ impl<'a> Collector<'a, '_, '_> {
/// Collect laid-out lines. /// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) { fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles); let align = styles.resolve(AlignElem::alignment);
let costs = TextElem::costs_in(styles); let costs = styles.get(TextElem::costs);
// Determine whether to prevent widow and orphans. // Determine whether to prevent widow and orphans.
let len = lines.len(); let len = lines.len();
@ -231,23 +233,23 @@ impl<'a> Collector<'a, '_, '_> {
/// whether it is breakable. /// whether it is breakable.
fn block(&mut self, elem: &'a Packed<BlockElem>, styles: StyleChain<'a>) { fn block(&mut self, elem: &'a Packed<BlockElem>, styles: StyleChain<'a>) {
let locator = self.locator.next(&elem.span()); let locator = self.locator.next(&elem.span());
let align = AlignElem::alignment_in(styles).resolve(styles); let align = styles.resolve(AlignElem::alignment);
let alone = self.children.len() == 1; let alone = self.children.len() == 1;
let sticky = elem.sticky(styles); let sticky = elem.sticky.get(styles);
let breakable = elem.breakable(styles); let breakable = elem.breakable.get(styles);
let fr = match elem.height(styles) { let fr = match elem.height.get(styles) {
Sizing::Fr(fr) => Some(fr), Sizing::Fr(fr) => Some(fr),
_ => None, _ => None,
}; };
let fallback = LazyCell::new(|| ParElem::spacing_in(styles)); let fallback = LazyCell::new(|| styles.resolve(ParElem::spacing));
let spacing = |amount| match amount { let spacing = |amount| match amount {
Smart::Auto => Child::Rel((*fallback).into(), 4), Smart::Auto => Child::Rel((*fallback).into(), 4),
Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3), Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr), Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
}; };
self.output.push(spacing(elem.above(styles))); self.output.push(spacing(elem.above.get(styles)));
if !breakable || fr.is_some() { if !breakable || fr.is_some() {
self.output.push(Child::Single(self.boxed(SingleChild { self.output.push(Child::Single(self.boxed(SingleChild {
@ -272,7 +274,7 @@ impl<'a> Collector<'a, '_, '_> {
}))); })));
}; };
self.output.push(spacing(elem.below(styles))); self.output.push(spacing(elem.below.get(styles)));
self.par_situation = ParSituation::Other; self.par_situation = ParSituation::Other;
} }
@ -282,13 +284,13 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<PlaceElem>, elem: &'a Packed<PlaceElem>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<()> { ) -> SourceResult<()> {
let alignment = elem.alignment(styles); let alignment = elem.alignment.get(styles);
let align_x = alignment.map_or(FixedAlignment::Center, |align| { let align_x = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles) align.x().unwrap_or_default().resolve(styles)
}); });
let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles))); let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let scope = elem.scope(styles); let scope = elem.scope.get(styles);
let float = elem.float(styles); let float = elem.float.get(styles);
match (float, align_y) { match (float, align_y) {
(true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!( (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
@ -312,8 +314,8 @@ impl<'a> Collector<'a, '_, '_> {
} }
let locator = self.locator.next(&elem.span()); let locator = self.locator.next(&elem.span());
let clearance = elem.clearance(styles); let clearance = elem.clearance.resolve(styles);
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); let delta = Axes::new(elem.dx.get(styles), elem.dy.get(styles)).resolve(styles);
self.output.push(Child::Placed(self.boxed(PlacedChild { self.output.push(Child::Placed(self.boxed(PlacedChild {
align_x, align_x,
align_y, align_y,
@ -631,7 +633,7 @@ impl PlacedChild<'_> {
pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> { pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
self.cell.get_or_init(base, |base| { self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap(); let aligned = AlignElem::alignment.set(align).wrap();
let styles = self.styles.chain(&aligned); let styles = self.styles.chain(&aligned);
let mut frame = layout_and_modify(styles, |styles| { let mut frame = layout_and_modify(styles, |styles| {
@ -682,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.
@ -851,7 +847,7 @@ fn layout_line_number_reset(
config: &Config, config: &Config,
locator: &mut SplitLocator, locator: &mut SplitLocator,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem()); let counter = Counter::of(ParLineMarker::ELEM);
let update = CounterUpdate::Set(CounterState::init(false)); let update = CounterUpdate::Set(CounterState::init(false));
let content = counter.update(Span::detached(), update); let content = counter.update(Span::detached(), update);
crate::layout_frame( crate::layout_frame(
@ -879,7 +875,7 @@ fn layout_line_number(
locator: &mut SplitLocator, locator: &mut SplitLocator,
numbering: &Numbering, numbering: &Numbering,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem()); let counter = Counter::of(ParLineMarker::ELEM);
let update = CounterUpdate::Step(NonZeroUsize::ONE); let update = CounterUpdate::Step(NonZeroUsize::ONE);
let numbering = Smart::Custom(numbering.clone()); let numbering = Smart::Custom(numbering.clone());

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.
@ -98,8 +98,8 @@ pub fn layout_columns(
locator.track(), locator.track(),
styles, styles,
regions, regions,
elem.count(styles), elem.count.get(styles),
elem.gutter(styles), elem.gutter.resolve(styles),
) )
} }
@ -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,
@ -251,22 +251,22 @@ fn configuration<'x>(
let gutter = column_gutter.relative_to(regions.base().x); let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64; let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared); let dir = shared.resolve(TextElem::dir);
ColumnConfig { count, width, gutter, dir } ColumnConfig { count, width, gutter, dir }
}, },
footnote: FootnoteConfig { footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared), separator: shared.get_cloned(FootnoteEntry::separator),
clearance: FootnoteEntry::clearance_in(shared), clearance: shared.resolve(FootnoteEntry::clearance),
gap: FootnoteEntry::gap_in(shared), gap: shared.resolve(FootnoteEntry::gap),
expand: regions.expand.x, expand: regions.expand.x,
}, },
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig { line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared), scope: shared.get(ParLine::numbering_scope),
default_clearance: { default_clearance: {
let width = if PageElem::flipped_in(shared) { let width = if shared.get(PageElem::flipped) {
PageElem::height_in(shared) shared.resolve(PageElem::height)
} else { } else {
PageElem::width_in(shared) shared.resolve(PageElem::width)
}; };
// Clamp below is safe (min <= max): if the font size is // Clamp below is safe (min <= max): if the font size is

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.
@ -249,7 +249,7 @@ impl<'a> GridLayouter<'a> {
rowspans: vec![], rowspans: vec![],
finished: vec![], finished: vec![],
finished_header_rows: vec![], finished_header_rows: vec![],
is_rtl: TextElem::dir_in(styles) == Dir::RTL, is_rtl: styles.resolve(TextElem::dir) == Dir::RTL,
repeating_headers: vec![], repeating_headers: vec![],
upcoming_headers: &grid.headers, upcoming_headers: &grid.headers,
pending_headers: Default::default(), pending_headers: Default::default(),
@ -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

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

View File

@ -21,15 +21,15 @@ pub fn layout_box(
region: Size, region: Size,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
// Fetch sizing properties. // Fetch sizing properties.
let width = elem.width(styles); let width = elem.width.get(styles);
let height = elem.height(styles); let height = elem.height.get(styles);
let inset = elem.inset(styles).unwrap_or_default(); let inset = elem.inset.resolve(styles).unwrap_or_default();
// Build the pod region. // Build the pod region.
let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region); let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
// Layout the body. // Layout the body.
let mut frame = match elem.body(styles) { let mut frame = match elem.body.get_ref(styles) {
// If we have no body, just create an empty frame. If necessary, // If we have no body, just create an empty frame. If necessary,
// its size will be adjusted below. // its size will be adjusted below.
None => Frame::hard(Size::zero()), None => Frame::hard(Size::zero()),
@ -50,18 +50,19 @@ pub fn layout_box(
} }
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill.get_cloned(styles);
let stroke = elem let stroke = elem
.stroke(styles) .stroke
.resolve(styles)
.unwrap_or_default() .unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default)); .map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking). // Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default()); let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default()); let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Clip the contents, if requested. // Clip the contents, if requested.
if elem.clip(styles) { if elem.clip.get(styles) {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset)); frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
} }
@ -78,7 +79,7 @@ pub fn layout_box(
// Apply baseline shift. Do this after setting the size and applying the // Apply baseline shift. Do this after setting the size and applying the
// inset, so that a relative shift is resolved relative to the final // inset, so that a relative shift is resolved relative to the final
// height. // height.
let shift = elem.baseline(styles).relative_to(frame.height()); let shift = elem.baseline.resolve(styles).relative_to(frame.height());
if !shift.is_zero() { if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift); frame.set_baseline(frame.baseline() - shift);
} }

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.
@ -144,7 +144,7 @@ pub fn collect<'a>(
collector.push_text(" ", styles); collector.push_text(" ", styles);
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| { collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles); let dir = styles.resolve(TextElem::dir);
if dir != config.dir { if dir != config.dir {
// Insert "Explicit Directional Embedding". // Insert "Explicit Directional Embedding".
match dir { match dir {
@ -154,7 +154,7 @@ pub fn collect<'a>(
} }
} }
if let Some(case) = TextElem::case_in(styles) { if let Some(case) = styles.get(TextElem::case) {
full.push_str(&case.apply(&elem.text)); full.push_str(&case.apply(&elem.text));
} else { } else {
full.push_str(&elem.text); full.push_str(&elem.text);
@ -174,20 +174,22 @@ pub fn collect<'a>(
Spacing::Fr(fr) => Item::Fractional(fr, None), Spacing::Fr(fr) => Item::Fractional(fr, None),
Spacing::Rel(rel) => Item::Absolute( Spacing::Rel(rel) => Item::Absolute(
rel.resolve(styles).relative_to(region.x), rel.resolve(styles).relative_to(region.x),
elem.weak(styles), elem.weak.get(styles),
), ),
}); });
} else if let Some(elem) = child.to_packed::<LinebreakElem>() { } else if let Some(elem) = child.to_packed::<LinebreakElem>() {
collector collector.push_text(
.push_text(if elem.justify(styles) { "\u{2028}" } else { "\n" }, styles); if elem.justify.get(styles) { "\u{2028}" } else { "\n" },
styles,
);
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() { } else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
let double = elem.double(styles); let double = elem.double.get(styles);
if elem.enabled(styles) { if elem.enabled.get(styles) {
let quotes = SmartQuotes::get( let quotes = SmartQuotes::get(
elem.quotes(styles), elem.quotes.get_ref(styles),
TextElem::lang_in(styles), styles.get(TextElem::lang),
TextElem::region_in(styles), styles.get(TextElem::region),
elem.alternative(styles), elem.alternative.get(styles),
); );
let before = let before =
collector.full.chars().rev().find(|&c| !is_default_ignorable(c)); collector.full.chars().rev().find(|&c| !is_default_ignorable(c));
@ -206,7 +208,7 @@ pub fn collect<'a>(
} }
InlineItem::Frame(mut frame) => { InlineItem::Frame(mut frame) => {
frame.modify(&FrameModifiers::get_in(styles)); frame.modify(&FrameModifiers::get_in(styles));
apply_baseline_shift(&mut frame, styles); apply_shift(&engine.world, &mut frame, styles);
collector.push_item(Item::Frame(frame)); collector.push_item(Item::Frame(frame));
} }
} }
@ -215,13 +217,13 @@ pub fn collect<'a>(
collector.push_item(Item::Skip(POP_ISOLATE)); collector.push_item(Item::Skip(POP_ISOLATE));
} else if let Some(elem) = child.to_packed::<BoxElem>() { } else if let Some(elem) = child.to_packed::<BoxElem>() {
let loc = locator.next(&elem.span()); let loc = locator.next(&elem.span());
if let Sizing::Fr(v) = elem.width(styles) { if let Sizing::Fr(v) = elem.width.get(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else { } else {
let mut frame = layout_and_modify(styles, |styles| { let mut frame = layout_and_modify(styles, |styles| {
layout_box(elem, engine, loc, styles, region) layout_box(elem, engine, loc, styles, region)
})?; })?;
apply_baseline_shift(&mut frame, styles); apply_shift(&engine.world, &mut frame, styles);
collector.push_item(Item::Frame(frame)); collector.push_item(Item::Frame(frame));
} }
} else if let Some(elem) = child.to_packed::<TagElem>() { } else if let Some(elem) = child.to_packed::<TagElem>() {
@ -272,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

@ -2,10 +2,11 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine; use typst_library::engine::Engine;
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::{Lang, TextElem}; use typst_library::text::{Lang, TextElem, variant};
use typst_utils::Numeric; use typst_utils::Numeric;
use super::*; use super::*;
@ -154,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.
@ -217,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); {
} items.push(fallback, usize::MAX);
} }
items items
@ -270,10 +271,10 @@ fn collect_range<'a>(
items: &mut Items<'a>, items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>, fallback: &mut Option<ItemEntry<'a>>,
) { ) {
for (subrange, item) in p.slice(range.clone()) { for (idx, (subrange, item)) in p.slice(range.clone()).enumerate() {
// All non-text items are just kept, they can't be split. // All non-text items are just kept, they can't be split.
let Item::Text(shaped) = item else { let Item::Text(shaped) = item else {
items.push(item); items.push(item, idx);
continue; continue;
}; };
@ -293,10 +294,10 @@ fn collect_range<'a>(
} else if split { } else if split {
// When the item is split in half, reshape it. // When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced); let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped)); items.push(Item::Text(reshaped), idx);
} else { } else {
// When the item is fully contained, just keep it. // When the item is fully contained, just keep it.
items.push(item); items.push(item, idx);
} }
} }
} }
@ -330,7 +331,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
let shrink = glyph.shrinkability().0; let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink); glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(glyph.size);
} else if p.config.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& glyph.x_offset > Em::zero() && glyph.x_offset > Em::zero()
@ -342,7 +343,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
glyph.x_advance -= shrink; glyph.x_advance -= shrink;
glyph.x_offset = Em::zero(); glyph.x_offset = Em::zero();
glyph.adjustability.shrinkability.0 = Em::zero(); glyph.adjustability.shrinkability.0 = Em::zero();
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(glyph.size);
} }
} }
@ -360,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().1; let shrink = glyph.shrinkability().1;
let punct = shaped.glyphs.to_mut().last_mut().unwrap(); let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink); punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(punct.size);
} else if p.config.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one() && (glyph.x_advance - glyph.x_offset) > Em::one()
@ -371,7 +372,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let glyph = shaped.glyphs.to_mut().last_mut().unwrap(); let glyph = shaped.glyphs.to_mut().last_mut().unwrap();
glyph.x_advance -= shrink; glyph.x_advance -= shrink;
glyph.adjustability.shrinkability.1 = Em::zero(); glyph.adjustability.shrinkability.1 = Em::zero();
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(glyph.size);
} }
} }
@ -412,9 +413,31 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
} }
} }
/// Apply the current baseline shift to a frame. /// Apply the current baseline shift and italic compensation to a frame.
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) { pub fn apply_shift<'a>(
frame.translate(Point::with_y(TextElem::baseline_in(styles))); world: &Tracked<'a, dyn World + 'a>,
frame: &mut Frame,
styles: StyleChain,
) {
let mut baseline = styles.resolve(TextElem::baseline);
let mut compensation = Abs::zero();
if let Some(scripts) = styles.get_ref(TextElem::shift_settings) {
let font_metrics = styles
.get_ref(TextElem::font)
.into_iter()
.find_map(|family| {
world
.book()
.select(family.as_str(), variant(styles))
.and_then(|id| world.font(id))
})
.map_or(*scripts.kind.default_metrics(), |f| {
*scripts.kind.read_metrics(f.metrics())
});
baseline -= scripts.shift.unwrap_or(font_metrics.vertical_offset).resolve(styles);
compensation += font_metrics.horizontal_offset.resolve(styles);
}
frame.translate(Point::new(compensation, baseline));
} }
/// Commit to a line and build its frame. /// Commit to a line and build its frame.
@ -438,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()
&& TextElem::overhang_in(text.styles) && 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(text.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()
&& TextElem::overhang_in(text.styles) && 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(text.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
@ -499,16 +518,16 @@ pub fn commit(
// Build the frames and determine the height and baseline. // Build the frames and determine the height and baseline.
let mut frames = vec![]; let mut frames = vec![];
for item in line.items.iter() { for &(idx, ref item) in line.items.indexed_iter() {
let mut push = |offset: &mut Abs, frame: Frame| { let mut push = |offset: &mut Abs, frame: Frame, idx: usize| {
let width = frame.width(); let width = frame.width();
top.set_max(frame.baseline()); top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline()); bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame)); frames.push((*offset, frame, idx));
*offset += width; *offset += width;
}; };
match item { match &**item {
Item::Absolute(v, _) => { Item::Absolute(v, _) => {
offset += *v; offset += *v;
} }
@ -519,8 +538,8 @@ pub fn commit(
let mut frame = layout_and_modify(*styles, |styles| { let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), styles, region) layout_box(elem, engine, loc.relayout(), styles, region)
})?; })?;
apply_baseline_shift(&mut frame, *styles); apply_shift(&engine.world, &mut frame, *styles);
push(&mut offset, frame); push(&mut offset, frame, idx);
} else { } else {
offset += amount; offset += amount;
} }
@ -532,15 +551,15 @@ pub fn commit(
justification_ratio, justification_ratio,
extra_justification, extra_justification,
); );
push(&mut offset, frame); push(&mut offset, frame, idx);
} }
Item::Frame(frame) => { Item::Frame(frame) => {
push(&mut offset, frame.clone()); push(&mut offset, frame.clone(), idx);
} }
Item::Tag(tag) => { Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero()); let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame)); frames.push((offset, frame, idx));
} }
Item::Skip(_) => {} Item::Skip(_) => {}
} }
@ -559,8 +578,13 @@ pub fn commit(
add_par_line_marker(&mut output, marker, engine, locator, top); add_par_line_marker(&mut output, marker, engine, locator, top);
} }
// Ensure that the final frame's items are in logical order rather than in
// visual order. This is important because it affects the order of elements
// during introspection and thus things like counters.
frames.sort_unstable_by_key(|(_, _, idx)| *idx);
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame, _) in frames {
let x = offset + p.config.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
@ -627,7 +651,7 @@ fn overhang(c: char) -> f64 {
} }
/// A collection of owned or borrowed inline items. /// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec<ItemEntry<'a>>); pub struct Items<'a>(Vec<(usize, ItemEntry<'a>)>);
impl<'a> Items<'a> { impl<'a> Items<'a> {
/// Create empty items. /// Create empty items.
@ -636,33 +660,38 @@ impl<'a> Items<'a> {
} }
/// Push a new item. /// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) { pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>, idx: usize) {
self.0.push(entry.into()); self.0.push((idx, entry.into()));
} }
/// Iterate over the items /// Iterate over the items.
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> { pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.0.iter().map(|item| &**item) self.0.iter().map(|(_, item)| &**item)
}
/// Iterate over the items with indices
pub fn indexed_iter(&self) -> impl Iterator<Item = &(usize, ItemEntry<'a>)> {
self.0.iter()
} }
/// Access the first item. /// Access the first item.
pub fn first(&self) -> Option<&Item<'a>> { pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item) self.0.first().map(|(_, item)| &**item)
} }
/// Access the last item. /// Access the last item.
pub fn last(&self) -> Option<&Item<'a>> { pub fn last(&self) -> Option<&Item<'a>> {
self.0.last().map(|item| &**item) self.0.last().map(|(_, item)| &**item)
} }
/// Access the first item mutably, if it is text. /// Access the first item mutably, if it is text.
pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.first_mut()?.text_mut() self.0.first_mut()?.1.text_mut()
} }
/// Access the last item mutably, if it is text. /// Access the last item mutably, if it is text.
pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.last_mut()?.text_mut() self.0.last_mut()?.1.text_mut()
} }
/// Reorder the items starting at the given index to RTL. /// Reorder the items starting at the given index to RTL.
@ -673,12 +702,12 @@ impl<'a> Items<'a> {
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> { impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self {
Self(iter.into_iter().collect()) Self(iter.into_iter().enumerate().collect())
} }
} }
impl<'a> Deref for Items<'a> { impl<'a> Deref for Items<'a> {
type Target = Vec<ItemEntry<'a>>; type Target = Vec<(usize, ItemEntry<'a>)>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@ -698,6 +727,10 @@ impl Debug for Items<'_> {
} }
/// A reference to or a boxed item. /// A reference to or a boxed item.
///
/// This is conceptually similar to a [`Cow<'a, Item<'a>>`][std::borrow::Cow],
/// but we box owned items since an [`Item`] is much bigger than
/// a box.
pub enum ItemEntry<'a> { pub enum ItemEntry<'a> {
Ref(&'a Item<'a>), Ref(&'a Item<'a>),
Box(Box<Item<'a>>), Box(Box<Item<'a>>),

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
@ -846,7 +846,9 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.config.hyphenate.unwrap_or_else(|| { p.config.hyphenate.unwrap_or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
match item.text() { match item.text() {
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), Some(text) => {
text.styles.get(TextElem::hyphenate).unwrap_or(p.config.justify)
}
None => false, None => false,
} }
}) })
@ -857,7 +859,7 @@ fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.config.lang.or_else(|| { let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; let styles = item.text()?.styles;
Some(TextElem::lang_in(styles)) Some(styles.get(TextElem::lang))
})?; })?;
let bytes = lang.as_str().as_bytes().try_into().ok()?; let bytes = lang.as_str().as_bytes().try_into().ok()?;
@ -892,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
}
} }
} }
@ -927,9 +925,9 @@ impl Estimates {
let byte_len = g.range.len(); let byte_len = g.range.len();
let stretch = g.stretchability().0 + g.stretchability().1; let stretch = g.stretchability().0 + g.stretchability().1;
let shrink = g.shrinkability().0 + g.shrinkability().1; let shrink = g.shrinkability().0 + g.shrinkability().1;
widths.push(byte_len, g.x_advance.at(shaped.size)); widths.push(byte_len, g.x_advance.at(g.size));
stretchability.push(byte_len, stretch.at(shaped.size)); stretchability.push(byte_len, stretch.at(g.size));
shrinkability.push(byte_len, shrink.at(shaped.size)); shrinkability.push(byte_len, shrink.at(g.size));
justifiables.push(byte_len, g.is_justifiable() as usize); justifiables.push(byte_len, g.is_justifiable() as usize);
} }
} else { } else {

View File

@ -12,9 +12,10 @@ 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, Resolve, Smart, StyleChain}; use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::{ use typst_library::model::{
@ -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_baseline_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.
@ -113,10 +113,10 @@ fn layout_par_impl(
expand, expand,
Some(situation), Some(situation),
&ConfigBase { &ConfigBase {
justify: elem.justify(styles), justify: elem.justify.get(styles),
linebreaks: elem.linebreaks(styles), linebreaks: elem.linebreaks.get(styles),
first_line_indent: elem.first_line_indent(styles), first_line_indent: elem.first_line_indent.get(styles),
hanging_indent: elem.hanging_indent(styles), hanging_indent: elem.hanging_indent.resolve(styles),
}, },
) )
} }
@ -139,10 +139,10 @@ pub fn layout_inline<'a>(
expand, expand,
None, None,
&ConfigBase { &ConfigBase {
justify: ParElem::justify_in(shared), justify: shared.get(ParElem::justify),
linebreaks: ParElem::linebreaks_in(shared), linebreaks: shared.get(ParElem::linebreaks),
first_line_indent: ParElem::first_line_indent_in(shared), first_line_indent: shared.get(ParElem::first_line_indent),
hanging_indent: ParElem::hanging_indent_in(shared), hanging_indent: shared.resolve(ParElem::hanging_indent),
}, },
) )
} }
@ -184,17 +184,13 @@ fn configuration(
situation: Option<ParSituation>, situation: Option<ParSituation>,
) -> Config { ) -> Config {
let justify = base.justify; let justify = base.justify;
let font_size = TextElem::size_in(shared); let font_size = shared.resolve(TextElem::size);
let dir = TextElem::dir_in(shared); let dir = shared.resolve(TextElem::dir);
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;
@ -207,7 +203,7 @@ fn configuration(
Some(ParSituation::Other) => all, Some(ParSituation::Other) => all,
None => false, None => false,
} }
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into() && shared.resolve(AlignElem::alignment).x == dir.start().into()
{ {
amount.at(font_size) amount.at(font_size)
} else { } else {
@ -219,26 +215,26 @@ fn configuration(
} else { } else {
Abs::zero() Abs::zero()
}, },
numbering_marker: ParLine::numbering_in(shared).map(|numbering| { numbering_marker: shared.get_cloned(ParLine::numbering).map(|numbering| {
Packed::new(ParLineMarker::new( Packed::new(ParLineMarker::new(
numbering, numbering,
ParLine::number_align_in(shared), shared.get(ParLine::number_align),
ParLine::number_margin_in(shared), shared.get(ParLine::number_margin),
// Delay resolving the number clearance until line numbers are // Delay resolving the number clearance until line numbers are
// laid out to avoid inconsistent spacing depending on varying // laid out to avoid inconsistent spacing depending on varying
// font size. // font size.
ParLine::number_clearance_in(shared), shared.get(ParLine::number_clearance),
)) ))
}), }),
align: AlignElem::alignment_in(shared).fix(dir).x, align: shared.get(AlignElem::alignment).fix(dir).x,
font_size, font_size,
dir, dir,
hyphenate: shared_get(children, shared, TextElem::hyphenate_in) hyphenate: shared_get(children, shared, |s| s.get(TextElem::hyphenate))
.map(|uniform| uniform.unwrap_or(justify)), .map(|uniform| uniform.unwrap_or(justify)),
lang: shared_get(children, shared, TextElem::lang_in), lang: shared_get(children, shared, |s| s.get(TextElem::lang)),
fallback: TextElem::fallback_in(shared), fallback: shared.get(TextElem::fallback),
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(), cjk_latin_spacing: shared.get(TextElem::cjk_latin_spacing).is_auto(),
costs: TextElem::costs_in(shared), costs: shared.get(TextElem::costs),
} }
} }
@ -314,7 +310,7 @@ fn shared_get<T: PartialEq>(
/// When we support some kind of more general ancestry mechanism, this can /// When we support some kind of more general ancestry mechanism, this can
/// become more elegant. /// become more elegant.
fn in_list(styles: StyleChain) -> bool { fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0 styles.get(ListElem::depth).0 > 0
|| !EnumElem::parents_in(styles).is_empty() || !styles.get_cloned(EnumElem::parents).is_empty()
|| TermsElem::within_in(styles) || styles.get(TermsElem::within)
} }

View File

@ -144,7 +144,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
// The spacing is default to 1/4 em, and can be shrunk to 1/8 em. // The spacing is default to 1/4 em, and can be shrunk to 1/8 em.
glyph.x_advance += Em::new(0.25); glyph.x_advance += Em::new(0.25);
glyph.adjustability.shrinkability.1 += Em::new(0.125); glyph.adjustability.shrinkability.1 += Em::new(0.125);
text.width += Em::new(0.25).at(text.size); text.width += Em::new(0.25).at(glyph.size);
} }
// Case 2: Latin followed by a CJ character // Case 2: Latin followed by a CJ character
@ -152,7 +152,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
glyph.x_advance += Em::new(0.25); glyph.x_advance += Em::new(0.25);
glyph.x_offset += Em::new(0.25); glyph.x_offset += Em::new(0.25);
glyph.adjustability.shrinkability.0 += Em::new(0.125); glyph.adjustability.shrinkability.0 += Em::new(0.125);
text.width += Em::new(0.25).at(text.size); text.width += Em::new(0.25).at(glyph.size);
} }
prev = Some(glyph); prev = Some(glyph);

View File

@ -3,21 +3,22 @@ use std::fmt::{self, Debug, Formatter};
use std::sync::Arc; use std::sync::Arc;
use az::SaturatingAs; use az::SaturatingAs;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer}; use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer};
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, 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.
@ -41,8 +42,6 @@ pub struct ShapedText<'a> {
pub styles: StyleChain<'a>, pub styles: StyleChain<'a>,
/// The font variant. /// The font variant.
pub variant: FontVariant, pub variant: FontVariant,
/// The font size.
pub size: Abs,
/// The width of the text's bounding box. /// The width of the text's bounding box.
pub width: Abs, pub width: Abs,
/// The shaped glyphs. /// The shaped glyphs.
@ -62,6 +61,8 @@ pub struct ShapedGlyph {
pub x_offset: Em, pub x_offset: Em,
/// The vertical offset of the glyph. /// The vertical offset of the glyph.
pub y_offset: Em, pub y_offset: Em,
/// The font size for the glyph.
pub size: Abs,
/// The adjustability of the glyph. /// The adjustability of the glyph.
pub adjustability: Adjustability, pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full inline layout. A /// The byte range of this glyph's cluster in the full inline layout. A
@ -222,14 +223,17 @@ impl<'a> ShapedText<'a> {
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
frame.set_baseline(top); frame.set_baseline(top);
let shift = TextElem::baseline_in(self.styles); let size = self.styles.resolve(TextElem::size);
let decos = TextElem::deco_in(self.styles); let shift = self.styles.resolve(TextElem::baseline);
let fill = TextElem::fill_in(self.styles); let decos = self.styles.get_cloned(TextElem::deco);
let stroke = TextElem::stroke_in(self.styles); let fill = self.styles.get_ref(TextElem::fill);
let span_offset = TextElem::span_offset_in(self.styles); let stroke = self.styles.resolve(TextElem::stroke);
let span_offset = self.styles.get(TextElem::span_offset);
for ((font, y_offset), group) in for ((font, y_offset, glyph_size), group) in self
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) .glyphs
.as_ref()
.group_by_key(|g| (g.font.clone(), g.y_offset, g.size))
{ {
let mut range = group[0].range.clone(); let mut range = group[0].range.clone();
for glyph in group { for glyph in group {
@ -237,7 +241,7 @@ impl<'a> ShapedText<'a> {
range.end = range.end.max(glyph.range.end); range.end = range.end.max(glyph.range.end);
} }
let pos = Point::new(offset, top + shift - y_offset.at(self.size)); let pos = Point::new(offset, top + shift - y_offset.at(size));
let glyphs: Vec<Glyph> = group let glyphs: Vec<Glyph> = group
.iter() .iter()
.map(|shaped: &ShapedGlyph| { .map(|shaped: &ShapedGlyph| {
@ -257,11 +261,11 @@ impl<'a> ShapedText<'a> {
adjustability_right * justification_ratio; adjustability_right * justification_ratio;
if shaped.is_justifiable() { if shaped.is_justifiable() {
justification_right += justification_right +=
Em::from_length(extra_justification, self.size) Em::from_abs(extra_justification, glyph_size)
} }
frame.size_mut().x += justification_left.at(self.size) frame.size_mut().x += justification_left.at(glyph_size)
+ justification_right.at(self.size); + justification_right.at(glyph_size);
// We may not be able to reach the offset completely if // We may not be able to reach the offset completely if
// it exceeds u16, but better to have a roughly correct // it exceeds u16, but better to have a roughly correct
@ -304,7 +308,7 @@ impl<'a> ShapedText<'a> {
let item = TextItem { let item = TextItem {
font, font,
size: self.size, size: glyph_size,
lang: self.lang, lang: self.lang,
region: self.region, region: self.region,
fill: fill.clone(), fill: fill.clone(),
@ -336,12 +340,13 @@ impl<'a> ShapedText<'a> {
let mut top = Abs::zero(); let mut top = Abs::zero();
let mut bottom = Abs::zero(); let mut bottom = Abs::zero();
let top_edge = TextElem::top_edge_in(self.styles); let size = self.styles.resolve(TextElem::size);
let bottom_edge = TextElem::bottom_edge_in(self.styles); let top_edge = self.styles.get(TextElem::top_edge);
let bottom_edge = self.styles.get(TextElem::bottom_edge);
// Expand top and bottom by reading the font's vertical metrics. // Expand top and bottom by reading the font's vertical metrics.
let mut expand = |font: &Font, bounds: TextEdgeBounds| { let mut expand = |font: &Font, bounds: TextEdgeBounds| {
let (t, b) = font.edges(top_edge, bottom_edge, self.size, bounds); let (t, b) = font.edges(top_edge, bottom_edge, size, bounds);
top.set_max(t); top.set_max(t);
bottom.set_max(b); bottom.set_max(b);
}; };
@ -388,18 +393,16 @@ impl<'a> ShapedText<'a> {
pub fn stretchability(&self) -> Abs { pub fn stretchability(&self) -> Abs {
self.glyphs self.glyphs
.iter() .iter()
.map(|g| g.stretchability().0 + g.stretchability().1) .map(|g| (g.stretchability().0 + g.stretchability().1).at(g.size))
.sum::<Em>() .sum()
.at(self.size)
} }
/// The shrinkability of the text /// The shrinkability of the text
pub fn shrinkability(&self) -> Abs { pub fn shrinkability(&self) -> Abs {
self.glyphs self.glyphs
.iter() .iter()
.map(|g| g.shrinkability().0 + g.shrinkability().1) .map(|g| (g.shrinkability().0 + g.shrinkability().1).at(g.size))
.sum::<Em>() .sum()
.at(self.size)
} }
/// Reshape a range of the shaped text, reusing information from this /// Reshape a range of the shaped text, reusing information from this
@ -418,9 +421,8 @@ impl<'a> ShapedText<'a> {
lang: self.lang, lang: self.lang,
region: self.region, region: self.region,
styles: self.styles, styles: self.styles,
size: self.size,
variant: self.variant, variant: self.variant,
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size), width: glyphs_width(glyphs),
glyphs: Cow::Borrowed(glyphs), glyphs: Cow::Borrowed(glyphs),
} }
} else { } else {
@ -484,13 +486,15 @@ impl<'a> ShapedText<'a> {
// that subtracting either of the endpoints by self.base doesn't // that subtracting either of the endpoints by self.base doesn't
// underflow. See <https://github.com/typst/typst/issues/2283>. // underflow. See <https://github.com/typst/typst/issues/2283>.
.unwrap_or_else(|| self.base..self.base); .unwrap_or_else(|| self.base..self.base);
self.width += x_advance.at(self.size); let size = self.styles.resolve(TextElem::size);
self.width += x_advance.at(size);
let glyph = ShapedGlyph { let glyph = ShapedGlyph {
font, font,
glyph_id: glyph_id.0, glyph_id: glyph_id.0,
x_advance, x_advance,
x_offset: Em::zero(), x_offset: Em::zero(),
y_offset: Em::zero(), y_offset: Em::zero(),
size,
adjustability: Adjustability::default(), adjustability: Adjustability::default(),
range, range,
safe_to_break: true, safe_to_break: true,
@ -535,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 {
@ -599,9 +599,9 @@ pub fn shape_range<'a>(
range: Range, range: Range,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) { ) {
let script = TextElem::script_in(styles); let script = styles.get(TextElem::script);
let lang = TextElem::lang_in(styles); let lang = styles.get(TextElem::lang);
let region = TextElem::region_in(styles); let region = styles.get(TextElem::region);
let mut process = |range: Range, level: BidiLevel| { let mut process = |range: Range, level: BidiLevel| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped = let shaped =
@ -665,7 +665,8 @@ fn shape<'a>(
lang: Lang, lang: Lang,
region: Option<Region>, region: Option<Region>,
) -> ShapedText<'a> { ) -> ShapedText<'a> {
let size = TextElem::size_in(styles); let size = styles.resolve(TextElem::size);
let shift_settings = styles.get(TextElem::shift_settings);
let mut ctx = ShapingContext { let mut ctx = ShapingContext {
engine, engine,
size, size,
@ -674,8 +675,9 @@ fn shape<'a>(
styles, styles,
variant: variant(styles), variant: variant(styles),
features: features(styles), features: features(styles),
fallback: TextElem::fallback_in(styles), fallback: styles.get(TextElem::fallback),
dir, dir,
shift_settings,
}; };
if !text.is_empty() { if !text.is_empty() {
@ -698,16 +700,25 @@ fn shape<'a>(
region, region,
styles, styles,
variant: ctx.variant, variant: ctx.variant,
size, width: glyphs_width(&ctx.glyphs),
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
glyphs: Cow::Owned(ctx.glyphs), glyphs: Cow::Owned(ctx.glyphs),
} }
} }
/// Computes the width of a run of glyphs relative to the font size, accounting
/// for their individual scaling factors and other font metrics.
fn glyphs_width(glyphs: &[ShapedGlyph]) -> Abs {
glyphs.iter().map(|g| g.x_advance.at(g.size)).sum()
}
/// Holds shaping results and metadata common to all shaped segments. /// Holds shaping results and metadata common to all shaped segments.
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,
@ -715,6 +726,7 @@ struct ShapingContext<'a, 'v> {
features: Vec<rustybuzz::Feature>, features: Vec<rustybuzz::Feature>,
fallback: bool, fallback: bool,
dir: Dir, dir: Dir,
shift_settings: Option<ShiftSettings>,
} }
/// Shape text with font fallback using the `families` iterator. /// Shape text with font fallback using the `families` iterator.
@ -765,13 +777,16 @@ 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();
buffer.push_str(text); buffer.push_str(text);
buffer.set_language(language(ctx.styles)); buffer.set_language(language(ctx.styles));
if let Some(script) = TextElem::script_in(ctx.styles).custom().and_then(|script| { if let Some(script) = ctx.styles.get(TextElem::script).custom().and_then(|script| {
rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes())) rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes()))
}) { }) {
buffer.set_script(script) buffer.set_script(script)
@ -789,6 +804,18 @@ fn shape_segment<'a>(
// text extraction. // text extraction.
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES); buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
let (script_shift, script_compensation, scale, shift_feature) = ctx
.shift_settings
.map_or((Em::zero(), Em::zero(), Em::one(), None), |settings| {
determine_shift(text, &font, settings)
});
let has_shift_feature = shift_feature.is_some();
if let Some(feat) = shift_feature {
// Temporarily push the feature.
ctx.features.push(feat)
}
// Prepare the shape plan. This plan depends on direction, script, language, // Prepare the shape plan. This plan depends on direction, script, language,
// and features, but is independent from the text and can thus be memoized. // and features, but is independent from the text and can thus be memoized.
let plan = create_shape_plan( let plan = create_shape_plan(
@ -799,6 +826,10 @@ fn shape_segment<'a>(
&ctx.features, &ctx.features,
); );
if has_shift_feature {
ctx.features.pop();
}
// Shape! // Shape!
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
let infos = buffer.glyph_infos(); let infos = buffer.glyph_infos();
@ -869,8 +900,9 @@ fn shape_segment<'a>(
glyph_id: info.glyph_id as u16, glyph_id: info.glyph_id as u16,
// TODO: Don't ignore y_advance. // TODO: Don't ignore y_advance.
x_advance, x_advance,
x_offset: font.to_em(pos[i].x_offset), x_offset: font.to_em(pos[i].x_offset) + script_compensation,
y_offset: font.to_em(pos[i].y_offset), y_offset: font.to_em(pos[i].y_offset) + script_shift,
size: scale.at(ctx.size),
adjustability: Adjustability::default(), adjustability: Adjustability::default(),
range: start..end, range: start..end,
safe_to_break: !info.unsafe_to_break(), safe_to_break: !info.unsafe_to_break(),
@ -932,6 +964,64 @@ fn shape_segment<'a>(
ctx.used.pop(); ctx.used.pop();
} }
/// Returns a `(script_shift, script_compensation, scale, feature)` quadruplet
/// describing how to produce scripts.
///
/// Those values determine how the rendered text should be transformed to
/// display sub-/super-scripts. If the OpenType feature can be used, the
/// rendered text should not be transformed in any way, and so those values are
/// neutral (`(0, 0, 1, None)`). If scripts should be synthesized, those values
/// determine how to transform the rendered text to display scripts as expected.
fn determine_shift(
text: &str,
font: &Font,
settings: ShiftSettings,
) -> (Em, Em, Em, Option<Feature>) {
settings
.typographic
.then(|| {
// If typographic scripts are enabled (i.e., we want to use the
// OpenType feature instead of synthesizing if possible), we add
// "subs"/"sups" to the feature list if supported by the font.
// In case of a problem, we just early exit
let gsub = font.rusty().tables().gsub?;
let subtable_index =
gsub.features.find(settings.kind.feature())?.lookup_indices.get(0)?;
let coverage = gsub
.lookups
.get(subtable_index)?
.subtables
.get::<SubstitutionSubtable>(0)?
.coverage();
text.chars()
.all(|c| {
font.rusty().glyph_index(c).is_some_and(|i| coverage.contains(i))
})
.then(|| {
// If we can use the OpenType feature, we can keep the text
// as is.
(
Em::zero(),
Em::zero(),
Em::one(),
Some(Feature::new(settings.kind.feature(), 1, ..)),
)
})
})
// Reunite the cases where `typographic` is `false` or where using the
// OpenType feature would not work.
.flatten()
.unwrap_or_else(|| {
let script_metrics = settings.kind.read_metrics(font.metrics());
(
settings.shift.unwrap_or(script_metrics.vertical_offset),
script_metrics.horizontal_offset,
settings.size.unwrap_or(script_metrics.height),
None,
)
})
}
/// Create a shape plan. /// Create a shape plan.
#[comemo::memoize] #[comemo::memoize]
pub fn create_shape_plan( pub fn create_shape_plan(
@ -963,6 +1053,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
x_advance, x_advance,
x_offset: Em::zero(), x_offset: Em::zero(),
y_offset: Em::zero(), y_offset: Em::zero(),
size: ctx.size,
adjustability: Adjustability::default(), adjustability: Adjustability::default(),
range: start..end, range: start..end,
safe_to_break: true, safe_to_break: true,
@ -985,9 +1076,11 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
/// Apply tracking and spacing to the shaped glyphs. /// Apply tracking and spacing to the shaped glyphs.
fn track_and_space(ctx: &mut ShapingContext) { fn track_and_space(ctx: &mut ShapingContext) {
let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size); let tracking = Em::from_abs(ctx.styles.resolve(TextElem::tracking), ctx.size);
let spacing = let spacing = ctx
TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size)); .styles
.resolve(TextElem::spacing)
.map(|abs| Em::from_abs(abs, ctx.size));
let mut glyphs = ctx.glyphs.iter_mut().peekable(); let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() { while let Some(glyph) = glyphs.next() {

View File

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

View File

@ -20,20 +20,17 @@ pub fn layout_list(
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = elem.indent(styles); let indent = elem.indent.get(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent.get(styles);
let tight = elem.tight(styles); let tight = elem.tight.get(styles);
let gutter = elem.spacing(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) }
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
}
}); });
let Depth(depth) = ListElem::depth_in(styles); let Depth(depth) = styles.get(ListElem::depth);
let marker = elem let marker = elem
.marker(styles) .marker
.get_ref(styles)
.resolve(engine, styles, depth)? .resolve(engine, styles, depth)?
// avoid '#set align' interference with the list // avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top); .aligned(HAlignment::Start + VAlignment::Top);
@ -52,7 +49,7 @@ pub fn layout_list(
cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( cells.push(Cell::new(
body.styled(ListElem::set_depth(Depth(1))), body.set(ListElem::depth, Depth(1)),
locator.next(&item.body.span()), locator.next(&item.body.span()),
)); ));
} }
@ -81,40 +78,33 @@ pub fn layout_enum(
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let numbering = elem.numbering(styles); let numbering = elem.numbering.get_ref(styles);
let reversed = elem.reversed(styles); let reversed = elem.reversed.get(styles);
let indent = elem.indent(styles); let indent = elem.indent.get(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent.get(styles);
let tight = elem.tight(styles); let tight = elem.tight.get(styles);
let gutter = elem.spacing(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) }
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
}
}); });
let mut cells = vec![]; let mut cells = vec![];
let mut locator = locator.split(); let mut locator = locator.split();
let mut number = elem.start(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 = EnumElem::parents_in(styles);
let full = elem.full(styles); let full = elem.full.get(styles);
// Horizontally align based on the given respective parameter. // Horizontally align based on the given respective parameter.
// Vertically align to the top to avoid inheriting `horizon` or `bottom` // Vertically align to the top to avoid inheriting `horizon` or `bottom`
// alignment from the context and having the number be displaced in // alignment from the context and having the number be displaced in
// relation to the item it refers to. // relation to the item it refers to.
let number_align = elem.number_align(styles); let number_align = elem.number_align.get(styles);
for item in &elem.children { for item in &elem.children {
number = item.number(styles).unwrap_or(number); number = item.number.get(styles).unwrap_or(number);
let context = Context::new(None, Some(styles)); let context = Context::new(None, Some(styles));
let resolved = if full { let resolved = if full {
@ -133,8 +123,7 @@ pub fn layout_enum(
// Disable overhang as a workaround to end-aligned dots glitching // Disable overhang as a workaround to end-aligned dots glitching
// and decreasing spacing between numbers and items. // and decreasing spacing between numbers and items.
let resolved = let resolved = resolved.aligned(number_align).set(TextElem::overhang, false);
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
// Text in wide enums shall always turn into paragraphs. // Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone(); let mut body = item.body.clone();
@ -146,7 +135,7 @@ pub fn layout_enum(
cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( cells.push(Cell::new(
body.styled(EnumElem::set_parents(smallvec![number])), body.set(EnumElem::parents, smallvec![number]),
locator.next(&item.body.span()), locator.next(&item.body.span()),
)); ));
number = number =

View File

@ -4,8 +4,8 @@ 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, GlyphFragment, MathContext, MathFragment, style_cramped, style_dtls,
MathFragment, style_flac,
}; };
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
@ -24,7 +24,7 @@ pub fn layout_accent(
// Try to replace the base glyph with its dotless variant. // Try to replace the base glyph with its dotless variant.
let dtls = style_dtls(); let dtls = style_dtls();
let base_styles = let base_styles =
if top_accent && elem.dotless(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 = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
@ -47,7 +47,7 @@ pub fn layout_accent(
// 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(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(glyph.item.size);
glyph.stretch_horizontal(ctx, width - short_fall); glyph.stretch_horizontal(ctx, width - short_fall);
let accent_attach = glyph.accent_attach.0; let accent_attach = glyph.accent_attach.0;

View File

@ -7,8 +7,8 @@ use typst_library::math::{
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 {
@ -31,16 +31,16 @@ pub fn layout_attach(
let mut base = ctx.layout_into_fragment(&elem.base, styles)?; let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles); let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style); let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain); let tl = elem.tl.get_cloned(sup_style_chain);
let tr = elem.tr(sup_style_chain); let tr = elem.tr.get_cloned(sup_style_chain);
let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>()); let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
let t = elem.t(sup_style_chain); let t = elem.t.get_cloned(sup_style_chain);
let sub_style = style_for_subscript(styles); let sub_style = style_for_subscript(styles);
let sub_style_chain = styles.chain(&sub_style); let sub_style_chain = styles.chain(&sub_style);
let bl = elem.bl(sub_style_chain); let bl = elem.bl.get_cloned(sub_style_chain);
let br = elem.br(sub_style_chain); let br = elem.br.get_cloned(sub_style_chain);
let b = elem.b(sub_style_chain); let b = elem.b.get_cloned(sub_style_chain);
let limits = base.limits().active(styles); let limits = base.limits().active(styles);
let (t, tr) = match (t, tr) { let (t, tr) = match (t, tr) {
@ -146,7 +146,7 @@ pub fn layout_limits(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; let limits = if elem.inline.get(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(limits); fragment.set_limits(limits);
ctx.push(fragment); ctx.push(fragment);
@ -161,7 +161,8 @@ fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs
base = &equation.body; base = &equation.body;
} }
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles)) base.to_packed::<StretchElem>()
.map(|stretch| stretch.size.resolve(styles))
} }
/// Lay out the attachments. /// Lay out the attachments.
@ -397,7 +398,7 @@ fn compute_script_shifts(
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 EquationElem::cramped_in(styles) { let sup_shift_up = if styles.get(EquationElem::cramped) {
scaled!(ctx, styles, superscript_shift_up_cramped) scaled!(ctx, styles, superscript_shift_up_cramped)
} else { } else {
scaled!(ctx, styles, superscript_shift_up) scaled!(ctx, styles, superscript_shift_up)

View File

@ -27,16 +27,16 @@ pub fn layout_cancel(
let mut body = body.into_frame(); let mut body = body.into_frame();
let body_size = body.size(); let body_size = body.size();
let span = elem.span(); let span = elem.span();
let length = elem.length(styles); let length = elem.length.resolve(styles);
let stroke = elem.stroke(styles).unwrap_or(FixedStroke { let stroke = elem.stroke.resolve(styles).unwrap_or(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(), paint: styles.get_ref(TextElem::fill).as_decoration(),
..Default::default() ..Default::default()
}); });
let invert = elem.inverted(styles); let invert = elem.inverted.get(styles);
let cross = elem.cross(styles); let cross = elem.cross.get(styles);
let angle = elem.angle(styles); let angle = elem.angle.get_ref(styles);
let invert_first_line = !cross && invert; let invert_first_line = !cross && invert;
let first_line = draw_cancel_line( let first_line = draw_cancel_line(
@ -44,7 +44,7 @@ pub fn layout_cancel(
length, length,
stroke.clone(), stroke.clone(),
invert_first_line, invert_first_line,
&angle, angle,
body_size, body_size,
styles, styles,
span, span,
@ -57,7 +57,7 @@ pub fn layout_cancel(
if cross { if cross {
// Draw the second line. // Draw the second line.
let second_line = let second_line =
draw_cancel_line(ctx, length, stroke, true, &angle, body_size, styles, span)?; draw_cancel_line(ctx, length, stroke, true, angle, body_size, styles, span)?;
body.push_frame(center, second_line); body.push_frame(center, second_line);
} }

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, GlyphFragment, MathContext, 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);
@ -124,7 +124,7 @@ fn layout_frac_like(
FrameItem::Shape( FrameItem::Shape(
Geometry::Line(Point::with_x(line_width)).stroked( Geometry::Line(Point::with_x(line_width)).stroked(
FixedStroke::from_pair( FixedStroke::from_pair(
TextElem::fill_in(styles).as_decoration(), styles.get_ref(TextElem::fill).as_decoration(),
thickness, thickness,
), ),
), ),

View File

@ -2,18 +2,18 @@ use std::fmt::{self, Debug, Formatter};
use az::SaturatingAs; use az::SaturatingAs;
use rustybuzz::{BufferFlags, UnicodeBuffer}; use rustybuzz::{BufferFlags, UnicodeBuffer};
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
use typst_library::diag::{bail, warning, SourceResult}; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use typst_library::diag::{SourceResult, bail, warning};
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag; use typst_library::introspection::Tag;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
}; };
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem}; use typst_library::text::{Font, Glyph, TextElem, TextItem, features, language};
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{default_math_class, Get}; use typst_utils::{Get, default_math_class};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::MathContext; use super::MathContext;
@ -215,7 +215,7 @@ impl MathFragment {
&glyph.item.font, &glyph.item.font,
GlyphId(glyph.item.glyphs[glyph_index].id), GlyphId(glyph.item.glyphs[glyph_index].id),
corner, corner,
Em::from_length(height, glyph.item.size), Em::from_abs(height, glyph.item.size),
) )
.unwrap_or_default() .unwrap_or_default()
.at(glyph.item.size) .at(glyph.item.size)
@ -315,7 +315,8 @@ impl GlyphFragment {
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);
let class = EquationElem::class_in(styles) let class = styles
.get(EquationElem::class)
.or_else(|| default_math_class(c)) .or_else(|| default_math_class(c))
.unwrap_or(MathClass::Normal); .unwrap_or(MathClass::Normal);
@ -331,11 +332,11 @@ impl GlyphFragment {
let item = TextItem { let item = TextItem {
font: font.clone(), font: font.clone(),
size: TextElem::size_in(styles), size: styles.resolve(TextElem::size),
fill: TextElem::fill_in(styles).as_decoration(), fill: styles.get_ref(TextElem::fill).as_decoration(),
stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), stroke: styles.resolve(TextElem::stroke).map(|s| s.unwrap_or_default()),
lang: TextElem::lang_in(styles), lang: styles.get(TextElem::lang),
region: TextElem::region_in(styles), region: styles.get(TextElem::region),
text: text.into(), text: text.into(),
glyphs: vec![glyph.clone()], glyphs: vec![glyph.clone()],
}; };
@ -344,7 +345,7 @@ impl GlyphFragment {
item, item,
base_glyph: glyph, base_glyph: glyph,
// Math // Math
math_size: EquationElem::size_in(styles), math_size: styles.get(EquationElem::size),
class, class,
limits, limits,
mid_stretched: None, mid_stretched: None,
@ -356,7 +357,7 @@ impl GlyphFragment {
baseline: None, baseline: None,
// Misc // Misc
align: Abs::zero(), align: Abs::zero(),
shift: TextElem::baseline_in(styles), shift: styles.resolve(TextElem::baseline),
modifiers: FrameModifiers::get_in(styles), modifiers: FrameModifiers::get_in(styles),
}; };
fragment.update_glyph(); fragment.update_glyph();
@ -541,9 +542,9 @@ impl FrameFragment {
let accent_attach = frame.width() / 2.0; let accent_attach = frame.width() / 2.0;
Self { Self {
frame: frame.modified(&FrameModifiers::get_in(styles)), frame: frame.modified(&FrameModifiers::get_in(styles)),
font_size: TextElem::size_in(styles), font_size: styles.resolve(TextElem::size),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), class: styles.get(EquationElem::class).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles), math_size: styles.get(EquationElem::size),
limits: Limits::Never, limits: Limits::Never,
spaced: false, spaced: false,
base_ascent, base_ascent,
@ -680,7 +681,11 @@ fn min_connector_overlap(font: &Font) -> Option<Em> {
.map(|variants| font.to_em(variants.min_connector_overlap)) .map(|variants| font.to_em(variants.min_connector_overlap))
} }
fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option<GlyphConstruction> { fn glyph_construction(
font: &Font,
id: GlyphId,
axis: Axis,
) -> Option<GlyphConstruction<'_>> {
font.ttf() font.ttf()
.tables() .tables()
.math? .math?
@ -767,8 +772,8 @@ fn assemble(
advance += ratio * (max_overlap - min_overlap); advance += ratio * (max_overlap - min_overlap);
} }
let (x, y) = match axis { let (x, y) = match axis {
Axis::X => (Em::from_length(advance, base.item.size), Em::zero()), Axis::X => (Em::from_abs(advance, base.item.size), Em::zero()),
Axis::Y => (Em::zero(), Em::from_length(advance, base.item.size)), Axis::Y => (Em::zero(), Em::from_abs(advance, base.item.size)),
}; };
glyphs.push(Glyph { glyphs.push(Glyph {
id: part.glyph_id.0, id: part.glyph_id.0,
@ -809,7 +814,10 @@ fn assemble(
/// Return an iterator over the assembly's parts with extenders repeated the /// Return an iterator over the assembly's parts with extenders repeated the
/// specified number of times. /// specified number of times.
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { fn parts(
assembly: GlyphAssembly<'_>,
repeat: usize,
) -> impl Iterator<Item = GlyphPart> + '_ {
assembly.parts.into_iter().flat_map(move |part| { assembly.parts.into_iter().flat_map(move |part| {
let count = if part.part_flags.extender() { repeat } else { 1 }; let count = if part.part_flags.extender() { repeat } else { 1 };
std::iter::repeat_n(part, count) std::iter::repeat_n(part, count)
@ -864,7 +872,7 @@ impl Limits {
pub fn active(&self, styles: StyleChain) -> bool { pub fn active(&self, styles: StyleChain) -> bool {
match self { match self {
Self::Always => true, Self::Always => true,
Self::Display => EquationElem::size_in(styles) == MathSize::Display, Self::Display => styles.get(EquationElem::size) == MathSize::Display,
Self::Never => false, Self::Never => false,
} }
} }

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(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)?;
@ -41,7 +41,7 @@ pub fn layout_lr(
.unwrap_or_default(); .unwrap_or_default();
let relative_to = 2.0 * max_extent; let relative_to = 2.0 * max_extent;
let height = elem.size(styles); let height = elem.size.resolve(styles);
// Scale up fragments at both ends. // Scale up fragments at both ends.
match inner_fragments { match inner_fragments {
@ -55,11 +55,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 +95,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,4 +1,4 @@
use typst_library::diag::{bail, warning, SourceResult}; use typst_library::diag::{SourceResult, bail, warning};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, 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, style_for_denominator,
}; };
const VERTICAL_PADDING: Ratio = Ratio::new(0.1); const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -30,15 +30,15 @@ pub fn layout_vec(
ctx, ctx,
styles, styles,
&[column], &[column],
elem.align(styles), elem.align.resolve(styles),
LeftRightAlternator::Right, LeftRightAlternator::Right,
None, None,
Axes::with_y(elem.gap(styles)), Axes::with_y(elem.gap.resolve(styles)),
span, span,
"elements", "elements",
)?; )?;
let delim = elem.delim(styles); let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
} }
@ -59,14 +59,17 @@ pub fn layout_cases(
FixedAlignment::Start, FixedAlignment::Start,
LeftRightAlternator::None, LeftRightAlternator::None,
None, None,
Axes::with_y(elem.gap(styles)), Axes::with_y(elem.gap.resolve(styles)),
span, span,
"branches", "branches",
)?; )?;
let delim = elem.delim(styles); let delim = elem.delim.get(styles);
let (open, close) = let (open, close) = if elem.reverse.get(styles) {
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; (None, delim.close())
} else {
(delim.open(), None)
};
layout_delimiters(ctx, styles, frame, open, close, span) layout_delimiters(ctx, styles, frame, open, close, span)
} }
@ -81,7 +84,7 @@ pub fn layout_mat(
let rows = &elem.rows; let rows = &elem.rows;
let ncols = rows.first().map_or(0, |row| row.len()); let ncols = rows.first().map_or(0, |row| row.len());
let augment = elem.augment(styles); let augment = elem.augment.resolve(styles);
if let Some(aug) = &augment { if let Some(aug) = &augment {
for &offset in &aug.hline.0 { for &offset in &aug.hline.0 {
if offset == 0 || offset.unsigned_abs() >= rows.len() { if offset == 0 || offset.unsigned_abs() >= rows.len() {
@ -116,15 +119,15 @@ pub fn layout_mat(
ctx, ctx,
styles, styles,
&columns, &columns,
elem.align(styles), elem.align.resolve(styles),
LeftRightAlternator::Right, LeftRightAlternator::Right,
augment, augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)), Axes::new(elem.column_gap.resolve(styles), elem.row_gap.resolve(styles)),
span, span,
"cells", "cells",
)?; )?;
let delim = elem.delim(styles); let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
} }
@ -157,7 +160,7 @@ fn layout_body(
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles); let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles);
let default_stroke = FixedStroke { let default_stroke = FixedStroke {
thickness: default_stroke_thickness, thickness: default_stroke_thickness,
paint: TextElem::fill_in(styles).as_decoration(), paint: styles.get_ref(TextElem::fill).as_decoration(),
cap: LineCap::Square, cap: LineCap::Square,
..Default::default() ..Default::default()
}; };

View File

@ -13,7 +13,8 @@ mod stretch;
mod text; mod text;
mod underover; mod underover;
use typst_library::diag::{bail, SourceResult}; use typst_library::World;
use typst_library::diag::{SourceResult, bail};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{ use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
@ -28,15 +29,14 @@ use typst_library::math::*;
use typst_library::model::ParElem; use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind}; use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{ use typst_library::text::{
families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, families, variant,
}; };
use typst_library::World;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::Numeric; use typst_utils::Numeric;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use self::fragment::{ use self::fragment::{
has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment, FrameFragment, GlyphFragment, Limits, MathFragment, has_dtls_feat, stretch_axes,
}; };
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder}; use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
use self::shared::*; use self::shared::*;
@ -51,7 +51,7 @@ pub fn layout_equation_inline(
styles: StyleChain, styles: StyleChain,
region: Size, region: Size,
) -> SourceResult<Vec<InlineItem>> { ) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block(styles)); assert!(!elem.block.get(styles));
let font = find_math_font(engine, styles, elem.span())?; let font = find_math_font(engine, styles, elem.span())?;
@ -78,12 +78,12 @@ pub fn layout_equation_inline(
for item in &mut items { for item in &mut items {
let InlineItem::Frame(frame) = item else { continue }; let InlineItem::Frame(frame) = item else { continue };
let slack = ParElem::leading_in(styles) * 0.7; let slack = styles.resolve(ParElem::leading) * 0.7;
let (t, b) = font.edges( let (t, b) = font.edges(
TextElem::top_edge_in(styles), styles.get(TextElem::top_edge),
TextElem::bottom_edge_in(styles), styles.get(TextElem::bottom_edge),
TextElem::size_in(styles), styles.resolve(TextElem::size),
TextEdgeBounds::Frame(frame), TextEdgeBounds::Frame(frame),
); );
@ -105,7 +105,7 @@ pub fn layout_equation_block(
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
assert!(elem.block(styles)); assert!(elem.block.get(styles));
let span = elem.span(); let span = elem.span();
let font = find_math_font(engine, styles, span)?; let font = find_math_font(engine, styles, span)?;
@ -121,7 +121,7 @@ pub fn layout_equation_block(
.multiline_frame_builder(styles); .multiline_frame_builder(styles);
let width = full_equation_builder.size.x; let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) { let equation_builders = if styles.get(BlockElem::breakable) {
let mut rows = full_equation_builder.frames.into_iter().peekable(); let mut rows = full_equation_builder.frames.into_iter().peekable();
let mut equation_builders = vec![]; let mut equation_builders = vec![];
let mut last_first_pos = Point::zero(); let mut last_first_pos = Point::zero();
@ -188,7 +188,7 @@ pub fn layout_equation_block(
vec![full_equation_builder] vec![full_equation_builder]
}; };
let Some(numbering) = (**elem).numbering(styles) else { let Some(numbering) = elem.numbering.get_ref(styles) else {
let frames = equation_builders let frames = equation_builders
.into_iter() .into_iter()
.map(MathRunFrameBuilder::build) .map(MathRunFrameBuilder::build)
@ -197,7 +197,7 @@ pub fn layout_equation_block(
}; };
let pod = Region::new(regions.base(), Axes::splat(false)); let pod = Region::new(regions.base(), Axes::splat(false));
let counter = Counter::of(EquationElem::elem()) let counter = Counter::of(EquationElem::ELEM)
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span); .spanned(span);
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?; let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
@ -205,7 +205,7 @@ pub fn layout_equation_block(
static NUMBER_GUTTER: Em = Em::new(0.5); static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
let number_align = match elem.number_align(styles) { let number_align = match elem.number_align.get(styles) {
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon), SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v), SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v), SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
@ -224,7 +224,7 @@ pub fn layout_equation_block(
builder, builder,
number.clone(), number.clone(),
number_align.resolve(styles), number_align.resolve(styles),
AlignElem::alignment_in(styles).resolve(styles).x, styles.get(AlignElem::alignment).resolve(styles).x,
regions.size.x, regions.size.x,
full_number_width, full_number_width,
) )
@ -472,7 +472,9 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
let outer = styles; let outer = styles;
for (elem, styles) in pairs { for (elem, styles) in pairs {
// Hack because the font is fixed in math. // Hack because the font is fixed in math.
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { if styles != outer
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
{
let frame = layout_external(elem, self, styles)?; let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true)); self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue; continue;
@ -601,10 +603,10 @@ fn layout_h(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount { if let Spacing::Rel(rel) = elem.amount
if rel.rel.is_zero() { && rel.rel.is_zero()
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); {
} ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak.get(styles)));
} }
Ok(()) Ok(())
} }
@ -616,7 +618,7 @@ fn layout_class(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let style = EquationElem::set_class(Some(elem.class)).wrap(); let style = EquationElem::class.set(Some(elem.class)).wrap();
let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
fragment.set_class(elem.class); fragment.set_class(elem.class);
fragment.set_limits(Limits::for_class(elem.class)); fragment.set_limits(Limits::for_class(elem.class));
@ -642,7 +644,7 @@ fn layout_op(
.with_italics_correction(italics) .with_italics_correction(italics)
.with_accent_attach(accent_attach) .with_accent_attach(accent_attach)
.with_text_like(text_like) .with_text_like(text_like)
.with_limits(if elem.limits(styles) { .with_limits(if elem.limits.get(styles) {
Limits::Display Limits::Display
} else { } else {
Limits::Never Limits::Never

View File

@ -5,7 +5,7 @@ use typst_library::math::{EquationElem, MathSize, RootElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry}; use typst_library::visualize::{FixedStroke, Geometry};
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext}; use super::{FrameFragment, GlyphFragment, MathContext, style_cramped};
/// Lays out a [`RootElem`]. /// Lays out a [`RootElem`].
/// ///
@ -17,7 +17,7 @@ pub fn layout_root(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = elem.index(styles); let index = elem.index.get_ref(styles);
let span = elem.span(); let span = elem.span();
let gap = scaled!( let gap = scaled!(
@ -54,7 +54,7 @@ pub fn layout_root(
let sqrt = sqrt.into_frame(); let sqrt = sqrt.into_frame();
// Layout the index. // Layout the index.
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
let index = index let index = index
.as_ref() .as_ref()
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
@ -112,7 +112,7 @@ pub fn layout_root(
FrameItem::Shape( FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked( Geometry::Line(Point::with_x(radicand.width())).stroked(
FixedStroke::from_pair( FixedStroke::from_pair(
TextElem::fill_in(styles).as_decoration(), styles.get_ref(TextElem::fill).as_decoration(),
thickness, thickness,
), ),
), ),

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
} }
@ -194,13 +194,13 @@ impl MathRun {
let row_count = rows.len(); let row_count = rows.len();
let alignments = alignments(&rows); let alignments = alignments(&rows);
let leading = if EquationElem::size_in(styles) >= MathSize::Text { let leading = if styles.get(EquationElem::size) >= MathSize::Text {
ParElem::leading_in(styles) styles.resolve(ParElem::leading)
} else { } else {
TIGHT_LEADING.resolve(styles) TIGHT_LEADING.resolve(styles)
}; };
let align = AlignElem::alignment_in(styles).resolve(styles).x; let align = styles.resolve(AlignElem::alignment).x;
let mut frames: Vec<(Frame, Point)> = vec![]; let mut frames: Vec<(Frame, Point)> = vec![];
let mut size = Size::zero(); let mut size = Size::zero();
for (i, row) in rows.into_iter().enumerate() { for (i, row) in rows.into_iter().enumerate() {
@ -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,5 +1,5 @@
use ttf_parser::math::MathValue;
use ttf_parser::Tag; use ttf_parser::Tag;
use ttf_parser::math::MathValue;
use typst_library::foundations::{Style, StyleChain}; use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
@ -10,7 +10,7 @@ use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
macro_rules! scaled { macro_rules! scaled {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match typst_library::math::EquationElem::size_in($styles) { match $styles.get(typst_library::math::EquationElem::size) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
_ => scaled!($ctx, $styles, $text), _ => scaled!($ctx, $styles, $text),
} }
@ -19,7 +19,7 @@ macro_rules! scaled {
$crate::math::Scaled::scaled( $crate::math::Scaled::scaled(
$ctx.constants.$name(), $ctx.constants.$name(),
$ctx, $ctx,
typst_library::text::TextElem::size_in($styles), $styles.resolve(typst_library::text::TextElem::size),
) )
}; };
} }
@ -58,55 +58,62 @@ impl Scaled for MathValue<'_> {
/// Styles something as cramped. /// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> { pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap() EquationElem::cramped.set(true).wrap()
} }
/// Sets flac OpenType feature. /// Sets flac OpenType feature.
pub fn style_flac() -> LazyHash<Style> { pub fn style_flac() -> LazyHash<Style> {
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)])).wrap() TextElem::features
.set(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)]))
.wrap()
} }
/// Sets dtls OpenType feature. /// Sets dtls OpenType feature.
pub fn style_dtls() -> LazyHash<Style> { pub fn style_dtls() -> LazyHash<Style> {
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)])).wrap() TextElem::features
.set(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)]))
.wrap()
} }
/// The style for subscripts in the current style. /// The style for subscripts in the current style.
pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] { pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_superscript(styles), EquationElem::set_cramped(true).wrap()] [style_for_superscript(styles), EquationElem::cramped.set(true).wrap()]
} }
/// The style for superscripts in the current style. /// The style for superscripts in the current style.
pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> { pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) { EquationElem::size
MathSize::Display | MathSize::Text => MathSize::Script, .set(match styles.get(EquationElem::size) {
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, MathSize::Display | MathSize::Text => MathSize::Script,
}) MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
.wrap() })
.wrap()
} }
/// The style for numerators in the current style. /// The style for numerators in the current style.
pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> { pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) { EquationElem::size
MathSize::Display => MathSize::Text, .set(match styles.get(EquationElem::size) {
MathSize::Text => MathSize::Script, MathSize::Display => MathSize::Text,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, MathSize::Text => MathSize::Script,
}) MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
.wrap() })
.wrap()
} }
/// The style for denominators in the current style. /// The style for denominators in the current style.
pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] { pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_numerator(styles), EquationElem::set_cramped(true).wrap()] [style_for_numerator(styles), EquationElem::cramped.set(true).wrap()]
} }
/// 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(ctx: &MathContext) -> LazyHash<Style> {
EquationElem::set_script_scale(( EquationElem::script_scale
ctx.constants.script_percent_scale_down(), .set((
ctx.constants.script_script_percent_scale_down(), ctx.constants.script_percent_scale_down(),
)) ctx.constants.script_script_percent_scale_down(),
.wrap() ))
.wrap()
} }
/// Stack rows on top of each other. /// Stack rows on top of each other.

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

View File

@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun, stack,
GlyphFragment, LeftRightAlternator, MathContext, MathRun, style_cramped, style_for_subscript, style_for_superscript,
}; };
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);
@ -56,7 +56,7 @@ pub fn layout_underbrace(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⏟', '⏟',
BRACE_GAP, BRACE_GAP,
Position::Under, Position::Under,
@ -75,7 +75,7 @@ pub fn layout_overbrace(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⏞', '⏞',
BRACE_GAP, BRACE_GAP,
Position::Over, Position::Over,
@ -94,7 +94,7 @@ pub fn layout_underbracket(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⎵', '⎵',
BRACKET_GAP, BRACKET_GAP,
Position::Under, Position::Under,
@ -113,7 +113,7 @@ pub fn layout_overbracket(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⎴', '⎴',
BRACKET_GAP, BRACKET_GAP,
Position::Over, Position::Over,
@ -132,7 +132,7 @@ pub fn layout_underparen(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⏝', '⏝',
PAREN_GAP, PAREN_GAP,
Position::Under, Position::Under,
@ -151,7 +151,7 @@ pub fn layout_overparen(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⏜', '⏜',
PAREN_GAP, PAREN_GAP,
Position::Over, Position::Over,
@ -170,7 +170,7 @@ pub fn layout_undershell(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⏡', '⏡',
SHELL_GAP, SHELL_GAP,
Position::Under, Position::Under,
@ -189,7 +189,7 @@ pub fn layout_overshell(
ctx, ctx,
styles, styles,
&elem.body, &elem.body,
&elem.annotation(styles), elem.annotation.get_ref(styles),
'⏠', '⏠',
SHELL_GAP, SHELL_GAP,
Position::Over, Position::Over,
@ -251,7 +251,7 @@ fn layout_underoverline(
line_pos, line_pos,
FrameItem::Shape( FrameItem::Shape(
Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke { Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(), paint: styles.get_ref(TextElem::fill).as_decoration(),
thickness: bar_height, thickness: bar_height,
..FixedStroke::default() ..FixedStroke::default()
}), }),

View File

@ -29,8 +29,8 @@ impl FrameModifiers {
/// Retrieve all modifications that should be applied per-frame. /// Retrieve all modifications that should be applied per-frame.
pub fn get_in(styles: StyleChain) -> Self { pub fn get_in(styles: StyleChain) -> Self {
Self { Self {
dest: LinkElem::current_in(styles), dest: styles.get_cloned(LinkElem::current),
hidden: HideElem::hidden_in(styles), hidden: styles.get(HideElem::hidden),
} }
} }
} }
@ -83,7 +83,7 @@ pub trait FrameModifyText {
impl FrameModifyText for Frame { impl FrameModifyText for Frame {
fn modify_text(&mut self, styles: StyleChain) { fn modify_text(&mut self, styles: StyleChain) {
let modifiers = FrameModifiers::get_in(styles); let modifiers = FrameModifiers::get_in(styles);
let expand_y = 0.5 * ParElem::leading_in(styles); let expand_y = 0.5 * styles.resolve(ParElem::leading);
let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y); let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y);
modify_frame(self, &modifiers, Some(outset)); modify_frame(self, &modifiers, Some(outset));
} }
@ -130,7 +130,7 @@ where
let outer = styles; let outer = styles;
let mut styles = styles; let mut styles = styles;
if modifiers.dest.is_some() { if modifiers.dest.is_some() {
reset = LinkElem::set_current(None).wrap(); reset = LinkElem::current.set(None).wrap();
styles = outer.chain(&reset); styles = outer.chain(&reset);
} }

View File

@ -1,6 +1,6 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Resolve, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::{
Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size, Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size,
@ -16,10 +16,10 @@ pub fn layout_pad(
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let padding = Sides::new( let padding = Sides::new(
elem.left(styles).resolve(styles), elem.left.resolve(styles),
elem.top(styles).resolve(styles), elem.top.resolve(styles),
elem.right(styles).resolve(styles), elem.right.resolve(styles),
elem.bottom(styles).resolve(styles), elem.bottom.resolve(styles),
); );
let mut backlog = vec![]; let mut backlog = vec![];

View File

@ -39,14 +39,14 @@ pub fn collect<'a>(
if let Some(pagebreak) = elem.to_packed::<PagebreakElem>() { if let Some(pagebreak) = elem.to_packed::<PagebreakElem>() {
// Add a blank page if we encounter a strong pagebreak and there was // Add a blank page if we encounter a strong pagebreak and there was
// a staged empty page. // a staged empty page.
let strong = !pagebreak.weak(styles); let strong = !pagebreak.weak.get(styles);
if strong && staged_empty_page { if strong && staged_empty_page {
let locator = locator.next(&elem.span()); let locator = locator.next(&elem.span());
items.push(Item::Run(&[], initial, locator)); items.push(Item::Run(&[], initial, locator));
} }
// Add an instruction to adjust the page parity if requested. // Add an instruction to adjust the page parity if requested.
if let Some(parity) = pagebreak.to(styles) { if let Some(parity) = pagebreak.to.get(styles) {
let locator = locator.next(&elem.span()); let locator = locator.next(&elem.span());
items.push(Item::Parity(parity, styles, locator)); items.push(Item::Parity(parity, styles, locator));
} }
@ -56,7 +56,7 @@ pub fn collect<'a>(
// the scope of a page set rule to ensure a page boundary. Its // the scope of a page set rule to ensure a page boundary. Its
// styles correspond to the styles _before_ the page set rule, so we // styles correspond to the styles _before_ the page set rule, so we
// don't want to apply it to a potential empty page. // don't want to apply it to a potential empty page.
if !pagebreak.boundary(styles) { if !pagebreak.boundary.get(styles) {
initial = styles; initial = styles;
} }
@ -94,7 +94,7 @@ pub fn collect<'a>(
if group.iter().all(|(c, _)| c.is::<TagElem>()) if group.iter().all(|(c, _)| c.is::<TagElem>())
&& !(staged_empty_page && !(staged_empty_page
&& children.iter().all(|&(c, s)| { && children.iter().all(|&(c, s)| {
c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary(s)) c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary.get(s))
})) }))
{ {
items.push(Item::Tags(group)); items.push(Item::Tags(group));

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
@ -101,10 +101,10 @@ fn layout_page_run_impl(
// When one of the lengths is infinite the page fits its content along // When one of the lengths is infinite the page fits its content along
// that axis. // that axis.
let width = PageElem::width_in(styles).unwrap_or(Abs::inf()); let width = styles.resolve(PageElem::width).unwrap_or(Abs::inf());
let height = PageElem::height_in(styles).unwrap_or(Abs::inf()); let height = styles.resolve(PageElem::height).unwrap_or(Abs::inf());
let mut size = Size::new(width, height); let mut size = Size::new(width, height);
if PageElem::flipped_in(styles) { if styles.get(PageElem::flipped) {
std::mem::swap(&mut size.x, &mut size.y); std::mem::swap(&mut size.x, &mut size.y);
} }
@ -115,7 +115,7 @@ fn layout_page_run_impl(
// Determine the margins. // Determine the margins.
let default = Rel::<Length>::from((2.5 / 21.0) * min); let default = Rel::<Length>::from((2.5 / 21.0) * min);
let margin = PageElem::margin_in(styles); let margin = styles.get(PageElem::margin);
let two_sided = margin.two_sided.unwrap_or(false); let two_sided = margin.two_sided.unwrap_or(false);
let margin = margin let margin = margin
.sides .sides
@ -123,22 +123,24 @@ fn layout_page_run_impl(
.resolve(styles) .resolve(styles)
.relative_to(size); .relative_to(size);
let fill = PageElem::fill_in(styles); let fill = styles.get_cloned(PageElem::fill);
let foreground = PageElem::foreground_in(styles); let foreground = styles.get_ref(PageElem::foreground);
let background = PageElem::background_in(styles); let background = styles.get_ref(PageElem::background);
let header_ascent = PageElem::header_ascent_in(styles).relative_to(margin.top); let header_ascent = styles.resolve(PageElem::header_ascent).relative_to(margin.top);
let footer_descent = PageElem::footer_descent_in(styles).relative_to(margin.bottom); let footer_descent =
let numbering = PageElem::numbering_in(styles); styles.resolve(PageElem::footer_descent).relative_to(margin.bottom);
let supplement = match PageElem::supplement_in(styles) { let numbering = styles.get_ref(PageElem::numbering);
let supplement = match styles.get_cloned(PageElem::supplement) {
Smart::Auto => TextElem::packed(PageElem::local_name_in(styles)), Smart::Auto => TextElem::packed(PageElem::local_name_in(styles)),
Smart::Custom(content) => content.unwrap_or_default(), Smart::Custom(content) => content.unwrap_or_default(),
}; };
let number_align = PageElem::number_align_in(styles); let number_align = styles.get(PageElem::number_align);
let binding = let binding = styles.get(PageElem::binding).unwrap_or_else(|| {
PageElem::binding_in(styles).unwrap_or_else(|| match TextElem::dir_in(styles) { match styles.resolve(TextElem::dir) {
Dir::LTR => Binding::Left, Dir::LTR => Binding::Left,
_ => Binding::Right, _ => Binding::Right,
}); }
});
// Construct the numbering (for header or footer). // Construct the numbering (for header or footer).
let numbering_marginal = numbering.as_ref().map(|numbering| { let numbering_marginal = numbering.as_ref().map(|numbering| {
@ -163,8 +165,8 @@ fn layout_page_run_impl(
counter counter
}); });
let header = PageElem::header_in(styles); let header = styles.get_ref(PageElem::header);
let footer = PageElem::footer_in(styles); let footer = styles.get_ref(PageElem::footer);
let (header, footer) = if matches!(number_align.y(), Some(OuterVAlignment::Top)) { let (header, footer) = if matches!(number_align.y(), Some(OuterVAlignment::Top)) {
(header.as_ref().unwrap_or(&numbering_marginal), footer.as_ref().unwrap_or(&None)) (header.as_ref().unwrap_or(&numbering_marginal), footer.as_ref().unwrap_or(&None))
} else { } else {
@ -179,15 +181,15 @@ fn layout_page_run_impl(
&mut locator, &mut locator,
styles, styles,
Regions::repeat(area, area.map(Abs::is_finite)), Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles), styles.get(PageElem::columns),
ColumnsElem::gutter_in(styles), styles.get(ColumnsElem::gutter).resolve(styles),
FlowMode::Root, FlowMode::Root,
)?; )?;
// Layouts a single marginal. // Layouts a single marginal.
let mut layout_marginal = |content: &Option<Content>, area, align| { let mut layout_marginal = |content: &Option<Content>, area, align| {
let Some(content) = content else { return Ok(None) }; let Some(content) = content else { return Ok(None) };
let aligned = content.clone().styled(AlignElem::set_alignment(align)); let aligned = content.clone().set(AlignElem::alignment, align);
crate::layout_frame( crate::layout_frame(
&mut engine, &mut engine,
&aligned, &aligned,

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;
@ -29,7 +29,7 @@ pub fn layout_repeat(
frame.set_baseline(piece.baseline()); frame.set_baseline(piece.baseline());
} }
let mut gap = elem.gap(styles).resolve(styles); let mut gap = elem.gap.resolve(styles);
let fill = region.size.x; let fill = region.size.x;
let width = piece.width(); let width = piece.width();
@ -47,12 +47,12 @@ pub fn layout_repeat(
let count = ((fill + gap) / (width + gap)).floor(); let count = ((fill + gap) / (width + gap)).floor();
let remaining = (fill + gap) % (width + gap); let remaining = (fill + gap) % (width + gap);
let justify = elem.justify(styles); let justify = elem.justify.get(styles);
if justify { if justify {
gap += remaining / (count - 1.0); gap += remaining / (count - 1.0);
} }
let align = AlignElem::alignment_in(styles).resolve(styles); let align = styles.get(AlignElem::alignment).resolve(styles);
let mut offset = Abs::zero(); let mut offset = Abs::zero();
if count == 1.0 || !justify { if count == 1.0 || !justify {
offset += align.x.position(remaining); offset += align.x.position(remaining);

View File

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

View File

@ -1,7 +1,7 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use kurbo::{CubicBez, ParamCurveExtrema}; use kurbo::{CubicBez, ParamCurveExtrema};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{SourceResult, bail};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
@ -11,8 +11,8 @@ use typst_library::layout::{
}; };
use typst_library::visualize::{ use typst_library::visualize::{
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule, CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem, FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
Shape, SquareElem, Stroke, RectElem, Shape, SquareElem, Stroke,
}; };
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, Numeric}; use typst_utils::{Get, Numeric};
@ -27,16 +27,20 @@ pub fn layout_line(
region: Region, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to); let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
let start = resolve(elem.start(styles)); let start = resolve(elem.start.resolve(styles));
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { let delta = elem
let length = elem.length(styles); .end
let angle = elem.angle(styles); .resolve(styles)
let x = angle.cos() * length; .map(|end| resolve(end) - start)
let y = angle.sin() * length; .unwrap_or_else(|| {
resolve(Axes::new(x, y)) let length = elem.length.resolve(styles);
}); let angle = elem.angle.get(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
let stroke = elem.stroke(styles).unwrap_or_default(); let stroke = elem.stroke.resolve(styles).unwrap_or_default();
let size = start.max(start + delta).max(Size::zero()); let size = start.max(start + delta).max(Size::zero());
if !size.is_finite() { if !size.is_finite() {
@ -105,7 +109,7 @@ pub fn layout_path(
add_cubic(from_point, to_point, from, to); add_cubic(from_point, to_point, from, to);
} }
if elem.closed(styles) { if elem.closed.get(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element. let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0]; let to = vertices[0];
let from_point = *points.last().unwrap(); let from_point = *points.last().unwrap();
@ -120,9 +124,9 @@ pub fn layout_path(
} }
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule(styles); let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke(styles) { let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None, Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -153,19 +157,19 @@ pub fn layout_curve(
for item in &elem.components { for item in &elem.components {
match item { match item {
CurveComponent::Move(element) => { CurveComponent::Move(element) => {
let relative = element.relative(styles); let relative = element.relative.get(styles);
let point = builder.resolve_point(element.start, relative); let point = builder.resolve_point(element.start, relative);
builder.move_(point); builder.move_(point);
} }
CurveComponent::Line(element) => { CurveComponent::Line(element) => {
let relative = element.relative(styles); let relative = element.relative.get(styles);
let point = builder.resolve_point(element.end, relative); let point = builder.resolve_point(element.end, relative);
builder.line(point); builder.line(point);
} }
CurveComponent::Quad(element) => { CurveComponent::Quad(element) => {
let relative = element.relative(styles); let relative = element.relative.get(styles);
let end = builder.resolve_point(element.end, relative); let end = builder.resolve_point(element.end, relative);
let control = match element.control { let control = match element.control {
Smart::Auto => { Smart::Auto => {
@ -178,7 +182,7 @@ pub fn layout_curve(
} }
CurveComponent::Cubic(element) => { CurveComponent::Cubic(element) => {
let relative = element.relative(styles); let relative = element.relative.get(styles);
let end = builder.resolve_point(element.end, relative); let end = builder.resolve_point(element.end, relative);
let c1 = match element.control_start { let c1 = match element.control_start {
Some(Smart::Custom(p)) => builder.resolve_point(p, relative), Some(Smart::Custom(p)) => builder.resolve_point(p, relative),
@ -193,7 +197,7 @@ pub fn layout_curve(
} }
CurveComponent::Close(element) => { CurveComponent::Close(element) => {
builder.close(element.mode(styles)); builder.close(element.mode.get(styles));
} }
} }
} }
@ -208,9 +212,9 @@ pub fn layout_curve(
} }
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule(styles); let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke(styles) { let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None, Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -418,9 +422,9 @@ pub fn layout_polygon(
} }
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule(styles); let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke(styles) { let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()), Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None, Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -459,12 +463,12 @@ pub fn layout_rect(
styles, styles,
region, region,
ShapeKind::Rect, ShapeKind::Rect,
elem.body(styles), elem.body.get_ref(styles),
elem.fill(styles), elem.fill.get_cloned(styles),
elem.stroke(styles), elem.stroke.resolve(styles),
elem.inset(styles), elem.inset.resolve(styles),
elem.outset(styles), elem.outset.resolve(styles),
elem.radius(styles), elem.radius.resolve(styles),
elem.span(), elem.span(),
) )
} }
@ -484,12 +488,12 @@ pub fn layout_square(
styles, styles,
region, region,
ShapeKind::Square, ShapeKind::Square,
elem.body(styles), elem.body.get_ref(styles),
elem.fill(styles), elem.fill.get_cloned(styles),
elem.stroke(styles), elem.stroke.resolve(styles),
elem.inset(styles), elem.inset.resolve(styles),
elem.outset(styles), elem.outset.resolve(styles),
elem.radius(styles), elem.radius.resolve(styles),
elem.span(), elem.span(),
) )
} }
@ -509,11 +513,11 @@ pub fn layout_ellipse(
styles, styles,
region, region,
ShapeKind::Ellipse, ShapeKind::Ellipse,
elem.body(styles), elem.body.get_ref(styles),
elem.fill(styles), elem.fill.get_cloned(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))), elem.stroke.resolve(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles), elem.inset.resolve(styles),
elem.outset(styles), elem.outset.resolve(styles),
Corners::splat(None), Corners::splat(None),
elem.span(), elem.span(),
) )
@ -534,11 +538,11 @@ pub fn layout_circle(
styles, styles,
region, region,
ShapeKind::Circle, ShapeKind::Circle,
elem.body(styles), elem.body.get_ref(styles),
elem.fill(styles), elem.fill.get_cloned(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))), elem.stroke.resolve(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles), elem.inset.resolve(styles),
elem.outset(styles), elem.outset.resolve(styles),
Corners::splat(None), Corners::splat(None),
elem.span(), elem.span(),
) )
@ -889,7 +893,13 @@ fn segmented_rect(
let end = current; let end = current;
last = current; last = current;
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue }; let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
let (shape, ontop) = segment(start, end, &corners, stroke); let start_cap = stroke.cap;
let end_cap = match strokes.get_ref(end.side_ccw()) {
Some(stroke) => stroke.cap,
None => start_cap,
};
let (shape, ontop) =
segment(start, end, start_cap, end_cap, &corners, stroke);
if ontop { if ontop {
res.push(shape); res.push(shape);
} else { } else {
@ -899,7 +909,14 @@ fn segmented_rect(
} }
} else if let Some(stroke) = &strokes.top { } else if let Some(stroke) = &strokes.top {
// single segment // single segment
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); let (shape, _) = segment(
Corner::TopLeft,
Corner::TopLeft,
stroke.cap,
stroke.cap,
&corners,
stroke,
);
res.push(shape); res.push(shape);
} }
res res
@ -946,6 +963,8 @@ fn curve_segment(
fn segment( fn segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: &FixedStroke, stroke: &FixedStroke,
) -> (Shape, bool) { ) -> (Shape, bool) {
@ -979,7 +998,7 @@ fn segment(
let use_fill = solid && fill_corners(start, end, corners); let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill { let shape = if use_fill {
fill_segment(start, end, corners, stroke) fill_segment(start, end, start_cap, end_cap, corners, stroke)
} else { } else {
stroke_segment(start, end, corners, stroke.clone()) stroke_segment(start, end, corners, stroke.clone())
}; };
@ -1010,6 +1029,8 @@ fn stroke_segment(
fn fill_segment( fn fill_segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: &FixedStroke, stroke: &FixedStroke,
) -> Shape { ) -> Shape {
@ -1035,8 +1056,7 @@ fn fill_segment(
if c.arc_outer() { if c.arc_outer() {
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
} else { } else {
curve.line(c.outer()); c.start_cap(&mut curve, start_cap);
curve.line(c.end_outer());
} }
} }
@ -1079,7 +1099,7 @@ fn fill_segment(
if c.arc_inner() { if c.arc_inner() {
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
} else { } else {
curve.line(c.center_inner()); c.end_cap(&mut curve, end_cap);
} }
} }
@ -1134,6 +1154,16 @@ struct ControlPoints {
} }
impl ControlPoints { impl ControlPoints {
/// Rotate point around the origin, relative to the top-left.
fn rotate_centered(&self, point: Point) -> Point {
match self.corner {
Corner::TopLeft => point,
Corner::TopRight => Point { x: -point.y, y: point.x },
Corner::BottomRight => Point { x: -point.x, y: -point.y },
Corner::BottomLeft => Point { x: point.y, y: -point.x },
}
}
/// Move and rotate the point from top-left to the required corner. /// Move and rotate the point from top-left to the required corner.
fn rotate(&self, point: Point) -> Point { fn rotate(&self, point: Point) -> Point {
match self.corner { match self.corner {
@ -1280,6 +1310,77 @@ impl ControlPoints {
y: self.stroke_after, y: self.stroke_after,
}) })
} }
/// Draw the cap at the beginning of the segment.
///
/// If this corner has a stroke before it,
/// a default "butt" cap is used.
///
/// NOTE: doesn't support the case where the corner has a radius.
pub fn start_cap(&self, curve: &mut Curve, cap_type: LineCap) {
if self.stroke_before != Abs::zero()
|| self.radius != Abs::zero()
|| cap_type == LineCap::Butt
{
// Just the default cap.
curve.line(self.outer());
} else if cap_type == LineCap::Square {
// Extend by the stroke width.
let offset =
self.rotate_centered(Point { x: -self.stroke_after, y: Abs::zero() });
curve.line(self.end_inner() + offset);
curve.line(self.outer() + offset);
} else if cap_type == LineCap::Round {
// We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we
// do not want in this case.
curve.arc(
self.end_inner(),
(self.end_inner()
+ self.rotate_centered(Point { x: Abs::raw(1.0), y: Abs::zero() })
+ self.outer())
/ 2.,
self.outer(),
);
}
curve.line(self.end_outer());
}
/// Draw the cap at the end of the segment.
///
/// If this corner has a stroke before it,
/// a default "butt" cap is used.
///
/// NOTE: doesn't support the case where the corner has a radius.
pub fn end_cap(&self, curve: &mut Curve, cap_type: LineCap) {
if self.stroke_after != Abs::zero()
|| self.radius != Abs::zero()
|| cap_type == LineCap::Butt
{
// Just the default cap.
curve.line(self.center_inner());
} else if cap_type == LineCap::Square {
// Extend by the stroke width.
let offset =
self.rotate_centered(Point { x: Abs::zero(), y: -self.stroke_before });
curve.line(self.outer() + offset);
curve.line(self.center_inner() + offset);
} else if cap_type == LineCap::Round {
// We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we
// do not want in this case.
curve.arc(
self.outer(),
(self.outer()
+ self.rotate_centered(Point { x: Abs::zero(), y: Abs::raw(1.0) })
+ self.center_inner())
/ 2.,
self.center_inner(),
);
}
}
} }
/// Helper to draw arcs with Bézier curves. /// Helper to draw arcs with Bézier curves.

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};
@ -19,12 +19,12 @@ pub fn layout_stack(
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let mut layouter = let mut layouter =
StackLayouter::new(elem.span(), elem.dir(styles), locator, styles, regions); StackLayouter::new(elem.span(), elem.dir.get(styles), locator, styles, regions);
let axis = layouter.dir.axis(); let axis = layouter.dir.axis();
// Spacing to insert before the next block. // Spacing to insert before the next block.
let spacing = elem.spacing(styles); let spacing = elem.spacing.get(styles);
let mut deferred = None; let mut deferred = None;
for child in &elem.children { for child in &elem.children {
@ -167,11 +167,11 @@ impl<'a> StackLayouter<'a> {
// Block-axis alignment of the `AlignElem` is respected by stacks. // Block-axis alignment of the `AlignElem` is respected by stacks.
let align = if let Some(align) = block.to_packed::<AlignElem>() { let align = if let Some(align) = block.to_packed::<AlignElem>() {
align.alignment(styles) align.alignment.get(styles)
} else if let Some(styled) = block.to_packed::<StyledElem>() { } else if let Some(styled) = block.to_packed::<StyledElem>() {
AlignElem::alignment_in(styles.chain(&styled.styles)) styles.chain(&styled.styles).get(AlignElem::alignment)
} else { } else {
AlignElem::alignment_in(styles) styles.get(AlignElem::alignment)
} }
.resolve(styles); .resolve(styles);

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