mirror of
https://github.com/typst/typst
synced 2025-05-15 01:25:28 +08:00
Compare commits
100 Commits
v0.13.0-rc
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
d204a28818 | ||
|
22a117a091 | ||
|
26c19a49c8 | ||
|
54c5113a83 | ||
|
9b09146a6b | ||
|
b322da930f | ||
|
14241ec1aa | ||
|
3e6691a93b | ||
|
7e072e2493 | ||
|
c21c1c391b | ||
|
94a497a01f | ||
|
9829bd8326 | ||
|
43c3d5d3af | ||
|
14a0565d95 | ||
|
bd2e76e11d | ||
|
14928ef962 | ||
|
d55abf0842 | ||
|
ea336a6ac7 | ||
|
387a8b4895 | ||
|
bf8751c063 | ||
|
ed2106e28d | ||
|
417f5846b6 | ||
|
12699eb7f4 | ||
|
96dd67e011 | ||
|
012e14d40c | ||
|
4f0fbfb7e0 | ||
|
a64af130dc | ||
|
1082181a6f | ||
|
e60d3021a7 | ||
|
326bec1f0d | ||
|
758ee78ef5 | ||
|
efdb75558f | ||
|
20ee446eba | ||
|
b7a4382a73 | ||
|
838a46dbb7 | ||
|
1f1c133878 | ||
|
1e591ac8dc | ||
|
38213ed534 | ||
|
636eea18bc | ||
|
91956d1f03 | ||
|
1b2714e1a7 | ||
|
95a7e28e25 | ||
|
37bb632d2e | ||
|
24b2f98bf9 | ||
|
0214320087 | ||
|
96f6957371 | ||
|
3650859ae8 | ||
|
bd531e08dc | ||
|
e66e190a21 | ||
|
db9a83d9fc | ||
|
8d3488a07d | ||
|
476c2df312 | ||
|
e0b2c32a8e | ||
|
99b7d2898e | ||
|
e1a9166e1d | ||
|
6271cdceae | ||
|
63fda9935f | ||
|
8820a00beb | ||
|
9a6ffbc7db | ||
|
bf0d45e2c0 | ||
|
d4def09962 | ||
|
66679920b2 | ||
|
cfb3b1a270 | ||
|
52f1f53973 | ||
|
d6b0d68ffa | ||
|
8f039dd614 | ||
|
2eef9e84e1 | ||
|
d11ad80dee | ||
|
bad343748b | ||
|
f31c971624 | ||
|
acd3a5b7a5 | ||
|
225e845021 | ||
|
36d83c8c09 | ||
|
3744c99b07 | ||
|
81efc82d3c | ||
|
69c3f95705 | ||
|
ebe2543264 | ||
|
56f4fa2b4d | ||
|
55bc5f4c94 | ||
|
240f238eee | ||
|
d199546f9f | ||
|
a543ee9445 | ||
|
3de3813ca0 | ||
|
74e4f78687 | ||
|
25c86accbb | ||
|
5fc679f3e7 | ||
|
19a12f379f | ||
|
02cd43e27f | ||
|
83ad407d3c | ||
|
a0cd89b478 | ||
|
81021fa1a2 | ||
|
89e71acecd | ||
|
ee47cb8469 | ||
|
25e27169e1 | ||
|
3fba256405 | ||
|
e4f8e57c53 | ||
|
a1c73b41b8 | ||
|
d61f57365b | ||
|
ca702c7f82 | ||
|
d897ab5e7d |
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@ -5,6 +5,7 @@ env:
|
|||||||
RUSTFLAGS: "-Dwarnings"
|
RUSTFLAGS: "-Dwarnings"
|
||||||
RUSTDOCFLAGS: "-Dwarnings"
|
RUSTDOCFLAGS: "-Dwarnings"
|
||||||
TYPST_TESTS_EXTENDED: true
|
TYPST_TESTS_EXTENDED: true
|
||||||
|
PKG_CONFIG_i686-unknown-linux-gnu: /usr/bin/i686-linux-gnu-pkgconf
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# This allows us to have one branch protection rule for the full test matrix.
|
# This allows us to have one branch protection rule for the full test matrix.
|
||||||
@ -27,30 +28,43 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
|
bits: [64]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
bits: 32
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83.0
|
- if: startsWith(matrix.os, 'ubuntu-') && matrix.bits == 32
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo test --workspace --no-run
|
with:
|
||||||
- run: cargo test --workspace --no-fail-fast
|
key: ${{ matrix.bits }}
|
||||||
|
- run: cargo test --workspace --no-run ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }}
|
||||||
|
- run: cargo test --workspace --no-fail-fast ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }}
|
||||||
- name: Upload rendered test output
|
- name: Upload rendered test output
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: tests-rendered-${{ matrix.os }}
|
name: tests-rendered-${{ matrix.os }}-${{ matrix.bits }}
|
||||||
path: tests/store/render/**
|
path: tests/store/render/**
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
- name: Update test artifacts
|
- name: Update test artifacts
|
||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
cargo test --workspace --test tests -- --update
|
cargo test --workspace --test tests ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} -- --update
|
||||||
echo 'updated_artifacts=1' >> "$GITHUB_ENV"
|
echo 'updated_artifacts=1' >> "$GITHUB_ENV"
|
||||||
- name: Upload updated reference output (for use if the test changes are desired)
|
- name: Upload updated reference output (for use if the test changes are desired)
|
||||||
if: failure() && env.updated_artifacts
|
if: failure() && env.updated_artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: tests-updated-${{ matrix.os }}
|
name: tests-updated-${{ matrix.os }}-${{ matrix.bits }}
|
||||||
path: tests/ref/**
|
path: tests/ref/**
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
@ -59,7 +73,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83.0
|
- uses: dtolnay/rust-toolchain@1.85.0
|
||||||
with:
|
with:
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
@ -73,7 +87,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.80.0
|
- uses: dtolnay/rust-toolchain@1.83.0
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo check --workspace
|
- run: cargo check --workspace
|
||||||
|
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@1.83.0
|
- uses: dtolnay/rust-toolchain@1.85.0
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
|
322
Cargo.lock
generated
322
Cargo.lock
generated
@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
@ -217,6 +217,20 @@ name = "bytemuck"
|
|||||||
version = "1.21.0"
|
version = "1.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck_derive"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@ -735,11 +749,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.35"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
|
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
"libz-rs-sys",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -749,6 +764,15 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float-cmp"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -761,6 +785,15 @@ version = "0.1.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "font-types"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fontconfig-parser"
|
name = "fontconfig-parser"
|
||||||
version = "0.5.7"
|
version = "0.5.7"
|
||||||
@ -772,9 +805,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fontdb"
|
name = "fontdb"
|
||||||
version = "0.21.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37be9fc20d966be438cd57a45767f73349477fb0f85ce86e000557f787298afb"
|
checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fontconfig-parser",
|
"fontconfig-parser",
|
||||||
"log",
|
"log",
|
||||||
@ -829,6 +862,15 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fxhash"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getopts"
|
name = "getopts"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@ -871,6 +913,12 @@ dependencies = [
|
|||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glidesort"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@ -966,7 +1014,7 @@ checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"serde",
|
"serde",
|
||||||
"yoke",
|
"yoke 0.7.5",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
@ -1064,7 +1112,7 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"tinystr",
|
"tinystr",
|
||||||
"writeable",
|
"writeable",
|
||||||
"yoke",
|
"yoke 0.7.5",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
@ -1175,9 +1223,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image-webp"
|
name = "image-webp"
|
||||||
version = "0.1.3"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904"
|
checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
"quick-error",
|
"quick-error",
|
||||||
@ -1211,6 +1259,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "infer"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -1310,6 +1364,50 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "krilla"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bumpalo",
|
||||||
|
"comemo",
|
||||||
|
"flate2",
|
||||||
|
"float-cmp 0.10.0",
|
||||||
|
"fxhash",
|
||||||
|
"gif",
|
||||||
|
"image-webp",
|
||||||
|
"imagesize",
|
||||||
|
"once_cell",
|
||||||
|
"pdf-writer",
|
||||||
|
"png",
|
||||||
|
"rayon",
|
||||||
|
"rustybuzz",
|
||||||
|
"siphasher",
|
||||||
|
"skrifa",
|
||||||
|
"subsetter",
|
||||||
|
"tiny-skia-path",
|
||||||
|
"xmp-writer",
|
||||||
|
"yoke 0.8.0",
|
||||||
|
"zune-jpeg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "krilla-svg"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
"fontdb",
|
||||||
|
"krilla",
|
||||||
|
"png",
|
||||||
|
"resvg",
|
||||||
|
"tiny-skia",
|
||||||
|
"usvg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kurbo"
|
name = "kurbo"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -1371,6 +1469,15 @@ dependencies = [
|
|||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libz-rs-sys"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d"
|
||||||
|
dependencies = [
|
||||||
|
"zlib-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@ -1458,9 +1565,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.3"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
@ -1601,9 +1708,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.70"
|
version = "0.10.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
|
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@ -1642,9 +1749,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.105"
|
version = "0.9.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
|
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1738,9 +1845,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdf-writer"
|
name = "pdf-writer"
|
||||||
version = "0.12.1"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5"
|
checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
@ -1804,9 +1911,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pixglyph"
|
name = "pixglyph"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d15afa937836bf3d876f5a04ce28810c06045857bf46c3d0d31073b8aada5494"
|
checksum = "3c1106193bc18a4b840eb075ff6664c8a0b0270f0531bb12a7e9c803e53b55c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
]
|
]
|
||||||
@ -1997,6 +2104,16 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "read-fonts"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"font-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@ -2048,9 +2165,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "resvg"
|
name = "resvg"
|
||||||
version = "0.43.0"
|
version = "0.45.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7314563c59c7ce31c18e23ad3dd092c37b928a0fa4e1c0a1a6504351ab411d1"
|
checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gif",
|
"gif",
|
||||||
"image-webp",
|
"image-webp",
|
||||||
@ -2121,9 +2238,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustybuzz"
|
name = "rustybuzz"
|
||||||
version = "0.18.0"
|
version = "0.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181"
|
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
@ -2315,6 +2432,16 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "skrifa"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"read-fonts",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slotmap"
|
name = "slotmap"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -2361,7 +2488,7 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"float-cmp",
|
"float-cmp 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2404,28 +2531,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subsetter"
|
name = "subsetter"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648"
|
checksum = "35539e8de3dcce8dd0c01f3575f85db1e5ac1aea1b996d2d09d89f148bc91497"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "svg2pdf"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5014c9dadcf318fb7ef8c16438e95abcc9de1ae24d60d5bccc64c55100c50364"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fontdb",
|
"fxhash",
|
||||||
"image",
|
|
||||||
"log",
|
|
||||||
"miniz_oxide",
|
|
||||||
"once_cell",
|
|
||||||
"pdf-writer",
|
|
||||||
"resvg",
|
|
||||||
"siphasher",
|
|
||||||
"subsetter",
|
|
||||||
"tiny-skia",
|
|
||||||
"ttf-parser",
|
|
||||||
"usvg",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2709,9 +2819,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.24.1"
|
version = "0.25.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a"
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core_maths",
|
"core_maths",
|
||||||
]
|
]
|
||||||
@ -2735,7 +2845,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst"
|
name = "typst"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
@ -2752,12 +2862,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-assets"
|
name = "typst-assets"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577"
|
source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-cli"
|
name = "typst-cli"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@ -2802,12 +2912,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-dev-assets"
|
name = "typst-dev-assets"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c"
|
source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-docs"
|
name = "typst-docs"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ecow",
|
"ecow",
|
||||||
@ -2830,7 +2940,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-eval"
|
name = "typst-eval"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
@ -2848,7 +2958,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-fuzz"
|
name = "typst-fuzz"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
@ -2860,7 +2970,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-html"
|
name = "typst-html"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
@ -2874,7 +2984,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-ide"
|
name = "typst-ide"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
@ -2891,16 +3001,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-kit"
|
name = "typst-kit"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"ecow",
|
"ecow",
|
||||||
"env_proxy",
|
"env_proxy",
|
||||||
|
"fastrand",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tar",
|
"tar",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
@ -2912,7 +3025,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-layout"
|
name = "typst-layout"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"az",
|
"az",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
@ -2942,7 +3055,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-library"
|
name = "typst-library"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"az",
|
"az",
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
@ -2955,6 +3068,7 @@ dependencies = [
|
|||||||
"ecow",
|
"ecow",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
|
"glidesort",
|
||||||
"hayagriva",
|
"hayagriva",
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
@ -2964,6 +3078,7 @@ dependencies = [
|
|||||||
"kamadak-exif",
|
"kamadak-exif",
|
||||||
"kurbo",
|
"kurbo",
|
||||||
"lipsum",
|
"lipsum",
|
||||||
|
"memchr",
|
||||||
"palette",
|
"palette",
|
||||||
"phf",
|
"phf",
|
||||||
"png",
|
"png",
|
||||||
@ -2992,6 +3107,7 @@ dependencies = [
|
|||||||
"typst-timing",
|
"typst-timing",
|
||||||
"typst-utils",
|
"typst-utils",
|
||||||
"unicode-math-class",
|
"unicode-math-class",
|
||||||
|
"unicode-normalization",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unscanny",
|
"unscanny",
|
||||||
"usvg",
|
"usvg",
|
||||||
@ -3001,7 +3117,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-macros"
|
name = "typst-macros"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3011,33 +3127,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-pdf"
|
name = "typst-pdf"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
|
||||||
"base64",
|
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"image",
|
"image",
|
||||||
"indexmap 2.7.1",
|
"infer",
|
||||||
"miniz_oxide",
|
"krilla",
|
||||||
"pdf-writer",
|
"krilla-svg",
|
||||||
"serde",
|
"serde",
|
||||||
"subsetter",
|
|
||||||
"svg2pdf",
|
|
||||||
"ttf-parser",
|
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-syntax",
|
"typst-syntax",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
"typst-utils",
|
"typst-utils",
|
||||||
"xmp-writer",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-realize"
|
name = "typst-realize"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
@ -3053,7 +3163,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-render"
|
name = "typst-render"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"comemo",
|
"comemo",
|
||||||
@ -3069,7 +3179,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-svg"
|
name = "typst-svg"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"comemo",
|
"comemo",
|
||||||
@ -3087,7 +3197,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-syntax"
|
name = "typst-syntax"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ecow",
|
"ecow",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3103,7 +3213,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-tests"
|
name = "typst-tests"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"comemo",
|
"comemo",
|
||||||
@ -3128,7 +3238,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-timing"
|
name = "typst-timing"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3138,7 +3248,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-utils"
|
name = "typst-utils"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
@ -3181,15 +3291,15 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi-mirroring"
|
name = "unicode-bidi-mirroring"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
|
checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ccc"
|
name = "unicode-ccc"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
|
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
@ -3284,9 +3394,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.43.0"
|
version = "0.45.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6803057b5cbb426e9fb8ce2216f3a9b4ca1dd2c705ba3cbebc13006e437735fd"
|
checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"data-url",
|
"data-url",
|
||||||
@ -3656,9 +3766,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xmp-writer"
|
name = "xmp-writer"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487"
|
checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xz2"
|
name = "xz2"
|
||||||
@ -3696,7 +3806,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive 0.7.5",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"stable_deref_trait",
|
||||||
|
"yoke-derive 0.8.0",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3712,6 +3834,18 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke-derive"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.35"
|
version = "0.7.35"
|
||||||
@ -3773,7 +3907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
|
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"yoke",
|
"yoke 0.7.5",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec-derive",
|
"zerovec-derive",
|
||||||
]
|
]
|
||||||
@ -3791,21 +3925,25 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "2.2.2"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45"
|
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arbitrary",
|
"arbitrary",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"displaydoc",
|
|
||||||
"flate2",
|
"flate2",
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
"memchr",
|
"memchr",
|
||||||
"thiserror 2.0.11",
|
|
||||||
"zopfli",
|
"zopfli",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
68
Cargo.toml
68
Cargo.toml
@ -4,8 +4,8 @@ default-members = ["crates/typst-cli"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
rust-version = "1.80" # also change in ci.yml
|
rust-version = "1.83" # also change in ci.yml
|
||||||
authors = ["The Typst Project Developers"]
|
authors = ["The Typst Project Developers"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://typst.app"
|
homepage = "https://typst.app"
|
||||||
@ -16,24 +16,24 @@ keywords = ["typst"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
typst = { path = "crates/typst", version = "0.12.0" }
|
typst = { path = "crates/typst", version = "0.13.1" }
|
||||||
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
|
typst-cli = { path = "crates/typst-cli", version = "0.13.1" }
|
||||||
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
|
typst-eval = { path = "crates/typst-eval", version = "0.13.1" }
|
||||||
typst-html = { path = "crates/typst-html", version = "0.12.0" }
|
typst-html = { path = "crates/typst-html", version = "0.13.1" }
|
||||||
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
|
typst-ide = { path = "crates/typst-ide", version = "0.13.1" }
|
||||||
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
|
typst-kit = { path = "crates/typst-kit", version = "0.13.1" }
|
||||||
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
|
typst-layout = { path = "crates/typst-layout", version = "0.13.1" }
|
||||||
typst-library = { path = "crates/typst-library", version = "0.12.0" }
|
typst-library = { path = "crates/typst-library", version = "0.13.1" }
|
||||||
typst-macros = { path = "crates/typst-macros", version = "0.12.0" }
|
typst-macros = { path = "crates/typst-macros", version = "0.13.1" }
|
||||||
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" }
|
typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" }
|
||||||
typst-realize = { path = "crates/typst-realize", version = "0.12.0" }
|
typst-realize = { path = "crates/typst-realize", version = "0.13.1" }
|
||||||
typst-render = { path = "crates/typst-render", version = "0.12.0" }
|
typst-render = { path = "crates/typst-render", version = "0.13.1" }
|
||||||
typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
|
typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
|
||||||
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
|
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
|
||||||
typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
|
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
|
||||||
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
|
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
|
||||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
|
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" }
|
||||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" }
|
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" }
|
||||||
arrayvec = "0.7.4"
|
arrayvec = "0.7.4"
|
||||||
az = "1.2"
|
az = "1.2"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
@ -55,9 +55,11 @@ ctrlc = "3.4.1"
|
|||||||
dirs = "6"
|
dirs = "6"
|
||||||
ecow = { version = "0.2", features = ["serde"] }
|
ecow = { version = "0.2", features = ["serde"] }
|
||||||
env_proxy = "0.4"
|
env_proxy = "0.4"
|
||||||
|
fastrand = "2.3"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
fontdb = { version = "0.21", default-features = false }
|
fontdb = { version = "0.23", default-features = false }
|
||||||
fs_extra = "1.3"
|
fs_extra = "1.3"
|
||||||
|
glidesort = "0.1.2"
|
||||||
hayagriva = "0.8.1"
|
hayagriva = "0.8.1"
|
||||||
heck = "0.5"
|
heck = "0.5"
|
||||||
hypher = "0.1.4"
|
hypher = "0.1.4"
|
||||||
@ -69,23 +71,25 @@ icu_segmenter = { version = "1.4", features = ["serde"] }
|
|||||||
if_chain = "1"
|
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"] }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
|
infer = { version = "0.19.0", default-features = false }
|
||||||
kamadak-exif = "0.6"
|
kamadak-exif = "0.6"
|
||||||
|
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
|
||||||
|
krilla-svg = "0.1.0"
|
||||||
kurbo = "0.11"
|
kurbo = "0.11"
|
||||||
libfuzzer-sys = "0.4"
|
libfuzzer-sys = "0.4"
|
||||||
lipsum = "0.9"
|
lipsum = "0.9"
|
||||||
miniz_oxide = "0.8"
|
memchr = "2"
|
||||||
native-tls = "0.2"
|
native-tls = "0.2"
|
||||||
notify = "8"
|
notify = "8"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
open = "5.0.1"
|
open = "5.0.1"
|
||||||
openssl = "0.10"
|
openssl = "0.10.72"
|
||||||
oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }
|
oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }
|
||||||
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
pdf-writer = "0.12.1"
|
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.11", features = ["macros"] }
|
||||||
pixglyph = "0.5.1"
|
pixglyph = "0.6"
|
||||||
png = "0.17"
|
png = "0.17"
|
||||||
portable-atomic = "1.6"
|
portable-atomic = "1.6"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
@ -95,10 +99,10 @@ quote = "1"
|
|||||||
rayon = "1.7.0"
|
rayon = "1.7.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
regex-syntax = "0.8"
|
regex-syntax = "0.8"
|
||||||
resvg = { version = "0.43", default-features = false, features = ["raster-images"] }
|
resvg = { version = "0.45", default-features = false, features = ["raster-images"] }
|
||||||
roxmltree = "0.20"
|
roxmltree = "0.20"
|
||||||
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
|
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
|
||||||
rustybuzz = "0.18"
|
rustybuzz = "0.20"
|
||||||
same-file = "1"
|
same-file = "1"
|
||||||
self-replace = "1.3.7"
|
self-replace = "1.3.7"
|
||||||
semver = "1"
|
semver = "1"
|
||||||
@ -110,8 +114,6 @@ sigpipe = "0.1"
|
|||||||
siphasher = "1"
|
siphasher = "1"
|
||||||
smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] }
|
smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] }
|
||||||
stacker = "0.1.15"
|
stacker = "0.1.15"
|
||||||
subsetter = "0.2"
|
|
||||||
svg2pdf = "0.12"
|
|
||||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||||
syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] }
|
syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] }
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
@ -121,26 +123,26 @@ time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
|
|||||||
tiny_http = "0.12"
|
tiny_http = "0.12"
|
||||||
tiny-skia = "0.11"
|
tiny-skia = "0.11"
|
||||||
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
|
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
|
||||||
ttf-parser = "0.24.1"
|
ttf-parser = "0.25.0"
|
||||||
two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] }
|
two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] }
|
||||||
typed-arena = "2"
|
typed-arena = "2"
|
||||||
unicode-bidi = "0.3.18"
|
unicode-bidi = "0.3.18"
|
||||||
unicode-ident = "1.0"
|
unicode-ident = "1.0"
|
||||||
unicode-math-class = "0.1"
|
unicode-math-class = "0.1"
|
||||||
unicode-script = "0.5"
|
unicode-script = "0.5"
|
||||||
|
unicode-normalization = "0.1.24"
|
||||||
unicode-segmentation = "1"
|
unicode-segmentation = "1"
|
||||||
unscanny = "0.1"
|
unscanny = "0.1"
|
||||||
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
|
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
|
||||||
usvg = { version = "0.43", default-features = false, features = ["text"] }
|
usvg = { version = "0.45", default-features = false, features = ["text"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
wasmi = "0.40.0"
|
wasmi = "0.40.0"
|
||||||
web-sys = "0.3"
|
web-sys = "0.3"
|
||||||
xmlparser = "0.13.5"
|
xmlparser = "0.13.5"
|
||||||
xmlwriter = "0.1.0"
|
xmlwriter = "0.1.0"
|
||||||
xmp-writer = "0.3.1"
|
|
||||||
xz2 = { version = "0.1", features = ["static"] }
|
xz2 = { version = "0.1", features = ["static"] }
|
||||||
yaml-front-matter = "0.1"
|
yaml-front-matter = "0.1"
|
||||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
zip = { version = "2.5", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
29
README.md
29
README.md
@ -113,7 +113,9 @@ Typst's CLI is available from different sources:
|
|||||||
|
|
||||||
- You can install Typst through different package managers. Note that the
|
- You can install Typst through different package managers. Note that the
|
||||||
versions in the package managers might lag behind the latest release.
|
versions in the package managers might lag behind the latest release.
|
||||||
- Linux: View [Typst on Repology][repology]
|
- Linux:
|
||||||
|
- View [Typst on Repology][repology]
|
||||||
|
- View [Typst's Snap][snap]
|
||||||
- macOS: `brew install typst`
|
- macOS: `brew install typst`
|
||||||
- Windows: `winget install --id Typst.Typst`
|
- Windows: `winget install --id Typst.Typst`
|
||||||
|
|
||||||
@ -175,22 +177,22 @@ If you prefer an integrated IDE-like experience with autocompletion and instant
|
|||||||
preview, you can also check out [Typst's free web app][app].
|
preview, you can also check out [Typst's free web app][app].
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
The main place where the community gathers is our [Discord server][discord].
|
The main places where the community gathers are our [Forum][forum] and our
|
||||||
Feel free to join there to ask questions, help out others, share cool things
|
[Discord server][discord]. The Forum is a great place to ask questions, help
|
||||||
you created with Typst, or just to chat.
|
others, and share cool things you created with Typst. The Discord server is more
|
||||||
|
suitable for quicker questions, discussions about contributing, or just to chat.
|
||||||
|
We'd be happy to see you there!
|
||||||
|
|
||||||
Aside from that there are a few places where you can find things built by
|
[Typst Universe][universe] is where the community shares templates and packages.
|
||||||
the community:
|
If you want to share your own creations, you can submit them to our
|
||||||
|
[package repository][packages].
|
||||||
- The official [package list](https://typst.app/docs/packages)
|
|
||||||
- The [Awesome Typst](https://github.com/qjcg/awesome-typst) repository
|
|
||||||
|
|
||||||
If you had a bad experience in our community, please [reach out to us][contact].
|
If you had a bad experience in our community, please [reach out to us][contact].
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
We would love to see contributions from the community. If you experience bugs,
|
We love to see contributions from the community. If you experience bugs, feel
|
||||||
feel free to open an issue. If you would like to implement a new feature or bug
|
free to open an issue. If you would like to implement a new feature or bug fix,
|
||||||
fix, please follow the steps outlined in the [contribution guide][contributing].
|
please follow the steps outlined in the [contribution guide][contributing].
|
||||||
|
|
||||||
To build Typst yourself, first ensure that you have the
|
To build Typst yourself, first ensure that you have the
|
||||||
[latest stable Rust][rust] installed. Then, clone this repository and build the
|
[latest stable Rust][rust] installed. Then, clone this repository and build the
|
||||||
@ -241,6 +243,8 @@ instant preview. To achieve these goals, we follow three core design principles:
|
|||||||
[docs]: https://typst.app/docs/
|
[docs]: https://typst.app/docs/
|
||||||
[app]: https://typst.app/
|
[app]: https://typst.app/
|
||||||
[discord]: https://discord.gg/2uDybryKPe
|
[discord]: https://discord.gg/2uDybryKPe
|
||||||
|
[forum]: https://forum.typst.app/
|
||||||
|
[universe]: https://typst.app/universe/
|
||||||
[tutorial]: https://typst.app/docs/tutorial/
|
[tutorial]: https://typst.app/docs/tutorial/
|
||||||
[show]: https://typst.app/docs/reference/styling/#show-rules
|
[show]: https://typst.app/docs/reference/styling/#show-rules
|
||||||
[math]: https://typst.app/docs/reference/math/
|
[math]: https://typst.app/docs/reference/math/
|
||||||
@ -254,3 +258,4 @@ instant preview. To achieve these goals, we follow three core design principles:
|
|||||||
[contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md
|
[contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md
|
||||||
[packages]: https://github.com/typst/packages/
|
[packages]: https://github.com/typst/packages/
|
||||||
[`comemo`]: https://github.com/typst/comemo/
|
[`comemo`]: https://github.com/typst/comemo/
|
||||||
|
[snap]: https://snapcraft.io/typst
|
||||||
|
@ -361,7 +361,7 @@ pub struct FontArgs {
|
|||||||
|
|
||||||
/// Ensures system fonts won't be searched, unless explicitly included via
|
/// Ensures system fonts won't be searched, unless explicitly included via
|
||||||
/// `--font-path`.
|
/// `--font-path`.
|
||||||
#[arg(long)]
|
#[arg(long, env = "TYPST_IGNORE_SYSTEM_FONTS")]
|
||||||
pub ignore_system_fonts: bool,
|
pub ignore_system_fonts: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,15 +467,45 @@ display_possible_values!(Feature);
|
|||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum PdfStandard {
|
pub enum PdfStandard {
|
||||||
|
/// PDF 1.4.
|
||||||
|
#[value(name = "1.4")]
|
||||||
|
V_1_4,
|
||||||
|
/// PDF 1.5.
|
||||||
|
#[value(name = "1.5")]
|
||||||
|
V_1_5,
|
||||||
|
/// PDF 1.5.
|
||||||
|
#[value(name = "1.6")]
|
||||||
|
V_1_6,
|
||||||
/// PDF 1.7.
|
/// PDF 1.7.
|
||||||
#[value(name = "1.7")]
|
#[value(name = "1.7")]
|
||||||
V_1_7,
|
V_1_7,
|
||||||
|
/// PDF 2.0.
|
||||||
|
#[value(name = "2.0")]
|
||||||
|
V_2_0,
|
||||||
|
/// PDF/A-1b.
|
||||||
|
#[value(name = "a-1b")]
|
||||||
|
A_1b,
|
||||||
/// PDF/A-2b.
|
/// PDF/A-2b.
|
||||||
#[value(name = "a-2b")]
|
#[value(name = "a-2b")]
|
||||||
A_2b,
|
A_2b,
|
||||||
/// PDF/A-3b.
|
/// PDF/A-2u.
|
||||||
|
#[value(name = "a-2u")]
|
||||||
|
A_2u,
|
||||||
|
/// PDF/A-3u.
|
||||||
#[value(name = "a-3b")]
|
#[value(name = "a-3b")]
|
||||||
A_3b,
|
A_3b,
|
||||||
|
/// PDF/A-3u.
|
||||||
|
#[value(name = "a-3u")]
|
||||||
|
A_3u,
|
||||||
|
/// PDF/A-4.
|
||||||
|
#[value(name = "a-4")]
|
||||||
|
A_4,
|
||||||
|
/// PDF/A-4f.
|
||||||
|
#[value(name = "a-4f")]
|
||||||
|
A_4f,
|
||||||
|
/// PDF/A-4e.
|
||||||
|
#[value(name = "a-4e")]
|
||||||
|
A_4e,
|
||||||
}
|
}
|
||||||
|
|
||||||
display_possible_values!(PdfStandard);
|
display_possible_values!(PdfStandard);
|
||||||
|
@ -6,8 +6,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
||||||
use codespan_reporting::term;
|
use codespan_reporting::term;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::eco_format;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
use pathdiff::diff_paths;
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
use typst::diag::{
|
use typst::diag::{
|
||||||
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
||||||
@ -62,8 +63,7 @@ pub struct CompileConfig {
|
|||||||
/// Opens the output file with the default viewer or a specific program after
|
/// Opens the output file with the default viewer or a specific program after
|
||||||
/// compilation.
|
/// compilation.
|
||||||
pub open: Option<Option<String>>,
|
pub open: Option<Option<String>>,
|
||||||
/// One (or multiple comma-separated) PDF standards that Typst will enforce
|
/// A list of standards the PDF should conform to.
|
||||||
/// conformance with.
|
|
||||||
pub pdf_standards: PdfStandards,
|
pub pdf_standards: PdfStandards,
|
||||||
/// A path to write a Makefile rule describing the current compilation.
|
/// A path to write a Makefile rule describing the current compilation.
|
||||||
pub make_deps: Option<PathBuf>,
|
pub make_deps: Option<PathBuf>,
|
||||||
@ -129,18 +129,9 @@ impl CompileConfig {
|
|||||||
PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
|
PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
|
||||||
});
|
});
|
||||||
|
|
||||||
let pdf_standards = {
|
let pdf_standards = PdfStandards::new(
|
||||||
let list = args
|
&args.pdf_standard.iter().copied().map(Into::into).collect::<Vec<_>>(),
|
||||||
.pdf_standard
|
)?;
|
||||||
.iter()
|
|
||||||
.map(|standard| match standard {
|
|
||||||
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
|
|
||||||
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
|
|
||||||
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
PdfStandards::new(&list)?
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "http-server")]
|
#[cfg(feature = "http-server")]
|
||||||
let server = match watch {
|
let server = match watch {
|
||||||
@ -188,7 +179,7 @@ pub fn compile_once(
|
|||||||
|
|
||||||
match output {
|
match output {
|
||||||
// Export the PDF / PNG.
|
// Export the PDF / PNG.
|
||||||
Ok(()) => {
|
Ok(outputs) => {
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
|
|
||||||
if config.watching {
|
if config.watching {
|
||||||
@ -202,7 +193,7 @@ pub fn compile_once(
|
|||||||
print_diagnostics(world, &[], &warnings, config.diagnostic_format)
|
print_diagnostics(world, &[], &warnings, config.diagnostic_format)
|
||||||
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
||||||
|
|
||||||
write_make_deps(world, config)?;
|
write_make_deps(world, config, outputs)?;
|
||||||
open_output(config)?;
|
open_output(config)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,12 +217,15 @@ pub fn compile_once(
|
|||||||
fn compile_and_export(
|
fn compile_and_export(
|
||||||
world: &mut SystemWorld,
|
world: &mut SystemWorld,
|
||||||
config: &mut CompileConfig,
|
config: &mut CompileConfig,
|
||||||
) -> Warned<SourceResult<()>> {
|
) -> Warned<SourceResult<Vec<Output>>> {
|
||||||
match config.output_format {
|
match config.output_format {
|
||||||
OutputFormat::Html => {
|
OutputFormat::Html => {
|
||||||
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
|
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
|
||||||
let result = output.and_then(|document| export_html(&document, config));
|
let result = output.and_then(|document| export_html(&document, config));
|
||||||
Warned { output: result, warnings }
|
Warned {
|
||||||
|
output: result.map(|()| vec![config.output.clone()]),
|
||||||
|
warnings,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
|
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
|
||||||
@ -257,9 +251,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Export to a paged target format.
|
/// Export to a paged target format.
|
||||||
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
|
fn export_paged(
|
||||||
|
document: &PagedDocument,
|
||||||
|
config: &CompileConfig,
|
||||||
|
) -> SourceResult<Vec<Output>> {
|
||||||
match config.output_format {
|
match config.output_format {
|
||||||
OutputFormat::Pdf => export_pdf(document, config),
|
OutputFormat::Pdf => {
|
||||||
|
export_pdf(document, config).map(|()| vec![config.output.clone()])
|
||||||
|
}
|
||||||
OutputFormat::Png => {
|
OutputFormat::Png => {
|
||||||
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
|
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
|
||||||
}
|
}
|
||||||
@ -286,6 +285,7 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let options = PdfOptions {
|
let options = PdfOptions {
|
||||||
ident: Smart::Auto,
|
ident: Smart::Auto,
|
||||||
timestamp,
|
timestamp,
|
||||||
@ -327,7 +327,7 @@ fn export_image(
|
|||||||
document: &PagedDocument,
|
document: &PagedDocument,
|
||||||
config: &CompileConfig,
|
config: &CompileConfig,
|
||||||
fmt: ImageExportFormat,
|
fmt: ImageExportFormat,
|
||||||
) -> StrResult<()> {
|
) -> StrResult<Vec<Output>> {
|
||||||
// Determine whether we have indexable templates in output
|
// Determine whether we have indexable templates in output
|
||||||
let can_handle_multiple = match config.output {
|
let can_handle_multiple = match config.output {
|
||||||
Output::Stdout => false,
|
Output::Stdout => false,
|
||||||
@ -341,7 +341,7 @@ fn export_image(
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(i, _)| {
|
.filter(|(i, _)| {
|
||||||
config.pages.as_ref().map_or(true, |exported_page_ranges| {
|
config.pages.as_ref().is_none_or(|exported_page_ranges| {
|
||||||
exported_page_ranges.includes_page_index(*i)
|
exported_page_ranges.includes_page_index(*i)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -383,7 +383,7 @@ fn export_image(
|
|||||||
&& config.export_cache.is_cached(*i, &page.frame)
|
&& config.export_cache.is_cached(*i, &page.frame)
|
||||||
&& path.exists()
|
&& path.exists()
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(Output::Path(path.to_path_buf()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Output::Path(path.to_owned())
|
Output::Path(path.to_owned())
|
||||||
@ -392,11 +392,9 @@ fn export_image(
|
|||||||
};
|
};
|
||||||
|
|
||||||
export_image_page(config, page, &output, fmt)?;
|
export_image_page(config, page, &output, fmt)?;
|
||||||
Ok(())
|
Ok(output)
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<()>, EcoString>>()?;
|
.collect::<StrResult<Vec<Output>>>()
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod output_template {
|
mod output_template {
|
||||||
@ -501,14 +499,25 @@ impl ExportCache {
|
|||||||
/// Writes a Makefile rule describing the relationship between the output and
|
/// Writes a Makefile rule describing the relationship between the output and
|
||||||
/// its dependencies to the path specified by the --make-deps argument, if it
|
/// its dependencies to the path specified by the --make-deps argument, if it
|
||||||
/// was provided.
|
/// was provided.
|
||||||
fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> {
|
fn write_make_deps(
|
||||||
|
world: &mut SystemWorld,
|
||||||
|
config: &CompileConfig,
|
||||||
|
outputs: Vec<Output>,
|
||||||
|
) -> StrResult<()> {
|
||||||
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
|
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
|
||||||
let Output::Path(output_path) = &config.output else {
|
let Ok(output_paths) = outputs
|
||||||
bail!("failed to create make dependencies file because output was stdout")
|
.into_iter()
|
||||||
};
|
.filter_map(|o| match o {
|
||||||
let Some(output_path) = output_path.as_os_str().to_str() else {
|
Output::Path(path) => Some(path.into_os_string().into_string()),
|
||||||
|
Output::Stdout => None,
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
else {
|
||||||
bail!("failed to create make dependencies file because output path was not valid unicode")
|
bail!("failed to create make dependencies file because output path was not valid unicode")
|
||||||
};
|
};
|
||||||
|
if output_paths.is_empty() {
|
||||||
|
bail!("failed to create make dependencies file because output was stdout")
|
||||||
|
}
|
||||||
|
|
||||||
// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
|
// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
|
||||||
// perfect as some special characters can't be escaped.
|
// perfect as some special characters can't be escaped.
|
||||||
@ -522,6 +531,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
|
|||||||
res.push('$');
|
res.push('$');
|
||||||
slashes = 0;
|
slashes = 0;
|
||||||
}
|
}
|
||||||
|
':' => {
|
||||||
|
res.push('\\');
|
||||||
|
slashes = 0;
|
||||||
|
}
|
||||||
' ' | '\t' => {
|
' ' | '\t' => {
|
||||||
// `munge`'s source contains a comment here that says: "A
|
// `munge`'s source contains a comment here that says: "A
|
||||||
// space or tab preceded by 2N+1 backslashes represents N
|
// space or tab preceded by 2N+1 backslashes represents N
|
||||||
@ -544,18 +557,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
|
|||||||
|
|
||||||
fn write(
|
fn write(
|
||||||
make_deps_path: &Path,
|
make_deps_path: &Path,
|
||||||
output_path: &str,
|
output_paths: Vec<String>,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
dependencies: impl Iterator<Item = PathBuf>,
|
dependencies: impl Iterator<Item = PathBuf>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut file = File::create(make_deps_path)?;
|
let mut file = File::create(make_deps_path)?;
|
||||||
|
let current_dir = std::env::current_dir()?;
|
||||||
|
let relative_root = diff_paths(&root, ¤t_dir).unwrap_or(root.clone());
|
||||||
|
|
||||||
file.write_all(munge(output_path).as_bytes())?;
|
for (i, output_path) in output_paths.into_iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
file.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
file.write_all(munge(&output_path).as_bytes())?;
|
||||||
|
}
|
||||||
file.write_all(b":")?;
|
file.write_all(b":")?;
|
||||||
for dependency in dependencies {
|
for dependency in dependencies {
|
||||||
let Some(dependency) =
|
let relative_dependency = match dependency.strip_prefix(&root) {
|
||||||
dependency.strip_prefix(&root).unwrap_or(&dependency).to_str()
|
Ok(root_relative_dependency) => {
|
||||||
else {
|
relative_root.join(root_relative_dependency)
|
||||||
|
}
|
||||||
|
Err(_) => dependency,
|
||||||
|
};
|
||||||
|
let Some(relative_dependency) = relative_dependency.to_str() else {
|
||||||
// Silently skip paths that aren't valid unicode so we still
|
// Silently skip paths that aren't valid unicode so we still
|
||||||
// produce a rule that will work for the other paths that can be
|
// produce a rule that will work for the other paths that can be
|
||||||
// processed.
|
// processed.
|
||||||
@ -563,14 +587,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
|
|||||||
};
|
};
|
||||||
|
|
||||||
file.write_all(b" ")?;
|
file.write_all(b" ")?;
|
||||||
file.write_all(munge(dependency).as_bytes())?;
|
file.write_all(munge(relative_dependency).as_bytes())?;
|
||||||
}
|
}
|
||||||
file.write_all(b"\n")?;
|
file.write_all(b"\n")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
write(make_deps_path, output_path, world.root().to_owned(), world.dependencies())
|
write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
eco_format!("failed to create make dependencies file due to IO error ({err})")
|
eco_format!("failed to create make dependencies file due to IO error ({err})")
|
||||||
})
|
})
|
||||||
@ -732,3 +756,23 @@ impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PdfStandard> for typst_pdf::PdfStandard {
|
||||||
|
fn from(standard: PdfStandard) -> Self {
|
||||||
|
match standard {
|
||||||
|
PdfStandard::V_1_4 => typst_pdf::PdfStandard::V_1_4,
|
||||||
|
PdfStandard::V_1_5 => typst_pdf::PdfStandard::V_1_5,
|
||||||
|
PdfStandard::V_1_6 => typst_pdf::PdfStandard::V_1_6,
|
||||||
|
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
|
||||||
|
PdfStandard::V_2_0 => typst_pdf::PdfStandard::V_2_0,
|
||||||
|
PdfStandard::A_1b => typst_pdf::PdfStandard::A_1b,
|
||||||
|
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
|
||||||
|
PdfStandard::A_2u => typst_pdf::PdfStandard::A_2u,
|
||||||
|
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
|
||||||
|
PdfStandard::A_3u => typst_pdf::PdfStandard::A_3u,
|
||||||
|
PdfStandard::A_4 => typst_pdf::PdfStandard::A_4,
|
||||||
|
PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f,
|
||||||
|
PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ use comemo::Track;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
|
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
|
||||||
|
use typst::engine::Sink;
|
||||||
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
||||||
use typst::layout::PagedDocument;
|
use typst::layout::PagedDocument;
|
||||||
use typst::syntax::Span;
|
use typst::syntax::Span;
|
||||||
@ -58,6 +59,8 @@ fn retrieve(
|
|||||||
let selector = eval_string(
|
let selector = eval_string(
|
||||||
&typst::ROUTINES,
|
&typst::ROUTINES,
|
||||||
world.track(),
|
world.track(),
|
||||||
|
// TODO: propagate warnings
|
||||||
|
Sink::new().track_mut(),
|
||||||
&command.selector,
|
&command.selector,
|
||||||
Span::detached(),
|
Span::detached(),
|
||||||
EvalMode::Code,
|
EvalMode::Code,
|
||||||
|
@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
|
|||||||
// Perform initial compilation.
|
// Perform initial compilation.
|
||||||
timer.record(&mut world, |world| compile_once(world, &mut config))??;
|
timer.record(&mut world, |world| compile_once(world, &mut config))??;
|
||||||
|
|
||||||
// Watch all dependencies of the initial compilation.
|
|
||||||
watcher.update(world.dependencies())?;
|
|
||||||
|
|
||||||
// Recompile whenever something relevant happens.
|
// Recompile whenever something relevant happens.
|
||||||
loop {
|
loop {
|
||||||
|
// Watch all dependencies of the most recent compilation.
|
||||||
|
watcher.update(world.dependencies())?;
|
||||||
|
|
||||||
// Wait until anything relevant happens.
|
// Wait until anything relevant happens.
|
||||||
watcher.wait()?;
|
watcher.wait()?;
|
||||||
|
|
||||||
@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
|
|||||||
|
|
||||||
// Evict the cache.
|
// Evict the cache.
|
||||||
comemo::evict(10);
|
comemo::evict(10);
|
||||||
|
|
||||||
// Adjust the file watching.
|
|
||||||
watcher.update(world.dependencies())?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +201,10 @@ impl Watcher {
|
|||||||
let event = event
|
let event = event
|
||||||
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
|
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
|
||||||
|
|
||||||
|
if !is_relevant_event_kind(&event.kind) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Workaround for notify-rs' implicit unwatch on remove/rename
|
// Workaround for notify-rs' implicit unwatch on remove/rename
|
||||||
// (triggered by some editors when saving files) with the
|
// (triggered by some editors when saving files) with the
|
||||||
// inotify backend. By keeping track of the potentially
|
// inotify backend. By keeping track of the potentially
|
||||||
@ -224,7 +225,17 @@ impl Watcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
relevant |= self.is_event_relevant(&event);
|
// Don't recompile because the output file changed.
|
||||||
|
// FIXME: This doesn't work properly for multifile image export.
|
||||||
|
if event
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
relevant = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a relevant event or if any of the missing files now
|
// If we found a relevant event or if any of the missing files now
|
||||||
@ -234,19 +245,11 @@ impl Watcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a watch event is relevant for compilation.
|
|
||||||
fn is_event_relevant(&self, event: ¬ify::Event) -> bool {
|
|
||||||
// Never recompile because the output file changed.
|
|
||||||
if event
|
|
||||||
.paths
|
|
||||||
.iter()
|
|
||||||
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match &event.kind {
|
/// Whether a kind of watch event is relevant for compilation.
|
||||||
|
fn is_relevant_event_kind(kind: ¬ify::EventKind) -> bool {
|
||||||
|
match kind {
|
||||||
notify::EventKind::Any => true,
|
notify::EventKind::Any => true,
|
||||||
notify::EventKind::Access(_) => false,
|
notify::EventKind::Access(_) => false,
|
||||||
notify::EventKind::Create(_) => true,
|
notify::EventKind::Create(_) => true,
|
||||||
@ -261,7 +264,6 @@ impl Watcher {
|
|||||||
notify::EventKind::Other => false,
|
notify::EventKind::Other => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// The status in which the watcher can be.
|
/// The status in which the watcher can be.
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
|
@ -210,7 +210,9 @@ impl World for SystemWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn font(&self, index: usize) -> Option<Font> {
|
fn font(&self, index: usize) -> Option<Font> {
|
||||||
self.fonts[index].get()
|
// comemo's validation may invoke this function with an invalid index. This is
|
||||||
|
// impossible in typst-cli but possible if a custom tool mutates the fonts.
|
||||||
|
self.fonts.get(index)?.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
|
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
|
||||||
|
@ -466,7 +466,7 @@ impl<'a> CapturesVisitor<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Code and content blocks create a scope.
|
// Code and content blocks create a scope.
|
||||||
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
|
Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => {
|
||||||
self.internal.enter();
|
self.internal.enter();
|
||||||
for child in node.children() {
|
for child in node.children() {
|
||||||
self.visit(child);
|
self.visit(child);
|
||||||
@ -516,7 +516,7 @@ impl<'a> CapturesVisitor<'a> {
|
|||||||
|
|
||||||
// A let expression contains a binding, but that binding is only
|
// A let expression contains a binding, but that binding is only
|
||||||
// active after the body is evaluated.
|
// active after the body is evaluated.
|
||||||
Some(ast::Expr::Let(expr)) => {
|
Some(ast::Expr::LetBinding(expr)) => {
|
||||||
if let Some(init) = expr.init() {
|
if let Some(init) = expr.init() {
|
||||||
self.visit(init.to_untyped());
|
self.visit(init.to_untyped());
|
||||||
}
|
}
|
||||||
@ -529,7 +529,7 @@ impl<'a> CapturesVisitor<'a> {
|
|||||||
// A for loop contains one or two bindings in its pattern. These are
|
// A for loop contains one or two bindings in its pattern. These are
|
||||||
// active after the iterable is evaluated but before the body is
|
// active after the iterable is evaluated but before the body is
|
||||||
// evaluated.
|
// evaluated.
|
||||||
Some(ast::Expr::For(expr)) => {
|
Some(ast::Expr::ForLoop(expr)) => {
|
||||||
self.visit(expr.iterable().to_untyped());
|
self.visit(expr.iterable().to_untyped());
|
||||||
self.internal.enter();
|
self.internal.enter();
|
||||||
|
|
||||||
@ -544,7 +544,7 @@ impl<'a> CapturesVisitor<'a> {
|
|||||||
|
|
||||||
// An import contains items, but these are active only after the
|
// An import contains items, but these are active only after the
|
||||||
// path is evaluated.
|
// path is evaluated.
|
||||||
Some(ast::Expr::Import(expr)) => {
|
Some(ast::Expr::ModuleImport(expr)) => {
|
||||||
self.visit(expr.source().to_untyped());
|
self.visit(expr.source().to_untyped());
|
||||||
if let Some(ast::Imports::Items(items)) = expr.imports() {
|
if let Some(ast::Imports::Items(items)) = expr.imports() {
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
|
@ -30,7 +30,7 @@ fn eval_code<'a>(
|
|||||||
while let Some(expr) = exprs.next() {
|
while let Some(expr) = exprs.next() {
|
||||||
let span = expr.span();
|
let span = expr.span();
|
||||||
let value = match expr {
|
let value = match expr {
|
||||||
ast::Expr::Set(set) => {
|
ast::Expr::SetRule(set) => {
|
||||||
let styles = set.eval(vm)?;
|
let styles = set.eval(vm)?;
|
||||||
if vm.flow.is_some() {
|
if vm.flow.is_some() {
|
||||||
break;
|
break;
|
||||||
@ -39,7 +39,7 @@ fn eval_code<'a>(
|
|||||||
let tail = eval_code(vm, exprs)?.display();
|
let tail = eval_code(vm, exprs)?.display();
|
||||||
Value::Content(tail.styled_with_map(styles))
|
Value::Content(tail.styled_with_map(styles))
|
||||||
}
|
}
|
||||||
ast::Expr::Show(show) => {
|
ast::Expr::ShowRule(show) => {
|
||||||
let recipe = show.eval(vm)?;
|
let recipe = show.eval(vm)?;
|
||||||
if vm.flow.is_some() {
|
if vm.flow.is_some() {
|
||||||
break;
|
break;
|
||||||
@ -94,9 +94,9 @@ impl Eval for ast::Expr<'_> {
|
|||||||
Self::Label(v) => v.eval(vm),
|
Self::Label(v) => v.eval(vm),
|
||||||
Self::Ref(v) => v.eval(vm).map(Value::Content),
|
Self::Ref(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Heading(v) => v.eval(vm).map(Value::Content),
|
Self::Heading(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::List(v) => v.eval(vm).map(Value::Content),
|
Self::ListItem(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Enum(v) => v.eval(vm).map(Value::Content),
|
Self::EnumItem(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Term(v) => v.eval(vm).map(Value::Content),
|
Self::TermItem(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Equation(v) => v.eval(vm).map(Value::Content),
|
Self::Equation(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Math(v) => v.eval(vm).map(Value::Content),
|
Self::Math(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::MathText(v) => v.eval(vm).map(Value::Content),
|
Self::MathText(v) => v.eval(vm).map(Value::Content),
|
||||||
@ -116,8 +116,8 @@ impl Eval for ast::Expr<'_> {
|
|||||||
Self::Float(v) => v.eval(vm),
|
Self::Float(v) => v.eval(vm),
|
||||||
Self::Numeric(v) => v.eval(vm),
|
Self::Numeric(v) => v.eval(vm),
|
||||||
Self::Str(v) => v.eval(vm),
|
Self::Str(v) => v.eval(vm),
|
||||||
Self::Code(v) => v.eval(vm),
|
Self::CodeBlock(v) => v.eval(vm),
|
||||||
Self::Content(v) => v.eval(vm).map(Value::Content),
|
Self::ContentBlock(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Array(v) => v.eval(vm).map(Value::Array),
|
Self::Array(v) => v.eval(vm).map(Value::Array),
|
||||||
Self::Dict(v) => v.eval(vm).map(Value::Dict),
|
Self::Dict(v) => v.eval(vm).map(Value::Dict),
|
||||||
Self::Parenthesized(v) => v.eval(vm),
|
Self::Parenthesized(v) => v.eval(vm),
|
||||||
@ -126,19 +126,19 @@ impl Eval for ast::Expr<'_> {
|
|||||||
Self::Closure(v) => v.eval(vm),
|
Self::Closure(v) => v.eval(vm),
|
||||||
Self::Unary(v) => v.eval(vm),
|
Self::Unary(v) => v.eval(vm),
|
||||||
Self::Binary(v) => v.eval(vm),
|
Self::Binary(v) => v.eval(vm),
|
||||||
Self::Let(v) => v.eval(vm),
|
Self::LetBinding(v) => v.eval(vm),
|
||||||
Self::DestructAssign(v) => v.eval(vm),
|
Self::DestructAssignment(v) => v.eval(vm),
|
||||||
Self::Set(_) => bail!(forbidden("set")),
|
Self::SetRule(_) => bail!(forbidden("set")),
|
||||||
Self::Show(_) => bail!(forbidden("show")),
|
Self::ShowRule(_) => bail!(forbidden("show")),
|
||||||
Self::Contextual(v) => v.eval(vm).map(Value::Content),
|
Self::Contextual(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Conditional(v) => v.eval(vm),
|
Self::Conditional(v) => v.eval(vm),
|
||||||
Self::While(v) => v.eval(vm),
|
Self::WhileLoop(v) => v.eval(vm),
|
||||||
Self::For(v) => v.eval(vm),
|
Self::ForLoop(v) => v.eval(vm),
|
||||||
Self::Import(v) => v.eval(vm),
|
Self::ModuleImport(v) => v.eval(vm),
|
||||||
Self::Include(v) => v.eval(vm).map(Value::Content),
|
Self::ModuleInclude(v) => v.eval(vm).map(Value::Content),
|
||||||
Self::Break(v) => v.eval(vm),
|
Self::LoopBreak(v) => v.eval(vm),
|
||||||
Self::Continue(v) => v.eval(vm),
|
Self::LoopContinue(v) => v.eval(vm),
|
||||||
Self::Return(v) => v.eval(vm),
|
Self::FuncReturn(v) => v.eval(vm),
|
||||||
}?
|
}?
|
||||||
.spanned(span);
|
.spanned(span);
|
||||||
|
|
||||||
|
@ -44,11 +44,10 @@ impl Eval for ast::ModuleImport<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If there is a rename, import the source itself under that name.
|
// If there is a rename, import the source itself under that name.
|
||||||
let bare_name = self.bare_name();
|
|
||||||
let new_name = self.new_name();
|
let new_name = self.new_name();
|
||||||
if let Some(new_name) = new_name {
|
if let Some(new_name) = new_name {
|
||||||
if let Ok(source_name) = &bare_name {
|
if let ast::Expr::Ident(ident) = self.source() {
|
||||||
if source_name == new_name.as_str() {
|
if ident.as_str() == new_name.as_str() {
|
||||||
// Warn on `import x as x`
|
// Warn on `import x as x`
|
||||||
vm.engine.sink.warn(warning!(
|
vm.engine.sink.warn(warning!(
|
||||||
new_name.span(),
|
new_name.span(),
|
||||||
@ -57,6 +56,7 @@ impl Eval for ast::ModuleImport<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define renamed module on the scope.
|
||||||
vm.define(new_name, source.clone());
|
vm.define(new_name, source.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +101,7 @@ pub fn eval(
|
|||||||
pub fn eval_string(
|
pub fn eval_string(
|
||||||
routines: &Routines,
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
|
sink: TrackedMut<Sink>,
|
||||||
string: &str,
|
string: &str,
|
||||||
span: Span,
|
span: Span,
|
||||||
mode: EvalMode,
|
mode: EvalMode,
|
||||||
@ -121,7 +122,6 @@ pub fn eval_string(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the engine.
|
// Prepare the engine.
|
||||||
let mut sink = Sink::new();
|
|
||||||
let introspector = Introspector::default();
|
let introspector = Introspector::default();
|
||||||
let traced = Traced::default();
|
let traced = Traced::default();
|
||||||
let engine = Engine {
|
let engine = Engine {
|
||||||
@ -129,7 +129,7 @@ pub fn eval_string(
|
|||||||
world,
|
world,
|
||||||
introspector: introspector.track(),
|
introspector: introspector.track(),
|
||||||
traced: traced.track(),
|
traced: traced.track(),
|
||||||
sink: sink.track_mut(),
|
sink,
|
||||||
route: Route::default(),
|
route: Route::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ fn eval_markup<'a>(
|
|||||||
|
|
||||||
while let Some(expr) = exprs.next() {
|
while let Some(expr) = exprs.next() {
|
||||||
match expr {
|
match expr {
|
||||||
ast::Expr::Set(set) => {
|
ast::Expr::SetRule(set) => {
|
||||||
let styles = set.eval(vm)?;
|
let styles = set.eval(vm)?;
|
||||||
if vm.flow.is_some() {
|
if vm.flow.is_some() {
|
||||||
break;
|
break;
|
||||||
@ -41,7 +41,7 @@ fn eval_markup<'a>(
|
|||||||
|
|
||||||
seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
|
seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
|
||||||
}
|
}
|
||||||
ast::Expr::Show(show) => {
|
ast::Expr::ShowRule(show) => {
|
||||||
let recipe = show.eval(vm)?;
|
let recipe = show.eval(vm)?;
|
||||||
if vm.flow.is_some() {
|
if vm.flow.is_some() {
|
||||||
break;
|
break;
|
||||||
|
@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> {
|
|||||||
|
|
||||||
let transform = self.transform();
|
let transform = self.transform();
|
||||||
let transform = match transform {
|
let transform = match transform {
|
||||||
ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?),
|
ast::Expr::SetRule(set) => Transformation::Style(set.eval(vm)?),
|
||||||
expr => expr.eval(vm)?.cast::<Transformation>().at(transform.span())?,
|
expr => expr.eval(vm)?.cast::<Transformation>().at(transform.span())?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,8 +83,8 @@ fn html_document_impl(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
|
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
|
||||||
|
let introspector = Introspector::html(&output);
|
||||||
let root = root_element(output, &info)?;
|
let root = root_element(output, &info)?;
|
||||||
let introspector = Introspector::html(&root);
|
|
||||||
|
|
||||||
Ok(HtmlDocument { info, root, introspector })
|
Ok(HtmlDocument { info, root, introspector })
|
||||||
}
|
}
|
||||||
@ -263,13 +263,13 @@ fn handle(
|
|||||||
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
|
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
|
||||||
/// supplying a suitable `<head>`.
|
/// supplying a suitable `<head>`.
|
||||||
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
|
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
|
||||||
|
let head = head_element(info);
|
||||||
let body = match classify_output(output)? {
|
let body = match classify_output(output)? {
|
||||||
OutputKind::Html(element) => return Ok(element),
|
OutputKind::Html(element) => return Ok(element),
|
||||||
OutputKind::Body(body) => body,
|
OutputKind::Body(body) => body,
|
||||||
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
|
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
|
||||||
};
|
};
|
||||||
Ok(HtmlElement::new(tag::html)
|
Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
|
||||||
.with_children(vec![head_element(info).into(), body.into()]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a `<head>` element.
|
/// Generate a `<head>` element.
|
||||||
@ -302,23 +302,41 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !info.author.is_empty() {
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::meta)
|
||||||
|
.with_attr(attr::name, "authors")
|
||||||
|
.with_attr(attr::content, info.author.join(", "))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.keywords.is_empty() {
|
||||||
|
children.push(
|
||||||
|
HtmlElement::new(tag::meta)
|
||||||
|
.with_attr(attr::name, "keywords")
|
||||||
|
.with_attr(attr::content, info.keywords.join(", "))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
HtmlElement::new(tag::head).with_children(children)
|
HtmlElement::new(tag::head).with_children(children)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine which kind of output the user generated.
|
/// Determine which kind of output the user generated.
|
||||||
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
|
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
|
||||||
let len = output.len();
|
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
|
||||||
for node in &mut output {
|
for node in &mut output {
|
||||||
let HtmlNode::Element(elem) = node else { continue };
|
let HtmlNode::Element(elem) = node else { continue };
|
||||||
let tag = elem.tag;
|
let tag = elem.tag;
|
||||||
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
|
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
|
||||||
match (tag, len) {
|
match (tag, count) {
|
||||||
(tag::html, 1) => return Ok(OutputKind::Html(take())),
|
(tag::html, 1) => return Ok(OutputKind::Html(take())),
|
||||||
(tag::body, 1) => return Ok(OutputKind::Body(take())),
|
(tag::body, 1) => return Ok(OutputKind::Body(take())),
|
||||||
(tag::html | tag::body, _) => bail!(
|
(tag::html | tag::body, _) => bail!(
|
||||||
elem.span,
|
elem.span,
|
||||||
"`{}` element must be the only element in the document",
|
"`{}` element must be the only element in the document",
|
||||||
elem.tag
|
elem.tag,
|
||||||
),
|
),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ pub fn analyze_expr(
|
|||||||
ast::Expr::Str(v) => Value::Str(v.get().into()),
|
ast::Expr::Str(v) => Value::Str(v.get().into()),
|
||||||
_ => {
|
_ => {
|
||||||
if node.kind() == SyntaxKind::Contextual {
|
if node.kind() == SyntaxKind::Contextual {
|
||||||
if let Some(child) = node.children().last() {
|
if let Some(child) = node.children().next_back() {
|
||||||
return analyze_expr(world, &child);
|
return analyze_expr(world, &child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Behind existing atom or identifier: "$a|$" or "$abc|$".
|
// Behind existing atom or identifier: "$a|$" or "$abc|$".
|
||||||
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) {
|
if matches!(
|
||||||
|
ctx.leaf.kind(),
|
||||||
|
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent
|
||||||
|
) {
|
||||||
ctx.from = ctx.leaf.offset();
|
ctx.from = ctx.leaf.offset();
|
||||||
math_completions(ctx);
|
math_completions(ctx);
|
||||||
return true;
|
return true;
|
||||||
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
|
|||||||
// Behind an expression plus dot: "emoji.|".
|
// Behind an expression plus dot: "emoji.|".
|
||||||
if_chain! {
|
if_chain! {
|
||||||
if ctx.leaf.kind() == SyntaxKind::Dot
|
if ctx.leaf.kind() == SyntaxKind::Dot
|
||||||
|| (ctx.leaf.kind() == SyntaxKind::Text
|
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
||||||
&& ctx.leaf.text() == ".");
|
&& ctx.leaf.text() == ".");
|
||||||
if ctx.leaf.range().end == ctx.cursor;
|
if ctx.leaf.range().end == ctx.cursor;
|
||||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
if let Some(prev) = ctx.leaf.prev_sibling();
|
||||||
@ -398,9 +401,27 @@ fn field_access_completions(
|
|||||||
value: &Value,
|
value: &Value,
|
||||||
styles: &Option<Styles>,
|
styles: &Option<Styles>,
|
||||||
) {
|
) {
|
||||||
for (name, binding) in value.ty().scope().iter() {
|
let scopes = {
|
||||||
|
let ty = value.ty().scope();
|
||||||
|
let elem = match value {
|
||||||
|
Value::Content(content) => Some(content.elem().scope()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
elem.into_iter().chain(Some(ty))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Autocomplete methods from the element's or type's scope. We only complete
|
||||||
|
// those which have a `self` parameter.
|
||||||
|
for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
|
||||||
|
let Ok(func) = binding.read().clone().cast::<Func>() else { continue };
|
||||||
|
if func
|
||||||
|
.params()
|
||||||
|
.and_then(|params| params.first())
|
||||||
|
.is_some_and(|param| param.name == "self")
|
||||||
|
{
|
||||||
ctx.call_completion(name.clone(), binding.read());
|
ctx.call_completion(name.clone(), binding.read());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(scope) = value.scope() {
|
if let Some(scope) = value.scope() {
|
||||||
for (name, binding) in scope.iter() {
|
for (name, binding) in scope.iter() {
|
||||||
@ -496,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
|||||||
// "#import "path.typ": a, b, |".
|
// "#import "path.typ": a, b, |".
|
||||||
if_chain! {
|
if_chain! {
|
||||||
if let Some(prev) = ctx.leaf.prev_sibling();
|
if let Some(prev) = ctx.leaf.prev_sibling();
|
||||||
if let Some(ast::Expr::Import(import)) = prev.get().cast();
|
if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast();
|
||||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
if let Some(ast::Imports::Items(items)) = import.imports();
|
||||||
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
|
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
|
||||||
then {
|
then {
|
||||||
@ -515,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
|||||||
if let Some(grand) = parent.parent();
|
if let Some(grand) = parent.parent();
|
||||||
if grand.kind() == SyntaxKind::ImportItems;
|
if grand.kind() == SyntaxKind::ImportItems;
|
||||||
if let Some(great) = grand.parent();
|
if let Some(great) = grand.parent();
|
||||||
if let Some(ast::Expr::Import(import)) = great.get().cast();
|
if let Some(ast::Expr::ModuleImport(import)) = great.get().cast();
|
||||||
if let Some(ast::Imports::Items(items)) = import.imports();
|
if let Some(ast::Imports::Items(items)) = import.imports();
|
||||||
if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
|
if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
|
||||||
then {
|
then {
|
||||||
@ -656,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
|
|||||||
if let Some(args) = parent.get().cast::<ast::Args>();
|
if let Some(args) = parent.get().cast::<ast::Args>();
|
||||||
if let Some(grand) = parent.parent();
|
if let Some(grand) = parent.parent();
|
||||||
if let Some(expr) = grand.get().cast::<ast::Expr>();
|
if let Some(expr) = grand.get().cast::<ast::Expr>();
|
||||||
let set = matches!(expr, ast::Expr::Set(_));
|
let set = matches!(expr, ast::Expr::SetRule(_));
|
||||||
if let Some(callee) = match expr {
|
if let Some(callee) = match expr {
|
||||||
ast::Expr::FuncCall(call) => Some(call.callee()),
|
ast::Expr::FuncCall(call) => Some(call.callee()),
|
||||||
ast::Expr::Set(set) => Some(set.target()),
|
ast::Expr::SetRule(set) => Some(set.target()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
then {
|
then {
|
||||||
@ -1442,7 +1463,7 @@ impl<'a> CompletionContext<'a> {
|
|||||||
let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
|
let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
|
||||||
named_items(self.world, self.leaf.clone(), |item| {
|
named_items(self.world, self.leaf.clone(), |item| {
|
||||||
let name = item.name();
|
let name = item.name();
|
||||||
if !name.is_empty() && item.value().as_ref().map_or(true, filter) {
|
if !name.is_empty() && item.value().as_ref().is_none_or(filter) {
|
||||||
defined.insert(name.clone(), item.value());
|
defined.insert(name.clone(), item.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1747,4 +1768,26 @@ mod tests {
|
|||||||
.must_include(["this", "that"])
|
.must_include(["this", "that"])
|
||||||
.must_exclude(["*", "figure"]);
|
.must_exclude(["*", "figure"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_type_methods() {
|
||||||
|
test("#\"hello\".", -1).must_include(["len", "contains"]);
|
||||||
|
test("#table().", -1).must_exclude(["cell"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_content_methods() {
|
||||||
|
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
|
||||||
|
.must_include(["indented", "body", "page"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autocomplete_symbol_variants() {
|
||||||
|
test("#sym.arrow.", -1)
|
||||||
|
.must_include(["r", "dashed"])
|
||||||
|
.must_exclude(["cases"]);
|
||||||
|
test("$ arrow. $", -3)
|
||||||
|
.must_include(["r", "dashed"])
|
||||||
|
.must_exclude(["cases"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use std::num::NonZeroUsize;
|
|||||||
use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
|
use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
|
||||||
use typst::model::{Destination, Url};
|
use typst::model::{Destination, Url};
|
||||||
use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind};
|
use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind};
|
||||||
use typst::visualize::Geometry;
|
use typst::visualize::{Curve, CurveItem, FillRule, Geometry};
|
||||||
use typst::WorldExt;
|
use typst::WorldExt;
|
||||||
|
|
||||||
use crate::IdeWorld;
|
use crate::IdeWorld;
|
||||||
@ -53,10 +53,20 @@ pub fn jump_from_click(
|
|||||||
for (mut pos, item) in frame.items().rev() {
|
for (mut pos, item) in frame.items().rev() {
|
||||||
match item {
|
match item {
|
||||||
FrameItem::Group(group) => {
|
FrameItem::Group(group) => {
|
||||||
// TODO: Handle transformation.
|
let pos = click - pos;
|
||||||
if let Some(span) =
|
if let Some(clip) = &group.clip {
|
||||||
jump_from_click(world, document, &group.frame, click - pos)
|
if !clip.contains(FillRule::NonZero, pos) {
|
||||||
{
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Realistic transforms should always be invertible.
|
||||||
|
// An example of one that isn't is a scale of 0, which would
|
||||||
|
// not be clickable anyway.
|
||||||
|
let Some(inv_transform) = group.transform.invert() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let pos = pos.transform_inf(inv_transform);
|
||||||
|
if let Some(span) = jump_from_click(world, document, &group.frame, pos) {
|
||||||
return Some(span);
|
return Some(span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +83,10 @@ pub fn jump_from_click(
|
|||||||
let Some(id) = span.id() else { continue };
|
let Some(id) = span.id() else { continue };
|
||||||
let source = world.source(id).ok()?;
|
let source = world.source(id).ok()?;
|
||||||
let node = source.find(span)?;
|
let node = source.find(span)?;
|
||||||
let pos = if node.kind() == SyntaxKind::Text {
|
let pos = if matches!(
|
||||||
|
node.kind(),
|
||||||
|
SyntaxKind::Text | SyntaxKind::MathText
|
||||||
|
) {
|
||||||
let range = node.range();
|
let range = node.range();
|
||||||
let mut offset = range.start + usize::from(span_offset);
|
let mut offset = range.start + usize::from(span_offset);
|
||||||
if (click.x - pos.x) > width / 2.0 {
|
if (click.x - pos.x) > width / 2.0 {
|
||||||
@ -91,12 +104,35 @@ pub fn jump_from_click(
|
|||||||
}
|
}
|
||||||
|
|
||||||
FrameItem::Shape(shape, span) => {
|
FrameItem::Shape(shape, span) => {
|
||||||
let Geometry::Rect(size) = shape.geometry else { continue };
|
if shape.fill.is_some() {
|
||||||
if is_in_rect(pos, size, click) {
|
let within = match &shape.geometry {
|
||||||
|
Geometry::Line(..) => false,
|
||||||
|
Geometry::Rect(size) => is_in_rect(pos, *size, click),
|
||||||
|
Geometry::Curve(curve) => {
|
||||||
|
curve.contains(shape.fill_rule, click - pos)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if within {
|
||||||
return Jump::from_span(world, *span);
|
return Jump::from_span(world, *span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(stroke) = &shape.stroke {
|
||||||
|
let within = !stroke.thickness.approx_empty() && {
|
||||||
|
// This curve is rooted at (0, 0), not `pos`.
|
||||||
|
let base_curve = match &shape.geometry {
|
||||||
|
Geometry::Line(to) => &Curve(vec![CurveItem::Line(*to)]),
|
||||||
|
Geometry::Rect(size) => &Curve::rect(*size),
|
||||||
|
Geometry::Curve(curve) => curve,
|
||||||
|
};
|
||||||
|
base_curve.stroke_contains(stroke, click - pos)
|
||||||
|
};
|
||||||
|
if within {
|
||||||
|
return Jump::from_span(world, *span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => {
|
FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => {
|
||||||
return Jump::from_span(world, *span);
|
return Jump::from_span(world, *span);
|
||||||
}
|
}
|
||||||
@ -115,7 +151,7 @@ pub fn jump_from_cursor(
|
|||||||
cursor: usize,
|
cursor: usize,
|
||||||
) -> Vec<Position> {
|
) -> Vec<Position> {
|
||||||
fn is_text(node: &LinkedNode) -> bool {
|
fn is_text(node: &LinkedNode) -> bool {
|
||||||
node.get().kind() == SyntaxKind::Text
|
matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
|
||||||
}
|
}
|
||||||
|
|
||||||
let root = LinkedNode::new(source.root());
|
let root = LinkedNode::new(source.root());
|
||||||
@ -143,9 +179,8 @@ pub fn jump_from_cursor(
|
|||||||
fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> {
|
fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> {
|
||||||
for (mut pos, item) in frame.items() {
|
for (mut pos, item) in frame.items() {
|
||||||
if let FrameItem::Group(group) = item {
|
if let FrameItem::Group(group) = item {
|
||||||
// TODO: Handle transformation.
|
|
||||||
if let Some(point) = find_in_frame(&group.frame, span) {
|
if let Some(point) = find_in_frame(&group.frame, span) {
|
||||||
return Some(point + pos);
|
return Some(pos + point.transform(group.transform));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +296,102 @@ mod tests {
|
|||||||
test_click(s, point(21.0, 12.0), cursor(56));
|
test_click(s, point(21.0, 12.0), cursor(56));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_click_math() {
|
||||||
|
test_click("$a + b$", point(28.0, 14.0), cursor(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_click_transform_clip() {
|
||||||
|
let margin = point(10.0, 10.0);
|
||||||
|
test_click(
|
||||||
|
"#rect(width: 20pt, height: 20pt, fill: black)",
|
||||||
|
point(10.0, 10.0) + margin,
|
||||||
|
cursor(1),
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#rect(width: 60pt, height: 10pt, fill: black)",
|
||||||
|
point(5.0, 30.0) + margin,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#rotate(90deg, origin: bottom + left, rect(width: 60pt, height: 10pt, fill: black))",
|
||||||
|
point(5.0, 30.0) + margin,
|
||||||
|
cursor(38),
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#scale(x: 300%, y: 300%, origin: top + left, rect(width: 10pt, height: 10pt, fill: black))",
|
||||||
|
point(20.0, 20.0) + margin,
|
||||||
|
cursor(45),
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#box(width: 10pt, height: 10pt, clip: true, scale(x: 300%, y: 300%, \
|
||||||
|
origin: top + left, rect(width: 10pt, height: 10pt, fill: black)))",
|
||||||
|
point(20.0, 20.0) + margin,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#box(width: 10pt, height: 10pt, clip: false, rect(width: 30pt, height: 30pt, fill: black))",
|
||||||
|
point(20.0, 20.0) + margin,
|
||||||
|
cursor(45),
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#box(width: 10pt, height: 10pt, clip: true, rect(width: 30pt, height: 30pt, fill: black))",
|
||||||
|
point(20.0, 20.0) + margin,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
test_click(
|
||||||
|
"#rotate(90deg, origin: bottom + left)[hello world]",
|
||||||
|
point(5.0, 15.0) + margin,
|
||||||
|
cursor(40),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_click_shapes() {
|
||||||
|
let margin = point(10.0, 10.0);
|
||||||
|
|
||||||
|
test_click(
|
||||||
|
"#rect(width: 30pt, height: 30pt, fill: black)",
|
||||||
|
point(15.0, 15.0) + margin,
|
||||||
|
cursor(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
let circle = "#circle(width: 30pt, height: 30pt, fill: black)";
|
||||||
|
test_click(circle, point(15.0, 15.0) + margin, cursor(1));
|
||||||
|
test_click(circle, point(1.0, 1.0) + margin, None);
|
||||||
|
|
||||||
|
let bowtie =
|
||||||
|
"#polygon(fill: black, (0pt, 0pt), (20pt, 20pt), (20pt, 0pt), (0pt, 20pt))";
|
||||||
|
test_click(bowtie, point(1.0, 2.0) + margin, cursor(1));
|
||||||
|
test_click(bowtie, point(2.0, 1.0) + margin, None);
|
||||||
|
test_click(bowtie, point(19.0, 10.0) + margin, cursor(1));
|
||||||
|
|
||||||
|
let evenodd = r#"#polygon(fill: black, fill-rule: "even-odd",
|
||||||
|
(0pt, 10pt), (30pt, 10pt), (30pt, 20pt), (20pt, 20pt),
|
||||||
|
(20pt, 0pt), (10pt, 0pt), (10pt, 30pt), (20pt, 30pt),
|
||||||
|
(20pt, 20pt), (0pt, 20pt))"#;
|
||||||
|
test_click(evenodd, point(15.0, 15.0) + margin, None);
|
||||||
|
test_click(evenodd, point(5.0, 15.0) + margin, cursor(1));
|
||||||
|
test_click(evenodd, point(15.0, 5.0) + margin, cursor(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_click_shapes_stroke() {
|
||||||
|
let margin = point(10.0, 10.0);
|
||||||
|
|
||||||
|
let rect =
|
||||||
|
"#place(dx: 10pt, dy: 10pt, rect(width: 10pt, height: 10pt, stroke: 5pt))";
|
||||||
|
test_click(rect, point(15.0, 15.0) + margin, None);
|
||||||
|
test_click(rect, point(10.0, 15.0) + margin, cursor(27));
|
||||||
|
|
||||||
|
test_click(
|
||||||
|
"#line(angle: 45deg, length: 10pt, stroke: 2pt)",
|
||||||
|
point(2.0, 2.0) + margin,
|
||||||
|
cursor(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_jump_from_cursor() {
|
fn test_jump_from_cursor() {
|
||||||
let s = "*Hello* #box[ABC] World";
|
let s = "*Hello* #box[ABC] World";
|
||||||
@ -268,6 +399,20 @@ mod tests {
|
|||||||
test_cursor(s, 14, pos(1, 37.55, 16.58));
|
test_cursor(s, 14, pos(1, 37.55, 16.58));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_cursor_math() {
|
||||||
|
test_cursor("$a + b$", -3, pos(1, 27.51, 16.83));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jump_from_cursor_transform() {
|
||||||
|
test_cursor(
|
||||||
|
r#"#rotate(90deg, origin: bottom + left, [hello world])"#,
|
||||||
|
-5,
|
||||||
|
pos(1, 10.0, 16.58),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_backlink() {
|
fn test_backlink() {
|
||||||
let s = "#footnote[Hi]";
|
let s = "#footnote[Hi]";
|
||||||
|
@ -232,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
|
|||||||
ast::Expr::FuncCall(call) => {
|
ast::Expr::FuncCall(call) => {
|
||||||
DerefTarget::Callee(expr_node.find(call.callee().span())?)
|
DerefTarget::Callee(expr_node.find(call.callee().span())?)
|
||||||
}
|
}
|
||||||
ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?),
|
ast::Expr::SetRule(set) => {
|
||||||
|
DerefTarget::Callee(expr_node.find(set.target().span())?)
|
||||||
|
}
|
||||||
ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => {
|
ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => {
|
||||||
DerefTarget::VarAccess(expr_node)
|
DerefTarget::VarAccess(expr_node)
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ impl World for TestWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn font(&self, index: usize) -> Option<Font> {
|
fn font(&self, index: usize) -> Option<Font> {
|
||||||
Some(self.base.fonts[index].clone())
|
self.base.fonts.get(index).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||||
|
@ -201,7 +201,7 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Toolti
|
|||||||
if let Some(expr) = grand_grand.cast::<ast::Expr>();
|
if let Some(expr) = grand_grand.cast::<ast::Expr>();
|
||||||
if let Some(ast::Expr::Ident(callee)) = match expr {
|
if let Some(ast::Expr::Ident(callee)) = match expr {
|
||||||
ast::Expr::FuncCall(call) => Some(call.callee()),
|
ast::Expr::FuncCall(call) => Some(call.callee()),
|
||||||
ast::Expr::Set(set) => Some(set.target()),
|
ast::Expr::SetRule(set) => Some(set.target()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,10 +19,13 @@ typst-utils = { workspace = true }
|
|||||||
dirs = { workspace = true, optional = true }
|
dirs = { workspace = true, optional = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
env_proxy = { workspace = true, optional = true }
|
env_proxy = { workspace = true, optional = true }
|
||||||
|
fastrand = { workspace = true, optional = true }
|
||||||
flate2 = { workspace = true, optional = true }
|
flate2 = { workspace = true, optional = true }
|
||||||
fontdb = { workspace = true, optional = true }
|
fontdb = { workspace = true, optional = true }
|
||||||
native-tls = { workspace = true, optional = true }
|
native-tls = { workspace = true, optional = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
tar = { workspace = true, optional = true }
|
tar = { workspace = true, optional = true }
|
||||||
ureq = { workspace = true, optional = true }
|
ureq = { workspace = true, optional = true }
|
||||||
|
|
||||||
@ -41,7 +44,7 @@ fonts = ["dep:fontdb", "fontdb/memmap", "fontdb/fontconfig"]
|
|||||||
downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"]
|
downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"]
|
||||||
|
|
||||||
# Add package downloading utilities, implies `downloads`
|
# Add package downloading utilities, implies `downloads`
|
||||||
packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar"]
|
packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar", "dep:fastrand"]
|
||||||
|
|
||||||
# Embeds some fonts into the binary:
|
# Embeds some fonts into the binary:
|
||||||
# - For text: Libertinus Serif, New Computer Modern
|
# - For text: Libertinus Serif, New Computer Modern
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
//! Download and unpack packages and package indices.
|
//! Download and unpack packages and package indices.
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use serde::Deserialize;
|
||||||
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
|
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
|
||||||
use typst_syntax::package::{
|
use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
|
||||||
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::download::{Downloader, Progress};
|
use crate::download::{Downloader, Progress};
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ pub struct PackageStorage {
|
|||||||
/// The downloader used for fetching the index and packages.
|
/// The downloader used for fetching the index and packages.
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
/// The cached index of the default namespace.
|
/// The cached index of the default namespace.
|
||||||
index: OnceCell<Vec<PackageInfo>>,
|
index: OnceCell<Vec<serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageStorage {
|
impl PackageStorage {
|
||||||
@ -42,6 +42,18 @@ impl PackageStorage {
|
|||||||
package_cache_path: Option<PathBuf>,
|
package_cache_path: Option<PathBuf>,
|
||||||
package_path: Option<PathBuf>,
|
package_path: Option<PathBuf>,
|
||||||
downloader: Downloader,
|
downloader: Downloader,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_index(package_cache_path, package_path, downloader, OnceCell::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new package storage with a pre-defined index.
|
||||||
|
///
|
||||||
|
/// Useful for testing.
|
||||||
|
fn with_index(
|
||||||
|
package_cache_path: Option<PathBuf>,
|
||||||
|
package_path: Option<PathBuf>,
|
||||||
|
downloader: Downloader,
|
||||||
|
index: OnceCell<Vec<serde_json::Value>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
package_cache_path: package_cache_path.or_else(|| {
|
package_cache_path: package_cache_path.or_else(|| {
|
||||||
@ -51,7 +63,7 @@ impl PackageStorage {
|
|||||||
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
|
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
|
||||||
}),
|
}),
|
||||||
downloader,
|
downloader,
|
||||||
index: OnceCell::new(),
|
index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +78,8 @@ impl PackageStorage {
|
|||||||
self.package_path.as_deref()
|
self.package_path.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a package available in the on-disk.
|
/// Makes a package available on-disk and returns the path at which it is
|
||||||
|
/// located (will be either in the cache or package directory).
|
||||||
pub fn prepare_package(
|
pub fn prepare_package(
|
||||||
&self,
|
&self,
|
||||||
spec: &PackageSpec,
|
spec: &PackageSpec,
|
||||||
@ -89,7 +102,7 @@ impl PackageStorage {
|
|||||||
|
|
||||||
// Download from network if it doesn't exist yet.
|
// Download from network if it doesn't exist yet.
|
||||||
if spec.namespace == DEFAULT_NAMESPACE {
|
if spec.namespace == DEFAULT_NAMESPACE {
|
||||||
self.download_package(spec, &dir, progress)?;
|
self.download_package(spec, cache_dir, progress)?;
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
return Ok(dir);
|
return Ok(dir);
|
||||||
}
|
}
|
||||||
@ -99,7 +112,7 @@ impl PackageStorage {
|
|||||||
Err(PackageError::NotFound(spec.clone()))
|
Err(PackageError::NotFound(spec.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to determine the latest version of a package.
|
/// Tries to determine the latest version of a package.
|
||||||
pub fn determine_latest_version(
|
pub fn determine_latest_version(
|
||||||
&self,
|
&self,
|
||||||
spec: &VersionlessPackageSpec,
|
spec: &VersionlessPackageSpec,
|
||||||
@ -109,6 +122,7 @@ impl PackageStorage {
|
|||||||
// version.
|
// version.
|
||||||
self.download_index()?
|
self.download_index()?
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter_map(|value| MinimalPackageInfo::deserialize(value).ok())
|
||||||
.filter(|package| package.name == spec.name)
|
.filter(|package| package.name == spec.name)
|
||||||
.map(|package| package.version)
|
.map(|package| package.version)
|
||||||
.max()
|
.max()
|
||||||
@ -131,7 +145,7 @@ impl PackageStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Download the package index. The result of this is cached for efficiency.
|
/// Download the package index. The result of this is cached for efficiency.
|
||||||
pub fn download_index(&self) -> StrResult<&[PackageInfo]> {
|
fn download_index(&self) -> StrResult<&[serde_json::Value]> {
|
||||||
self.index
|
self.index
|
||||||
.get_or_try_init(|| {
|
.get_or_try_init(|| {
|
||||||
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
|
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
|
||||||
@ -152,10 +166,10 @@ impl PackageStorage {
|
|||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`.
|
/// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`.
|
||||||
pub fn download_package(
|
fn download_package(
|
||||||
&self,
|
&self,
|
||||||
spec: &PackageSpec,
|
spec: &PackageSpec,
|
||||||
package_dir: &Path,
|
cache_dir: &Path,
|
||||||
progress: &mut dyn Progress,
|
progress: &mut dyn Progress,
|
||||||
) -> PackageResult<()> {
|
) -> PackageResult<()> {
|
||||||
assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
|
assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
|
||||||
@ -179,10 +193,132 @@ impl PackageStorage {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The directory in which the package's version lives.
|
||||||
|
let base_dir = cache_dir.join(format!("{}/{}", spec.namespace, spec.name));
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// into a temporary directory first and then move this directory to
|
||||||
|
// its final destination.
|
||||||
|
//
|
||||||
|
// In the `rename` function's documentation it is stated:
|
||||||
|
// > This will not work if the new name is on a different mount point.
|
||||||
|
//
|
||||||
|
// By locating the temporary directory directly next to where the
|
||||||
|
// package directory will live, we are (trying our best) making sure
|
||||||
|
// that `tempdir` and `package_dir` are on the same mount point.
|
||||||
|
let tempdir = Tempdir::create(base_dir.join(format!(
|
||||||
|
".tmp-{}-{}",
|
||||||
|
spec.version,
|
||||||
|
fastrand::u32(..),
|
||||||
|
)))
|
||||||
|
.map_err(|err| error("failed to create temporary package directory", err))?;
|
||||||
|
|
||||||
|
// Decompress the archive into the temporary directory.
|
||||||
let decompressed = flate2::read::GzDecoder::new(data.as_slice());
|
let decompressed = flate2::read::GzDecoder::new(data.as_slice());
|
||||||
tar::Archive::new(decompressed).unpack(package_dir).map_err(|err| {
|
tar::Archive::new(decompressed)
|
||||||
fs::remove_dir_all(package_dir).ok();
|
.unpack(&tempdir)
|
||||||
PackageError::MalformedArchive(Some(eco_format!("{err}")))
|
.map_err(|err| PackageError::MalformedArchive(Some(eco_format!("{err}"))))?;
|
||||||
})
|
|
||||||
|
// When trying to move (i.e., `rename`) the directory from one place to
|
||||||
|
// another and the target/destination directory is empty, then the
|
||||||
|
// operation will succeed (if it's atomic, or hardware doesn't fail, or
|
||||||
|
// power doesn't go off, etc.). If however the target directory is not
|
||||||
|
// empty, i.e., another instance already successfully moved the package,
|
||||||
|
// then we can safely ignore the `DirectoryNotEmpty` error.
|
||||||
|
//
|
||||||
|
// This means that we do not check the integrity of an existing moved
|
||||||
|
// package, just like we don't check the integrity if the package
|
||||||
|
// directory already existed in the first place. If situations with
|
||||||
|
// broken packages still occur even with the rename safeguard, we might
|
||||||
|
// consider more complex solutions like file locking or checksums.
|
||||||
|
match fs::rename(&tempdir, &package_dir) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()),
|
||||||
|
Err(err) => Err(error("failed to move downloaded package directory", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal information required about a package to determine its latest
|
||||||
|
/// version.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MinimalPackageInfo {
|
||||||
|
name: String,
|
||||||
|
version: PackageVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A temporary directory that is a automatically cleaned up.
|
||||||
|
struct Tempdir(PathBuf);
|
||||||
|
|
||||||
|
impl Tempdir {
|
||||||
|
/// Creates a directory at the path and auto-cleans it.
|
||||||
|
fn create(path: PathBuf) -> io::Result<Self> {
|
||||||
|
std::fs::create_dir_all(&path)?;
|
||||||
|
Ok(Self(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Tempdir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
_ = fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for Tempdir {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enriches an I/O error with a message and turns it into a
|
||||||
|
/// `PackageError::Other`.
|
||||||
|
#[cold]
|
||||||
|
fn error(message: &str, err: io::Error) -> PackageError {
|
||||||
|
PackageError::Other(Some(eco_format!("{message}: {err}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lazy_deser_index() {
|
||||||
|
let storage = PackageStorage::with_index(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Downloader::new("typst/test"),
|
||||||
|
OnceCell::with_value(vec![
|
||||||
|
serde_json::json!({
|
||||||
|
"name": "charged-ieee",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"entrypoint": "lib.typ",
|
||||||
|
}),
|
||||||
|
serde_json::json!({
|
||||||
|
"name": "unequivocal-ams",
|
||||||
|
// This version number is currently not valid, so this package
|
||||||
|
// can't be parsed.
|
||||||
|
"version": "0.2.0-dev",
|
||||||
|
"entrypoint": "lib.typ",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "charged-ieee".into(),
|
||||||
|
});
|
||||||
|
assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 }));
|
||||||
|
|
||||||
|
let ams_version = storage.determine_latest_version(&VersionlessPackageSpec {
|
||||||
|
namespace: "preview".into(),
|
||||||
|
name: "unequivocal-ams".into(),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
ams_version,
|
||||||
|
Err("failed to find package @preview/unequivocal-ams".into())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
styles,
|
styles,
|
||||||
self.base,
|
self.base,
|
||||||
self.expand,
|
self.expand,
|
||||||
None,
|
|
||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
self.output.push(Child::Tag(&elem.tag));
|
self.output.push(Child::Tag(&elem.tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lines(lines, styles);
|
let leading = ParElem::leading_in(styles);
|
||||||
|
self.lines(lines, leading, styles);
|
||||||
|
|
||||||
for (c, _) in &self.children[end..] {
|
for (c, _) in &self.children[end..] {
|
||||||
let elem = c.to_packed::<TagElem>().unwrap();
|
let elem = c.to_packed::<TagElem>().unwrap();
|
||||||
@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
)?
|
)?
|
||||||
.into_frames();
|
.into_frames();
|
||||||
|
|
||||||
let spacing = ParElem::spacing_in(styles);
|
let spacing = elem.spacing(styles);
|
||||||
|
let leading = elem.leading(styles);
|
||||||
|
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
|
|
||||||
self.lines(lines, styles);
|
self.lines(lines, leading, styles);
|
||||||
|
|
||||||
self.output.push(Child::Rel(spacing.into(), 4));
|
self.output.push(Child::Rel(spacing.into(), 4));
|
||||||
self.par_situation = ParSituation::Consecutive;
|
self.par_situation = ParSituation::Consecutive;
|
||||||
@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Collect laid-out lines.
|
/// Collect laid-out lines.
|
||||||
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) {
|
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
|
||||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||||
let leading = ParElem::leading_in(styles);
|
|
||||||
let costs = TextElem::costs_in(styles);
|
let costs = TextElem::costs_in(styles);
|
||||||
|
|
||||||
// Determine whether to prevent widow and orphans.
|
// Determine whether to prevent widow and orphans.
|
||||||
|
@ -115,7 +115,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
|||||||
let column_height = regions.size.y;
|
let column_height = regions.size.y;
|
||||||
let backlog: Vec<_> = std::iter::once(&column_height)
|
let backlog: Vec<_> = std::iter::once(&column_height)
|
||||||
.chain(regions.backlog)
|
.chain(regions.backlog)
|
||||||
.flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count))
|
.flat_map(|&h| std::iter::repeat_n(h, self.config.columns.count))
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
|
|||||||
mode: FlowMode,
|
mode: FlowMode,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
// Prepare configuration that is shared across the whole flow.
|
// Prepare configuration that is shared across the whole flow.
|
||||||
let config = Config {
|
let config = configuration(shared, regions, columns, column_gutter, mode);
|
||||||
|
|
||||||
|
// Collect the elements into pre-processed children. These are much easier
|
||||||
|
// to handle than the raw elements.
|
||||||
|
let bump = Bump::new();
|
||||||
|
let children = collect(
|
||||||
|
engine,
|
||||||
|
&bump,
|
||||||
|
children,
|
||||||
|
locator.next(&()),
|
||||||
|
Size::new(config.columns.width, regions.full),
|
||||||
|
regions.expand.x,
|
||||||
|
mode,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut work = Work::new(&children);
|
||||||
|
let mut finished = vec![];
|
||||||
|
|
||||||
|
// This loop runs once per region produced by the flow layout.
|
||||||
|
loop {
|
||||||
|
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
|
||||||
|
finished.push(frame);
|
||||||
|
|
||||||
|
// Terminate the loop when everything is processed, though draining the
|
||||||
|
// backlog if necessary.
|
||||||
|
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
regions.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Fragment::frames(finished))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the flow's configuration.
|
||||||
|
fn configuration<'x>(
|
||||||
|
shared: StyleChain<'x>,
|
||||||
|
regions: Regions,
|
||||||
|
columns: NonZeroUsize,
|
||||||
|
column_gutter: Rel<Abs>,
|
||||||
|
mode: FlowMode,
|
||||||
|
) -> Config<'x> {
|
||||||
|
Config {
|
||||||
mode,
|
mode,
|
||||||
shared,
|
shared,
|
||||||
columns: {
|
columns: {
|
||||||
@ -235,39 +278,7 @@ pub fn layout_flow<'a>(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
|
||||||
|
|
||||||
// Collect the elements into pre-processed children. These are much easier
|
|
||||||
// to handle than the raw elements.
|
|
||||||
let bump = Bump::new();
|
|
||||||
let children = collect(
|
|
||||||
engine,
|
|
||||||
&bump,
|
|
||||||
children,
|
|
||||||
locator.next(&()),
|
|
||||||
Size::new(config.columns.width, regions.full),
|
|
||||||
regions.expand.x,
|
|
||||||
mode,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut work = Work::new(&children);
|
|
||||||
let mut finished = vec![];
|
|
||||||
|
|
||||||
// This loop runs once per region produced by the flow layout.
|
|
||||||
loop {
|
|
||||||
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
|
|
||||||
finished.push(frame);
|
|
||||||
|
|
||||||
// Terminate the loop when everything is processed, though draining the
|
|
||||||
// backlog if necessary.
|
|
||||||
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
regions.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Fragment::frames(finished))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The work that is left to do by flow layout.
|
/// The work that is left to do by flow layout.
|
||||||
|
@ -11,7 +11,7 @@ use typst_library::layout::{
|
|||||||
use typst_library::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use typst_library::visualize::Geometry;
|
use typst_library::visualize::Geometry;
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::{MaybeReverseIter, Numeric};
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
|
generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
|
||||||
@ -574,7 +574,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
|
|
||||||
// Reverse with RTL so that later columns start first.
|
// Reverse with RTL so that later columns start first.
|
||||||
let mut dx = Abs::zero();
|
let mut dx = Abs::zero();
|
||||||
for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
for (x, &col) in self.rcols.iter().enumerate() {
|
||||||
let mut dy = Abs::zero();
|
let mut dy = Abs::zero();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
// We want to only draw the fill starting at the parent
|
// We want to only draw the fill starting at the parent
|
||||||
@ -643,18 +643,13 @@ impl<'a> GridLayouter<'a> {
|
|||||||
.sum()
|
.sum()
|
||||||
};
|
};
|
||||||
let width = self.cell_spanned_width(cell, x);
|
let width = self.cell_spanned_width(cell, x);
|
||||||
// In the grid, cell colspans expand to the right,
|
let mut pos = Point::new(dx, dy);
|
||||||
// so we're at the leftmost (lowest 'x') column
|
if self.is_rtl {
|
||||||
// spanned by the cell. However, in RTL, cells
|
// In RTL cells expand to the left, thus the
|
||||||
// expand to the left. Therefore, without the
|
// position must additionally be offset by the
|
||||||
// offset below, cell fills would start at the
|
// cell's width.
|
||||||
// rightmost visual position of a cell and extend
|
pos.x = self.width - (dx + width);
|
||||||
// over to unrelated columns to the right in RTL.
|
}
|
||||||
// We avoid this by ensuring the fill starts at the
|
|
||||||
// very left of the cell, even with colspan > 1.
|
|
||||||
let offset =
|
|
||||||
if self.is_rtl { -width + col } else { Abs::zero() };
|
|
||||||
let pos = Point::new(dx + offset, dy);
|
|
||||||
let size = Size::new(width, height);
|
let size = Size::new(width, height);
|
||||||
let rect = Geometry::Rect(size).filled(fill);
|
let rect = Geometry::Rect(size).filled(fill);
|
||||||
fills.push((pos, FrameItem::Shape(rect, self.span)));
|
fills.push((pos, FrameItem::Shape(rect, self.span)));
|
||||||
@ -1236,10 +1231,9 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut output = Frame::soft(Size::new(self.width, height));
|
let mut output = Frame::soft(Size::new(self.width, height));
|
||||||
let mut pos = Point::zero();
|
let mut offset = Point::zero();
|
||||||
|
|
||||||
// Reverse the column order when using RTL.
|
for (x, &rcol) in self.rcols.iter().enumerate() {
|
||||||
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
|
||||||
if let Some(cell) = self.grid.cell(x, y) {
|
if let Some(cell) = self.grid.cell(x, y) {
|
||||||
// Rowspans have a separate layout step
|
// Rowspans have a separate layout step
|
||||||
if cell.rowspan.get() == 1 {
|
if cell.rowspan.get() == 1 {
|
||||||
@ -1257,25 +1251,17 @@ impl<'a> GridLayouter<'a> {
|
|||||||
let frame =
|
let frame =
|
||||||
layout_cell(cell, engine, disambiguator, self.styles, pod)?
|
layout_cell(cell, engine, disambiguator, self.styles, pod)?
|
||||||
.into_frame();
|
.into_frame();
|
||||||
let mut pos = pos;
|
let mut pos = offset;
|
||||||
if self.is_rtl {
|
if self.is_rtl {
|
||||||
// In the grid, cell colspans expand to the right,
|
// In RTL cells expand to the left, thus the position
|
||||||
// so we're at the leftmost (lowest 'x') column
|
// must additionally be offset by the cell's width.
|
||||||
// spanned by the cell. However, in RTL, cells
|
pos.x = self.width - (pos.x + width);
|
||||||
// expand to the left. Therefore, without the
|
|
||||||
// offset below, the cell's contents would be laid out
|
|
||||||
// starting at its rightmost visual position and extend
|
|
||||||
// over to unrelated cells to its right in RTL.
|
|
||||||
// We avoid this by ensuring the rendered cell starts at
|
|
||||||
// the very left of the cell, even with colspan > 1.
|
|
||||||
let offset = -width + rcol;
|
|
||||||
pos.x += offset;
|
|
||||||
}
|
}
|
||||||
output.push_frame(pos, frame);
|
output.push_frame(pos, frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pos.x += rcol;
|
offset.x += rcol;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
@ -1302,8 +1288,8 @@ impl<'a> GridLayouter<'a> {
|
|||||||
pod.backlog = &heights[1..];
|
pod.backlog = &heights[1..];
|
||||||
|
|
||||||
// Layout the row.
|
// Layout the row.
|
||||||
let mut pos = Point::zero();
|
let mut offset = Point::zero();
|
||||||
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
|
for (x, &rcol) in self.rcols.iter().enumerate() {
|
||||||
if let Some(cell) = self.grid.cell(x, y) {
|
if let Some(cell) = self.grid.cell(x, y) {
|
||||||
// Rowspans have a separate layout step
|
// Rowspans have a separate layout step
|
||||||
if cell.rowspan.get() == 1 {
|
if cell.rowspan.get() == 1 {
|
||||||
@ -1314,17 +1300,19 @@ impl<'a> GridLayouter<'a> {
|
|||||||
let fragment =
|
let fragment =
|
||||||
layout_cell(cell, engine, disambiguator, self.styles, pod)?;
|
layout_cell(cell, engine, disambiguator, self.styles, pod)?;
|
||||||
for (output, frame) in outputs.iter_mut().zip(fragment) {
|
for (output, frame) in outputs.iter_mut().zip(fragment) {
|
||||||
let mut pos = pos;
|
let mut pos = offset;
|
||||||
if self.is_rtl {
|
if self.is_rtl {
|
||||||
let offset = -width + rcol;
|
// In RTL cells expand to the left, thus the
|
||||||
pos.x += offset;
|
// position must additionally be offset by the
|
||||||
|
// cell's width.
|
||||||
|
pos.x = self.width - (offset.x + width);
|
||||||
}
|
}
|
||||||
output.push_frame(pos, frame);
|
output.push_frame(pos, frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pos.x += rcol;
|
offset.x += rcol;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Fragment::frames(outputs))
|
Ok(Fragment::frames(outputs))
|
||||||
@ -1377,7 +1365,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
.footer
|
.footer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(Repeatable::as_repeated)
|
.and_then(Repeatable::as_repeated)
|
||||||
.map_or(true, |footer| footer.start != header.end)
|
.is_none_or(|footer| footer.start != header.end)
|
||||||
&& self.lrows.last().is_some_and(|row| row.index() < header.end)
|
&& self.lrows.last().is_some_and(|row| row.index() < header.end)
|
||||||
&& !in_last_with_offset(
|
&& !in_last_with_offset(
|
||||||
self.regions,
|
self.regions,
|
||||||
@ -1446,7 +1434,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
.iter_mut()
|
.iter_mut()
|
||||||
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
|
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
|
||||||
.filter(|rowspan| {
|
.filter(|rowspan| {
|
||||||
rowspan.max_resolved_row.map_or(true, |max_row| y > max_row)
|
rowspan.max_resolved_row.is_none_or(|max_row| y > max_row)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
// If the first region wasn't defined yet, it will have the
|
// If the first region wasn't defined yet, it will have the
|
||||||
@ -1469,7 +1457,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// last height is the one for the current region.
|
// last height is the one for the current region.
|
||||||
rowspan
|
rowspan
|
||||||
.heights
|
.heights
|
||||||
.extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights));
|
.extend(std::iter::repeat_n(Abs::zero(), amount_missing_heights));
|
||||||
|
|
||||||
// Ensure that, in this region, the rowspan will span at least
|
// Ensure that, in this region, the rowspan will span at least
|
||||||
// this row.
|
// this row.
|
||||||
@ -1494,7 +1482,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
// laid out at the first frame of the row).
|
// laid out at the first frame of the row).
|
||||||
// Any rowspans ending before this row are laid out even
|
// Any rowspans ending before this row are laid out even
|
||||||
// on this row's first frame.
|
// on this row's first frame.
|
||||||
if laid_out_footer_start.map_or(true, |footer_start| {
|
if laid_out_footer_start.is_none_or(|footer_start| {
|
||||||
// If this is a footer row, then only lay out this rowspan
|
// If this is a footer row, then only lay out this rowspan
|
||||||
// if the rowspan is contained within the footer.
|
// if the rowspan is contained within the footer.
|
||||||
y < footer_start || rowspan.y >= footer_start
|
y < footer_start || rowspan.y >= footer_start
|
||||||
@ -1580,5 +1568,5 @@ pub(super) fn points(
|
|||||||
/// our case, headers).
|
/// our case, headers).
|
||||||
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
|
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
|
||||||
regions.backlog.is_empty()
|
regions.backlog.is_empty()
|
||||||
&& regions.last.map_or(true, |height| regions.size.y + offset == height)
|
&& regions.last.is_none_or(|height| regions.size.y + offset == height)
|
||||||
}
|
}
|
||||||
|
@ -463,7 +463,7 @@ pub fn hline_stroke_at_column(
|
|||||||
// region, we have the last index, and (as a failsafe) we don't have the
|
// region, we have the last index, and (as a failsafe) we don't have the
|
||||||
// last row of cells above us.
|
// last row of cells above us.
|
||||||
let use_bottom_border_stroke = !in_last_region
|
let use_bottom_border_stroke = !in_last_region
|
||||||
&& local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len())
|
&& local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len())
|
||||||
&& y == grid.rows.len();
|
&& y == grid.rows.len();
|
||||||
let bottom_y =
|
let bottom_y =
|
||||||
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };
|
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };
|
||||||
|
@ -3,7 +3,6 @@ use typst_library::engine::Engine;
|
|||||||
use typst_library::foundations::Resolve;
|
use typst_library::foundations::Resolve;
|
||||||
use typst_library::layout::grid::resolve::Repeatable;
|
use typst_library::layout::grid::resolve::Repeatable;
|
||||||
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
||||||
use typst_utils::MaybeReverseIter;
|
|
||||||
|
|
||||||
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
|
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
|
||||||
use super::{layout_cell, Cell, GridLayouter};
|
use super::{layout_cell, Cell, GridLayouter};
|
||||||
@ -23,6 +22,10 @@ pub struct Rowspan {
|
|||||||
/// specified for the parent cell's `breakable` field.
|
/// specified for the parent cell's `breakable` field.
|
||||||
pub is_effectively_unbreakable: bool,
|
pub is_effectively_unbreakable: bool,
|
||||||
/// The horizontal offset of this rowspan in all regions.
|
/// The horizontal offset of this rowspan in all regions.
|
||||||
|
///
|
||||||
|
/// This is the offset from the text direction start, meaning that, on RTL
|
||||||
|
/// grids, this is the offset from the right of the grid, whereas, on LTR
|
||||||
|
/// grids, it is the offset from the left.
|
||||||
pub dx: Abs,
|
pub dx: Abs,
|
||||||
/// The vertical offset of this rowspan in the first region.
|
/// The vertical offset of this rowspan in the first region.
|
||||||
pub dy: Abs,
|
pub dy: Abs,
|
||||||
@ -118,10 +121,11 @@ impl GridLayouter<'_> {
|
|||||||
// Nothing to layout.
|
// Nothing to layout.
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let first_column = self.rcols[x];
|
|
||||||
let cell = self.grid.cell(x, y).unwrap();
|
let cell = self.grid.cell(x, y).unwrap();
|
||||||
let width = self.cell_spanned_width(cell, x);
|
let width = self.cell_spanned_width(cell, x);
|
||||||
let dx = if self.is_rtl { dx - width + first_column } else { dx };
|
// In RTL cells expand to the left, thus the position
|
||||||
|
// must additionally be offset by the cell's width.
|
||||||
|
let dx = if self.is_rtl { self.width - (dx + width) } else { dx };
|
||||||
|
|
||||||
// Prepare regions.
|
// Prepare regions.
|
||||||
let size = Size::new(width, *first_height);
|
let size = Size::new(width, *first_height);
|
||||||
@ -185,10 +189,8 @@ impl GridLayouter<'_> {
|
|||||||
/// Checks if a row contains the beginning of one or more rowspan cells.
|
/// Checks if a row contains the beginning of one or more rowspan cells.
|
||||||
/// If so, adds them to the rowspans vector.
|
/// If so, adds them to the rowspans vector.
|
||||||
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
|
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
|
||||||
// We will compute the horizontal offset of each rowspan in advance.
|
let offsets = points(self.rcols.iter().copied());
|
||||||
// For that reason, we must reverse the column order when using RTL.
|
for (x, dx) in (0..self.rcols.len()).zip(offsets) {
|
||||||
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
|
|
||||||
for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) {
|
|
||||||
let Some(cell) = self.grid.cell(x, y) else {
|
let Some(cell) = self.grid.cell(x, y) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@ -588,7 +590,7 @@ impl GridLayouter<'_> {
|
|||||||
measurement_data: &CellMeasurementData<'_>,
|
measurement_data: &CellMeasurementData<'_>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if sizes.len() <= 1
|
if sizes.len() <= 1
|
||||||
&& sizes.first().map_or(true, |&first_frame_size| {
|
&& sizes.first().is_none_or(|&first_frame_size| {
|
||||||
first_frame_size <= measurement_data.height_in_this_region
|
first_frame_size <= measurement_data.height_in_this_region
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
@ -95,6 +95,8 @@ pub fn layout_image(
|
|||||||
} else {
|
} else {
|
||||||
// If neither is forced, take the natural image size at the image's
|
// If neither is forced, take the natural image size at the image's
|
||||||
// DPI bounded by the available space.
|
// DPI bounded by the available space.
|
||||||
|
//
|
||||||
|
// Division by DPI is fine since it's guaranteed to be positive.
|
||||||
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
||||||
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
||||||
Size::new(
|
Size::new(
|
||||||
|
@ -2,10 +2,8 @@ use typst_library::diag::warning;
|
|||||||
use typst_library::foundations::{Packed, Resolve};
|
use typst_library::foundations::{Packed, Resolve};
|
||||||
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
|
||||||
Spacing,
|
|
||||||
};
|
};
|
||||||
use typst_library::model::{EnumElem, ListElem, TermsElem};
|
|
||||||
use typst_library::routines::Pair;
|
use typst_library::routines::Pair;
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
||||||
@ -123,41 +121,21 @@ pub fn collect<'a>(
|
|||||||
children: &[Pair<'a>],
|
children: &[Pair<'a>],
|
||||||
engine: &mut Engine<'_>,
|
engine: &mut Engine<'_>,
|
||||||
locator: &mut SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: StyleChain<'a>,
|
config: &Config,
|
||||||
region: Size,
|
region: Size,
|
||||||
situation: Option<ParSituation>,
|
|
||||||
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
|
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
|
||||||
let mut collector = Collector::new(2 + children.len());
|
let mut collector = Collector::new(2 + children.len());
|
||||||
let mut quoter = SmartQuoter::new();
|
let mut quoter = SmartQuoter::new();
|
||||||
|
|
||||||
let outer_dir = TextElem::dir_in(styles);
|
if !config.first_line_indent.is_zero() {
|
||||||
|
collector.push_item(Item::Absolute(config.first_line_indent, false));
|
||||||
if let Some(situation) = situation {
|
|
||||||
let first_line_indent = ParElem::first_line_indent_in(styles);
|
|
||||||
if !first_line_indent.amount.is_zero()
|
|
||||||
&& match situation {
|
|
||||||
// First-line indent for the first paragraph after a list bullet
|
|
||||||
// just looks bad.
|
|
||||||
ParSituation::First => first_line_indent.all && !in_list(styles),
|
|
||||||
ParSituation::Consecutive => true,
|
|
||||||
ParSituation::Other => first_line_indent.all,
|
|
||||||
}
|
|
||||||
&& AlignElem::alignment_in(styles).resolve(styles).x
|
|
||||||
== outer_dir.start().into()
|
|
||||||
{
|
|
||||||
collector.push_item(Item::Absolute(
|
|
||||||
first_line_indent.amount.resolve(styles),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
collector.spans.push(1, Span::detached());
|
collector.spans.push(1, Span::detached());
|
||||||
}
|
}
|
||||||
|
|
||||||
let hang = ParElem::hanging_indent_in(styles);
|
if !config.hanging_indent.is_zero() {
|
||||||
if !hang.is_zero() {
|
collector.push_item(Item::Absolute(-config.hanging_indent, false));
|
||||||
collector.push_item(Item::Absolute(-hang, false));
|
|
||||||
collector.spans.push(1, Span::detached());
|
collector.spans.push(1, Span::detached());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for &(child, styles) in children {
|
for &(child, styles) in children {
|
||||||
let prev_len = collector.full.len();
|
let prev_len = collector.full.len();
|
||||||
@ -167,7 +145,7 @@ pub fn collect<'a>(
|
|||||||
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
} else if let Some(elem) = child.to_packed::<TextElem>() {
|
||||||
collector.build_text(styles, |full| {
|
collector.build_text(styles, |full| {
|
||||||
let dir = TextElem::dir_in(styles);
|
let dir = TextElem::dir_in(styles);
|
||||||
if dir != outer_dir {
|
if dir != config.dir {
|
||||||
// Insert "Explicit Directional Embedding".
|
// Insert "Explicit Directional Embedding".
|
||||||
match dir {
|
match dir {
|
||||||
Dir::LTR => full.push_str(LTR_EMBEDDING),
|
Dir::LTR => full.push_str(LTR_EMBEDDING),
|
||||||
@ -182,7 +160,7 @@ pub fn collect<'a>(
|
|||||||
full.push_str(&elem.text);
|
full.push_str(&elem.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir != outer_dir {
|
if dir != config.dir {
|
||||||
// Insert "Pop Directional Formatting".
|
// Insert "Pop Directional Formatting".
|
||||||
full.push_str(POP_EMBEDDING);
|
full.push_str(POP_EMBEDDING);
|
||||||
}
|
}
|
||||||
@ -265,16 +243,6 @@ pub fn collect<'a>(
|
|||||||
Ok((collector.full, collector.segments, collector.spans))
|
Ok((collector.full, collector.segments, collector.spans))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we have a list ancestor.
|
|
||||||
///
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collects segments.
|
/// Collects segments.
|
||||||
struct Collector<'a> {
|
struct Collector<'a> {
|
||||||
full: String,
|
full: String,
|
||||||
|
@ -9,7 +9,6 @@ pub fn finalize(
|
|||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
p: &Preparation,
|
p: &Preparation,
|
||||||
lines: &[Line],
|
lines: &[Line],
|
||||||
styles: StyleChain,
|
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
locator: &mut SplitLocator<'_>,
|
locator: &mut SplitLocator<'_>,
|
||||||
@ -19,9 +18,10 @@ pub fn finalize(
|
|||||||
let width = if !region.x.is_finite()
|
let width = if !region.x.is_finite()
|
||||||
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
|
|| (!expand && lines.iter().all(|line| line.fr().is_zero()))
|
||||||
{
|
{
|
||||||
region
|
region.x.min(
|
||||||
.x
|
p.config.hanging_indent
|
||||||
.min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default())
|
+ lines.iter().map(|line| line.width).max().unwrap_or_default(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
region.x
|
region.x
|
||||||
};
|
};
|
||||||
@ -29,7 +29,7 @@ pub fn finalize(
|
|||||||
// Stack the lines into one frame per region.
|
// Stack the lines into one frame per region.
|
||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|line| commit(engine, p, line, width, region.y, locator, styles))
|
.map(|line| commit(engine, p, line, width, region.y, locator))
|
||||||
.collect::<SourceResult<_>>()
|
.collect::<SourceResult<_>>()
|
||||||
.map(Fragment::frames)
|
.map(Fragment::frames)
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use typst_library::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use typst_library::foundations::NativeElement;
|
|
||||||
use typst_library::introspection::{SplitLocator, Tag};
|
use typst_library::introspection::{SplitLocator, Tag};
|
||||||
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
||||||
use typst_library::model::{ParLine, ParLineMarker};
|
use typst_library::model::ParLineMarker;
|
||||||
use typst_library::text::{Lang, TextElem};
|
use typst_library::text::{Lang, TextElem};
|
||||||
use typst_utils::Numeric;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
@ -135,7 +134,7 @@ pub fn line<'a>(
|
|||||||
|
|
||||||
// Whether the line is justified.
|
// Whether the line is justified.
|
||||||
let justify = full.ends_with(LINE_SEPARATOR)
|
let justify = full.ends_with(LINE_SEPARATOR)
|
||||||
|| (p.justify && breakpoint != Breakpoint::Mandatory);
|
|| (p.config.justify && breakpoint != Breakpoint::Mandatory);
|
||||||
|
|
||||||
// Process dashes.
|
// Process dashes.
|
||||||
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
|
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
|
||||||
@ -155,16 +154,16 @@ pub fn line<'a>(
|
|||||||
let mut items = collect_items(engine, p, range, trim);
|
let mut items = collect_items(engine, p, range, trim);
|
||||||
|
|
||||||
// Add a hyphen at the line start, if a previous dash should be repeated.
|
// Add a hyphen at the line start, if a previous dash should be repeated.
|
||||||
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
|
if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) {
|
||||||
if let Some(shaped) = items.first_text_mut() {
|
if let Some(shaped) = items.first_text_mut() {
|
||||||
shaped.prepend_hyphen(engine, p.fallback);
|
shaped.prepend_hyphen(engine, p.config.fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a hyphen at the line end, if we ended on a soft hyphen.
|
// Add a hyphen at the line end, if we ended on a soft hyphen.
|
||||||
if dash == Some(Dash::Soft) {
|
if dash == Some(Dash::Soft) {
|
||||||
if let Some(shaped) = items.last_text_mut() {
|
if let Some(shaped) = items.last_text_mut() {
|
||||||
shaped.push_hyphen(engine, p.fallback);
|
shaped.push_hyphen(engine, p.config.fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,13 +233,13 @@ where
|
|||||||
{
|
{
|
||||||
// If there is nothing bidirectional going on, skip reordering.
|
// If there is nothing bidirectional going on, skip reordering.
|
||||||
let Some(bidi) = &p.bidi else {
|
let Some(bidi) = &p.bidi else {
|
||||||
f(range, p.dir == Dir::RTL);
|
f(range, p.config.dir == Dir::RTL);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The bidi crate panics for empty lines.
|
// The bidi crate panics for empty lines.
|
||||||
if range.is_empty() {
|
if range.is_empty() {
|
||||||
f(range, p.dir == Dir::RTL);
|
f(range, p.config.dir == Dir::RTL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,13 +307,13 @@ fn collect_range<'a>(
|
|||||||
/// punctuation marks at line start or line end.
|
/// punctuation marks at line start or line end.
|
||||||
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
|
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
|
||||||
if text.starts_with(BEGIN_PUNCT_PAT)
|
if text.starts_with(BEGIN_PUNCT_PAT)
|
||||||
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script))
|
|| (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
|
||||||
{
|
{
|
||||||
adjust_cj_at_line_start(p, items);
|
adjust_cj_at_line_start(p, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.ends_with(END_PUNCT_PAT)
|
if text.ends_with(END_PUNCT_PAT)
|
||||||
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script))
|
|| (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
|
||||||
{
|
{
|
||||||
adjust_cj_at_line_end(p, items);
|
adjust_cj_at_line_end(p, items);
|
||||||
}
|
}
|
||||||
@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
|
|||||||
let shrink = glyph.shrinkability().0;
|
let shrink = glyph.shrinkability().0;
|
||||||
glyph.shrink_left(shrink);
|
glyph.shrink_left(shrink);
|
||||||
shaped.width -= shrink.at(shaped.size);
|
shaped.width -= shrink.at(shaped.size);
|
||||||
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() {
|
} else if p.config.cjk_latin_spacing
|
||||||
|
&& glyph.is_cj_script()
|
||||||
|
&& glyph.x_offset > Em::zero()
|
||||||
|
{
|
||||||
// If the first glyph is a CJK character adjusted by
|
// If the first glyph is a CJK character adjusted by
|
||||||
// [`add_cjk_latin_spacing`], restore the original width.
|
// [`add_cjk_latin_spacing`], restore the original width.
|
||||||
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
|
let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
|
||||||
@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
|
|||||||
let punct = shaped.glyphs.to_mut().last_mut().unwrap();
|
let punct = shaped.glyphs.to_mut().last_mut().unwrap();
|
||||||
punct.shrink_right(shrink);
|
punct.shrink_right(shrink);
|
||||||
shaped.width -= shrink.at(shaped.size);
|
shaped.width -= shrink.at(shaped.size);
|
||||||
} else if p.cjk_latin_spacing
|
} else if p.config.cjk_latin_spacing
|
||||||
&& glyph.is_cj_script()
|
&& glyph.is_cj_script()
|
||||||
&& (glyph.x_advance - glyph.x_offset) > Em::one()
|
&& (glyph.x_advance - glyph.x_offset) > Em::one()
|
||||||
{
|
{
|
||||||
@ -404,7 +406,7 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
|
|||||||
//
|
//
|
||||||
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
|
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
|
||||||
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
|
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
|
||||||
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()),
|
Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()),
|
||||||
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@ -424,16 +426,15 @@ pub fn commit(
|
|||||||
width: Abs,
|
width: Abs,
|
||||||
full: Abs,
|
full: Abs,
|
||||||
locator: &mut SplitLocator<'_>,
|
locator: &mut SplitLocator<'_>,
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
let mut remaining = width - line.width - p.hang;
|
let mut remaining = width - line.width - p.config.hanging_indent;
|
||||||
let mut offset = Abs::zero();
|
let mut offset = Abs::zero();
|
||||||
|
|
||||||
// We always build the line from left to right. In an LTR paragraph, we must
|
// We always build the line from left to right. In an LTR paragraph, we must
|
||||||
// thus add the hanging indent to the offset. In an RTL paragraph, the
|
// thus add the hanging indent to the offset. In an RTL paragraph, the
|
||||||
// hanging indent arises naturally due to the line width.
|
// hanging indent arises naturally due to the line width.
|
||||||
if p.dir == Dir::LTR {
|
if p.config.dir == Dir::LTR {
|
||||||
offset += p.hang;
|
offset += p.config.hanging_indent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle hanging punctuation to the left.
|
// Handle hanging punctuation to the left.
|
||||||
@ -554,11 +555,13 @@ pub fn commit(
|
|||||||
let mut output = Frame::soft(size);
|
let mut output = Frame::soft(size);
|
||||||
output.set_baseline(top);
|
output.set_baseline(top);
|
||||||
|
|
||||||
add_par_line_marker(&mut output, styles, engine, locator, top);
|
if let Some(marker) = &p.config.numbering_marker {
|
||||||
|
add_par_line_marker(&mut output, marker, engine, locator, top);
|
||||||
|
}
|
||||||
|
|
||||||
// Construct the line's frame.
|
// Construct the line's frame.
|
||||||
for (offset, frame) in frames {
|
for (offset, frame) in frames {
|
||||||
let x = offset + p.align.position(remaining);
|
let x = offset + p.config.align.position(remaining);
|
||||||
let y = top - frame.baseline();
|
let y = top - frame.baseline();
|
||||||
output.push_frame(Point::new(x, y), frame);
|
output.push_frame(Point::new(x, y), frame);
|
||||||
}
|
}
|
||||||
@ -575,26 +578,18 @@ pub fn commit(
|
|||||||
/// number in the margin, is aligned to the line's baseline.
|
/// number in the margin, is aligned to the line's baseline.
|
||||||
fn add_par_line_marker(
|
fn add_par_line_marker(
|
||||||
output: &mut Frame,
|
output: &mut Frame,
|
||||||
styles: StyleChain,
|
marker: &Packed<ParLineMarker>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
locator: &mut SplitLocator,
|
locator: &mut SplitLocator,
|
||||||
top: Abs,
|
top: Abs,
|
||||||
) {
|
) {
|
||||||
let Some(numbering) = ParLine::numbering_in(styles) else { return };
|
|
||||||
let margin = ParLine::number_margin_in(styles);
|
|
||||||
let align = ParLine::number_align_in(styles);
|
|
||||||
|
|
||||||
// Delay resolving the number clearance until line numbers are laid out to
|
|
||||||
// avoid inconsistent spacing depending on varying font size.
|
|
||||||
let clearance = ParLine::number_clearance_in(styles);
|
|
||||||
|
|
||||||
// Elements in tags must have a location for introspection to work. We do
|
// Elements in tags must have a location for introspection to work. We do
|
||||||
// the work here instead of going through all of the realization process
|
// the work here instead of going through all of the realization process
|
||||||
// just for this, given we don't need to actually place the marker as we
|
// just for this, given we don't need to actually place the marker as we
|
||||||
// manually search for it in the frame later (when building a root flow,
|
// manually search for it in the frame later (when building a root flow,
|
||||||
// where line numbers can be displayed), so we just need it to be in a tag
|
// where line numbers can be displayed), so we just need it to be in a tag
|
||||||
// and to be valid (to have a location).
|
// and to be valid (to have a location).
|
||||||
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
|
let mut marker = marker.clone();
|
||||||
let key = typst_utils::hash128(&marker);
|
let key = typst_utils::hash128(&marker);
|
||||||
let loc = locator.next_location(engine.introspector, key);
|
let loc = locator.next_location(engine.introspector, key);
|
||||||
marker.set_location(loc);
|
marker.set_location(loc);
|
||||||
@ -606,7 +601,7 @@ fn add_par_line_marker(
|
|||||||
// line's general baseline. However, the line number will still need to
|
// line's general baseline. However, the line number will still need to
|
||||||
// manually adjust its own 'y' position based on its own baseline.
|
// manually adjust its own 'y' position based on its own baseline.
|
||||||
let pos = Point::with_y(top);
|
let pos = Point::with_y(top);
|
||||||
output.push(pos, FrameItem::Tag(Tag::Start(marker)));
|
output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
|
||||||
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
|
output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,15 +110,7 @@ pub fn linebreak<'a>(
|
|||||||
p: &'a Preparation<'a>,
|
p: &'a Preparation<'a>,
|
||||||
width: Abs,
|
width: Abs,
|
||||||
) -> Vec<Line<'a>> {
|
) -> Vec<Line<'a>> {
|
||||||
let linebreaks = p.linebreaks.unwrap_or_else(|| {
|
match p.config.linebreaks {
|
||||||
if p.justify {
|
|
||||||
Linebreaks::Optimized
|
|
||||||
} else {
|
|
||||||
Linebreaks::Simple
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match linebreaks {
|
|
||||||
Linebreaks::Simple => linebreak_simple(engine, p, width),
|
Linebreaks::Simple => linebreak_simple(engine, p, width),
|
||||||
Linebreaks::Optimized => linebreak_optimized(engine, p, width),
|
Linebreaks::Optimized => linebreak_optimized(engine, p, width),
|
||||||
}
|
}
|
||||||
@ -298,7 +290,7 @@ fn linebreak_optimized_bounded<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this attempt is better than what we had before, take it!
|
// If this attempt is better than what we had before, take it!
|
||||||
if best.as_ref().map_or(true, |best| best.total >= total) {
|
if best.as_ref().is_none_or(|best| best.total >= total) {
|
||||||
best = Some(Entry { pred: pred_index, total, line: attempt, end });
|
best = Some(Entry { pred: pred_index, total, line: attempt, end });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -384,7 +376,7 @@ fn linebreak_optimized_approximate(
|
|||||||
|
|
||||||
// Whether the line is justified. This is not 100% accurate w.r.t
|
// Whether the line is justified. This is not 100% accurate w.r.t
|
||||||
// to line()'s behaviour, but good enough.
|
// to line()'s behaviour, but good enough.
|
||||||
let justify = p.justify && breakpoint != Breakpoint::Mandatory;
|
let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
|
||||||
|
|
||||||
// We don't really know whether the line naturally ends with a dash
|
// We don't really know whether the line naturally ends with a dash
|
||||||
// here, so we can miss that case, but it's ok, since all of this
|
// here, so we can miss that case, but it's ok, since all of this
|
||||||
@ -431,7 +423,7 @@ fn linebreak_optimized_approximate(
|
|||||||
let total = pred.total + line_cost;
|
let total = pred.total + line_cost;
|
||||||
|
|
||||||
// If this attempt is better than what we had before, take it!
|
// If this attempt is better than what we had before, take it!
|
||||||
if best.as_ref().map_or(true, |best| best.total >= total) {
|
if best.as_ref().is_none_or(|best| best.total >= total) {
|
||||||
best = Some(Entry {
|
best = Some(Entry {
|
||||||
pred: pred_index,
|
pred: pred_index,
|
||||||
total,
|
total,
|
||||||
@ -573,7 +565,7 @@ fn raw_ratio(
|
|||||||
// calculate the extra amount. Also, don't divide by zero.
|
// calculate the extra amount. Also, don't divide by zero.
|
||||||
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
|
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
|
||||||
// Normalize the amount by half the em size.
|
// Normalize the amount by half the em size.
|
||||||
ratio = 1.0 + extra_stretch / (p.size / 2.0);
|
ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The min value must be < MIN_RATIO, but how much smaller doesn't matter
|
// The min value must be < MIN_RATIO, but how much smaller doesn't matter
|
||||||
@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hyphenate = p.hyphenate != Some(false);
|
let hyphenate = p.config.hyphenate != Some(false);
|
||||||
let lb = LINEBREAK_DATA.as_borrowed();
|
let lb = LINEBREAK_DATA.as_borrowed();
|
||||||
let segmenter = match p.lang {
|
let segmenter = match p.config.lang {
|
||||||
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
|
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
|
||||||
_ => &SEGMENTER,
|
_ => &SEGMENTER,
|
||||||
};
|
};
|
||||||
@ -698,13 +690,34 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
|
|||||||
let breakpoint = if point == text.len() {
|
let breakpoint = if point == text.len() {
|
||||||
Breakpoint::Mandatory
|
Breakpoint::Mandatory
|
||||||
} else {
|
} else {
|
||||||
|
const OBJ_REPLACE: char = '\u{FFFC}';
|
||||||
match lb.get(c) {
|
match lb.get(c) {
|
||||||
// Fix for: https://github.com/unicode-org/icu4x/issues/4146
|
|
||||||
LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue,
|
|
||||||
LineBreak::MandatoryBreak
|
LineBreak::MandatoryBreak
|
||||||
| LineBreak::CarriageReturn
|
| LineBreak::CarriageReturn
|
||||||
| LineBreak::LineFeed
|
| LineBreak::LineFeed
|
||||||
| LineBreak::NextLine => Breakpoint::Mandatory,
|
| LineBreak::NextLine => Breakpoint::Mandatory,
|
||||||
|
|
||||||
|
// https://github.com/typst/typst/issues/5489
|
||||||
|
//
|
||||||
|
// OBJECT-REPLACEMENT-CHARACTERs provide Contingent Break
|
||||||
|
// opportunities before and after by default. This behaviour
|
||||||
|
// is however tailorable, see:
|
||||||
|
// https://www.unicode.org/reports/tr14/#CB
|
||||||
|
// https://www.unicode.org/reports/tr14/#TailorableBreakingRules
|
||||||
|
// https://www.unicode.org/reports/tr14/#LB20
|
||||||
|
//
|
||||||
|
// Don't provide a line breaking opportunity between a LTR-
|
||||||
|
// ISOLATE (or any other Combining Mark) and an OBJECT-
|
||||||
|
// REPLACEMENT-CHARACTER representing an inline item, if the
|
||||||
|
// LTR-ISOLATE could end up as the only character on the
|
||||||
|
// previous line.
|
||||||
|
LineBreak::CombiningMark
|
||||||
|
if text[point..].starts_with(OBJ_REPLACE)
|
||||||
|
&& last + c.len_utf8() == point =>
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
_ => Breakpoint::Normal,
|
_ => Breakpoint::Normal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -830,18 +843,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
|
|||||||
|
|
||||||
/// Whether hyphenation is enabled at the given offset.
|
/// Whether hyphenation is enabled at the given offset.
|
||||||
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
|
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
|
||||||
p.hyphenate
|
p.config.hyphenate.unwrap_or_else(|| {
|
||||||
.or_else(|| {
|
|
||||||
let (_, item) = p.get(offset);
|
let (_, item) = p.get(offset);
|
||||||
let styles = item.text()?.styles;
|
match item.text() {
|
||||||
Some(TextElem::hyphenate_in(styles))
|
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The text language at the given offset.
|
/// The text language at the given offset.
|
||||||
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
|
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
|
||||||
let lang = p.lang.or_else(|| {
|
let lang = p.config.lang.or_else(|| {
|
||||||
let (_, item) = p.get(offset);
|
let (_, item) = p.get(offset);
|
||||||
let styles = item.text()?.styles;
|
let styles = item.text()?.styles;
|
||||||
Some(TextElem::lang_in(styles))
|
Some(TextElem::lang_in(styles))
|
||||||
@ -865,13 +878,13 @@ impl CostMetrics {
|
|||||||
fn compute(p: &Preparation) -> Self {
|
fn compute(p: &Preparation) -> Self {
|
||||||
Self {
|
Self {
|
||||||
// When justifying, we may stretch spaces below their natural width.
|
// When justifying, we may stretch spaces below their natural width.
|
||||||
min_ratio: if p.justify { MIN_RATIO } else { 0.0 },
|
min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
|
||||||
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 },
|
min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
|
||||||
// Approximate hyphen width for estimates.
|
// Approximate hyphen width for estimates.
|
||||||
approx_hyphen_width: Em::new(0.33).at(p.size),
|
approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
|
||||||
// Costs.
|
// Costs.
|
||||||
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
|
hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
|
||||||
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
|
runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +13,17 @@ pub use self::box_::layout_box;
|
|||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use typst_library::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Packed, StyleChain};
|
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||||
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
|
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
|
||||||
use typst_library::layout::{Fragment, Size};
|
use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
|
||||||
use typst_library::model::ParElem;
|
use typst_library::model::{
|
||||||
|
EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
|
||||||
|
TermsElem,
|
||||||
|
};
|
||||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
|
use typst_library::text::{Costs, Lang, TextElem};
|
||||||
use typst_library::World;
|
use typst_library::World;
|
||||||
|
use typst_utils::{Numeric, SliceExt};
|
||||||
|
|
||||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
use self::collect::{collect, Item, Segment, SpanMapper};
|
||||||
use self::deco::decorate;
|
use self::deco::decorate;
|
||||||
@ -98,7 +103,7 @@ fn layout_par_impl(
|
|||||||
styles,
|
styles,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
layout_inline(
|
layout_inline_impl(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&children,
|
&children,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
@ -106,33 +111,134 @@ fn layout_par_impl(
|
|||||||
region,
|
region,
|
||||||
expand,
|
expand,
|
||||||
Some(situation),
|
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),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays out realized content with inline layout.
|
/// Lays out realized content with inline layout.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn layout_inline<'a>(
|
pub fn layout_inline<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &[Pair<'a>],
|
children: &[Pair<'a>],
|
||||||
locator: &mut SplitLocator<'a>,
|
locator: &mut SplitLocator<'a>,
|
||||||
styles: StyleChain<'a>,
|
shared: StyleChain<'a>,
|
||||||
|
region: Size,
|
||||||
|
expand: bool,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
layout_inline_impl(
|
||||||
|
engine,
|
||||||
|
children,
|
||||||
|
locator,
|
||||||
|
shared,
|
||||||
|
region,
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal implementation of [`layout_inline`].
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn layout_inline_impl<'a>(
|
||||||
|
engine: &mut Engine,
|
||||||
|
children: &[Pair<'a>],
|
||||||
|
locator: &mut SplitLocator<'a>,
|
||||||
|
shared: StyleChain<'a>,
|
||||||
region: Size,
|
region: Size,
|
||||||
expand: bool,
|
expand: bool,
|
||||||
par: Option<ParSituation>,
|
par: Option<ParSituation>,
|
||||||
|
base: &ConfigBase,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
|
// Prepare configuration that is shared across the whole inline layout.
|
||||||
|
let config = configuration(base, children, shared, par);
|
||||||
|
|
||||||
// Collect all text into one string for BiDi analysis.
|
// Collect all text into one string for BiDi analysis.
|
||||||
let (text, segments, spans) =
|
let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
|
||||||
collect(children, engine, locator, styles, region, par)?;
|
|
||||||
|
|
||||||
// Perform BiDi analysis and performs some preparation steps before we
|
// Perform BiDi analysis and performs some preparation steps before we
|
||||||
// proceed to line breaking.
|
// proceed to line breaking.
|
||||||
let p = prepare(engine, children, &text, segments, spans, styles, par)?;
|
let p = prepare(engine, &config, &text, segments, spans)?;
|
||||||
|
|
||||||
// Break the text into lines.
|
// Break the text into lines.
|
||||||
let lines = linebreak(engine, &p, region.x - p.hang);
|
let lines = linebreak(engine, &p, region.x - config.hanging_indent);
|
||||||
|
|
||||||
// Turn the selected lines into frames.
|
// Turn the selected lines into frames.
|
||||||
finalize(engine, &p, &lines, styles, region, expand, locator)
|
finalize(engine, &p, &lines, region, expand, locator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the inline layout's configuration.
|
||||||
|
fn configuration(
|
||||||
|
base: &ConfigBase,
|
||||||
|
children: &[Pair],
|
||||||
|
shared: StyleChain,
|
||||||
|
situation: Option<ParSituation>,
|
||||||
|
) -> Config {
|
||||||
|
let justify = base.justify;
|
||||||
|
let font_size = TextElem::size_in(shared);
|
||||||
|
let dir = TextElem::dir_in(shared);
|
||||||
|
|
||||||
|
Config {
|
||||||
|
justify,
|
||||||
|
linebreaks: base.linebreaks.unwrap_or_else(|| {
|
||||||
|
if justify {
|
||||||
|
Linebreaks::Optimized
|
||||||
|
} else {
|
||||||
|
Linebreaks::Simple
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
first_line_indent: {
|
||||||
|
let FirstLineIndent { amount, all } = base.first_line_indent;
|
||||||
|
if !amount.is_zero()
|
||||||
|
&& match situation {
|
||||||
|
// First-line indent for the first paragraph after a list
|
||||||
|
// bullet just looks bad.
|
||||||
|
Some(ParSituation::First) => all && !in_list(shared),
|
||||||
|
Some(ParSituation::Consecutive) => true,
|
||||||
|
Some(ParSituation::Other) => all,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
|
||||||
|
{
|
||||||
|
amount.at(font_size)
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hanging_indent: if situation.is_some() {
|
||||||
|
base.hanging_indent
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
},
|
||||||
|
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
|
||||||
|
Packed::new(ParLineMarker::new(
|
||||||
|
numbering,
|
||||||
|
ParLine::number_align_in(shared),
|
||||||
|
ParLine::number_margin_in(shared),
|
||||||
|
// 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),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
align: AlignElem::alignment_in(shared).fix(dir).x,
|
||||||
|
font_size,
|
||||||
|
dir,
|
||||||
|
hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
|
||||||
|
.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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Distinguishes between a few different kinds of paragraphs.
|
/// Distinguishes between a few different kinds of paragraphs.
|
||||||
@ -148,3 +254,66 @@ pub enum ParSituation {
|
|||||||
/// Any other kind of paragraph.
|
/// Any other kind of paragraph.
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
|
||||||
|
struct ConfigBase {
|
||||||
|
justify: bool,
|
||||||
|
linebreaks: Smart<Linebreaks>,
|
||||||
|
first_line_indent: FirstLineIndent,
|
||||||
|
hanging_indent: Abs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared configuration for the whole inline layout.
|
||||||
|
struct Config {
|
||||||
|
/// Whether to justify text.
|
||||||
|
justify: bool,
|
||||||
|
/// How to determine line breaks.
|
||||||
|
linebreaks: Linebreaks,
|
||||||
|
/// The indent the first line of a paragraph should have.
|
||||||
|
first_line_indent: Abs,
|
||||||
|
/// The indent that all but the first line of a paragraph should have.
|
||||||
|
hanging_indent: Abs,
|
||||||
|
/// Configuration for line numbering.
|
||||||
|
numbering_marker: Option<Packed<ParLineMarker>>,
|
||||||
|
/// The resolved horizontal alignment.
|
||||||
|
align: FixedAlignment,
|
||||||
|
/// The text size.
|
||||||
|
font_size: Abs,
|
||||||
|
/// The dominant direction.
|
||||||
|
dir: Dir,
|
||||||
|
/// A uniform hyphenation setting (only `Some(_)` if it's the same for all
|
||||||
|
/// children, otherwise `None`).
|
||||||
|
hyphenate: Option<bool>,
|
||||||
|
/// The text language (only `Some(_)` if it's the same for all
|
||||||
|
/// children, otherwise `None`).
|
||||||
|
lang: Option<Lang>,
|
||||||
|
/// Whether font fallback is enabled.
|
||||||
|
fallback: bool,
|
||||||
|
/// Whether to add spacing between CJK and Latin characters.
|
||||||
|
cjk_latin_spacing: bool,
|
||||||
|
/// Costs for various layout decisions.
|
||||||
|
costs: Costs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a style property, but only if it is the same for all of the children.
|
||||||
|
fn shared_get<T: PartialEq>(
|
||||||
|
children: &[Pair],
|
||||||
|
styles: StyleChain<'_>,
|
||||||
|
getter: fn(StyleChain) -> T,
|
||||||
|
) -> Option<T> {
|
||||||
|
let value = getter(styles);
|
||||||
|
children
|
||||||
|
.group_by_key(|&(_, s)| s)
|
||||||
|
.all(|(s, _)| getter(s) == value)
|
||||||
|
.then_some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether we have a list ancestor.
|
||||||
|
///
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
use typst_library::foundations::{Resolve, Smart};
|
use typst_library::layout::{Dir, Em};
|
||||||
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
|
||||||
use typst_library::model::Linebreaks;
|
|
||||||
use typst_library::routines::Pair;
|
|
||||||
use typst_library::text::{Costs, Lang, TextElem};
|
|
||||||
use typst_utils::SliceExt;
|
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -17,6 +12,8 @@ use super::*;
|
|||||||
pub struct Preparation<'a> {
|
pub struct Preparation<'a> {
|
||||||
/// The full text.
|
/// The full text.
|
||||||
pub text: &'a str,
|
pub text: &'a str,
|
||||||
|
/// Configuration for inline layout.
|
||||||
|
pub config: &'a Config,
|
||||||
/// Bidirectional text embedding levels.
|
/// Bidirectional text embedding levels.
|
||||||
///
|
///
|
||||||
/// This is `None` if all text directions are uniform (all the base
|
/// This is `None` if all text directions are uniform (all the base
|
||||||
@ -28,28 +25,6 @@ pub struct Preparation<'a> {
|
|||||||
pub indices: Vec<usize>,
|
pub indices: Vec<usize>,
|
||||||
/// The span mapper.
|
/// The span mapper.
|
||||||
pub spans: SpanMapper,
|
pub spans: SpanMapper,
|
||||||
/// Whether to hyphenate if it's the same for all children.
|
|
||||||
pub hyphenate: Option<bool>,
|
|
||||||
/// Costs for various layout decisions.
|
|
||||||
pub costs: Costs,
|
|
||||||
/// The dominant direction.
|
|
||||||
pub dir: Dir,
|
|
||||||
/// The text language if it's the same for all children.
|
|
||||||
pub lang: Option<Lang>,
|
|
||||||
/// The resolved horizontal alignment.
|
|
||||||
pub align: FixedAlignment,
|
|
||||||
/// Whether to justify text.
|
|
||||||
pub justify: bool,
|
|
||||||
/// Hanging indent to apply.
|
|
||||||
pub hang: Abs,
|
|
||||||
/// Whether to add spacing between CJK and Latin characters.
|
|
||||||
pub cjk_latin_spacing: bool,
|
|
||||||
/// Whether font fallback is enabled.
|
|
||||||
pub fallback: bool,
|
|
||||||
/// How to determine line breaks.
|
|
||||||
pub linebreaks: Smart<Linebreaks>,
|
|
||||||
/// The text size.
|
|
||||||
pub size: Abs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Preparation<'a> {
|
impl<'a> Preparation<'a> {
|
||||||
@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
|
|||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
pub fn prepare<'a>(
|
pub fn prepare<'a>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &[Pair<'a>],
|
config: &'a Config,
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
segments: Vec<Segment<'a>>,
|
segments: Vec<Segment<'a>>,
|
||||||
spans: SpanMapper,
|
spans: SpanMapper,
|
||||||
styles: StyleChain<'a>,
|
|
||||||
situation: Option<ParSituation>,
|
|
||||||
) -> SourceResult<Preparation<'a>> {
|
) -> SourceResult<Preparation<'a>> {
|
||||||
let dir = TextElem::dir_in(styles);
|
let default_level = match config.dir {
|
||||||
let default_level = match dir {
|
|
||||||
Dir::RTL => BidiLevel::rtl(),
|
Dir::RTL => BidiLevel::rtl(),
|
||||||
_ => BidiLevel::ltr(),
|
_ => BidiLevel::ltr(),
|
||||||
};
|
};
|
||||||
@ -124,51 +96,20 @@ pub fn prepare<'a>(
|
|||||||
indices.extend(range.clone().map(|_| i));
|
indices.extend(range.clone().map(|_| i));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
|
if config.cjk_latin_spacing {
|
||||||
if cjk_latin_spacing {
|
|
||||||
add_cjk_latin_spacing(&mut items);
|
add_cjk_latin_spacing(&mut items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only apply hanging indent to real paragraphs.
|
|
||||||
let hang = if situation.is_some() {
|
|
||||||
ParElem::hanging_indent_in(styles)
|
|
||||||
} else {
|
|
||||||
Abs::zero()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Preparation {
|
Ok(Preparation {
|
||||||
|
config,
|
||||||
text,
|
text,
|
||||||
bidi: is_bidi.then_some(bidi),
|
bidi: is_bidi.then_some(bidi),
|
||||||
items,
|
items,
|
||||||
indices,
|
indices,
|
||||||
spans,
|
spans,
|
||||||
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
|
|
||||||
costs: TextElem::costs_in(styles),
|
|
||||||
dir,
|
|
||||||
lang: shared_get(children, styles, TextElem::lang_in),
|
|
||||||
align: AlignElem::alignment_in(styles).resolve(styles).x,
|
|
||||||
justify: ParElem::justify_in(styles),
|
|
||||||
hang,
|
|
||||||
cjk_latin_spacing,
|
|
||||||
fallback: TextElem::fallback_in(styles),
|
|
||||||
linebreaks: ParElem::linebreaks_in(styles),
|
|
||||||
size: TextElem::size_in(styles),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a style property, but only if it is the same for all of the children.
|
|
||||||
fn shared_get<T: PartialEq>(
|
|
||||||
children: &[Pair],
|
|
||||||
styles: StyleChain<'_>,
|
|
||||||
getter: fn(StyleChain) -> T,
|
|
||||||
) -> Option<T> {
|
|
||||||
let value = getter(styles);
|
|
||||||
children
|
|
||||||
.group_by_key(|&(_, s)| s)
|
|
||||||
.all(|(s, _)| getter(s) == value)
|
|
||||||
.then_some(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add some spacing between Han characters and western characters. See
|
/// Add some spacing between Han characters and western characters. See
|
||||||
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
|
||||||
/// in Horizontal Written Mode
|
/// in Horizontal Written Mode
|
||||||
|
@ -20,7 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
|||||||
use unicode_script::{Script, UnicodeScript};
|
use unicode_script::{Script, UnicodeScript};
|
||||||
|
|
||||||
use super::{decorate, Item, Range, SpanMapper};
|
use super::{decorate, Item, Range, SpanMapper};
|
||||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
use crate::modifiers::FrameModifyText;
|
||||||
|
|
||||||
/// The result of shaping text.
|
/// The result of shaping text.
|
||||||
///
|
///
|
||||||
@ -327,7 +327,7 @@ impl<'a> ShapedText<'a> {
|
|||||||
offset += width;
|
offset += width;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.modify(&FrameModifiers::get_in(self.styles));
|
frame.modify_text(self.styles);
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +465,7 @@ impl<'a> ShapedText<'a> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let mut chain = families(self.styles)
|
let mut chain = families(self.styles)
|
||||||
.filter(|family| family.covers().map_or(true, |c| c.is_match("-")))
|
.filter(|family| family.covers().is_none_or(|c| c.is_match("-")))
|
||||||
.map(|family| book.select(family.as_str(), self.variant))
|
.map(|family| book.select(family.as_str(), self.variant))
|
||||||
.chain(fallback_func.iter().map(|f| f()))
|
.chain(fallback_func.iter().map(|f| f()))
|
||||||
.flatten();
|
.flatten();
|
||||||
@ -570,7 +570,7 @@ impl<'a> ShapedText<'a> {
|
|||||||
// for the next line.
|
// for the next line.
|
||||||
let dec = if ltr { usize::checked_sub } else { usize::checked_add };
|
let dec = if ltr { usize::checked_sub } else { usize::checked_add };
|
||||||
while let Some(next) = dec(idx, 1) {
|
while let Some(next) = dec(idx, 1) {
|
||||||
if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) {
|
if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
idx = next;
|
idx = next;
|
||||||
@ -812,7 +812,7 @@ fn shape_segment<'a>(
|
|||||||
.nth(1)
|
.nth(1)
|
||||||
.map(|(i, _)| offset + i)
|
.map(|(i, _)| offset + i)
|
||||||
.unwrap_or(text.len());
|
.unwrap_or(text.len());
|
||||||
covers.map_or(true, |cov| cov.is_match(&text[offset..end]))
|
covers.is_none_or(|cov| cov.is_match(&text[offset..end]))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Collect the shaped glyphs, doing fallback and shaping parts again with
|
// Collect the shaped glyphs, doing fallback and shaping parts again with
|
||||||
@ -824,12 +824,42 @@ fn shape_segment<'a>(
|
|||||||
|
|
||||||
// Add the glyph to the shaped output.
|
// Add the glyph to the shaped output.
|
||||||
if info.glyph_id != 0 && is_covered(cluster) {
|
if info.glyph_id != 0 && is_covered(cluster) {
|
||||||
// Determine the text range of the glyph.
|
// Assume we have the following sequence of (glyph_id, cluster):
|
||||||
|
// [(120, 0), (80, 0), (3, 3), (755, 4), (69, 4), (424, 13),
|
||||||
|
// (63, 13), (193, 25), (80, 25), (3, 31)
|
||||||
|
//
|
||||||
|
// We then want the sequence of (glyph_id, text_range) to look as follows:
|
||||||
|
// [(120, 0..3), (80, 0..3), (3, 3..4), (755, 4..13), (69, 4..13),
|
||||||
|
// (424, 13..25), (63, 13..25), (193, 25..31), (80, 25..31), (3, 31..x)]
|
||||||
|
//
|
||||||
|
// Each glyph in the same cluster should be assigned the full text
|
||||||
|
// range. This is necessary because only this way krilla can
|
||||||
|
// properly assign `ActualText` attributes in complex shaping
|
||||||
|
// scenarios.
|
||||||
|
|
||||||
|
// The start of the glyph's text range.
|
||||||
let start = base + cluster;
|
let start = base + cluster;
|
||||||
let end = base
|
|
||||||
+ if ltr { i.checked_add(1) } else { i.checked_sub(1) }
|
// Determine the end of the glyph's text range.
|
||||||
.and_then(|last| infos.get(last))
|
let mut k = i;
|
||||||
.map_or(text.len(), |info| info.cluster as usize);
|
let step: isize = if ltr { 1 } else { -1 };
|
||||||
|
let end = loop {
|
||||||
|
// If we've reached the end of the glyphs, the `end` of the
|
||||||
|
// range should be the end of the full text.
|
||||||
|
let Some((next, next_info)) = k
|
||||||
|
.checked_add_signed(step)
|
||||||
|
.and_then(|n| infos.get(n).map(|info| (n, info)))
|
||||||
|
else {
|
||||||
|
break base + text.len();
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the cluster doesn't match anymore, we've reached the end.
|
||||||
|
if next_info.cluster != info.cluster {
|
||||||
|
break base + next_info.cluster as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
k = next;
|
||||||
|
};
|
||||||
|
|
||||||
let c = text[cluster..].chars().next().unwrap();
|
let c = text[cluster..].chars().next().unwrap();
|
||||||
let script = c.script();
|
let script = c.script();
|
||||||
|
@ -96,9 +96,13 @@ pub fn layout_enum(
|
|||||||
|
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
let mut locator = locator.split();
|
let mut locator = locator.split();
|
||||||
let mut number =
|
let mut number = elem.start(styles).unwrap_or_else(|| {
|
||||||
elem.start(styles)
|
if reversed {
|
||||||
.unwrap_or_else(|| if reversed { elem.children.len() } else { 1 });
|
elem.children.len() as u64
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
});
|
||||||
let mut parents = EnumElem::parents_in(styles);
|
let mut parents = EnumElem::parents_in(styles);
|
||||||
|
|
||||||
let full = elem.full(styles);
|
let full = elem.full(styles);
|
||||||
|
@ -19,9 +19,11 @@ pub fn layout_accent(
|
|||||||
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
|
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
|
||||||
|
|
||||||
// Try to replace a glyph with its dotless variant.
|
// Try to replace a glyph with its dotless variant.
|
||||||
|
if elem.dotless(styles) {
|
||||||
if let MathFragment::Glyph(glyph) = &mut base {
|
if let MathFragment::Glyph(glyph) = &mut base {
|
||||||
glyph.make_dotless_form(ctx);
|
glyph.make_dotless_form(ctx);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve class to preserve automatic spacing.
|
// Preserve class to preserve automatic spacing.
|
||||||
let base_class = base.class();
|
let base_class = base.class();
|
||||||
@ -34,7 +36,7 @@ pub fn layout_accent(
|
|||||||
|
|
||||||
// Try to replace accent glyph with flattened variant.
|
// Try to replace accent glyph with flattened variant.
|
||||||
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
||||||
if base.height() > flattened_base_height {
|
if base.ascent() > flattened_base_height {
|
||||||
glyph.make_flattened_accent_form(ctx);
|
glyph.make_flattened_accent_form(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ pub fn layout_accent(
|
|||||||
// minus the accent base height. Only if the base is very small, we need
|
// 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.
|
// a larger gap so that the accent doesn't move too low.
|
||||||
let accent_base_height = scaled!(ctx, styles, accent_base_height);
|
let accent_base_height = scaled!(ctx, styles, accent_base_height);
|
||||||
let gap = -accent.descent() - base.height().min(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 size = Size::new(base.width(), accent.height() + gap + base.height());
|
||||||
let accent_pos = Point::with_x(base_attach - accent_attach);
|
let accent_pos = Point::with_x(base_attach - accent_attach);
|
||||||
let base_pos = Point::with_y(accent.height() + gap);
|
let base_pos = Point::with_y(accent.height() + gap);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{bail, warning, SourceResult};
|
||||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
||||||
@ -9,7 +9,7 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
|
|||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult,
|
alignments, delimiter_alignment, style_for_denominator, AlignmentResult,
|
||||||
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
|
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,67 +23,23 @@ pub fn layout_vec(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let delim = elem.delim(styles);
|
let span = elem.span();
|
||||||
let frame = layout_vec_body(
|
|
||||||
|
let column: Vec<&Content> = elem.children.iter().collect();
|
||||||
|
let frame = layout_body(
|
||||||
ctx,
|
ctx,
|
||||||
styles,
|
styles,
|
||||||
&elem.children,
|
&[column],
|
||||||
elem.align(styles),
|
elem.align(styles),
|
||||||
elem.gap(styles),
|
|
||||||
LeftRightAlternator::Right,
|
LeftRightAlternator::Right,
|
||||||
|
None,
|
||||||
|
Axes::with_y(elem.gap(styles)),
|
||||||
|
span,
|
||||||
|
"elements",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lays out a [`MatElem`].
|
|
||||||
#[typst_macros::time(name = "math.mat", span = elem.span())]
|
|
||||||
pub fn layout_mat(
|
|
||||||
elem: &Packed<MatElem>,
|
|
||||||
ctx: &mut MathContext,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<()> {
|
|
||||||
let augment = elem.augment(styles);
|
|
||||||
let rows = &elem.rows;
|
|
||||||
|
|
||||||
if let Some(aug) = &augment {
|
|
||||||
for &offset in &aug.hline.0 {
|
|
||||||
if offset == 0 || offset.unsigned_abs() >= rows.len() {
|
|
||||||
bail!(
|
|
||||||
elem.span(),
|
|
||||||
"cannot draw a horizontal line after row {} of a matrix with {} rows",
|
|
||||||
if offset < 0 { rows.len() as isize + offset } else { offset },
|
|
||||||
rows.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ncols = rows.first().map_or(0, |row| row.len());
|
|
||||||
|
|
||||||
for &offset in &aug.vline.0 {
|
|
||||||
if offset == 0 || offset.unsigned_abs() >= ncols {
|
|
||||||
bail!(
|
|
||||||
elem.span(),
|
|
||||||
"cannot draw a vertical line after column {} of a matrix with {} columns",
|
|
||||||
if offset < 0 { ncols as isize + offset } else { offset },
|
|
||||||
ncols
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let delim = elem.delim(styles);
|
let delim = elem.delim(styles);
|
||||||
let frame = layout_mat_body(
|
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
|
||||||
ctx,
|
|
||||||
styles,
|
|
||||||
rows,
|
|
||||||
elem.align(styles),
|
|
||||||
augment,
|
|
||||||
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
|
|
||||||
elem.span(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays out a [`CasesElem`].
|
/// Lays out a [`CasesElem`].
|
||||||
@ -93,60 +49,100 @@ pub fn layout_cases(
|
|||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
let delim = elem.delim(styles);
|
let span = elem.span();
|
||||||
let frame = layout_vec_body(
|
|
||||||
|
let column: Vec<&Content> = elem.children.iter().collect();
|
||||||
|
let frame = layout_body(
|
||||||
ctx,
|
ctx,
|
||||||
styles,
|
styles,
|
||||||
&elem.children,
|
&[column],
|
||||||
FixedAlignment::Start,
|
FixedAlignment::Start,
|
||||||
elem.gap(styles),
|
|
||||||
LeftRightAlternator::None,
|
LeftRightAlternator::None,
|
||||||
|
None,
|
||||||
|
Axes::with_y(elem.gap(styles)),
|
||||||
|
span,
|
||||||
|
"branches",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let delim = elem.delim(styles);
|
||||||
let (open, close) =
|
let (open, close) =
|
||||||
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
|
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
|
||||||
|
layout_delimiters(ctx, styles, frame, open, close, span)
|
||||||
layout_delimiters(ctx, styles, frame, open, close, elem.span())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout the inner contents of a vector.
|
/// Lays out a [`MatElem`].
|
||||||
fn layout_vec_body(
|
#[typst_macros::time(name = "math.mat", span = elem.span())]
|
||||||
|
pub fn layout_mat(
|
||||||
|
elem: &Packed<MatElem>,
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
column: &[Content],
|
) -> SourceResult<()> {
|
||||||
|
let span = elem.span();
|
||||||
|
let rows = &elem.rows;
|
||||||
|
let ncols = rows.first().map_or(0, |row| row.len());
|
||||||
|
|
||||||
|
let augment = elem.augment(styles);
|
||||||
|
if let Some(aug) = &augment {
|
||||||
|
for &offset in &aug.hline.0 {
|
||||||
|
if offset == 0 || offset.unsigned_abs() >= rows.len() {
|
||||||
|
bail!(
|
||||||
|
span,
|
||||||
|
"cannot draw a horizontal line after row {} of a matrix with {} rows",
|
||||||
|
if offset < 0 { rows.len() as isize + offset } else { offset },
|
||||||
|
rows.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for &offset in &aug.vline.0 {
|
||||||
|
if offset == 0 || offset.unsigned_abs() >= ncols {
|
||||||
|
bail!(
|
||||||
|
span,
|
||||||
|
"cannot draw a vertical line after column {} of a matrix with {} columns",
|
||||||
|
if offset < 0 { ncols as isize + offset } else { offset },
|
||||||
|
ncols
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transpose rows of the matrix into columns.
|
||||||
|
let mut row_iters: Vec<_> = rows.iter().map(|i| i.iter()).collect();
|
||||||
|
let columns: Vec<Vec<_>> = (0..ncols)
|
||||||
|
.map(|_| row_iters.iter_mut().map(|i| i.next().unwrap()).collect())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let frame = layout_body(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
&columns,
|
||||||
|
elem.align(styles),
|
||||||
|
LeftRightAlternator::Right,
|
||||||
|
augment,
|
||||||
|
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
|
||||||
|
span,
|
||||||
|
"cells",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let delim = elem.delim(styles);
|
||||||
|
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the inner contents of a matrix, vector, or cases.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn layout_body(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
columns: &[Vec<&Content>],
|
||||||
align: FixedAlignment,
|
align: FixedAlignment,
|
||||||
row_gap: Rel<Abs>,
|
|
||||||
alternator: LeftRightAlternator,
|
alternator: LeftRightAlternator,
|
||||||
) -> SourceResult<Frame> {
|
|
||||||
let gap = row_gap.relative_to(ctx.region.size.y);
|
|
||||||
|
|
||||||
let denom_style = style_for_denominator(styles);
|
|
||||||
let mut flat = vec![];
|
|
||||||
for child in column {
|
|
||||||
// We allow linebreaks in cases and vectors, which are functionally
|
|
||||||
// identical to commas.
|
|
||||||
flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows());
|
|
||||||
}
|
|
||||||
// We pad ascent and descent with the ascent and descent of the paren
|
|
||||||
// to ensure that normal vectors are aligned with others unless they are
|
|
||||||
// way too big.
|
|
||||||
let paren =
|
|
||||||
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
|
|
||||||
Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout the inner contents of a matrix.
|
|
||||||
fn layout_mat_body(
|
|
||||||
ctx: &mut MathContext,
|
|
||||||
styles: StyleChain,
|
|
||||||
rows: &[Vec<Content>],
|
|
||||||
align: FixedAlignment,
|
|
||||||
augment: Option<Augment<Abs>>,
|
augment: Option<Augment<Abs>>,
|
||||||
gap: Axes<Rel<Abs>>,
|
gap: Axes<Rel<Abs>>,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
children: &str,
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
let ncols = rows.first().map_or(0, |row| row.len());
|
let nrows = columns.first().map_or(0, |col| col.len());
|
||||||
let nrows = rows.len();
|
let ncols = columns.len();
|
||||||
if ncols == 0 || nrows == 0 {
|
if ncols == 0 || nrows == 0 {
|
||||||
return Ok(Frame::soft(Size::zero()));
|
return Ok(Frame::soft(Size::zero()));
|
||||||
}
|
}
|
||||||
@ -178,16 +174,11 @@ fn layout_mat_body(
|
|||||||
// Before the full matrix body can be laid out, the
|
// Before the full matrix body can be laid out, the
|
||||||
// individual cells must first be independently laid out
|
// individual cells must first be independently laid out
|
||||||
// so we can ensure alignment across rows and columns.
|
// so we can ensure alignment across rows and columns.
|
||||||
|
let mut cols = vec![vec![]; ncols];
|
||||||
|
|
||||||
// This variable stores the maximum ascent and descent for each row.
|
// This variable stores the maximum ascent and descent for each row.
|
||||||
let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
|
let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
|
||||||
|
|
||||||
// We want to transpose our data layout to columns
|
|
||||||
// before final layout. For efficiency, the columns
|
|
||||||
// variable is set up here and newly generated
|
|
||||||
// individual cells are then added to it.
|
|
||||||
let mut cols = vec![vec![]; ncols];
|
|
||||||
|
|
||||||
let denom_style = style_for_denominator(styles);
|
let denom_style = style_for_denominator(styles);
|
||||||
// We pad ascent and descent with the ascent and descent of the paren
|
// We pad ascent and descent with the ascent and descent of the paren
|
||||||
// to ensure that normal matrices are aligned with others unless they are
|
// to ensure that normal matrices are aligned with others unless they are
|
||||||
@ -195,10 +186,22 @@ fn layout_mat_body(
|
|||||||
let paren =
|
let paren =
|
||||||
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
|
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
|
||||||
|
|
||||||
for (row, (ascent, descent)) in rows.iter().zip(&mut heights) {
|
for (column, col) in columns.iter().zip(&mut cols) {
|
||||||
for (cell, col) in row.iter().zip(&mut cols) {
|
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
|
||||||
|
let cell_span = cell.span();
|
||||||
let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?;
|
let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?;
|
||||||
|
|
||||||
|
// We ignore linebreaks in the cells as we can't differentiate
|
||||||
|
// alignment points for the whole body from ones for a specific
|
||||||
|
// cell, and multiline cells don't quite make sense at the moment.
|
||||||
|
if cell.is_multiline() {
|
||||||
|
ctx.engine.sink.warn(warning!(
|
||||||
|
cell_span,
|
||||||
|
"linebreaks are ignored in {}", children;
|
||||||
|
hint: "use commas instead to separate each line"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
ascent.set_max(cell.ascent().max(paren.ascent));
|
ascent.set_max(cell.ascent().max(paren.ascent));
|
||||||
descent.set_max(cell.descent().max(paren.descent));
|
descent.set_max(cell.descent().max(paren.descent));
|
||||||
|
|
||||||
@ -222,7 +225,7 @@ fn layout_mat_body(
|
|||||||
let mut y = Abs::zero();
|
let mut y = Abs::zero();
|
||||||
|
|
||||||
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
|
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
|
||||||
let cell = cell.into_line_frame(&points, LeftRightAlternator::Right);
|
let cell = cell.into_line_frame(&points, alternator);
|
||||||
let pos = Point::new(
|
let pos = Point::new(
|
||||||
if points.is_empty() {
|
if points.is_empty() {
|
||||||
x + align.position(rcol - cell.width())
|
x + align.position(rcol - cell.width())
|
||||||
|
@ -85,14 +85,15 @@ pub fn layout_root(
|
|||||||
ascent.set_max(shift_up + index.ascent());
|
ascent.set_max(shift_up + index.ascent());
|
||||||
}
|
}
|
||||||
|
|
||||||
let radicand_x = sqrt_offset + sqrt.width();
|
let sqrt_x = sqrt_offset.max(Abs::zero());
|
||||||
|
let radicand_x = sqrt_x + sqrt.width();
|
||||||
let radicand_y = ascent - radicand.ascent();
|
let radicand_y = ascent - radicand.ascent();
|
||||||
let width = radicand_x + radicand.width();
|
let width = radicand_x + radicand.width();
|
||||||
let size = Size::new(width, ascent + descent);
|
let size = Size::new(width, ascent + descent);
|
||||||
|
|
||||||
// The extra "- thickness" comes from the fact that the sqrt is placed
|
// The extra "- thickness" comes from the fact that the sqrt is placed
|
||||||
// in `push_frame` with respect to its top, not its baseline.
|
// in `push_frame` with respect to its top, not its baseline.
|
||||||
let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness);
|
let sqrt_pos = Point::new(sqrt_x, radicand_y - gap - thickness);
|
||||||
let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
|
let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
|
||||||
let radicand_pos = Point::new(radicand_x, radicand_y);
|
let radicand_pos = Point::new(radicand_x, radicand_y);
|
||||||
|
|
||||||
@ -100,7 +101,8 @@ pub fn layout_root(
|
|||||||
frame.set_baseline(ascent);
|
frame.set_baseline(ascent);
|
||||||
|
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up);
|
let index_x = -sqrt_offset.min(Abs::zero()) + kern_before;
|
||||||
|
let index_pos = Point::new(index_x, ascent - index.ascent() - shift_up);
|
||||||
frame.push_frame(index_pos, index);
|
frame.push_frame(index_pos, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,6 @@ pub fn stack(
|
|||||||
gap: Abs,
|
gap: Abs,
|
||||||
baseline: usize,
|
baseline: usize,
|
||||||
alternator: LeftRightAlternator,
|
alternator: LeftRightAlternator,
|
||||||
minimum_ascent_descent: Option<(Abs, Abs)>,
|
|
||||||
) -> Frame {
|
) -> Frame {
|
||||||
let AlignmentResult { points, width } = alignments(&rows);
|
let AlignmentResult { points, width } = alignments(&rows);
|
||||||
let rows: Vec<_> = rows
|
let rows: Vec<_> = rows
|
||||||
@ -125,13 +124,9 @@ pub fn stack(
|
|||||||
.map(|row| row.into_line_frame(&points, alternator))
|
.map(|row| row.into_line_frame(&points, alternator))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let padded_height = |height: Abs| {
|
|
||||||
height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d))
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut frame = Frame::soft(Size::new(
|
let mut frame = Frame::soft(Size::new(
|
||||||
width,
|
width,
|
||||||
rows.iter().map(|row| padded_height(row.height())).sum::<Abs>()
|
rows.iter().map(|row| row.height()).sum::<Abs>()
|
||||||
+ rows.len().saturating_sub(1) as f64 * gap,
|
+ rows.len().saturating_sub(1) as f64 * gap,
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -142,14 +137,11 @@ pub fn stack(
|
|||||||
} else {
|
} else {
|
||||||
Abs::zero()
|
Abs::zero()
|
||||||
};
|
};
|
||||||
let ascent_padded_part = minimum_ascent_descent
|
let pos = Point::new(x, y);
|
||||||
.map_or(Abs::zero(), |(a, _)| (a - row.ascent()))
|
|
||||||
.max(Abs::zero());
|
|
||||||
let pos = Point::new(x, y + ascent_padded_part);
|
|
||||||
if i == baseline {
|
if i == baseline {
|
||||||
frame.set_baseline(y + row.baseline() + ascent_padded_part);
|
frame.set_baseline(y + row.baseline());
|
||||||
}
|
}
|
||||||
y += padded_height(row.height()) + gap;
|
y += row.height() + gap;
|
||||||
frame.push_frame(pos, row);
|
frame.push_frame(pos, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,6 +302,6 @@ fn assemble(
|
|||||||
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
|
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
|
||||||
assembly.parts.into_iter().flat_map(move |part| {
|
assembly.parts.into_iter().flat_map(move |part| {
|
||||||
let count = if part.part_flags.extender() { repeat } else { 1 };
|
let count = if part.part_flags.extender() { repeat } else { 1 };
|
||||||
std::iter::repeat(part).take(count)
|
std::iter::repeat_n(part, count)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,6 @@ fn layout_inline_text(
|
|||||||
styles,
|
styles,
|
||||||
Size::splat(Abs::inf()),
|
Size::splat(Abs::inf()),
|
||||||
false,
|
false,
|
||||||
None,
|
|
||||||
)?
|
)?
|
||||||
.into_frame();
|
.into_frame();
|
||||||
|
|
||||||
|
@ -312,14 +312,8 @@ fn layout_underoverspreader(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let frame = stack(
|
let frame =
|
||||||
rows,
|
stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right);
|
||||||
FixedAlignment::Center,
|
|
||||||
gap,
|
|
||||||
baseline,
|
|
||||||
LeftRightAlternator::Right,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
ctx.push(FrameFragment::new(styles, frame).with_class(body_class));
|
ctx.push(FrameFragment::new(styles, frame).with_class(body_class));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use typst_library::foundations::StyleChain;
|
use typst_library::foundations::StyleChain;
|
||||||
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point};
|
use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides};
|
||||||
use typst_library::model::{Destination, LinkElem};
|
use typst_library::model::{Destination, LinkElem, ParElem};
|
||||||
|
|
||||||
/// Frame-level modifications resulting from styles that do not impose any
|
/// Frame-level modifications resulting from styles that do not impose any
|
||||||
/// layout structure.
|
/// layout structure.
|
||||||
@ -52,14 +52,7 @@ pub trait FrameModify {
|
|||||||
|
|
||||||
impl FrameModify for Frame {
|
impl FrameModify for Frame {
|
||||||
fn modify(&mut self, modifiers: &FrameModifiers) {
|
fn modify(&mut self, modifiers: &FrameModifiers) {
|
||||||
if let Some(dest) = &modifiers.dest {
|
modify_frame(self, modifiers, None);
|
||||||
let size = self.size();
|
|
||||||
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
|
|
||||||
}
|
|
||||||
|
|
||||||
if modifiers.hidden {
|
|
||||||
self.hide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +75,41 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait FrameModifyText {
|
||||||
|
/// Resolve and apply [`FrameModifiers`] for this text frame.
|
||||||
|
fn modify_text(&mut self, styles: StyleChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y);
|
||||||
|
modify_frame(self, &modifiers, Some(outset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_frame(
|
||||||
|
frame: &mut Frame,
|
||||||
|
modifiers: &FrameModifiers,
|
||||||
|
link_box_outset: Option<Sides<Abs>>,
|
||||||
|
) {
|
||||||
|
if let Some(dest) = &modifiers.dest {
|
||||||
|
let mut pos = Point::zero();
|
||||||
|
let mut size = frame.size();
|
||||||
|
if let Some(outset) = link_box_outset {
|
||||||
|
pos.y -= outset.top;
|
||||||
|
pos.x -= outset.left;
|
||||||
|
size += outset.sum_by_axis();
|
||||||
|
}
|
||||||
|
frame.push(pos, FrameItem::Link(dest.clone(), size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if modifiers.hidden {
|
||||||
|
frame.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs layout and modification in one step.
|
/// Performs layout and modification in one step.
|
||||||
///
|
///
|
||||||
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,
|
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,
|
||||||
|
@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> {
|
|||||||
self.last_point = point;
|
self.last_point = point;
|
||||||
self.last_control_from = point;
|
self.last_control_from = point;
|
||||||
self.is_started = true;
|
self.is_started = true;
|
||||||
|
self.is_empty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a line segment.
|
/// Add a line segment.
|
||||||
@ -1281,7 +1282,7 @@ impl ControlPoints {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to draw arcs with bezier curves.
|
/// Helper to draw arcs with Bézier curves.
|
||||||
trait CurveExt {
|
trait CurveExt {
|
||||||
fn arc(&mut self, start: Point, center: Point, end: Point);
|
fn arc(&mut self, start: Point, center: Point, end: Point);
|
||||||
fn arc_move(&mut self, start: Point, center: Point, end: Point);
|
fn arc_move(&mut self, start: Point, center: Point, end: Point);
|
||||||
@ -1305,7 +1306,7 @@ impl CurveExt for Curve {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the control points for a bezier curve that approximates a circular arc for
|
/// Get the control points for a Bézier curve that approximates a circular arc for
|
||||||
/// a start point, an end point and a center of the circle whose arc connects
|
/// a start point, an end point and a center of the circle whose arc connects
|
||||||
/// the two.
|
/// the two.
|
||||||
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {
|
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {
|
||||||
|
@ -29,6 +29,7 @@ csv = { workspace = true }
|
|||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
flate2 = { workspace = true }
|
flate2 = { workspace = true }
|
||||||
fontdb = { workspace = true }
|
fontdb = { workspace = true }
|
||||||
|
glidesort = { workspace = true }
|
||||||
hayagriva = { workspace = true }
|
hayagriva = { workspace = true }
|
||||||
icu_properties = { workspace = true }
|
icu_properties = { workspace = true }
|
||||||
icu_provider = { workspace = true }
|
icu_provider = { workspace = true }
|
||||||
@ -38,6 +39,7 @@ indexmap = { workspace = true }
|
|||||||
kamadak-exif = { workspace = true }
|
kamadak-exif = { workspace = true }
|
||||||
kurbo = { workspace = true }
|
kurbo = { workspace = true }
|
||||||
lipsum = { workspace = true }
|
lipsum = { workspace = true }
|
||||||
|
memchr = { workspace = true }
|
||||||
palette = { workspace = true }
|
palette = { workspace = true }
|
||||||
phf = { workspace = true }
|
phf = { workspace = true }
|
||||||
png = { workspace = true }
|
png = { workspace = true }
|
||||||
@ -60,6 +62,7 @@ ttf-parser = { workspace = true }
|
|||||||
two-face = { workspace = true }
|
two-face = { workspace = true }
|
||||||
typed-arena = { workspace = true }
|
typed-arena = { workspace = true }
|
||||||
unicode-math-class = { workspace = true }
|
unicode-math-class = { workspace = true }
|
||||||
|
unicode-normalization = { workspace = true }
|
||||||
unicode-segmentation = { workspace = true }
|
unicode-segmentation = { workspace = true }
|
||||||
unscanny = { workspace = true }
|
unscanny = { workspace = true }
|
||||||
usvg = { workspace = true }
|
usvg = { workspace = true }
|
||||||
|
@ -312,7 +312,8 @@ impl Route<'_> {
|
|||||||
if !self.within(Route::MAX_SHOW_RULE_DEPTH) {
|
if !self.within(Route::MAX_SHOW_RULE_DEPTH) {
|
||||||
bail!(
|
bail!(
|
||||||
"maximum show rule depth exceeded";
|
"maximum show rule depth exceeded";
|
||||||
hint: "check whether the show rule matches its own output"
|
hint: "maybe a show rule matches its own output";
|
||||||
|
hint: "maybe there are too deeply nested elements"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -172,17 +172,29 @@ impl Array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the first item in the array. May be used on the left-hand side
|
/// Returns the first item in the array. May be used on the left-hand side
|
||||||
/// of an assignment. Fails with an error if the array is empty.
|
/// an assignment. Returns the default value if the array is empty
|
||||||
|
/// or fails with an error is no default value was specified.
|
||||||
#[func]
|
#[func]
|
||||||
pub fn first(&self) -> StrResult<Value> {
|
pub fn first(
|
||||||
self.0.first().cloned().ok_or_else(array_is_empty)
|
&self,
|
||||||
|
/// A default value to return if the array is empty.
|
||||||
|
#[named]
|
||||||
|
default: Option<Value>,
|
||||||
|
) -> StrResult<Value> {
|
||||||
|
self.0.first().cloned().or(default).ok_or_else(array_is_empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the last item in the array. May be used on the left-hand side of
|
/// Returns the last item in the array. May be used on the left-hand side of
|
||||||
/// an assignment. Fails with an error if the array is empty.
|
/// an assignment. Returns the default value if the array is empty
|
||||||
|
/// or fails with an error is no default value was specified.
|
||||||
#[func]
|
#[func]
|
||||||
pub fn last(&self) -> StrResult<Value> {
|
pub fn last(
|
||||||
self.0.last().cloned().ok_or_else(array_is_empty)
|
&self,
|
||||||
|
/// A default value to return if the array is empty.
|
||||||
|
#[named]
|
||||||
|
default: Option<Value>,
|
||||||
|
) -> StrResult<Value> {
|
||||||
|
self.0.last().cloned().or(default).ok_or_else(array_is_empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the item at the specified index in the array. May be used on the
|
/// Returns the item at the specified index in the array. May be used on the
|
||||||
@ -751,7 +763,7 @@ impl Array {
|
|||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #let array = (1, 2, 3, 4, 5, 6, 7, 8)
|
/// #let array = (1, 2, 3, 4, 5, 6, 7, 8)
|
||||||
/// #array.chunks(3)
|
/// #array.chunks(3) \
|
||||||
/// #array.chunks(3, exact: true)
|
/// #array.chunks(3, exact: true)
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func]
|
||||||
@ -796,7 +808,7 @@ impl Array {
|
|||||||
/// function. The sorting algorithm used is stable.
|
/// function. The sorting algorithm used is stable.
|
||||||
///
|
///
|
||||||
/// Returns an error if two values could not be compared or if the key
|
/// Returns an error if two values could not be compared or if the key
|
||||||
/// function (if given) yields an error.
|
/// or comparison function (if given) yields an error.
|
||||||
///
|
///
|
||||||
/// To sort according to multiple criteria at once, e.g. in case of equality
|
/// To sort according to multiple criteria at once, e.g. in case of equality
|
||||||
/// between some criteria, the key function can return an array. The results
|
/// between some criteria, the key function can return an array. The results
|
||||||
@ -820,17 +832,116 @@ impl Array {
|
|||||||
/// determine the keys to sort by.
|
/// determine the keys to sort by.
|
||||||
#[named]
|
#[named]
|
||||||
key: Option<Func>,
|
key: Option<Func>,
|
||||||
|
/// If given, uses this function to compare elements in the array.
|
||||||
|
///
|
||||||
|
/// This function should return a boolean: `{true}` indicates that the
|
||||||
|
/// elements are in order, while `{false}` indicates that they should be
|
||||||
|
/// swapped. To keep the sort stable, if the two elements are equal, the
|
||||||
|
/// function should return `{true}`.
|
||||||
|
///
|
||||||
|
/// If this function does not order the elements properly (e.g., by
|
||||||
|
/// returning `{false}` for both `{(x, y)}` and `{(y, x)}`, or for
|
||||||
|
/// `{(x, x)}`), the resulting array will be in unspecified order.
|
||||||
|
///
|
||||||
|
/// When used together with `key`, `by` will be passed the keys instead
|
||||||
|
/// of the elements.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #(
|
||||||
|
/// "sorted",
|
||||||
|
/// "by",
|
||||||
|
/// "decreasing",
|
||||||
|
/// "length",
|
||||||
|
/// ).sorted(
|
||||||
|
/// key: s => s.len(),
|
||||||
|
/// by: (l, r) => l >= r,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#[named]
|
||||||
|
by: Option<Func>,
|
||||||
) -> SourceResult<Array> {
|
) -> SourceResult<Array> {
|
||||||
|
match by {
|
||||||
|
Some(by) => {
|
||||||
|
let mut are_in_order = |mut x, mut y| {
|
||||||
|
if let Some(f) = &key {
|
||||||
|
// We rely on `comemo`'s memoization of function
|
||||||
|
// evaluation to not excessively reevaluate the key.
|
||||||
|
x = f.call(engine, context, [x])?;
|
||||||
|
y = f.call(engine, context, [y])?;
|
||||||
|
}
|
||||||
|
match by.call(engine, context, [x, y])? {
|
||||||
|
Value::Bool(b) => Ok(b),
|
||||||
|
x => {
|
||||||
|
bail!(
|
||||||
|
span,
|
||||||
|
"expected boolean from `by` function, got {}",
|
||||||
|
x.ty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// If a comparison function is provided, we use `glidesort`
|
||||||
|
// instead of the standard library sorting algorithm to prevent
|
||||||
|
// panics in case the comparison function does not define a
|
||||||
|
// valid order (see https://github.com/typst/typst/pull/5627).
|
||||||
let mut result = Ok(());
|
let mut result = Ok(());
|
||||||
let mut vec = self.0;
|
let mut vec = self.0.into_iter().enumerate().collect::<Vec<_>>();
|
||||||
|
glidesort::sort_by(&mut vec, |(i, x), (j, y)| {
|
||||||
|
// Because we use booleans for the comparison function, in
|
||||||
|
// order to keep the sort stable, we need to compare in the
|
||||||
|
// right order.
|
||||||
|
if i < j {
|
||||||
|
// If `x` and `y` appear in this order in the original
|
||||||
|
// array, then we should change their order (i.e.,
|
||||||
|
// return `Ordering::Greater`) iff `y` is strictly less
|
||||||
|
// than `x` (i.e., `compare(x, y)` returns `false`).
|
||||||
|
// Otherwise, we should keep them in the same order
|
||||||
|
// (i.e., return `Ordering::Less`).
|
||||||
|
match are_in_order(x.clone(), y.clone()) {
|
||||||
|
Ok(false) => Ordering::Greater,
|
||||||
|
Ok(true) => Ordering::Less,
|
||||||
|
Err(err) => {
|
||||||
|
if result.is_ok() {
|
||||||
|
result = Err(err);
|
||||||
|
}
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If `x` and `y` appear in the opposite order in the
|
||||||
|
// original array, then we should change their order
|
||||||
|
// (i.e., return `Ordering::Less`) iff `x` is strictly
|
||||||
|
// less than `y` (i.e., `compare(y, x)` returns
|
||||||
|
// `false`). Otherwise, we should keep them in the same
|
||||||
|
// order (i.e., return `Ordering::Less`).
|
||||||
|
match are_in_order(y.clone(), x.clone()) {
|
||||||
|
Ok(false) => Ordering::Less,
|
||||||
|
Ok(true) => Ordering::Greater,
|
||||||
|
Err(err) => {
|
||||||
|
if result.is_ok() {
|
||||||
|
result = Err(err);
|
||||||
|
}
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.map(|()| vec.into_iter().map(|(_, x)| x).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
let mut key_of = |x: Value| match &key {
|
let mut key_of = |x: Value| match &key {
|
||||||
// NOTE: We are relying on `comemo`'s memoization of function
|
// We rely on `comemo`'s memoization of function evaluation
|
||||||
// evaluation to not excessively reevaluate the `key`.
|
// to not excessively reevaluate the key.
|
||||||
Some(f) => f.call(engine, context, [x]),
|
Some(f) => f.call(engine, context, [x]),
|
||||||
None => Ok(x),
|
None => Ok(x),
|
||||||
};
|
};
|
||||||
|
// If no comparison function is provided, we know the order is
|
||||||
|
// valid, so we can use the standard library sort and prevent an
|
||||||
|
// extra allocation.
|
||||||
|
let mut result = Ok(());
|
||||||
|
let mut vec = self.0;
|
||||||
vec.make_mut().sort_by(|a, b| {
|
vec.make_mut().sort_by(|a, b| {
|
||||||
// Until we get `try` blocks :)
|
|
||||||
match (key_of(a.clone()), key_of(b.clone())) {
|
match (key_of(a.clone()), key_of(b.clone())) {
|
||||||
(Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| {
|
(Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| {
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
@ -846,7 +957,9 @@ impl Array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
result.map(|_| vec.into())
|
result.map(|()| vec.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deduplicates all items in the array.
|
/// Deduplicates all items in the array.
|
||||||
|
@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::iter::{self, Sum};
|
use std::iter::{self, Sum};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::ops::{Add, AddAssign, Deref, DerefMut};
|
use std::ops::{Add, AddAssign, ControlFlow, Deref, DerefMut};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
@ -414,10 +414,11 @@ impl Content {
|
|||||||
/// Elements produced in `show` rules will not be included in the results.
|
/// Elements produced in `show` rules will not be included in the results.
|
||||||
pub fn query(&self, selector: Selector) -> Vec<Content> {
|
pub fn query(&self, selector: Selector) -> Vec<Content> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
self.traverse(&mut |element| {
|
self.traverse(&mut |element| -> ControlFlow<()> {
|
||||||
if selector.matches(&element, None) {
|
if selector.matches(&element, None) {
|
||||||
results.push(element);
|
results.push(element);
|
||||||
}
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
});
|
});
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
@ -427,54 +428,58 @@ impl Content {
|
|||||||
///
|
///
|
||||||
/// Elements produced in `show` rules will not be included in the results.
|
/// Elements produced in `show` rules will not be included in the results.
|
||||||
pub fn query_first(&self, selector: &Selector) -> Option<Content> {
|
pub fn query_first(&self, selector: &Selector) -> Option<Content> {
|
||||||
let mut result = None;
|
self.traverse(&mut |element| -> ControlFlow<Content> {
|
||||||
self.traverse(&mut |element| {
|
if selector.matches(&element, None) {
|
||||||
if result.is_none() && selector.matches(&element, None) {
|
ControlFlow::Break(element)
|
||||||
result = Some(element);
|
} else {
|
||||||
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
result
|
.break_value()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the plain text of this content.
|
/// Extracts the plain text of this content.
|
||||||
pub fn plain_text(&self) -> EcoString {
|
pub fn plain_text(&self) -> EcoString {
|
||||||
let mut text = EcoString::new();
|
let mut text = EcoString::new();
|
||||||
self.traverse(&mut |element| {
|
self.traverse(&mut |element| -> ControlFlow<()> {
|
||||||
if let Some(textable) = element.with::<dyn PlainText>() {
|
if let Some(textable) = element.with::<dyn PlainText>() {
|
||||||
textable.plain_text(&mut text);
|
textable.plain_text(&mut text);
|
||||||
}
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
});
|
});
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Traverse this content.
|
/// Traverse this content.
|
||||||
fn traverse<F>(&self, f: &mut F)
|
fn traverse<F, B>(&self, f: &mut F) -> ControlFlow<B>
|
||||||
where
|
where
|
||||||
F: FnMut(Content),
|
F: FnMut(Content) -> ControlFlow<B>,
|
||||||
{
|
{
|
||||||
f(self.clone());
|
|
||||||
|
|
||||||
self.inner
|
|
||||||
.elem
|
|
||||||
.fields()
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|(_, value)| walk_value(value, f));
|
|
||||||
|
|
||||||
/// Walks a given value to find any content that matches the selector.
|
/// Walks a given value to find any content that matches the selector.
|
||||||
fn walk_value<F>(value: Value, f: &mut F)
|
///
|
||||||
|
/// Returns early if the function gives `ControlFlow::Break`.
|
||||||
|
fn walk_value<F, B>(value: Value, f: &mut F) -> ControlFlow<B>
|
||||||
where
|
where
|
||||||
F: FnMut(Content),
|
F: FnMut(Content) -> ControlFlow<B>,
|
||||||
{
|
{
|
||||||
match value {
|
match value {
|
||||||
Value::Content(content) => content.traverse(f),
|
Value::Content(content) => content.traverse(f),
|
||||||
Value::Array(array) => {
|
Value::Array(array) => {
|
||||||
for value in array {
|
for value in array {
|
||||||
walk_value(value, f);
|
walk_value(value, f)?;
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
_ => ControlFlow::Continue(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
// Call f on the element itself before recursively iterating its fields.
|
||||||
|
f(self.clone())?;
|
||||||
|
for (_, value) in self.inner.elem.fields() {
|
||||||
|
walk_value(value, f)?;
|
||||||
}
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ impl f64 {
|
|||||||
f64::signum(self)
|
f64::signum(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts bytes to a float.
|
/// Interprets bytes as a float.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
|
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
|
||||||
@ -120,8 +120,10 @@ impl f64 {
|
|||||||
pub fn from_bytes(
|
pub fn from_bytes(
|
||||||
/// The bytes that should be converted to a float.
|
/// The bytes that should be converted to a float.
|
||||||
///
|
///
|
||||||
/// Must be of length exactly 8 so that the result fits into a 64-bit
|
/// Must have a length of either 4 or 8. The bytes are then
|
||||||
/// float.
|
/// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s
|
||||||
|
/// binary32 (single-precision) or binary64 (double-precision) format
|
||||||
|
/// depending on the length of the bytes.
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
/// The endianness of the conversion.
|
/// The endianness of the conversion.
|
||||||
#[named]
|
#[named]
|
||||||
@ -158,6 +160,13 @@ impl f64 {
|
|||||||
#[named]
|
#[named]
|
||||||
#[default(Endianness::Little)]
|
#[default(Endianness::Little)]
|
||||||
endian: Endianness,
|
endian: Endianness,
|
||||||
|
/// The size of the resulting bytes.
|
||||||
|
///
|
||||||
|
/// This must be either 4 or 8. The call will return the
|
||||||
|
/// representation of this float in either
|
||||||
|
/// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32
|
||||||
|
/// (single-precision) or binary64 (double-precision) format
|
||||||
|
/// depending on the provided size.
|
||||||
#[named]
|
#[named]
|
||||||
#[default(8)]
|
#[default(8)]
|
||||||
size: u32,
|
size: u32,
|
||||||
|
@ -112,7 +112,7 @@ use crate::foundations::{
|
|||||||
/// it into another file by writing `{import "foo.typ": alert}`.
|
/// it into another file by writing `{import "foo.typ": alert}`.
|
||||||
///
|
///
|
||||||
/// # Unnamed functions { #unnamed }
|
/// # Unnamed functions { #unnamed }
|
||||||
/// You can also created an unnamed function without creating a binding by
|
/// You can also create an unnamed function without creating a binding by
|
||||||
/// specifying a parameter list followed by `=>` and the function body. If your
|
/// specifying a parameter list followed by `=>` and the function body. If your
|
||||||
/// function has just one parameter, the parentheses around the parameter list
|
/// function has just one parameter, the parentheses around the parameter list
|
||||||
/// are optional. Unnamed functions are mainly useful for show rules, but also
|
/// are optional. Unnamed functions are mainly useful for show rules, but also
|
||||||
@ -437,10 +437,10 @@ impl PartialEq for Func {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<&NativeFuncData> for Func {
|
impl PartialEq<&'static NativeFuncData> for Func {
|
||||||
fn eq(&self, other: &&NativeFuncData) -> bool {
|
fn eq(&self, other: &&'static NativeFuncData) -> bool {
|
||||||
match &self.repr {
|
match &self.repr {
|
||||||
Repr::Native(native) => native.function == other.function,
|
Repr::Native(native) => *native == Static(*other),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ pub use {
|
|||||||
indexmap::IndexMap,
|
indexmap::IndexMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use comemo::TrackedMut;
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst_syntax::Spanned;
|
use typst_syntax::Spanned;
|
||||||
|
|
||||||
@ -297,5 +298,14 @@ pub fn eval(
|
|||||||
for (key, value) in dict {
|
for (key, value) in dict {
|
||||||
scope.bind(key.into(), Binding::new(value, span));
|
scope.bind(key.into(), Binding::new(value, span));
|
||||||
}
|
}
|
||||||
(engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope)
|
|
||||||
|
(engine.routines.eval_string)(
|
||||||
|
engine.routines,
|
||||||
|
engine.world,
|
||||||
|
TrackedMut::reborrow_mut(&mut engine.sink),
|
||||||
|
&text,
|
||||||
|
span,
|
||||||
|
mode,
|
||||||
|
scope,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ use typst_syntax::FileId;
|
|||||||
use crate::diag::{bail, DeprecationSink, StrResult};
|
use crate::diag::{bail, DeprecationSink, StrResult};
|
||||||
use crate::foundations::{repr, ty, Content, Scope, Value};
|
use crate::foundations::{repr, ty, Content, Scope, Value};
|
||||||
|
|
||||||
/// An module of definitions.
|
/// A module of definitions.
|
||||||
///
|
///
|
||||||
/// A module
|
/// A module
|
||||||
/// - be built-in
|
/// - be built-in
|
||||||
|
@ -148,9 +148,7 @@ use crate::loading::{DataSource, Load};
|
|||||||
#[func(scope)]
|
#[func(scope)]
|
||||||
pub fn plugin(
|
pub fn plugin(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// A path to a WebAssembly file or raw WebAssembly bytes.
|
/// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
) -> SourceResult<Module> {
|
) -> SourceResult<Module> {
|
||||||
let data = source.load(engine.world)?;
|
let data = source.load(engine.world)?;
|
||||||
|
@ -7,12 +7,13 @@ use comemo::Tracked;
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
|
use unicode_normalization::UnicodeNormalization;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func,
|
cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func,
|
||||||
IntoValue, Label, Repr, Type, Value, Version,
|
IntoValue, Label, Repr, Type, Value, Version,
|
||||||
};
|
};
|
||||||
use crate::layout::Alignment;
|
use crate::layout::Alignment;
|
||||||
@ -286,6 +287,30 @@ impl Str {
|
|||||||
Ok(c.into())
|
Ok(c.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalizes the string to the given Unicode normal form.
|
||||||
|
///
|
||||||
|
/// This is useful when manipulating strings containing Unicode combining
|
||||||
|
/// characters.
|
||||||
|
///
|
||||||
|
/// ```typ
|
||||||
|
/// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}")
|
||||||
|
/// #assert.eq("ſ́".normalize(form: "nfkc"), "ś")
|
||||||
|
/// ```
|
||||||
|
#[func]
|
||||||
|
pub fn normalize(
|
||||||
|
&self,
|
||||||
|
#[named]
|
||||||
|
#[default(UnicodeNormalForm::Nfc)]
|
||||||
|
form: UnicodeNormalForm,
|
||||||
|
) -> Str {
|
||||||
|
match form {
|
||||||
|
UnicodeNormalForm::Nfc => self.nfc().collect(),
|
||||||
|
UnicodeNormalForm::Nfd => self.nfd().collect(),
|
||||||
|
UnicodeNormalForm::Nfkc => self.nfkc().collect(),
|
||||||
|
UnicodeNormalForm::Nfkd => self.nfkd().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the string contains the specified pattern.
|
/// Whether the string contains the specified pattern.
|
||||||
///
|
///
|
||||||
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
|
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
|
||||||
@ -788,6 +813,25 @@ cast! {
|
|||||||
v: Str => Self::Str(v),
|
v: Str => Self::Str(v),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A Unicode normalization form.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
|
pub enum UnicodeNormalForm {
|
||||||
|
/// Canonical composition where e.g. accented letters are turned into a
|
||||||
|
/// single Unicode codepoint.
|
||||||
|
#[string("nfc")]
|
||||||
|
Nfc,
|
||||||
|
/// Canonical decomposition where e.g. accented letters are split into a
|
||||||
|
/// separate base and diacritic.
|
||||||
|
#[string("nfd")]
|
||||||
|
Nfd,
|
||||||
|
/// Like NFC, but using the Unicode compatibility decompositions.
|
||||||
|
#[string("nfkc")]
|
||||||
|
Nfkc,
|
||||||
|
/// Like NFD, but using the Unicode compatibility decompositions.
|
||||||
|
#[string("nfkd")]
|
||||||
|
Nfkd,
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert an item of std's `match_indices` to a dictionary.
|
/// Convert an item of std's `match_indices` to a dictionary.
|
||||||
fn match_to_dict((start, text): (usize, &str)) -> Dict {
|
fn match_to_dict((start, text): (usize, &str)) -> Dict {
|
||||||
dict! {
|
dict! {
|
||||||
|
@ -471,7 +471,8 @@ impl Debug for Recipe {
|
|||||||
selector.fmt(f)?;
|
selector.fmt(f)?;
|
||||||
f.write_str(", ")?;
|
f.write_str(", ")?;
|
||||||
}
|
}
|
||||||
self.transform.fmt(f)
|
self.transform.fmt(f)?;
|
||||||
|
f.write_str(")")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ use crate::foundations::{
|
|||||||
/// be accessed using [field access notation]($scripting/#fields):
|
/// be accessed using [field access notation]($scripting/#fields):
|
||||||
///
|
///
|
||||||
/// - General symbols are defined in the [`sym` module]($category/symbols/sym)
|
/// - General symbols are defined in the [`sym` module]($category/symbols/sym)
|
||||||
|
/// and are accessible without the `sym.` prefix in math mode.
|
||||||
/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
|
/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
|
||||||
///
|
///
|
||||||
/// Moreover, you can define custom symbols with this type's constructor
|
/// Moreover, you can define custom symbols with this type's constructor
|
||||||
@ -410,7 +411,7 @@ fn find<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let score = (matching, Reverse(total));
|
let score = (matching, Reverse(total));
|
||||||
if best_score.map_or(true, |b| score > b) {
|
if best_score.is_none_or(|b| score > b) {
|
||||||
best = Some(candidate.1);
|
best = Some(candidate.1);
|
||||||
best_score = Some(score);
|
best_score = Some(score);
|
||||||
}
|
}
|
||||||
|
@ -39,11 +39,25 @@ use crate::foundations::{
|
|||||||
/// #type(image("glacier.jpg")).
|
/// #type(image("glacier.jpg")).
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// The type of `10` is `int`. Now, what is the type of `int` or even `type`?
|
/// The type of `{10}` is `int`. Now, what is the type of `int` or even `type`?
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #type(int) \
|
/// #type(int) \
|
||||||
/// #type(type)
|
/// #type(type)
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// Unlike other types like `int`, [none] and [auto] do not have a name
|
||||||
|
/// representing them. To test if a value is one of these, compare your value to
|
||||||
|
/// them directly, e.g:
|
||||||
|
/// ```example
|
||||||
|
/// #let val = none
|
||||||
|
/// #if val == none [
|
||||||
|
/// Yep, it's none.
|
||||||
|
/// ]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note that `type` will return [`content`] for all document elements. To
|
||||||
|
/// programmatically determine which kind of content you are dealing with, see
|
||||||
|
/// [`content.func`].
|
||||||
#[ty(scope, cast)]
|
#[ty(scope, cast)]
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct Type(Static<NativeTypeData>);
|
pub struct Type(Static<NativeTypeData>);
|
||||||
|
@ -229,10 +229,10 @@ impl Counter {
|
|||||||
if self.is_page() {
|
if self.is_page() {
|
||||||
let at_delta =
|
let at_delta =
|
||||||
engine.introspector.page(location).get().saturating_sub(at_page.get());
|
engine.introspector.page(location).get().saturating_sub(at_page.get());
|
||||||
at_state.step(NonZeroUsize::ONE, at_delta);
|
at_state.step(NonZeroUsize::ONE, at_delta as u64);
|
||||||
let final_delta =
|
let final_delta =
|
||||||
engine.introspector.pages().get().saturating_sub(final_page.get());
|
engine.introspector.pages().get().saturating_sub(final_page.get());
|
||||||
final_state.step(NonZeroUsize::ONE, final_delta);
|
final_state.step(NonZeroUsize::ONE, final_delta as u64);
|
||||||
}
|
}
|
||||||
Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
|
Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
|
||||||
}
|
}
|
||||||
@ -250,7 +250,7 @@ impl Counter {
|
|||||||
if self.is_page() {
|
if self.is_page() {
|
||||||
let delta =
|
let delta =
|
||||||
engine.introspector.page(location).get().saturating_sub(page.get());
|
engine.introspector.page(location).get().saturating_sub(page.get());
|
||||||
state.step(NonZeroUsize::ONE, delta);
|
state.step(NonZeroUsize::ONE, delta as u64);
|
||||||
}
|
}
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
@ -319,7 +319,7 @@ impl Counter {
|
|||||||
|
|
||||||
let delta = page.get() - prev.get();
|
let delta = page.get() - prev.get();
|
||||||
if delta > 0 {
|
if delta > 0 {
|
||||||
state.step(NonZeroUsize::ONE, delta);
|
state.step(NonZeroUsize::ONE, delta as u64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,7 +500,7 @@ impl Counter {
|
|||||||
let (mut state, page) = sequence.last().unwrap().clone();
|
let (mut state, page) = sequence.last().unwrap().clone();
|
||||||
if self.is_page() {
|
if self.is_page() {
|
||||||
let delta = engine.introspector.pages().get().saturating_sub(page.get());
|
let delta = engine.introspector.pages().get().saturating_sub(page.get());
|
||||||
state.step(NonZeroUsize::ONE, delta);
|
state.step(NonZeroUsize::ONE, delta as u64);
|
||||||
}
|
}
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
@ -616,13 +616,13 @@ pub trait Count {
|
|||||||
|
|
||||||
/// Counts through elements with different levels.
|
/// Counts through elements with different levels.
|
||||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
pub struct CounterState(pub SmallVec<[usize; 3]>);
|
pub struct CounterState(pub SmallVec<[u64; 3]>);
|
||||||
|
|
||||||
impl CounterState {
|
impl CounterState {
|
||||||
/// Get the initial counter state for the key.
|
/// Get the initial counter state for the key.
|
||||||
pub fn init(page: bool) -> Self {
|
pub fn init(page: bool) -> Self {
|
||||||
// Special case, because pages always start at one.
|
// Special case, because pages always start at one.
|
||||||
Self(smallvec![usize::from(page)])
|
Self(smallvec![u64::from(page)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advance the counter and return the numbers for the given heading.
|
/// Advance the counter and return the numbers for the given heading.
|
||||||
@ -645,7 +645,7 @@ impl CounterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Advance the number of the given level by the specified amount.
|
/// Advance the number of the given level by the specified amount.
|
||||||
pub fn step(&mut self, level: NonZeroUsize, by: usize) {
|
pub fn step(&mut self, level: NonZeroUsize, by: u64) {
|
||||||
let level = level.get();
|
let level = level.get();
|
||||||
|
|
||||||
while self.0.len() < level {
|
while self.0.len() < level {
|
||||||
@ -657,7 +657,7 @@ impl CounterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the first number of the state.
|
/// Get the first number of the state.
|
||||||
pub fn first(&self) -> usize {
|
pub fn first(&self) -> u64 {
|
||||||
self.0.first().copied().unwrap_or(1)
|
self.0.first().copied().unwrap_or(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,7 +675,7 @@ impl CounterState {
|
|||||||
cast! {
|
cast! {
|
||||||
CounterState,
|
CounterState,
|
||||||
self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()),
|
self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()),
|
||||||
num: usize => Self(smallvec![num]),
|
num: u64 => Self(smallvec![num]),
|
||||||
array: Array => Self(array
|
array: Array => Self(array
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Value::cast)
|
.map(Value::cast)
|
||||||
@ -758,7 +758,7 @@ impl Show for Packed<CounterDisplayElem> {
|
|||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct ManualPageCounter {
|
pub struct ManualPageCounter {
|
||||||
physical: NonZeroUsize,
|
physical: NonZeroUsize,
|
||||||
logical: usize,
|
logical: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManualPageCounter {
|
impl ManualPageCounter {
|
||||||
@ -773,7 +773,7 @@ impl ManualPageCounter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current logical page counter state.
|
/// Get the current logical page counter state.
|
||||||
pub fn logical(&self) -> usize {
|
pub fn logical(&self) -> u64 {
|
||||||
self.logical
|
self.logical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use typst_utils::NonZeroExt;
|
|||||||
|
|
||||||
use crate::diag::{bail, StrResult};
|
use crate::diag::{bail, StrResult};
|
||||||
use crate::foundations::{Content, Label, Repr, Selector};
|
use crate::foundations::{Content, Label, Repr, Selector};
|
||||||
use crate::html::{HtmlElement, HtmlNode};
|
use crate::html::HtmlNode;
|
||||||
use crate::introspection::{Location, Tag};
|
use crate::introspection::{Location, Tag};
|
||||||
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
|
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
|
||||||
use crate::model::Numbering;
|
use crate::model::Numbering;
|
||||||
@ -55,8 +55,8 @@ impl Introspector {
|
|||||||
|
|
||||||
/// Creates an introspector for HTML.
|
/// Creates an introspector for HTML.
|
||||||
#[typst_macros::time(name = "introspect html")]
|
#[typst_macros::time(name = "introspect html")]
|
||||||
pub fn html(root: &HtmlElement) -> Self {
|
pub fn html(output: &[HtmlNode]) -> Self {
|
||||||
IntrospectorBuilder::new().build_html(root)
|
IntrospectorBuilder::new().build_html(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over all locatable elements.
|
/// Iterates over all locatable elements.
|
||||||
@ -392,9 +392,9 @@ impl IntrospectorBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build an introspector for an HTML document.
|
/// Build an introspector for an HTML document.
|
||||||
fn build_html(mut self, root: &HtmlElement) -> Introspector {
|
fn build_html(mut self, output: &[HtmlNode]) -> Introspector {
|
||||||
let mut elems = Vec::new();
|
let mut elems = Vec::new();
|
||||||
self.discover_in_html(&mut elems, root);
|
self.discover_in_html(&mut elems, output);
|
||||||
self.finalize(elems)
|
self.finalize(elems)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,16 +434,16 @@ impl IntrospectorBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the tags in the HTML element.
|
/// Processes the tags in the HTML element.
|
||||||
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) {
|
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, nodes: &[HtmlNode]) {
|
||||||
for child in &elem.children {
|
for node in nodes {
|
||||||
match child {
|
match node {
|
||||||
HtmlNode::Tag(tag) => self.discover_in_tag(
|
HtmlNode::Tag(tag) => self.discover_in_tag(
|
||||||
sink,
|
sink,
|
||||||
tag,
|
tag,
|
||||||
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
||||||
),
|
),
|
||||||
HtmlNode::Text(_, _) => {}
|
HtmlNode::Text(_, _) => {}
|
||||||
HtmlNode::Element(elem) => self.discover_in_html(sink, elem),
|
HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
|
||||||
HtmlNode::Frame(frame) => self.discover_in_frame(
|
HtmlNode::Frame(frame) => self.discover_in_frame(
|
||||||
sink,
|
sink,
|
||||||
frame,
|
frame,
|
||||||
|
@ -50,6 +50,42 @@ impl Dir {
|
|||||||
pub const TTB: Self = Self::TTB;
|
pub const TTB: Self = Self::TTB;
|
||||||
pub const BTT: Self = Self::BTT;
|
pub const BTT: Self = Self::BTT;
|
||||||
|
|
||||||
|
/// Returns a direction from a starting point.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// direction.from(left) \
|
||||||
|
/// direction.from(right) \
|
||||||
|
/// direction.from(top) \
|
||||||
|
/// direction.from(bottom)
|
||||||
|
/// ```
|
||||||
|
#[func]
|
||||||
|
pub const fn from(side: Side) -> Dir {
|
||||||
|
match side {
|
||||||
|
Side::Left => Self::LTR,
|
||||||
|
Side::Right => Self::RTL,
|
||||||
|
Side::Top => Self::TTB,
|
||||||
|
Side::Bottom => Self::BTT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a direction from an end point.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// direction.to(left) \
|
||||||
|
/// direction.to(right) \
|
||||||
|
/// direction.to(top) \
|
||||||
|
/// direction.to(bottom)
|
||||||
|
/// ```
|
||||||
|
#[func]
|
||||||
|
pub const fn to(side: Side) -> Dir {
|
||||||
|
match side {
|
||||||
|
Side::Right => Self::LTR,
|
||||||
|
Side::Left => Self::RTL,
|
||||||
|
Side::Bottom => Self::TTB,
|
||||||
|
Side::Top => Self::BTT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The axis this direction belongs to, either `{"horizontal"}` or
|
/// The axis this direction belongs to, either `{"horizontal"}` or
|
||||||
/// `{"vertical"}`.
|
/// `{"vertical"}`.
|
||||||
///
|
///
|
||||||
@ -65,6 +101,22 @@ impl Dir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The corresponding sign, for use in calculations.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #ltr.sign() \
|
||||||
|
/// #rtl.sign() \
|
||||||
|
/// #ttb.sign() \
|
||||||
|
/// #btt.sign()
|
||||||
|
/// ```
|
||||||
|
#[func]
|
||||||
|
pub const fn sign(self) -> i64 {
|
||||||
|
match self {
|
||||||
|
Self::LTR | Self::TTB => 1,
|
||||||
|
Self::RTL | Self::BTT => -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The start point of this direction, as an alignment.
|
/// The start point of this direction, as an alignment.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,8 @@ use crate::layout::{BlockElem, Size};
|
|||||||
/// #let text = lorem(30)
|
/// #let text = lorem(30)
|
||||||
/// #layout(size => [
|
/// #layout(size => [
|
||||||
/// #let (height,) = measure(
|
/// #let (height,) = measure(
|
||||||
/// block(width: size.width, text),
|
/// width: size.width,
|
||||||
|
/// text,
|
||||||
/// )
|
/// )
|
||||||
/// This text is #height high with
|
/// This text is #height high with
|
||||||
/// the current page width: \
|
/// the current page width: \
|
||||||
|
@ -75,9 +75,10 @@ pub struct PageElem {
|
|||||||
/// The height of the page.
|
/// The height of the page.
|
||||||
///
|
///
|
||||||
/// If this is set to `{auto}`, page breaks can only be triggered manually
|
/// If this is set to `{auto}`, page breaks can only be triggered manually
|
||||||
/// by inserting a [page break]($pagebreak). Most examples throughout this
|
/// by inserting a [page break]($pagebreak) or by adding another non-empty
|
||||||
/// documentation use `{auto}` for the height of the page to dynamically
|
/// page set rule. Most examples throughout this documentation use `{auto}`
|
||||||
/// grow and shrink to fit their content.
|
/// for the height of the page to dynamically grow and shrink to fit their
|
||||||
|
/// content.
|
||||||
#[resolve]
|
#[resolve]
|
||||||
#[parse(
|
#[parse(
|
||||||
args.named("height")?
|
args.named("height")?
|
||||||
@ -483,7 +484,7 @@ pub struct Page {
|
|||||||
pub supplement: Content,
|
pub supplement: Content,
|
||||||
/// The logical page number (controlled by `counter(page)` and may thus not
|
/// The logical page number (controlled by `counter(page)` and may thus not
|
||||||
/// match the physical number).
|
/// match the physical number).
|
||||||
pub number: usize,
|
pub number: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
@ -8,15 +8,35 @@ use crate::foundations::{repr, ty, Repr};
|
|||||||
|
|
||||||
/// A ratio of a whole.
|
/// A ratio of a whole.
|
||||||
///
|
///
|
||||||
/// Written as a number, followed by a percent sign.
|
/// A ratio is written as a number, followed by a percent sign. Ratios most
|
||||||
|
/// often appear as part of a [relative length]($relative), to specify the size
|
||||||
|
/// of some layout element relative to the page or some container.
|
||||||
///
|
///
|
||||||
/// # Example
|
|
||||||
/// ```example
|
/// ```example
|
||||||
/// #set align(center)
|
/// #rect(width: 25%)
|
||||||
/// #scale(x: 150%)[
|
|
||||||
/// Scaled apart.
|
|
||||||
/// ]
|
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// However, they can also describe any other property that is relative to some
|
||||||
|
/// base, e.g. an amount of [horizontal scaling]($scale.x) or the
|
||||||
|
/// [height of parentheses]($math.lr.size) relative to the height of the content
|
||||||
|
/// they enclose.
|
||||||
|
///
|
||||||
|
/// # Scripting
|
||||||
|
/// Within your own code, you can use ratios as you like. You can multiply them
|
||||||
|
/// with various other types as shown below:
|
||||||
|
///
|
||||||
|
/// | Multiply by | Example | Result |
|
||||||
|
/// |-----------------|-------------------------|-----------------|
|
||||||
|
/// | [`ratio`] | `{27% * 10%}` | `{2.7%}` |
|
||||||
|
/// | [`length`] | `{27% * 100pt}` | `{27pt}` |
|
||||||
|
/// | [`relative`] | `{27% * (10% + 100pt)}` | `{2.7% + 27pt}` |
|
||||||
|
/// | [`angle`] | `{27% * 100deg}` | `{27deg}` |
|
||||||
|
/// | [`int`] | `{27% * 2}` | `{54%}` |
|
||||||
|
/// | [`float`] | `{27% * 0.37037}` | `{10%}` |
|
||||||
|
/// | [`fraction`] | `{27% * 3fr}` | `{0.81fr}` |
|
||||||
|
///
|
||||||
|
/// When ratios are displayed in the document, they are rounded to two
|
||||||
|
/// significant digits for readability.
|
||||||
#[ty(cast)]
|
#[ty(cast)]
|
||||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub struct Ratio(Scalar);
|
pub struct Ratio(Scalar);
|
||||||
|
@ -14,17 +14,58 @@ use crate::layout::{Abs, Em, Length, Ratio};
|
|||||||
/// addition and subtraction of a length and a ratio. Wherever a relative length
|
/// addition and subtraction of a length and a ratio. Wherever a relative length
|
||||||
/// is expected, you can also use a bare length or ratio.
|
/// is expected, you can also use a bare length or ratio.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Relative to the page
|
||||||
/// ```example
|
/// A common use case is setting the width or height of a layout element (e.g.,
|
||||||
/// #rect(width: 100% - 50pt)
|
/// [block], [rect], etc.) as a certain percentage of the width of the page.
|
||||||
|
/// Here, the rectangle's width is set to `{25%}`, so it takes up one fourth of
|
||||||
|
/// the page's _inner_ width (the width minus margins).
|
||||||
///
|
///
|
||||||
/// #(100% - 50pt).length \
|
/// ```example
|
||||||
/// #(100% - 50pt).ratio
|
/// #rect(width: 25%)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// Bare lengths or ratios are always valid where relative lengths are expected,
|
||||||
|
/// but the two can also be freely mixed:
|
||||||
|
/// ```example
|
||||||
|
/// #rect(width: 25% + 1cm)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If you're trying to size an element so that it takes up the page's _full_
|
||||||
|
/// width, you have a few options (this highly depends on your exact use case):
|
||||||
|
///
|
||||||
|
/// 1. Set page margins to `{0pt}` (`[#set page(margin: 0pt)]`)
|
||||||
|
/// 2. Multiply the ratio by the known full page width (`{21cm * 69%}`)
|
||||||
|
/// 3. Use padding which will negate the margins (`[#pad(x: -2.5cm, ...)]`)
|
||||||
|
/// 4. Use the page [background](page.background) or
|
||||||
|
/// [foreground](page.foreground) field as those don't take margins into
|
||||||
|
/// account (note that it will render the content outside of the document
|
||||||
|
/// flow, see [place] to control the content position)
|
||||||
|
///
|
||||||
|
/// # Relative to a container
|
||||||
|
/// When a layout element (e.g. a [rect]) is nested in another layout container
|
||||||
|
/// (e.g. a [block]) instead of being a direct descendant of the page, relative
|
||||||
|
/// widths become relative to the container:
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #block(
|
||||||
|
/// width: 100pt,
|
||||||
|
/// fill: aqua,
|
||||||
|
/// rect(width: 50%),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Scripting
|
||||||
|
/// You can multiply relative lengths by [ratios]($ratio), [integers]($int), and
|
||||||
|
/// [floats]($float).
|
||||||
|
///
|
||||||
/// A relative length has the following fields:
|
/// A relative length has the following fields:
|
||||||
/// - `length`: Its length component.
|
/// - `length`: Its length component.
|
||||||
/// - `ratio`: Its ratio component.
|
/// - `ratio`: Its ratio component.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #(100% - 50pt).length \
|
||||||
|
/// #(100% - 50pt).ratio
|
||||||
|
/// ```
|
||||||
#[ty(cast, name = "relative", title = "Relative Length")]
|
#[ty(cast, name = "relative", title = "Relative Length")]
|
||||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct Rel<T: Numeric = Length> {
|
pub struct Rel<T: Numeric = Length> {
|
||||||
|
@ -307,6 +307,20 @@ impl Transform {
|
|||||||
Self { sx, sy, ..Self::identity() }
|
Self { sx, sy, ..Self::identity() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A scale transform at a specific position.
|
||||||
|
pub fn scale_at(sx: Ratio, sy: Ratio, px: Abs, py: Abs) -> Self {
|
||||||
|
Self::translate(px, py)
|
||||||
|
.pre_concat(Self::scale(sx, sy))
|
||||||
|
.pre_concat(Self::translate(-px, -py))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rotate transform at a specific position.
|
||||||
|
pub fn rotate_at(angle: Angle, px: Abs, py: Abs) -> Self {
|
||||||
|
Self::translate(px, py)
|
||||||
|
.pre_concat(Self::rotate(angle))
|
||||||
|
.pre_concat(Self::translate(-px, -py))
|
||||||
|
}
|
||||||
|
|
||||||
/// A rotate transform.
|
/// A rotate transform.
|
||||||
pub fn rotate(angle: Angle) -> Self {
|
pub fn rotate(angle: Angle) -> Self {
|
||||||
let cos = Ratio::new(angle.cos());
|
let cos = Ratio::new(angle.cos());
|
||||||
|
@ -20,9 +20,7 @@ use crate::loading::{DataSource, Load};
|
|||||||
#[func(scope, title = "CBOR")]
|
#[func(scope, title = "CBOR")]
|
||||||
pub fn cbor(
|
pub fn cbor(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// A path to a CBOR file or raw CBOR bytes.
|
/// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
let data = source.load(engine.world)?;
|
let data = source.load(engine.world)?;
|
||||||
|
@ -26,9 +26,7 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
#[func(scope, title = "CSV")]
|
#[func(scope, title = "CSV")]
|
||||||
pub fn csv(
|
pub fn csv(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// Path to a CSV file or raw CSV bytes.
|
/// A [path]($syntax/#paths) to a CSV file or raw CSV bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
/// The delimiter that separates columns in the CSV file.
|
/// The delimiter that separates columns in the CSV file.
|
||||||
/// Must be a single ASCII character.
|
/// Must be a single ASCII character.
|
||||||
|
@ -51,9 +51,7 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
#[func(scope, title = "JSON")]
|
#[func(scope, title = "JSON")]
|
||||||
pub fn json(
|
pub fn json(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// Path to a JSON file or raw JSON bytes.
|
/// A [path]($syntax/#paths) to a JSON file or raw JSON bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
let data = source.load(engine.world)?;
|
let data = source.load(engine.world)?;
|
||||||
|
@ -29,9 +29,7 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
#[func(scope, title = "TOML")]
|
#[func(scope, title = "TOML")]
|
||||||
pub fn toml(
|
pub fn toml(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// A path to a TOML file or raw TOML bytes.
|
/// A [path]($syntax/#paths) to a TOML file or raw TOML bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
let data = source.load(engine.world)?;
|
let data = source.load(engine.world)?;
|
||||||
|
@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
/// let author = find-child(elem, "author")
|
/// let author = find-child(elem, "author")
|
||||||
/// let pars = find-child(elem, "content")
|
/// let pars = find-child(elem, "content")
|
||||||
///
|
///
|
||||||
/// heading(title.children.first())
|
/// [= #title.children.first()]
|
||||||
/// text(10pt, weight: "medium")[
|
/// text(10pt, weight: "medium")[
|
||||||
/// Published by
|
/// Published by
|
||||||
/// #author.children.first()
|
/// #author.children.first()
|
||||||
/// ]
|
/// ]
|
||||||
///
|
///
|
||||||
/// for p in pars.children {
|
/// for p in pars.children {
|
||||||
/// if (type(p) == "dictionary") {
|
/// if type(p) == dictionary {
|
||||||
/// parbreak()
|
/// parbreak()
|
||||||
/// p.children.first()
|
/// p.children.first()
|
||||||
/// }
|
/// }
|
||||||
@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
///
|
///
|
||||||
/// #let data = xml("example.xml")
|
/// #let data = xml("example.xml")
|
||||||
/// #for elem in data.first().children {
|
/// #for elem in data.first().children {
|
||||||
/// if (type(elem) == "dictionary") {
|
/// if type(elem) == dictionary {
|
||||||
/// article(elem)
|
/// article(elem)
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
@ -58,9 +58,7 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
#[func(scope, title = "XML")]
|
#[func(scope, title = "XML")]
|
||||||
pub fn xml(
|
pub fn xml(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// A path to an XML file or raw XML bytes.
|
/// A [path]($syntax/#paths) to an XML file or raw XML bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
let data = source.load(engine.world)?;
|
let data = source.load(engine.world)?;
|
||||||
|
@ -41,9 +41,7 @@ use crate::loading::{DataSource, Load, Readable};
|
|||||||
#[func(scope, title = "YAML")]
|
#[func(scope, title = "YAML")]
|
||||||
pub fn yaml(
|
pub fn yaml(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
/// A path to a YAML file or raw YAML bytes.
|
/// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
source: Spanned<DataSource>,
|
source: Spanned<DataSource>,
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
let data = source.load(engine.world)?;
|
let data = source.load(engine.world)?;
|
||||||
|
@ -13,8 +13,8 @@ use crate::math::Mathy;
|
|||||||
/// ```
|
/// ```
|
||||||
#[elem(Mathy)]
|
#[elem(Mathy)]
|
||||||
pub struct AccentElem {
|
pub struct AccentElem {
|
||||||
/// The base to which the accent is applied.
|
/// The base to which the accent is applied. May consist of multiple
|
||||||
/// May consist of multiple letters.
|
/// letters.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
/// $arrow(A B C)$
|
/// $arrow(A B C)$
|
||||||
@ -51,9 +51,24 @@ pub struct AccentElem {
|
|||||||
pub accent: Accent,
|
pub accent: Accent,
|
||||||
|
|
||||||
/// The size of the accent, relative to the width of the base.
|
/// The size of the accent, relative to the width of the base.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// $dash(A, size: #150%)$
|
||||||
|
/// ```
|
||||||
#[resolve]
|
#[resolve]
|
||||||
#[default(Rel::one())]
|
#[default(Rel::one())]
|
||||||
pub size: Rel<Length>,
|
pub size: Rel<Length>,
|
||||||
|
|
||||||
|
/// Whether to remove the dot on top of lowercase i and j when adding a top
|
||||||
|
/// accent.
|
||||||
|
///
|
||||||
|
/// This enables the `dtls` OpenType feature.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// $hat(dotless: #false, i)$
|
||||||
|
/// ```
|
||||||
|
#[default(true)]
|
||||||
|
pub dotless: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An accent character.
|
/// An accent character.
|
||||||
@ -103,11 +118,18 @@ macro_rules! accents {
|
|||||||
/// The size of the accent, relative to the width of the base.
|
/// The size of the accent, relative to the width of the base.
|
||||||
#[named]
|
#[named]
|
||||||
size: Option<Rel<Length>>,
|
size: Option<Rel<Length>>,
|
||||||
|
/// Whether to remove the dot on top of lowercase i and j when
|
||||||
|
/// adding a top accent.
|
||||||
|
#[named]
|
||||||
|
dotless: Option<bool>,
|
||||||
) -> Content {
|
) -> Content {
|
||||||
let mut accent = AccentElem::new(base, Accent::new($primary));
|
let mut accent = AccentElem::new(base, Accent::new($primary));
|
||||||
if let Some(size) = size {
|
if let Some(size) = size {
|
||||||
accent = accent.with_size(size);
|
accent = accent.with_size(size);
|
||||||
}
|
}
|
||||||
|
if let Some(dotless) = dotless {
|
||||||
|
accent = accent.with_dotless(dotless);
|
||||||
|
}
|
||||||
accent.pack()
|
accent.pack()
|
||||||
}
|
}
|
||||||
)+
|
)+
|
||||||
|
@ -15,7 +15,7 @@ use crate::math::Mathy;
|
|||||||
/// # Syntax
|
/// # Syntax
|
||||||
/// This function also has dedicated syntax: Use a slash to turn neighbouring
|
/// This function also has dedicated syntax: Use a slash to turn neighbouring
|
||||||
/// expressions into a fraction. Multiple atoms can be grouped into a single
|
/// expressions into a fraction. Multiple atoms can be grouped into a single
|
||||||
/// expression using round grouping parenthesis. Such parentheses are removed
|
/// expression using round grouping parentheses. Such parentheses are removed
|
||||||
/// from the output, but you can nest multiple to force them.
|
/// from the output, but you can nest multiple to force them.
|
||||||
#[elem(title = "Fraction", Mathy)]
|
#[elem(title = "Fraction", Mathy)]
|
||||||
pub struct FracElem {
|
pub struct FracElem {
|
||||||
|
@ -6,7 +6,7 @@ use std::num::NonZeroUsize;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use comemo::Tracked;
|
use comemo::{Track, Tracked};
|
||||||
use ecow::{eco_format, EcoString, EcoVec};
|
use ecow::{eco_format, EcoString, EcoVec};
|
||||||
use hayagriva::archive::ArchivedStyle;
|
use hayagriva::archive::ArchivedStyle;
|
||||||
use hayagriva::io::BibLaTeXError;
|
use hayagriva::io::BibLaTeXError;
|
||||||
@ -20,7 +20,7 @@ use typst_syntax::{Span, Spanned};
|
|||||||
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
|
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
|
||||||
|
|
||||||
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
|
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::{Engine, Sink};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
|
elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
|
||||||
OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles,
|
OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles,
|
||||||
@ -94,7 +94,7 @@ pub struct BibliographyElem {
|
|||||||
/// - A path string to load a bibliography file from the given path. For
|
/// - A path string to load a bibliography file from the given path. For
|
||||||
/// more details about paths, see the [Paths section]($syntax/#paths).
|
/// more details about paths, see the [Paths section]($syntax/#paths).
|
||||||
/// - Raw bytes from which the bibliography should be decoded.
|
/// - Raw bytes from which the bibliography should be decoded.
|
||||||
/// - An array where each item is one the above.
|
/// - An array where each item is one of the above.
|
||||||
#[required]
|
#[required]
|
||||||
#[parse(
|
#[parse(
|
||||||
let sources = args.expect("sources")?;
|
let sources = args.expect("sources")?;
|
||||||
@ -999,6 +999,8 @@ impl ElemRenderer<'_> {
|
|||||||
(self.routines.eval_string)(
|
(self.routines.eval_string)(
|
||||||
self.routines,
|
self.routines,
|
||||||
self.world,
|
self.world,
|
||||||
|
// TODO: propagate warnings
|
||||||
|
Sink::new().track_mut(),
|
||||||
math,
|
math,
|
||||||
self.span,
|
self.span,
|
||||||
EvalMode::Math,
|
EvalMode::Math,
|
||||||
|
@ -129,7 +129,7 @@ pub struct EnumElem {
|
|||||||
/// [Ahead],
|
/// [Ahead],
|
||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
pub start: Smart<usize>,
|
pub start: Smart<u64>,
|
||||||
|
|
||||||
/// Whether to display the full numbering, including the numbers of
|
/// Whether to display the full numbering, including the numbers of
|
||||||
/// all parent enumerations.
|
/// all parent enumerations.
|
||||||
@ -217,7 +217,7 @@ pub struct EnumElem {
|
|||||||
#[internal]
|
#[internal]
|
||||||
#[fold]
|
#[fold]
|
||||||
#[ghost]
|
#[ghost]
|
||||||
pub parents: SmallVec<[usize; 4]>,
|
pub parents: SmallVec<[u64; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
@ -259,10 +259,11 @@ impl Show for Packed<EnumElem> {
|
|||||||
.spanned(self.span());
|
.spanned(self.span());
|
||||||
|
|
||||||
if tight {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let spacing = self
|
||||||
let spacing =
|
.spacing(styles)
|
||||||
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
.unwrap_or_else(|| ParElem::leading_in(styles).into());
|
||||||
realized = spacing + realized;
|
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
|
||||||
|
realized = v + realized;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(realized)
|
Ok(realized)
|
||||||
@ -274,7 +275,7 @@ impl Show for Packed<EnumElem> {
|
|||||||
pub struct EnumItem {
|
pub struct EnumItem {
|
||||||
/// The item's number.
|
/// The item's number.
|
||||||
#[positional]
|
#[positional]
|
||||||
pub number: Option<usize>,
|
pub number: Option<u64>,
|
||||||
|
|
||||||
/// The item's body.
|
/// The item's body.
|
||||||
#[required]
|
#[required]
|
||||||
|
@ -457,7 +457,7 @@ impl Outlinable for Packed<FigureElem> {
|
|||||||
/// customize the appearance of captions for all figures or figures of a
|
/// customize the appearance of captions for all figures or figures of a
|
||||||
/// specific kind.
|
/// specific kind.
|
||||||
///
|
///
|
||||||
/// In addition to its `pos` and `body`, the `caption` also provides the
|
/// In addition to its `position` and `body`, the `caption` also provides the
|
||||||
/// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These
|
/// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These
|
||||||
/// parts can be used in [`where`]($function.where) selectors and show rules to
|
/// parts can be used in [`where`]($function.where) selectors and show rules to
|
||||||
/// build a completely custom caption.
|
/// build a completely custom caption.
|
||||||
|
@ -11,7 +11,7 @@ use crate::foundations::{
|
|||||||
use crate::html::{attr, tag, HtmlElem};
|
use crate::html::{attr, tag, HtmlElem};
|
||||||
use crate::introspection::Location;
|
use crate::introspection::Location;
|
||||||
use crate::layout::Position;
|
use crate::layout::Position;
|
||||||
use crate::text::{Hyphenate, TextElem};
|
use crate::text::TextElem;
|
||||||
|
|
||||||
/// Links to a URL or a location in the document.
|
/// Links to a URL or a location in the document.
|
||||||
///
|
///
|
||||||
@ -138,7 +138,7 @@ impl Show for Packed<LinkElem> {
|
|||||||
impl ShowSet for Packed<LinkElem> {
|
impl ShowSet for Packed<LinkElem> {
|
||||||
fn show_set(&self, _: StyleChain) -> Styles {
|
fn show_set(&self, _: StyleChain) -> Styles {
|
||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
|
out.set(TextElem::set_hyphenate(Smart::Custom(false)));
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,10 +166,11 @@ impl Show for Packed<ListElem> {
|
|||||||
.spanned(self.span());
|
.spanned(self.span());
|
||||||
|
|
||||||
if tight {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let spacing = self
|
||||||
let spacing =
|
.spacing(styles)
|
||||||
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
.unwrap_or_else(|| ParElem::leading_in(styles).into());
|
||||||
realized = spacing + realized;
|
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
|
||||||
|
realized = v + realized;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(realized)
|
Ok(realized)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use chinese_number::{
|
use chinese_number::{
|
||||||
from_usize_to_chinese_ten_thousand as usize_to_chinese, ChineseCase, ChineseVariant,
|
from_u64_to_chinese_ten_thousand as u64_to_chinese, ChineseCase, ChineseVariant,
|
||||||
};
|
};
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::{eco_format, EcoString, EcoVec};
|
use ecow::{eco_format, EcoString, EcoVec};
|
||||||
@ -85,7 +85,7 @@ pub fn numbering(
|
|||||||
/// If `numbering` is a pattern and more numbers than counting symbols are
|
/// If `numbering` is a pattern and more numbers than counting symbols are
|
||||||
/// given, the last counting symbol with its prefix is repeated.
|
/// given, the last counting symbol with its prefix is repeated.
|
||||||
#[variadic]
|
#[variadic]
|
||||||
numbers: Vec<usize>,
|
numbers: Vec<u64>,
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
numbering.apply(engine, context, &numbers)
|
numbering.apply(engine, context, &numbers)
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ impl Numbering {
|
|||||||
&self,
|
&self,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
context: Tracked<Context>,
|
context: Tracked<Context>,
|
||||||
numbers: &[usize],
|
numbers: &[u64],
|
||||||
) -> SourceResult<Value> {
|
) -> SourceResult<Value> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
|
Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
|
||||||
@ -156,7 +156,7 @@ pub struct NumberingPattern {
|
|||||||
|
|
||||||
impl NumberingPattern {
|
impl NumberingPattern {
|
||||||
/// Apply the pattern to the given number.
|
/// Apply the pattern to the given number.
|
||||||
pub fn apply(&self, numbers: &[usize]) -> EcoString {
|
pub fn apply(&self, numbers: &[u64]) -> EcoString {
|
||||||
let mut fmt = EcoString::new();
|
let mut fmt = EcoString::new();
|
||||||
let mut numbers = numbers.iter();
|
let mut numbers = numbers.iter();
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ impl NumberingPattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Apply only the k-th segment of the pattern to a number.
|
/// Apply only the k-th segment of the pattern to a number.
|
||||||
pub fn apply_kth(&self, k: usize, number: usize) -> EcoString {
|
pub fn apply_kth(&self, k: usize, number: u64) -> EcoString {
|
||||||
let mut fmt = EcoString::new();
|
let mut fmt = EcoString::new();
|
||||||
if let Some((prefix, _)) = self.pieces.first() {
|
if let Some((prefix, _)) = self.pieces.first() {
|
||||||
fmt.push_str(prefix);
|
fmt.push_str(prefix);
|
||||||
@ -379,7 +379,7 @@ impl NumberingKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the numbering to the given number.
|
/// Apply the numbering to the given number.
|
||||||
pub fn apply(self, n: usize) -> EcoString {
|
pub fn apply(self, n: u64) -> EcoString {
|
||||||
match self {
|
match self {
|
||||||
Self::Arabic => eco_format!("{n}"),
|
Self::Arabic => eco_format!("{n}"),
|
||||||
Self::LowerRoman => roman_numeral(n, Case::Lower),
|
Self::LowerRoman => roman_numeral(n, Case::Lower),
|
||||||
@ -392,9 +392,10 @@ impl NumberingKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
|
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
|
||||||
let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
|
let n_symbols = SYMBOLS.len() as u64;
|
||||||
let amount = ((n - 1) / SYMBOLS.len()) + 1;
|
let symbol = SYMBOLS[((n - 1) % n_symbols) as usize];
|
||||||
std::iter::repeat(symbol).take(amount).collect()
|
let amount = ((n - 1) / n_symbols) + 1;
|
||||||
|
std::iter::repeat_n(symbol, amount.try_into().unwrap()).collect()
|
||||||
}
|
}
|
||||||
Self::Hebrew => hebrew_numeral(n),
|
Self::Hebrew => hebrew_numeral(n),
|
||||||
|
|
||||||
@ -489,18 +490,16 @@ impl NumberingKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self::LowerSimplifiedChinese => {
|
Self::LowerSimplifiedChinese => {
|
||||||
usize_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into()
|
u64_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into()
|
||||||
}
|
}
|
||||||
Self::UpperSimplifiedChinese => {
|
Self::UpperSimplifiedChinese => {
|
||||||
usize_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into()
|
u64_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into()
|
||||||
}
|
}
|
||||||
Self::LowerTraditionalChinese => {
|
Self::LowerTraditionalChinese => {
|
||||||
usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n)
|
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n).into()
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
Self::UpperTraditionalChinese => {
|
Self::UpperTraditionalChinese => {
|
||||||
usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n)
|
u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into()
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::EasternArabic => decimal('\u{0660}', n),
|
Self::EasternArabic => decimal('\u{0660}', n),
|
||||||
@ -512,7 +511,7 @@ impl NumberingKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stringify an integer to a Hebrew number.
|
/// Stringify an integer to a Hebrew number.
|
||||||
fn hebrew_numeral(mut n: usize) -> EcoString {
|
fn hebrew_numeral(mut n: u64) -> EcoString {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return '-'.into();
|
return '-'.into();
|
||||||
}
|
}
|
||||||
@ -566,7 +565,7 @@ fn hebrew_numeral(mut n: usize) -> EcoString {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stringify an integer to a Roman numeral.
|
/// Stringify an integer to a Roman numeral.
|
||||||
fn roman_numeral(mut n: usize, case: Case) -> EcoString {
|
fn roman_numeral(mut n: u64, case: Case) -> EcoString {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return match case {
|
return match case {
|
||||||
Case::Lower => 'n'.into(),
|
Case::Lower => 'n'.into(),
|
||||||
@ -622,7 +621,7 @@ fn roman_numeral(mut n: usize, case: Case) -> EcoString {
|
|||||||
///
|
///
|
||||||
/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm
|
/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm
|
||||||
/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/
|
/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/
|
||||||
fn greek_numeral(n: usize, case: Case) -> EcoString {
|
fn greek_numeral(n: u64, case: Case) -> EcoString {
|
||||||
let thousands = [
|
let thousands = [
|
||||||
["͵α", "͵Α"],
|
["͵α", "͵Α"],
|
||||||
["͵β", "͵Β"],
|
["͵β", "͵Β"],
|
||||||
@ -683,7 +682,7 @@ fn greek_numeral(n: usize, case: Case) -> EcoString {
|
|||||||
let mut decimal_digits: Vec<usize> = Vec::new();
|
let mut decimal_digits: Vec<usize> = Vec::new();
|
||||||
let mut n = n;
|
let mut n = n;
|
||||||
while n > 0 {
|
while n > 0 {
|
||||||
decimal_digits.push(n % 10);
|
decimal_digits.push((n % 10) as usize);
|
||||||
n /= 10;
|
n /= 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,18 +777,16 @@ fn greek_numeral(n: usize, case: Case) -> EcoString {
|
|||||||
///
|
///
|
||||||
/// You might be familiar with this scheme from the way spreadsheet software
|
/// You might be familiar with this scheme from the way spreadsheet software
|
||||||
/// tends to label its columns.
|
/// tends to label its columns.
|
||||||
fn zeroless<const N_DIGITS: usize>(
|
fn zeroless<const N_DIGITS: usize>(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString {
|
||||||
alphabet: [char; N_DIGITS],
|
|
||||||
mut n: usize,
|
|
||||||
) -> EcoString {
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return '-'.into();
|
return '-'.into();
|
||||||
}
|
}
|
||||||
|
let n_digits = N_DIGITS as u64;
|
||||||
let mut cs = EcoString::new();
|
let mut cs = EcoString::new();
|
||||||
while n > 0 {
|
while n > 0 {
|
||||||
n -= 1;
|
n -= 1;
|
||||||
cs.push(alphabet[n % N_DIGITS]);
|
cs.push(alphabet[(n % n_digits) as usize]);
|
||||||
n /= N_DIGITS;
|
n /= n_digits;
|
||||||
}
|
}
|
||||||
cs.chars().rev().collect()
|
cs.chars().rev().collect()
|
||||||
}
|
}
|
||||||
@ -797,7 +794,7 @@ fn zeroless<const N_DIGITS: usize>(
|
|||||||
/// Stringify a number using a base-10 counting system with a zero digit.
|
/// Stringify a number using a base-10 counting system with a zero digit.
|
||||||
///
|
///
|
||||||
/// This function assumes that the digits occupy contiguous codepoints.
|
/// This function assumes that the digits occupy contiguous codepoints.
|
||||||
fn decimal(start: char, mut n: usize) -> EcoString {
|
fn decimal(start: char, mut n: u64) -> EcoString {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return start.into();
|
return start.into();
|
||||||
}
|
}
|
||||||
|
@ -388,7 +388,7 @@ pub struct OutlineEntry {
|
|||||||
/// space between the entry's body and the page number. When using show
|
/// space between the entry's body and the page number. When using show
|
||||||
/// rules to override outline entries, it is thus recommended to wrap the
|
/// rules to override outline entries, it is thus recommended to wrap the
|
||||||
/// fill in a [`box`] with fractional width, i.e.
|
/// fill in a [`box`] with fractional width, i.e.
|
||||||
/// `{box(width: 1fr, it.fill}`.
|
/// `{box(width: 1fr, it.fill)}`.
|
||||||
///
|
///
|
||||||
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
|
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
|
||||||
/// to tweak the visual weight of the fill.
|
/// to tweak the visual weight of the fill.
|
||||||
@ -623,7 +623,7 @@ impl OutlineEntry {
|
|||||||
|
|
||||||
/// The content which is displayed in place of the referred element at its
|
/// The content which is displayed in place of the referred element at its
|
||||||
/// entry in the outline. For a heading, this is its
|
/// entry in the outline. For a heading, this is its
|
||||||
/// [`body`]($heading.body), for a figure a caption, and for equations it is
|
/// [`body`]($heading.body); for a figure a caption and for equations, it is
|
||||||
/// empty.
|
/// empty.
|
||||||
#[func]
|
#[func]
|
||||||
pub fn body(&self) -> StrResult<Content> {
|
pub fn body(&self) -> StrResult<Content> {
|
||||||
|
@ -161,7 +161,7 @@ impl Show for Packed<QuoteElem> {
|
|||||||
let block = self.block(styles);
|
let block = self.block(styles);
|
||||||
let html = TargetElem::target_in(styles).is_html();
|
let html = TargetElem::target_in(styles).is_html();
|
||||||
|
|
||||||
if self.quotes(styles) == Smart::Custom(true) || !block {
|
if self.quotes(styles).unwrap_or(!block) {
|
||||||
let quotes = SmartQuotes::get(
|
let quotes = SmartQuotes::get(
|
||||||
SmartQuoteElem::quotes_in(styles),
|
SmartQuoteElem::quotes_in(styles),
|
||||||
TextElem::lang_in(styles),
|
TextElem::lang_in(styles),
|
||||||
|
@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
|
|||||||
|
|
||||||
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
|
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
|
||||||
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
|
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
|
||||||
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect();
|
let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
|
||||||
|
|
||||||
let tr = |tag, row: &[Entry]| {
|
let tr = |tag, row: &[Entry]| {
|
||||||
let row = row
|
let row = row
|
||||||
|
@ -189,13 +189,15 @@ impl Show for Packed<TermsElem> {
|
|||||||
.styled(TermsElem::set_within(true));
|
.styled(TermsElem::set_within(true));
|
||||||
|
|
||||||
if tight {
|
if tight {
|
||||||
let leading = ParElem::leading_in(styles);
|
let spacing = self
|
||||||
let spacing = VElem::new(leading.into())
|
.spacing(styles)
|
||||||
|
.unwrap_or_else(|| ParElem::leading_in(styles).into());
|
||||||
|
let v = VElem::new(spacing.into())
|
||||||
.with_weak(true)
|
.with_weak(true)
|
||||||
.with_attach(true)
|
.with_attach(true)
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(span);
|
.spanned(span);
|
||||||
realized = spacing + realized;
|
realized = v + realized;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(realized)
|
Ok(realized)
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
|
use typst_library::foundations::Target;
|
||||||
use typst_syntax::Spanned;
|
use typst_syntax::Spanned;
|
||||||
|
|
||||||
use crate::diag::{At, SourceResult};
|
use crate::diag::{warning, At, SourceResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain};
|
use crate::foundations::{
|
||||||
|
elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem,
|
||||||
|
};
|
||||||
use crate::introspection::Locatable;
|
use crate::introspection::Locatable;
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -32,12 +35,10 @@ use crate::World;
|
|||||||
/// embedded file conforms to PDF/A-1 or PDF/A-2.
|
/// embedded file conforms to PDF/A-1 or PDF/A-2.
|
||||||
#[elem(Show, Locatable)]
|
#[elem(Show, Locatable)]
|
||||||
pub struct EmbedElem {
|
pub struct EmbedElem {
|
||||||
/// Path of the file to be embedded.
|
/// The [path]($syntax/#paths) of the file to be embedded.
|
||||||
///
|
///
|
||||||
/// Must always be specified, but is only read from if no data is provided
|
/// Must always be specified, but is only read from if no data is provided
|
||||||
/// in the following argument.
|
/// in the following argument.
|
||||||
///
|
|
||||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
|
||||||
#[required]
|
#[required]
|
||||||
#[parse(
|
#[parse(
|
||||||
let Spanned { v: path, span } =
|
let Spanned { v: path, span } =
|
||||||
@ -80,7 +81,12 @@ pub struct EmbedElem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Show for Packed<EmbedElem> {
|
impl Show for Packed<EmbedElem> {
|
||||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||||
|
if TargetElem::target_in(styles) == Target::Html {
|
||||||
|
engine
|
||||||
|
.sink
|
||||||
|
.warn(warning!(self.span(), "embed was ignored during HTML export"));
|
||||||
|
}
|
||||||
Ok(Content::empty())
|
Ok(Content::empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ routines! {
|
|||||||
fn eval_string(
|
fn eval_string(
|
||||||
routines: &Routines,
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
|
sink: TrackedMut<Sink>,
|
||||||
string: &str,
|
string: &str,
|
||||||
span: Span,
|
span: Span,
|
||||||
mode: EvalMode,
|
mode: EvalMode,
|
||||||
|
@ -160,7 +160,7 @@ impl FontBook {
|
|||||||
current.variant.weight.distance(variant.weight),
|
current.variant.weight.distance(variant.weight),
|
||||||
);
|
);
|
||||||
|
|
||||||
if best_key.map_or(true, |b| key < b) {
|
if best_key.is_none_or(|b| key < b) {
|
||||||
best = Some(id);
|
best = Some(id);
|
||||||
best_key = Some(key);
|
best_key = Some(key);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ macro_rules! translation {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSLATIONS: [(&str, &str); 38] = [
|
const TRANSLATIONS: [(&str, &str); 39] = [
|
||||||
translation!("ar"),
|
translation!("ar"),
|
||||||
translation!("bg"),
|
translation!("bg"),
|
||||||
translation!("ca"),
|
translation!("ca"),
|
||||||
@ -31,6 +31,7 @@ const TRANSLATIONS: [(&str, &str); 38] = [
|
|||||||
translation!("el"),
|
translation!("el"),
|
||||||
translation!("he"),
|
translation!("he"),
|
||||||
translation!("hu"),
|
translation!("hu"),
|
||||||
|
translation!("id"),
|
||||||
translation!("is"),
|
translation!("is"),
|
||||||
translation!("it"),
|
translation!("it"),
|
||||||
translation!("ja"),
|
translation!("ja"),
|
||||||
@ -82,6 +83,7 @@ impl Lang {
|
|||||||
pub const HEBREW: Self = Self(*b"he ", 2);
|
pub const HEBREW: Self = Self(*b"he ", 2);
|
||||||
pub const HUNGARIAN: Self = Self(*b"hu ", 2);
|
pub const HUNGARIAN: Self = Self(*b"hu ", 2);
|
||||||
pub const ICELANDIC: Self = Self(*b"is ", 2);
|
pub const ICELANDIC: Self = Self(*b"is ", 2);
|
||||||
|
pub const INDONESIAN: Self = Self(*b"id ", 2);
|
||||||
pub const ITALIAN: Self = Self(*b"it ", 2);
|
pub const ITALIAN: Self = Self(*b"it ", 2);
|
||||||
pub const JAPANESE: Self = Self(*b"ja ", 2);
|
pub const JAPANESE: Self = Self(*b"ja ", 2);
|
||||||
pub const LATIN: Self = Self(*b"la ", 2);
|
pub const LATIN: Self = Self(*b"la ", 2);
|
||||||
|
@ -42,7 +42,7 @@ use ttf_parser::Tag;
|
|||||||
use typst_syntax::Spanned;
|
use typst_syntax::Spanned;
|
||||||
use typst_utils::singleton;
|
use typst_utils::singleton;
|
||||||
|
|
||||||
use crate::diag::{bail, warning, HintedStrResult, SourceResult};
|
use crate::diag::{bail, warning, HintedStrResult, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue,
|
cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue,
|
||||||
@ -51,7 +51,6 @@ use crate::foundations::{
|
|||||||
};
|
};
|
||||||
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
|
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
|
||||||
use crate::math::{EquationElem, MathSize};
|
use crate::math::{EquationElem, MathSize};
|
||||||
use crate::model::ParElem;
|
|
||||||
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
|
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -504,9 +503,8 @@ pub struct TextElem {
|
|||||||
/// enabling hyphenation can
|
/// enabling hyphenation can
|
||||||
/// improve justification.
|
/// improve justification.
|
||||||
/// ```
|
/// ```
|
||||||
#[resolve]
|
|
||||||
#[ghost]
|
#[ghost]
|
||||||
pub hyphenate: Hyphenate,
|
pub hyphenate: Smart<bool>,
|
||||||
|
|
||||||
/// The "cost" of various choices when laying out text. A higher cost means
|
/// The "cost" of various choices when laying out text. A higher cost means
|
||||||
/// the layout engine will make the choice less often. Costs are specified
|
/// the layout engine will make the choice less often. Costs are specified
|
||||||
@ -893,9 +891,21 @@ cast! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Font family fallback list.
|
/// Font family fallback list.
|
||||||
|
///
|
||||||
|
/// Must contain at least one font.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
||||||
pub struct FontList(pub Vec<FontFamily>);
|
pub struct FontList(pub Vec<FontFamily>);
|
||||||
|
|
||||||
|
impl FontList {
|
||||||
|
pub fn new(fonts: Vec<FontFamily>) -> StrResult<Self> {
|
||||||
|
if fonts.is_empty() {
|
||||||
|
bail!("font fallback list must not be empty")
|
||||||
|
} else {
|
||||||
|
Ok(Self(fonts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a FontList {
|
impl<'a> IntoIterator for &'a FontList {
|
||||||
type IntoIter = std::slice::Iter<'a, FontFamily>;
|
type IntoIter = std::slice::Iter<'a, FontFamily>;
|
||||||
type Item = &'a FontFamily;
|
type Item = &'a FontFamily;
|
||||||
@ -913,7 +923,7 @@ cast! {
|
|||||||
self.0.into_value()
|
self.0.into_value()
|
||||||
},
|
},
|
||||||
family: FontFamily => Self(vec![family]),
|
family: FontFamily => Self(vec![family]),
|
||||||
values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?),
|
values: Array => Self::new(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a prioritized iterator over the font families.
|
/// Resolve a prioritized iterator over the font families.
|
||||||
@ -1110,27 +1120,6 @@ impl Resolve for TextDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to hyphenate text.
|
|
||||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct Hyphenate(pub Smart<bool>);
|
|
||||||
|
|
||||||
cast! {
|
|
||||||
Hyphenate,
|
|
||||||
self => self.0.into_value(),
|
|
||||||
v: Smart<bool> => Self(v),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resolve for Hyphenate {
|
|
||||||
type Output = bool;
|
|
||||||
|
|
||||||
fn resolve(self, styles: StyleChain) -> Self::Output {
|
|
||||||
match self.0 {
|
|
||||||
Smart::Auto => ParElem::justify_in(styles),
|
|
||||||
Smart::Custom(v) => v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A set of stylistic sets to enable.
|
/// A set of stylistic sets to enable.
|
||||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
|
||||||
pub struct StylisticSets(u32);
|
pub struct StylisticSets(u32);
|
||||||
@ -1403,24 +1392,7 @@ pub fn is_default_ignorable(c: char) -> bool {
|
|||||||
fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
|
fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
|
||||||
let book = engine.world.book();
|
let book = engine.world.book();
|
||||||
for family in &list.v {
|
for family in &list.v {
|
||||||
let found = book.contains_family(family.as_str());
|
if !book.contains_family(family.as_str()) {
|
||||||
if family.as_str() == "linux libertine" {
|
|
||||||
let mut warning = warning!(
|
|
||||||
list.span,
|
|
||||||
"Typst's default font has changed from Linux Libertine to its successor Libertinus Serif";
|
|
||||||
hint: "please set the font to `\"Libertinus Serif\"` instead"
|
|
||||||
);
|
|
||||||
|
|
||||||
if found {
|
|
||||||
warning.hint(
|
|
||||||
"Linux Libertine is available on your system - \
|
|
||||||
you can ignore this warning if you are sure you want to use it",
|
|
||||||
);
|
|
||||||
warning.hint("this warning will be removed in Typst 0.13");
|
|
||||||
}
|
|
||||||
|
|
||||||
engine.sink.warn(warning);
|
|
||||||
} else if !found {
|
|
||||||
engine.sink.warn(warning!(
|
engine.sink.warn(warning!(
|
||||||
list.span,
|
list.span,
|
||||||
"unknown font family: {}",
|
"unknown font family: {}",
|
||||||
|
@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem};
|
|||||||
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
|
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
|
||||||
use crate::loading::{DataSource, Load};
|
use crate::loading::{DataSource, Load};
|
||||||
use crate::model::{Figurable, ParElem};
|
use crate::model::{Figurable, ParElem};
|
||||||
use crate::text::{
|
use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
|
||||||
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
|
|
||||||
};
|
|
||||||
use crate::visualize::Color;
|
use crate::visualize::Color;
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -190,7 +188,7 @@ pub struct RawElem {
|
|||||||
/// - A path string to load a syntax file from the given path. For more
|
/// - A path string to load a syntax file from the given path. For more
|
||||||
/// details about paths, see the [Paths section]($syntax/#paths).
|
/// details about paths, see the [Paths section]($syntax/#paths).
|
||||||
/// - Raw bytes from which the syntax should be decoded.
|
/// - Raw bytes from which the syntax should be decoded.
|
||||||
/// - An array where each item is one the above.
|
/// - An array where each item is one of the above.
|
||||||
///
|
///
|
||||||
/// ````example
|
/// ````example
|
||||||
/// #set raw(syntaxes: "SExpressions.sublime-syntax")
|
/// #set raw(syntaxes: "SExpressions.sublime-syntax")
|
||||||
@ -448,7 +446,11 @@ impl Show for Packed<RawElem> {
|
|||||||
let mut realized = Content::sequence(seq);
|
let mut realized = Content::sequence(seq);
|
||||||
|
|
||||||
if TargetElem::target_in(styles).is_html() {
|
if TargetElem::target_in(styles).is_html() {
|
||||||
return Ok(HtmlElem::new(tag::pre)
|
return Ok(HtmlElem::new(if self.block(styles) {
|
||||||
|
tag::pre
|
||||||
|
} else {
|
||||||
|
tag::code
|
||||||
|
})
|
||||||
.with_body(Some(realized))
|
.with_body(Some(realized))
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(self.span()));
|
.spanned(self.span()));
|
||||||
@ -472,7 +474,7 @@ impl ShowSet for Packed<RawElem> {
|
|||||||
let mut out = Styles::new();
|
let mut out = Styles::new();
|
||||||
out.set(TextElem::set_overhang(false));
|
out.set(TextElem::set_overhang(false));
|
||||||
out.set(TextElem::set_lang(Lang::ENGLISH));
|
out.set(TextElem::set_lang(Lang::ENGLISH));
|
||||||
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
|
out.set(TextElem::set_hyphenate(Smart::Custom(false)));
|
||||||
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
|
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
|
||||||
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
|
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
|
||||||
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));
|
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));
|
||||||
|
@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
|
|||||||
{
|
{
|
||||||
let covers = family.covers();
|
let covers = family.covers();
|
||||||
return text.chars().all(|c| {
|
return text.chars().all(|c| {
|
||||||
covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
|
covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
|
||||||
&& font.ttf().glyph_index(c).is_some()
|
&& font.ttf().glyph_index(c).is_some()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ impl<'s> SmartQuotes<'s> {
|
|||||||
"cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
|
"cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
|
||||||
"da" => ("‘", "’", "“", "”"),
|
"da" => ("‘", "’", "“", "”"),
|
||||||
"fr" | "ru" if alternative => default,
|
"fr" | "ru" if alternative => default,
|
||||||
"fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"),
|
"fr" => ("“", "”", "«\u{202F}", "\u{202F}»"),
|
||||||
"fi" | "sv" if alternative => ("’", "’", "»", "»"),
|
"fi" | "sv" if alternative => ("’", "’", "»", "»"),
|
||||||
"bs" | "fi" | "sv" => ("’", "’", "”", "”"),
|
"bs" | "fi" | "sv" => ("’", "’", "”", "”"),
|
||||||
"it" if alternative => default,
|
"it" if alternative => default,
|
||||||
@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> {
|
|||||||
"el" => ("‘", "’", "«", "»"),
|
"el" => ("‘", "’", "«", "»"),
|
||||||
"he" => ("’", "’", "”", "”"),
|
"he" => ("’", "’", "”", "”"),
|
||||||
"hr" => ("‘", "’", "„", "”"),
|
"hr" => ("‘", "’", "„", "”"),
|
||||||
|
"bg" => ("’", "’", "„", "“"),
|
||||||
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
|
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
|
||||||
_ => default,
|
_ => default,
|
||||||
};
|
};
|
||||||
|
@ -130,7 +130,7 @@ static TO_SRGB: LazyLock<qcms::Transform> = LazyLock::new(|| {
|
|||||||
///
|
///
|
||||||
/// # Predefined color maps
|
/// # Predefined color maps
|
||||||
/// Typst also includes a number of preset color maps that can be used for
|
/// Typst also includes a number of preset color maps that can be used for
|
||||||
/// [gradients]($gradient.linear). These are simply arrays of colors defined in
|
/// [gradients]($gradient/#stops). These are simply arrays of colors defined in
|
||||||
/// the module `color.map`.
|
/// the module `color.map`.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -148,11 +148,11 @@ static TO_SRGB: LazyLock<qcms::Transform> = LazyLock::new(|| {
|
|||||||
/// | `magma` | A black to purple to yellow color map. |
|
/// | `magma` | A black to purple to yellow color map. |
|
||||||
/// | `plasma` | A purple to pink to yellow color map. |
|
/// | `plasma` | A purple to pink to yellow color map. |
|
||||||
/// | `rocket` | A black to red to white color map. |
|
/// | `rocket` | A black to red to white color map. |
|
||||||
/// | `mako` | A black to teal to yellow color map. |
|
/// | `mako` | A black to teal to white color map. |
|
||||||
/// | `vlag` | A light blue to white to red color map. |
|
/// | `vlag` | A light blue to white to red color map. |
|
||||||
/// | `icefire` | A light teal to black to yellow color map. |
|
/// | `icefire` | A light teal to black to orange color map. |
|
||||||
/// | `flare` | A orange to purple color map that is perceptually uniform. |
|
/// | `flare` | A orange to purple color map that is perceptually uniform. |
|
||||||
/// | `crest` | A blue to white to red color map. |
|
/// | `crest` | A light green to blue color map. |
|
||||||
///
|
///
|
||||||
/// Some popular presets are not included because they are not available under a
|
/// Some popular presets are not included because they are not available under a
|
||||||
/// free licence. Others, like
|
/// free licence. Others, like
|
||||||
|
@ -10,12 +10,14 @@ use crate::foundations::{
|
|||||||
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
|
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
|
||||||
use crate::visualize::{FillRule, Paint, Stroke};
|
use crate::visualize::{FillRule, Paint, Stroke};
|
||||||
|
|
||||||
/// A curve consisting of movements, lines, and Beziér segments.
|
use super::FixedStroke;
|
||||||
|
|
||||||
|
/// A curve consisting of movements, lines, and Bézier segments.
|
||||||
///
|
///
|
||||||
/// At any point in time, there is a conceptual pen or cursor.
|
/// At any point in time, there is a conceptual pen or cursor.
|
||||||
/// - Move elements move the cursor without drawing.
|
/// - Move elements move the cursor without drawing.
|
||||||
/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
|
/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
|
||||||
/// position, potentially with control point for a Beziér curve.
|
/// position, potentially with control point for a Bézier curve.
|
||||||
/// - Close elements draw a straight or smooth line back to the start of the
|
/// - Close elements draw a straight or smooth line back to the start of the
|
||||||
/// curve or the latest preceding move segment.
|
/// curve or the latest preceding move segment.
|
||||||
///
|
///
|
||||||
@ -26,7 +28,7 @@ use crate::visualize::{FillRule, Paint, Stroke};
|
|||||||
/// or relative to the current pen/cursor position, that is, the position where
|
/// or relative to the current pen/cursor position, that is, the position where
|
||||||
/// the previous segment ended.
|
/// the previous segment ended.
|
||||||
///
|
///
|
||||||
/// Beziér curve control points can be skipped by passing `{none}` or
|
/// Bézier curve control points can be skipped by passing `{none}` or
|
||||||
/// automatically mirrored from the preceding segment by passing `{auto}`.
|
/// automatically mirrored from the preceding segment by passing `{auto}`.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
@ -88,7 +90,7 @@ pub struct CurveElem {
|
|||||||
#[fold]
|
#[fold]
|
||||||
pub stroke: Smart<Option<Stroke>>,
|
pub stroke: Smart<Option<Stroke>>,
|
||||||
|
|
||||||
/// The components of the curve, in the form of moves, line and Beziér
|
/// The components of the curve, in the form of moves, line and Bézier
|
||||||
/// segment, and closes.
|
/// segment, and closes.
|
||||||
#[variadic]
|
#[variadic]
|
||||||
pub components: Vec<CurveComponent>,
|
pub components: Vec<CurveComponent>,
|
||||||
@ -225,7 +227,7 @@ pub struct CurveLine {
|
|||||||
pub relative: bool,
|
pub relative: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a quadratic Beziér curve segment from the last point to `end`, using
|
/// Adds a quadratic Bézier curve segment from the last point to `end`, using
|
||||||
/// `control` as the control point.
|
/// `control` as the control point.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -245,9 +247,9 @@ pub struct CurveLine {
|
|||||||
/// ```
|
/// ```
|
||||||
#[elem(name = "quad", title = "Curve Quadratic Segment")]
|
#[elem(name = "quad", title = "Curve Quadratic Segment")]
|
||||||
pub struct CurveQuad {
|
pub struct CurveQuad {
|
||||||
/// The control point of the quadratic Beziér curve.
|
/// The control point of the quadratic Bézier curve.
|
||||||
///
|
///
|
||||||
/// - If `{auto}` and this segment follows another quadratic Beziér curve,
|
/// - If `{auto}` and this segment follows another quadratic Bézier curve,
|
||||||
/// the previous control point will be mirrored.
|
/// the previous control point will be mirrored.
|
||||||
/// - If `{none}`, the control point defaults to `end`, and the curve will
|
/// - If `{none}`, the control point defaults to `end`, and the curve will
|
||||||
/// be a straight line.
|
/// be a straight line.
|
||||||
@ -272,7 +274,7 @@ pub struct CurveQuad {
|
|||||||
pub relative: bool,
|
pub relative: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a cubic Beziér curve segment from the last point to `end`, using
|
/// Adds a cubic Bézier curve segment from the last point to `end`, using
|
||||||
/// `control-start` and `control-end` as the control points.
|
/// `control-start` and `control-end` as the control points.
|
||||||
///
|
///
|
||||||
/// ```example
|
/// ```example
|
||||||
@ -388,7 +390,7 @@ pub enum CloseMode {
|
|||||||
Straight,
|
Straight,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A curve consisting of movements, lines, and Beziér segments.
|
/// A curve consisting of movements, lines, and Bézier segments.
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct Curve(pub Vec<CurveItem>);
|
pub struct Curve(pub Vec<CurveItem>);
|
||||||
|
|
||||||
@ -530,3 +532,65 @@ impl Curve {
|
|||||||
Size::new(max_x - min_x, max_y - min_y)
|
Size::new(max_x - min_x, max_y - min_y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Curve {
|
||||||
|
fn to_kurbo(&self) -> impl Iterator<Item = kurbo::PathEl> + '_ {
|
||||||
|
use kurbo::PathEl;
|
||||||
|
|
||||||
|
self.0.iter().map(|item| match *item {
|
||||||
|
CurveItem::Move(point) => PathEl::MoveTo(point_to_kurbo(point)),
|
||||||
|
CurveItem::Line(point) => PathEl::LineTo(point_to_kurbo(point)),
|
||||||
|
CurveItem::Cubic(point, point1, point2) => PathEl::CurveTo(
|
||||||
|
point_to_kurbo(point),
|
||||||
|
point_to_kurbo(point1),
|
||||||
|
point_to_kurbo(point2),
|
||||||
|
),
|
||||||
|
CurveItem::Close => PathEl::ClosePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When this curve is interpreted as a clip mask, would it contain `point`?
|
||||||
|
pub fn contains(&self, fill_rule: FillRule, needle: Point) -> bool {
|
||||||
|
let kurbo = kurbo::BezPath::from_vec(self.to_kurbo().collect());
|
||||||
|
let windings = kurbo::Shape::winding(&kurbo, point_to_kurbo(needle));
|
||||||
|
match fill_rule {
|
||||||
|
FillRule::NonZero => windings != 0,
|
||||||
|
FillRule::EvenOdd => windings % 2 != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When this curve is stroked with `stroke`, would the stroke contain
|
||||||
|
/// `point`?
|
||||||
|
pub fn stroke_contains(&self, stroke: &FixedStroke, needle: Point) -> bool {
|
||||||
|
let width = stroke.thickness.to_raw();
|
||||||
|
let cap = match stroke.cap {
|
||||||
|
super::LineCap::Butt => kurbo::Cap::Butt,
|
||||||
|
super::LineCap::Round => kurbo::Cap::Round,
|
||||||
|
super::LineCap::Square => kurbo::Cap::Square,
|
||||||
|
};
|
||||||
|
let join = match stroke.join {
|
||||||
|
super::LineJoin::Miter => kurbo::Join::Miter,
|
||||||
|
super::LineJoin::Round => kurbo::Join::Round,
|
||||||
|
super::LineJoin::Bevel => kurbo::Join::Bevel,
|
||||||
|
};
|
||||||
|
let miter_limit = stroke.miter_limit.get();
|
||||||
|
let mut style = kurbo::Stroke::new(width)
|
||||||
|
.with_caps(cap)
|
||||||
|
.with_join(join)
|
||||||
|
.with_miter_limit(miter_limit);
|
||||||
|
if let Some(dash) = &stroke.dash {
|
||||||
|
style = style.with_dashes(
|
||||||
|
dash.phase.to_raw(),
|
||||||
|
dash.array.iter().copied().map(Abs::to_raw),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let opts = kurbo::StrokeOpts::default();
|
||||||
|
let tolerance = 0.01;
|
||||||
|
let expanded = kurbo::stroke(self.to_kurbo(), &style, &opts, tolerance);
|
||||||
|
kurbo::Shape::contains(&expanded, point_to_kurbo(needle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_to_kurbo(point: Point) -> kurbo::Point {
|
||||||
|
kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
|
||||||
|
}
|
||||||
|
@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
|
|||||||
/// the offsets when defining a gradient. In this case, Typst will space all
|
/// the offsets when defining a gradient. In this case, Typst will space all
|
||||||
/// stops evenly.
|
/// stops evenly.
|
||||||
///
|
///
|
||||||
|
/// Typst predefines color maps that you can use as stops. See the
|
||||||
|
/// [`color`]($color/#predefined-color-maps) documentation for more details.
|
||||||
|
///
|
||||||
/// # Relativeness
|
/// # Relativeness
|
||||||
/// The location of the `{0%}` and `{100%}` stops depends on the dimensions
|
/// The location of the `{0%}` and `{100%}` stops depends on the dimensions
|
||||||
/// of a container. This container can either be the shape that it is being
|
/// of a container. This container can either be the shape that it is being
|
||||||
@ -117,12 +120,12 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
|
|||||||
/// #let spaces = (
|
/// #let spaces = (
|
||||||
/// ("Oklab", color.oklab),
|
/// ("Oklab", color.oklab),
|
||||||
/// ("Oklch", color.oklch),
|
/// ("Oklch", color.oklch),
|
||||||
/// ("linear-RGB", color.linear-rgb),
|
|
||||||
/// ("sRGB", color.rgb),
|
/// ("sRGB", color.rgb),
|
||||||
|
/// ("linear-RGB", color.linear-rgb),
|
||||||
/// ("CMYK", color.cmyk),
|
/// ("CMYK", color.cmyk),
|
||||||
|
/// ("Grayscale", color.luma),
|
||||||
/// ("HSL", color.hsl),
|
/// ("HSL", color.hsl),
|
||||||
/// ("HSV", color.hsv),
|
/// ("HSV", color.hsv),
|
||||||
/// ("Grayscale", color.luma),
|
|
||||||
/// )
|
/// )
|
||||||
///
|
///
|
||||||
/// #for (name, space) in spaces {
|
/// #for (name, space) in spaces {
|
||||||
@ -157,10 +160,6 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
|
|||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Presets
|
|
||||||
/// Typst predefines color maps that you can use with your gradients. See the
|
|
||||||
/// [`color`]($color/#predefined-color-maps) documentation for more details.
|
|
||||||
///
|
|
||||||
/// # Note on file sizes
|
/// # Note on file sizes
|
||||||
///
|
///
|
||||||
/// Gradients can be quite large, especially if they have many stops. This is
|
/// Gradients can be quite large, especially if they have many stops. This is
|
||||||
@ -288,7 +287,7 @@ impl Gradient {
|
|||||||
/// )),
|
/// )),
|
||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func(title = "Radial Gradient")]
|
||||||
fn radial(
|
fn radial(
|
||||||
span: Span,
|
span: Span,
|
||||||
/// The color [stops](#stops) of the gradient.
|
/// The color [stops](#stops) of the gradient.
|
||||||
@ -402,7 +401,7 @@ impl Gradient {
|
|||||||
/// )),
|
/// )),
|
||||||
/// )
|
/// )
|
||||||
/// ```
|
/// ```
|
||||||
#[func]
|
#[func(title = "Conic Gradient")]
|
||||||
pub fn conic(
|
pub fn conic(
|
||||||
span: Span,
|
span: Span,
|
||||||
/// The color [stops](#stops) of the gradient.
|
/// The color [stops](#stops) of the gradient.
|
||||||
@ -575,19 +574,17 @@ impl Gradient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let n = repetitions.v;
|
let n = repetitions.v;
|
||||||
let mut stops = std::iter::repeat(self.stops_ref())
|
let mut stops = std::iter::repeat_n(self.stops_ref(), n)
|
||||||
.take(n)
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.flat_map(|(i, stops)| {
|
.flat_map(|(i, stops)| {
|
||||||
let mut stops = stops
|
let mut stops = stops
|
||||||
.iter()
|
.iter()
|
||||||
.map(move |&(color, offset)| {
|
.map(move |&(color, offset)| {
|
||||||
let t = i as f64 / n as f64;
|
|
||||||
let r = offset.get();
|
let r = offset.get();
|
||||||
if i % 2 == 1 && mirror {
|
if i % 2 == 1 && mirror {
|
||||||
(color, Ratio::new(t + (1.0 - r) / n as f64))
|
(color, Ratio::new((i as f64 + 1.0 - r) / n as f64))
|
||||||
} else {
|
} else {
|
||||||
(color, Ratio::new(t + r / n as f64))
|
(color, Ratio::new((i as f64 + r) / n as f64))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -1230,7 +1227,7 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
|
|||||||
};
|
};
|
||||||
|
|
||||||
if stop.get() < last_stop {
|
if stop.get() < last_stop {
|
||||||
bail!(*span, "offsets must be in strictly monotonic order");
|
bail!(*span, "offsets must be in monotonic order");
|
||||||
}
|
}
|
||||||
|
|
||||||
last_stop = stop.get();
|
last_stop = stop.get();
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user