Compare commits

...

31 Commits

Author SHA1 Message Date
Laurenz
db9a83d9fc Bump version on main
The tagged commit itself is on the 0.13 branch.
2025-03-07 11:19:12 +01:00
Laurenz
8d3488a07d
0.13.1 changelog (#6025) 2025-03-07 10:03:52 +00:00
Laurenz
476c2df312
Mark breaking symbol changes as breaking in 0.13.0 changelog (#6024) 2025-03-07 09:17:11 +00:00
Malo
e0b2c32a8e
Mention that sym.ohm was removed in the 0.13.0 changelog (#6017)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-07 09:05:16 +00:00
Laurenz
99b7d2898e
Replace par function call in tutorial (#6023) 2025-03-07 08:47:56 +00:00
Laurenz
e1a9166e1d
Hotfix for labels on symbols (#6015) 2025-03-07 08:22:42 +00:00
Andrew Voynov
6271cdceae
Fix debug implementation of Recipe (#5997) 2025-03-04 09:33:39 +00:00
LN Liberda
63fda9935f
Run tests on 32-bit via Ubuntu multilib (#5937)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-03 13:10:58 +00:00
3w36zj6
8820a00beb
Respect quotes: false in inline quote (#5991)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-03 11:50:47 +00:00
andis854
9a6ffbc7db
Added snap to installation instructions (#5984) 2025-03-03 11:40:58 +00:00
Andrew Voynov
bf0d45e2c0
Make array.chunks example more readable (#5975) 2025-03-03 11:31:39 +00:00
F2011
d4def09962
Correct typo (#5971) 2025-03-03 11:23:29 +00:00
Tijme
66679920b2
Fix docs example with type/string comparison (#5987) 2025-03-03 09:32:06 +00:00
Ian Wrzesinski
cfb3b1a270
Improve clarity of ast.rs for newcomers to the codebase (#5784)
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
Co-authored-by: T0mstone <39707032+T0mstone@users.noreply.github.com>
2025-02-26 20:10:36 +00:00
Emmanuel Lesueur
52f1f53973
Fix curve with multiple non-closed components. (#5963) 2025-02-26 18:07:29 +00:00
Malo
d6b0d68ffa
Add more methods to direction (#5893) 2025-02-25 14:19:17 +00:00
Laurenz
8f039dd614
Only autocomplete methods which take self (#5824) 2025-02-25 14:10:01 +00:00
Malo
2eef9e84e1
Improve hints for show rule recursion depth (#5856) 2025-02-25 14:09:52 +00:00
evie
d11ad80dee
Add #str.normalize(form) (#5631)
Co-authored-by: +merlan #flirora <uruwi@protonmail.com>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-25 14:01:01 +00:00
Laurenz
bad343748b
Fix paper name in page setup guide (#5956) 2025-02-25 13:00:22 +00:00
Laurenz
f31c971624
Deduplicate watcher update call (#5955) 2025-02-25 12:47:41 +00:00
aodenis
acd3a5b7a5
Fix high CPU usage due to inotify watch triggering itself (#5905)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-25 12:41:54 +00:00
Laurenz
225e845021
Fix introspection of HTML root sibling metadata (#5953) 2025-02-25 11:31:15 +00:00
Sharzy
36d83c8c09
HTML export: fix elem counting on classify_output (#5910)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-24 16:35:13 +00:00
Malo
3744c99b07
Override the default math class of some characters (#5949) 2025-02-24 16:15:17 +00:00
Max
81efc82d3c
Fix math accent base height calculation (#5941) 2025-02-24 16:05:36 +00:00
Laurenz
69c3f95705
Bump MSRV to 1.83 and Rust in CI to 1.85 (#5946) 2025-02-24 12:28:01 +00:00
Laurenz
ebe2543264
Fix comparison of Func and NativeFuncData (#5943) 2025-02-24 11:17:31 +00:00
Malo
56f4fa2b4d
Documentation improvements (#5888) 2025-02-23 11:31:28 +00:00
Max
55bc5f4c94
Make math shorthands noncontinuable (#5925) 2025-02-23 11:28:24 +00:00
PgBiel
240f238eee
Fix HTML export of table with gutter (#5920) 2025-02-23 11:26:14 +00:00
87 changed files with 971 additions and 505 deletions

View File

@ -5,6 +5,7 @@ env:
RUSTFLAGS: "-Dwarnings"
RUSTDOCFLAGS: "-Dwarnings"
TYPST_TESTS_EXTENDED: true
PKG_CONFIG_i686-unknown-linux-gnu: /usr/bin/i686-linux-gnu-pkgconf
jobs:
# This allows us to have one branch protection rule for the full test matrix.
@ -27,30 +28,43 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
bits: [64]
include:
- os: ubuntu-latest
bits: 32
runs-on: ${{ matrix.os }}
steps:
- 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
- run: cargo test --workspace --no-run
- run: cargo test --workspace --no-fail-fast
with:
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
if: failure()
uses: actions/upload-artifact@v4
with:
name: tests-rendered-${{ matrix.os }}
name: tests-rendered-${{ matrix.os }}-${{ matrix.bits }}
path: tests/store/render/**
retention-days: 3
- name: Update test artifacts
if: failure()
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"
- name: Upload updated reference output (for use if the test changes are desired)
if: failure() && env.updated_artifacts
uses: actions/upload-artifact@v4
with:
name: tests-updated-${{ matrix.os }}
name: tests-updated-${{ matrix.os }}-${{ matrix.bits }}
path: tests/ref/**
retention-days: 3
@ -59,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0
- uses: dtolnay/rust-toolchain@1.85.0
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
@ -73,7 +87,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.80.0
- uses: dtolnay/rust-toolchain@1.83.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspace

View File

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

49
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler2"
@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typst"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2752,12 +2752,12 @@ dependencies = [
[[package]]
name = "typst-assets"
version = "0.13.0"
source = "git+https://github.com/typst/typst-assets?rev=fa0f8a4#fa0f8a438cc4bc2113cc0aa3304cd68cdc2bc020"
version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a"
[[package]]
name = "typst-cli"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"chrono",
"clap",
@ -2802,12 +2802,12 @@ dependencies = [
[[package]]
name = "typst-dev-assets"
version = "0.13.0"
source = "git+https://github.com/typst/typst-dev-assets?rev=61aebe9#61aebe9575a5abff889f76d73c7b01dc8e17e340"
version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=9879589#9879589f4b3247b12c5e694d0d7fa86d4d8a198e"
[[package]]
name = "typst-docs"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"clap",
"ecow",
@ -2830,7 +2830,7 @@ dependencies = [
[[package]]
name = "typst-eval"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2848,7 +2848,7 @@ dependencies = [
[[package]]
name = "typst-fuzz"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"comemo",
"libfuzzer-sys",
@ -2860,7 +2860,7 @@ dependencies = [
[[package]]
name = "typst-html"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2874,7 +2874,7 @@ dependencies = [
[[package]]
name = "typst-ide"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2891,7 +2891,7 @@ dependencies = [
[[package]]
name = "typst-kit"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"dirs",
"ecow",
@ -2914,7 +2914,7 @@ dependencies = [
[[package]]
name = "typst-layout"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"az",
"bumpalo",
@ -2944,7 +2944,7 @@ dependencies = [
[[package]]
name = "typst-library"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"az",
"bitflags 2.8.0",
@ -2995,6 +2995,7 @@ dependencies = [
"typst-timing",
"typst-utils",
"unicode-math-class",
"unicode-normalization",
"unicode-segmentation",
"unscanny",
"usvg",
@ -3004,7 +3005,7 @@ dependencies = [
[[package]]
name = "typst-macros"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"heck",
"proc-macro2",
@ -3014,7 +3015,7 @@ dependencies = [
[[package]]
name = "typst-pdf"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"arrayvec",
"base64",
@ -3040,7 +3041,7 @@ dependencies = [
[[package]]
name = "typst-realize"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"arrayvec",
"bumpalo",
@ -3056,7 +3057,7 @@ dependencies = [
[[package]]
name = "typst-render"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"bytemuck",
"comemo",
@ -3072,7 +3073,7 @@ dependencies = [
[[package]]
name = "typst-svg"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"base64",
"comemo",
@ -3090,7 +3091,7 @@ dependencies = [
[[package]]
name = "typst-syntax"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"ecow",
"serde",
@ -3106,7 +3107,7 @@ dependencies = [
[[package]]
name = "typst-tests"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"clap",
"comemo",
@ -3131,7 +3132,7 @@ dependencies = [
[[package]]
name = "typst-timing"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"parking_lot",
"serde",
@ -3141,7 +3142,7 @@ dependencies = [
[[package]]
name = "typst-utils"
version = "0.13.0"
version = "0.13.1"
dependencies = [
"once_cell",
"portable-atomic",

View File

@ -4,8 +4,8 @@ default-members = ["crates/typst-cli"]
resolver = "2"
[workspace.package]
version = "0.13.0"
rust-version = "1.80" # also change in ci.yml
version = "0.13.1"
rust-version = "1.83" # also change in ci.yml
authors = ["The Typst Project Developers"]
edition = "2021"
homepage = "https://typst.app"
@ -16,24 +16,24 @@ keywords = ["typst"]
readme = "README.md"
[workspace.dependencies]
typst = { path = "crates/typst", version = "0.13.0" }
typst-cli = { path = "crates/typst-cli", version = "0.13.0" }
typst-eval = { path = "crates/typst-eval", version = "0.13.0" }
typst-html = { path = "crates/typst-html", version = "0.13.0" }
typst-ide = { path = "crates/typst-ide", version = "0.13.0" }
typst-kit = { path = "crates/typst-kit", version = "0.13.0" }
typst-layout = { path = "crates/typst-layout", version = "0.13.0" }
typst-library = { path = "crates/typst-library", version = "0.13.0" }
typst-macros = { path = "crates/typst-macros", version = "0.13.0" }
typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" }
typst-realize = { path = "crates/typst-realize", version = "0.13.0" }
typst-render = { path = "crates/typst-render", version = "0.13.0" }
typst-svg = { path = "crates/typst-svg", version = "0.13.0" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" }
typst-timing = { path = "crates/typst-timing", version = "0.13.0" }
typst-utils = { path = "crates/typst-utils", version = "0.13.0" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fa0f8a4" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "61aebe9" }
typst = { path = "crates/typst", version = "0.13.1" }
typst-cli = { path = "crates/typst-cli", version = "0.13.1" }
typst-eval = { path = "crates/typst-eval", version = "0.13.1" }
typst-html = { path = "crates/typst-html", version = "0.13.1" }
typst-ide = { path = "crates/typst-ide", version = "0.13.1" }
typst-kit = { path = "crates/typst-kit", version = "0.13.1" }
typst-layout = { path = "crates/typst-layout", version = "0.13.1" }
typst-library = { path = "crates/typst-library", version = "0.13.1" }
typst-macros = { path = "crates/typst-macros", version = "0.13.1" }
typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" }
typst-realize = { path = "crates/typst-realize", version = "0.13.1" }
typst-render = { path = "crates/typst-render", version = "0.13.1" }
typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "9879589" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -129,6 +129,7 @@ unicode-bidi = "0.3.18"
unicode-ident = "1.0"
unicode-math-class = "0.1"
unicode-script = "0.5"
unicode-normalization = "0.1.24"
unicode-segmentation = "1"
unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }

View File

@ -113,7 +113,9 @@ Typst's CLI is available from different sources:
- You can install Typst through different package managers. Note that the
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`
- Windows: `winget install --id Typst.Typst`
@ -254,3 +256,4 @@ instant preview. To achieve these goals, we follow three core design principles:
[contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md
[packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst

View File

@ -350,7 +350,7 @@ fn export_image(
.iter()
.enumerate()
.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)
})
})

View File

@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Perform initial compilation.
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.
loop {
// Watch all dependencies of the most recent compilation.
watcher.update(world.dependencies())?;
// Wait until anything relevant happens.
watcher.wait()?;
@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Evict the cache.
comemo::evict(10);
// Adjust the file watching.
watcher.update(world.dependencies())?;
}
}
@ -204,6 +201,10 @@ impl Watcher {
let event = event
.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
// (triggered by some editors when saving files) with the
// 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
@ -234,19 +245,11 @@ impl Watcher {
}
}
}
/// Whether a watch event is relevant for compilation.
fn is_event_relevant(&self, event: &notify::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: &notify::EventKind) -> bool {
match kind {
notify::EventKind::Any => true,
notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true,
@ -261,7 +264,6 @@ impl Watcher {
notify::EventKind::Other => false,
}
}
}
/// The status in which the watcher can be.
pub enum Status {

View File

@ -466,7 +466,7 @@ impl<'a> CapturesVisitor<'a> {
}
// Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => {
self.internal.enter();
for child in node.children() {
self.visit(child);
@ -516,7 +516,7 @@ impl<'a> CapturesVisitor<'a> {
// A let expression contains a binding, but that binding is only
// active after the body is evaluated.
Some(ast::Expr::Let(expr)) => {
Some(ast::Expr::LetBinding(expr)) => {
if let Some(init) = expr.init() {
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
// active after the iterable is evaluated but before the body is
// evaluated.
Some(ast::Expr::For(expr)) => {
Some(ast::Expr::ForLoop(expr)) => {
self.visit(expr.iterable().to_untyped());
self.internal.enter();
@ -544,7 +544,7 @@ impl<'a> CapturesVisitor<'a> {
// An import contains items, but these are active only after the
// path is evaluated.
Some(ast::Expr::Import(expr)) => {
Some(ast::Expr::ModuleImport(expr)) => {
self.visit(expr.source().to_untyped());
if let Some(ast::Imports::Items(items)) = expr.imports() {
for item in items.iter() {

View File

@ -30,7 +30,7 @@ fn eval_code<'a>(
while let Some(expr) = exprs.next() {
let span = expr.span();
let value = match expr {
ast::Expr::Set(set) => {
ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?;
if vm.flow.is_some() {
break;
@ -39,7 +39,7 @@ fn eval_code<'a>(
let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_map(styles))
}
ast::Expr::Show(show) => {
ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?;
if vm.flow.is_some() {
break;
@ -94,9 +94,9 @@ impl Eval for ast::Expr<'_> {
Self::Label(v) => v.eval(vm),
Self::Ref(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::Enum(v) => v.eval(vm).map(Value::Content),
Self::Term(v) => v.eval(vm).map(Value::Content),
Self::ListItem(v) => v.eval(vm).map(Value::Content),
Self::EnumItem(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::Math(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::Numeric(v) => v.eval(vm),
Self::Str(v) => v.eval(vm),
Self::Code(v) => v.eval(vm),
Self::Content(v) => v.eval(vm).map(Value::Content),
Self::CodeBlock(v) => v.eval(vm),
Self::ContentBlock(v) => v.eval(vm).map(Value::Content),
Self::Array(v) => v.eval(vm).map(Value::Array),
Self::Dict(v) => v.eval(vm).map(Value::Dict),
Self::Parenthesized(v) => v.eval(vm),
@ -126,19 +126,19 @@ impl Eval for ast::Expr<'_> {
Self::Closure(v) => v.eval(vm),
Self::Unary(v) => v.eval(vm),
Self::Binary(v) => v.eval(vm),
Self::Let(v) => v.eval(vm),
Self::DestructAssign(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")),
Self::LetBinding(v) => v.eval(vm),
Self::DestructAssignment(v) => v.eval(vm),
Self::SetRule(_) => bail!(forbidden("set")),
Self::ShowRule(_) => bail!(forbidden("show")),
Self::Contextual(v) => v.eval(vm).map(Value::Content),
Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm),
Self::For(v) => v.eval(vm),
Self::Import(v) => v.eval(vm),
Self::Include(v) => v.eval(vm).map(Value::Content),
Self::Break(v) => v.eval(vm),
Self::Continue(v) => v.eval(vm),
Self::Return(v) => v.eval(vm),
Self::WhileLoop(v) => v.eval(vm),
Self::ForLoop(v) => v.eval(vm),
Self::ModuleImport(v) => v.eval(vm),
Self::ModuleInclude(v) => v.eval(vm).map(Value::Content),
Self::LoopBreak(v) => v.eval(vm),
Self::LoopContinue(v) => v.eval(vm),
Self::FuncReturn(v) => v.eval(vm),
}?
.spanned(span);

View File

@ -33,7 +33,7 @@ fn eval_markup<'a>(
while let Some(expr) = exprs.next() {
match expr {
ast::Expr::Set(set) => {
ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?;
if vm.flow.is_some() {
break;
@ -41,7 +41,7 @@ fn eval_markup<'a>(
seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
}
ast::Expr::Show(show) => {
ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?;
if vm.flow.is_some() {
break;

View File

@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> {
let transform = self.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())?,
};

View File

@ -83,8 +83,8 @@ fn html_document_impl(
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector })
}
@ -307,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
/// Determine which kind of output the user generated.
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 {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
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::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag
elem.tag,
),
_ => {}
}

View File

@ -410,10 +410,18 @@ fn field_access_completions(
elem.into_iter().chain(Some(ty))
};
// Autocomplete methods from the element's or type's scope.
// 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());
}
}
if let Some(scope) = value.scope() {
for (name, binding) in scope.iter() {
@ -509,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
// "#import "path.typ": a, b, |".
if_chain! {
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(source) = prev.children().find(|child| child.is::<ast::Expr>());
then {
@ -528,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if let Some(grand) = parent.parent();
if grand.kind() == SyntaxKind::ImportItems;
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(source) = great.children().find(|child| child.is::<ast::Expr>());
then {
@ -669,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
if let Some(args) = parent.get().cast::<ast::Args>();
if let Some(grand) = parent.parent();
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 {
ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()),
ast::Expr::SetRule(set) => Some(set.target()),
_ => None,
};
then {
@ -1455,7 +1463,7 @@ impl<'a> CompletionContext<'a> {
let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
named_items(self.world, self.leaf.clone(), |item| {
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());
}
@ -1764,6 +1772,7 @@ mod tests {
#[test]
fn test_autocomplete_type_methods() {
test("#\"hello\".", -1).must_include(["len", "contains"]);
test("#table().", -1).must_exclude(["cell"]);
}
#[test]

View File

@ -232,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
ast::Expr::FuncCall(call) => {
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(_) => {
DerefTarget::VarAccess(expr_node)
}

View File

@ -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(ast::Expr::Ident(callee)) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()),
ast::Expr::SetRule(set) => Some(set.target()),
_ => None,
};

View File

@ -1377,7 +1377,7 @@ impl<'a> GridLayouter<'a> {
.footer
.as_ref()
.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)
&& !in_last_with_offset(
self.regions,
@ -1446,7 +1446,7 @@ impl<'a> GridLayouter<'a> {
.iter_mut()
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
.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
@ -1494,7 +1494,7 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even
// 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 the rowspan is contained within the footer.
y < footer_start || rowspan.y >= footer_start
@ -1580,5 +1580,5 @@ pub(super) fn points(
/// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
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)
}

View File

@ -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
// last row of cells above us.
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();
let bottom_y =
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };

View File

@ -588,7 +588,7 @@ impl GridLayouter<'_> {
measurement_data: &CellMeasurementData<'_>,
) -> bool {
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
})
{

View File

@ -154,7 +154,7 @@ pub fn line<'a>(
let mut items = collect_items(engine, p, range, trim);
// 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() {
shaped.prepend_hyphen(engine, p.config.fallback);
}
@ -406,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"
// 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,
}

View File

@ -290,7 +290,7 @@ fn linebreak_optimized_bounded<'a>(
}
// 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 });
}
}
@ -423,7 +423,7 @@ fn linebreak_optimized_approximate(
let total = pred.total + line_cost;
// 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,

View File

@ -465,7 +465,7 @@ impl<'a> ShapedText<'a> {
None
};
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))
.chain(fallback_func.iter().map(|f| f()))
.flatten();
@ -570,7 +570,7 @@ impl<'a> ShapedText<'a> {
// for the next line.
let dec = if ltr { usize::checked_sub } else { usize::checked_add };
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;
}
idx = next;
@ -812,7 +812,7 @@ fn shape_segment<'a>(
.nth(1)
.map(|(i, _)| offset + i)
.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

View File

@ -34,7 +34,7 @@ pub fn layout_accent(
// Try to replace accent glyph with flattened variant.
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);
}
@ -50,7 +50,7 @@ pub fn layout_accent(
// minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.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 accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);

View File

@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> {
self.last_point = point;
self.last_control_from = point;
self.is_started = true;
self.is_empty = true;
}
/// Add a line segment.

View File

@ -61,6 +61,7 @@ ttf-parser = { workspace = true }
two-face = { workspace = true }
typed-arena = { workspace = true }
unicode-math-class = { workspace = true }
unicode-normalization = { workspace = true }
unicode-segmentation = { workspace = true }
unscanny = { workspace = true }
usvg = { workspace = true }

View File

@ -312,7 +312,8 @@ impl Route<'_> {
if !self.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!(
"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(())

View File

@ -751,7 +751,7 @@ impl Array {
///
/// ```example
/// #let array = (1, 2, 3, 4, 5, 6, 7, 8)
/// #array.chunks(3)
/// #array.chunks(3) \
/// #array.chunks(3, exact: true)
/// ```
#[func]

View File

@ -437,10 +437,10 @@ impl PartialEq for Func {
}
}
impl PartialEq<&NativeFuncData> for Func {
fn eq(&self, other: &&NativeFuncData) -> bool {
impl PartialEq<&'static NativeFuncData> for Func {
fn eq(&self, other: &&'static NativeFuncData) -> bool {
match &self.repr {
Repr::Native(native) => native.function == other.function,
Repr::Native(native) => *native == Static(*other),
_ => false,
}
}

View File

@ -7,12 +7,13 @@ use comemo::Tracked;
use ecow::EcoString;
use serde::{Deserialize, Serialize};
use typst_syntax::{Span, Spanned};
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
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,
};
use crate::layout::Alignment;
@ -286,6 +287,30 @@ impl Str {
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.
///
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
@ -788,6 +813,25 @@ cast! {
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.
fn match_to_dict((start, text): (usize, &str)) -> Dict {
dict! {

View File

@ -471,7 +471,8 @@ impl Debug for Recipe {
selector.fmt(f)?;
f.write_str(", ")?;
}
self.transform.fmt(f)
self.transform.fmt(f)?;
f.write_str(")")
}
}

View File

@ -21,6 +21,7 @@ use crate::foundations::{
/// be accessed using [field access notation]($scripting/#fields):
///
/// - 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)
///
/// Moreover, you can define custom symbols with this type's constructor
@ -410,7 +411,7 @@ fn find<'a>(
}
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_score = Some(score);
}

View File

@ -10,7 +10,7 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector};
use crate::html::{HtmlElement, HtmlNode};
use crate::html::HtmlNode;
use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering;
@ -55,8 +55,8 @@ impl Introspector {
/// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")]
pub fn html(root: &HtmlElement) -> Self {
IntrospectorBuilder::new().build_html(root)
pub fn html(output: &[HtmlNode]) -> Self {
IntrospectorBuilder::new().build_html(output)
}
/// Iterates over all locatable elements.
@ -392,9 +392,9 @@ impl IntrospectorBuilder {
}
/// 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();
self.discover_in_html(&mut elems, root);
self.discover_in_html(&mut elems, output);
self.finalize(elems)
}
@ -434,16 +434,16 @@ impl IntrospectorBuilder {
}
/// Processes the tags in the HTML element.
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) {
for child in &elem.children {
match child {
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, nodes: &[HtmlNode]) {
for node in nodes {
match node {
HtmlNode::Tag(tag) => self.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
),
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(
sink,
frame,

View File

@ -50,6 +50,42 @@ impl Dir {
pub const TTB: Self = Self::TTB;
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
/// `{"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.
///
/// ```example

View File

@ -1387,7 +1387,7 @@ impl<'a> CellGrid<'a> {
// Include the gutter right before the footer, unless there is
// none, or the gutter is already included in the header (no
// rows between the header and the footer).
if header_end.map_or(true, |header_end| header_end != footer.start) {
if header_end != Some(footer.start) {
footer.start = footer.start.saturating_sub(1);
}
}
@ -1526,11 +1526,7 @@ impl<'a> CellGrid<'a> {
self.entry(x, y).map(|entry| match entry {
Entry::Cell(_) => Axes::new(x, y),
Entry::Merged { parent } => {
let c = if self.has_gutter {
1 + self.cols.len() / 2
} else {
self.cols.len()
};
let c = self.non_gutter_column_count();
let factor = if self.has_gutter { 2 } else { 1 };
Axes::new(factor * (*parent % c), factor * (*parent / c))
}
@ -1602,6 +1598,21 @@ impl<'a> CellGrid<'a> {
cell.rowspan.get()
}
}
#[inline]
pub fn non_gutter_column_count(&self) -> usize {
if self.has_gutter {
// Calculation: With gutters, we have
// 'cols = 2 * (non-gutter cols) - 1', since there is a gutter
// column between each regular column. Therefore,
// 'floor(cols / 2)' will be equal to
// 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1',
// so 'non-gutter cols = 1 + floor(cols / 2)'.
1 + self.cols.len() / 2
} else {
self.cols.len()
}
}
}
/// Given a cell's requested x and y, the vector with the resolved cell

View File

@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable};
/// let author = find-child(elem, "author")
/// let pars = find-child(elem, "content")
///
/// heading(title.children.first())
/// [= #title.children.first()]
/// text(10pt, weight: "medium")[
/// Published by
/// #author.children.first()
/// ]
///
/// for p in pars.children {
/// if (type(p) == "dictionary") {
/// if type(p) == dictionary {
/// parbreak()
/// p.children.first()
/// }
@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable};
///
/// #let data = xml("example.xml")
/// #for elem in data.first().children {
/// if (type(elem) == "dictionary") {
/// if type(elem) == dictionary {
/// article(elem)
/// }
/// }

View File

@ -161,7 +161,7 @@ impl Show for Packed<QuoteElem> {
let block = self.block(styles);
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(
SmartQuoteElem::quotes_in(styles),
TextElem::lang_in(styles),

View File

@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, 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 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 row = row

View File

@ -160,7 +160,7 @@ impl FontBook {
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_key = Some(key);
}

View File

@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
{
let covers = family.covers();
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()
});
}

View File

@ -130,7 +130,7 @@ static TO_SRGB: LazyLock<qcms::Transform> = LazyLock::new(|| {
///
/// # Predefined color maps
/// 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`.
///
/// ```example

View File

@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// the offsets when defining a gradient. In this case, Typst will space all
/// stops evenly.
///
/// Typst predefines color maps that you can use as stops. See the
/// [`color`]($color/#predefined-color-maps) documentation for more details.
///
/// # Relativeness
/// 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
@ -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
///
/// 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(
span: Span,
/// The color [stops](#stops) of the gradient.
@ -402,7 +401,7 @@ impl Gradient {
/// )),
/// )
/// ```
#[func]
#[func(title = "Conic Gradient")]
pub fn conic(
span: Span,
/// The color [stops](#stops) of the gradient.

View File

@ -70,7 +70,7 @@ pub(crate) fn write_outline(
// (not exceeding whichever is the most restrictive depth limit
// of those two).
while children.last().is_some_and(|last| {
last_skipped_level.map_or(true, |l| last.level < l)
last_skipped_level.is_none_or(|l| last.level < l)
&& last.level < leaf.level
}) {
children = &mut children.last_mut().unwrap().children;
@ -83,7 +83,7 @@ pub(crate) fn write_outline(
// needed, following the usual rules listed above.
last_skipped_level = None;
children.push(leaf);
} else if last_skipped_level.map_or(true, |l| leaf.level < l) {
} else if last_skipped_level.is_none_or(|l| leaf.level < l) {
// Only the topmost / lowest-level skipped heading matters when you
// have consecutive skipped headings (since none of them are being
// added to the bookmark tree), hence the condition above.

View File

@ -326,7 +326,10 @@ fn visit_math_rules<'a>(
// Symbols in non-math content transparently convert to `TextElem` so we
// don't have to handle them in non-math layout.
if let Some(elem) = content.to_packed::<SymbolElem>() {
let text = TextElem::packed(elem.text).spanned(elem.span());
let mut text = TextElem::packed(elem.text).spanned(elem.span());
if let Some(label) = elem.label() {
text.set_label(label);
}
visit(s, s.store(text), styles)?;
return Ok(true);
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ use std::sync::Arc;
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use crate::ast::AstNode;
use crate::{FileId, Span, SyntaxKind};
/// A node in the untyped syntax tree.
@ -119,26 +118,6 @@ impl SyntaxNode {
}
}
/// Whether the node can be cast to the given AST node.
pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool {
self.cast::<T>().is_some()
}
/// Try to convert the node to a typed AST node.
pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option<T> {
T::from_untyped(self)
}
/// Cast the first child that can cast to the AST type `T`.
pub fn cast_first_match<'a, T: AstNode<'a>>(&'a self) -> Option<T> {
self.children().find_map(Self::cast)
}
/// Cast the last child that can cast to the AST type `T`.
pub fn cast_last_match<'a, T: AstNode<'a>>(&'a self) -> Option<T> {
self.children().rev().find_map(Self::cast)
}
/// Whether the node or its children contain an error.
pub fn erroneous(&self) -> bool {
match &self.0 {
@ -753,7 +732,7 @@ impl<'a> LinkedNode<'a> {
// sibling's span number is larger than the target span's number.
if children
.peek()
.map_or(true, |next| next.span().number() > span.number())
.is_none_or(|next| next.span().number() > span.number())
{
if let Some(found) = child.find(span) {
return Some(found);

View File

@ -327,8 +327,8 @@ impl PackageVersion {
/// missing in the bound are ignored.
pub fn matches_eq(&self, bound: &VersionBound) -> bool {
self.major == bound.major
&& bound.minor.map_or(true, |minor| self.minor == minor)
&& bound.patch.map_or(true, |patch| self.patch == patch)
&& bound.minor.is_none_or(|minor| self.minor == minor)
&& bound.patch.is_none_or(|patch| self.patch == patch)
}
/// Performs a `>` match with the given version bound. The match only

View File

@ -271,7 +271,8 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
}
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => {
continuable = matches!(
continuable = !p.at(SyntaxKind::MathShorthand)
&& matches!(
math_class(p.current_text()),
None | Some(MathClass::Alphabetic)
);

View File

@ -360,6 +360,21 @@ pub fn default_math_class(c: char) -> Option<MathClass> {
// https://github.com/typst/typst/pull/5714
'\u{22A5}' => Some(MathClass::Normal),
// Used as a binary connector in linear logic, where it is referred to
// as "par".
// https://github.com/typst/typst/issues/5764
'⅋' => Some(MathClass::Binary),
// Those overrides should become the default in the next revision of
// MathClass.txt.
// https://github.com/typst/typst/issues/5764#issuecomment-2632435247
'⎰' | '⟅' => Some(MathClass::Opening),
'⎱' | '⟆' => Some(MathClass::Closing),
// Both and ⟑ are classified as Binary.
// https://github.com/typst/typst/issues/5764
'⟇' => Some(MathClass::Binary),
c => unicode_math_class::class(c),
}
}

View File

@ -28,7 +28,7 @@ impl Scalar {
///
/// If the value is NaN, then it is set to `0.0` in the result.
pub const fn new(x: f64) -> Self {
Self(if is_nan(x) { 0.0 } else { x })
Self(if x.is_nan() { 0.0 } else { x })
}
/// Gets the value of this [`Scalar`].
@ -37,17 +37,6 @@ impl Scalar {
}
}
// We have to detect NaNs this way since `f64::is_nan` isnt const
// on stable yet:
// ([tracking issue](https://github.com/rust-lang/rust/issues/57241))
#[allow(clippy::unusual_byte_groupings)]
const fn is_nan(x: f64) -> bool {
// Safety: all bit patterns are valid for u64, and f64 has no padding bits.
// We cannot use `f64::to_bits` because it is not const.
let x_bits = unsafe { std::mem::transmute::<f64, u64>(x) };
(x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0
}
impl Numeric for Scalar {
fn zero() -> Self {
Self(0.0)

View File

@ -294,20 +294,20 @@ feature flag.
`errorbar.diamond.stroked`, `errorbar.diamond.filled`,
`errorbar.circle.stroked`, `errorbar.circle.filled`
- `numero`
- `Omega.inv`
- Renamed
- Renamed **(Breaking change)**
- `ohm.inv` to `Omega.inv`
- Changed codepoint
- Changed codepoint **(Breaking change)**
- `angle.l.double` from `《` to `⟪`
- `angle.r.double` from `》` to `⟫`
- `angstrom` from U+212B (`Å`) to U+00C5 (`Å`)
- Deprecated
- `sect` and all its variants in favor of `inter`
- `integral.sect` in favor of `integral.inter`
- Removed
- Removed **(Breaking change)**
- `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math)
- `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math)
- `kelvin` in favor of just K (`[$upright(K)$]` in math)
- `ohm` in favor of `Omega`
## Deprecations
- The [`path`] function in favor of the [`curve`] function

29
docs/changelog/0.13.1.md Normal file
View File

@ -0,0 +1,29 @@
---
title: 0.13.1
description: Changes in Typst 0.13.1
---
# Version 0.13.1 (March 7, 2025)
## Command Line Interface
- Fixed high CPU usage for `typst watch` on Linux. Depending on the project
size, CPU usage would spike for varying amounts of time. This bug appeared
with 0.13.0 due to a behavioral change in the inotify file watching backend.
## HTML export
- Fixed export of tables with [gutters]($table.gutter)
- Fixed usage of `<html>` and `<body>` element within [context]
- Fixed querying of [metadata] next to `<html>` and `<body>` element
## Visualization
- Fixed [curves]($curve) with multiple non-closed components
## Introspection
- Fixed a regression where labelled [symbols]($symbol) could not be
[queried]($query) by label
## Deprecations
- Fixed false positives in deprecation warnings for type/str comparisons
## Contributors
<contributors from="v0.13.0" to="v0.13.1" />

View File

@ -10,6 +10,7 @@ forward. This section documents all changes to Typst since its initial public
release.
## Versions
- [Typst 0.13.1]($changelog/0.13.1)
- [Typst 0.13.0]($changelog/0.13.0)
- [Typst 0.12.0]($changelog/0.12.0)
- [Typst 0.11.1]($changelog/0.11.1)

View File

@ -447,7 +447,7 @@ document.
To let a function style your whole document, the show rule processes everything
that comes after it and calls the function specified after the colon with the
result as an argument. The `.with` part is a _method_ that takes the `conf`
function and pre-configures some if its arguments before passing it on to the
function and pre-configures some of its arguments before passing it on to the
show rule.
</div>

View File

@ -56,7 +56,7 @@ requirements with examples.
Typst's default page size is A4 paper. Depending on your region and your use
case, you will want to change this. You can do this by using the
[`{page}`]($page) set rule and passing it a string argument to use a common page
size. Options include the complete ISO 216 series (e.g. `"iso-a4"`, `"iso-c2"`),
size. Options include the complete ISO 216 series (e.g. `"a4"` and `"iso-c2"`),
customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the
reference for the [page's paper argument]($page.paper) to learn about all
available options.

View File

@ -170,8 +170,8 @@
category: symbols
path: ["emoji"]
details: |
Named emoji.
Named emojis.
For example, `#emoji.face` produces the 😀 emoji. If you frequently use
certain emojis, you can also import them from the `emoji` module (`[#import
emoji: face]`) to use them without the `#emoji.` prefix.
emoji: face]`) to use them without the `emoji.` prefix.

View File

@ -188,6 +188,7 @@ fn changelog_pages(resolver: &dyn Resolver) -> PageModel {
let mut page = md_page(resolver, resolver.base(), load!("changelog/welcome.md"));
let base = format!("{}changelog/", resolver.base());
page.children = vec![
md_page(resolver, &base, load!("changelog/0.13.1.md")),
md_page(resolver, &base, load!("changelog/0.13.0.md")),
md_page(resolver, &base, load!("changelog/0.12.0.md")),
md_page(resolver, &base, load!("changelog/0.11.1.md")),

View File

@ -13,11 +13,11 @@ your report using Typst's styling system.
As we have seen in the previous chapter, Typst has functions that _insert_
content (e.g. the [`image`] function) and others that _manipulate_ content that
they received as arguments (e.g. the [`align`] function). The first impulse you
might have when you want, for example, to justify the report, could be to look
might have when you want, for example, to change the font, could be to look
for a function that does that and wrap the complete document in it.
```example
#par(justify: true)[
#text(font: "New Computer Modern")[
= Background
In the case of glaciers, fluid
dynamics principles can be used
@ -37,9 +37,9 @@ do in Typst, there is special syntax for it: Instead of putting the content
inside of the argument list, you can write it in square brackets directly after
the normal arguments, saving on punctuation.
As seen above, that works. The [`par`] function justifies all paragraphs within
it. However, wrapping the document in countless functions and applying styles
selectively and in-situ can quickly become cumbersome.
As seen above, that works. With the [`text`] function, we can adjust the font
for all text within it. However, wrapping the document in countless functions
and applying styles selectively and in-situ can quickly become cumbersome.
Fortunately, Typst has a more elegant solution. With _set rules,_ you can apply
style properties to all occurrences of some kind of content. You write a set
@ -47,7 +47,9 @@ rule by entering the `{set}` keyword, followed by the name of the function whose
properties you want to set, and a list of arguments in parentheses.
```example
#set par(justify: true)
#set text(
font: "New Computer Modern"
)
= Background
In the case of glaciers, fluid

6
flake.lock generated
View File

@ -112,13 +112,13 @@
"rust-manifest": {
"flake": false,
"locked": {
"narHash": "sha256-Yqu2/i9170R7pQhvOCR1f5SyFr7PcFbO6xcMr9KWruQ=",
"narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=",
"type": "file",
"url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml"
"url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"
},
"original": {
"type": "file",
"url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml"
"url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"
}
},
"systems": {

View File

@ -10,7 +10,7 @@
inputs.nixpkgs.follows = "nixpkgs";
};
rust-manifest = {
url = "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml";
url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml";
flake = false;
};
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html></html>

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html>Hi</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -149,7 +149,7 @@ impl Collector {
for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() {
let entry = entry.unwrap();
let path = entry.path();
if !path.extension().is_some_and(|ext| ext == "typ") {
if path.extension().is_none_or(|ext| ext != "typ") {
continue;
}
@ -168,7 +168,7 @@ impl Collector {
for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() {
let entry = entry.unwrap();
let path = entry.path();
if !path.extension().is_some_and(|ext| ext == "png") {
if path.extension().is_none_or(|ext| ext != "png") {
continue;
}

View File

@ -161,7 +161,7 @@ impl<'a> Runner<'a> {
// Compare against reference output if available.
// Test that is ok doesn't need to be updated.
if ref_data.as_ref().map_or(false, |r| D::matches(&live, r)) {
if ref_data.as_ref().is_ok_and(|r| D::matches(&live, r)) {
return;
}

View File

@ -86,6 +86,13 @@
// Error: 2-28 0x110000 is not a valid codepoint
#str.from-unicode(0x110000) // 0x10ffff is the highest valid code point
--- str-normalize ---
// Test the `normalize` method.
#test("e\u{0301}".normalize(form: "nfc"), "é")
#test("é".normalize(form: "nfd"), "e\u{0301}")
#test("ſ\u{0301}".normalize(form: "nfkc"), "ś")
#test("ſ\u{0301}".normalize(form: "nfkd"), "s\u{0301}")
--- string-len ---
// Test the `len` method.
#test("Hello World!".len(), 12)

15
tests/suite/html/elem.typ Normal file
View File

@ -0,0 +1,15 @@
--- html-elem-alone-context html ---
#context html.elem("html")
--- html-elem-not-alone html ---
// Error: 2-19 `<html>` element must be the only element in the document
#html.elem("html")
Text
--- html-elem-metadata html ---
#html.elem("html", context {
let val = query(<l>).first().value
test(val, "Hi")
val
})
#metadata("Hi") <l>

View File

@ -1,10 +1,35 @@
--- dir-from ---
#test(direction.from(left), ltr)
#test(direction.from(right), rtl)
#test(direction.from(top), ttb)
#test(direction.from(bottom), btt)
--- dir-from-invalid ---
// Error: 17-23 cannot convert this alignment to a side
#direction.from(center)
--- dir-to ---
#test(direction.to(left), rtl)
#test(direction.to(right), ltr)
#test(direction.to(top), btt)
#test(direction.to(bottom), ttb)
-- dir-to-invalid ---
// Error: 15-21 cannot convert this alignment to a side
#direction.to(center)
--- dir-axis ---
// Test direction methods.
#test(ltr.axis(), "horizontal")
#test(rtl.axis(), "horizontal")
#test(ttb.axis(), "vertical")
#test(btt.axis(), "vertical")
--- dir-sign ---
#test(ltr.sign(), 1)
#test(rtl.sign(), -1)
#test(ttb.sign(), 1)
#test(btt.sign(), -1)
--- dir-start ---
#test(ltr.start(), left)
#test(rtl.start(), right)

View File

@ -30,3 +30,30 @@
[row],
),
)
--- col-gutter-table html ---
#table(
columns: 3,
column-gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
--- row-gutter-table html ---
#table(
columns: 3,
row-gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
--- col-row-gutter-table html ---
#table(
columns: 3,
gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)

View File

@ -13,6 +13,11 @@ $ underline(f' : NN -> RR) \
1 - 0 thick &...,
) $
--- math-shorthands-noncontinuable ---
// Test that shorthands are not continuable.
$ x >=(y) / z \
x >= (y) / z $
--- math-common-symbols ---
// Test common symbols.
$ dot \ dots \ ast \ tilde \ star $

View File

@ -49,7 +49,6 @@
2000000001, "βΜκʹ, αʹ",
2000010001, "βΜκʹ, αΜαʹ, αʹ",
2056839184, "βΜκʹ, αΜ͵εχπγ, ͵θρπδ",
12312398676, "βΜρκγʹ, αΜ͵ασλθ, ͵ηχοϛ",
)
#t(
pat: sym.Alpha,

View File

@ -118,3 +118,6 @@ An inline #quote[quote.]
#quote(block: true, attribution: [The Test Author])[
A block-level quote.
]
--- issue-5536-quote-inline-quotes-false ---
Lorem #quote(block: false, quotes: false)[dolor].

View File

@ -44,18 +44,21 @@
--- recursion-via-include-in-layout ---
// Test cyclic imports during layout.
// Error: 2-38 maximum show rule depth exceeded
// Hint: 2-38 check whether the show rule matches its own output
// Hint: 2-38 maybe a show rule matches its own output
// Hint: 2-38 maybe there are too deeply nested elements
#layout(_ => include "recursion.typ")
--- recursion-show-math ---
// Test recursive show rules.
// Error: 22-25 maximum show rule depth exceeded
// Hint: 22-25 check whether the show rule matches its own output
// Hint: 22-25 maybe a show rule matches its own output
// Hint: 22-25 maybe there are too deeply nested elements
#show math.equation: $x$
$ x $
--- recursion-show-math-realize ---
// Error: 22-33 maximum show rule depth exceeded
// Hint: 22-33 check whether the show rule matches its own output
// Hint: 22-33 maybe a show rule matches its own output
// Hint: 22-33 maybe there are too deeply nested elements
#show heading: it => heading[it]
$ #heading[hi] $

View File

@ -151,3 +151,7 @@
--- symbol-sect-deprecated ---
// Warning: 5-9 `sect` is deprecated, use `inter` instead
$ A sect B = A inter B $
--- issue-5930-symbol-label ---
#emoji.face<lab>
#context test(query(<lab>).first().text, "😀")

View File

@ -38,6 +38,16 @@
curve.close(mode: "smooth"),
)
--- curve-multiple-non-closed ---
#curve(
stroke: 2pt,
curve.line((20pt, 0pt)),
curve.move((0pt, 10pt)),
curve.line((20pt, 10pt)),
curve.move((0pt, 20pt)),
curve.line((20pt, 20pt)),
)
--- curve-line ---
#curve(
fill: purple,