Compare commits

...

98 Commits

Author SHA1 Message Date
mkorje
640cca9613
WIP 2025-07-08 22:58:37 +10: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
Ilia
bd41fb9427
Check that all translation files are added to TRANSLATIONS and ends with newline (#6424)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-12 10:30:53 +00:00
cAttte
46d57b00b5
Warn when using variable fonts (#6425) 2025-06-11 14:42:57 +00:00
Laurenz
ada90fdd87 Fix untidy Cargo.lock 2025-06-11 16:44:13 +02:00
Y.D.X.
19325d5027
Warning when watching stdin (#6381)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-11 14:21:05 +00:00
Lachlan Kermode
1f5846ce24
Render #super as <sup>, #sub as <sub> in HTML (#6422) 2025-06-11 14:07:25 +00:00
T0mstone
d7e0c52dd5
Use codex::ModifierSet (#6159)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-11 13:28:03 +00:00
Tobias Schmitz
d1c7757da8
Fix panic when test source is not found in world (#6428) 2025-06-11 10:19:41 +00:00
Andrew Voynov
71032c8349
List both YAML file extensions in bibliography docs (#6426) 2025-06-11 10:04:10 +00:00
Malo
3a6d5fd6b2
Do not force math.mid elements to have the Large math class (#5980) 2025-06-11 08:29:38 +00:00
Laurenz
98034903e4
Adjust source file API surface (#6423) 2025-06-10 18:13:33 +00:00
Andrew Voynov
a6ab6ac99a
Specify which CSL style is not suitable for bibliographies (#6306)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-10 17:59:06 +00:00
Andrew Voynov
5d34d54df4
Make a more descriptive definition of module (#6380) 2025-06-10 15:34:57 +00:00
3w36zj6
5d13b35458
Standardize trailing slashes in docs route paths (#6420) 2025-06-10 15:34:27 +00:00
Max
44d410dd00
Use the shaper in math (#6336) 2025-06-10 14:44:38 +00:00
PgBiel
7c7b962b98
Table multiple headers and subheaders (#6168) 2025-06-10 14:41:16 +00:00
Tobias Schmitz
a18ca3481d
Report errors in external files (#6308)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-10 12:46:27 +00:00
Ian Wrzesinski
82da96ed95
Improve number lexing (#5969) 2025-06-10 09:11:27 +00:00
Andrew Voynov
e632bffc2e
Document how to escape lr delimiter auto-scaling (#6410)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-09 16:34:39 +00:00
Andrew Voynov
2a3746c51d
Update docs for gradient.repeat (#6385) 2025-06-09 14:25:33 +00:00
cAttte
df4c08f852
Autocomplete fixes for math mode (#6415) 2025-06-09 14:16:47 +00:00
Ian Wrzesinski
832fab58b3
Clean up some parser comments (#6398) 2025-06-09 13:48:55 +00:00
PgBiel
6725061841
Pin colspan and rowspan for blank cells (#6401) 2025-06-09 13:46:29 +00:00
Sam Ireson
494e6a6422
Numbering implementation refactor (#6122) 2025-06-04 13:31:06 +00:00
Max
aee99408e1
Apply short fall consistently in math when stretching (#6377) 2025-06-04 10:14:24 +00:00
Linus Unnebäck
1de2095f67
Add support for WebP images (#6311) 2025-06-04 09:54:03 +00:00
Max
5f776c7372
Bump New CM fonts to version 7.0.2 (#6376) 2025-06-04 09:41:08 +00:00
Max
128c40d839
Apply script-style to numbers consistently in math (#6320) 2025-06-04 08:20:54 +00:00
Nazar Serhiichuk
4a8367e90a
Fix Ukrainian secondary smart quotes (#6372) 2025-06-04 08:13:39 +00:00
Andrew Voynov
f7f163a8fa
Change Russian secondary smart quotes & remove alternatives (#6331) 2025-06-03 14:42:22 +00:00
Max
dd95f7d594
Fix bottom accent positioning in math (#6187) 2025-06-03 14:08:18 +00:00
dependabot[bot]
1b399646c2
Bump crossbeam-channel from 0.5.14 to 0.5.15 (#6369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 12:53:13 +00:00
Kristofers Solo
ff0dc5ab66
Add Latvian translations (#6348) 2025-06-03 12:38:21 +00:00
Laurenz
664d33a681
Be a bit lazier in function call evaluation (#6368) 2025-06-02 16:53:35 +00:00
Laurenz
e023db5f1d
Bump Rust to 1.87 in CI (#6367) 2025-06-02 16:44:43 +00:00
Lachlan Kermode
6164ade9ce
Add typst-html to architecture crates list (#6364) 2025-06-02 14:15:04 +00:00
Andrew Voynov
fd08c4bb3f
Fix typo in layout docs, change "size" to "height" (#6344) 2025-06-02 13:12:42 +00:00
Malo
4329a15a1c
Improve calc.round documentation (#6345) 2025-06-02 13:04:49 +00:00
Andrew Voynov
83e249dd33
Fix Greek numbering docs (#6360) 2025-06-02 13:03:03 +00:00
Malo
61dee554ba
Add an example of show-set place.clearance for figures in the doc (#6208) 2025-06-02 13:02:01 +00:00
450 changed files with 12204 additions and 5931 deletions

View File

@ -40,7 +40,7 @@ jobs:
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386
- uses: dtolnay/rust-toolchain@1.85.0
- uses: dtolnay/rust-toolchain@1.87.0
with:
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
- uses: Swatinem/rust-cache@v2
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0
- uses: dtolnay/rust-toolchain@1.87.0
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
@ -81,6 +81,7 @@ jobs:
- run: cargo clippy --workspace --all-targets --no-default-features
- run: cargo fmt --check --all
- run: cargo doc --workspace --no-deps
- run: git diff --exit-code
min-version:
name: Check minimum Rust version
@ -102,3 +103,15 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo install --locked cargo-fuzz@0.12.0
- 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-2024-10-29
- uses: Swatinem/rust-cache@v2
- run: cargo miri test -p typst-library test_miri

View File

@ -44,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0
- uses: dtolnay/rust-toolchain@1.87.0
with:
target: ${{ matrix.target }}

34
Cargo.lock generated
View File

@ -413,8 +413,7 @@ dependencies = [
[[package]]
name = "codex"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "724d27a0ee38b700e5e164350e79aba601a0db673ac47fce1cb74c3e38864036"
source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928"
[[package]]
name = "color-print"
@ -508,9 +507,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
@ -787,9 +786,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "font-types"
version = "0.8.4"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf"
checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [
"bytemuck",
]
@ -1215,6 +1214,7 @@ dependencies = [
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"num-traits",
"png",
"zune-core",
@ -1367,8 +1367,7 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
dependencies = [
"base64",
"bumpalo",
@ -1396,8 +1395,7 @@ dependencies = [
[[package]]
name = "krilla-svg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
dependencies = [
"flate2",
"fontdb",
@ -2106,9 +2104,9 @@ dependencies = [
[[package]]
name = "read-fonts"
version = "0.28.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288"
checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [
"bytemuck",
"font-types",
@ -2434,9 +2432,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skrifa"
version = "0.30.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8"
checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97"
dependencies = [
"bytemuck",
"read-fonts",
@ -2863,7 +2861,7 @@ dependencies = [
[[package]]
name = "typst-assets"
version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a"
source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a"
[[package]]
name = "typst-cli"
@ -2913,13 +2911,14 @@ dependencies = [
[[package]]
name = "typst-dev-assets"
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=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
[[package]]
name = "typst-docs"
version = "0.13.1"
dependencies = [
"clap",
"codex",
"ecow",
"heck",
"pulldown-cmark",
@ -3038,6 +3037,7 @@ dependencies = [
"icu_provider_blob",
"icu_segmenter",
"kurbo",
"memchr",
"rustybuzz",
"smallvec",
"ttf-parser",
@ -3111,6 +3111,7 @@ dependencies = [
"unicode-segmentation",
"unscanny",
"usvg",
"utf8_iter",
"wasmi",
"xmlwriter",
]
@ -3199,6 +3200,7 @@ dependencies = [
name = "typst-syntax"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
"serde",
"toml",

View File

@ -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-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1"
clap_mangen = "0.2.10"
codespan-reporting = "0.11"
codex = "0.1.1"
codex = { git = "https://github.com/typst/codex", rev = "a5428cb" }
color-print = "0.3.6"
comemo = "0.4"
csv = "1"
@ -69,12 +69,12 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = "0.1.0"
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" }
kurbo = "0.11"
libfuzzer-sys = "0.4"
lipsum = "0.9"
@ -135,6 +135,7 @@ unicode-segmentation = "1"
unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
usvg = { version = "0.45", default-features = false, features = ["text"] }
utf8_iter = "1.0.4"
walkdir = "2"
wasmi = "0.40.0"
web-sys = "0.3"

View File

@ -240,6 +240,26 @@ instant preview. To achieve these goals, we follow three core design principles:
Luckily we have [`comemo`], a system for incremental compilation which does
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/
[app]: https://typst.app/
[discord]: https://discord.gg/2uDybryKPe
@ -259,3 +279,4 @@ instant preview. To achieve these goals, we follow three core design principles:
[packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst
[GitHub sponsors]: https://github.com/sponsors/typst/

View File

@ -491,7 +491,7 @@ pub enum PdfStandard {
/// PDF/A-2u.
#[value(name = "a-2u")]
A_2u,
/// PDF/A-3u.
/// PDF/A-3b.
#[value(name = "a-3b")]
A_3b,
/// PDF/A-3u.

View File

@ -16,7 +16,7 @@ use typst::diag::{
use typst::foundations::{Datetime, Smart};
use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span};
use typst::syntax::{FileId, Lines, Span};
use typst::WorldExt;
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
@ -696,7 +696,7 @@ fn label(world: &SystemWorld, span: Span) -> Option<Label<FileId>> {
impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
type FileId = FileId;
type Name = String;
type Source = Source;
type Source = Lines<String>;
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
let vpath = id.vpath();

View File

@ -5,9 +5,9 @@ use typst::diag::{bail, HintedStrResult, StrResult, Warned};
use typst::engine::Sink;
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::layout::PagedDocument;
use typst::syntax::Span;
use typst::syntax::{Span, SyntaxMode};
use typst::World;
use typst_eval::{eval_string, EvalMode};
use typst_eval::eval_string;
use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics;
@ -63,7 +63,7 @@ fn retrieve(
Sink::new().track_mut(),
&command.selector,
Span::detached(),
EvalMode::Code,
SyntaxMode::Code,
Scope::default(),
)
.map_err(|errors| {

View File

@ -85,6 +85,6 @@ fn resolve_span(world: &SystemWorld, span: Span) -> Option<(String, u32)> {
let id = span.id()?;
let source = world.source(id).ok()?;
let range = source.range(span)?;
let line = source.byte_to_line(range.start)?;
let line = source.lines().byte_to_line(range.start)?;
Some((format!("{id:?}"), line as u32 + 1))
}

View File

@ -10,11 +10,12 @@ use codespan_reporting::term::{self, termcolor};
use ecow::eco_format;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
use same_file::is_same_file;
use typst::diag::{bail, StrResult};
use typst::diag::{bail, warning, StrResult};
use typst::syntax::Span;
use typst::utils::format_duration;
use crate::args::{Input, Output, WatchCommand};
use crate::compile::{compile_once, CompileConfig};
use crate::compile::{compile_once, print_diagnostics, CompileConfig};
use crate::timings::Timer;
use crate::world::{SystemWorld, WorldCreationError};
use crate::{print_error, terminal};
@ -55,6 +56,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Print warning when trying to watch stdin.
if matches!(&config.input, Input::Stdin) {
warn_watching_std(&world, &config)?;
}
// Recompile whenever something relevant happens.
loop {
// Watch all dependencies of the most recent compilation.
@ -332,3 +338,15 @@ impl Status {
}
}
}
/// Emits a warning when trying to watch stdin.
fn warn_watching_std(world: &SystemWorld, config: &CompileConfig) -> StrResult<()> {
let warning = warning!(
Span::detached(),
"cannot watch changes for stdin";
hint: "to recompile on changes, watch a regular file instead";
hint: "to compile once and exit, please use `typst compile` instead"
);
print_diagnostics(world, &[], &[warning], config.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))
}

View File

@ -9,7 +9,7 @@ use ecow::{eco_format, EcoString};
use parking_lot::Mutex;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::syntax::{FileId, Lines, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, World};
@ -181,10 +181,20 @@ impl SystemWorld {
}
}
/// Lookup a source file by id.
/// Lookup line metadata for a file by id.
#[track_caller]
pub fn lookup(&self, id: FileId) -> Source {
self.source(id).expect("file id does not point to any source file")
pub fn lookup(&self, id: FileId) -> Lines<String> {
self.slot(id, |slot| {
if let Some(source) = slot.source.get() {
let source = source.as_ref().expect("file is not valid");
source.lines().clone()
} else if let Some(bytes) = slot.file.get() {
let bytes = bytes.as_ref().expect("file is not valid");
Lines::try_from(bytes).expect("file is not valid utf-8")
} else {
panic!("file id does not point to any source file");
}
})
}
}
@ -339,6 +349,11 @@ impl<T: Clone> SlotCell<T> {
self.accessed = false;
}
/// Gets the contents of the cell.
fn get(&self) -> Option<&FileResult<T>> {
self.data.as_ref()
}
/// Gets the contents of the cell or initialize them.
fn get_or_init(
&mut self,

View File

@ -25,15 +25,13 @@ impl Eval for ast::FuncCall<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.span();
let callee = self.callee();
let in_math = in_math(callee);
let callee_span = callee.span();
let args = self.args();
let trailing_comma = args.trailing_comma();
vm.engine.route.check_call_depth().at(span)?;
// Try to evaluate as a call to an associated function or field.
let (callee, args) = if let ast::Expr::FieldAccess(access) = callee {
let (callee_value, args_value) = if let ast::Expr::FieldAccess(access) = callee {
let target = access.target();
let field = access.field();
match eval_field_call(target, field, args, span, vm)? {
@ -50,9 +48,15 @@ impl Eval for ast::FuncCall<'_> {
(callee.eval(vm)?, args.eval(vm)?.spanned(span))
};
let func_result = callee.clone().cast::<Func>();
if in_math && func_result.is_err() {
return wrap_args_in_math(callee, callee_span, args, trailing_comma);
let func_result = callee_value.clone().cast::<Func>();
if func_result.is_err() && in_math(callee) {
return wrap_args_in_math(
callee_value,
callee_span,
args_value,
args.trailing_comma(),
);
}
let func = func_result
@ -61,8 +65,11 @@ impl Eval for ast::FuncCall<'_> {
let point = || Tracepoint::Call(func.name().map(Into::into));
let f = || {
func.call(&mut vm.engine, vm.context, args)
.trace(vm.world(), point, span)
func.call(&mut vm.engine, vm.context, args_value).trace(
vm.world(),
point,
span,
)
};
// Stacker is broken on WASM.

View File

@ -18,7 +18,6 @@ pub use self::call::{eval_closure, CapturesVisitor};
pub use self::flow::FlowEvent;
pub use self::import::import;
pub use self::vm::Vm;
pub use typst_library::routines::EvalMode;
use self::access::*;
use self::binding::*;
@ -32,7 +31,7 @@ use typst_library::introspection::Introspector;
use typst_library::math::EquationElem;
use typst_library::routines::Routines;
use typst_library::World;
use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span};
use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span, SyntaxMode};
/// Evaluate a source file and return the resulting module.
#[comemo::memoize]
@ -104,13 +103,13 @@ pub fn eval_string(
sink: TrackedMut<Sink>,
string: &str,
span: Span,
mode: EvalMode,
mode: SyntaxMode,
scope: Scope,
) -> SourceResult<Value> {
let mut root = match mode {
EvalMode::Code => parse_code(string),
EvalMode::Markup => parse(string),
EvalMode::Math => parse_math(string),
SyntaxMode::Code => parse_code(string),
SyntaxMode::Markup => parse(string),
SyntaxMode::Math => parse_math(string),
};
root.synthesize(span);
@ -141,11 +140,11 @@ pub fn eval_string(
// Evaluate the code.
let output = match mode {
EvalMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?,
EvalMode::Markup => {
SyntaxMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?,
SyntaxMode::Markup => {
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)?)
.with_block(false)
.pack()

View File

@ -186,7 +186,7 @@ impl Eval for ast::Raw<'_> {
let lines = self.lines().map(|line| (line.get().clone(), line.span())).collect();
let mut elem = RawElem::new(RawContent::Lines(lines)).with_block(self.block());
if let Some(lang) = self.lang() {
elem.push_lang(Some(lang.get().clone()));
elem.lang.set(Some(lang.get().clone()));
}
Ok(elem.pack())
}
@ -205,7 +205,9 @@ impl Eval for ast::Label<'_> {
type Output = Value;
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;
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);
if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content(
supplement.eval(vm)?,
))));
elem.supplement
.set(Smart::Custom(Some(Supplement::Content(supplement.eval(vm)?))));
}
Ok(elem.pack())
}
@ -249,7 +251,7 @@ impl Eval for ast::EnumItem<'_> {
let body = self.body().eval(vm)?;
let mut elem = EnumItem::new(body);
if let Some(number) = self.number() {
elem.push_number(Some(number));
elem.number.set(Some(number));
}
Ok(elem.pack())
}

View File

@ -80,17 +80,17 @@ impl Eval for ast::MathAttach<'_> {
let mut elem = AttachElem::new(base);
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),
// i.e. at the top-right corner.
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() {
elem.push_b(Some(expr.eval_display(vm)?));
elem.b.set(Some(expr.eval_display(vm)?));
}
Ok(elem.pack())

View File

@ -1,6 +1,6 @@
use typst_library::diag::{warning, At, SourceResult};
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::model::ParElem;
@ -62,8 +62,7 @@ fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) {
if let Some(Selector::Elem(elem, _)) = recipe.selector();
if *elem == Element::of::<ParElem>();
if let Transformation::Style(styles) = recipe.transform();
if styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Above as _) ||
styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Below as _);
if styles.has(BlockElem::above) || styles.has(BlockElem::below);
then {
vm.engine.sink.warn(warning!(
recipe.span(),

View File

@ -2,8 +2,9 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
use typst_library::layout::Frame;
use typst_library::html::{
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
};
use typst_syntax::Span;
/// Encodes an HTML document into a string.
@ -28,7 +29,7 @@ struct Writer {
pretty: bool,
}
/// Write a newline and indent, if pretty printing is enabled.
/// Writes a newline and indent, if pretty printing is enabled.
fn write_indent(w: &mut Writer) {
if w.pretty {
w.buf.push('\n');
@ -38,7 +39,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<()> {
match node {
HtmlNode::Tag(_) => {}
@ -49,7 +50,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
Ok(())
}
/// Encode plain text into the writer.
/// Encodes plain text into the writer.
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
for c in text.chars() {
if charsets::is_valid_in_normal_element_text(c) {
@ -61,7 +62,7 @@ fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
Ok(())
}
/// Encode one element into the write.
/// Encodes one element into the writer.
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.buf.push('<');
w.buf.push_str(&element.tag.resolve());
@ -69,54 +70,37 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
for (attr, value) in &element.attrs.0 {
w.buf.push(' ');
w.buf.push_str(&attr.resolve());
w.buf.push('=');
w.buf.push('"');
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)?;
// If the string is empty, we can use shorthand syntax.
// `<elem attr="">..</div` is equivalent to `<elem attr>..</div>`
if !value.is_empty() {
w.buf.push('=');
w.buf.push('"');
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('>');
if tag::is_void(element.tag) {
if !element.children.is_empty() {
bail!(element.span, "HTML void elements must not have children");
}
return Ok(());
}
let pretty = w.pretty;
if !element.children.is_empty() {
let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
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);
if tag::is_raw(element.tag) {
write_raw(w, element)?;
} else if !element.children.is_empty() {
write_children(w, element)?;
}
w.pretty = pretty;
w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve());
@ -125,6 +109,159 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
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),
_ => 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
/// element's contents.
///
@ -160,15 +297,21 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
c if charsets::is_w3c_text_char(c) && c != '\r' => {
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(())
}
/// 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_frame(frame)
.replace("<svg class", "<svg style=\"overflow: visible;\" class");
let svg = typst_svg::svg_frame(&frame.inner).replace(
"<svg class",
&format!(
"<svg style=\"overflow: visible; width: {}em; height: {}em;\" class",
frame.inner.width() / frame.text_size,
frame.inner.height() / frame.text_size,
),
);
w.buf.push_str(&svg);
}

View File

@ -9,7 +9,7 @@ use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode,
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
};
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
@ -177,15 +177,12 @@ fn handle(
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) {
if let Some(body) = elem.body.get_ref(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(),
attrs: elem.attrs.get_cloned(styles),
children,
span: elem.span(),
};
@ -201,7 +198,7 @@ fn handle(
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// TODO: This is rather incomplete.
if let Some(body) = elem.body(styles) {
if let Some(body) = elem.body.get_ref(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
@ -215,7 +212,7 @@ fn handle(
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body(styles) {
.and_then(|elem| match elem.body.get_ref(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
@ -236,12 +233,12 @@ fn handle(
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 { '\'' },
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::set_target(Target::Paged).wrap();
let style = TargetElem::target.set(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
@ -249,7 +246,10 @@ fn handle(
styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?;
output.push(HtmlNode::Frame(frame));
output.push(HtmlNode::Frame(HtmlFrame {
inner: frame,
text_size: styles.resolve(TextElem::size),
}));
} else {
engine.sink.warn(warning!(
child.span(),

View File

@ -298,13 +298,20 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
return false;
}
// Start of an interpolated identifier: "#|".
// Start of an interpolated identifier: "$#|$".
if ctx.leaf.kind() == SyntaxKind::Hash {
ctx.from = ctx.cursor;
code_completions(ctx, true);
return true;
}
// Behind existing interpolated identifier: "$#pa|$".
if ctx.leaf.kind() == SyntaxKind::Ident {
ctx.from = ctx.leaf.offset();
code_completions(ctx, true);
return true;
}
// Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(
ctx.leaf.kind(),
@ -441,7 +448,7 @@ fn field_access_completions(
match value {
Value::Symbol(symbol) => {
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 {
kind: CompletionKind::Symbol(modified.get()),
label: modifier.into(),
@ -694,7 +701,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
let mut deciding = ctx.leaf.clone();
while !matches!(
deciding.kind(),
SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon
SyntaxKind::LeftParen
| SyntaxKind::RightParen
| SyntaxKind::Comma
| SyntaxKind::Colon
) {
let Some(prev) = deciding.prev_leaf() else { break };
deciding = prev;
@ -841,7 +851,9 @@ fn param_value_completions<'a>(
/// Returns which file extensions to complete for the given parameter if any.
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
Some(match (func.name(), param.name) {
(Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
(Some("image"), "source") => {
&["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp"]
}
(Some("csv"), "source") => &["csv"],
(Some("plugin"), "source") => &["wasm"],
(Some("cbor"), "source") => &["cbor"],
@ -1664,6 +1676,13 @@ mod tests {
test("#{() .a}", -2).must_include(["at", "any", "all"]);
}
/// Test that autocomplete in math uses the correct global scope.
#[test]
fn test_autocomplete_math_scope() {
test("$#col$", -2).must_include(["colbreak"]).must_exclude(["colon"]);
test("$col$", -2).must_include(["colon"]).must_exclude(["colbreak"]);
}
/// Test that the `before_window` doesn't slice into invalid byte
/// boundaries.
#[test]
@ -1682,7 +1701,7 @@ mod tests {
// Then, add the invalid `#cite` call. Had the document been invalid
// initially, we would have no populated document to autocomplete with.
let end = world.main.len_bytes();
let end = world.main.text().len();
world.main.edit(end..end, " #cite()");
test_with_doc(&world, -2, doc.as_ref())
@ -1718,6 +1737,8 @@ mod tests {
test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"])
.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
@ -1813,18 +1834,27 @@ mod tests {
#[test]
fn test_autocomplete_fonts() {
test("#text(font:)", -1)
test("#text(font:)", -2)
.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\""]);
test("#show math.equation: set text(font: )", -1)
test("#show math.equation: set text(font: )", -2)
.must_include(["\"New Computer Modern Math\""])
.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_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

@ -72,7 +72,8 @@ pub fn definition(
// Try to jump to the referenced content.
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 elem = document?.introspector.query_first(&selector)?;
return Some(Definition::Span(elem.span()));
@ -186,6 +187,6 @@ mod tests {
#[test]
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

@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash};
use typst::{Library, World};
use typst::{Feature, Library, World};
use crate::IdeWorld;
@ -168,14 +168,14 @@ fn library() -> Library {
// 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
// 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
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));
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())));
.set(PageElem::margin, Margin::splat(Some(Smart::Custom(Abs::pt(10.0).into()))));
lib.styles.set(TextElem::size, TextSize(Abs::pt(10.0).into()));
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 {
fn resolve(self, world: &TestWorld) -> (Source, usize);
}
@ -228,7 +229,7 @@ impl FilePos for (&str, isize) {
#[track_caller]
fn cursor(source: &Source, cursor: isize) -> usize {
if cursor < 0 {
source.len_bytes().checked_add_signed(cursor + 1).unwrap()
source.text().len().checked_add_signed(cursor + 1).unwrap()
} else {
cursor as usize
}

View File

@ -114,7 +114,9 @@ pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope {
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathAttach)
);
) && leaf
.prev_leaf()
.is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash));
let library = world.library();
if in_math {

View File

@ -128,8 +128,7 @@ impl Downloader {
}
// Configure native TLS.
let connector =
tls.build().map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let connector = tls.build().map_err(io::Error::other)?;
builder = builder.tls_connector(Arc::new(connector));
builder.build().get(url).call()

View File

@ -199,7 +199,7 @@ impl PackageStorage {
// The place at which the specific package version will live in the end.
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
// its final destination.
//

View File

@ -30,6 +30,7 @@ icu_provider_adapters = { workspace = true }
icu_provider_blob = { workspace = true }
icu_segmenter = { workspace = true }
kurbo = { workspace = true }
memchr = { workspace = true }
rustybuzz = { workspace = true }
smallvec = { workspace = true }
ttf-parser = { workspace = true }

View File

@ -24,15 +24,15 @@ pub fn layout_single_block(
region: Region,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
let width = elem.width.get(styles);
let height = elem.height.get(styles);
let inset = elem.inset.resolve(styles).unwrap_or_default();
// Build the pod regions.
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size);
// Layout the body.
let body = elem.body(styles);
let body = elem.body.get_ref(styles);
let mut frame = match body {
// If we have no body, just create one frame. Its size will be
// adjusted below.
@ -73,18 +73,19 @@ pub fn layout_single_block(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill = elem.fill.get_cloned(styles);
let stroke = elem
.stroke(styles)
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
if elem.clip.get(styles) {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
}
@ -111,9 +112,9 @@ pub fn layout_multi_block(
regions: Regions,
) -> SourceResult<Fragment> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
let width = elem.width.get(styles);
let height = elem.height.get(styles);
let inset = elem.inset.resolve(styles).unwrap_or_default();
// Allocate a small vector for backlogs.
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);
// Layout the body.
let body = elem.body(styles);
let body = elem.body.get_ref(styles);
let mut fragment = match body {
// If we have no body, just create one frame plus one per backlog
// region. We create them zero-sized; if necessary, their size will
@ -188,18 +189,19 @@ pub fn layout_multi_block(
};
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill = elem.fill.get_ref(styles);
let stroke = elem
.stroke(styles)
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// 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_inset = !inset.is_zero();
let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));

View File

@ -89,7 +89,7 @@ impl<'a> Collector<'a, '_, '_> {
} else if child.is::<FlushElem>() {
self.output.push(Child::Flush);
} 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>() {
bail!(
child.span(), "pagebreaks are not allowed inside of containers";
@ -132,7 +132,7 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag));
}
let leading = ParElem::leading_in(styles);
let leading = styles.resolve(ParElem::leading);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] {
@ -146,7 +146,9 @@ impl<'a> Collector<'a, '_, '_> {
/// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
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),
});
}
@ -169,8 +171,8 @@ impl<'a> Collector<'a, '_, '_> {
)?
.into_frames();
let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
let spacing = elem.spacing.resolve(styles);
let leading = elem.leading.resolve(styles);
self.output.push(Child::Rel(spacing.into(), 4));
@ -184,8 +186,8 @@ impl<'a> Collector<'a, '_, '_> {
/// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles);
let costs = TextElem::costs_in(styles);
let align = styles.resolve(AlignElem::alignment);
let costs = styles.get(TextElem::costs);
// Determine whether to prevent widow and orphans.
let len = lines.len();
@ -231,23 +233,23 @@ impl<'a> Collector<'a, '_, '_> {
/// whether it is breakable.
fn block(&mut self, elem: &'a Packed<BlockElem>, styles: StyleChain<'a>) {
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 sticky = elem.sticky(styles);
let breakable = elem.breakable(styles);
let fr = match elem.height(styles) {
let sticky = elem.sticky.get(styles);
let breakable = elem.breakable.get(styles);
let fr = match elem.height.get(styles) {
Sizing::Fr(fr) => Some(fr),
_ => None,
};
let fallback = LazyCell::new(|| ParElem::spacing_in(styles));
let fallback = LazyCell::new(|| styles.resolve(ParElem::spacing));
let spacing = |amount| match amount {
Smart::Auto => Child::Rel((*fallback).into(), 4),
Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
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() {
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;
}
@ -282,13 +284,13 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<PlaceElem>,
styles: StyleChain<'a>,
) -> SourceResult<()> {
let alignment = elem.alignment(styles);
let alignment = elem.alignment.get(styles);
let align_x = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles)
});
let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let scope = elem.scope(styles);
let float = elem.float(styles);
let scope = elem.scope.get(styles);
let float = elem.float.get(styles);
match (float, align_y) {
(true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
@ -312,8 +314,8 @@ impl<'a> Collector<'a, '_, '_> {
}
let locator = self.locator.next(&elem.span());
let clearance = elem.clearance(styles);
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
let clearance = elem.clearance.resolve(styles);
let delta = Axes::new(elem.dx.get(styles), elem.dy.get(styles)).resolve(styles);
self.output.push(Child::Placed(self.boxed(PlacedChild {
align_x,
align_y,
@ -631,7 +633,7 @@ impl PlacedChild<'_> {
pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
self.cell.get_or_init(base, |base| {
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 mut frame = layout_and_modify(styles, |styles| {

View File

@ -851,7 +851,7 @@ fn layout_line_number_reset(
config: &Config,
locator: &mut SplitLocator,
) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem());
let counter = Counter::of(ParLineMarker::ELEM);
let update = CounterUpdate::Set(CounterState::init(false));
let content = counter.update(Span::detached(), update);
crate::layout_frame(
@ -879,7 +879,7 @@ fn layout_line_number(
locator: &mut SplitLocator,
numbering: &Numbering,
) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem());
let counter = Counter::of(ParLineMarker::ELEM);
let update = CounterUpdate::Step(NonZeroUsize::ONE);
let numbering = Smart::Custom(numbering.clone());

View File

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

View File

@ -3,7 +3,9 @@ use std::fmt::Debug;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable};
use typst_library::layout::grid::resolve::{
Cell, CellGrid, Header, LinePosition, Repeatable,
};
use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing,
@ -30,10 +32,8 @@ pub struct GridLayouter<'a> {
pub(super) rcols: Vec<Abs>,
/// The sum of `rcols`.
pub(super) width: Abs,
/// Resolve row sizes, by region.
/// Resolved row sizes, by region.
pub(super) rrows: Vec<Vec<RowPiece>>,
/// Rows in the current region.
pub(super) lrows: Vec<Row>,
/// The amount of unbreakable rows remaining to be laid out in the
/// current unbreakable row group. While this is positive, no region breaks
/// should occur.
@ -41,24 +41,155 @@ pub struct GridLayouter<'a> {
/// Rowspans not yet laid out because not all of their spanned rows were
/// laid out yet.
pub(super) rowspans: Vec<Rowspan>,
/// The initial size of the current region before we started subtracting.
pub(super) initial: Size,
/// Grid layout state for the current region.
pub(super) current: Current,
/// Frames for finished regions.
pub(super) finished: Vec<Frame>,
/// The amount and height of header rows on each finished region.
pub(super) finished_header_rows: Vec<FinishedHeaderRowInfo>,
/// Whether this is an RTL grid.
pub(super) is_rtl: bool,
/// The simulated header height.
/// This field is reset in `layout_header` and properly updated by
/// Currently repeating headers, one per level. Sorted by increasing
/// levels.
///
/// Note that some levels may be absent, in particular level 0, which does
/// not exist (so all levels are >= 1).
pub(super) repeating_headers: Vec<&'a Header>,
/// Headers, repeating or not, awaiting their first successful layout.
/// Sorted by increasing levels.
pub(super) pending_headers: &'a [Repeatable<Header>],
/// Next headers to be processed.
pub(super) upcoming_headers: &'a [Repeatable<Header>],
/// State of the row being currently laid out.
///
/// This is kept as a field to avoid passing down too many parameters from
/// `layout_row` into called functions, which would then have to pass them
/// down to `push_row`, which reads these values.
pub(super) row_state: RowState,
/// The span of the grid element.
pub(super) span: Span,
}
/// Grid layout state for the current region. This should be reset or updated
/// on each region break.
pub(super) struct Current {
/// The initial size of the current region before we started subtracting.
pub(super) initial: Size,
/// The height of the region after repeated headers were placed and footers
/// prepared. This also includes pending repeating headers from the start,
/// even if they were not repeated yet, since they will be repeated in the
/// next region anyway (bar orphan prevention).
///
/// This is used to quickly tell if any additional space in the region has
/// been occupied since then, meaning that additional space will become
/// available after a region break (see
/// [`GridLayouter::may_progress_with_repeats`]).
pub(super) initial_after_repeats: Abs,
/// Whether `layouter.regions.may_progress()` was `true` at the top of the
/// region.
pub(super) could_progress_at_top: bool,
/// Rows in the current region.
pub(super) lrows: Vec<Row>,
/// The amount of repeated header rows at the start of the current region.
/// Thus, excludes rows from pending headers (which were placed for the
/// first time).
///
/// Note that `repeating_headers` and `pending_headers` can change if we
/// find a new header inside the region (not at the top), so this field
/// is required to access information from the top of the region.
///
/// This information is used on finish region to calculate the total height
/// of resolved header rows at the top of the region, which is used by
/// multi-page rowspans so they can properly skip the header rows at the
/// top of each region during layout.
pub(super) repeated_header_rows: usize,
/// The end bound of the row range of the last repeating header at the
/// start of the region.
///
/// The last row might have disappeared from layout due to being empty, so
/// this is how we can become aware of where the last header ends without
/// having to check the vector of rows. Line layout uses this to determine
/// when to prioritize the last lines under a header.
///
/// A value of zero indicates no repeated headers were placed.
pub(super) last_repeated_header_end: usize,
/// Stores the length of `lrows` before a sequence of rows equipped with
/// orphan prevention was laid out. In this case, if no more rows without
/// orphan prevention are laid out after those rows before the region ends,
/// the rows will be removed, and there may be an attempt to place them
/// again in the new region. Effectively, this is the mechanism used for
/// orphan prevention of rows.
///
/// At the moment, this is only used by repeated headers (they aren't laid
/// out if alone in the region) and by new headers, which are moved to the
/// `pending_headers` vector and so will automatically be placed again
/// until they fit and are not orphans in at least one region (or exactly
/// one, for non-repeated headers).
pub(super) lrows_orphan_snapshot: Option<usize>,
/// The height of effectively repeating headers, that is, ignoring
/// non-repeating pending headers, in the current region.
///
/// This is used by multi-page auto rows so they can inform cell layout on
/// how much space should be taken by headers if they break across regions.
/// In particular, non-repeating headers only occupy the initial region,
/// but disappear on new regions, so they can be ignored.
///
/// This field is reset on each new region and properly updated by
/// `layout_auto_row` and `layout_relative_row`, and should not be read
/// before all header rows are fully laid out. It is usually fine because
/// header rows themselves are unbreakable, and unbreakable rows do not
/// need to read this field at all.
pub(super) header_height: Abs,
///
/// This height is not only computed at the beginning of the region. It is
/// updated whenever a new header is found, subtracting the height of
/// headers which stopped repeating and adding the height of all new
/// headers.
pub(super) repeating_header_height: Abs,
/// The height for each repeating header that was placed in this region.
/// Note that this includes headers not at the top of the region, before
/// their first repetition (pending headers), and excludes headers removed
/// by virtue of a new, conflicting header being found (short-lived
/// headers).
///
/// This is used to know how much to update `repeating_header_height` by
/// when finding a new header and causing existing repeating headers to
/// stop.
pub(super) repeating_header_heights: Vec<Abs>,
/// The simulated footer height for this region.
///
/// The simulation occurs before any rows are laid out for a region.
pub(super) footer_height: Abs,
/// The span of the grid element.
pub(super) span: Span,
}
/// Data about the row being laid out right now.
#[derive(Debug, Default)]
pub(super) struct RowState {
/// If this is `Some`, this will be updated by the currently laid out row's
/// height if it is auto or relative. This is used for header height
/// calculation.
pub(super) current_row_height: Option<Abs>,
/// This is `true` when laying out non-short lived headers and footers.
/// That is, headers and footers which are not immediately followed or
/// preceded (respectively) by conflicting headers and footers of same or
/// lower level, or the end or start of the table (respectively), which
/// would cause them to never repeat, even once.
///
/// If this is `false`, the next row to be laid out will remove an active
/// orphan snapshot and will flush pending headers, as there is no risk
/// that they will be orphans anymore.
pub(super) in_active_repeatable: bool,
}
/// Data about laid out repeated header rows for a specific finished region.
#[derive(Debug, Default)]
pub(super) struct FinishedHeaderRowInfo {
/// The amount of repeated headers at the top of the region.
pub(super) repeated_amount: usize,
/// The end bound of the row range of the last repeated header at the top
/// of the region.
pub(super) last_repeated_header_end: usize,
/// The total height of repeated headers at the top of the region.
pub(super) repeated_height: Abs,
}
/// Details about a resulting row piece.
@ -114,14 +245,27 @@ impl<'a> GridLayouter<'a> {
rcols: vec![Abs::zero(); grid.cols.len()],
width: Abs::zero(),
rrows: vec![],
lrows: vec![],
unbreakable_rows_left: 0,
rowspans: vec![],
initial: regions.size,
finished: vec![],
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
header_height: Abs::zero(),
footer_height: Abs::zero(),
finished_header_rows: vec![],
is_rtl: styles.resolve(TextElem::dir) == Dir::RTL,
repeating_headers: vec![],
upcoming_headers: &grid.headers,
pending_headers: Default::default(),
row_state: RowState::default(),
current: Current {
initial: regions.size,
initial_after_repeats: regions.size.y,
could_progress_at_top: regions.may_progress(),
lrows: vec![],
repeated_header_rows: 0,
last_repeated_header_end: 0,
lrows_orphan_snapshot: None,
repeating_header_height: Abs::zero(),
repeating_header_heights: vec![],
footer_height: Abs::zero(),
},
span,
}
}
@ -130,38 +274,57 @@ impl<'a> GridLayouter<'a> {
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
self.measure_columns(engine)?;
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
// Ensure rows in the first region will be aware of the possible
// presence of the footer.
self.prepare_footer(footer, engine, 0)?;
if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) {
// No repeatable header, so we won't subtract it later.
self.regions.size.y -= self.footer_height;
if let Some(footer) = &self.grid.footer {
if footer.repeated {
// Ensure rows in the first region will be aware of the
// possible presence of the footer.
self.prepare_footer(footer, engine, 0)?;
self.regions.size.y -= self.current.footer_height;
self.current.initial_after_repeats = self.regions.size.y;
}
}
for y in 0..self.grid.rows.len() {
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
if y < header.end {
if y == 0 {
self.layout_header(header, engine, 0)?;
self.regions.size.y -= self.footer_height;
}
let mut y = 0;
let mut consecutive_header_count = 0;
while y < self.grid.rows.len() {
if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count)
{
if next_header.range.contains(&y) {
self.place_new_headers(&mut consecutive_header_count, engine)?;
y = next_header.range.end;
// Skip header rows during normal layout.
continue;
}
}
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
if y >= footer.start {
if let Some(footer) = &self.grid.footer {
if footer.repeated && y >= footer.start {
if y == footer.start {
self.layout_footer(footer, engine, self.finished.len())?;
self.flush_orphans();
}
y = footer.end;
continue;
}
}
self.layout_row(y, engine, 0)?;
// After the first non-header row is placed, pending headers are no
// longer orphans and can repeat, so we move them to repeating
// headers.
//
// Note that this is usually done in `push_row`, since the call to
// `layout_row` above might trigger region breaks (for multi-page
// auto rows), whereas this needs to be called as soon as any part
// of a row is laid out. However, it's possible a row has no
// visible output and thus does not push any rows even though it
// was successfully laid out, in which case we additionally flush
// here just in case.
self.flush_orphans();
y += 1;
}
self.finish_region(engine, true)?;
@ -184,12 +347,46 @@ impl<'a> GridLayouter<'a> {
self.render_fills_strokes()
}
/// Layout the given row.
/// Layout a row with a certain initial state, returning the final state.
#[inline]
pub(super) fn layout_row_with_state(
&mut self,
y: usize,
engine: &mut Engine,
disambiguator: usize,
initial_state: RowState,
) -> SourceResult<RowState> {
// Keep a copy of the previous value in the stack, as this function can
// call itself recursively (e.g. if a region break is triggered and a
// header is placed), so we shouldn't outright overwrite it, but rather
// save and later restore the state when back to this call.
let previous = std::mem::replace(&mut self.row_state, initial_state);
// Keep it as a separate function to allow inlining the return below,
// as it's usually not needed.
self.layout_row_internal(y, engine, disambiguator)?;
Ok(std::mem::replace(&mut self.row_state, previous))
}
/// Layout the given row with the default row state.
#[inline]
pub(super) fn layout_row(
&mut self,
y: usize,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<()> {
self.layout_row_with_state(y, engine, disambiguator, RowState::default())?;
Ok(())
}
/// Layout the given row using the current state.
pub(super) fn layout_row_internal(
&mut self,
y: usize,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<()> {
// Skip to next region if current one is full, but only for content
// rows, not for gutter rows, and only if we aren't laying out an
@ -206,13 +403,18 @@ impl<'a> GridLayouter<'a> {
}
// Don't layout gutter rows at the top of a region.
if is_content_row || !self.lrows.is_empty() {
if is_content_row || !self.current.lrows.is_empty() {
match self.grid.rows[y] {
Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?,
Sizing::Rel(v) => {
self.layout_relative_row(engine, disambiguator, v, y)?
}
Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)),
Sizing::Fr(v) => {
if !self.row_state.in_active_repeatable {
self.flush_orphans();
}
self.current.lrows.push(Row::Fr(v, y, disambiguator))
}
}
}
@ -225,8 +427,13 @@ impl<'a> GridLayouter<'a> {
fn render_fills_strokes(mut self) -> SourceResult<Fragment> {
let mut finished = std::mem::take(&mut self.finished);
let frame_amount = finished.len();
for ((frame_index, frame), rows) in
finished.iter_mut().enumerate().zip(&self.rrows)
for (((frame_index, frame), rows), finished_header_rows) in
finished.iter_mut().enumerate().zip(&self.rrows).zip(
self.finished_header_rows
.iter()
.map(Some)
.chain(std::iter::repeat(None)),
)
{
if self.rcols.is_empty() || rows.is_empty() {
continue;
@ -347,7 +554,8 @@ impl<'a> GridLayouter<'a> {
let hline_indices = rows
.iter()
.map(|piece| piece.y)
.chain(std::iter::once(self.grid.rows.len()));
.chain(std::iter::once(self.grid.rows.len()))
.enumerate();
// Converts a row to the corresponding index in the vector of
// hlines.
@ -372,7 +580,7 @@ impl<'a> GridLayouter<'a> {
};
let mut prev_y = None;
for (y, dy) in hline_indices.zip(hline_offsets) {
for ((i, y), dy) in hline_indices.zip(hline_offsets) {
// Position of lines below the row index in the previous iteration.
let expected_prev_line_position = prev_y
.map(|prev_y| {
@ -383,47 +591,40 @@ impl<'a> GridLayouter<'a> {
})
.unwrap_or(LinePosition::Before);
// FIXME: In the future, directly specify in 'self.rrows' when
// we place a repeated header rather than its original rows.
// That would let us remove most of those verbose checks, both
// in 'lines.rs' and here. Those checks also aren't fully
// accurate either, since they will also trigger when some rows
// have been removed between the header and what's below it.
let is_under_repeated_header = self
.grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.zip(prev_y)
.is_some_and(|(header, prev_y)| {
// Note: 'y == header.end' would mean we're right below
// the NON-REPEATED header, so that case should return
// false.
prev_y < header.end && y > header.end
});
// Header's lines at the bottom have priority when repeated.
// This will store the end bound of the last header if the
// current iteration is calculating lines under it.
let last_repeated_header_end_above = match finished_header_rows {
Some(info) if prev_y.is_some() && i == info.repeated_amount => {
Some(info.last_repeated_header_end)
}
_ => None,
};
// If some grid rows were omitted between the previous resolved
// row and the current one, we ensure lines below the previous
// row don't "disappear" and are considered, albeit with less
// priority. However, don't do this when we're below a header,
// as it must have more priority instead of less, so it is
// chained later instead of before. The exception is when the
// chained later instead of before (stored in the
// 'header_hlines' variable below). The exception is when the
// last row in the header is removed, in which case we append
// both the lines under the row above us and also (later) the
// lines under the header's (removed) last row.
let prev_lines = prev_y
.filter(|prev_y| {
prev_y + 1 != y
&& (!is_under_repeated_header
|| self
.grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| prev_y + 1 != header.end))
})
.map(|prev_y| get_hlines_at(prev_y + 1))
.unwrap_or(&[]);
let prev_lines = match prev_y {
Some(prev_y)
if prev_y + 1 != y
&& last_repeated_header_end_above.is_none_or(
|last_repeated_header_end| {
prev_y + 1 != last_repeated_header_end
},
) =>
{
get_hlines_at(prev_y + 1)
}
_ => &[],
};
let expected_hline_position =
expected_line_position(y, y == self.grid.rows.len());
@ -441,15 +642,13 @@ impl<'a> GridLayouter<'a> {
};
let mut expected_header_line_position = LinePosition::Before;
let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) =
self.grid.header.as_ref().zip(prev_y)
{
if is_under_repeated_header
&& (!self.grid.has_gutter
let header_hlines = match (last_repeated_header_end_above, prev_y) {
(Some(header_end_above), Some(prev_y))
if !self.grid.has_gutter
|| matches!(
self.grid.rows[prev_y],
Sizing::Rel(length) if length.is_zero()
))
) =>
{
// For lines below a header, give priority to the
// lines originally below the header rather than
@ -468,15 +667,13 @@ impl<'a> GridLayouter<'a> {
// column-gutter is specified, for example. In that
// case, we still repeat the line under the gutter.
expected_header_line_position = expected_line_position(
header.end,
header.end == self.grid.rows.len(),
header_end_above,
header_end_above == self.grid.rows.len(),
);
get_hlines_at(header.end)
} else {
&[]
get_hlines_at(header_end_above)
}
} else {
&[]
_ => &[],
};
// The effective hlines to be considered at this row index are
@ -529,6 +726,7 @@ impl<'a> GridLayouter<'a> {
grid,
rows,
local_top_y,
last_repeated_header_end_above,
in_last_region,
y,
x,
@ -941,15 +1139,9 @@ impl<'a> GridLayouter<'a> {
let frame = self.layout_single_row(engine, disambiguator, first, y)?;
self.push_row(frame, y, true);
if self
.grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| y < header.end)
{
// Add to header height.
self.header_height += first;
if let Some(row_height) = &mut self.row_state.current_row_height {
// Add to header height, as we are in a header row.
*row_height += first;
}
return Ok(());
@ -958,19 +1150,21 @@ impl<'a> GridLayouter<'a> {
// Expand all but the last region.
// Skip the first region if the space is eaten up by an fr row.
let len = resolved.len();
for ((i, region), target) in self
.regions
.iter()
.enumerate()
.zip(&mut resolved[..len - 1])
.skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
for ((i, region), target) in
self.regions
.iter()
.enumerate()
.zip(&mut resolved[..len - 1])
.skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..)))
as usize)
{
// Subtract header and footer heights from the region height when
// it's not the first.
// it's not the first. Ignore non-repeating headers as they only
// appear on the first region by definition.
target.set_max(
region.y
- if i > 0 {
self.header_height + self.footer_height
self.current.repeating_header_height + self.current.footer_height
} else {
Abs::zero()
},
@ -1181,25 +1375,19 @@ impl<'a> GridLayouter<'a> {
let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
let frame = self.layout_single_row(engine, disambiguator, resolved, y)?;
if self
.grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|header| y < header.end)
{
// Add to header height.
self.header_height += resolved;
if let Some(row_height) = &mut self.row_state.current_row_height {
// Add to header height, as we are in a header row.
*row_height += resolved;
}
// Skip to fitting region, but only if we aren't part of an unbreakable
// row group. We use 'in_last_with_offset' so our 'in_last' call
// properly considers that a header and a footer would be added on each
// region break.
// row group. We use 'may_progress_with_repeats' to stop trying if we
// would skip to a region with the same height and where the same
// headers would be repeated.
let height = frame.height();
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(height)
&& !in_last_with_offset(self.regions, self.header_height + self.footer_height)
&& self.may_progress_with_repeats()
{
self.finish_region(engine, false)?;
@ -1323,8 +1511,13 @@ impl<'a> GridLayouter<'a> {
/// will be pushed for this particular row. It can be `false` for rows
/// spanning multiple regions.
fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) {
if !self.row_state.in_active_repeatable {
// There is now a row after the rows equipped with orphan
// prevention, so no need to keep moving them anymore.
self.flush_orphans();
}
self.regions.size.y -= frame.height();
self.lrows.push(Row::Frame(frame, y, is_last));
self.current.lrows.push(Row::Frame(frame, y, is_last));
}
/// Finish rows for one region.
@ -1333,68 +1526,73 @@ impl<'a> GridLayouter<'a> {
engine: &mut Engine,
last: bool,
) -> SourceResult<()> {
// The latest rows have orphan prevention (headers) and no other rows
// were placed, so remove those rows and try again in a new region,
// unless this is the last region.
if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take() {
if !last {
self.current.lrows.truncate(orphan_snapshot);
self.current.repeated_header_rows =
self.current.repeated_header_rows.min(orphan_snapshot);
if orphan_snapshot == 0 {
// Removed all repeated headers.
self.current.last_repeated_header_end = 0;
}
}
}
if self
.current
.lrows
.last()
.is_some_and(|row| self.grid.is_gutter_track(row.index()))
{
// Remove the last row in the region if it is a gutter row.
self.lrows.pop().unwrap();
self.current.lrows.pop().unwrap();
self.current.repeated_header_rows =
self.current.repeated_header_rows.min(self.current.lrows.len());
}
// If no rows other than the footer have been laid out so far, and
// there are rows beside the footer, then don't lay it out at all.
// This check doesn't apply, and is thus overridden, when there is a
// header.
let mut footer_would_be_orphan = self.lrows.is_empty()
&& !in_last_with_offset(
self.regions,
self.header_height + self.footer_height,
)
&& self
.grid
.footer
.as_ref()
.and_then(Repeatable::as_repeated)
.is_some_and(|footer| footer.start != 0);
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
if self.grid.rows.len() > header.end
&& self
.grid
.footer
.as_ref()
.and_then(Repeatable::as_repeated)
.is_none_or(|footer| footer.start != header.end)
&& self.lrows.last().is_some_and(|row| row.index() < header.end)
&& !in_last_with_offset(
self.regions,
self.header_height + self.footer_height,
)
{
// Header and footer would be alone in this region, but there are more
// rows beyond the header and the footer. Push an empty region.
self.lrows.clear();
footer_would_be_orphan = true;
}
}
// If no rows other than the footer have been laid out so far
// (e.g. due to header orphan prevention), and there are rows
// beside the footer, then don't lay it out at all.
//
// It is worth noting that the footer is made non-repeatable at
// the grid resolving stage if it is short-lived, that is, if
// it is at the start of the table (or right after headers at
// the start of the table).
//
// TODO(subfooters): explicitly check for short-lived footers.
// TODO(subfooters): widow prevention for non-repeated footers with a
// similar mechanism / when implementing multiple footers.
let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated)
&& self.current.lrows.is_empty()
&& self.current.could_progress_at_top;
let mut laid_out_footer_start = None;
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
// Don't layout the footer if it would be alone with the header in
// the page, and don't layout it twice.
if !footer_would_be_orphan
&& self.lrows.iter().all(|row| row.index() < footer.start)
{
laid_out_footer_start = Some(footer.start);
self.layout_footer(footer, engine, self.finished.len())?;
if !footer_would_be_widow {
if let Some(footer) = &self.grid.footer {
// 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
// twice (check below).
//
// TODO(subfooters): this check can be replaced by a vector of
// repeating footers in the future, and/or some "pending
// footers" vector for footers we're about to place.
if footer.repeated
&& self.current.lrows.iter().all(|row| row.index() < footer.start)
{
laid_out_footer_start = Some(footer.start);
self.layout_footer(footer, engine, self.finished.len())?;
}
}
}
// Determine the height of existing rows in the region.
let mut used = Abs::zero();
let mut fr = Fr::zero();
for row in &self.lrows {
for row in &self.current.lrows {
match row {
Row::Frame(frame, _, _) => used += frame.height(),
Row::Fr(v, _, _) => fr += *v,
@ -1403,9 +1601,9 @@ impl<'a> GridLayouter<'a> {
// Determine the size of the grid in this region, expanding fully if
// there are fr rows.
let mut size = Size::new(self.width, used).min(self.initial);
if fr.get() > 0.0 && self.initial.y.is_finite() {
size.y = self.initial.y;
let mut size = Size::new(self.width, used).min(self.current.initial);
if fr.get() > 0.0 && self.current.initial.y.is_finite() {
size.y = self.current.initial.y;
}
// The frame for the region.
@ -1413,9 +1611,10 @@ impl<'a> GridLayouter<'a> {
let mut pos = Point::zero();
let mut rrows = vec![];
let current_region = self.finished.len();
let mut repeated_header_row_height = Abs::zero();
// Place finished rows and layout fractional rows.
for row in std::mem::take(&mut self.lrows) {
for (i, row) in std::mem::take(&mut self.current.lrows).into_iter().enumerate() {
let (frame, y, is_last) = match row {
Row::Frame(frame, y, is_last) => (frame, y, is_last),
Row::Fr(v, y, disambiguator) => {
@ -1426,6 +1625,9 @@ impl<'a> GridLayouter<'a> {
};
let height = frame.height();
if i < self.current.repeated_header_rows {
repeated_header_row_height += height;
}
// Ensure rowspans which span this row will have enough space to
// be laid out over it later.
@ -1504,7 +1706,11 @@ impl<'a> GridLayouter<'a> {
// we have to check the same index again in the next
// iteration.
let rowspan = self.rowspans.remove(i);
self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?;
self.layout_rowspan(
rowspan,
Some((&mut output, repeated_header_row_height)),
engine,
)?;
} else {
i += 1;
}
@ -1515,21 +1721,40 @@ impl<'a> GridLayouter<'a> {
pos.y += height;
}
self.finish_region_internal(output, rrows);
self.finish_region_internal(
output,
rrows,
FinishedHeaderRowInfo {
repeated_amount: self.current.repeated_header_rows,
last_repeated_header_end: self.current.last_repeated_header_end,
repeated_height: repeated_header_row_height,
},
);
if !last {
self.current.repeated_header_rows = 0;
self.current.last_repeated_header_end = 0;
self.current.repeating_header_height = Abs::zero();
self.current.repeating_header_heights.clear();
let disambiguator = self.finished.len();
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
if let Some(footer) =
self.grid.footer.as_ref().and_then(Repeatable::as_repeated)
{
self.prepare_footer(footer, engine, disambiguator)?;
}
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
// Add a header to the new region.
self.layout_header(header, engine, disambiguator)?;
}
// Ensure rows don't try to overrun the footer.
self.regions.size.y -= self.footer_height;
// Note that header layout will only subtract this again if it has
// to skip regions to fit headers, so there is no risk of
// subtracting this twice.
self.regions.size.y -= self.current.footer_height;
self.current.initial_after_repeats = self.regions.size.y;
if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() {
// Add headers to the new region.
self.layout_active_headers(engine)?;
}
}
Ok(())
@ -1541,11 +1766,26 @@ impl<'a> GridLayouter<'a> {
&mut self,
output: Frame,
resolved_rows: Vec<RowPiece>,
header_row_info: FinishedHeaderRowInfo,
) {
self.finished.push(output);
self.rrows.push(resolved_rows);
self.regions.next();
self.initial = self.regions.size;
self.current.initial = self.regions.size;
// Repeats haven't been laid out yet, so in the meantime, this will
// represent the initial height after repeats laid out so far, and will
// be gradually updated when preparing footers and repeating headers.
self.current.initial_after_repeats = self.current.initial.y;
self.current.could_progress_at_top = self.regions.may_progress();
if !self.grid.headers.is_empty() {
self.finished_header_rows.push(header_row_info);
}
// Ensure orphan prevention is handled before resolving rows.
debug_assert!(self.current.lrows_orphan_snapshot.is_none());
}
}
@ -1560,13 +1800,3 @@ pub(super) fn points(
offset
})
}
/// Checks if the first region of a sequence of regions is the last usable
/// region, assuming that the last region will always be occupied by some
/// specific offset height, even after calling `.next()`, due to some
/// additional logic which adds content automatically on each region turn (in
/// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
regions.backlog.is_empty()
&& regions.last.is_none_or(|height| regions.size.y + offset == height)
}

View File

@ -391,10 +391,12 @@ pub fn vline_stroke_at_row(
///
/// This function assumes columns are sorted by increasing `x`, and rows are
/// sorted by increasing `y`.
#[allow(clippy::too_many_arguments)]
pub fn hline_stroke_at_column(
grid: &CellGrid,
rows: &[RowPiece],
local_top_y: Option<usize>,
header_end_above: Option<usize>,
in_last_region: bool,
y: usize,
x: usize,
@ -499,17 +501,15 @@ pub fn hline_stroke_at_column(
// Top border stroke and header stroke are generally prioritized, unless
// they don't have explicit hline overrides and one or more user-provided
// hlines would appear at the same position, which then are prioritized.
let top_stroke_comes_from_header = grid
.header
.as_ref()
.and_then(Repeatable::as_repeated)
.zip(local_top_y)
.is_some_and(|(header, local_top_y)| {
// Ensure the row above us is a repeated header.
// FIXME: Make this check more robust when headers at arbitrary
// positions are added.
local_top_y < header.end && y > header.end
});
let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and(
|(last_repeated_header_end, local_top_y)| {
// Check if the last repeated header row is above this line.
//
// Note that `y == last_repeated_header_end` is impossible for a
// strictly repeated header (not in its original position).
local_top_y < last_repeated_header_end && y > last_repeated_header_end
},
);
// Prioritize the footer's top stroke as well where applicable.
let bottom_stroke_comes_from_footer = grid
@ -637,7 +637,7 @@ mod test {
},
vec![],
vec![],
None,
vec![],
None,
entries,
)
@ -1175,7 +1175,7 @@ mod test {
},
vec![],
vec![],
None,
vec![],
None,
entries,
)
@ -1268,6 +1268,7 @@ mod test {
grid,
&rows,
y.checked_sub(1),
None,
true,
y,
x,
@ -1461,6 +1462,7 @@ mod test {
grid,
&rows,
y.checked_sub(1),
None,
true,
y,
x,
@ -1506,6 +1508,7 @@ mod test {
grid,
&rows,
if y == 4 { Some(2) } else { y.checked_sub(1) },
None,
true,
y,
x,

View File

@ -1,57 +1,446 @@
use std::ops::Deref;
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::GridLayouter;
use super::layouter::{GridLayouter, RowState};
use super::rowspans::UnbreakableRowGroup;
impl GridLayouter<'_> {
/// Layouts the header's rows.
/// Skips regions as necessary.
pub fn layout_header(
impl<'a> GridLayouter<'a> {
/// Checks whether a region break could help a situation where we're out of
/// space for the next row. The criteria are:
///
/// 1. If we could progress at the top of the region, that indicates the
/// region has a backlog, or (if we're at the first region) a region break
/// is at all possible (`regions.last` is `Some()`), so that's sufficient.
///
/// 2. Otherwise, we may progress if another region break is possible
/// (`regions.last` is still `Some()`) and non-repeating rows have been
/// placed, since that means the space they occupy will be available in the
/// next region.
#[inline]
pub fn may_progress_with_repeats(&self) -> bool {
// TODO(subfooters): check below isn't enough to detect non-repeating
// footers... we can also change 'initial_after_repeats' to stop being
// calculated if there were any non-repeating footers.
self.current.could_progress_at_top
|| self.regions.last.is_some()
&& self.regions.size.y != self.current.initial_after_repeats
}
pub fn place_new_headers(
&mut self,
consecutive_header_count: &mut usize,
engine: &mut Engine,
) -> SourceResult<()> {
*consecutive_header_count += 1;
let (consecutive_headers, new_upcoming_headers) =
self.upcoming_headers.split_at(*consecutive_header_count);
if new_upcoming_headers.first().is_some_and(|next_header| {
consecutive_headers.last().is_none_or(|latest_header| {
!latest_header.short_lived
&& next_header.range.start == latest_header.range.end
}) && !next_header.short_lived
}) {
// More headers coming, so wait until we reach them.
return Ok(());
}
self.upcoming_headers = new_upcoming_headers;
*consecutive_header_count = 0;
let [first_header, ..] = consecutive_headers else {
self.flush_orphans();
return Ok(());
};
// Assuming non-conflicting headers sorted by increasing y, this must
// be the header with the lowest level (sorted by increasing levels).
let first_level = first_header.level;
// Stop repeating conflicting headers, even if the new headers are
// short-lived or won't repeat.
//
// If we go to a new region before the new headers fit alongside their
// children (or in general, for short-lived), the old headers should
// not be displayed anymore.
let first_conflicting_pos =
self.repeating_headers.partition_point(|h| h.level < first_level);
self.repeating_headers.truncate(first_conflicting_pos);
// Ensure upcoming rows won't see that these headers will occupy any
// space in future regions anymore.
for removed_height in
self.current.repeating_header_heights.drain(first_conflicting_pos..)
{
self.current.repeating_header_height -= removed_height;
}
// Layout short-lived headers immediately.
if consecutive_headers.last().is_some_and(|h| h.short_lived) {
// No chance of orphans as we're immediately placing conflicting
// headers afterwards, which basically are not headers, for all intents
// and purposes. It is therefore guaranteed that all new headers have
// been placed at least once.
self.flush_orphans();
// Layout each conflicting header independently, without orphan
// prevention (as they don't go into 'pending_headers').
// These headers are short-lived as they are immediately followed by a
// header of the same or lower level, such that they never actually get
// to repeat.
self.layout_new_headers(consecutive_headers, true, engine)?;
} else {
// Let's try to place pending headers at least once.
// This might be a waste as we could generate an orphan and thus have
// to try to place old and new headers all over again, but that happens
// for every new region anyway, so it's rather unavoidable.
let snapshot_created =
self.layout_new_headers(consecutive_headers, false, engine)?;
// Queue the new headers for layout. They will remain in this
// vector due to orphan prevention.
//
// After the first subsequent row is laid out, move to repeating, as
// it's then confirmed the headers won't be moved due to orphan
// prevention anymore.
self.pending_headers = consecutive_headers;
if !snapshot_created {
// Region probably couldn't progress.
//
// Mark new pending headers as final and ensure there isn't a
// snapshot.
self.flush_orphans();
}
}
Ok(())
}
/// Lays out rows belonging to a header, returning the calculated header
/// height only for that header. Indicates to the laid out rows that they
/// should inform their laid out heights if appropriate (auto or fixed
/// size rows only).
#[inline]
fn layout_header_rows(
&mut self,
header: &Header,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<()> {
let header_rows =
self.simulate_header(header, &self.regions, engine, disambiguator)?;
let mut skipped_region = false;
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(header_rows.height + self.footer_height)
&& self.regions.may_progress()
{
// Advance regions without any output until we can place the
// header and the footer.
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
skipped_region = true;
as_short_lived: bool,
) -> SourceResult<Abs> {
let mut header_height = Abs::zero();
for y in header.range.clone() {
header_height += self
.layout_row_with_state(
y,
engine,
disambiguator,
RowState {
current_row_height: Some(Abs::zero()),
in_active_repeatable: !as_short_lived,
},
)?
.current_row_height
.unwrap_or_default();
}
Ok(header_height)
}
/// This function should be called each time an additional row has been
/// laid out in a region to indicate that orphan prevention has succeeded.
///
/// It removes the current orphan snapshot and flushes pending headers,
/// such that a non-repeating header won't try to be laid out again
/// anymore, and a repeating header will begin to be part of
/// `repeating_headers`.
pub fn flush_orphans(&mut self) {
self.current.lrows_orphan_snapshot = None;
self.flush_pending_headers();
}
/// Indicates all currently pending headers have been successfully placed
/// once, since another row has been placed after them, so they are
/// certainly not orphans.
pub fn flush_pending_headers(&mut self) {
if self.pending_headers.is_empty() {
return;
}
// Reset the header height for this region.
// It will be re-calculated when laying out each header row.
self.header_height = Abs::zero();
if let Some(Repeatable::Repeated(footer)) = &self.grid.footer {
if skipped_region {
// Simulate the footer again; the region's 'full' might have
// changed.
self.footer_height = self
.simulate_footer(footer, &self.regions, engine, disambiguator)?
.height;
for header in self.pending_headers {
if header.repeated {
// Vector remains sorted by increasing levels:
// - 'pending_headers' themselves are sorted, since we only
// push non-mutually-conflicting headers at a time.
// - Before pushing new pending headers in
// 'layout_new_pending_headers', we truncate repeating headers
// to remove anything with the same or higher levels as the
// first pending header.
// - Assuming it was sorted before, that truncation only keeps
// elements with a lower level.
// - Therefore, by pushing this header to the end, it will have
// a level larger than all the previous headers, and is thus
// in its 'correct' position.
self.repeating_headers.push(header);
}
}
// Header is unbreakable.
self.pending_headers = Default::default();
}
/// Lays out the rows of repeating and pending headers at the top of the
/// region.
///
/// Assumes the footer height for the current region has already been
/// calculated. Skips regions as necessary to fit all headers and all
/// footers.
pub fn layout_active_headers(&mut self, engine: &mut Engine) -> SourceResult<()> {
// Generate different locations for content in headers across its
// repetitions by assigning a unique number for each one.
let disambiguator = self.finished.len();
let header_height = self.simulate_header_height(
self.repeating_headers
.iter()
.copied()
.chain(self.pending_headers.iter().map(Repeatable::deref)),
&self.regions,
engine,
disambiguator,
)?;
// We already take the footer into account below.
// While skipping regions, footer height won't be automatically
// re-calculated until the end.
let mut skipped_region = false;
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(header_height)
&& self.may_progress_with_repeats()
{
// Advance regions without any output until we can place the
// header and the footer.
self.finish_region_internal(
Frame::soft(Axes::splat(Abs::zero())),
vec![],
Default::default(),
);
// TODO(layout model): re-calculate heights of headers and footers
// on each region if 'full' changes? (Assuming height doesn't
// change for now...)
//
// Would remove the footer height update below (move it here).
skipped_region = true;
self.regions.size.y -= self.current.footer_height;
self.current.initial_after_repeats = self.regions.size.y;
}
if let Some(footer) = &self.grid.footer {
if footer.repeated && skipped_region {
// Simulate the footer again; the region's 'full' might have
// changed.
self.regions.size.y += self.current.footer_height;
self.current.footer_height = self
.simulate_footer(footer, &self.regions, engine, disambiguator)?
.height;
self.regions.size.y -= self.current.footer_height;
}
}
let repeating_header_rows =
total_header_row_count(self.repeating_headers.iter().copied());
let pending_header_rows =
total_header_row_count(self.pending_headers.iter().map(Repeatable::deref));
// Group of headers is unbreakable.
// Thus, no risk of 'finish_region' being recursively called from
// within 'layout_row'.
self.unbreakable_rows_left += header.end;
for y in 0..header.end {
self.layout_row(y, engine, disambiguator)?;
self.unbreakable_rows_left += repeating_header_rows + pending_header_rows;
self.current.last_repeated_header_end =
self.repeating_headers.last().map(|h| h.range.end).unwrap_or_default();
// Reset the header height for this region.
// It will be re-calculated when laying out each header row.
self.current.repeating_header_height = Abs::zero();
self.current.repeating_header_heights.clear();
debug_assert!(self.current.lrows.is_empty());
debug_assert!(self.current.lrows_orphan_snapshot.is_none());
let may_progress = self.may_progress_with_repeats();
if may_progress {
// Enable orphan prevention for headers at the top of the region.
// Otherwise, we will flush pending headers below, after laying
// them out.
//
// It is very rare for this to make a difference as we're usually
// at the 'last' region after the first skip, at which the snapshot
// is handled by 'layout_new_headers'. Either way, we keep this
// here for correctness.
self.current.lrows_orphan_snapshot = Some(self.current.lrows.len());
}
// Use indices to avoid double borrow. We don't mutate headers in
// 'layout_row' so this is fine.
let mut i = 0;
while let Some(&header) = self.repeating_headers.get(i) {
let header_height =
self.layout_header_rows(header, engine, disambiguator, false)?;
self.current.repeating_header_height += header_height;
// We assume that this vector will be sorted according
// to increasing levels like 'repeating_headers' and
// 'pending_headers' - and, in particular, their union, as this
// vector is pushed repeating heights from both.
//
// This is guaranteed by:
// 1. We always push pending headers after repeating headers,
// as we assume they don't conflict because we remove
// conflicting repeating headers when pushing a new pending
// header.
//
// 2. We push in the same order as each.
//
// 3. This vector is also modified when pushing a new pending
// header, where we remove heights for conflicting repeating
// headers which have now stopped repeating. They are always at
// the end and new pending headers respect the existing sort,
// so the vector will remain sorted.
self.current.repeating_header_heights.push(header_height);
i += 1;
}
self.current.repeated_header_rows = self.current.lrows.len();
self.current.initial_after_repeats = self.regions.size.y;
let mut has_non_repeated_pending_header = false;
for header in self.pending_headers {
if !header.repeated {
self.current.initial_after_repeats = self.regions.size.y;
has_non_repeated_pending_header = true;
}
let header_height =
self.layout_header_rows(header, engine, disambiguator, false)?;
if header.repeated {
self.current.repeating_header_height += header_height;
self.current.repeating_header_heights.push(header_height);
}
}
if !has_non_repeated_pending_header {
self.current.initial_after_repeats = self.regions.size.y;
}
if !may_progress {
// Flush pending headers immediately, as placing them again later
// won't help.
self.flush_orphans();
}
Ok(())
}
/// Lays out headers found for the first time during row layout.
///
/// If 'short_lived' is true, these headers are immediately followed by
/// a conflicting header, so it is assumed they will not be pushed to
/// pending headers.
///
/// Returns whether orphan prevention was successfully setup, or couldn't
/// due to short-lived headers or the region couldn't progress.
pub fn layout_new_headers(
&mut self,
headers: &'a [Repeatable<Header>],
short_lived: bool,
engine: &mut Engine,
) -> SourceResult<bool> {
// At first, only consider the height of the given headers. However,
// for upcoming regions, we will have to consider repeating headers as
// well.
let header_height = self.simulate_header_height(
headers.iter().map(Repeatable::deref),
&self.regions,
engine,
0,
)?;
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(header_height)
&& self.may_progress_with_repeats()
{
// Note that, after the first region skip, the new headers will go
// at the top of the region, but after the repeating headers that
// remained (which will be automatically placed in 'finish_region').
self.finish_region(engine, false)?;
}
// Remove new headers at the end of the region if the upcoming row
// doesn't fit.
// TODO(subfooters): what if there is a footer right after it?
let should_snapshot = !short_lived
&& self.current.lrows_orphan_snapshot.is_none()
&& self.may_progress_with_repeats();
if should_snapshot {
// If we don't enter this branch while laying out non-short lived
// headers, that means we will have to immediately flush pending
// headers and mark them as final, since trying to place them in
// the next page won't help get more space.
self.current.lrows_orphan_snapshot = Some(self.current.lrows.len());
}
let mut at_top = self.regions.size.y == self.current.initial_after_repeats;
self.unbreakable_rows_left +=
total_header_row_count(headers.iter().map(Repeatable::deref));
for header in headers {
let header_height = self.layout_header_rows(header, engine, 0, false)?;
// Only store this header height if it is actually going to
// become a pending header. Otherwise, pretend it's not a
// header... This is fine for consumers of 'header_height' as
// it is guaranteed this header won't appear in a future
// region, so multi-page rows and cells can effectively ignore
// this header.
if !short_lived && header.repeated {
self.current.repeating_header_height += header_height;
self.current.repeating_header_heights.push(header_height);
if at_top {
self.current.initial_after_repeats = self.regions.size.y;
}
} else {
at_top = false;
}
}
Ok(should_snapshot)
}
/// Calculates the total expected height of several headers.
pub fn simulate_header_height<'h: 'a>(
&self,
headers: impl IntoIterator<Item = &'h Header>,
regions: &Regions<'_>,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<Abs> {
let mut height = Abs::zero();
for header in headers {
height +=
self.simulate_header(header, regions, engine, disambiguator)?.height;
}
Ok(height)
}
/// Simulate the header's group of rows.
pub fn simulate_header(
&self,
@ -66,8 +455,8 @@ impl GridLayouter<'_> {
// assume that the amount of unbreakable rows following the first row
// in the header will be precisely the rows in the header.
self.simulate_unbreakable_row_group(
0,
Some(header.end),
header.range.start,
Some(header.range.end - header.range.start),
regions,
engine,
disambiguator,
@ -91,11 +480,22 @@ impl GridLayouter<'_> {
{
// Advance regions without any output until we can place the
// footer.
self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]);
self.finish_region_internal(
Frame::soft(Axes::splat(Abs::zero())),
vec![],
Default::default(),
);
skipped_region = true;
}
self.footer_height = if skipped_region {
// TODO(subfooters): Consider resetting header height etc. if we skip
// region. (Maybe move that step to `finish_region_internal`.)
//
// That is unnecessary at the moment as 'prepare_footers' is only
// called at the start of the region, so header height is always zero
// and no headers were placed so far, but what about when we can have
// footers in the middle of the region? Let's think about this then.
self.current.footer_height = if skipped_region {
// Simulate the footer again; the region's 'full' might have
// changed.
self.simulate_footer(footer, &self.regions, engine, disambiguator)?
@ -118,12 +518,22 @@ impl GridLayouter<'_> {
// Ensure footer rows have their own height available.
// Won't change much as we're creating an unbreakable row group
// anyway, so this is mostly for correctness.
self.regions.size.y += self.footer_height;
self.regions.size.y += self.current.footer_height;
let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated);
let footer_len = self.grid.rows.len() - footer.start;
self.unbreakable_rows_left += footer_len;
for y in footer.start..self.grid.rows.len() {
self.layout_row(y, engine, disambiguator)?;
self.layout_row_with_state(
y,
engine,
disambiguator,
RowState {
in_active_repeatable: repeats,
..Default::default()
},
)?;
}
Ok(())
@ -144,10 +554,18 @@ impl GridLayouter<'_> {
// in the footer will be precisely the rows in the footer.
self.simulate_unbreakable_row_group(
footer.start,
Some(self.grid.rows.len() - footer.start),
Some(footer.end - footer.start),
regions,
engine,
disambiguator,
)
}
}
/// The total amount of rows in the given list of headers.
#[inline]
pub fn total_header_row_count<'h>(
headers: impl IntoIterator<Item = &'h Header>,
) -> usize {
headers.into_iter().map(|h| h.range.end - h.range.start).sum()
}

View File

@ -4,7 +4,7 @@ use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::layouter::{points, Row};
use super::{layout_cell, Cell, GridLayouter};
/// All information needed to layout a single rowspan.
@ -90,10 +90,10 @@ pub struct CellMeasurementData<'layouter> {
impl GridLayouter<'_> {
/// Layout a rowspan over the already finished regions, plus the current
/// region's frame and resolved rows, if it wasn't finished yet (because
/// we're being called from `finish_region`, but note that this function is
/// also called once after all regions are finished, in which case
/// `current_region_data` is `None`).
/// region's frame and height of resolved header rows, if it wasn't
/// finished yet (because we're being called from `finish_region`, but note
/// that this function is also called once after all regions are finished,
/// in which case `current_region_data` is `None`).
///
/// We need to do this only once we already know the heights of all
/// spanned rows, which is only possible after laying out the last row
@ -101,7 +101,7 @@ impl GridLayouter<'_> {
pub fn layout_rowspan(
&mut self,
rowspan_data: Rowspan,
current_region_data: Option<(&mut Frame, &[RowPiece])>,
current_region_data: Option<(&mut Frame, Abs)>,
engine: &mut Engine,
) -> SourceResult<()> {
let Rowspan {
@ -146,11 +146,31 @@ impl GridLayouter<'_> {
// Push the layouted frames directly into the finished frames.
let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self
let (current_region, current_header_row_height) = current_region_data.unzip();
// Clever trick to process finished header rows:
// - If there are grid headers, the vector will be filled with one
// finished header row height per region, so, chaining with the height
// for the current one, we get the header row height for each region.
//
// - But if there are no grid headers, the vector will be empty, so in
// theory the regions and resolved header row heights wouldn't match.
// But that's fine - 'current_header_row_height' can only be either
// 'Some(zero)' or 'None' in such a case, and for all other rows we
// append infinite zeros. That is, in such a case, the resolved header
// row height is always zero, so that's our fallback.
let finished_header_rows = self
.finished_header_rows
.iter()
.map(|info| info.repeated_height)
.chain(current_header_row_height)
.chain(std::iter::repeat(Abs::zero()));
for ((i, (finished, header_dy)), frame) in self
.finished
.iter_mut()
.chain(current_region.into_iter())
.zip(finished_header_rows)
.skip(first_region)
.enumerate()
.zip(fragment)
@ -162,22 +182,9 @@ impl GridLayouter<'_> {
} else {
// The rowspan continuation starts after the header (thus,
// at a position after the sum of the laid out header
// rows).
if let Some(Repeatable::Repeated(header)) = &self.grid.header {
let header_rows = self
.rrows
.get(i)
.map(Vec::as_slice)
.or(current_rrows)
.unwrap_or(&[])
.iter()
.take_while(|row| row.y < header.end);
header_rows.map(|row| row.height).sum()
} else {
// Without a header, start at the very top of the region.
Abs::zero()
}
// rows). Without a header, this is zero, so the rowspan can
// start at the very top of the region as usual.
header_dy
};
finished.push_frame(Point::new(dx, dy), frame);
@ -231,15 +238,13 @@ impl GridLayouter<'_> {
// current row is dynamic and depends on the amount of upcoming
// unbreakable cells (with or without a rowspan setting).
let mut amount_unbreakable_rows = None;
if let Some(Repeatable::NotRepeated(header)) = &self.grid.header {
if current_row < header.end {
// Non-repeated header, so keep it unbreakable.
amount_unbreakable_rows = Some(header.end);
}
}
if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer {
if current_row >= footer.start {
if let Some(footer) = &self.grid.footer {
if !footer.repeated && current_row >= footer.start {
// Non-repeated footer, so keep it unbreakable.
//
// TODO(subfooters): This will become unnecessary
// once non-repeated footers are treated differently and
// have widow prevention.
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
}
}
@ -254,10 +259,7 @@ impl GridLayouter<'_> {
// Skip to fitting region.
while !self.regions.size.y.fits(row_group.height)
&& !in_last_with_offset(
self.regions,
self.header_height + self.footer_height,
)
&& self.may_progress_with_repeats()
{
self.finish_region(engine, false)?;
}
@ -396,16 +398,29 @@ impl GridLayouter<'_> {
// auto rows don't depend on the backlog, as they only span one
// region.
if breakable
&& (matches!(self.grid.header, Some(Repeatable::Repeated(_)))
|| matches!(self.grid.footer, Some(Repeatable::Repeated(_))))
&& (!self.repeating_headers.is_empty()
|| !self.pending_headers.is_empty()
|| matches!(&self.grid.footer, Some(footer) if footer.repeated))
{
// Subtract header and footer height from all upcoming regions
// when measuring the cell, including the last repeated region.
//
// This will update the 'custom_backlog' vector with the
// updated heights of the upcoming regions.
//
// We predict that header height will only include that of
// repeating headers, as we can assume non-repeating headers in
// the first region have been successfully placed, unless
// something didn't fit on the first region of the auto row,
// but we will only find that out after measurement, and if
// that happens, we discard the measurement and try again.
let mapped_regions = self.regions.map(&mut custom_backlog, |size| {
Size::new(size.x, size.y - self.header_height - self.footer_height)
Size::new(
size.x,
size.y
- self.current.repeating_header_height
- self.current.footer_height,
)
});
// Callees must use the custom backlog instead of the current
@ -459,6 +474,7 @@ impl GridLayouter<'_> {
// Height of the rowspan covered by spanned rows in the current
// region.
let laid_out_height: Abs = self
.current
.lrows
.iter()
.filter_map(|row| match row {
@ -506,7 +522,12 @@ impl GridLayouter<'_> {
.iter()
.copied()
.chain(std::iter::once(if breakable {
self.initial.y - self.header_height - self.footer_height
// Here we are calculating the available height for a
// rowspan from the top of the current region, so
// we have to use initial header heights (note that
// header height can change in the middle of the
// region).
self.current.initial_after_repeats
} else {
// When measuring unbreakable auto rows, infinite
// height is available for content to expand.
@ -518,11 +539,13 @@ impl GridLayouter<'_> {
// rowspan's already laid out heights with the current
// region's height and current backlog to ensure a good
// level of accuracy in the measurements.
let backlog = self
.regions
.backlog
.iter()
.map(|&size| size - self.header_height - self.footer_height);
//
// Assume only repeating headers will survive starting at
// the next region.
let backlog = self.regions.backlog.iter().map(|&size| {
size - self.current.repeating_header_height
- self.current.footer_height
});
heights_up_to_current_region.chain(backlog).collect::<Vec<_>>()
} else {
@ -536,10 +559,10 @@ impl GridLayouter<'_> {
height = *rowspan_height;
backlog = None;
full = rowspan_full;
last = self
.regions
.last
.map(|size| size - self.header_height - self.footer_height);
last = self.regions.last.map(|size| {
size - self.current.repeating_header_height
- self.current.footer_height
});
} else {
// The rowspan started in the current region, as its vector
// of heights in regions is currently empty.
@ -741,10 +764,11 @@ impl GridLayouter<'_> {
simulated_regions.next();
disambiguator += 1;
// Subtract the initial header and footer height, since that's the
// height we used when subtracting from the region backlog's
// Subtract the repeating header and footer height, since that's
// the height we used when subtracting from the region backlog's
// heights while measuring cells.
simulated_regions.size.y -= self.header_height + self.footer_height;
simulated_regions.size.y -=
self.current.repeating_header_height + self.current.footer_height;
}
if let Some(original_last_resolved_size) = last_resolved_size {
@ -876,12 +900,8 @@ impl GridLayouter<'_> {
// which, when used and combined with upcoming spanned rows, covers all
// of the requested rowspan height, we give up.
for _attempt in 0..5 {
let rowspan_simulator = RowspanSimulator::new(
disambiguator,
simulated_regions,
self.header_height,
self.footer_height,
);
let rowspan_simulator =
RowspanSimulator::new(disambiguator, simulated_regions, &self.current);
let total_spanned_height = rowspan_simulator.simulate_rowspan_layout(
y,
@ -963,7 +983,8 @@ impl GridLayouter<'_> {
{
extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero());
simulated_regions.next();
simulated_regions.size.y -= self.header_height + self.footer_height;
simulated_regions.size.y -=
self.current.repeating_header_height + self.current.footer_height;
disambiguator += 1;
}
simulated_regions.size.y -= extra_amount_to_grow;
@ -980,10 +1001,17 @@ struct RowspanSimulator<'a> {
finished: usize,
/// The state of regions during the simulation.
regions: Regions<'a>,
/// The height of the header in the currently simulated region.
/// The total height of headers in the currently simulated region.
header_height: Abs,
/// The height of the footer in the currently simulated region.
/// The total height of footers in the currently simulated region.
footer_height: Abs,
/// Whether `self.regions.may_progress()` was `true` at the top of the
/// region, indicating we can progress anywhere in the current region,
/// even right after a repeated header.
could_progress_at_top: bool,
/// Available height after laying out repeated headers at the top of the
/// currently simulated region.
initial_after_repeats: Abs,
/// The total spanned height so far in the simulation.
total_spanned_height: Abs,
/// Height of the latest spanned gutter row in the simulation.
@ -997,14 +1025,19 @@ impl<'a> RowspanSimulator<'a> {
fn new(
finished: usize,
regions: Regions<'a>,
header_height: Abs,
footer_height: Abs,
current: &super::layouter::Current,
) -> Self {
Self {
finished,
regions,
header_height,
footer_height,
// There can be no new headers or footers within a multi-page
// rowspan, since headers and footers are unbreakable, so
// assuming the repeating header height and footer height
// won't change is safe.
header_height: current.repeating_header_height,
footer_height: current.footer_height,
could_progress_at_top: current.could_progress_at_top,
initial_after_repeats: current.initial_after_repeats,
total_spanned_height: Abs::zero(),
latest_spanned_gutter_height: Abs::zero(),
}
@ -1053,10 +1086,7 @@ impl<'a> RowspanSimulator<'a> {
0,
)?;
while !self.regions.size.y.fits(row_group.height)
&& !in_last_with_offset(
self.regions,
self.header_height + self.footer_height,
)
&& self.may_progress_with_repeats()
{
self.finish_region(layouter, engine)?;
}
@ -1078,10 +1108,7 @@ impl<'a> RowspanSimulator<'a> {
let mut skipped_region = false;
while unbreakable_rows_left == 0
&& !self.regions.size.y.fits(height)
&& !in_last_with_offset(
self.regions,
self.header_height + self.footer_height,
)
&& self.may_progress_with_repeats()
{
self.finish_region(layouter, engine)?;
@ -1127,23 +1154,37 @@ impl<'a> RowspanSimulator<'a> {
// our simulation checks what happens AFTER the auto row, so we can
// just use the original backlog from `self.regions`.
let disambiguator = self.finished;
let header_height =
if let Some(Repeatable::Repeated(header)) = &layouter.grid.header {
layouter
.simulate_header(header, &self.regions, engine, disambiguator)?
.height
} else {
Abs::zero()
};
let footer_height =
if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer {
layouter
.simulate_footer(footer, &self.regions, engine, disambiguator)?
.height
} else {
Abs::zero()
};
let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty()
|| !layouter.pending_headers.is_empty()
{
// Only repeating headers have survived after the first region
// break.
let repeating_headers = layouter.repeating_headers.iter().copied().chain(
layouter.pending_headers.iter().filter_map(Repeatable::as_repeated),
);
let header_height = layouter.simulate_header_height(
repeating_headers.clone(),
&self.regions,
engine,
disambiguator,
)?;
(Some(repeating_headers), header_height)
} else {
(None, Abs::zero())
};
let footer_height = if let Some(footer) =
layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
{
layouter
.simulate_footer(footer, &self.regions, engine, disambiguator)?
.height
} else {
Abs::zero()
};
let mut skipped_region = false;
@ -1156,19 +1197,24 @@ impl<'a> RowspanSimulator<'a> {
skipped_region = true;
}
if let Some(Repeatable::Repeated(header)) = &layouter.grid.header {
if let Some(repeating_headers) = repeating_headers {
self.header_height = if skipped_region {
// Simulate headers again, at the new region, as
// the full region height may change.
layouter
.simulate_header(header, &self.regions, engine, disambiguator)?
.height
layouter.simulate_header_height(
repeating_headers,
&self.regions,
engine,
disambiguator,
)?
} else {
header_height
};
}
if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer {
if let Some(footer) =
layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
{
self.footer_height = if skipped_region {
// Simulate footers again, at the new region, as
// the full region height may change.
@ -1185,6 +1231,7 @@ impl<'a> RowspanSimulator<'a> {
// header or footer (as an invariant, any rowspans spanning any header
// or footer rows are fully contained within that header's or footer's rows).
self.regions.size.y -= self.header_height + self.footer_height;
self.initial_after_repeats = self.regions.size.y;
Ok(())
}
@ -1201,8 +1248,18 @@ impl<'a> RowspanSimulator<'a> {
self.regions.next();
self.finished += 1;
self.could_progress_at_top = self.regions.may_progress();
self.simulate_header_footer_layout(layouter, engine)
}
/// Similar to [`GridLayouter::may_progress_with_repeats`] but for rowspan
/// simulation.
#[inline]
fn may_progress_with_repeats(&self) -> bool {
self.could_progress_at_top
|| self.regions.last.is_some()
&& self.regions.size.y != self.initial_after_repeats
}
}
/// Subtracts some size from the end of a vector of sizes.

View File

@ -1,6 +1,6 @@
use std::ffi::OsStr;
use typst_library::diag::{warning, At, SourceResult, StrResult};
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
@ -27,17 +27,17 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let Derived { source, derived: data } = &elem.source;
let format = match elem.format(styles) {
let Derived { source, derived: loaded } = &elem.source;
let format = match elem.format.get(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(source, data).at(span)?,
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 =
data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
if has_foreign_object {
engine.sink.warn(warning!(
@ -53,23 +53,23 @@ pub fn layout_image(
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
data.clone(),
loaded.data.clone(),
format,
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
data.clone(),
loaded.data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.at(span)?,
.within(loaded)?,
),
};
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles));
// Determine the image's pixel aspect ratio.
let pxw = image.width();
@ -106,7 +106,7 @@ pub fn layout_image(
};
// Compute the actual size of the fitted image.
let fit = elem.fit(styles);
let fit = elem.fit.get(styles);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) {
@ -147,6 +147,7 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
"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()),
_ => {}
}
}

View File

@ -21,15 +21,15 @@ pub fn layout_box(
region: Size,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
let width = elem.width.get(styles);
let height = elem.height.get(styles);
let inset = elem.inset.resolve(styles).unwrap_or_default();
// Build the pod region.
let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
// 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,
// its size will be adjusted below.
None => Frame::hard(Size::zero()),
@ -50,18 +50,19 @@ pub fn layout_box(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill = elem.fill.get_cloned(styles);
let stroke = elem
.stroke(styles)
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
if elem.clip.get(styles) {
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
// inset, so that a relative shift is resolved relative to the final
// height.
let shift = elem.baseline(styles).relative_to(frame.height());
let shift = elem.baseline.resolve(styles).relative_to(frame.height());
if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift);
}

View File

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

View File

@ -2,10 +2,11 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine;
use typst_library::foundations::Resolve;
use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem};
use typst_library::text::{variant, Lang, TextElem};
use typst_utils::Numeric;
use super::*;
@ -219,7 +220,7 @@ fn collect_items<'a>(
// Add fallback text to expand the line height, if necessary.
if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback {
items.push(fallback);
items.push(fallback, usize::MAX);
}
}
@ -270,10 +271,10 @@ fn collect_range<'a>(
items: &mut Items<'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.
let Item::Text(shaped) = item else {
items.push(item);
items.push(item, idx);
continue;
};
@ -293,10 +294,10 @@ fn collect_range<'a>(
} else if split {
// When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped));
items.push(Item::Text(reshaped), idx);
} else {
// 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 shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size);
shaped.width -= shrink.at(glyph.size);
} else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& 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_offset = 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 punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size);
shaped.width -= shrink.at(punct.size);
} else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& (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();
glyph.x_advance -= shrink;
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.
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
/// Apply the current baseline shift and italic compensation to a frame.
pub fn apply_shift<'a>(
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.
@ -441,10 +464,10 @@ pub fn commit(
if let Some(Item::Text(text)) = line.items.first() {
if let Some(glyph) = text.glyphs.first() {
if !text.dir.is_positive()
&& TextElem::overhang_in(text.styles)
&& text.styles.get(TextElem::overhang)
&& (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;
remaining += amount;
}
@ -455,10 +478,10 @@ pub fn commit(
if let Some(Item::Text(text)) = line.items.last() {
if let Some(glyph) = text.glyphs.last() {
if text.dir.is_positive()
&& TextElem::overhang_in(text.styles)
&& text.styles.get(TextElem::overhang)
&& (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;
}
}
@ -499,16 +522,16 @@ pub fn commit(
// Build the frames and determine the height and baseline.
let mut frames = vec![];
for item in line.items.iter() {
let mut push = |offset: &mut Abs, frame: Frame| {
for &(idx, ref item) in line.items.indexed_iter() {
let mut push = |offset: &mut Abs, frame: Frame, idx: usize| {
let width = frame.width();
top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame));
frames.push((*offset, frame, idx));
*offset += width;
};
match item {
match &**item {
Item::Absolute(v, _) => {
offset += *v;
}
@ -519,8 +542,8 @@ pub fn commit(
let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), styles, region)
})?;
apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame);
apply_shift(&engine.world, &mut frame, *styles);
push(&mut offset, frame, idx);
} else {
offset += amount;
}
@ -532,15 +555,15 @@ pub fn commit(
justification_ratio,
extra_justification,
);
push(&mut offset, frame);
push(&mut offset, frame, idx);
}
Item::Frame(frame) => {
push(&mut offset, frame.clone());
push(&mut offset, frame.clone(), idx);
}
Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame));
frames.push((offset, frame, idx));
}
Item::Skip(_) => {}
}
@ -559,8 +582,13 @@ pub fn commit(
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.
for (offset, frame) in frames {
for (offset, frame, _) in frames {
let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame);
@ -627,7 +655,7 @@ fn overhang(c: char) -> f64 {
}
/// 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> {
/// Create empty items.
@ -636,33 +664,38 @@ impl<'a> Items<'a> {
}
/// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) {
self.0.push(entry.into());
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>, idx: usize) {
self.0.push((idx, entry.into()));
}
/// Iterate over the items
/// Iterate over the items.
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.
pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item)
self.0.first().map(|(_, item)| &**item)
}
/// Access the last item.
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.
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.
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.
@ -673,12 +706,12 @@ impl<'a> Items<'a> {
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
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> {
type Target = Vec<ItemEntry<'a>>;
type Target = Vec<(usize, ItemEntry<'a>)>;
fn deref(&self) -> &Self::Target {
&self.0
@ -698,6 +731,10 @@ impl Debug for Items<'_> {
}
/// 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> {
Ref(&'a Item<'a>),
Box(Box<Item<'a>>),

View File

@ -846,7 +846,9 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.config.hyphenate.unwrap_or_else(|| {
let (_, item) = p.get(offset);
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,
}
})
@ -857,7 +859,7 @@ fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset);
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()?;
@ -927,9 +929,9 @@ impl Estimates {
let byte_len = g.range.len();
let stretch = g.stretchability().0 + g.stretchability().1;
let shrink = g.shrinkability().0 + g.shrinkability().1;
widths.push(byte_len, g.x_advance.at(shaped.size));
stretchability.push(byte_len, stretch.at(shaped.size));
shrinkability.push(byte_len, shrink.at(shaped.size));
widths.push(byte_len, g.x_advance.at(g.size));
stretchability.push(byte_len, stretch.at(g.size));
shrinkability.push(byte_len, shrink.at(g.size));
justifiables.push(byte_len, g.is_justifiable() as usize);
}
} else {

View File

@ -9,11 +9,12 @@ mod prepare;
mod shaping;
pub use self::box_::layout_box;
pub use self::shaping::create_shape_plan;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
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::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::{
@ -28,7 +29,7 @@ use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
use self::finalize::finalize;
use self::line::{apply_baseline_shift, commit, line, Line};
use self::line::{apply_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation};
use self::shaping::{
@ -112,10 +113,10 @@ fn layout_par_impl(
expand,
Some(situation),
&ConfigBase {
justify: elem.justify(styles),
linebreaks: elem.linebreaks(styles),
first_line_indent: elem.first_line_indent(styles),
hanging_indent: elem.hanging_indent(styles),
justify: elem.justify.get(styles),
linebreaks: elem.linebreaks.get(styles),
first_line_indent: elem.first_line_indent.get(styles),
hanging_indent: elem.hanging_indent.resolve(styles),
},
)
}
@ -138,10 +139,10 @@ pub fn layout_inline<'a>(
expand,
None,
&ConfigBase {
justify: ParElem::justify_in(shared),
linebreaks: ParElem::linebreaks_in(shared),
first_line_indent: ParElem::first_line_indent_in(shared),
hanging_indent: ParElem::hanging_indent_in(shared),
justify: shared.get(ParElem::justify),
linebreaks: shared.get(ParElem::linebreaks),
first_line_indent: shared.get(ParElem::first_line_indent),
hanging_indent: shared.resolve(ParElem::hanging_indent),
},
)
}
@ -183,8 +184,8 @@ fn configuration(
situation: Option<ParSituation>,
) -> Config {
let justify = base.justify;
let font_size = TextElem::size_in(shared);
let dir = TextElem::dir_in(shared);
let font_size = shared.resolve(TextElem::size);
let dir = shared.resolve(TextElem::dir);
Config {
justify,
@ -206,7 +207,7 @@ fn configuration(
Some(ParSituation::Other) => all,
None => false,
}
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
&& shared.resolve(AlignElem::alignment).x == dir.start().into()
{
amount.at(font_size)
} else {
@ -218,26 +219,26 @@ fn configuration(
} else {
Abs::zero()
},
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
numbering_marker: shared.get_cloned(ParLine::numbering).map(|numbering| {
Packed::new(ParLineMarker::new(
numbering,
ParLine::number_align_in(shared),
ParLine::number_margin_in(shared),
shared.get(ParLine::number_align),
shared.get(ParLine::number_margin),
// Delay resolving the number clearance until line numbers are
// laid out to avoid inconsistent spacing depending on varying
// 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,
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)),
lang: shared_get(children, shared, TextElem::lang_in),
fallback: TextElem::fallback_in(shared),
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
costs: TextElem::costs_in(shared),
lang: shared_get(children, shared, |s| s.get(TextElem::lang)),
fallback: shared.get(TextElem::fallback),
cjk_latin_spacing: shared.get(TextElem::cjk_latin_spacing).is_auto(),
costs: shared.get(TextElem::costs),
}
}
@ -313,7 +314,7 @@ fn shared_get<T: PartialEq>(
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
styles.get(ListElem::depth).0 > 0
|| !styles.get_cloned(EnumElem::parents).is_empty()
|| 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.
glyph.x_advance += Em::new(0.25);
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
@ -152,7 +152,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
glyph.x_advance += Em::new(0.25);
glyph.x_offset += Em::new(0.25);
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);

View File

@ -1,18 +1,17 @@
use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::str::FromStr;
use std::sync::Arc;
use az::SaturatingAs;
use ecow::EcoString;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer};
use ttf_parser::gsub::SubstitutionSubtable;
use ttf_parser::Tag;
use typst_library::engine::Engine;
use typst_library::foundations::{Smart, StyleChain};
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{
families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant,
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
families, features, is_default_ignorable, language, variant, Font, FontFamily,
FontVariant, Glyph, Lang, Region, ShiftSettings, TextEdgeBounds, TextElem, TextItem,
};
use typst_library::World;
use typst_utils::SliceExt;
@ -43,8 +42,6 @@ pub struct ShapedText<'a> {
pub styles: StyleChain<'a>,
/// The font variant.
pub variant: FontVariant,
/// The font size.
pub size: Abs,
/// The width of the text's bounding box.
pub width: Abs,
/// The shaped glyphs.
@ -64,6 +61,8 @@ pub struct ShapedGlyph {
pub x_offset: Em,
/// The vertical offset of the glyph.
pub y_offset: Em,
/// The font size for the glyph.
pub size: Abs,
/// The adjustability of the glyph.
pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full inline layout. A
@ -224,14 +223,17 @@ impl<'a> ShapedText<'a> {
let mut frame = Frame::soft(size);
frame.set_baseline(top);
let shift = TextElem::baseline_in(self.styles);
let decos = TextElem::deco_in(self.styles);
let fill = TextElem::fill_in(self.styles);
let stroke = TextElem::stroke_in(self.styles);
let span_offset = TextElem::span_offset_in(self.styles);
let size = self.styles.resolve(TextElem::size);
let shift = self.styles.resolve(TextElem::baseline);
let decos = self.styles.get_cloned(TextElem::deco);
let fill = self.styles.get_ref(TextElem::fill);
let stroke = self.styles.resolve(TextElem::stroke);
let span_offset = self.styles.get(TextElem::span_offset);
for ((font, y_offset), group) in
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
for ((font, y_offset, glyph_size), group) in self
.glyphs
.as_ref()
.group_by_key(|g| (g.font.clone(), g.y_offset, g.size))
{
let mut range = group[0].range.clone();
for glyph in group {
@ -239,7 +241,7 @@ impl<'a> ShapedText<'a> {
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
.iter()
.map(|shaped: &ShapedGlyph| {
@ -259,11 +261,11 @@ impl<'a> ShapedText<'a> {
adjustability_right * justification_ratio;
if shaped.is_justifiable() {
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)
+ justification_right.at(self.size);
frame.size_mut().x += justification_left.at(glyph_size)
+ justification_right.at(glyph_size);
// We may not be able to reach the offset completely if
// it exceeds u16, but better to have a roughly correct
@ -295,6 +297,8 @@ impl<'a> ShapedText<'a> {
+ justification_left
+ justification_right,
x_offset: shaped.x_offset + justification_left,
y_advance: Em::zero(),
y_offset: Em::zero(),
range: (shaped.range.start - range.start).saturating_as()
..(shaped.range.end - range.start).saturating_as(),
span,
@ -304,7 +308,7 @@ impl<'a> ShapedText<'a> {
let item = TextItem {
font,
size: self.size,
size: glyph_size,
lang: self.lang,
region: self.region,
fill: fill.clone(),
@ -336,12 +340,13 @@ impl<'a> ShapedText<'a> {
let mut top = Abs::zero();
let mut bottom = Abs::zero();
let top_edge = TextElem::top_edge_in(self.styles);
let bottom_edge = TextElem::bottom_edge_in(self.styles);
let size = self.styles.resolve(TextElem::size);
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.
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);
bottom.set_max(b);
};
@ -388,18 +393,16 @@ impl<'a> ShapedText<'a> {
pub fn stretchability(&self) -> Abs {
self.glyphs
.iter()
.map(|g| g.stretchability().0 + g.stretchability().1)
.sum::<Em>()
.at(self.size)
.map(|g| (g.stretchability().0 + g.stretchability().1).at(g.size))
.sum()
}
/// The shrinkability of the text
pub fn shrinkability(&self) -> Abs {
self.glyphs
.iter()
.map(|g| g.shrinkability().0 + g.shrinkability().1)
.sum::<Em>()
.at(self.size)
.map(|g| (g.shrinkability().0 + g.shrinkability().1).at(g.size))
.sum()
}
/// Reshape a range of the shaped text, reusing information from this
@ -418,9 +421,8 @@ impl<'a> ShapedText<'a> {
lang: self.lang,
region: self.region,
styles: self.styles,
size: self.size,
variant: self.variant,
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
width: glyphs_width(glyphs),
glyphs: Cow::Borrowed(glyphs),
}
} else {
@ -484,13 +486,15 @@ impl<'a> ShapedText<'a> {
// that subtracting either of the endpoints by self.base doesn't
// underflow. See <https://github.com/typst/typst/issues/2283>.
.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 {
font,
glyph_id: glyph_id.0,
x_advance,
x_offset: Em::zero(),
y_offset: Em::zero(),
size,
adjustability: Adjustability::default(),
range,
safe_to_break: true,
@ -599,9 +603,9 @@ pub fn shape_range<'a>(
range: Range,
styles: StyleChain<'a>,
) {
let script = TextElem::script_in(styles);
let lang = TextElem::lang_in(styles);
let region = TextElem::region_in(styles);
let script = styles.get(TextElem::script);
let lang = styles.get(TextElem::lang);
let region = styles.get(TextElem::region);
let mut process = |range: Range, level: BidiLevel| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped =
@ -665,7 +669,8 @@ fn shape<'a>(
lang: Lang,
region: Option<Region>,
) -> 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 {
engine,
size,
@ -674,8 +679,9 @@ fn shape<'a>(
styles,
variant: variant(styles),
features: features(styles),
fallback: TextElem::fallback_in(styles),
fallback: styles.get(TextElem::fallback),
dir,
shift_settings,
};
if !text.is_empty() {
@ -698,12 +704,17 @@ fn shape<'a>(
region,
styles,
variant: ctx.variant,
size,
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
width: glyphs_width(&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.
struct ShapingContext<'a, 'v> {
engine: &'a Engine<'v>,
@ -715,6 +726,7 @@ struct ShapingContext<'a, 'v> {
features: Vec<rustybuzz::Feature>,
fallback: bool,
dir: Dir,
shift_settings: Option<ShiftSettings>,
}
/// Shape text with font fallback using the `families` iterator.
@ -771,7 +783,7 @@ fn shape_segment<'a>(
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
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()))
}) {
buffer.set_script(script)
@ -789,6 +801,18 @@ fn shape_segment<'a>(
// text extraction.
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,
// and features, but is independent from the text and can thus be memoized.
let plan = create_shape_plan(
@ -799,6 +823,10 @@ fn shape_segment<'a>(
&ctx.features,
);
if has_shift_feature {
ctx.features.pop();
}
// Shape!
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
let infos = buffer.glyph_infos();
@ -869,8 +897,9 @@ fn shape_segment<'a>(
glyph_id: info.glyph_id as u16,
// TODO: Don't ignore y_advance.
x_advance,
x_offset: font.to_em(pos[i].x_offset),
y_offset: font.to_em(pos[i].y_offset),
x_offset: font.to_em(pos[i].x_offset) + script_compensation,
y_offset: font.to_em(pos[i].y_offset) + script_shift,
size: scale.at(ctx.size),
adjustability: Adjustability::default(),
range: start..end,
safe_to_break: !info.unsafe_to_break(),
@ -932,9 +961,67 @@ fn shape_segment<'a>(
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.
#[comemo::memoize]
fn create_shape_plan(
pub fn create_shape_plan(
font: &Font,
direction: rustybuzz::Direction,
script: rustybuzz::Script,
@ -952,7 +1039,7 @@ fn create_shape_plan(
/// Shape the text with tofus from the given font.
fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
let x_advance = font.advance(0).unwrap_or_default();
let x_advance = font.x_advance(0).unwrap_or_default();
let add_glyph = |(cluster, c): (usize, char)| {
let start = base + cluster;
let end = start + c.len_utf8();
@ -963,6 +1050,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
x_advance,
x_offset: Em::zero(),
y_offset: Em::zero(),
size: ctx.size,
adjustability: Adjustability::default(),
range: start..end,
safe_to_break: true,
@ -985,9 +1073,11 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
/// Apply tracking and spacing to the shaped glyphs.
fn track_and_space(ctx: &mut ShapingContext) {
let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size);
let spacing =
TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size));
let tracking = Em::from_abs(ctx.styles.resolve(TextElem::tracking), ctx.size);
let spacing = ctx
.styles
.resolve(TextElem::spacing)
.map(|abs| Em::from_abs(abs, ctx.size));
let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
@ -1044,20 +1134,8 @@ fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<
/// Difference between non-breaking and normal space.
fn nbsp_delta(font: &Font) -> Option<Em> {
let space = font.ttf().glyph_index(' ')?.0;
let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
Some(font.advance(nbsp)? - font.advance(space)?)
}
/// Process the language and region of a style chain into a
/// rustybuzz-compatible BCP 47 language.
fn language(styles: StyleChain) -> rustybuzz::Language {
let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
if let Some(region) = TextElem::region_in(styles) {
bcp.push('-');
bcp.push_str(region.as_str());
}
rustybuzz::Language::from_str(&bcp).unwrap()
Some(font.x_advance(nbsp)? - font.space_width()?)
}
/// Returns true if all glyphs in `glyphs` have ranges within the range `range`.

View File

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

View File

@ -1,9 +1,11 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::{Accent, AccentElem};
use typst_library::math::AccentElem;
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment};
use super::{
style_cramped, style_dtls, style_flac, FrameFragment, MathContext, MathFragment,
};
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
@ -15,55 +17,76 @@ pub fn layout_accent(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let cramped = style_cramped();
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
let accent = elem.accent;
let top_accent = !accent.is_bottom();
// Try to replace a glyph with its dotless variant.
if elem.dotless(styles) {
if let MathFragment::Glyph(glyph) = &mut base {
glyph.make_dotless_form(ctx);
}
}
let base_span = elem.base.span();
// Try to replace the base glyph with its dotless variant.
let dtls = style_dtls();
let base_styles =
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
let cramped = style_cramped();
let base_styles = base_styles.chain(&cramped);
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
// Preserve class to preserve automatic spacing.
let base_class = base.class();
let base_attach = base.accent_attach();
let width = elem.size(styles).relative_to(base.width());
// Try to replace the accent glyph with its flattened variant.
let flattened_base_height =
value!(ctx, base_styles, base_span, base, flattened_accent_base_height);
let flac = style_flac();
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
styles.chain(&flac)
} else {
styles
};
let Accent(c) = elem.accent;
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
let accent = SymbolElem::packed(accent.0).spanned(elem.span());
let mut accent = ctx.layout_into_fragment(&accent, accent_styles)?;
// Try to replace accent glyph with flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
if base.ascent() > flattened_base_height {
glyph.make_flattened_accent_form(ctx);
}
// Forcing the accent to be at least as large as the base makes it too wide
// in many cases.
let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(base.font_size().unwrap_or_default());
accent.stretch_horizontal(ctx, width - short_fall);
let accent_attach = accent.accent_attach().0;
let accent = accent.into_frame();
// Forcing the accent to be at least as large as the base makes it too
// wide in many case.
let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size);
let variant = glyph.stretch_horizontal(ctx, width, short_fall);
let accent = variant.frame;
let accent_attach = variant.accent_attach;
let (gap, accent_pos, base_pos) = if top_accent {
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we
// need a larger gap so that the accent doesn't move too low.
let accent_base_height =
value!(ctx, base_styles, base_span, base, accent_base_height);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);
(gap, accent_pos, base_pos)
} else {
let gap = -accent.ascent();
let accent_pos = Point::new(base_attach.1 - accent_attach, base.height() + gap);
let base_pos = Point::zero();
(gap, accent_pos, base_pos)
};
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);
let baseline = base_pos.y + base.ascent();
let base_italics_correction = base.italics_correction();
let base_text_like = base.is_text_like();
let base_ascent = match &base {
MathFragment::Frame(frame) => frame.base_ascent,
_ => base.ascent(),
};
let base_descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
let mut frame = Frame::soft(size);
frame.set_baseline(baseline);
@ -73,6 +96,7 @@ pub fn layout_accent(
FrameFragment::new(styles, frame)
.with_class(base_class)
.with_base_ascent(base_ascent)
.with_base_descent(base_descent)
.with_italics_correction(base_italics_correction)
.with_accent_attach(base_attach)
.with_text_like(base_text_like),

View File

@ -4,11 +4,13 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
};
use typst_library::text::Font;
use typst_syntax::Span;
use typst_utils::OptionExt;
use super::{
stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits,
MathContext, MathFragment,
find_math_font, stretch_fragment, style_for_subscript, style_for_superscript,
FrameFragment, Limits, MathContext, MathFragment,
};
macro_rules! measure {
@ -31,16 +33,16 @@ pub fn layout_attach(
let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
let tr = elem.tr(sup_style_chain);
let tl = elem.tl.get_cloned(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 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_chain = styles.chain(&sub_style);
let bl = elem.bl(sub_style_chain);
let br = elem.br(sub_style_chain);
let b = elem.b(sub_style_chain);
let bl = elem.bl.get_cloned(sub_style_chain);
let br = elem.br.get_cloned(sub_style_chain);
let b = elem.b.get_cloned(sub_style_chain);
let limits = base.limits().active(styles);
let (t, tr) = match (t, tr) {
@ -66,7 +68,6 @@ pub fn layout_attach(
let relative_to_width = measure!(t, width).max(measure!(b, width));
stretch_fragment(
ctx,
styles,
&mut base,
Some(Axis::X),
Some(relative_to_width),
@ -147,7 +148,7 @@ pub fn layout_limits(
ctx: &mut MathContext,
styles: StyleChain,
) -> 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)?;
fragment.set_limits(limits);
ctx.push(fragment);
@ -162,7 +163,8 @@ fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs
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.
@ -173,19 +175,20 @@ fn layout_attachments(
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> {
let base_class = base.class();
let font = find_math_font(ctx.engine.world, styles, Span::detached())?;
// Calculate the distance from the base's baseline to the superscripts' and
// subscripts' baseline.
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
(Abs::zero(), Abs::zero())
} else {
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
compute_script_shifts(&font, styles, &base, [&tl, &tr, &bl, &br])
};
// Calculate the distance from the base's baseline to the top attachment's
// and bottom attachment's baseline.
let (t_shift, b_shift) =
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
compute_limit_shifts(&font, styles, &base, [t.as_ref(), b.as_ref()]);
// Calculate the final frame height.
let ascent = base
@ -215,12 +218,14 @@ fn layout_attachments(
// `space_after_script` is extra spacing that is at the start before each
// pre-script, and at the end after each post-script (see the MathConstants
// table in the OpenType MATH spec).
let space_after_script = scaled!(ctx, styles, space_after_script);
let space_after_script = typst_library::foundations::Resolve::resolve(
value!(font, space_after_script),
styles,
);
// Calculate the distance each pre-script extends to the left of the base's
// width.
let (tl_pre_width, bl_pre_width) = compute_pre_script_widths(
ctx,
&base,
[tl.as_ref(), bl.as_ref()],
(tx_shift, bx_shift),
@ -231,7 +236,6 @@ fn layout_attachments(
// base's width. Also calculate each post-script's kerning (we need this for
// its position later).
let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths(
ctx,
&base,
[tr.as_ref(), br.as_ref()],
(tx_shift, bx_shift),
@ -287,14 +291,13 @@ fn layout_attachments(
/// post-script's kerning value. The first tuple is for the post-superscript,
/// and the second is for the post-subscript.
fn compute_post_script_widths(
ctx: &MathContext,
base: &MathFragment,
[tr, br]: [Option<&MathFragment>; 2],
(tr_shift, br_shift): (Abs, Abs),
space_after_post_script: Abs,
) -> ((Abs, Abs), (Abs, Abs)) {
let tr_values = tr.map_or_default(|tr| {
let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight);
let kern = math_kern(base, tr, tr_shift, Corner::TopRight);
(space_after_post_script + tr.width() + kern, kern)
});
@ -302,7 +305,7 @@ fn compute_post_script_widths(
// need to shift the post-subscript left by the base's italic correction
// (see the kerning algorithm as described in the OpenType MATH spec).
let br_values = br.map_or_default(|br| {
let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight)
let kern = math_kern(base, br, br_shift, Corner::BottomRight)
- base.italics_correction();
(space_after_post_script + br.width() + kern, kern)
});
@ -317,19 +320,18 @@ fn compute_post_script_widths(
/// extends left of the base's width and the second being the distance the
/// pre-subscript extends left of the base's width.
fn compute_pre_script_widths(
ctx: &MathContext,
base: &MathFragment,
[tl, bl]: [Option<&MathFragment>; 2],
(tl_shift, bl_shift): (Abs, Abs),
space_before_pre_script: Abs,
) -> (Abs, Abs) {
let tl_pre_width = tl.map_or_default(|tl| {
let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft);
let kern = math_kern(base, tl, tl_shift, Corner::TopLeft);
space_before_pre_script + tl.width() + kern
});
let bl_pre_width = bl.map_or_default(|bl| {
let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft);
let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft);
space_before_pre_script + bl.width() + kern
});
@ -368,7 +370,7 @@ fn compute_limit_widths(
/// Returns two lengths, the first being the distance to the upper-limit's
/// baseline and the second being the distance to the lower-limit's baseline.
fn compute_limit_shifts(
ctx: &MathContext,
font: &Font,
styles: StyleChain,
base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2],
@ -377,16 +379,27 @@ fn compute_limit_shifts(
// ascender of the limits respectively, whereas `upper_rise_min` and
// `lower_drop_min` give gaps to each limit's baseline (see the
// MathConstants table in the OpenType MATH spec).
let t_shift = t.map_or_default(|t| {
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
let upper_gap_min = typst_library::foundations::Resolve::resolve(
value!(font, upper_limit_gap_min),
styles,
);
let upper_rise_min = typst_library::foundations::Resolve::resolve(
value!(font, upper_limit_baseline_rise_min),
styles,
);
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
});
let b_shift = b.map_or_default(|b| {
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
let lower_gap_min = typst_library::foundations::Resolve::resolve(
value!(font, lower_limit_gap_min),
styles,
);
let lower_drop_min = typst_library::foundations::Resolve::resolve(
value!(font, lower_limit_baseline_drop_min),
styles,
);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
});
@ -397,25 +410,48 @@ fn compute_limit_shifts(
/// Returns two lengths, the first being the distance to the superscripts'
/// baseline and the second being the distance to the subscripts' baseline.
fn compute_script_shifts(
ctx: &MathContext,
font: &Font,
styles: StyleChain,
base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) {
let sup_shift_up = if EquationElem::cramped_in(styles) {
scaled!(ctx, styles, superscript_shift_up_cramped)
} else {
scaled!(ctx, styles, superscript_shift_up)
};
let sup_shift_up = typst_library::foundations::Resolve::resolve(
if styles.get(EquationElem::cramped) {
value!(font, superscript_shift_up_cramped)
} else {
value!(font, superscript_shift_up)
},
styles,
);
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min);
let sup_bottom_max_with_sub =
scaled!(ctx, styles, superscript_bottom_max_with_subscript);
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max);
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min);
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down);
let sub_top_max = scaled!(ctx, styles, subscript_top_max);
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min);
let sup_bottom_min = typst_library::foundations::Resolve::resolve(
value!(font, superscript_bottom_min),
styles,
);
let sup_bottom_max_with_sub = typst_library::foundations::Resolve::resolve(
value!(font, superscript_bottom_max_with_subscript),
styles,
);
let sup_drop_max = typst_library::foundations::Resolve::resolve(
value!(font, superscript_baseline_drop_max),
styles,
);
let gap_min = typst_library::foundations::Resolve::resolve(
value!(font, sub_superscript_gap_min),
styles,
);
let sub_shift_down = typst_library::foundations::Resolve::resolve(
value!(font, subscript_shift_down),
styles,
);
let sub_top_max = typst_library::foundations::Resolve::resolve(
value!(font, subscript_top_max),
styles,
);
let sub_drop_min = typst_library::foundations::Resolve::resolve(
value!(font, subscript_baseline_drop_min),
styles,
);
let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero();
@ -434,9 +470,13 @@ fn compute_script_shifts(
}
if bl.is_some() || br.is_some() {
let descent = match &base {
MathFragment::Frame(frame) => frame.base_descent,
_ => base.descent(),
};
shift_down = shift_down
.max(sub_shift_down)
.max(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min })
.max(if is_text_like { Abs::zero() } else { descent + sub_drop_min })
.max(measure!(bl, ascent) - sub_top_max)
.max(measure!(br, ascent) - sub_top_max);
}
@ -467,13 +507,7 @@ fn compute_script_shifts(
/// a negative value means shifting the script closer to the base. Requires the
/// distance from the base's baseline to the script's baseline, as well as the
/// script's corner (tl, tr, bl, br).
fn math_kern(
ctx: &MathContext,
base: &MathFragment,
script: &MathFragment,
shift: Abs,
pos: Corner,
) -> Abs {
fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs {
// This process is described under the MathKernInfo table in the OpenType
// MATH spec.
@ -498,8 +532,8 @@ fn math_kern(
// Calculate the sum of kerning values for each correction height.
let summed_kern = |height| {
let base_kern = base.kern_at_height(ctx, pos, height);
let attach_kern = script.kern_at_height(ctx, pos.inv(), height);
let base_kern = base.kern_at_height(pos, height);
let attach_kern = script.kern_at_height(pos.inv(), height);
base_kern + attach_kern
};

View File

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

View File

@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;
use super::{
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment,
MathContext, DELIM_SHORT_FALL,
style_for_denominator, style_for_numerator, FrameFragment, MathContext,
DELIM_SHORT_FALL,
};
const FRAC_AROUND: Em = Em::new(0.1);
@ -50,26 +50,26 @@ fn layout_frac_like(
span: Span,
) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height);
let thickness = scaled!(ctx, styles, fraction_rule_thickness);
let shift_up = scaled!(
ctx, styles,
text: fraction_numerator_shift_up,
let axis = value!(ctx, styles, span, axis_height);
let thickness = value!(ctx, styles, span, fraction_rule_thickness);
let shift_up = value!(
ctx, styles, span,
inline: fraction_numerator_shift_up,
display: fraction_numerator_display_style_shift_up,
);
let shift_down = scaled!(
ctx, styles,
text: fraction_denominator_shift_down,
let shift_down = value!(
ctx, styles, span,
inline: fraction_denominator_shift_down,
display: fraction_denominator_display_style_shift_down,
);
let num_min = scaled!(
ctx, styles,
text: fraction_numerator_gap_min,
let num_min = value!(
ctx, styles, span,
inline: fraction_numerator_gap_min,
display: fraction_num_display_style_gap_min,
);
let denom_min = scaled!(
ctx, styles,
text: fraction_denominator_gap_min,
let denom_min = value!(
ctx, styles, span,
inline: fraction_denominator_gap_min,
display: fraction_denom_display_style_gap_min,
);
@ -109,14 +109,16 @@ fn layout_frac_like(
frame.push_frame(denom_pos, denom);
if binom {
let mut left = GlyphFragment::new(ctx, styles, '(', span)
.stretch_vertical(ctx, height, short_fall);
left.center_on_axis(ctx);
let left = SymbolElem::packed('(').spanned(span);
let mut left = ctx.layout_into_fragment(&left, styles)?;
left.stretch_vertical(ctx, height - short_fall);
left.center_on_axis();
ctx.push(left);
ctx.push(FrameFragment::new(styles, frame));
let mut right = GlyphFragment::new(ctx, styles, ')', span)
.stretch_vertical(ctx, height, short_fall);
right.center_on_axis(ctx);
let right = SymbolElem::packed(')').spanned(span);
let mut right = ctx.layout_into_fragment(&right, styles)?;
right.stretch_vertical(ctx, height - short_fall);
right.center_on_axis();
ctx.push(right);
} else {
frame.push(
@ -124,7 +126,7 @@ fn layout_frac_like(
FrameItem::Shape(
Geometry::Line(Point::with_x(line_width)).stroked(
FixedStroke::from_pair(
TextElem::fill_in(styles).as_decoration(),
styles.get_ref(TextElem::fill).as_decoration(),
thickness,
),
),

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ pub fn layout_lr(
// Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() {
if lr.size(styles).is_one() {
if lr.size.get(styles).is_one() {
body = &lr.body;
}
}
@ -33,32 +33,32 @@ pub fn layout_lr(
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height);
let max_extent = inner_fragments
.iter()
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
.max()
.unwrap_or_default();
let mut max_extent = Abs::zero();
for fragment in inner_fragments.iter() {
let axis = value!(ctx, styles, elem.span(), fragment, axis_height);
let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
max_extent = max_extent.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.
match inner_fragments {
[one] => scale(ctx, styles, one, relative_to, height, None),
[one] => scale_if_delimiter(ctx, one, relative_to, height, None),
[first, .., last] => {
scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing));
scale_if_delimiter(ctx, first, relative_to, height, Some(MathClass::Opening));
scale_if_delimiter(ctx, last, relative_to, height, Some(MathClass::Closing));
}
_ => {}
[] => {}
}
// Handle MathFragment::Variant fragments that should be scaled up.
// Handle MathFragment::Glyph fragments that should be scaled up.
for fragment in inner_fragments.iter_mut() {
if let MathFragment::Variant(ref mut variant) = fragment {
if variant.mid_stretched == Some(false) {
variant.mid_stretched = Some(true);
scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large));
if let MathFragment::Glyph(ref mut glyph) = fragment {
if glyph.mid_stretched == Some(false) {
glyph.mid_stretched = Some(true);
scale(ctx, fragment, relative_to, height);
}
}
}
@ -95,18 +95,9 @@ pub fn layout_mid(
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments {
match fragment {
MathFragment::Glyph(glyph) => {
let mut new = glyph.clone().into_variant();
new.mid_stretched = Some(false);
new.class = MathClass::Fence;
*fragment = MathFragment::Variant(new);
}
MathFragment::Variant(variant) => {
variant.mid_stretched = Some(false);
variant.class = MathClass::Fence;
}
_ => {}
if let MathFragment::Glyph(ref mut glyph) = fragment {
glyph.mid_stretched = Some(false);
glyph.class = MathClass::Relation;
}
}
@ -114,10 +105,13 @@ pub fn layout_mid(
Ok(())
}
/// Scale a math fragment to a height.
fn scale(
/// Scales a math fragment to a height if it has the class Opening, Closing, or
/// Fence.
///
/// In case `apply` is `Some(class)`, `class` will be applied to the fragment if
/// it is a delimiter, in a way that cannot be overridden by the user.
fn scale_if_delimiter(
ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment,
relative_to: Abs,
height: Rel<Abs>,
@ -127,21 +121,23 @@ fn scale(
fragment.class(),
MathClass::Opening | MathClass::Closing | MathClass::Fence
) {
// This unwrap doesn't really matter. If it is None, then the fragment
// won't be stretchable anyways.
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(
ctx,
styles,
fragment,
Some(Axis::Y),
Some(relative_to),
height,
short_fall,
);
scale(ctx, fragment, relative_to, height);
if let Some(class) = apply {
fragment.set_class(class);
}
}
}
/// Scales a math fragment to a height.
fn scale(
ctx: &mut MathContext,
fragment: &mut MathFragment,
relative_to: Abs,
height: Rel<Abs>,
) {
// This unwrap doesn't really matter. If it is None, then the fragment
// won't be stretchable anyways.
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(ctx, fragment, Some(Axis::Y), Some(relative_to), height, short_fall);
}

View File

@ -1,5 +1,5 @@
use typst_library::diag::{bail, warning, SourceResult};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{
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 super::{
alignments, delimiter_alignment, style_for_denominator, AlignmentResult,
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment,
LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
};
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -30,15 +30,15 @@ pub fn layout_vec(
ctx,
styles,
&[column],
elem.align(styles),
elem.align.resolve(styles),
LeftRightAlternator::Right,
None,
Axes::with_y(elem.gap(styles)),
Axes::with_y(elem.gap.resolve(styles)),
span,
"elements",
)?;
let delim = elem.delim(styles);
let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
}
@ -59,14 +59,17 @@ pub fn layout_cases(
FixedAlignment::Start,
LeftRightAlternator::None,
None,
Axes::with_y(elem.gap(styles)),
Axes::with_y(elem.gap.resolve(styles)),
span,
"branches",
)?;
let delim = elem.delim(styles);
let (open, close) =
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
let delim = elem.delim.get(styles);
let (open, close) = if elem.reverse.get(styles) {
(None, delim.close())
} else {
(delim.open(), None)
};
layout_delimiters(ctx, styles, frame, open, close, span)
}
@ -81,7 +84,7 @@ pub fn layout_mat(
let rows = &elem.rows;
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 {
for &offset in &aug.hline.0 {
if offset == 0 || offset.unsigned_abs() >= rows.len() {
@ -116,15 +119,15 @@ pub fn layout_mat(
ctx,
styles,
&columns,
elem.align(styles),
elem.align.resolve(styles),
LeftRightAlternator::Right,
augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
Axes::new(elem.column_gap.resolve(styles), elem.row_gap.resolve(styles)),
span,
"cells",
)?;
let delim = elem.delim(styles);
let delim = elem.delim.get(styles);
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 = FixedStroke {
thickness: default_stroke_thickness,
paint: TextElem::fill_in(styles).as_decoration(),
paint: styles.get_ref(TextElem::fill).as_decoration(),
cap: LineCap::Square,
..Default::default()
};
@ -183,8 +186,10 @@ fn layout_body(
// We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are
// way too big.
// This will never panic as a paren will never shape into nothing.
let paren =
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
.unwrap();
for (column, col) in columns.iter().zip(&mut cols) {
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
@ -202,8 +207,8 @@ fn layout_body(
));
}
ascent.set_max(cell.ascent().max(paren.ascent));
descent.set_max(cell.descent().max(paren.descent));
ascent.set_max(cell.ascent().max(paren.ascent()));
descent.set_max(cell.descent().max(paren.descent()));
col.push(cell);
}
@ -307,24 +312,26 @@ fn layout_delimiters(
span: Span,
) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height);
let axis = value!(ctx, styles, span, axis_height);
let height = frame.height();
let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis);
if let Some(left) = left {
let mut left = GlyphFragment::new(ctx, styles, left, span)
.stretch_vertical(ctx, target, short_fall);
left.align_on_axis(ctx, delimiter_alignment(left.c));
if let Some(left_c) = left {
let left = SymbolElem::packed(left_c).spanned(span);
let mut left = ctx.layout_into_fragment(&left, styles)?;
left.stretch_vertical(ctx, target - short_fall);
left.center_on_axis();
ctx.push(left);
}
ctx.push(FrameFragment::new(styles, frame));
if let Some(right) = right {
let mut right = GlyphFragment::new(ctx, styles, right, span)
.stretch_vertical(ctx, target, short_fall);
right.align_on_axis(ctx, delimiter_alignment(right.c));
if let Some(right_c) = right {
let right = SymbolElem::packed(right_c).spanned(span);
let mut right = ctx.layout_into_fragment(&right, styles)?;
right.stretch_vertical(ctx, target - short_fall);
right.center_on_axis();
ctx.push(right);
}

View File

@ -13,9 +13,7 @@ mod stretch;
mod text;
mod underover;
use rustybuzz::Feature;
use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
@ -29,20 +27,16 @@ use typst_library::layout::{
use typst_library::math::*;
use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
};
use typst_library::World;
use typst_syntax::Span;
use typst_library::text::{LinebreakElem, SpaceElem, TextEdgeBounds, TextElem};
use typst_utils::Numeric;
use unicode_math_class::MathClass;
use self::fragment::{
FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment,
has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment,
};
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
use self::shared::*;
use self::stretch::{stretch_fragment, stretch_glyph};
use self::stretch::stretch_fragment;
/// Layout an inline equation (in a paragraph).
#[typst_macros::time(span = elem.span())]
@ -53,14 +47,13 @@ pub fn layout_equation_inline(
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block(styles));
let font = find_math_font(engine, styles, elem.span())?;
assert!(!elem.block.get(styles));
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
let mut ctx = MathContext::new(engine, &mut locator, region);
let scale_style = style_for_script_scale(&ctx);
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style);
let run = ctx.layout_into_run(&elem.body, styles)?;
@ -80,12 +73,12 @@ pub fn layout_equation_inline(
for item in &mut items {
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(
TextElem::top_edge_in(styles),
TextElem::bottom_edge_in(styles),
TextElem::size_in(styles),
styles.get(TextElem::top_edge),
styles.get(TextElem::bottom_edge),
styles.resolve(TextElem::size),
TextEdgeBounds::Frame(frame),
);
@ -107,15 +100,15 @@ pub fn layout_equation_block(
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
assert!(elem.block(styles));
assert!(elem.block.get(styles));
let span = elem.span();
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
let mut ctx = MathContext::new(engine, &mut locator, regions.base());
let scale_style = style_for_script_scale(&ctx);
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style);
let full_equation_builder = ctx
@ -123,7 +116,7 @@ pub fn layout_equation_block(
.multiline_frame_builder(styles);
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 equation_builders = vec![];
let mut last_first_pos = Point::zero();
@ -190,7 +183,7 @@ pub fn layout_equation_block(
vec![full_equation_builder]
};
let Some(numbering) = (**elem).numbering(styles) else {
let Some(numbering) = elem.numbering.get_ref(styles) else {
let frames = equation_builders
.into_iter()
.map(MathRunFrameBuilder::build)
@ -199,7 +192,7 @@ pub fn layout_equation_block(
};
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)?
.spanned(span);
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
@ -207,7 +200,7 @@ pub fn layout_equation_block(
static NUMBER_GUTTER: Em = Em::new(0.5);
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::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
@ -226,7 +219,7 @@ pub fn layout_equation_block(
builder,
number.clone(),
number_align.resolve(styles),
AlignElem::alignment_in(styles).resolve(styles).x,
styles.get(AlignElem::alignment).resolve(styles).x,
regions.size.x,
full_number_width,
)
@ -236,24 +229,6 @@ pub fn layout_equation_block(
Ok(Fragment::frames(frames))
}
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let world = engine.world;
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
Some(font)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
fn add_equation_number(
equation_builder: MathRunFrameBuilder,
number: Frame,
@ -372,16 +347,6 @@ struct MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
region: Region,
// Font-related.
font: &'a Font,
ttf: &'a ttf_parser::Face<'a>,
table: ttf_parser::math::Table<'a>,
constants: ttf_parser::math::Constants<'a>,
dtls_table: Option<GlyphwiseSubsts<'a>>,
flac_table: Option<GlyphwiseSubsts<'a>>,
ssty_table: Option<GlyphwiseSubsts<'a>>,
glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
space_width: Em,
// Mutable.
fragments: Vec<MathFragment>,
}
@ -391,46 +356,12 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
fn new(
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
styles: StyleChain<'a>,
base: Size,
font: &'a Font,
) -> Self {
let math_table = font.ttf().tables().math.unwrap();
let gsub_table = font.ttf().tables().gsub;
let constants = math_table.constants.unwrap();
let feat = |tag: &[u8; 4]| {
GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..))
};
let features = features(styles);
let glyphwise_tables = Some(
features
.into_iter()
.filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature))
.collect(),
);
let ttf = font.ttf();
let space_width = ttf
.glyph_index(' ')
.and_then(|id| ttf.glyph_hor_advance(id))
.map(|advance| font.to_em(advance))
.unwrap_or(THICK);
Self {
engine,
locator,
region: Region::new(base, Axes::splat(false)),
font,
ttf,
table: math_table,
constants,
dtls_table: feat(b"dtls"),
flac_table: feat(b"flac"),
ssty_table: feat(b"ssty"),
glyphwise_tables,
space_width,
fragments: vec![],
}
}
@ -504,15 +435,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
styles,
)?;
let outer = styles;
for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue;
}
layout_realized(elem, self, styles)?;
}
@ -529,7 +452,9 @@ fn layout_realized(
if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() {
ctx.push(MathFragment::Space(ctx.space_width.resolve(styles)));
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let space_width = font.space_width().unwrap_or(THICK);
ctx.push(MathFragment::Space(space_width.resolve(styles)));
} else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak);
} else if let Some(elem) = elem.to_packed::<HElem>() {
@ -599,7 +524,8 @@ fn layout_realized(
} else {
let mut frame = layout_external(elem, ctx, styles)?;
if !frame.has_baseline() {
let axis = scaled!(ctx, styles, axis_height);
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let axis = value!(font, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
}
ctx.push(
@ -637,7 +563,10 @@ fn layout_h(
) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount {
if 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(())
@ -650,7 +579,7 @@ fn layout_class(
ctx: &mut MathContext,
styles: StyleChain,
) -> 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))?;
fragment.set_class(elem.class);
fragment.set_limits(Limits::for_class(elem.class));
@ -676,7 +605,7 @@ fn layout_op(
.with_italics_correction(italics)
.with_accent_attach(accent_attach)
.with_text_like(text_like)
.with_limits(if elem.limits(styles) {
.with_limits(if elem.limits.get(styles) {
Limits::Display
} else {
Limits::Never

View File

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

View File

@ -194,13 +194,13 @@ impl MathRun {
let row_count = rows.len();
let alignments = alignments(&rows);
let leading = if EquationElem::size_in(styles) >= MathSize::Text {
ParElem::leading_in(styles)
let leading = if styles.get(EquationElem::size) >= MathSize::Text {
styles.resolve(ParElem::leading)
} else {
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 size = Size::zero();
for (i, row) in rows.into_iter().enumerate() {

View File

@ -1,109 +1,178 @@
use ttf_parser::math::MathValue;
use comemo::Tracked;
use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{families, variant, Font, FontFeatures, TextElem};
use typst_library::World;
use typst_syntax::Span;
use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
use super::{LeftRightAlternator, MathFragment, MathRun};
macro_rules! scaled {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match typst_library::math::EquationElem::size_in($styles) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
_ => scaled!($ctx, $styles, $text),
macro_rules! value {
($ctx:expr, $styles:expr, $span:expr, $fragment:expr, inline: $inline:ident, display: $display:ident $(,)?) => {
match $fragment
.math_size()
.unwrap_or_else(|| $styles.get(typst_library::math::EquationElem::size))
{
typst_library::math::MathSize::Display => {
value!($ctx, $styles, $span, $fragment, $display)
}
_ => value!($ctx, $styles, $span, $fragment, $inline),
}
};
($ctx:expr, $styles:expr, $name:ident) => {
$crate::math::Scaled::scaled(
$ctx.constants.$name(),
$ctx,
typst_library::text::TextElem::size_in($styles),
)
($ctx:expr, $styles:expr, $span:expr, inline: $inline:ident, display: $display:ident $(,)?) => {
match $styles.get(typst_library::math::EquationElem::size) {
typst_library::math::MathSize::Display => {
value!($ctx, $styles, $span, $display)
}
_ => value!($ctx, $styles, $span, $inline),
}
};
($ctx:expr, $styles:expr, $span:expr, $fragment:expr, $name:ident) => {{
let font = match $fragment {
crate::math::MathFragment::Glyph(ref glyph) => glyph.item.font.clone(),
_ => crate::math::find_math_font($ctx.engine.world, $styles, $span)?,
};
font.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| font.to_em(constants.$name().value))
.unwrap()
.at($fragment
.font_size()
.unwrap_or_else(|| $styles.resolve(typst_library::text::TextElem::size)))
}};
($ctx:expr, $styles:expr, $span:expr, $name:ident) => {{
let font = crate::math::find_math_font($ctx.engine.world, $styles, $span)?;
value!(font, $styles, $name)
}};
($font:expr, $styles:expr, $name:ident) => {
typst_library::foundations::Resolve::resolve(value!($font, $name), $styles)
};
($font:expr, $name:ident) => {
$font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| $font.to_em(constants.$name().value))
.unwrap()
};
}
macro_rules! percent {
($ctx:expr, $name:ident) => {
$ctx.constants.$name() as f64 / 100.0
($ctx:expr, $styles:expr, $span:expr, $fragment:expr, $name:ident) => {
match $fragment {
MathFragment::Glyph(ref glyph) => glyph.item.font.clone(),
_ => find_math_font($ctx.engine.world, $styles, $span)?,
}
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| constants.$name())
.unwrap() as f64
/ 100.0
};
}
macro_rules! word {
($font:expr, $name:ident) => {
$font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| $font.to_em(constants.$name()))
.unwrap()
};
}
#[comemo::memoize]
pub fn find_math_font(
world: Tracked<dyn World + '_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
// Take the base font as the "main" math font.
family.covers().map_or(Some(font), |_| None)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
/// How much less high scaled delimiters can be than what they wrap.
pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// Converts some unit to an absolute length with the current font & font size.
pub trait Scaled {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
}
impl Scaled for i16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
ctx.font.to_em(self).at(font_size)
}
}
impl Scaled for u16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
ctx.font.to_em(self).at(font_size)
}
}
impl Scaled for MathValue<'_> {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
self.value.scaled(ctx, font_size)
}
}
/// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap()
EquationElem::cramped.set(true).wrap()
}
/// Sets flac OpenType feature.
pub fn style_flac() -> LazyHash<Style> {
TextElem::features
.set(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)]))
.wrap()
}
/// Sets dtls OpenType feature.
pub fn style_dtls() -> LazyHash<Style> {
TextElem::features
.set(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)]))
.wrap()
}
/// The style for subscripts in the current style.
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.
pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) {
MathSize::Display | MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
EquationElem::size
.set(match styles.get(EquationElem::size) {
MathSize::Display | MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
}
/// The style for numerators in the current style.
pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) {
MathSize::Display => MathSize::Text,
MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
EquationElem::size
.set(match styles.get(EquationElem::size) {
MathSize::Display => MathSize::Text,
MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
}
/// The style for denominators in the current style.
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.
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
EquationElem::set_script_scale((
ctx.constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(),
))
.wrap()
}
/// How a delimieter should be aligned when scaling.
pub fn delimiter_alignment(delimiter: char) -> VAlignment {
match delimiter {
'⌜' | '⌝' => VAlignment::Top,
'⌞' | '⌟' => VAlignment::Bottom,
_ => VAlignment::Horizon,
}
pub fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
let constants = font.ttf().tables().math.and_then(|math| math.constants).unwrap();
EquationElem::script_scale
.set((
constants.script_percent_scale_down(),
constants.script_script_percent_scale_down(),
))
.wrap()
}
/// Stack rows on top of each other.

View File

@ -1,19 +1,10 @@
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::LazyArray16;
use typst_library::diag::{warning, SourceResult};
use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Frame, Point, Rel, Size};
use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::StretchElem;
use typst_utils::Get;
use super::{
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
VariantFragment,
};
use crate::modifiers::FrameModify;
/// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024;
use super::{stretch_axes, MathContext, MathFragment};
/// Lays out a [`StretchElem`].
#[typst_macros::time(name = "math.stretch", span = elem.span())]
@ -25,11 +16,10 @@ pub fn layout_stretch(
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment(
ctx,
styles,
&mut fragment,
None,
None,
elem.size(styles),
elem.size.resolve(styles),
Abs::zero(),
);
ctx.push(fragment);
@ -39,269 +29,49 @@ pub fn layout_stretch(
/// Attempts to stretch the given fragment by/to the amount given in stretch.
pub fn stretch_fragment(
ctx: &mut MathContext,
styles: StyleChain,
fragment: &mut MathFragment,
axis: Option<Axis>,
relative_to: Option<Abs>,
stretch: Rel<Abs>,
short_fall: Abs,
) {
let glyph = match fragment {
MathFragment::Glyph(glyph) => glyph.clone(),
MathFragment::Variant(variant) => {
GlyphFragment::new(ctx, styles, variant.c, variant.span)
}
_ => return,
};
let size = fragment.size();
let MathFragment::Glyph(ref mut glyph) = fragment else { return };
// Return if we attempt to stretch along an axis which isn't stretchable,
// so that the original fragment isn't modified.
let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { return };
let axis = axis.unwrap_or(stretch_axis);
if axis != stretch_axis {
return;
}
let relative_to_size = relative_to.unwrap_or_else(|| fragment.size().get(axis));
let mut variant = stretch_glyph(
ctx,
glyph,
stretch.relative_to(relative_to_size),
short_fall,
axis,
);
if axis == Axis::Y {
variant.align_on_axis(ctx, delimiter_alignment(variant.c));
}
*fragment = MathFragment::Variant(variant);
}
/// Return whether the glyph is stretchable and if it is, along which axis it
/// can be stretched.
fn stretch_axis(ctx: &mut MathContext, base: &GlyphFragment) -> Option<Axis> {
let base_id = base.id;
let vertical = ctx
.table
.variants
.and_then(|variants| variants.vertical_constructions.get(base_id))
.map(|_| Axis::Y);
let horizontal = ctx
.table
.variants
.and_then(|variants| variants.horizontal_constructions.get(base_id))
.map(|_| Axis::X);
match (vertical, horizontal) {
(vertical, None) => vertical,
(None, horizontal) => horizontal,
_ => {
// As far as we know, there aren't any glyphs that have both
// vertical and horizontal constructions. So for the time being, we
// will assume that a glyph cannot have both.
ctx.engine.sink.warn(warning!(
base.span,
"glyph has both vertical and horizontal constructions";
hint: "this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues"
));
None
let axes = stretch_axes(&glyph.item.font, glyph.base_glyph.id);
let stretch_axis = if let Some(axis) = axis {
if !axes.get(axis) {
return;
}
}
}
/// Try to stretch a glyph to a desired width or height.
///
/// The resulting frame may not have the exact desired width.
pub fn stretch_glyph(
ctx: &mut MathContext,
mut base: GlyphFragment,
target: Abs,
short_fall: Abs,
axis: Axis,
) -> VariantFragment {
// If the base glyph is good enough, use it.
let advance = match axis {
Axis::X => base.width,
Axis::Y => base.height(),
};
let short_target = target - short_fall;
if short_target <= advance {
return base.into_variant();
}
let mut min_overlap = Abs::zero();
let construction = ctx
.table
.variants
.and_then(|variants| {
min_overlap = variants.min_connector_overlap.scaled(ctx, base.font_size);
match axis {
Axis::X => variants.horizontal_constructions,
Axis::Y => variants.vertical_constructions,
axis
} else {
match (axes.x, axes.y) {
(true, false) => Axis::X,
(false, true) => Axis::Y,
(false, false) => return,
(true, true) => {
// As far as we know, there aren't any glyphs that have both
// vertical and horizontal constructions. So for the time being, we
// will assume that a glyph cannot have both.
ctx.engine.sink.warn(warning!(
glyph.item.glyphs[0].span.0,
"glyph has both vertical and horizontal constructions";
hint: "this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues"
));
return;
}
.get(base.id)
})
.unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
// Search for a pre-made variant with a good advance.
let mut best_id = base.id;
let mut best_advance = base.width;
for variant in construction.variants {
best_id = variant.variant_glyph;
best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size);
if short_target <= best_advance {
break;
}
}
// This is either good or the best we've got.
if short_target <= best_advance || construction.assembly.is_none() {
base.set_id(ctx, best_id);
return base.into_variant();
}
// Assemble from parts.
let assembly = construction.assembly.unwrap();
assemble(ctx, base, assembly, min_overlap, target, axis)
}
/// Assemble a glyph from parts.
fn assemble(
ctx: &mut MathContext,
base: GlyphFragment,
assembly: GlyphAssembly,
min_overlap: Abs,
target: Abs,
axis: Axis,
) -> VariantFragment {
// Determine the number of times the extenders need to be repeated as well
// as a ratio specifying how much to spread the parts apart
// (0 = maximal overlap, 1 = minimal overlap).
let mut full;
let mut ratio;
let mut repeat = 0;
loop {
full = Abs::zero();
ratio = 0.0;
let mut parts = parts(assembly, repeat).peekable();
let mut growable = Abs::zero();
while let Some(part) = parts.next() {
let mut advance = part.full_advance.scaled(ctx, base.font_size);
if let Some(next) = parts.peek() {
let max_overlap = part
.end_connector_length
.min(next.start_connector_length)
.scaled(ctx, base.font_size);
if max_overlap < min_overlap {
// This condition happening is indicative of a bug in the
// font.
ctx.engine.sink.warn(warning!(
base.span,
"glyph has assembly parts with overlap less than minConnectorOverlap";
hint: "its rendering may appear broken - this is probably a font bug";
hint: "please file an issue at https://github.com/typst/typst/issues"
));
}
advance -= max_overlap;
growable += max_overlap - min_overlap;
}
full += advance;
}
if full < target {
let delta = target - full;
ratio = (delta / growable).min(1.0);
full += ratio * growable;
}
if target <= full || repeat >= MAX_REPEATS {
break;
}
repeat += 1;
}
let mut selected = vec![];
let mut parts = parts(assembly, repeat).peekable();
while let Some(part) = parts.next() {
let mut advance = part.full_advance.scaled(ctx, base.font_size);
if let Some(next) = parts.peek() {
let max_overlap = part
.end_connector_length
.min(next.start_connector_length)
.scaled(ctx, base.font_size);
advance -= max_overlap;
advance += ratio * (max_overlap - min_overlap);
}
let mut fragment = base.clone();
fragment.set_id(ctx, part.glyph_id);
selected.push((fragment, advance));
}
let size;
let baseline;
match axis {
Axis::X => {
let height = base.ascent + base.descent;
size = Size::new(full, height);
baseline = base.ascent;
}
Axis::Y => {
let axis = ctx.constants.axis_height().scaled(ctx, base.font_size);
let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
size = Size::new(width, full);
baseline = full / 2.0 + axis;
}
}
let mut frame = Frame::soft(size);
let mut offset = Abs::zero();
frame.set_baseline(baseline);
frame.modify(&base.modifiers);
for (fragment, advance) in selected {
let pos = match axis {
Axis::X => Point::new(offset, frame.baseline() - fragment.ascent),
Axis::Y => Point::with_y(full - offset - fragment.height()),
};
frame.push_frame(pos, fragment.into_frame());
offset += advance;
}
let accent_attach = match axis {
Axis::X => frame.width() / 2.0,
Axis::Y => base.accent_attach,
};
VariantFragment {
c: base.c,
frame,
font_size: base.font_size,
italics_correction: Abs::zero(),
accent_attach,
class: base.class,
math_size: base.math_size,
span: base.span,
limits: base.limits,
mid_stretched: None,
extended_shape: true,
let relative_to_size = relative_to.unwrap_or_else(|| size.get(stretch_axis));
glyph.stretch(ctx, stretch.relative_to(relative_to_size) - short_fall, stretch_axis);
if stretch_axis == Axis::Y {
glyph.center_on_axis();
}
}
/// Return an iterator over the assembly's parts with extenders repeated the
/// specified number of times.
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
assembly.parts.into_iter().flat_map(move |part| {
let count = if part.part_flags.extender() { repeat } else { 1 };
std::iter::repeat_n(part, count)
})
}

View File

@ -12,7 +12,10 @@ use typst_syntax::{is_newline, Span};
use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation;
use super::{FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun};
use super::{
find_math_font, has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext,
MathFragment, MathRun,
};
/// Lays out a [`TextElem`].
pub fn layout_text(
@ -48,7 +51,7 @@ fn layout_text_lines<'a>(
}
}
let mut frame = MathRun::new(fragments).into_frame(styles);
let axis = scaled!(ctx, styles, axis_height);
let axis = value!(ctx, styles, span, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
Ok(FrameFragment::new(styles, frame))
}
@ -65,27 +68,19 @@ fn layout_inline_text(
// Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![];
let is_single = text.chars().count() == 1;
for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false);
let mut glyph = GlyphFragment::new(ctx, styles, c, span);
if is_single {
// Duplicate what `layout_glyph` does exactly even if it's
// probably incorrect here.
match EquationElem::size_in(styles) {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
}
// This won't panic as ASCII digits and '.' will never end up as
// nothing after shaping.
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
Ok(FrameFragment::new(styles, frame).with_text_like(true))
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
TextElem::top_edge.set(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::bottom_edge.set(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
]
.map(|p| p.wrap());
@ -126,55 +121,31 @@ pub fn layout_symbol(
) -> SourceResult<()> {
// Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass.
let (unstyled_c, dtls) = match try_dotless(elem.text) {
Some(c) if ctx.dtls_table.is_some() => (c, true),
_ => (elem.text, false),
// let dtls = style_dtls();
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
// Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
_ => (elem.text, styles),
};
let c = styled_char(styles, unstyled_c, true);
let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
None => {
// Not in the math font, fallback to normal inline text layout.
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
.into()
}
let Some(mut glyph) = GlyphFragment::new_char(ctx, symbol_styles, c, elem.span())?
else {
return Ok(());
};
ctx.push(fragment);
Ok(())
}
/// Layout a [`GlyphFragment`].
fn layout_glyph(
mut glyph: GlyphFragment,
dtls: bool,
ctx: &mut MathContext,
styles: StyleChain,
) -> MathFragment {
if dtls {
glyph.make_dotless_form(ctx);
}
let math_size = EquationElem::size_in(styles);
match math_size {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero())
} else {
glyph.into_variant()
if styles.get(EquationElem::size) == MathSize::Display {
let height = word!(glyph.item.font, display_operator_min_height)
.at(glyph.item.size)
.max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height);
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
variant.center_on_axis(ctx);
variant.into()
} else {
glyph.into()
glyph.center_on_axis();
}
ctx.push(glyph);
Ok(())
}
/// Style the character by selecting the unicode codepoint for italic, bold,
@ -185,9 +156,9 @@ fn layout_glyph(
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(
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic).unwrap_or(
auto_italic
&& matches!(
c,

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
@ -11,7 +11,7 @@ use typst_syntax::Span;
use super::{
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
GlyphFragment, LeftRightAlternator, MathContext, MathRun,
LeftRightAlternator, MathContext, MathRun,
};
const BRACE_GAP: Em = Em::new(0.25);
@ -56,7 +56,7 @@ pub fn layout_underbrace(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏟',
BRACE_GAP,
Position::Under,
@ -75,7 +75,7 @@ pub fn layout_overbrace(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏞',
BRACE_GAP,
Position::Over,
@ -94,7 +94,7 @@ pub fn layout_underbracket(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⎵',
BRACKET_GAP,
Position::Under,
@ -113,7 +113,7 @@ pub fn layout_overbracket(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⎴',
BRACKET_GAP,
Position::Over,
@ -132,7 +132,7 @@ pub fn layout_underparen(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏝',
PAREN_GAP,
Position::Under,
@ -151,7 +151,7 @@ pub fn layout_overparen(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏜',
PAREN_GAP,
Position::Over,
@ -170,7 +170,7 @@ pub fn layout_undershell(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏡',
SHELL_GAP,
Position::Under,
@ -189,7 +189,7 @@ pub fn layout_overshell(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏠',
SHELL_GAP,
Position::Over,
@ -208,27 +208,27 @@ fn layout_underoverline(
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
match position {
Position::Under => {
let sep = scaled!(ctx, styles, underbar_extra_descender);
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
let gap = scaled!(ctx, styles, underbar_vertical_gap);
extra_height = sep + bar_height + gap;
content = ctx.layout_into_fragment(body, styles)?;
let sep = value!(ctx, styles, span, content, underbar_extra_descender);
bar_height = value!(ctx, styles, span, content, underbar_rule_thickness);
let gap = value!(ctx, styles, span, content, underbar_vertical_gap);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
content_pos = Point::zero();
baseline = content.ascent();
line_adjust = -content.italics_correction();
}
Position::Over => {
let sep = scaled!(ctx, styles, overbar_extra_ascender);
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
let gap = scaled!(ctx, styles, overbar_vertical_gap);
extra_height = sep + bar_height + gap;
let cramped = style_cramped();
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;
let sep = value!(ctx, styles, span, content, overbar_extra_ascender);
bar_height = value!(ctx, styles, span, content, overbar_rule_thickness);
let gap = value!(ctx, styles, span, content, overbar_vertical_gap);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(sep + bar_height / 2.0);
content_pos = Point::with_y(extra_height);
baseline = content.ascent() + extra_height;
@ -251,7 +251,7 @@ fn layout_underoverline(
line_pos,
FrameItem::Shape(
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,
..FixedStroke::default()
}),
@ -285,14 +285,16 @@ fn layout_underoverspreader(
let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class();
let body = body.into_fragment(styles);
let glyph = GlyphFragment::new(ctx, styles, c, span);
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
let c = SymbolElem::packed(c).spanned(span);
let mut c = ctx.layout_into_fragment(&c, styles)?;
c.stretch_horizontal(ctx, body.width());
let mut rows = vec![];
let baseline = match position {
Position::Under => {
rows.push(MathRun::new(vec![body]));
rows.push(stretched.into());
rows.push(c.into());
if let Some(annotation) = annotation {
let under_style = style_for_subscript(styles);
let annotation_styles = styles.chain(&under_style);
@ -306,7 +308,7 @@ fn layout_underoverspreader(
let annotation_styles = styles.chain(&over_style);
rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
}
rows.push(stretched.into());
rows.push(c.into());
rows.push(MathRun::new(vec![body]));
rows.len() - 1
}

View File

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

View File

@ -1,6 +1,6 @@
use typst_library::diag::SourceResult;
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::layout::{
Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size,
@ -16,10 +16,10 @@ pub fn layout_pad(
regions: Regions,
) -> SourceResult<Fragment> {
let padding = Sides::new(
elem.left(styles).resolve(styles),
elem.top(styles).resolve(styles),
elem.right(styles).resolve(styles),
elem.bottom(styles).resolve(styles),
elem.left.resolve(styles),
elem.top.resolve(styles),
elem.right.resolve(styles),
elem.bottom.resolve(styles),
);
let mut backlog = vec![];

View File

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

View File

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

View File

@ -29,7 +29,7 @@ pub fn layout_repeat(
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 width = piece.width();
@ -47,12 +47,12 @@ pub fn layout_repeat(
let count = ((fill + gap) / (width + gap)).floor();
let remaining = (fill + gap) % (width + gap);
let justify = elem.justify(styles);
let justify = elem.justify.get(styles);
if justify {
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();
if count == 1.0 || !justify {
offset += align.x.position(remaining);

View File

@ -11,8 +11,8 @@ use typst_library::layout::{
};
use typst_library::visualize::{
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
Shape, SquareElem, Stroke,
FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
RectElem, Shape, SquareElem, Stroke,
};
use typst_syntax::Span;
use typst_utils::{Get, Numeric};
@ -27,16 +27,20 @@ pub fn layout_line(
region: Region,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
let start = resolve(elem.start(styles));
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
let length = elem.length(styles);
let angle = elem.angle(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
let start = resolve(elem.start.resolve(styles));
let delta = elem
.end
.resolve(styles)
.map(|end| resolve(end) - start)
.unwrap_or_else(|| {
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());
if !size.is_finite() {
@ -105,7 +109,7 @@ pub fn layout_path(
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 to = vertices[0];
let from_point = *points.last().unwrap();
@ -120,9 +124,9 @@ pub fn layout_path(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -153,19 +157,19 @@ pub fn layout_curve(
for item in &elem.components {
match item {
CurveComponent::Move(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let point = builder.resolve_point(element.start, relative);
builder.move_(point);
}
CurveComponent::Line(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let point = builder.resolve_point(element.end, relative);
builder.line(point);
}
CurveComponent::Quad(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let end = builder.resolve_point(element.end, relative);
let control = match element.control {
Smart::Auto => {
@ -178,7 +182,7 @@ pub fn layout_curve(
}
CurveComponent::Cubic(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let end = builder.resolve_point(element.end, relative);
let c1 = match element.control_start {
Some(Smart::Custom(p)) => builder.resolve_point(p, relative),
@ -193,7 +197,7 @@ pub fn layout_curve(
}
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.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -418,9 +422,9 @@ pub fn layout_polygon(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -459,12 +463,12 @@ pub fn layout_rect(
styles,
region,
ShapeKind::Rect,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
elem.radius.resolve(styles),
elem.span(),
)
}
@ -484,12 +488,12 @@ pub fn layout_square(
styles,
region,
ShapeKind::Square,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
elem.radius.resolve(styles),
elem.span(),
)
}
@ -509,11 +513,11 @@ pub fn layout_ellipse(
styles,
region,
ShapeKind::Ellipse,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles).map(|s| Sides::splat(Some(s))),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
Corners::splat(None),
elem.span(),
)
@ -534,11 +538,11 @@ pub fn layout_circle(
styles,
region,
ShapeKind::Circle,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles).map(|s| Sides::splat(Some(s))),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
Corners::splat(None),
elem.span(),
)
@ -889,7 +893,13 @@ fn segmented_rect(
let end = current;
last = current;
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 {
res.push(shape);
} else {
@ -899,7 +909,14 @@ fn segmented_rect(
}
} else if let Some(stroke) = &strokes.top {
// 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
@ -946,6 +963,8 @@ fn curve_segment(
fn segment(
start: Corner,
end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>,
stroke: &FixedStroke,
) -> (Shape, bool) {
@ -979,7 +998,7 @@ fn segment(
let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill {
fill_segment(start, end, corners, stroke)
fill_segment(start, end, start_cap, end_cap, corners, stroke)
} else {
stroke_segment(start, end, corners, stroke.clone())
};
@ -1010,6 +1029,8 @@ fn stroke_segment(
fn fill_segment(
start: Corner,
end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>,
stroke: &FixedStroke,
) -> Shape {
@ -1035,8 +1056,7 @@ fn fill_segment(
if c.arc_outer() {
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
} else {
curve.line(c.outer());
curve.line(c.end_outer());
c.start_cap(&mut curve, start_cap);
}
}
@ -1079,7 +1099,7 @@ fn fill_segment(
if c.arc_inner() {
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
} else {
curve.line(c.center_inner());
c.end_cap(&mut curve, end_cap);
}
}
@ -1134,6 +1154,16 @@ struct 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.
fn rotate(&self, point: Point) -> Point {
match self.corner {
@ -1280,6 +1310,77 @@ impl ControlPoints {
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.

View File

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

View File

@ -20,7 +20,7 @@ pub fn layout_move(
region: Region,
) -> SourceResult<Frame> {
let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?;
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
let delta = Axes::new(elem.dx.resolve(styles), elem.dy.resolve(styles));
let delta = delta.zip_map(region.size, Rel::relative_to);
frame.translate(delta.to_point());
Ok(frame)
@ -35,8 +35,8 @@ pub fn layout_rotate(
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let angle = elem.angle(styles);
let align = elem.origin(styles).resolve(styles);
let angle = elem.angle.get(styles);
let align = elem.origin.resolve(styles);
// Compute the new region's approximate size.
let is_finite = region.size.is_finite();
@ -55,7 +55,7 @@ pub fn layout_rotate(
&elem.body,
Transform::rotate(angle),
align,
elem.reflow(styles),
elem.reflow.get(styles),
)
}
@ -83,8 +83,8 @@ pub fn layout_scale(
styles,
&elem.body,
Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles),
elem.reflow(styles),
elem.origin.resolve(styles),
elem.reflow.get(styles),
)
}
@ -121,13 +121,13 @@ fn resolve_scale(
});
let x = resolve_axis(
elem.x(styles),
elem.x.get(styles),
|| size.as_ref().map(|size| size.x).map_err(Clone::clone),
styles,
)?;
let y = resolve_axis(
elem.y(styles),
elem.y.get(styles),
|| size.as_ref().map(|size| size.y).map_err(Clone::clone),
styles,
)?;
@ -152,9 +152,9 @@ pub fn layout_skew(
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let ax = elem.ax(styles);
let ay = elem.ay(styles);
let align = elem.origin(styles).resolve(styles);
let ax = elem.ax.get(styles);
let ay = elem.ay.get(styles);
let align = elem.origin.resolve(styles);
// Compute the new region's approximate size.
let size = if region.size.is_finite() {
@ -172,7 +172,7 @@ pub fn layout_skew(
&elem.body,
Transform::skew(ax, ay),
align,
elem.reflow(styles),
elem.reflow.get(styles),
)
}

View File

@ -66,6 +66,7 @@ unicode-normalization = { workspace = true }
unicode-segmentation = { workspace = true }
unscanny = { workspace = true }
usvg = { workspace = true }
utf8_iter = { workspace = true }
wasmi = { workspace = true }
xmlwriter = { workspace = true }

View File

@ -1,17 +1,20 @@
//! Diagnostics.
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Display, Formatter, Write as _};
use std::io;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use std::string::FromUtf8Error;
use az::SaturatingAs;
use comemo::Tracked;
use ecow::{eco_vec, EcoVec};
use typst_syntax::package::{PackageSpec, PackageVersion};
use typst_syntax::{Span, Spanned, SyntaxError};
use typst_syntax::{Lines, Span, Spanned, SyntaxError};
use utf8_iter::ErrorReportingUtf8Chars;
use crate::engine::Engine;
use crate::loading::{LoadSource, Loaded};
use crate::{World, WorldExt};
/// Early-return with a [`StrResult`] or [`SourceResult`].
@ -148,7 +151,7 @@ pub struct Warned<T> {
pub warnings: EcoVec<SourceDiagnostic>,
}
/// An error or warning in a source file.
/// An error or warning in a source or text file.
///
/// The contained spans will only be detached if any of the input source files
/// were detached.
@ -568,31 +571,287 @@ impl From<PackageError> for EcoString {
}
}
/// A result type with a data-loading-related error.
pub type LoadResult<T> = Result<T, LoadError>;
/// A call site independent error that occurred during data loading. This avoids
/// polluting the memoization with [`Span`]s and [`FileId`]s from source files.
/// Can be turned into a [`SourceDiagnostic`] using the [`LoadedWithin::within`]
/// method available on [`LoadResult`].
///
/// [`FileId`]: typst_syntax::FileId
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LoadError {
/// The position in the file at which the error occured.
pos: ReportPos,
/// Must contain a message formatted like this: `"failed to do thing (cause)"`.
message: EcoString,
}
impl LoadError {
/// Creates a new error from a position in a file, a base message
/// (e.g. `failed to parse JSON`) and a concrete error (e.g. `invalid
/// number`)
pub fn new(
pos: impl Into<ReportPos>,
message: impl std::fmt::Display,
error: impl std::fmt::Display,
) -> Self {
Self {
pos: pos.into(),
message: eco_format!("{message} ({error})"),
}
}
}
impl From<Utf8Error> for LoadError {
fn from(err: Utf8Error) -> Self {
let start = err.valid_up_to();
let end = start + err.error_len().unwrap_or(0);
LoadError::new(
start..end,
"failed to convert to string",
"file is not valid utf-8",
)
}
}
/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`]
/// context.
pub trait LoadedWithin<T> {
/// Report an error, possibly in an external file.
fn within(self, loaded: &Loaded) -> SourceResult<T>;
}
impl<T, E> LoadedWithin<T> for Result<T, E>
where
E: Into<LoadError>,
{
fn within(self, loaded: &Loaded) -> SourceResult<T> {
self.map_err(|err| {
let LoadError { pos, message } = err.into();
load_err_in_text(loaded, pos, message)
})
}
}
/// Report an error, possibly in an external file. This will delegate to
/// [`load_err_in_invalid_text`] if the data isn't valid utf-8.
fn load_err_in_text(
loaded: &Loaded,
pos: impl Into<ReportPos>,
mut message: EcoString,
) -> EcoVec<SourceDiagnostic> {
let pos = pos.into();
// This also does utf-8 validation. Only report an error in an external
// file if it is human readable (valid utf-8), otherwise fall back to
// `load_err_in_invalid_text`.
let lines = Lines::try_from(&loaded.data);
match (loaded.source.v, lines) {
(LoadSource::Path(file_id), Ok(lines)) => {
if let Some(range) = pos.range(&lines) {
let span = Span::from_range(file_id, range);
return eco_vec![SourceDiagnostic::error(span, message)];
}
// Either `ReportPos::None` was provided, or resolving the range
// from the line/column failed. If present report the possibly
// wrong line/column in the error message anyway.
let span = Span::from_range(file_id, 0..loaded.data.len());
if let Some(pair) = pos.line_col(&lines) {
message.pop();
let (line, col) = pair.numbers();
write!(&mut message, " at {line}:{col})").ok();
}
eco_vec![SourceDiagnostic::error(span, message)]
}
(LoadSource::Bytes, Ok(lines)) => {
if let Some(pair) = pos.line_col(&lines) {
message.pop();
let (line, col) = pair.numbers();
write!(&mut message, " at {line}:{col})").ok();
}
eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
}
_ => load_err_in_invalid_text(loaded, pos, message),
}
}
/// Report an error (possibly from an external file) that isn't valid utf-8.
fn load_err_in_invalid_text(
loaded: &Loaded,
pos: impl Into<ReportPos>,
mut message: EcoString,
) -> EcoVec<SourceDiagnostic> {
let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers());
match (loaded.source.v, line_col) {
(LoadSource::Path(file), _) => {
message.pop();
if let Some(package) = file.package() {
write!(
&mut message,
" in {package}{}",
file.vpath().as_rooted_path().display()
)
.ok();
} else {
write!(&mut message, " in {}", file.vpath().as_rootless_path().display())
.ok();
};
if let Some((line, col)) = line_col {
write!(&mut message, ":{line}:{col}").ok();
}
message.push(')');
}
(LoadSource::Bytes, Some((line, col))) => {
message.pop();
write!(&mut message, " at {line}:{col})").ok();
}
(LoadSource::Bytes, None) => (),
}
eco_vec![SourceDiagnostic::error(loaded.source.span, message)]
}
/// A position at which an error was reported.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub enum ReportPos {
/// Contains a range, and a line/column pair.
Full(std::ops::Range<u32>, LineCol),
/// Contains a range.
Range(std::ops::Range<u32>),
/// Contains a line/column pair.
LineCol(LineCol),
#[default]
None,
}
impl From<std::ops::Range<usize>> for ReportPos {
fn from(value: std::ops::Range<usize>) -> Self {
Self::Range(value.start.saturating_as()..value.end.saturating_as())
}
}
impl From<LineCol> for ReportPos {
fn from(value: LineCol) -> Self {
Self::LineCol(value)
}
}
impl ReportPos {
/// Creates a position from a pre-existing range and line-column pair.
pub fn full(range: std::ops::Range<usize>, pair: LineCol) -> Self {
let range = range.start.saturating_as()..range.end.saturating_as();
Self::Full(range, pair)
}
/// Tries to determine the byte range for this position.
fn range(&self, lines: &Lines<String>) -> Option<std::ops::Range<usize>> {
match self {
ReportPos::Full(range, _) => Some(range.start as usize..range.end as usize),
ReportPos::Range(range) => Some(range.start as usize..range.end as usize),
&ReportPos::LineCol(pair) => {
let i =
lines.line_column_to_byte(pair.line as usize, pair.col as usize)?;
Some(i..i)
}
ReportPos::None => None,
}
}
/// Tries to determine the line/column for this position.
fn line_col(&self, lines: &Lines<String>) -> Option<LineCol> {
match self {
&ReportPos::Full(_, pair) => Some(pair),
ReportPos::Range(range) => {
let (line, col) = lines.byte_to_line_column(range.start as usize)?;
Some(LineCol::zero_based(line, col))
}
&ReportPos::LineCol(pair) => Some(pair),
ReportPos::None => None,
}
}
/// Either gets the line/column pair, or tries to compute it from possibly
/// invalid utf-8 data.
fn try_line_col(&self, bytes: &[u8]) -> Option<LineCol> {
match self {
&ReportPos::Full(_, pair) => Some(pair),
ReportPos::Range(range) => {
LineCol::try_from_byte_pos(range.start as usize, bytes)
}
&ReportPos::LineCol(pair) => Some(pair),
ReportPos::None => None,
}
}
}
/// A line/column pair.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LineCol {
/// The 0-based line.
line: u32,
/// The 0-based column.
col: u32,
}
impl LineCol {
/// Constructs the line/column pair from 0-based indices.
pub fn zero_based(line: usize, col: usize) -> Self {
Self {
line: line.saturating_as(),
col: col.saturating_as(),
}
}
/// Constructs the line/column pair from 1-based numbers.
pub fn one_based(line: usize, col: usize) -> Self {
Self::zero_based(line.saturating_sub(1), col.saturating_sub(1))
}
/// Try to compute a line/column pair from possibly invalid utf-8 data.
pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option<Self> {
let bytes = &bytes[..pos];
let mut line = 0;
#[allow(clippy::double_ended_iterator_last)]
let line_start = memchr::memchr_iter(b'\n', bytes)
.inspect(|_| line += 1)
.last()
.map(|i| i + 1)
.unwrap_or(bytes.len());
let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count();
Some(LineCol::zero_based(line, col))
}
/// Returns the 0-based line/column indices.
pub fn indices(&self) -> (usize, usize) {
(self.line as usize, self.col as usize)
}
/// Returns the 1-based line/column numbers.
pub fn numbers(&self) -> (usize, usize) {
(self.line as usize + 1, self.col as usize + 1)
}
}
/// Format a user-facing error message for an XML-like file format.
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString {
match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
eco_format!(
"failed to parse {format} (found closing tag '{actual}' \
instead of '{expected}' in line {})",
pos.row
)
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError {
let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize);
let message = match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => {
eco_format!("failed to parse {format} (found closing tag '{actual}' instead of '{expected}')")
}
roxmltree::Error::UnknownEntityReference(entity, pos) => {
eco_format!(
"failed to parse {format} (unknown entity '{entity}' in line {})",
pos.row
)
roxmltree::Error::UnknownEntityReference(entity, _) => {
eco_format!("failed to parse {format} (unknown entity '{entity}')")
}
roxmltree::Error::DuplicatedAttribute(attr, pos) => {
eco_format!(
"failed to parse {format} (duplicate attribute '{attr}' in line {})",
pos.row
)
roxmltree::Error::DuplicatedAttribute(attr, _) => {
eco_format!("failed to parse {format} (duplicate attribute '{attr}')")
}
roxmltree::Error::NoRootNode => {
eco_format!("failed to parse {format} (missing root node)")
}
err => eco_format!("failed to parse {format} ({err})"),
}
};
LoadError { pos: pos.into(), message }
}

View File

@ -94,7 +94,7 @@ impl Array {
}
/// Iterate over references to the contained values.
pub fn iter(&self) -> std::slice::Iter<Value> {
pub fn iter(&self) -> std::slice::Iter<'_, Value> {
self.0.iter()
}
@ -604,7 +604,7 @@ impl Array {
Ok(acc)
}
/// Calculates the product all items (works for all types that can be
/// Calculates the product of all items (works for all types that can be
/// multiplied).
#[func]
pub fn product(

View File

@ -7,6 +7,7 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use typst_syntax::Lines;
use typst_utils::LazyHash;
use crate::diag::{bail, StrResult};
@ -286,6 +287,16 @@ impl Serialize for Bytes {
}
}
impl TryFrom<&Bytes> for Lines<String> {
type Error = Utf8Error;
#[comemo::memoize]
fn try_from(value: &Bytes) -> Result<Lines<String>, Utf8Error> {
let text = value.as_str()?;
Ok(Lines::new(text.to_string()))
}
}
/// Any type that can back a byte buffer.
trait Bytelike: Send + Sync {
fn as_bytes(&self) -> &[u8];

View File

@ -207,9 +207,9 @@ pub fn sqrt(
/// ```
#[func]
pub fn root(
/// The expression to take the root of
/// The expression to take the root of.
radicand: f64,
/// Which root of the radicand to take
/// Which root of the radicand to take.
index: Spanned<i64>,
) -> SourceResult<f64> {
if index.v == 0 {
@ -317,7 +317,7 @@ pub fn asin(
/// ```
#[func(title = "Arccosine")]
pub fn acos(
/// The number whose arcsine to calculate. Must be between -1 and 1.
/// The number whose arccosine to calculate. Must be between -1 and 1.
value: Spanned<Num>,
) -> SourceResult<Angle> {
let val = value.v.float();
@ -387,7 +387,7 @@ pub fn cosh(
value.cosh()
}
/// Calculates the hyperbolic tangent of an hyperbolic angle.
/// Calculates the hyperbolic tangent of a hyperbolic angle.
///
/// ```example
/// #calc.tanh(0) \
@ -708,12 +708,13 @@ pub fn fract(
}
}
/// Rounds a number to the nearest integer away from zero.
/// Rounds a number to the nearest integer.
///
/// Optionally, a number of decimal places can be specified.
/// Half-integers are rounded away from zero.
///
/// If the number of digits is negative, its absolute value will indicate the
/// amount of significant integer digits to remove before the decimal point.
/// Optionally, a number of decimal places can be specified. If negative, its
/// absolute value will indicate the amount of significant integer digits to
/// remove before the decimal point.
///
/// Note that this function will return the same type as the operand. That is,
/// applying `round` to a [`float`] will return a `float`, and to a [`decimal`],

View File

@ -9,7 +9,7 @@ use std::ops::Add;
use ecow::eco_format;
use smallvec::SmallVec;
use typst_syntax::{Span, Spanned};
use typst_syntax::{Span, Spanned, SyntaxMode};
use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
@ -459,6 +459,21 @@ impl FromValue for Never {
}
}
cast! {
SyntaxMode,
self => IntoValue::into_value(match self {
SyntaxMode::Markup => "markup",
SyntaxMode::Math => "math",
SyntaxMode::Code => "code",
}),
/// Evaluate as markup, as in a Typst file.
"markup" => SyntaxMode::Markup,
/// Evaluate as math, as in an equation.
"math" => SyntaxMode::Math,
/// Evaluate as code, as after a hash.
"code" => SyntaxMode::Code,
}
cast! {
MathClass,
self => IntoValue::into_value(match self {

View File

@ -2,52 +2,54 @@ use std::any::TypeId;
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::Hash;
use std::ptr::NonNull;
use std::sync::LazyLock;
use std::sync::OnceLock;
use ecow::EcoString;
use smallvec::SmallVec;
#[doc(inline)]
pub use typst_macros::elem;
use typst_utils::Static;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
cast, Args, Content, Dict, FieldAccessError, Func, ParamInfo, Repr, Scope, Selector,
StyleChain, Styles, Value,
cast, Args, Content, ContentVtable, FieldAccessError, Func, ParamInfo, Repr, Scope,
Selector, StyleChain, Styles, Value,
};
use crate::text::{Lang, Region};
/// A document element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Element(Static<NativeElementData>);
pub struct Element(Static<ContentVtable>);
impl Element {
/// Get the element for `T`.
pub fn of<T: NativeElement>() -> Self {
T::elem()
pub const fn of<T: NativeElement>() -> Self {
T::ELEM
}
/// Get the element for `T`.
pub const fn from_vtable(vtable: &'static ContentVtable) -> Self {
Self(Static(vtable))
}
/// The element's normal name (e.g. `enum`).
pub fn name(self) -> &'static str {
self.0.name
self.vtable().name
}
/// The element's title case name, for use in documentation
/// (e.g. `Numbered List`).
pub fn title(&self) -> &'static str {
self.0.title
self.vtable().title
}
/// Documentation for the element (as Markdown).
pub fn docs(&self) -> &'static str {
self.0.docs
self.vtable().docs
}
/// Search keywords for the element.
pub fn keywords(&self) -> &'static [&'static str] {
self.0.keywords
self.vtable().keywords
}
/// Construct an instance of this element.
@ -56,12 +58,12 @@ impl Element {
engine: &mut Engine,
args: &mut Args,
) -> SourceResult<Content> {
(self.0.construct)(engine, args)
(self.vtable().construct)(engine, args)
}
/// Execute the set rule for the element and return the resulting style map.
pub fn set(self, engine: &mut Engine, mut args: Args) -> SourceResult<Styles> {
let styles = (self.0.set)(engine, &mut args)?;
let styles = (self.vtable().set)(engine, &mut args)?;
args.finish()?;
Ok(styles)
}
@ -77,12 +79,7 @@ impl Element {
/// Whether the element has the given capability where the capability is
/// given by a `TypeId`.
pub fn can_type_id(self, type_id: TypeId) -> bool {
(self.0.vtable)(type_id).is_some()
}
/// The VTable for capabilities dispatch.
pub fn vtable(self) -> fn(of: TypeId) -> Option<NonNull<()>> {
self.0.vtable
(self.vtable().capability)(type_id).is_some()
}
/// Create a selector for this element.
@ -98,12 +95,29 @@ impl Element {
/// The element's associated scope of sub-definition.
pub fn scope(&self) -> &'static Scope {
&(self.0).0.scope
(self.vtable().store)().scope.get_or_init(|| (self.vtable().scope)())
}
/// Details about the element's fields.
pub fn params(&self) -> &'static [ParamInfo] {
&(self.0).0.params
(self.vtable().store)().params.get_or_init(|| {
self.vtable()
.fields
.iter()
.filter(|field| !field.synthesized)
.map(|field| ParamInfo {
name: field.name,
docs: field.docs,
input: (field.input)(),
default: field.default,
positional: field.positional,
named: !field.positional,
variadic: field.variadic,
required: field.required,
settable: field.settable,
})
.collect()
})
}
/// Extract the field ID for the given field name.
@ -111,7 +125,7 @@ impl Element {
if name == "label" {
return Some(255);
}
(self.0.field_id)(name)
(self.vtable().field_id)(name)
}
/// Extract the field name for the given field ID.
@ -119,7 +133,7 @@ impl Element {
if id == 255 {
return Some("label");
}
(self.0.field_name)(id)
self.vtable().field(id).map(|data| data.name)
}
/// Extract the value of the field for the given field ID and style chain.
@ -128,12 +142,20 @@ impl Element {
id: u8,
styles: StyleChain,
) -> Result<Value, FieldAccessError> {
(self.0.field_from_styles)(id, styles)
self.vtable()
.field(id)
.and_then(|field| (field.get_from_styles)(styles))
.ok_or(FieldAccessError::Unknown)
}
/// The element's local name, if any.
pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> {
(self.0).0.local_name.map(|f| f(lang, region))
self.vtable().local_name.map(|f| f(lang, region))
}
/// Retrieves the element's vtable for dynamic dispatch.
pub(super) fn vtable(&self) -> &'static ContentVtable {
(self.0).0
}
}
@ -167,84 +189,34 @@ cast! {
v: Func => v.element().ok_or("expected element")?,
}
/// A Typst element that is defined by a native Rust type.
pub trait NativeElement:
Debug
+ Clone
+ PartialEq
+ Hash
+ Construct
+ Set
+ Capable
+ Fields
+ Repr
+ Send
+ Sync
+ 'static
{
/// Get the element for the native Rust element.
fn elem() -> Element
where
Self: Sized,
{
Element::from(Self::data())
}
/// Pack the element into type-erased content.
fn pack(self) -> Content
where
Self: Sized,
{
Content::new(self)
}
/// Get the element data for the native Rust element.
fn data() -> &'static NativeElementData
where
Self: Sized;
/// Lazily initialized data for an element.
#[derive(Default)]
pub struct LazyElementStore {
pub scope: OnceLock<Scope>,
pub params: OnceLock<Vec<ParamInfo>>,
}
/// Used to cast an element to a trait object for a trait it implements.
impl LazyElementStore {
/// Create an empty store.
pub const fn new() -> Self {
Self { scope: OnceLock::new(), params: OnceLock::new() }
}
}
/// A Typst element that is defined by a native Rust type.
///
/// # Safety
/// If the `vtable` function returns `Some(p)`, then `p` must be a valid pointer
/// to a vtable of `Packed<Self>` w.r.t to the trait `C` where `capability` is
/// `TypeId::of::<dyn C>()`.
pub unsafe trait Capable {
/// Get the pointer to the vtable for the given capability / trait.
fn vtable(capability: TypeId) -> Option<NonNull<()>>;
}
/// `ELEM` must hold the correct `Element` for `Self`.
pub unsafe trait NativeElement:
Debug + Clone + Hash + Construct + Set + Send + Sync + 'static
{
/// The associated element.
const ELEM: Element;
/// Defines how fields of an element are accessed.
pub trait Fields {
/// An enum with the fields of the element.
type Enum
where
Self: Sized;
/// Whether the element has the given field set.
fn has(&self, id: u8) -> bool;
/// Get the field with the given field ID.
fn field(&self, id: u8) -> Result<Value, FieldAccessError>;
/// Get the field with the given ID in the presence of styles.
fn field_with_styles(
&self,
id: u8,
styles: StyleChain,
) -> Result<Value, FieldAccessError>;
/// Get the field with the given ID from the styles.
fn field_from_styles(id: u8, styles: StyleChain) -> Result<Value, FieldAccessError>
where
Self: Sized;
/// Resolve all fields with the styles and save them in-place.
fn materialize(&mut self, styles: StyleChain);
/// Get the fields of the element.
fn fields(&self) -> Dict;
/// Pack the element into type-erased content.
fn pack(self) -> Content {
Content::new(self)
}
}
/// An element's constructor function.
@ -266,48 +238,6 @@ pub trait Set {
Self: Sized;
}
/// Defines a native element.
#[derive(Debug)]
pub struct NativeElementData {
/// The element's normal name (e.g. `align`), as exposed to Typst.
pub name: &'static str,
/// The element's title case name (e.g. `Align`).
pub title: &'static str,
/// The documentation for this element as a string.
pub docs: &'static str,
/// A list of alternate search terms for this element.
pub keywords: &'static [&'static str],
/// The constructor for this element (see [`Construct`]).
pub construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>,
/// Executes this element's set rule (see [`Set`]).
pub set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>,
/// Gets the vtable for one of this element's capabilities
/// (see [`Capable`]).
pub vtable: fn(capability: TypeId) -> Option<NonNull<()>>,
/// Gets the numeric index of this field by its name.
pub field_id: fn(name: &str) -> Option<u8>,
/// Gets the name of a field by its numeric index.
pub field_name: fn(u8) -> Option<&'static str>,
/// Get the field with the given ID in the presence of styles (see [`Fields`]).
pub field_from_styles: fn(u8, StyleChain) -> Result<Value, FieldAccessError>,
/// Gets the localized name for this element (see [`LocalName`][crate::text::LocalName]).
pub local_name: Option<fn(Lang, Option<Region>) -> &'static str>,
pub scope: LazyLock<Scope>,
/// A list of parameter information for each field.
pub params: LazyLock<Vec<ParamInfo>>,
}
impl From<&'static NativeElementData> for Element {
fn from(data: &'static NativeElementData) -> Self {
Self(Static(data))
}
}
cast! {
&'static NativeElementData,
self => Element::from(self).into_value(),
}
/// Synthesize fields on an element. This happens before execution of any show
/// rule.
pub trait Synthesize {
@ -331,3 +261,9 @@ pub trait ShowSet {
/// that should work even in the face of a user-defined show rule.
fn show_set(&self, styles: StyleChain) -> Styles;
}
/// Tries to extract the plain-text representation of the element.
pub trait PlainText {
/// Write this element's plain text into the given buffer.
fn plain_text(&self, text: &mut EcoString);
}

View File

@ -0,0 +1,564 @@
use std::fmt::{self, Debug};
use std::hash::Hash;
use std::marker::PhantomData;
use std::sync::OnceLock;
use ecow::{eco_format, EcoString};
use crate::foundations::{
Container, Content, FieldVtable, Fold, FoldFn, IntoValue, NativeElement, Packed,
Property, Reflect, Repr, Resolve, StyleChain,
};
/// An accessor for the `I`-th field of the element `E`. Values of this type are
/// generated for each field of an element can be used to interact with this
/// field programmatically, for example to access the style chain, as in
/// `styles.get(TextElem::size)`.
#[derive(Copy, Clone)]
pub struct Field<E: NativeElement, const I: u8>(pub PhantomData<E>);
impl<E: NativeElement, const I: u8> Field<E, I> {
/// Creates a new zero-sized accessor.
pub const fn new() -> Self {
Self(PhantomData)
}
/// The index of the projected field.
pub const fn index(self) -> u8 {
I
}
/// Creates a dynamic property instance for this field.
///
/// Prefer [`Content::set`] or
/// [`Styles::set`](crate::foundations::Styles::set) when working with
/// existing content or style value.
pub fn set(self, value: E::Type) -> Property
where
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
Property::new(self, value)
}
}
impl<E: NativeElement, const I: u8> Default for Field<E, I> {
fn default() -> Self {
Self::new()
}
}
/// A field that is present on every instance of the element.
pub trait RequiredField<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: RequiredFieldData<Self, I>;
}
/// Metadata and routines for a [`RequiredField`].
pub struct RequiredFieldData<E: RequiredField<I>, const I: u8> {
name: &'static str,
docs: &'static str,
get: fn(&E) -> &E::Type,
}
impl<E: RequiredField<I>, const I: u8> RequiredFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
get: fn(&E) -> &E::Type,
) -> Self {
Self { name, docs, get }
}
/// Creates the vtable for a `#[required]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: RequiredField<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: true,
required: true,
variadic: false,
settable: false,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: None,
has: |_| true,
get: |elem| Some((E::FIELD.get)(elem).clone().into_value()),
get_with_styles: |elem, _| Some((E::FIELD.get)(elem).clone().into_value()),
get_from_styles: |_| None,
materialize: |_, _| {},
eq: |a, b| (E::FIELD.get)(a) == (E::FIELD.get)(b),
}
}
/// Creates the vtable for a `#[variadic]` field.
pub const fn vtable_variadic() -> FieldVtable<Packed<E>>
where
E: RequiredField<I>,
E::Type: Container + IntoValue + PartialEq,
<E::Type as Container>::Inner: Reflect,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: true,
required: true,
variadic: true,
settable: false,
synthesized: false,
input: || <<E::Type as Container>::Inner as Reflect>::input(),
default: None,
has: |_| true,
get: |elem| Some((E::FIELD.get)(elem).clone().into_value()),
get_with_styles: |elem, _| Some((E::FIELD.get)(elem).clone().into_value()),
get_from_styles: |_| None,
materialize: |_, _| {},
eq: |a, b| (E::FIELD.get)(a) == (E::FIELD.get)(b),
}
}
}
/// A field that is initially unset, but may be set through a
/// [`Synthesize`](crate::foundations::Synthesize) implementation.
pub trait SynthesizedField<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: SynthesizedFieldData<Self, I>;
}
/// Metadata and routines for a [`SynthesizedField`].
pub struct SynthesizedFieldData<E: SynthesizedField<I>, const I: u8> {
name: &'static str,
docs: &'static str,
get: fn(&E) -> &Option<E::Type>,
}
impl<E: SynthesizedField<I>, const I: u8> SynthesizedFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
get: fn(&E) -> &Option<E::Type>,
) -> Self {
Self { name, docs, get }
}
/// Creates type-erased metadata and routines for a `#[synthesized]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: SynthesizedField<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: false,
required: false,
variadic: false,
settable: false,
synthesized: true,
input: || <E::Type as Reflect>::input(),
default: None,
has: |elem| (E::FIELD.get)(elem).is_some(),
get: |elem| (E::FIELD.get)(elem).clone().map(|v| v.into_value()),
get_with_styles: |elem, _| {
(E::FIELD.get)(elem).clone().map(|v| v.into_value())
},
get_from_styles: |_| None,
materialize: |_, _| {},
// Synthesized fields don't affect equality.
eq: |_, _| true,
}
}
}
/// A field that is not actually there. It's only visible in the docs.
pub trait ExternalField<const I: u8>: NativeElement {
type Type;
const FIELD: ExternalFieldData<Self, I>;
}
/// Metadata for an [`ExternalField`].
pub struct ExternalFieldData<E: ExternalField<I>, const I: u8> {
name: &'static str,
docs: &'static str,
default: fn() -> E::Type,
}
impl<E: ExternalField<I>, const I: u8> ExternalFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
default: fn() -> E::Type,
) -> Self {
Self { name, docs, default }
}
/// Creates type-erased metadata and routines for an `#[external]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: ExternalField<I>,
E::Type: Reflect + IntoValue,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: false,
required: false,
variadic: false,
settable: false,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: Some(|| (E::FIELD.default)().into_value()),
has: |_| false,
get: |_| None,
get_with_styles: |_, _| None,
get_from_styles: |_| None,
materialize: |_, _| {},
eq: |_, _| true,
}
}
}
/// A field that has a default value and can be configured via a set rule, but
/// can also present on elements and be present in the constructor.
pub trait SettableField<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: SettableFieldData<Self, I>;
}
/// Metadata and routines for a [`SettableField`].
pub struct SettableFieldData<E: SettableField<I>, const I: u8> {
get: fn(&E) -> &Settable<E, I>,
get_mut: fn(&mut E) -> &mut Settable<E, I>,
property: SettablePropertyData<E, I>,
}
impl<E: SettableField<I>, const I: u8> SettableFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
positional: bool,
get: fn(&E) -> &Settable<E, I>,
get_mut: fn(&mut E) -> &mut Settable<E, I>,
default: fn() -> E::Type,
slot: fn() -> &'static OnceLock<E::Type>,
) -> Self {
Self {
get,
get_mut,
property: SettablePropertyData::new(name, docs, positional, default, slot),
}
}
/// Ensures that the property is folded on every access. See the
/// documentation of the [`Fold`] trait for more details.
pub const fn with_fold(mut self) -> Self
where
E::Type: Fold,
{
self.property.fold = Some(E::Type::fold);
self
}
/// Creates type-erased metadata and routines for a normal settable field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: SettableField<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.property.name,
docs: E::FIELD.property.docs,
positional: E::FIELD.property.positional,
required: false,
variadic: false,
settable: true,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: Some(|| E::default().into_value()),
has: |elem| (E::FIELD.get)(elem).is_set(),
get: |elem| (E::FIELD.get)(elem).as_option().clone().map(|v| v.into_value()),
get_with_styles: |elem, styles| {
Some((E::FIELD.get)(elem).get_cloned(styles).into_value())
},
get_from_styles: |styles| {
Some(styles.get_cloned::<E, I>(Field::new()).into_value())
},
materialize: |elem, styles| {
if !(E::FIELD.get)(elem).is_set() {
(E::FIELD.get_mut)(elem).set(styles.get_cloned::<E, I>(Field::new()));
}
},
eq: |a, b| (E::FIELD.get)(a).as_option() == (E::FIELD.get)(b).as_option(),
}
}
}
/// A field that has a default value and can be configured via a set rule, but
/// is never present on elements.
///
/// This is provided for all `SettableField` impls through a blanket impl. In
/// the case of `#[ghost]` fields, which only live in the style chain and not in
/// elements, it is also implemented manually.
pub trait SettableProperty<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: SettablePropertyData<Self, I>;
const FOLD: Option<FoldFn<Self::Type>> = Self::FIELD.fold;
/// Produces an instance of the property's default value.
fn default() -> Self::Type {
// Avoid recreating an expensive instance over and over, but also
// avoid unnecessary lazy initialization for cheap types.
if std::mem::needs_drop::<Self::Type>() {
Self::default_ref().clone()
} else {
(Self::FIELD.default)()
}
}
/// Produces a static reference to this property's default value.
fn default_ref() -> &'static Self::Type {
(Self::FIELD.slot)().get_or_init(Self::FIELD.default)
}
}
impl<T, const I: u8> SettableProperty<I> for T
where
T: SettableField<I>,
{
type Type = <Self as SettableField<I>>::Type;
const FIELD: SettablePropertyData<Self, I> =
<Self as SettableField<I>>::FIELD.property;
}
/// Metadata and routines for a [`SettableProperty`].
pub struct SettablePropertyData<E: SettableProperty<I>, const I: u8> {
name: &'static str,
docs: &'static str,
positional: bool,
default: fn() -> E::Type,
slot: fn() -> &'static OnceLock<E::Type>,
fold: Option<FoldFn<E::Type>>,
}
impl<E: SettableProperty<I>, const I: u8> SettablePropertyData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
positional: bool,
default: fn() -> E::Type,
slot: fn() -> &'static OnceLock<E::Type>,
) -> Self {
Self { name, docs, positional, default, slot, fold: None }
}
/// Ensures that the property is folded on every access. See the
/// documentation of the [`Fold`] trait for more details.
pub const fn with_fold(self) -> Self
where
E::Type: Fold,
{
Self { fold: Some(E::Type::fold), ..self }
}
/// Creates type-erased metadata and routines for a `#[ghost]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: SettableProperty<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: E::FIELD.positional,
required: false,
variadic: false,
settable: true,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: Some(|| E::default().into_value()),
has: |_| false,
get: |_| None,
get_with_styles: |_, styles| {
Some(styles.get_cloned::<E, I>(Field::new()).into_value())
},
get_from_styles: |styles| {
Some(styles.get_cloned::<E, I>(Field::new()).into_value())
},
materialize: |_, _| {},
eq: |_, _| true,
}
}
}
/// A settable property that can be accessed by reference (because it is not
/// folded).
pub trait RefableProperty<const I: u8>: SettableProperty<I> {}
/// A settable field of an element.
///
/// The field can be in two states: Unset or present.
///
/// See [`StyleChain`] for more details about the available accessor methods.
#[derive(Copy, Clone, Hash)]
pub struct Settable<E: NativeElement, const I: u8>(Option<E::Type>)
where
E: SettableProperty<I>;
impl<E: NativeElement, const I: u8> Settable<E, I>
where
E: SettableProperty<I>,
{
/// Creates a new unset instance.
pub fn new() -> Self {
Self(None)
}
/// Sets the instance to a value.
pub fn set(&mut self, value: E::Type) {
self.0 = Some(value);
}
/// Clears the value from the instance.
pub fn unset(&mut self) {
self.0 = None;
}
/// Views the type as an [`Option`] which is `Some` if the type is set
/// and `None` if it is unset.
pub fn as_option(&self) -> &Option<E::Type> {
&self.0
}
/// Views the type as a mutable [`Option`].
pub fn as_option_mut(&mut self) -> &mut Option<E::Type> {
&mut self.0
}
/// Whether the field is set.
pub fn is_set(&self) -> bool {
self.0.is_some()
}
/// Retrieves the value given styles. The styles are used if the value is
/// unset.
pub fn get<'a>(&'a self, styles: StyleChain<'a>) -> E::Type
where
E::Type: Copy,
{
self.get_cloned(styles)
}
/// Retrieves and clones the value given styles. The styles are used if the
/// value is unset or if it needs folding.
pub fn get_cloned<'a>(&'a self, styles: StyleChain<'a>) -> E::Type {
if let Some(fold) = E::FOLD {
let mut res = styles.get_cloned::<E, I>(Field::new());
if let Some(value) = &self.0 {
res = fold(value.clone(), res);
}
res
} else if let Some(value) = &self.0 {
value.clone()
} else {
styles.get_cloned::<E, I>(Field::new())
}
}
/// Retrieves a reference to the value given styles. The styles are used if
/// the value is unset.
pub fn get_ref<'a>(&'a self, styles: StyleChain<'a>) -> &'a E::Type
where
E: RefableProperty<I>,
{
if let Some(value) = &self.0 {
value
} else {
styles.get_ref::<E, I>(Field::new())
}
}
/// Retrieves the value and then immediately [resolves](Resolve) it.
pub fn resolve<'a>(&'a self, styles: StyleChain<'a>) -> <E::Type as Resolve>::Output
where
E::Type: Resolve,
{
self.get_cloned(styles).resolve(styles)
}
}
impl<E: NativeElement, const I: u8> Debug for Settable<E, I>
where
E: SettableProperty<I>,
E::Type: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<E: NativeElement, const I: u8> Default for Settable<E, I>
where
E: SettableProperty<I>,
{
fn default() -> Self {
Self(None)
}
}
impl<E: NativeElement, const I: u8> From<Option<E::Type>> for Settable<E, I>
where
E: SettableProperty<I>,
{
fn from(value: Option<E::Type>) -> Self {
Self(value)
}
}
/// An error arising when trying to access a field of content.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldAccessError {
Unknown,
Unset,
}
impl FieldAccessError {
/// Formats the error message given the content and the field name.
#[cold]
pub fn message(self, content: &Content, field: &str) -> EcoString {
let elem_name = content.elem().name();
match self {
FieldAccessError::Unknown => {
eco_format!("{elem_name} does not have field {}", field.repr())
}
FieldAccessError::Unset => {
eco_format!(
"field {} in {elem_name} is not known at this point",
field.repr()
)
}
}
}
/// Formats the error message for an `at` calls without a default value.
#[cold]
pub fn message_no_default(self, content: &Content, field: &str) -> EcoString {
let mut msg = self.message(content, field);
msg.push_str(" and no default was specified");
msg
}
}

View File

@ -1,23 +1,33 @@
use std::any::TypeId;
mod element;
mod field;
mod packed;
mod raw;
mod vtable;
pub use self::element::*;
pub use self::field::*;
pub use self::packed::Packed;
pub use self::vtable::{ContentVtable, FieldVtable};
#[doc(inline)]
pub use typst_macros::elem;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::hash::Hash;
use std::iter::{self, Sum};
use std::marker::PhantomData;
use std::ops::{Add, AddAssign, ControlFlow, Deref, DerefMut};
use std::sync::Arc;
use std::ops::{Add, AddAssign, ControlFlow};
use comemo::Tracked;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
use typst_utils::singleton;
use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label,
NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles,
Value,
func, repr, scope, ty, Context, Dict, IntoValue, Label, Property, Recipe,
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
};
use crate::introspection::Location;
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
@ -68,43 +78,14 @@ use crate::text::UnderlineElem;
/// elements the content is composed of and what fields they have.
/// Alternatively, you can inspect the output of the [`repr`] function.
#[ty(scope, cast)]
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Content {
/// The partially element-dependent inner data.
inner: Arc<Inner<dyn Bounds>>,
/// The element's source code location.
span: Span,
}
/// The inner representation behind the `Arc`.
#[derive(Hash)]
struct Inner<T: ?Sized + 'static> {
/// An optional label attached to the element.
label: Option<Label>,
/// The element's location which identifies it in the layouted output.
location: Option<Location>,
/// Manages the element during realization.
/// - If bit 0 is set, the element is prepared.
/// - If bit n is set, the element is guarded against the n-th show rule
/// recipe from the top of the style chain (counting from 1).
lifecycle: SmallBitSet,
/// The element's raw data.
elem: LazyHash<T>,
}
#[derive(Clone, PartialEq, Hash)]
#[repr(transparent)]
pub struct Content(raw::RawContent);
impl Content {
/// Creates a new content from an element.
pub fn new<T: NativeElement>(elem: T) -> Self {
Self {
inner: Arc::new(Inner {
label: None,
location: None,
lifecycle: SmallBitSet::new(),
elem: elem.into(),
}),
span: Span::detached(),
}
Self(raw::RawContent::new(elem))
}
/// Creates a empty sequence content.
@ -114,25 +95,25 @@ impl Content {
/// Get the element of this content.
pub fn elem(&self) -> Element {
self.inner.elem.dyn_elem()
self.0.elem()
}
/// Get the span of the content.
pub fn span(&self) -> Span {
self.span
self.0.span()
}
/// Set the span of the content.
pub fn spanned(mut self, span: Span) -> Self {
if self.span.is_detached() {
self.span = span;
if self.0.span().is_detached() {
*self.0.span_mut() = span;
}
self
}
/// Get the label of the content.
pub fn label(&self) -> Option<Label> {
self.inner.label
self.0.meta().label
}
/// Attach a label to the content.
@ -143,7 +124,7 @@ impl Content {
/// Set the label of the content.
pub fn set_label(&mut self, label: Label) {
self.make_mut().label = Some(label);
self.0.meta_mut().label = Some(label);
}
/// Assigns a location to the content.
@ -159,28 +140,28 @@ impl Content {
/// Set the location of the content.
pub fn set_location(&mut self, location: Location) {
self.make_mut().location = Some(location);
self.0.meta_mut().location = Some(location);
}
/// Check whether a show rule recipe is disabled.
pub fn is_guarded(&self, index: RecipeIndex) -> bool {
self.inner.lifecycle.contains(index.0)
self.0.meta().lifecycle.contains(index.0)
}
/// Disable a show rule recipe.
pub fn guarded(mut self, index: RecipeIndex) -> Self {
self.make_mut().lifecycle.insert(index.0);
self.0.meta_mut().lifecycle.insert(index.0);
self
}
/// Whether this content has already been prepared.
pub fn is_prepared(&self) -> bool {
self.inner.lifecycle.contains(0)
self.0.meta().lifecycle.contains(0)
}
/// Mark this content as prepared.
pub fn mark_prepared(&mut self) {
self.make_mut().lifecycle.insert(0);
self.0.meta_mut().lifecycle.insert(0);
}
/// Get a field by ID.
@ -198,9 +179,14 @@ impl Content {
return Ok(label.into_value());
}
}
match styles {
Some(styles) => self.inner.elem.field_with_styles(id, styles),
None => self.inner.elem.field(id),
match self.0.handle().field(id) {
Some(handle) => match styles {
Some(styles) => handle.get_with_styles(styles),
None => handle.get(),
}
.ok_or(FieldAccessError::Unset),
None => Err(FieldAccessError::Unknown),
}
}
@ -215,8 +201,11 @@ impl Content {
.map(|label| label.into_value())
.ok_or(FieldAccessError::Unknown);
}
let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?;
self.get(id, None)
match self.elem().field_id(name).and_then(|id| self.0.handle().field(id)) {
Some(handle) => handle.get().ok_or(FieldAccessError::Unset),
None => Err(FieldAccessError::Unknown),
}
}
/// Get a field by ID, returning a missing field error if it does not exist.
@ -240,7 +229,9 @@ impl Content {
/// Resolve all fields with the styles and save them in-place.
pub fn materialize(&mut self, styles: StyleChain) {
self.make_mut().elem.materialize(styles);
for id in 0..self.elem().vtable().fields.len() as u8 {
self.0.handle_mut().field(id).unwrap().materialize(styles);
}
}
/// Create a new sequence element from multiples elements.
@ -257,7 +248,7 @@ impl Content {
/// Whether the contained element is of type `T`.
pub fn is<T: NativeElement>(&self) -> bool {
self.inner.elem.dyn_type_id() == TypeId::of::<T>()
self.0.is::<T>()
}
/// Downcasts the element to a packed value.
@ -280,16 +271,6 @@ impl Content {
self.into_packed::<T>().map(Packed::unpack)
}
/// Makes sure the content is not shared and returns a mutable reference to
/// the inner data.
fn make_mut(&mut self) -> &mut Inner<dyn Bounds> {
let arc = &mut self.inner;
if Arc::strong_count(arc) > 1 || Arc::weak_count(arc) > 0 {
*self = arc.elem.dyn_clone(arc, self.span);
}
Arc::get_mut(&mut self.inner).unwrap()
}
/// Whether the contained element has the given capability.
pub fn can<C>(&self) -> bool
where
@ -304,13 +285,7 @@ impl Content {
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`.
// Since any `Packed<T>` is a repr(transparent) `Content`, we can also
// use a `*const Content` pointer.
let vtable = self.elem().vtable()(TypeId::of::<C>())?;
let data = self as *const Content as *const ();
Some(unsafe { &*fat::from_raw_parts(data, vtable.as_ptr()) })
self.0.with::<C>()
}
/// Cast to a mutable trait object if the contained element has the given
@ -319,18 +294,7 @@ impl Content {
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`.
// Since any `Packed<T>` is a repr(transparent) `Content`, we can also
// use a `*const Content` pointer.
//
// The resulting trait object contains an `&mut Packed<T>`. We do _not_
// need to ensure that we hold the only reference to the `Arc` here
// because `Packed<T>`'s DerefMut impl will take care of that if
// mutable access is required.
let vtable = self.elem().vtable()(TypeId::of::<C>())?;
let data = self as *mut Content as *mut ();
Some(unsafe { &mut *fat::from_raw_parts_mut(data, vtable.as_ptr()) })
self.0.with_mut::<C>()
}
/// Whether the content is an empty sequence.
@ -372,6 +336,15 @@ impl Content {
Self::sequence(std::iter::repeat_with(|| self.clone()).take(count))
}
/// Sets a style property on the content.
pub fn set<E, const I: u8>(self, field: Field<E, I>, value: E::Type) -> Self
where
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
self.styled(Property::new(field, value))
}
/// Style this content with a style entry.
pub fn styled(mut self, style: impl Into<Style>) -> Self {
if let Some(style_elem) = self.to_packed_mut::<StyledElem>() {
@ -476,7 +449,7 @@ impl Content {
// Call f on the element itself before recursively iterating its fields.
f(self.clone())?;
for (_, value) in self.inner.elem.fields() {
for (_, value) in self.fields() {
walk_value(value, f)?;
}
ControlFlow::Continue(())
@ -504,12 +477,12 @@ impl Content {
/// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self {
self.styled(LinkElem::set_current(Some(dest)))
self.set(LinkElem::current, Some(dest))
}
/// Set alignments for this content.
pub fn aligned(self, align: Alignment) -> Self {
self.styled(AlignElem::set_alignment(align))
self.set(AlignElem::alignment, align)
}
/// Pad this content at the sides.
@ -562,7 +535,10 @@ impl Content {
return false;
};
self.inner.elem.has(id)
match self.0.handle().field(id) {
Some(field) => field.has(),
None => false,
}
}
/// Access the specified field on the content. Returns the default value if
@ -592,7 +568,12 @@ impl Content {
/// ```
#[func]
pub fn fields(&self) -> Dict {
let mut dict = self.inner.elem.fields();
let mut dict = Dict::new();
for field in self.0.handle().fields() {
if let Some(value) = field.get() {
dict.insert(field.name.into(), value);
}
}
if let Some(label) = self.label() {
dict.insert("label".into(), label.into_value());
}
@ -605,7 +586,7 @@ impl Content {
/// used with [counters]($counter), [state] and [queries]($query).
#[func]
pub fn location(&self) -> Option<Location> {
self.inner.location
self.0.meta().location
}
}
@ -617,7 +598,7 @@ impl Default for Content {
impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.inner.elem.fmt(f)
self.0.fmt(f)
}
}
@ -627,16 +608,22 @@ impl<T: NativeElement> From<T> for Content {
}
}
impl PartialEq for Content {
fn eq(&self, other: &Self) -> bool {
// Additional short circuit for different elements.
self.elem() == other.elem() && self.inner.elem.dyn_eq(other)
}
}
impl Repr for Content {
fn repr(&self) -> EcoString {
self.inner.elem.repr()
self.0.handle().repr().unwrap_or_else(|| {
let fields = self
.0
.handle()
.fields()
.filter_map(|field| field.get().map(|v| (field.name, v.repr())))
.map(|(name, value)| eco_format!("{name}: {value}"))
.collect::<Vec<_>>();
eco_format!(
"{}{}",
self.elem().name(),
repr::pretty_array_like(&fields, false),
)
})
}
}
@ -717,190 +704,8 @@ impl Serialize for Content {
}
}
/// The trait that combines all the other traits into a trait object.
trait Bounds: Debug + Repr + Fields + Send + Sync + 'static {
fn dyn_type_id(&self) -> TypeId;
fn dyn_elem(&self) -> Element;
fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content;
fn dyn_hash(&self, hasher: &mut dyn Hasher);
fn dyn_eq(&self, other: &Content) -> bool;
}
impl<T: NativeElement> Bounds for T {
fn dyn_type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn dyn_elem(&self) -> Element {
Self::elem()
}
fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content {
Content {
inner: Arc::new(Inner {
label: inner.label,
location: inner.location,
lifecycle: inner.lifecycle.clone(),
elem: LazyHash::reuse(self.clone(), &inner.elem),
}),
span,
}
}
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
TypeId::of::<Self>().hash(&mut state);
self.hash(&mut state);
}
fn dyn_eq(&self, other: &Content) -> bool {
let Some(other) = other.to_packed::<Self>() else {
return false;
};
*self == **other
}
}
impl Hash for dyn Bounds {
fn hash<H: Hasher>(&self, state: &mut H) {
self.dyn_hash(state);
}
}
/// A packed element of a static type.
#[derive(Clone, PartialEq, Hash)]
#[repr(transparent)]
pub struct Packed<T: NativeElement>(
/// Invariant: Must be of type `T`.
Content,
PhantomData<T>,
);
impl<T: NativeElement> Packed<T> {
/// Pack element while retaining its static type.
pub fn new(element: T) -> Self {
// Safety: The element is known to be of type `T`.
Packed(element.pack(), PhantomData)
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_ref(content: &Content) -> Option<&Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe { std::mem::transmute::<&Content, &Packed<T>>(content) });
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_mut(content: &mut Content) -> Option<&mut Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe {
std::mem::transmute::<&mut Content, &mut Packed<T>>(content)
});
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_owned(content: Content) -> Result<Self, Content> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Ok(unsafe { std::mem::transmute::<Content, Packed<T>>(content) });
}
Err(content)
}
/// Pack back into content.
pub fn pack(self) -> Content {
self.0
}
/// Extract the raw underlying element.
pub fn unpack(self) -> T {
// This function doesn't yet need owned self, but might in the future.
(*self).clone()
}
/// The element's span.
pub fn span(&self) -> Span {
self.0.span()
}
/// Set the span of the element.
pub fn spanned(self, span: Span) -> Self {
Self(self.0.spanned(span), PhantomData)
}
/// Accesses the label of the element.
pub fn label(&self) -> Option<Label> {
self.0.label()
}
/// Accesses the location of the element.
pub fn location(&self) -> Option<Location> {
self.0.location()
}
/// Sets the location of the element.
pub fn set_location(&mut self, location: Location) {
self.0.set_location(location);
}
}
impl<T: NativeElement> AsRef<T> for Packed<T> {
fn as_ref(&self) -> &T {
self
}
}
impl<T: NativeElement> AsMut<T> for Packed<T> {
fn as_mut(&mut self) -> &mut T {
self
}
}
impl<T: NativeElement> Deref for Packed<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// Safety:
// - Packed<T> guarantees that the content trait object wraps
// an element of type `T`.
// - This downcast works the same way as dyn Any's does. We can't reuse
// that one because we don't want to pay the cost for every deref.
let elem = &*self.0.inner.elem;
unsafe { &*(elem as *const dyn Bounds as *const T) }
}
}
impl<T: NativeElement> DerefMut for Packed<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
// Safety:
// - Packed<T> guarantees that the content trait object wraps
// an element of type `T`.
// - We have guaranteed unique access thanks to `make_mut`.
// - This downcast works the same way as dyn Any's does. We can't reuse
// that one because we don't want to pay the cost for every deref.
let elem = &mut *self.0.make_mut().elem;
unsafe { &mut *(elem as *mut dyn Bounds as *mut T) }
}
}
impl<T: NativeElement + Debug> Debug for Packed<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
/// A sequence of content.
#[elem(Debug, Repr, PartialEq)]
#[elem(Debug, Repr)]
pub struct SequenceElem {
/// The elements.
#[required]
@ -922,19 +727,13 @@ impl Default for SequenceElem {
}
}
impl PartialEq for SequenceElem {
fn eq(&self, other: &Self) -> bool {
self.children.iter().eq(other.children.iter())
}
}
impl Repr for SequenceElem {
fn repr(&self) -> EcoString {
if self.children.is_empty() {
"[]".into()
} else {
let elements = crate::foundations::repr::pretty_array_like(
&self.children.iter().map(|c| c.inner.elem.repr()).collect::<Vec<_>>(),
&self.children.iter().map(|c| c.repr()).collect::<Vec<_>>(),
false,
);
eco_format!("sequence{}", elements)
@ -974,49 +773,8 @@ impl Repr for StyledElem {
}
}
/// Tries to extract the plain-text representation of the element.
pub trait PlainText {
/// Write this element's plain text into the given buffer.
fn plain_text(&self, text: &mut EcoString);
}
/// An error arising when trying to access a field of content.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldAccessError {
Unknown,
Unset,
Internal,
}
impl FieldAccessError {
/// Formats the error message given the content and the field name.
#[cold]
pub fn message(self, content: &Content, field: &str) -> EcoString {
let elem_name = content.elem().name();
match self {
FieldAccessError::Unknown => {
eco_format!("{elem_name} does not have field {}", field.repr())
}
FieldAccessError::Unset => {
eco_format!(
"field {} in {elem_name} is not known at this point",
field.repr()
)
}
FieldAccessError::Internal => {
eco_format!(
"internal error when accessing field {} in {elem_name} this is a bug",
field.repr()
)
}
}
}
/// Formats the error message for an `at` calls without a default value.
#[cold]
pub fn message_no_default(self, content: &Content, field: &str) -> EcoString {
let mut msg = self.message(content, field);
msg.push_str(" and no default was specified");
msg
impl<T: NativeElement> IntoValue for T {
fn into_value(self) -> Value {
Value::Content(self.pack())
}
}

View File

@ -0,0 +1,147 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use typst_syntax::Span;
use crate::foundations::{Content, Label, NativeElement};
use crate::introspection::Location;
/// A packed element of a static type.
#[derive(Clone)]
#[repr(transparent)]
pub struct Packed<T: NativeElement>(
/// Invariant: Must be of type `T`.
Content,
PhantomData<T>,
);
impl<T: NativeElement> Packed<T> {
/// Pack element while retaining its static type.
pub fn new(element: T) -> Self {
// Safety: The element is known to be of type `T`.
Packed(element.pack(), PhantomData)
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_ref(content: &Content) -> Option<&Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe { std::mem::transmute::<&Content, &Packed<T>>(content) });
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_mut(content: &mut Content) -> Option<&mut Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe {
std::mem::transmute::<&mut Content, &mut Packed<T>>(content)
});
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_owned(content: Content) -> Result<Self, Content> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Ok(unsafe { std::mem::transmute::<Content, Packed<T>>(content) });
}
Err(content)
}
/// Pack back into content.
pub fn pack(self) -> Content {
self.0
}
/// Extract the raw underlying element.
pub fn unpack(self) -> T {
// This function doesn't yet need owned self, but might in the future.
(*self).clone()
}
/// The element's span.
pub fn span(&self) -> Span {
self.0.span()
}
/// Set the span of the element.
pub fn spanned(self, span: Span) -> Self {
Self(self.0.spanned(span), PhantomData)
}
/// Accesses the label of the element.
pub fn label(&self) -> Option<Label> {
self.0.label()
}
/// Accesses the location of the element.
pub fn location(&self) -> Option<Location> {
self.0.location()
}
/// Sets the location of the element.
pub fn set_location(&mut self, location: Location) {
self.0.set_location(location);
}
pub fn as_content(&self) -> &Content {
&self.0
}
}
impl<T: NativeElement> AsRef<T> for Packed<T> {
fn as_ref(&self) -> &T {
self
}
}
impl<T: NativeElement> AsMut<T> for Packed<T> {
fn as_mut(&mut self) -> &mut T {
self
}
}
impl<T: NativeElement> Deref for Packed<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// Safety: Packed<T> guarantees that the content is of element type `T`.
unsafe { (self.0).0.data::<T>() }
}
}
impl<T: NativeElement> DerefMut for Packed<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
// Safety: Packed<T> guarantees that the content is of element type `T`.
unsafe { (self.0).0.data_mut::<T>() }
}
}
impl<T: NativeElement + Debug> Debug for Packed<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl<T: NativeElement> PartialEq for Packed<T> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T: NativeElement> Hash for Packed<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}

View File

@ -0,0 +1,426 @@
use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ptr::NonNull;
use std::sync::atomic::{self, AtomicUsize, Ordering};
use typst_syntax::Span;
use typst_utils::{fat, HashLock, SmallBitSet};
use super::vtable;
use crate::foundations::{Element, Label, NativeElement, Packed};
use crate::introspection::Location;
/// The raw, low-level implementation of content.
///
/// The `ptr` + `elem` fields implement a fat pointer setup similar to an
/// `Arc<Inner<dyn Trait>>`, but in a manual way, allowing us to have a custom
/// [vtable].
pub struct RawContent {
/// A type-erased pointer to an allocation containing two things:
/// - A header that is the same for all elements
/// - Element-specific `data` that holds the specific element
///
/// This pointer is valid for both a `Header` and an `Inner<E>` where
/// `E::ELEM == self.elem` and can be freely cast between both. This is
/// possible because
/// - `Inner<E>` is `repr(C)`
/// - The first field of `Inner<E>` is `Header`
/// - ISO/IEC 9899:TC2 C standard § 6.7.2.1 - 13 states that a pointer to a
/// structure "points to its initial member" with no padding at the start
ptr: NonNull<Header>,
/// Describes which kind of element this content holds. This is used for
///
/// - Direct comparisons, e.g. `is::<HeadingElem>()`
/// - Behavior: An `Element` is just a pointer to a `ContentVtable`
/// containing not just data, but also function pointers for various
/// element-specific operations that can be performed
///
/// It is absolutely crucial that `elem == <E as NativeElement>::ELEM` for
/// `Inner<E>` pointed to by `ptr`. Otherwise, things will go very wrong
/// since we'd be using the wrong vtable.
elem: Element,
/// The content's span.
span: Span,
}
/// The allocated part of an element's representation.
///
/// This is `repr(C)` to ensure that a pointer to the whole structure may be
/// cast to a pointer to its first field.
#[repr(C)]
struct Inner<E> {
/// It is crucial that this is the first field because we cast between
/// pointers to `Inner<E>` and pointers to `Header`. See the documentation
/// of `RawContent::ptr` for more details.
header: Header,
/// The element struct. E.g. `E = HeadingElem`.
data: E,
}
/// The header that is shared by all elements.
struct Header {
/// The element's reference count. This works just like for `Arc`.
/// Unfortunately, we have to reimplement reference counting because we
/// have a custom fat pointer and `Arc` wouldn't know how to drop its
/// contents. Something with `ManuallyDrop<Arc<_>>` might also work, but at
/// that point we're not gaining much and with the way it's implemented now
/// we can also skip the unnecessary weak reference count.
refs: AtomicUsize,
/// Metadata for the element.
meta: Meta,
/// A cell for memoizing the hash of just the `data` part of the content.
hash: HashLock,
}
/// Metadata that elements can hold.
#[derive(Clone, Hash)]
pub(super) struct Meta {
/// An optional label attached to the element.
pub label: Option<Label>,
/// The element's location which identifies it in the laid-out output.
pub location: Option<Location>,
/// Manages the element during realization.
/// - If bit 0 is set, the element is prepared.
/// - If bit n is set, the element is guarded against the n-th show rule
/// recipe from the top of the style chain (counting from 1).
pub lifecycle: SmallBitSet,
}
impl RawContent {
/// Creates raw content wrapping an element, with all metadata set to
/// default (including a detached span).
pub(super) fn new<E: NativeElement>(data: E) -> Self {
Self::create(
data,
Meta {
label: None,
location: None,
lifecycle: SmallBitSet::new(),
},
HashLock::new(),
Span::detached(),
)
}
/// Creates and allocates raw content.
fn create<E: NativeElement>(data: E, meta: Meta, hash: HashLock, span: Span) -> Self {
let raw = Box::into_raw(Box::<Inner<E>>::new(Inner {
header: Header { refs: AtomicUsize::new(1), meta, hash },
data,
}));
// Safety: `Box` always holds a non-null pointer. See also
// `Box::into_non_null` (which is unstable).
let non_null = unsafe { NonNull::new_unchecked(raw) };
// Safety: See `RawContent::ptr`.
let ptr = non_null.cast::<Header>();
Self { ptr, elem: E::ELEM, span }
}
/// Destroys raw content and deallocates.
///
/// # Safety
/// - The reference count must be zero.
/// - The raw content must be be of type `E`.
pub(super) unsafe fn drop_impl<E: NativeElement>(&mut self) {
debug_assert_eq!(self.header().refs.load(Ordering::Relaxed), 0);
// Safety:
// - The caller guarantees that the content is of type `E`.
// - Thus, `ptr` must have been created from `Box<Inner<E>>` (see
// `RawContent::ptr`).
// - And to clean it up, we can just reproduce our box.
unsafe {
let ptr = self.ptr.cast::<Inner<E>>();
drop(Box::<Inner<E>>::from_raw(ptr.as_ptr()));
}
}
/// Clones a packed element into new raw content.
pub(super) fn clone_impl<E: NativeElement>(elem: &Packed<E>) -> Self {
let raw = &elem.as_content().0;
let header = raw.header();
RawContent::create(
elem.as_ref().clone(),
header.meta.clone(),
header.hash.clone(),
raw.span,
)
}
/// Accesses the header part of the raw content.
fn header(&self) -> &Header {
// Safety: `self.ptr` is a valid pointer to a header structure.
unsafe { self.ptr.as_ref() }
}
/// Mutably accesses the header part of the raw content.
fn header_mut(&mut self) -> &mut Header {
self.make_unique();
// Safety:
// - `self.ptr` is a valid pointer to a header structure.
// - We have unique access to the backing allocation (just ensured).
unsafe { self.ptr.as_mut() }
}
/// Retrieves the contained element **without checking that the content is
/// of the correct type.**
///
/// # Safety
/// This must be preceded by a check to [`is`]. The safe API for this is
/// [`Content::to_packed`] and the [`Packed`] struct.
pub(super) unsafe fn data<E: NativeElement>(&self) -> &E {
debug_assert!(self.is::<E>());
// Safety:
// - The caller guarantees that the content is of type `E`.
// - `self.ptr` is a valid pointer to an `Inner<E>` (see
// `RawContent::ptr`).
unsafe { &self.ptr.cast::<Inner<E>>().as_ref().data }
}
/// Retrieves the contained element mutably **without checking that the
/// content is of the correct type.**
///
/// Ensures that the element's allocation is unique.
///
/// # Safety
/// This must be preceded by a check to [`is`]. The safe API for this is
/// [`Content::to_packed_mut`] and the [`Packed`] struct.
pub(super) unsafe fn data_mut<E: NativeElement>(&mut self) -> &mut E {
debug_assert!(self.is::<E>());
// Ensure that the memoized hash is reset because we may mutate the
// element.
self.header_mut().hash.reset();
// Safety:
// - The caller guarantees that the content is of type `E`.
// - `self.ptr` is a valid pointer to an `Inner<E>` (see
// `RawContent::ptr`).
// - We have unique access to the backing allocation (due to header_mut).
unsafe { &mut self.ptr.cast::<Inner<E>>().as_mut().data }
}
/// Ensures that we have unique access to the backing allocation by cloning
/// if the reference count exceeds 1. This is used before performing
/// mutable operations, implementing a clone-on-write scheme.
fn make_unique(&mut self) {
if self.header().refs.load(Ordering::Relaxed) > 1 {
*self = self.handle().clone();
}
}
/// Retrieves the element this content is for.
pub(super) fn elem(&self) -> Element {
self.elem
}
/// Whether this content holds an element of type `E`.
pub(super) fn is<E: NativeElement>(&self) -> bool {
self.elem == E::ELEM
}
/// Retrieves the content's span.
pub(super) fn span(&self) -> Span {
self.span
}
/// Retrieves the content's span mutably.
pub(super) fn span_mut(&mut self) -> &mut Span {
&mut self.span
}
/// Retrieves the content's metadata.
pub(super) fn meta(&self) -> &Meta {
&self.header().meta
}
/// Retrieves the content's metadata mutably.
pub(super) fn meta_mut(&mut self) -> &mut Meta {
&mut self.header_mut().meta
}
/// Casts into a trait object for a given trait if the packed element
/// implements said trait.
pub(super) fn with<C>(&self) -> Option<&C>
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`. Since
// any `Packed<T>` is repr(transparent) with `Content` and `RawContent`,
// we can also use a `*const RawContent` pointer.
let vtable = (self.elem.vtable().capability)(TypeId::of::<C>())?;
let data = self as *const Self as *const ();
Some(unsafe { &*fat::from_raw_parts(data, vtable.as_ptr()) })
}
/// Casts into a mutable trait object for a given trait if the packed
/// element implements said trait.
pub(super) fn with_mut<C>(&mut self) -> Option<&mut C>
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`. Since
// any `Packed<T>` is repr(transparent) with `Content` and `RawContent`,
// we can also use a `*const Content` pointer.
//
// The resulting trait object contains an `&mut Packed<T>`. We do _not_
// need to ensure that we hold the only reference to the `Arc` here
// because `Packed<T>`'s DerefMut impl will take care of that if mutable
// access is required.
let vtable = (self.elem.vtable().capability)(TypeId::of::<C>())?;
let data = self as *mut Self as *mut ();
Some(unsafe { &mut *fat::from_raw_parts_mut(data, vtable.as_ptr()) })
}
}
impl RawContent {
/// Retrieves the element's vtable.
pub(super) fn handle(&self) -> vtable::ContentHandle<&RawContent> {
// Safety `self.elem.vtable()` is a matching vtable for `self`.
unsafe { vtable::Handle::new(self, self.elem.vtable()) }
}
/// Retrieves the element's vtable.
pub(super) fn handle_mut(&mut self) -> vtable::ContentHandle<&mut RawContent> {
// Safety `self.elem.vtable()` is a matching vtable for `self`.
unsafe { vtable::Handle::new(self, self.elem.vtable()) }
}
/// Retrieves the element's vtable.
pub(super) fn handle_pair<'a, 'b>(
&'a self,
other: &'b RawContent,
) -> Option<vtable::ContentHandle<(&'a RawContent, &'b RawContent)>> {
(self.elem == other.elem).then(|| {
// Safety:
// - `self.elem.vtable()` is a matching vtable for `self`.
// - It's also matching for `other` because `self.elem == other.elem`.
unsafe { vtable::Handle::new((self, other), self.elem.vtable()) }
})
}
}
impl Debug for RawContent {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.handle().debug(f)
}
}
impl Clone for RawContent {
fn clone(&self) -> Self {
// See Arc's clone impl for details about memory ordering.
let prev = self.header().refs.fetch_add(1, Ordering::Relaxed);
// See Arc's clone impl details about guarding against incredibly
// degenerate programs.
if prev > isize::MAX as usize {
ref_count_overflow(self.ptr, self.elem, self.span);
}
Self { ptr: self.ptr, elem: self.elem, span: self.span }
}
}
impl Drop for RawContent {
fn drop(&mut self) {
// Drop our ref-count. If there was more than one content before
// (including this one), we shouldn't deallocate. See Arc's drop impl
// for details about memory ordering.
if self.header().refs.fetch_sub(1, Ordering::Release) != 1 {
return;
}
// See Arc's drop impl for details.
atomic::fence(Ordering::Acquire);
// Safety:
// No other content references the backing allocation (just checked)
unsafe {
self.handle_mut().drop();
}
}
}
impl PartialEq for RawContent {
fn eq(&self, other: &Self) -> bool {
let Some(handle) = self.handle_pair(other) else { return false };
handle
.eq()
.unwrap_or_else(|| handle.fields().all(|handle| handle.eq()))
}
}
impl Hash for RawContent {
fn hash<H: Hasher>(&self, state: &mut H) {
self.elem.hash(state);
let header = self.header();
header.meta.hash(state);
header.hash.get_or_insert_with(|| self.handle().hash()).hash(state);
self.span.hash(state);
}
}
// Safety:
// - Works like `Arc`.
// - `NativeElement` implies `Send` and `Sync`, see below.
unsafe impl Sync for RawContent {}
unsafe impl Send for RawContent {}
fn _ensure_send_sync<T: NativeElement>() {
fn needs_send_sync<T: Send + Sync>() {}
needs_send_sync::<T>();
}
#[cold]
fn ref_count_overflow(ptr: NonNull<Header>, elem: Element, span: Span) -> ! {
// Drop to decrement the ref count to counter the increment in `clone()`
drop(RawContent { ptr, elem, span });
panic!("reference count overflow");
}
#[cfg(test)]
mod tests {
use crate::foundations::{NativeElement, Repr, StyleChain, Value};
use crate::introspection::Location;
use crate::model::HeadingElem;
use crate::text::TextElem;
#[test]
fn test_miri() {
let styles = StyleChain::default();
let mut first = HeadingElem::new(TextElem::packed("Hi!")).with_offset(2).pack();
let hash1 = typst_utils::hash128(&first);
first.set_location(Location::new(10));
let _ = format!("{first:?}");
let _ = first.repr();
assert!(first.is::<HeadingElem>());
assert!(!first.is::<TextElem>());
assert_eq!(first.to_packed::<TextElem>(), None);
assert_eq!(first.location(), Some(Location::new(10)));
assert_eq!(first.field_by_name("offset"), Ok(Value::Int(2)));
assert!(!first.has("depth".into()));
let second = first.clone();
first.materialize(styles);
let first_packed = first.to_packed::<HeadingElem>().unwrap();
let second_packed = second.to_packed::<HeadingElem>().unwrap();
assert!(first.has("depth".into()));
assert!(!second.has("depth".into()));
assert!(first_packed.depth.is_set());
assert!(!second_packed.depth.is_set());
assert_ne!(first, second);
assert_ne!(hash1, typst_utils::hash128(&first));
}
}

View File

@ -0,0 +1,383 @@
//! A custom [vtable] implementation for content.
//!
//! This is similar to what is generated by the Rust compiler under the hood
//! when using trait objects. However, ours has two key advantages:
//!
//! - It can store a _slice_ of sub-vtables for field-specific operations.
//! - It can store not only methods, but also plain data, allowing us to access
//! that data without going through dynamic dispatch.
//!
//! Because our vtable pointers are backed by `static` variables, we can also
//! perform checks for element types by comparing raw vtable pointers giving us
//! `RawContent::is` without dynamic dispatch.
//!
//! Overall, the custom vtable gives us just a little more flexibility and
//! optimizability than using built-in trait objects.
//!
//! Note that all vtable methods receive elements of type `Packed<E>`, but some
//! only perform actions on the `E` itself, with the shared part kept outside of
//! the vtable (e.g. `hash`), while some perform the full action (e.g. `clone`
//! as it needs to return new, fully populated raw content). Which one it is, is
//! documented for each.
//!
//! # Safety
//! This module contains a lot of `unsafe` keywords, but almost all of it is the
//! same and quite straightfoward. All function pointers that operate on a
//! specific element type are marked as unsafe. In combination with `repr(C)`,
//! this grants us the ability to safely transmute a `ContentVtable<Packed<E>>`
//! into a `ContentVtable<RawContent>` (or just short `ContentVtable`). Callers
//! of functions marked as unsafe have to guarantee that the `ContentVtable` was
//! transmuted from the same `E` as the RawContent was constructed from. The
//! `Handle` struct provides a safe access layer, moving the guarantee that the
//! vtable is matching into a single spot.
//!
//! [vtable]: https://en.wikipedia.org/wiki/Virtual_method_table
use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use std::ops::Deref;
use std::ptr::NonNull;
use ecow::EcoString;
use super::raw::RawContent;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
Args, CastInfo, Construct, Content, LazyElementStore, NativeElement, NativeScope,
Packed, Repr, Scope, Set, StyleChain, Styles, Value,
};
use crate::text::{Lang, LocalName, Region};
/// Encapsulates content and a vtable, granting safe access to vtable operations.
pub(super) struct Handle<T, V: 'static>(T, &'static V);
impl<T, V> Handle<T, V> {
/// Produces a new handle from content and a vtable.
///
/// # Safety
/// The content and vtable must be matching, i.e. `vtable` must be derived
/// from the content's vtable.
pub(super) unsafe fn new(content: T, vtable: &'static V) -> Self {
Self(content, vtable)
}
}
impl<T, V> Deref for Handle<T, V> {
type Target = V;
fn deref(&self) -> &Self::Target {
self.1
}
}
pub(super) type ContentHandle<T> = Handle<T, ContentVtable>;
pub(super) type FieldHandle<T> = Handle<T, FieldVtable>;
/// A vtable for performing element-specific actions on type-erased content.
/// Also contains general metadata for the specific element.
#[repr(C)]
pub struct ContentVtable<T: 'static = RawContent> {
/// The element's normal name, as in code.
pub(super) name: &'static str,
/// The element's title-cased name.
pub(super) title: &'static str,
/// The element's documentation (as Markdown).
pub(super) docs: &'static str,
/// Search keywords for the documentation.
pub(super) keywords: &'static [&'static str],
/// Subvtables for all fields of the element.
pub(super) fields: &'static [FieldVtable<T>],
/// Determines the ID for a field name. This is a separate function instead
/// of searching through `fields` so that Rust can generate optimized code
/// for the string matching.
pub(super) field_id: fn(name: &str) -> Option<u8>,
/// The constructor of the element.
pub(super) construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>,
/// The set rule of the element.
pub(super) set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>,
/// The element's local name in a specific lang-region pairing.
pub(super) local_name: Option<fn(Lang, Option<Region>) -> &'static str>,
/// Produces the associated [`Scope`] of the element.
pub(super) scope: fn() -> Scope,
/// If the `capability` function returns `Some(p)`, then `p` must be a valid
/// pointer to a native Rust vtable of `Packed<Self>` w.r.t to the trait `C`
/// where `capability` is `TypeId::of::<dyn C>()`.
pub(super) capability: fn(capability: TypeId) -> Option<NonNull<()>>,
/// The `Drop` impl (for the whole raw content). The content must have a
/// reference count of zero and may not be used anymore after `drop` was
/// called.
pub(super) drop: unsafe fn(&mut RawContent),
/// The `Clone` impl (for the whole raw content).
pub(super) clone: unsafe fn(&T) -> RawContent,
/// The `Hash` impl (for just the element).
pub(super) hash: unsafe fn(&T) -> u128,
/// The `Debug` impl (for just the element).
pub(super) debug: unsafe fn(&T, &mut Formatter) -> fmt::Result,
/// The `PartialEq` impl (for just the element). If this is `None`,
/// field-wise equality checks (via `FieldVtable`) should be performed.
pub(super) eq: Option<unsafe fn(&T, &T) -> bool>,
/// The `Repr` impl (for just the element). If this is `None`, a generic
/// name + fields representation should be produced.
pub(super) repr: Option<unsafe fn(&T) -> EcoString>,
/// Produces a reference to a `static` variable holding a `LazyElementStore`
/// that is unique for this element and can be populated with data that is
/// somewhat costly to initialize at runtime and shouldn't be initialized
/// over and over again. Must be a function rather than a direct reference
/// so that we can store the vtable in a `const` without Rust complaining
/// about the presence of interior mutability.
pub(super) store: fn() -> &'static LazyElementStore,
}
impl ContentVtable {
/// Creates the vtable for an element.
pub const fn new<E: NativeElement>(
name: &'static str,
title: &'static str,
docs: &'static str,
fields: &'static [FieldVtable<Packed<E>>],
field_id: fn(name: &str) -> Option<u8>,
capability: fn(TypeId) -> Option<NonNull<()>>,
store: fn() -> &'static LazyElementStore,
) -> ContentVtable<Packed<E>> {
ContentVtable {
name,
title,
docs,
keywords: &[],
fields,
field_id,
construct: <E as Construct>::construct,
set: <E as Set>::set,
local_name: None,
scope: || Scope::new(),
capability,
drop: RawContent::drop_impl::<E>,
clone: RawContent::clone_impl::<E>,
hash: |elem| typst_utils::hash128(elem.as_ref()),
debug: |elem, f| Debug::fmt(elem.as_ref(), f),
eq: None,
repr: None,
store,
}
}
/// Retrieves the vtable of the element with the given ID.
pub fn field(&self, id: u8) -> Option<&'static FieldVtable> {
self.fields.get(usize::from(id))
}
}
impl<E: NativeElement> ContentVtable<Packed<E>> {
/// Attaches search keywords for the documentation.
pub const fn with_keywords(mut self, keywords: &'static [&'static str]) -> Self {
self.keywords = keywords;
self
}
/// Takes a [`Repr`] impl into account.
pub const fn with_repr(mut self) -> Self
where
E: Repr,
{
self.repr = Some(|e| E::repr(&**e));
self
}
/// Takes a [`PartialEq`] impl into account.
pub const fn with_partial_eq(mut self) -> Self
where
E: PartialEq,
{
self.eq = Some(|a, b| E::eq(&**a, &**b));
self
}
/// Takes a [`LocalName`] impl into account.
pub const fn with_local_name(mut self) -> Self
where
Packed<E>: LocalName,
{
self.local_name = Some(<Packed<E> as LocalName>::local_name);
self
}
/// Takes a [`NativeScope`] impl into account.
pub const fn with_scope(mut self) -> Self
where
E: NativeScope,
{
self.scope = || E::scope();
self
}
/// Type-erases the data.
pub const fn erase(self) -> ContentVtable {
// Safety:
// - `ContentVtable` is `repr(C)`.
// - `ContentVtable` does not hold any `E`-specific data except for
// function pointers.
// - All functions pointers have the same memory layout.
// - All functions containing `E` are marked as unsafe and callers need
// to uphold the guarantee that they only call them with raw content
// that is of type `E`.
// - `Packed<E>` and `RawContent` have the exact same memory layout
// because of `repr(transparent)`.
unsafe {
std::mem::transmute::<ContentVtable<Packed<E>>, ContentVtable<RawContent>>(
self,
)
}
}
}
impl<T> ContentHandle<T> {
/// Provides safe access to operations for the field with the given `id`.
pub(super) fn field(self, id: u8) -> Option<FieldHandle<T>> {
self.fields.get(usize::from(id)).map(|vtable| {
// Safety: Field vtables are of same type as the content vtable.
unsafe { Handle::new(self.0, vtable) }
})
}
/// Provides safe access to all field operations.
pub(super) fn fields(self) -> impl Iterator<Item = FieldHandle<T>>
where
T: Copy,
{
self.fields.iter().map(move |vtable| {
// Safety: Field vtables are of same type as the content vtable.
unsafe { Handle::new(self.0, vtable) }
})
}
}
impl ContentHandle<&RawContent> {
/// See [`ContentVtable::debug`].
pub fn debug(&self, f: &mut Formatter) -> fmt::Result {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.debug)(self.0, f) }
}
/// See [`ContentVtable::repr`].
pub fn repr(&self) -> Option<EcoString> {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { self.1.repr.map(|f| f(self.0)) }
}
/// See [`ContentVtable::clone`].
pub fn clone(&self) -> RawContent {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.clone)(self.0) }
}
/// See [`ContentVtable::hash`].
pub fn hash(&self) -> u128 {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.hash)(self.0) }
}
}
impl ContentHandle<&mut RawContent> {
/// See [`ContentVtable::drop`].
pub unsafe fn drop(&mut self) {
// Safety:
// - `Handle` has the invariant that the vtable is matching.
// - The caller satifies the requirements of `drop`
unsafe { (self.1.drop)(self.0) }
}
}
impl ContentHandle<(&RawContent, &RawContent)> {
/// See [`ContentVtable::eq`].
pub fn eq(&self) -> Option<bool> {
// Safety: `Handle` has the invariant that the vtable is matching.
let (a, b) = self.0;
unsafe { self.1.eq.map(|f| f(a, b)) }
}
}
/// A vtable for performing field-specific actions on type-erased
/// content. Also contains general metadata for the specific field.
#[repr(C)]
pub struct FieldVtable<T: 'static = RawContent> {
/// The field's name, as in code.
pub(super) name: &'static str,
/// The fields's documentation (as Markdown).
pub(super) docs: &'static str,
/// Whether the field's parameter is positional.
pub(super) positional: bool,
/// Whether the field's parameter is variadic.
pub(super) variadic: bool,
/// Whether the field's parameter is required.
pub(super) required: bool,
/// Whether the field can be set via a set rule.
pub(super) settable: bool,
/// Whether the field is synthesized (i.e. initially not present).
pub(super) synthesized: bool,
/// Reflects what types the field's parameter accepts.
pub(super) input: fn() -> CastInfo,
/// Produces the default value of the field, if any. This would e.g. be
/// `None` for a required parameter.
pub(super) default: Option<fn() -> Value>,
/// Whether the field is set on the given element. Always true for required
/// fields, but can be false for settable or synthesized fields.
pub(super) has: unsafe fn(elem: &T) -> bool,
/// Retrieves the field and [turns it into a
/// value](crate::foundations::IntoValue).
pub(super) get: unsafe fn(elem: &T) -> Option<Value>,
/// Retrieves the field given styles. The resulting value may come from the
/// element, the style chain, or a mix (if it's a
/// [`Fold`](crate::foundations::Fold) field).
pub(super) get_with_styles: unsafe fn(elem: &T, StyleChain) -> Option<Value>,
/// Retrieves the field just from the styles.
pub(super) get_from_styles: fn(StyleChain) -> Option<Value>,
/// Sets the field from the styles if it is currently unset. (Or merges
/// with the style data in case of a `Fold` field).
pub(super) materialize: unsafe fn(elem: &mut T, styles: StyleChain),
/// Compares the field for equality.
pub(super) eq: unsafe fn(a: &T, b: &T) -> bool,
}
impl FieldHandle<&RawContent> {
/// See [`FieldVtable::has`].
pub fn has(&self) -> bool {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.has)(self.0) }
}
/// See [`FieldVtable::get`].
pub fn get(&self) -> Option<Value> {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.get)(self.0) }
}
/// See [`FieldVtable::get_with_styles`].
pub fn get_with_styles(&self, styles: StyleChain) -> Option<Value> {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.get_with_styles)(self.0, styles) }
}
}
impl FieldHandle<&mut RawContent> {
/// See [`FieldVtable::materialize`].
pub fn materialize(&mut self, styles: StyleChain) {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.materialize)(self.0, styles) }
}
}
impl FieldHandle<(&RawContent, &RawContent)> {
/// See [`FieldVtable::eq`].
pub fn eq(&self) -> bool {
// Safety: `Handle` has the invariant that the vtable is matching.
let (a, b) = self.0;
unsafe { (self.1.eq)(a, b) }
}
}

View File

@ -114,7 +114,7 @@ impl Dict {
}
/// Iterate over pairs of references to the contained keys and values.
pub fn iter(&self) -> indexmap::map::Iter<Str, Value> {
pub fn iter(&self) -> indexmap::map::Iter<'_, Str, Value> {
self.0.iter()
}

View File

@ -16,6 +16,21 @@ impl Duration {
pub fn is_zero(&self) -> bool {
self.0.is_zero()
}
/// Decomposes the time into whole weeks, days, hours, minutes, and seconds.
pub fn decompose(&self) -> [i64; 5] {
let mut tmp = self.0;
let weeks = tmp.whole_weeks();
tmp -= weeks.weeks();
let days = tmp.whole_days();
tmp -= days.days();
let hours = tmp.whole_hours();
tmp -= hours.hours();
let minutes = tmp.whole_minutes();
tmp -= minutes.minutes();
let seconds = tmp.whole_seconds();
[weeks, days, hours, minutes, seconds]
}
}
#[scope]
@ -118,34 +133,25 @@ impl Debug for Duration {
impl Repr for Duration {
fn repr(&self) -> EcoString {
let mut tmp = self.0;
let [weeks, days, hours, minutes, seconds] = self.decompose();
let mut vec = Vec::with_capacity(5);
let weeks = tmp.whole_seconds() / 604_800.0 as i64;
if weeks != 0 {
vec.push(eco_format!("weeks: {}", weeks.repr()));
}
tmp -= weeks.weeks();
let days = tmp.whole_days();
if days != 0 {
vec.push(eco_format!("days: {}", days.repr()));
}
tmp -= days.days();
let hours = tmp.whole_hours();
if hours != 0 {
vec.push(eco_format!("hours: {}", hours.repr()));
}
tmp -= hours.hours();
let minutes = tmp.whole_minutes();
if minutes != 0 {
vec.push(eco_format!("minutes: {}", minutes.repr()));
}
tmp -= minutes.minutes();
let seconds = tmp.whole_seconds();
if seconds != 0 {
vec.push(eco_format!("seconds: {}", seconds.repr()));
}

View File

@ -210,3 +210,25 @@ cast! {
fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> {
s.replace(repr::MINUS_SIGN, "-").parse()
}
/// A floating-point number that must be positive (strictly larger than zero).
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct PositiveF64(f64);
impl PositiveF64 {
/// Wrap a float if it is positive.
pub fn new(value: f64) -> Option<Self> {
(value > 0.0).then_some(Self(value))
}
/// Get the underlying value.
pub fn get(self) -> f64 {
self.0
}
}
cast! {
PositiveF64,
self => self.get().into_value(),
v: f64 => Self::new(v).ok_or("number must be positive")?,
}

View File

@ -20,7 +20,7 @@ use crate::foundations::{
///
/// You can call a function by writing a comma-separated list of function
/// _arguments_ enclosed in parentheses directly after the function name.
/// Additionally, you can pass any number of trailing content blocks arguments
/// Additionally, you can pass any number of trailing content block arguments
/// to a function _after_ the normal argument list. If the normal argument list
/// would become empty, it can be omitted. Typst supports positional and named
/// arguments. The former are identified by position and type, while the latter
@ -307,7 +307,7 @@ impl Func {
) -> SourceResult<Value> {
match &self.repr {
Repr::Native(native) => {
let value = (native.function)(engine, context, &mut args)?;
let value = (native.function.0)(engine, context, &mut args)?;
args.finish()?;
Ok(value)
}
@ -491,8 +491,8 @@ pub trait NativeFunc {
/// Defines a native function.
#[derive(Debug)]
pub struct NativeFuncData {
/// Invokes the function from Typst.
pub function: fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value>,
/// The implementation of the function.
pub function: NativeFuncPtr,
/// The function's normal name (e.g. `align`), as exposed to Typst.
pub name: &'static str,
/// The function's title case name (e.g. `Align`).
@ -504,11 +504,11 @@ pub struct NativeFuncData {
/// Whether this function makes use of context.
pub contextual: bool,
/// Definitions in the scope of the function.
pub scope: LazyLock<Scope>,
pub scope: DynLazyLock<Scope>,
/// A list of parameter information for each parameter.
pub params: LazyLock<Vec<ParamInfo>>,
pub params: DynLazyLock<Vec<ParamInfo>>,
/// Information about the return value of this function.
pub returns: LazyLock<CastInfo>,
pub returns: DynLazyLock<CastInfo>,
}
cast! {
@ -516,6 +516,28 @@ cast! {
self => Func::from(self).into_value(),
}
/// A pointer to a native function's implementation.
pub struct NativeFuncPtr(pub &'static NativeFuncSignature);
/// The signature of a native function's implementation.
type NativeFuncSignature =
dyn Fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value> + Send + Sync;
impl Debug for NativeFuncPtr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad("NativeFuncPtr(..)")
}
}
/// A `LazyLock` that uses a static closure for initialization instead of only
/// working with function pointers.
///
/// Can be created from a normal function or closure by prepending with a `&`,
/// e.g. `LazyLock::new(&|| "hello")`. Can be created from a dynamic closure
/// by allocating and then leaking it. This is equivalent to having it
/// statically allocated, but allows for it to be generated at runtime.
type DynLazyLock<T> = LazyLock<T, &'static (dyn Fn() -> T + Send + Sync)>;
/// Describes a function parameter.
#[derive(Debug, Clone)]
pub struct ParamInfo {

View File

@ -1,4 +1,6 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError};
use std::num::{
NonZeroI64, NonZeroIsize, NonZeroU32, NonZeroU64, NonZeroUsize, ParseIntError,
};
use ecow::{eco_format, EcoString};
use smallvec::SmallVec;
@ -482,3 +484,16 @@ cast! {
"number too large"
})?,
}
cast! {
NonZeroU32,
self => Value::Int(self.get() as _),
v: i64 => v
.try_into()
.and_then(|v: u32| v.try_into())
.map_err(|_| if v <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}

View File

@ -1,7 +1,8 @@
use ecow::{eco_format, EcoString};
use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::foundations::{func, scope, ty, Repr, Str};
use crate::diag::StrResult;
use crate::foundations::{bail, func, scope, ty, Repr, Str};
/// A label for an element.
///
@ -27,7 +28,8 @@ use crate::foundations::{func, scope, ty, Repr, Str};
/// # Syntax
/// This function also has dedicated syntax: You can create a label by enclosing
/// its name in angle brackets. This works both in markup and code. A label's
/// name can contain letters, numbers, `_`, `-`, `:`, and `.`.
/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. A label cannot
/// be empty.
///
/// Note that there is a syntactical difference when using the dedicated syntax
/// for this function. In the code below, the `[<a>]` terminates the heading and
@ -50,8 +52,11 @@ pub struct Label(PicoStr);
impl Label {
/// Creates a label from an interned string.
pub fn new(name: PicoStr) -> Self {
Self(name)
///
/// Returns `None` if the given string is empty.
pub fn new(name: PicoStr) -> Option<Self> {
const EMPTY: PicoStr = PicoStr::constant("");
(name != EMPTY).then_some(Self(name))
}
/// Resolves the label to a string.
@ -70,16 +75,25 @@ impl Label {
/// Creates a label from a string.
#[func(constructor)]
pub fn construct(
/// The name of the label.
/// The name of the label. Must not be empty.
name: Str,
) -> Label {
Self(PicoStr::intern(name.as_str()))
) -> StrResult<Label> {
if name.is_empty() {
bail!("label name must not be empty");
}
Ok(Self(PicoStr::intern(name.as_str())))
}
}
impl Repr for Label {
fn repr(&self) -> EcoString {
eco_format!("<{}>", self.resolve())
let resolved = self.resolve();
if typst_syntax::is_valid_label_literal_id(&resolved) {
eco_format!("<{resolved}>")
} else {
eco_format!("label({})", resolved.repr())
}
}
}

View File

@ -17,7 +17,6 @@ mod datetime;
mod decimal;
mod dict;
mod duration;
mod element;
mod fields;
mod float;
mod func;
@ -49,7 +48,6 @@ pub use self::datetime::*;
pub use self::decimal::*;
pub use self::dict::*;
pub use self::duration::*;
pub use self::element::*;
pub use self::fields::*;
pub use self::float::*;
pub use self::func::*;
@ -69,6 +67,7 @@ pub use self::ty::*;
pub use self::value::*;
pub use self::version::*;
pub use typst_macros::{scope, ty};
use typst_syntax::SyntaxMode;
#[rustfmt::skip]
#[doc(hidden)]
@ -83,7 +82,6 @@ use typst_syntax::Spanned;
use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine;
use crate::routines::EvalMode;
use crate::{Feature, Features};
/// Hook up all `foundations` definitions.
@ -273,8 +271,8 @@ pub fn eval(
/// #eval("1_2^3", mode: "math")
/// ```
#[named]
#[default(EvalMode::Code)]
mode: EvalMode,
#[default(SyntaxMode::Code)]
mode: SyntaxMode,
/// A scope of definitions that are made available.
///
/// ```example

View File

@ -7,9 +7,10 @@ use typst_syntax::FileId;
use crate::diag::{bail, DeprecationSink, StrResult};
use crate::foundations::{repr, ty, Content, Scope, Value};
/// A module of definitions.
/// A collection of variables and functions that are commonly related to
/// a single theme.
///
/// A module
/// A module can
/// - be built-in
/// - stem from a [file import]($scripting/#modules)
/// - stem from a [package import]($scripting/#packages) (and thus indirectly
@ -18,11 +19,8 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
///
/// You can access definitions from the module using [field access
/// notation]($scripting/#fields) and interact with it using the [import and
/// include syntaxes]($scripting/#modules). Alternatively, it is possible to
/// convert a module to a dictionary, and therefore access its contents
/// dynamically, using the [dictionary constructor]($dictionary/#constructor).
/// include syntaxes]($scripting/#modules).
///
/// # Example
/// ```example
/// <<< #import "utils.typ"
/// <<< #utils.add(2, 5)
@ -33,6 +31,20 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
/// >>>
/// >>> #(-3)
/// ```
///
/// You can check whether a definition is present in a module using the `{in}`
/// operator, with a string on the left-hand side. This can be useful to
/// [conditionally access]($category/foundations/std/#conditional-access)
/// definitions in a module.
///
/// ```example
/// #("table" in std) \
/// #("nope" in std)
/// ```
///
/// Alternatively, it is possible to convert a module to a dictionary, and
/// therefore access its contents dynamically, using the [dictionary
/// constructor]($dictionary/#constructor).
#[ty(cast)]
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]

View File

@ -558,6 +558,7 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
(Str(a), Dict(b)) => Some(b.contains(a)),
(Str(a), Module(b)) => Some(b.scope().get(a).is_some()),
(a, Array(b)) => Some(b.contains(a.clone())),
_ => Option::None,

View File

@ -151,8 +151,8 @@ pub fn plugin(
/// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
source: Spanned<DataSource>,
) -> SourceResult<Module> {
let data = source.load(engine.world)?;
Plugin::module(data).at(source.span)
let loaded = source.load(engine.world)?;
Plugin::module(loaded.data).at(source.span)
}
#[scope]

View File

@ -8,8 +8,7 @@ use typst_syntax::Span;
use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult};
use crate::foundations::{
Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType,
Type, Value,
Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Value,
};
use crate::{Category, Library};
@ -149,15 +148,15 @@ impl Scope {
/// Define a native type.
#[track_caller]
pub fn define_type<T: NativeType>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Type::from(data))
let ty = T::ty();
self.define(ty.short_name(), ty)
}
/// Define a native element.
#[track_caller]
pub fn define_elem<T: NativeElement>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Element::from(data))
let elem = T::ELEM;
self.define(elem.name(), elem)
}
/// Define a built-in with compile-time known name and returns a mutable

View File

@ -21,12 +21,12 @@ macro_rules! __select_where {
let mut fields = ::smallvec::SmallVec::new();
$(
fields.push((
<$ty as $crate::foundations::Fields>::Enum::$field as u8,
<$ty>::$field.index(),
$crate::foundations::IntoValue::into_value($value),
));
)*
$crate::foundations::Selector::Elem(
<$ty as $crate::foundations::NativeElement>::elem(),
<$ty as $crate::foundations::NativeElement>::ELEM,
Some(fields),
)
}};

View File

@ -12,8 +12,8 @@ use typst_utils::LazyHash;
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr,
Selector,
cast, ty, Content, Context, Element, Field, Func, NativeElement, OneOrMultiple,
RefableProperty, Repr, Selector, SettableProperty,
};
use crate::text::{FontFamily, FontList, TextElem};
@ -48,7 +48,16 @@ impl Styles {
/// If the property needs folding and the value is already contained in the
/// style map, `self` contributes the outer values and `value` is the inner
/// one.
pub fn set(&mut self, style: impl Into<Style>) {
pub fn set<E, const I: u8>(&mut self, field: Field<E, I>, value: E::Type)
where
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
self.push(Property::new(field, value));
}
/// Add a new style to the list.
pub fn push(&mut self, style: impl Into<Style>) {
self.0.push(LazyHash::new(style.into()));
}
@ -101,22 +110,25 @@ impl Styles {
}
/// Whether there is a style for the given field of the given element.
pub fn has<T: NativeElement>(&self, field: u8) -> bool {
let elem = T::elem();
pub fn has<E: NativeElement, const I: u8>(&self, _: Field<E, I>) -> bool {
let elem = E::ELEM;
self.0
.iter()
.filter_map(|style| style.property())
.any(|property| property.is_of(elem) && property.id == field)
.any(|property| property.is_of(elem) && property.id == I)
}
/// Set a font family composed of a preferred family and existing families
/// from a style chain.
pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
self.set(TextElem::set_font(FontList(
std::iter::once(preferred)
.chain(TextElem::font_in(existing).into_iter().cloned())
.collect(),
)));
self.set(
TextElem::font,
FontList(
std::iter::once(preferred)
.chain(existing.get_ref(TextElem::font).into_iter().cloned())
.collect(),
),
);
}
}
@ -281,14 +293,14 @@ pub struct Property {
impl Property {
/// Create a new property from a key-value pair.
pub fn new<E, T>(id: u8, value: T) -> Self
pub fn new<E, const I: u8>(_: Field<E, I>, value: E::Type) -> Self
where
E: NativeElement,
T: Debug + Clone + Hash + Send + Sync + 'static,
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
Self {
elem: E::elem(),
id,
elem: E::ELEM,
id: I,
value: Block::new(value),
span: Span::detached(),
liftable: false,
@ -340,8 +352,11 @@ impl Block {
}
/// Downcasts the block to the specified type.
fn downcast<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref()
fn downcast<T: 'static>(&self, func: Element, id: u8) -> &T {
self.0
.as_any()
.downcast_ref()
.unwrap_or_else(|| block_wrong_type(func, id, self))
}
}
@ -528,6 +543,103 @@ impl<'a> StyleChain<'a> {
Self { head: &root.0, tail: None }
}
/// Retrieves the value of the given field from the style chain.
///
/// A `Field` value is a zero-sized value that specifies which field of an
/// element you want to retrieve on the type-system level. It also ensures
/// that Rust can infer the correct return type.
///
/// Should be preferred over [`get_cloned`](Self::get_cloned) or
/// [`get_ref`](Self::get_ref), but is only available for [`Copy`] types.
/// For other types an explicit decision needs to be made whether cloning is
/// necessary.
pub fn get<E, const I: u8>(self, field: Field<E, I>) -> E::Type
where
E: SettableProperty<I>,
E::Type: Copy,
{
self.get_cloned(field)
}
/// Retrieves and clones the value from the style chain.
///
/// Prefer [`get`](Self::get) if the type is `Copy` and
/// [`get_ref`](Self::get_ref) if a reference suffices.
pub fn get_cloned<E, const I: u8>(self, _: Field<E, I>) -> E::Type
where
E: SettableProperty<I>,
{
if let Some(fold) = E::FOLD {
self.get_folded::<E::Type>(E::ELEM, I, fold, E::default())
} else {
self.get_unfolded::<E::Type>(E::ELEM, I)
.cloned()
.unwrap_or_else(E::default)
}
}
/// Retrieves a reference to the value of the given field from the style
/// chain.
///
/// Not possible if the value needs folding.
pub fn get_ref<E, const I: u8>(self, _: Field<E, I>) -> &'a E::Type
where
E: RefableProperty<I>,
{
self.get_unfolded(E::ELEM, I).unwrap_or_else(|| E::default_ref())
}
/// Retrieves the value and then immediately [resolves](Resolve) it.
pub fn resolve<E, const I: u8>(
self,
field: Field<E, I>,
) -> <E::Type as Resolve>::Output
where
E: SettableProperty<I>,
E::Type: Resolve,
{
self.get_cloned(field).resolve(self)
}
/// Retrieves a reference to a field, also taking into account the
/// instance's value if any.
fn get_unfolded<T: 'static>(self, func: Element, id: u8) -> Option<&'a T> {
self.find(func, id).map(|block| block.downcast(func, id))
}
/// Retrieves a reference to a field, also taking into account the
/// instance's value if any.
fn get_folded<T: 'static + Clone>(
self,
func: Element,
id: u8,
fold: fn(T, T) -> T,
default: T,
) -> T {
let iter = self
.properties(func, id)
.map(|block| block.downcast::<T>(func, id).clone());
if let Some(folded) = iter.reduce(fold) {
fold(folded, default)
} else {
default
}
}
/// Iterate over all values for the given property in the chain.
fn find(self, func: Element, id: u8) -> Option<&'a Block> {
self.properties(func, id).next()
}
/// Iterate over all values for the given property in the chain.
fn properties(self, func: Element, id: u8) -> impl Iterator<Item = &'a Block> {
self.entries()
.filter_map(|style| style.property())
.filter(move |property| property.is(func, id))
.map(|property| &property.value)
}
/// Make the given chainable the first link of this chain.
///
/// The resulting style chain contains styles from `local` as well as
@ -540,80 +652,6 @@ impl<'a> StyleChain<'a> {
Chainable::chain(local, self)
}
/// Cast the first value for the given property in the chain.
pub fn get<T: Clone + 'static>(
self,
func: Element,
id: u8,
inherent: Option<&T>,
default: impl Fn() -> T,
) -> T {
self.properties::<T>(func, id, inherent)
.next()
.cloned()
.unwrap_or_else(default)
}
/// Cast the first value for the given property in the chain,
/// returning a borrowed value.
pub fn get_ref<T: 'static>(
self,
func: Element,
id: u8,
inherent: Option<&'a T>,
default: impl Fn() -> &'a T,
) -> &'a T {
self.properties::<T>(func, id, inherent)
.next()
.unwrap_or_else(default)
}
/// Cast the first value for the given property in the chain, taking
/// `Fold` implementations into account.
pub fn get_folded<T: Fold + Clone + 'static>(
self,
func: Element,
id: u8,
inherent: Option<&T>,
default: impl Fn() -> T,
) -> T {
fn next<T: Fold>(
mut values: impl Iterator<Item = T>,
default: &impl Fn() -> T,
) -> T {
values
.next()
.map(|value| value.fold(next(values, default)))
.unwrap_or_else(default)
}
next(self.properties::<T>(func, id, inherent).cloned(), &default)
}
/// Iterate over all values for the given property in the chain.
fn properties<T: 'static>(
self,
func: Element,
id: u8,
inherent: Option<&'a T>,
) -> impl Iterator<Item = &'a T> {
inherent.into_iter().chain(
self.entries()
.filter_map(|style| style.property())
.filter(move |property| property.is(func, id))
.map(|property| &property.value)
.map(move |value| {
value.downcast().unwrap_or_else(|| {
panic!(
"attempted to read a value of a different type than was written {}.{}: {:?}",
func.name(),
func.field_name(id).unwrap(),
value
)
})
}),
)
}
/// Iterate over the entries of the chain.
pub fn entries(self) -> Entries<'a> {
Entries { inner: [].as_slice().iter(), links: self.links() }
@ -804,6 +842,9 @@ impl<T: Resolve> Resolve for Option<T> {
/// #set rect(stroke: 4pt)
/// #rect()
/// ```
///
/// Note: Folding must be associative, i.e. any implementation must satisfy
/// `fold(fold(a, b), c) == fold(a, fold(b, c))`.
pub trait Fold {
/// Fold this inner value with an outer folded value.
fn fold(self, outer: Self) -> Self;
@ -847,6 +888,9 @@ impl<T> Fold for OneOrMultiple<T> {
}
}
/// A [folding](Fold) function.
pub type FoldFn<T> = fn(T, T) -> T;
/// A variant of fold for foldable optional (`Option<T>`) values where an inner
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
/// implementation, with which folding with an inner `None` always returns
@ -884,3 +928,13 @@ impl Fold for Depth {
Self(outer.0 + self.0)
}
}
#[cold]
fn block_wrong_type(func: Element, id: u8, value: &Block) -> ! {
panic!(
"attempted to read a value of a different type than was written {}.{}: {:?}",
func.name(),
func.field_name(id).unwrap(),
value
)
}

View File

@ -1,14 +1,14 @@
use std::cmp::Reverse;
use std::collections::{BTreeSet, HashMap};
use std::fmt::{self, Debug, Display, Formatter, Write};
use std::sync::Arc;
use codex::ModifierSet;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128;
use crate::diag::{bail, SourceResult, StrResult};
use crate::diag::{bail, DeprecationSink, SourceResult, StrResult};
use crate::foundations::{
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
PlainText, Repr as _,
@ -54,18 +54,22 @@ enum Repr {
/// A native symbol that has no named variant.
Single(char),
/// A native symbol with multiple named variants.
Complex(&'static [(&'static str, char)]),
Complex(&'static [Variant<&'static str>]),
/// A symbol with multiple named variants, where some modifiers may have
/// been applied. Also used for symbols defined at runtime by the user with
/// no modifier applied.
Modified(Arc<(List, EcoString)>),
Modified(Arc<(List, ModifierSet<EcoString>)>),
}
/// A symbol variant, consisting of a set of modifiers, a character, and an
/// optional deprecation message.
type Variant<S> = (ModifierSet<S>, char, Option<S>);
/// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)]
enum List {
Static(&'static [(&'static str, char)]),
Runtime(Box<[(EcoString, char)]>),
Static(&'static [Variant<&'static str>]),
Runtime(Box<[Variant<EcoString>]>),
}
impl Symbol {
@ -76,24 +80,28 @@ impl Symbol {
/// Create a symbol with a static variant list.
#[track_caller]
pub const fn list(list: &'static [(&'static str, char)]) -> Self {
pub const fn list(list: &'static [Variant<&'static str>]) -> Self {
debug_assert!(!list.is_empty());
Self(Repr::Complex(list))
}
/// Create a symbol with a runtime variant list.
#[track_caller]
pub fn runtime(list: Box<[(EcoString, char)]>) -> Self {
pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self {
debug_assert!(!list.is_empty());
Self(Repr::Modified(Arc::new((List::Runtime(list), EcoString::new()))))
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
}
/// Get the symbol's character.
pub fn get(&self) -> char {
match &self.0 {
Repr::Single(c) => *c,
Repr::Complex(_) => find(self.variants(), "").unwrap(),
Repr::Modified(arc) => find(self.variants(), &arc.1).unwrap(),
Repr::Complex(_) => ModifierSet::<&'static str>::default()
.best_match_in(self.variants().map(|(m, c, _)| (m, c)))
.unwrap(),
Repr::Modified(arc) => {
arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap()
}
}
}
@ -126,18 +134,25 @@ impl Symbol {
}
/// Apply a modifier to the symbol.
pub fn modified(mut self, modifier: &str) -> StrResult<Self> {
pub fn modified(
mut self,
sink: impl DeprecationSink,
modifier: &str,
) -> StrResult<Self> {
if let Repr::Complex(list) = self.0 {
self.0 = Repr::Modified(Arc::new((List::Static(list), EcoString::new())));
self.0 =
Repr::Modified(Arc::new((List::Static(list), ModifierSet::default())));
}
if let Repr::Modified(arc) = &mut self.0 {
let (list, modifiers) = Arc::make_mut(arc);
if !modifiers.is_empty() {
modifiers.push('.');
}
modifiers.push_str(modifier);
if find(list.variants(), modifiers).is_some() {
modifiers.insert_raw(modifier);
if let Some(deprecation) =
modifiers.best_match_in(list.variants().map(|(m, _, d)| (m, d)))
{
if let Some(message) = deprecation {
sink.emit(message)
}
return Ok(self);
}
}
@ -146,7 +161,7 @@ impl Symbol {
}
/// The characters that are covered by this symbol.
pub fn variants(&self) -> impl Iterator<Item = (&str, char)> {
pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> {
match &self.0 {
Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
Repr::Complex(list) => Variants::Static(list.iter()),
@ -156,17 +171,15 @@ impl Symbol {
/// Possible modifiers.
pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ {
let mut set = BTreeSet::new();
let modifiers = match &self.0 {
Repr::Modified(arc) => arc.1.as_str(),
_ => "",
Repr::Modified(arc) => arc.1.as_deref(),
_ => ModifierSet::default(),
};
for modifier in self.variants().flat_map(|(name, _)| name.split('.')) {
if !modifier.is_empty() && !contained(modifiers, modifier) {
set.insert(modifier);
}
}
set.into_iter()
self.variants()
.flat_map(|(m, _, _)| m)
.filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier))
.collect::<BTreeSet<_>>()
.into_iter()
}
}
@ -256,7 +269,10 @@ impl Symbol {
seen.insert(hash, i);
}
let list = variants.into_iter().map(|s| (s.v.0, s.v.1)).collect();
let list = variants
.into_iter()
.map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1, None))
.collect();
Ok(Symbol::runtime(list))
}
}
@ -291,14 +307,23 @@ impl crate::foundations::Repr for Symbol {
match &self.0 {
Repr::Single(c) => eco_format!("symbol(\"{}\")", *c),
Repr::Complex(variants) => {
eco_format!("symbol{}", repr_variants(variants.iter().copied(), ""))
eco_format!(
"symbol{}",
repr_variants(variants.iter().copied(), ModifierSet::default())
)
}
Repr::Modified(arc) => {
let (list, modifiers) = arc.as_ref();
if modifiers.is_empty() {
eco_format!("symbol{}", repr_variants(list.variants(), ""))
eco_format!(
"symbol{}",
repr_variants(list.variants(), ModifierSet::default())
)
} else {
eco_format!("symbol{}", repr_variants(list.variants(), modifiers))
eco_format!(
"symbol{}",
repr_variants(list.variants(), modifiers.as_deref())
)
}
}
}
@ -306,24 +331,24 @@ impl crate::foundations::Repr for Symbol {
}
fn repr_variants<'a>(
variants: impl Iterator<Item = (&'a str, char)>,
applied_modifiers: &str,
variants: impl Iterator<Item = Variant<&'a str>>,
applied_modifiers: ModifierSet<&str>,
) -> String {
crate::foundations::repr::pretty_array_like(
&variants
.filter(|(variant, _)| {
.filter(|(modifiers, _, _)| {
// Only keep variants that can still be accessed, i.e., variants
// that contain all applied modifiers.
parts(applied_modifiers).all(|am| variant.split('.').any(|m| m == am))
applied_modifiers.iter().all(|am| modifiers.contains(am))
})
.map(|(variant, c)| {
let trimmed_variant = variant
.split('.')
.filter(|&m| parts(applied_modifiers).all(|am| m != am));
if trimmed_variant.clone().all(|m| m.is_empty()) {
.map(|(modifiers, c, _)| {
let trimmed_modifiers =
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
if trimmed_modifiers.clone().all(|m| m.is_empty()) {
eco_format!("\"{c}\"")
} else {
let trimmed_modifiers = trimmed_variant.collect::<Vec<_>>().join(".");
let trimmed_modifiers =
trimmed_modifiers.collect::<Vec<_>>().join(".");
eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c)
}
})
@ -369,65 +394,22 @@ cast! {
/// Iterator over variants.
enum Variants<'a> {
Single(std::option::IntoIter<char>),
Static(std::slice::Iter<'static, (&'static str, char)>),
Runtime(std::slice::Iter<'a, (EcoString, char)>),
Static(std::slice::Iter<'static, Variant<&'static str>>),
Runtime(std::slice::Iter<'a, Variant<EcoString>>),
}
impl<'a> Iterator for Variants<'a> {
type Item = (&'a str, char);
type Item = Variant<&'a str>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Single(iter) => Some(("", iter.next()?)),
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)),
Self::Static(list) => list.next().copied(),
Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)),
}
}
}
/// Find the best symbol from the list.
fn find<'a>(
variants: impl Iterator<Item = (&'a str, char)>,
modifiers: &str,
) -> Option<char> {
let mut best = None;
let mut best_score = None;
// Find the best table entry with this name.
'outer: for candidate in variants {
for modifier in parts(modifiers) {
if !contained(candidate.0, modifier) {
continue 'outer;
Self::Runtime(list) => {
list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref()))
}
}
let mut matching = 0;
let mut total = 0;
for modifier in parts(candidate.0) {
if contained(modifiers, modifier) {
matching += 1;
}
total += 1;
}
let score = (matching, Reverse(total));
if best_score.is_none_or(|b| score > b) {
best = Some(candidate.1);
best_score = Some(score);
}
}
best
}
/// Split a modifier list into its parts.
fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
modifiers.split('.').filter(|s| !s.is_empty())
}
/// Whether the modifier string contains the modifier `m`.
fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m)
}
/// A single character.

View File

@ -73,5 +73,5 @@ pub struct TargetElem {
/// ```
#[func(contextual)]
pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?))
Ok(context.styles()?.get(TargetElem::target))
}

View File

@ -157,7 +157,9 @@ impl Value {
/// Try to access a field on the value.
pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> {
match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
Self::Symbol(symbol) => {
symbol.clone().modified(sink, field).map(Self::Symbol)
}
Self::Version(version) => version.component(field).map(Self::Int),
Self::Dict(dict) => dict.get(field).cloned(),
Self::Content(content) => content.field_by_name(field),

View File

@ -7,7 +7,7 @@ use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{cast, Dict, Repr, Str};
use crate::introspection::{Introspector, Tag};
use crate::layout::Frame;
use crate::layout::{Abs, Frame};
use crate::model::DocumentInfo;
/// An HTML document.
@ -30,8 +30,8 @@ pub enum HtmlNode {
Text(EcoString, Span),
/// Another element.
Element(HtmlElement),
/// A frame that will be displayed as an embedded SVG.
Frame(Frame),
/// Layouted content that will be embedded into HTML as an SVG.
Frame(HtmlFrame),
}
impl HtmlNode {
@ -188,7 +188,7 @@ cast! {
.collect::<HintedStrResult<_>>()?),
}
/// An attribute of an HTML.
/// An attribute of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr);
@ -263,6 +263,17 @@ cast! {
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,
}
/// Defines syntactical properties of HTML tags, attributes, and text.
pub mod charsets {
/// Check whether a character is in a tag name.
@ -347,135 +358,124 @@ pub mod charsets {
}
/// Predefined constants for HTML tags.
#[allow(non_upper_case_globals)]
pub mod tag {
use super::HtmlTag;
macro_rules! tags {
($($tag:ident)*) => {
$(#[allow(non_upper_case_globals)]
pub const $tag: HtmlTag = HtmlTag::constant(
stringify!($tag)
);)*
}
}
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");
tags! {
a
abbr
address
area
article
aside
audio
b
base
bdi
bdo
blockquote
body
br
button
canvas
caption
cite
code
col
colgroup
data
datalist
dd
del
details
dfn
dialog
div
dl
dt
em
embed
fieldset
figcaption
figure
footer
form
h1
h2
h3
h4
h5
h6
head
header
hgroup
hr
html
i
iframe
img
input
ins
kbd
label
legend
li
link
main
map
mark
menu
meta
meter
nav
noscript
object
ol
optgroup
option
output
p
param
picture
pre
progress
q
rp
rt
ruby
s
samp
script
search
section
select
slot
small
source
span
strong
style
sub
summary
sup
table
tbody
td
template
textarea
tfoot
th
thead
time
title
tr
track
u
ul
var
video
wbr
}
/// Whether this is a void tag whose associated element may not have a
/// Whether this is a void tag whose associated element may not have
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
@ -490,7 +490,6 @@ pub mod tag {
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
@ -629,36 +628,196 @@ pub mod tag {
}
}
/// Predefined constants for HTML attributes.
///
/// Note: These are very incomplete.
#[allow(non_upper_case_globals)]
#[rustfmt::skip]
pub mod attr {
use super::HtmlAttr;
macro_rules! attrs {
($($attr:ident)*) => {
$(#[allow(non_upper_case_globals)]
pub const $attr: HtmlAttr = HtmlAttr::constant(
stringify!($attr)
);)*
}
}
attrs! {
charset
cite
colspan
content
href
name
reversed
role
rowspan
start
style
value
}
use crate::html::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

@ -1,6 +1,7 @@
//! HTML output.
mod dom;
mod typed;
pub use self::dom::*;
@ -14,6 +15,7 @@ pub fn module() -> Module {
html.start_category(crate::Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
self::typed::define(&mut html);
Module::new("html", html)
}
@ -45,21 +47,22 @@ pub struct HtmlElem {
pub tag: HtmlTag,
/// The element's HTML attributes.
#[borrowed]
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl HtmlElem {
/// Add an attribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.get_or_insert_with(Default::default).push(attr, value);
self.attrs
.as_option_mut()
.get_or_insert_with(Default::default)
.push(attr, value);
self
}
}

View File

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

@ -338,7 +338,7 @@ impl Counter {
/// The selector relevant for this counter's updates.
fn selector(&self) -> Selector {
let mut selector = select_where!(CounterUpdateElem, Key => self.0.clone());
let mut selector = select_where!(CounterUpdateElem, key => self.0.clone());
if let CounterKey::Selector(key) = &self.0 {
selector = Selector::Or(eco_vec![selector, key.clone()]);
@ -367,16 +367,16 @@ impl Counter {
.or_else(|| {
let styles = styles?;
match self.0 {
CounterKey::Page => PageElem::numbering_in(styles).clone(),
CounterKey::Page => styles.get_cloned(PageElem::numbering),
CounterKey::Selector(Selector::Elem(func, _)) => {
if func == HeadingElem::elem() {
HeadingElem::numbering_in(styles).clone()
} else if func == FigureElem::elem() {
FigureElem::numbering_in(styles).clone()
} else if func == EquationElem::elem() {
EquationElem::numbering_in(styles).clone()
} else if func == FootnoteElem::elem() {
Some(FootnoteElem::numbering_in(styles).clone())
if func == HeadingElem::ELEM {
styles.get_cloned(HeadingElem::numbering)
} else if func == FigureElem::ELEM {
styles.get_cloned(FigureElem::numbering)
} else if func == EquationElem::ELEM {
styles.get_cloned(EquationElem::numbering)
} else if func == FootnoteElem::ELEM {
Some(styles.get_cloned(FootnoteElem::numbering))
} else {
None
}
@ -398,7 +398,7 @@ impl Counter {
/// Selects all state updates.
pub fn select_any() -> Selector {
CounterUpdateElem::elem().select()
CounterUpdateElem::ELEM.select()
}
}
@ -565,14 +565,14 @@ pub enum CounterKey {
cast! {
CounterKey,
self => match self {
Self::Page => PageElem::elem().into_value(),
Self::Page => PageElem::ELEM.into_value(),
Self::Selector(v) => v.into_value(),
Self::Str(v) => v.into_value(),
},
v: Str => Self::Str(v),
v: Label => Self::Selector(Selector::Label(v)),
v: Element => {
if v == PageElem::elem() {
if v == PageElem::ELEM {
Self::Page
} else {
Self::Selector(LocatableSelector::from_value(v.into_value())?.0)

View File

@ -446,7 +446,7 @@ impl IntrospectorBuilder {
HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame(
sink,
frame,
&frame.inner,
NonZeroUsize::ONE,
Transform::identity(),
),

View File

@ -259,12 +259,12 @@ impl State {
/// The selector for this state's updates.
fn selector(&self) -> Selector {
select_where!(StateUpdateElem, Key => self.key.clone())
select_where!(StateUpdateElem, key => self.key.clone())
}
/// Selects all state updates.
pub fn select_any() -> Selector {
StateUpdateElem::elem().select()
StateUpdateElem::ELEM.select()
}
}

View File

@ -100,11 +100,11 @@ pub struct AlignElem {
impl Show for Packed<AlignElem> {
#[typst_macros::time(name = "align", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().aligned(self.alignment(styles)))
Ok(self.body.clone().aligned(self.alignment.get(styles)))
}
}
/// Where to [align] something along an axis.
/// Where to align something along an axis.
///
/// Possible values are:
/// - `start`: Aligns at the [start]($direction.start) of the [text
@ -277,7 +277,7 @@ impl Resolve for Alignment {
type Output = Axes<FixedAlignment>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}
@ -378,7 +378,7 @@ impl Resolve for HAlignment {
type Output = FixedAlignment;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}
@ -414,7 +414,7 @@ impl Resolve for OuterHAlignment {
type Output = FixedAlignment;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}
@ -636,7 +636,7 @@ where
type Output = Axes<FixedAlignment>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}

View File

@ -4,9 +4,12 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not};
use typst_utils::Get;
use crate::diag::bail;
use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain};
use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size};
use crate::diag::{bail, HintedStrResult};
use crate::foundations::{
array, cast, Array, CastInfo, FromValue, IntoValue, Reflect, Resolve, Smart,
StyleChain, Value,
};
use crate::layout::{Abs, Dir, Rel, Size};
/// A container with a horizontal and vertical component.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
@ -275,40 +278,39 @@ impl BitAndAssign for Axes<bool> {
}
}
cast! {
Axes<Rel<Length>>,
self => array![self.x, self.y].into_value(),
array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => bail!("point array must contain exactly two entries"),
}
},
impl<T: Reflect> Reflect for Axes<T> {
fn input() -> CastInfo {
Array::input()
}
fn output() -> CastInfo {
Array::output()
}
fn castable(value: &Value) -> bool {
Array::castable(value)
}
}
cast! {
Axes<Ratio>,
self => array![self.x, self.y].into_value(),
array: Array => {
impl<T: FromValue> FromValue for Axes<T> {
fn from_value(value: Value) -> HintedStrResult<Self> {
let array = value.cast::<Array>()?;
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => bail!("ratio array must contain exactly two entries"),
(Some(a), Some(b), None) => Ok(Axes::new(a.cast()?, b.cast()?)),
_ => bail!(
"array must contain exactly two items";
hint: "the first item determines the value for the X axis \
and the second item the value for the Y axis"
),
}
},
}
}
cast! {
Axes<Length>,
self => array![self.x, self.y].into_value(),
array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => bail!("length array must contain exactly two entries"),
}
},
impl<T: IntoValue> IntoValue for Axes<T> {
fn into_value(self) -> Value {
array![self.x.into_value(), self.y.into_value()].into_value()
}
}
impl<T: Resolve> Resolve for Axes<T> {

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