Merge branch 'upstream-main' into krilla-port

# Conflicts:
#	crates/typst-pdf/src/image.rs
#	crates/typst-pdf/src/lib.rs
This commit is contained in:
Laurenz Stampfl 2024-12-14 19:39:27 +01:00
commit 4ad90c1f38
207 changed files with 4617 additions and 3697 deletions

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.82.0 - uses: dtolnay/rust-toolchain@1.83.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --no-run - run: cargo test --workspace --no-run
- run: cargo test --workspace --no-fail-fast - run: cargo test --workspace --no-fail-fast
@ -59,11 +59,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.82.0 - uses: dtolnay/rust-toolchain@1.83.0
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets --all-features - run: cargo clippy --workspace --all-targets --all-features
- run: cargo clippy --workspace --all-targets --no-default-features
- run: cargo fmt --check --all - run: cargo fmt --check --all
- run: cargo doc --workspace --no-deps - run: cargo doc --workspace --no-deps

View File

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

76
Cargo.lock generated
View File

@ -123,6 +123,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "ascii"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -296,6 +302,12 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "chunked_transfer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
[[package]] [[package]]
name = "ciborium" name = "ciborium"
version = "0.2.2" version = "0.2.2"
@ -409,6 +421,11 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "codex"
version = "0.1.0"
source = "git+https://github.com/typst/codex?rev=343a9b1#343a9b199430681ba3ca0e2242097c6419492d55"
[[package]] [[package]]
name = "color-print" name = "color-print"
version = "0.3.6" version = "0.3.6"
@ -919,9 +936,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.0" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]] [[package]]
name = "hayagriva" name = "hayagriva"
@ -950,6 +967,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hypher" name = "hypher"
version = "0.1.5" version = "0.1.5"
@ -1192,7 +1215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.0", "hashbrown 0.15.2",
"rayon", "rayon",
"serde", "serde",
] ]
@ -1368,13 +1391,12 @@ dependencies = [
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
version = "0.4.7" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"cc", "cc",
"once_cell",
] ]
[[package]] [[package]]
@ -2355,6 +2377,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "sigpipe"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5584bfb3e0d348139d8210285e39f6d2f8a1902ac06de343e06357d1d763d8e6"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@ -2697,6 +2728,18 @@ dependencies = [
"strict-num", "strict-num",
] ]
[[package]]
name = "tiny_http"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
dependencies = [
"ascii",
"chunked_transfer",
"httpdate",
"log",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@ -2790,6 +2833,7 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"typst-eval", "typst-eval",
"typst-html",
"typst-layout", "typst-layout",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
@ -2802,7 +2846,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.12.0" version = "0.12.0"
source = "git+https://github.com/typst/typst-assets?rev=5c0dcc0#5c0dcc062eddd9c1b1a8994fe4948ab604a1690f" source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -2830,11 +2874,14 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml 0.9.34+deprecated", "serde_yaml 0.9.34+deprecated",
"shell-escape", "shell-escape",
"sigpipe",
"tar", "tar",
"tempfile", "tempfile",
"tiny_http",
"toml", "toml",
"typst", "typst",
"typst-eval", "typst-eval",
"typst-html",
"typst-kit", "typst-kit",
"typst-macros", "typst-macros",
"typst-pdf", "typst-pdf",
@ -2902,6 +2949,20 @@ dependencies = [
"typst-syntax", "typst-syntax",
] ]
[[package]]
name = "typst-html"
version = "0.12.0"
dependencies = [
"comemo",
"ecow",
"typst-library",
"typst-macros",
"typst-svg",
"typst-syntax",
"typst-timing",
"typst-utils",
]
[[package]] [[package]]
name = "typst-ide" name = "typst-ide"
version = "0.12.0" version = "0.12.0"
@ -2979,6 +3040,7 @@ dependencies = [
"bumpalo", "bumpalo",
"chinese-number", "chinese-number",
"ciborium", "ciborium",
"codex",
"comemo", "comemo",
"csv", "csv",
"ecow", "ecow",

View File

@ -19,6 +19,7 @@ readme = "README.md"
typst = { path = "crates/typst", version = "0.12.0" } typst = { path = "crates/typst", version = "0.12.0" }
typst-cli = { path = "crates/typst-cli", version = "0.12.0" } typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
typst-eval = { path = "crates/typst-eval", version = "0.12.0" } typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
typst-html = { path = "crates/typst-html", version = "0.12.0" }
typst-ide = { path = "crates/typst-ide", version = "0.12.0" } typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
typst-kit = { path = "crates/typst-kit", version = "0.12.0" } typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
typst-layout = { path = "crates/typst-layout", version = "0.12.0" } typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
@ -31,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
typst-utils = { path = "crates/typst-utils", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "5c0dcc0" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
@ -46,6 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "343a9b1" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"
@ -104,6 +106,7 @@ serde = { version = "1.0.184", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_yaml = "0.9" serde_yaml = "0.9"
shell-escape = "0.1.5" shell-escape = "0.1.5"
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"
@ -115,6 +118,7 @@ tar = "0.4"
tempfile = "3.7.0" tempfile = "3.7.0"
thin-vec = "0.2.13" thin-vec = "0.2.13"
time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] } time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
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.24.1"

View File

@ -20,6 +20,7 @@ doc = false
[dependencies] [dependencies]
typst = { workspace = true } typst = { workspace = true }
typst-eval = { workspace = true } typst-eval = { workspace = true }
typst-html = { workspace = true }
typst-kit = { workspace = true } typst-kit = { workspace = true }
typst-macros = { workspace = true } typst-macros = { workspace = true }
typst-pdf = { workspace = true } typst-pdf = { workspace = true }
@ -46,8 +47,10 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_yaml = { workspace = true } serde_yaml = { workspace = true }
shell-escape = { workspace = true } shell-escape = { workspace = true }
sigpipe = { workspace = true }
tar = { workspace = true } tar = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
tiny_http = { workspace = true, optional = true }
toml = { workspace = true } toml = { workspace = true }
ureq = { workspace = true } ureq = { workspace = true }
xz2 = { workspace = true, optional = true } xz2 = { workspace = true, optional = true }
@ -62,11 +65,14 @@ color-print = { workspace = true }
semver = { workspace = true } semver = { workspace = true }
[features] [features]
default = ["embed-fonts"] default = ["embed-fonts", "http-server"]
# Embeds some fonts into the binary, see typst-kit # Embeds some fonts into the binary, see typst-kit
embed-fonts = ["typst-kit/embed-fonts"] embed-fonts = ["typst-kit/embed-fonts"]
# Enables the built-in HTTP server for `typst watch` and HTML export.
http-server = ["dep:tiny_http"]
# Permits the CLI to update itself without a package manager. # Permits the CLI to update itself without a package manager.
self-update = ["dep:self-replace", "dep:xz2", "dep:zip"] self-update = ["dep:self-replace", "dep:xz2", "dep:zip"]

View File

@ -33,7 +33,7 @@ const AFTER_HELP: &str = color_print::cstr!("\
<s>Forum for questions:</> https://forum.typst.app/ <s>Forum for questions:</> https://forum.typst.app/
"); ");
/// The Typst compiler /// The Typst compiler.
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
#[clap( #[clap(
name = "typst", name = "typst",
@ -44,24 +44,16 @@ const AFTER_HELP: &str = color_print::cstr!("\
max_term_width = 80, max_term_width = 80,
)] )]
pub struct CliArguments { pub struct CliArguments {
/// The command to run /// The command to run.
#[command(subcommand)] #[command(subcommand)]
pub command: Command, pub command: Command,
/// Set when to use color. /// Whether to use color. When set to `auto` if the terminal to supports it.
/// auto = use color if a capable terminal is detected #[clap(long, default_value_t = ColorChoice::Auto, default_missing_value = "always")]
#[clap(
long,
value_name = "WHEN",
require_equals = true,
num_args = 0..=1,
default_value = "auto",
default_missing_value = "always",
)]
pub color: ColorChoice, pub color: ColorChoice,
/// Path to a custom CA certificate to use when making network requests. /// Path to a custom CA certificate to use when making network requests.
#[clap(long = "cert", env = "TYPST_CERT")] #[clap(long, env = "TYPST_CERT")]
pub cert: Option<PathBuf>, pub cert: Option<PathBuf>,
} }
@ -69,109 +61,53 @@ pub struct CliArguments {
#[derive(Debug, Clone, Subcommand)] #[derive(Debug, Clone, Subcommand)]
#[command()] #[command()]
pub enum Command { pub enum Command {
/// Compiles an input file into a supported output format /// Compiles an input file into a supported output format.
#[command(visible_alias = "c")] #[command(visible_alias = "c")]
Compile(CompileCommand), Compile(CompileCommand),
/// Watches an input file and recompiles on changes /// Watches an input file and recompiles on changes.
#[command(visible_alias = "w")] #[command(visible_alias = "w")]
Watch(CompileCommand), Watch(WatchCommand),
/// Initializes a new project from a template /// Initializes a new project from a template.
Init(InitCommand), Init(InitCommand),
/// Processes an input file to extract provided metadata /// Processes an input file to extract provided metadata.
Query(QueryCommand), Query(QueryCommand),
/// Lists all discovered fonts in system and custom font paths /// Lists all discovered fonts in system and custom font paths.
Fonts(FontsCommand), Fonts(FontsCommand),
/// Self update the Typst CLI /// Self update the Typst CLI.
#[cfg_attr(not(feature = "self-update"), clap(hide = true))] #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
Update(UpdateCommand), Update(UpdateCommand),
} }
/// Compiles an input file into a supported output format /// Compiles an input file into a supported output format.
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
pub struct CompileCommand { pub struct CompileCommand {
/// Shared arguments /// Arguments for compilation.
#[clap(flatten)] #[clap(flatten)]
pub common: SharedArgs, pub args: CompileArgs,
/// Path to output file (PDF, PNG or SVG). Use `-` to write output to stdout.
///
/// For output formats emitting one file per page (PNG & SVG), a page number template
/// must be present if the source document renders to multiple pages. Use `{p}` for page
/// numbers, `{0p}` for zero padded page numbers and `{t}` for page count. For example,
/// `page-{0p}-of-{t}.png` creates `page-01-of-10.png`, `page-02-of-10.png` and so on.
#[clap(
required_if_eq("input", "-"),
value_parser = make_output_value_parser(),
value_hint = ValueHint::FilePath,
)]
pub output: Option<Output>,
/// Which pages to export. When unspecified, all document pages are exported.
///
/// Pages to export are separated by commas, and can be either simple page
/// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges
/// (e.g. '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and
/// any pages after it).
///
/// Page numbers are one-indexed and correspond to real page numbers in the
/// document (therefore not being affected by the document's page counter).
#[arg(long = "pages", value_delimiter = ',')]
pub pages: Option<Vec<PageRangeArgument>>,
/// Output a Makefile rule describing the current compilation
#[clap(long = "make-deps", value_name = "PATH")]
pub make_deps: Option<PathBuf>,
/// The format of the output file, inferred from the extension by default
#[arg(long = "format", short = 'f')]
pub format: Option<OutputFormat>,
/// Opens the output file with the default viewer or a specific program after
/// compilation
///
/// Ignored if output is stdout.
#[arg(long = "open", value_name = "VIEWER")]
pub open: Option<Option<String>>,
/// The PPI (pixels per inch) to use for PNG export
#[arg(long = "ppi", default_value_t = 144.0)]
pub ppi: f32,
/// Produces performance timings of the compilation process (experimental)
///
/// The resulting JSON file can be loaded into a tracing tool such as
/// https://ui.perfetto.dev. It does not contain any sensitive information
/// apart from file names and line numbers.
#[arg(long = "timings", value_name = "OUTPUT_JSON")]
pub timings: Option<Option<PathBuf>>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce
/// conformance with.
#[arg(long = "pdf-standard", value_delimiter = ',')]
pub pdf_standard: Vec<PdfStandard>,
} }
/// A PDF standard that Typst can enforce conformance with. /// Compiles an input file into a supported output format.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[derive(Debug, Clone, Parser)]
#[allow(non_camel_case_types)] pub struct WatchCommand {
pub enum PdfStandard { /// Arguments for compilation.
/// PDF 1.7. #[clap(flatten)]
#[value(name = "1.7")] pub args: CompileArgs,
V_1_7,
/// PDF/A-2b. /// Arguments for the HTTP server.
#[value(name = "a-2b")] #[cfg(feature = "http-server")]
A_2b, #[clap(flatten)]
pub server: ServerArgs,
} }
/// Initializes a new project from a template /// Initializes a new project from a template.
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
pub struct InitCommand { pub struct InitCommand {
/// The template to use, e.g. `@preview/charged-ieee` /// The template to use, e.g. `@preview/charged-ieee`.
/// ///
/// You can specify the version by appending e.g. `:0.1.0`. If no version is /// You can specify the version by appending e.g. `:0.1.0`. If no version is
/// specified, Typst will default to the latest version. /// specified, Typst will default to the latest version.
@ -179,34 +115,34 @@ pub struct InitCommand {
/// Supports both local and published templates. /// Supports both local and published templates.
pub template: String, pub template: String,
/// The project directory, defaults to the template's name /// The project directory, defaults to the template's name.
pub dir: Option<String>, pub dir: Option<String>,
/// Arguments related to storage of packages in the system /// Arguments related to storage of packages in the system.
#[clap(flatten)] #[clap(flatten)]
pub package_storage_args: PackageStorageArgs, pub package: PackageArgs,
} }
/// Processes an input file to extract provided metadata /// Processes an input file to extract provided metadata.
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
pub struct QueryCommand { pub struct QueryCommand {
/// Shared arguments /// Path to input Typst file. Use `-` to read input from stdin.
#[clap(flatten)] #[clap(value_parser = input_value_parser(), value_hint = ValueHint::FilePath)]
pub common: SharedArgs, pub input: Input,
/// Defines which elements to retrieve /// Defines which elements to retrieve.
pub selector: String, pub selector: String,
/// Extracts just one field from all retrieved elements /// Extracts just one field from all retrieved elements.
#[clap(long = "field")] #[clap(long = "field")]
pub field: Option<String>, pub field: Option<String>,
/// Expects and retrieves exactly one element /// Expects and retrieves exactly one element.
#[clap(long = "one", default_value = "false")] #[clap(long = "one", default_value = "false")]
pub one: bool, pub one: bool,
/// The format to serialize in /// The format to serialize in.
#[clap(long = "format", default_value = "json")] #[clap(long = "format", default_value_t)]
pub format: SerializationFormat, pub format: SerializationFormat,
/// Whether to pretty-print the serialized output. /// Whether to pretty-print the serialized output.
@ -214,38 +150,153 @@ pub struct QueryCommand {
/// Only applies to JSON format. /// Only applies to JSON format.
#[clap(long)] #[clap(long)]
pub pretty: bool, pub pretty: bool,
/// World arguments.
#[clap(flatten)]
pub world: WorldArgs,
/// Processing arguments.
#[clap(flatten)]
pub process: ProcessArgs,
} }
// Output file format for query command /// Lists all discovered fonts in system and custom font paths.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[derive(Debug, Clone, Parser)]
pub enum SerializationFormat { pub struct FontsCommand {
Json, /// Common font arguments.
Yaml, #[clap(flatten)]
pub font: FontArgs,
/// Also lists style variants of each font family.
#[arg(long)]
pub variants: bool,
} }
/// Common arguments of compile, watch, and query. /// Update the CLI using a pre-compiled binary from a Typst GitHub release.
#[derive(Debug, Clone, Parser)]
pub struct UpdateCommand {
/// Which version to update to (defaults to latest).
pub version: Option<Version>,
/// Forces a downgrade to an older version (required for downgrading).
#[clap(long, default_value_t = false)]
pub force: bool,
/// Reverts to the version from before the last update (only possible if
/// `typst update` has previously ran).
#[clap(
long,
default_value_t = false,
conflicts_with = "version",
conflicts_with = "force"
)]
pub revert: bool,
/// Custom path to the backup file created on update and used by `--revert`,
/// defaults to system-dependent location
#[clap(long = "backup-path", env = "TYPST_UPDATE_BACKUP_PATH", value_name = "FILE")]
pub backup_path: Option<PathBuf>,
}
/// Arguments for compilation and watching.
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct SharedArgs { pub struct CompileArgs {
/// Path to input Typst file. Use `-` to read input from stdin /// Path to input Typst file. Use `-` to read input from stdin.
#[clap(value_parser = make_input_value_parser(), value_hint = ValueHint::FilePath)] #[clap(value_parser = input_value_parser(), value_hint = ValueHint::FilePath)]
pub input: Input, pub input: Input,
/// Configures the project root (for absolute paths) /// Path to output file (PDF, PNG, SVG, or HTML). Use `-` to write output to
/// stdout.
///
/// For output formats emitting one file per page (PNG & SVG), a page number
/// template must be present if the source document renders to multiple
/// pages. Use `{p}` for page numbers, `{0p}` for zero padded page numbers
/// and `{t}` for page count. For example, `page-{0p}-of-{t}.png` creates
/// `page-01-of-10.png`, `page-02-of-10.png`, and so on.
#[clap(
required_if_eq("input", "-"),
value_parser = output_value_parser(),
value_hint = ValueHint::FilePath,
)]
pub output: Option<Output>,
/// The format of the output file, inferred from the extension by default.
#[arg(long = "format", short = 'f')]
pub format: Option<OutputFormat>,
/// World arguments.
#[clap(flatten)]
pub world: WorldArgs,
/// Which pages to export. When unspecified, all pages are exported.
///
/// Pages to export are separated by commas, and can be either simple page
/// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g.
/// '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any
/// pages after it).
///
/// Page numbers are one-indexed and correspond to physical page numbers in
/// the document (therefore not being affected by the document's page
/// counter).
#[arg(long = "pages", value_delimiter = ',')]
pub pages: Option<Vec<Pages>>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce
/// conformance with.
#[arg(long = "pdf-standard", value_delimiter = ',')]
pub pdf_standard: Vec<PdfStandard>,
/// The PPI (pixels per inch) to use for PNG export.
#[arg(long = "ppi", default_value_t = 144.0)]
pub ppi: f32,
/// File path to which a Makefile with the current compilation's
/// dependencies will be written.
#[clap(long = "make-deps", value_name = "PATH")]
pub make_deps: Option<PathBuf>,
/// Processing arguments.
#[clap(flatten)]
pub process: ProcessArgs,
/// Opens the output file with the default viewer or a specific program
/// after compilation. Ignored if output is stdout.
#[arg(long = "open", value_name = "VIEWER")]
pub open: Option<Option<String>>,
/// Produces performance timings of the compilation process. (experimental)
///
/// The resulting JSON file can be loaded into a tracing tool such as
/// https://ui.perfetto.dev. It does not contain any sensitive information
/// apart from file names and line numbers.
#[arg(long = "timings", value_name = "OUTPUT_JSON")]
pub timings: Option<Option<PathBuf>>,
}
/// Arguments for the construction of a world. Shared by compile, watch, and
/// query.
#[derive(Debug, Clone, Args)]
pub struct WorldArgs {
/// Configures the project root (for absolute paths).
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")] #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
pub root: Option<PathBuf>, pub root: Option<PathBuf>,
/// Add a string key-value pair visible through `sys.inputs` /// Add a string key-value pair visible through `sys.inputs`.
#[clap( #[clap(
long = "input", long = "input",
value_name = "key=value", value_name = "key=value",
action = ArgAction::Append, action = ArgAction::Append,
value_parser = ValueParser::new(parse_input_pair), value_parser = ValueParser::new(parse_sys_input_pair),
)] )]
pub inputs: Vec<(String, String)>, pub inputs: Vec<(String, String)>,
/// Common font arguments /// Common font arguments.
#[clap(flatten)] #[clap(flatten)]
pub font_args: FontArgs, pub font: FontArgs,
/// Arguments related to storage of packages in the system.
#[clap(flatten)]
pub package: PackageArgs,
/// The document's creation date formatted as a UNIX timestamp. /// The document's creation date formatted as a UNIX timestamp.
/// ///
@ -257,42 +308,34 @@ pub struct SharedArgs {
value_parser = parse_source_date_epoch, value_parser = parse_source_date_epoch,
)] )]
pub creation_timestamp: Option<DateTime<Utc>>, pub creation_timestamp: Option<DateTime<Utc>>,
}
/// The format to emit diagnostics in /// Arguments for configuration the process of compilation itself.
#[clap( #[derive(Debug, Clone, Args)]
long, pub struct ProcessArgs {
default_value_t = DiagnosticFormat::Human, /// Number of parallel jobs spawned during compilation. Defaults to number
value_parser = clap::value_parser!(DiagnosticFormat) /// of CPUs. Setting it to 1 disables parallelism.
)]
pub diagnostic_format: DiagnosticFormat,
/// Arguments related to storage of packages in the system
#[clap(flatten)]
pub package_storage_args: PackageStorageArgs,
/// Number of parallel jobs spawned during compilation,
/// defaults to number of CPUs. Setting it to 1 disables parallelism.
#[clap(long, short)] #[clap(long, short)]
pub jobs: Option<usize>, pub jobs: Option<usize>,
/// Enables in-development features that may be changed or removed at any /// Enables in-development features that may be changed or removed at any
/// time. /// time.
#[arg(long = "feature", value_delimiter = ',')] #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
pub feature: Vec<Feature>, pub features: Vec<Feature>,
}
/// An in-development feature that may be changed or removed at any time. /// The format to emit diagnostics in.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[clap(long, default_value_t)]
pub enum Feature {} pub diagnostic_format: DiagnosticFormat,
}
/// Arguments related to where packages are stored in the system. /// Arguments related to where packages are stored in the system.
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct PackageStorageArgs { pub struct PackageArgs {
/// Custom path to local packages, defaults to system-dependent location /// Custom path to local packages, defaults to system-dependent location.
#[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")] #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
pub package_path: Option<PathBuf>, pub package_path: Option<PathBuf>,
/// Custom path to package cache, defaults to system-dependent location /// Custom path to package cache, defaults to system-dependent location.
#[clap( #[clap(
long = "package-cache-path", long = "package-cache-path",
env = "TYPST_PACKAGE_CACHE_PATH", env = "TYPST_PACKAGE_CACHE_PATH",
@ -301,13 +344,58 @@ pub struct PackageStorageArgs {
pub package_cache_path: Option<PathBuf>, pub package_cache_path: Option<PathBuf>,
} }
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/> /// Common arguments to customize available fonts.
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> { #[derive(Debug, Clone, Parser)]
let timestamp: i64 = raw pub struct FontArgs {
.parse() /// Adds additional directories that are recursively searched for fonts.
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?; ///
DateTime::from_timestamp(timestamp, 0) /// If multiple paths are specified, they are separated by the system's path
.ok_or_else(|| "timestamp out of range".to_string()) /// separator (`:` on Unix-like systems and `;` on Windows).
#[clap(
long = "font-path",
env = "TYPST_FONT_PATHS",
value_name = "DIR",
value_delimiter = ENV_PATH_SEP,
)]
pub font_paths: Vec<PathBuf>,
/// Ensures system fonts won't be searched, unless explicitly included via
/// `--font-path`.
#[arg(long)]
pub ignore_system_fonts: bool,
}
/// Arguments for the HTTP server.
#[cfg(feature = "http-server")]
#[derive(Debug, Clone, Parser)]
pub struct ServerArgs {
/// Disables the built-in HTTP server for HTML export.
#[clap(long)]
pub no_serve: bool,
/// Disables the injected live reload script for HTML export. The HTML that
/// is written to disk isn't affected either way.
#[clap(long)]
pub no_reload: bool,
/// The port where HTML is served.
///
/// Defaults to the first free port in the range 3000-3005.
#[clap(long)]
pub port: Option<u16>,
}
macro_rules! display_possible_values {
($ty:ty) => {
impl Display for $ty {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
}
};
} }
/// An input that is either stdin or a real path. /// An input that is either stdin or a real path.
@ -319,6 +407,15 @@ pub enum Input {
Path(PathBuf), Path(PathBuf),
} }
impl Display for Input {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Input::Stdin => f.pad("stdin"),
Input::Path(path) => path.display().fmt(f),
}
}
}
/// An output that is either stdout or a real path. /// An output that is either stdout or a real path.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Output { pub enum Output {
@ -337,8 +434,105 @@ impl Display for Output {
} }
} }
/// Which format to use for the generated output file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum OutputFormat {
Pdf,
Png,
Svg,
Html,
}
display_possible_values!(OutputFormat);
/// Which format to use for diagnostics.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum DiagnosticFormat {
#[default]
Human,
Short,
}
display_possible_values!(DiagnosticFormat);
/// An in-development feature that may be changed or removed at any time.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum Feature {
Html,
}
display_possible_values!(Feature);
/// A PDF standard that Typst can enforce conformance with.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
#[allow(non_camel_case_types)]
pub enum PdfStandard {
/// PDF 1.7.
#[value(name = "1.7")]
V_1_7,
/// PDF/A-2b.
#[value(name = "a-2b")]
A_2b,
}
display_possible_values!(PdfStandard);
// Output file format for query command
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum SerializationFormat {
#[default]
Json,
Yaml,
}
display_possible_values!(SerializationFormat);
/// Implements parsing of page ranges (`1-3`, `4`, `5-`, `-2`), used by the
/// `CompileCommand.pages` argument, through the `FromStr` trait instead of a
/// value parser, in order to generate better errors.
///
/// See also: https://github.com/clap-rs/clap/issues/5065
#[derive(Debug, Clone)]
pub struct Pages(pub RangeInclusive<Option<NonZeroUsize>>);
impl FromStr for Pages {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.split('-').map(str::trim).collect::<Vec<_>>().as_slice() {
[] | [""] => Err("page export range must not be empty"),
[single_page] => {
let page_number = parse_page_number(single_page)?;
Ok(Pages(Some(page_number)..=Some(page_number)))
}
["", ""] => Err("page export range must have start or end"),
[start, ""] => Ok(Pages(Some(parse_page_number(start)?)..=None)),
["", end] => Ok(Pages(None..=Some(parse_page_number(end)?))),
[start, end] => {
let start = parse_page_number(start)?;
let end = parse_page_number(end)?;
if start > end {
Err("page export range must end at a page after the start")
} else {
Ok(Pages(Some(start)..=Some(end)))
}
}
[_, _, _, ..] => Err("page export range must have a single hyphen"),
}
}
}
/// Parses a single page number.
fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
if value == "0" {
Err("page numbers start at one")
} else {
NonZeroUsize::from_str(value).map_err(|_| "not a valid page number")
}
}
/// The clap value parser used by `SharedArgs.input` /// The clap value parser used by `SharedArgs.input`
fn make_input_value_parser() -> impl TypedValueParser<Value = Input> { fn input_value_parser() -> impl TypedValueParser<Value = Input> {
clap::builder::OsStringValueParser::new().try_map(|value| { clap::builder::OsStringValueParser::new().try_map(|value| {
if value.is_empty() { if value.is_empty() {
Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) Err(clap::Error::new(clap::error::ErrorKind::InvalidValue))
@ -351,7 +545,7 @@ fn make_input_value_parser() -> impl TypedValueParser<Value = Input> {
} }
/// The clap value parser used by `CompileCommand.output` /// The clap value parser used by `CompileCommand.output`
fn make_output_value_parser() -> impl TypedValueParser<Value = Output> { fn output_value_parser() -> impl TypedValueParser<Value = Output> {
clap::builder::OsStringValueParser::new().try_map(|value| { clap::builder::OsStringValueParser::new().try_map(|value| {
// Empty value also handled by clap for `Option<Output>` // Empty value also handled by clap for `Option<Output>`
if value.is_empty() { if value.is_empty() {
@ -368,7 +562,7 @@ fn make_output_value_parser() -> impl TypedValueParser<Value = Output> {
/// ///
/// This function will return an error if the argument contains no equals sign /// This function will return an error if the argument contains no equals sign
/// or contains the key (before the equals sign) is empty. /// or contains the key (before the equals sign) is empty.
fn parse_input_pair(raw: &str) -> Result<(String, String), String> { fn parse_sys_input_pair(raw: &str) -> Result<(String, String), String> {
let (key, val) = raw let (key, val) = raw
.split_once('=') .split_once('=')
.ok_or("input must be a key and a value separated by an equal sign")?; .ok_or("input must be a key and a value separated by an equal sign")?;
@ -380,143 +574,11 @@ fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
Ok((key, val)) Ok((key, val))
} }
/// Implements parsing of page ranges (`1-3`, `4`, `5-`, `-2`), used by the /// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
/// `CompileCommand.pages` argument, through the `FromStr` trait instead of fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
/// a value parser, in order to generate better errors. let timestamp: i64 = raw
/// .parse()
/// See also: https://github.com/clap-rs/clap/issues/5065 .map_err(|err| format!("timestamp must be decimal integer ({err})"))?;
#[derive(Debug, Clone)] DateTime::from_timestamp(timestamp, 0)
pub struct PageRangeArgument(RangeInclusive<Option<NonZeroUsize>>); .ok_or_else(|| "timestamp out of range".to_string())
impl PageRangeArgument {
pub fn to_range(&self) -> RangeInclusive<Option<NonZeroUsize>> {
self.0.clone()
}
}
impl FromStr for PageRangeArgument {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.split('-').map(str::trim).collect::<Vec<_>>().as_slice() {
[] | [""] => Err("page export range must not be empty"),
[single_page] => {
let page_number = parse_page_number(single_page)?;
Ok(PageRangeArgument(Some(page_number)..=Some(page_number)))
}
["", ""] => Err("page export range must have start or end"),
[start, ""] => Ok(PageRangeArgument(Some(parse_page_number(start)?)..=None)),
["", end] => Ok(PageRangeArgument(None..=Some(parse_page_number(end)?))),
[start, end] => {
let start = parse_page_number(start)?;
let end = parse_page_number(end)?;
if start > end {
Err("page export range must end at a page after the start")
} else {
Ok(PageRangeArgument(Some(start)..=Some(end)))
}
}
[_, _, _, ..] => Err("page export range must have a single hyphen"),
}
}
}
fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
if value == "0" {
Err("page numbers start at one")
} else {
NonZeroUsize::from_str(value).map_err(|_| "not a valid page number")
}
}
/// Lists all discovered fonts in system and custom font paths
#[derive(Debug, Clone, Parser)]
pub struct FontsCommand {
/// Common font arguments
#[clap(flatten)]
pub font_args: FontArgs,
/// Also lists style variants of each font family
#[arg(long)]
pub variants: bool,
}
/// Common arguments to customize available fonts
#[derive(Debug, Clone, Parser)]
pub struct FontArgs {
/// Adds additional directories that are recursively searched for fonts
///
/// If multiple paths are specified, they are separated by the system's path
/// separator (`:` on Unix-like systems and `;` on Windows).
#[clap(
long = "font-path",
env = "TYPST_FONT_PATHS",
value_name = "DIR",
value_delimiter = ENV_PATH_SEP,
)]
pub font_paths: Vec<PathBuf>,
/// Ensures system fonts won't be searched, unless explicitly included via
/// `--font-path`
#[arg(long)]
pub ignore_system_fonts: bool,
}
/// Which format to use for diagnostics.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum DiagnosticFormat {
Human,
Short,
}
impl Display for DiagnosticFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
}
/// Update the CLI using a pre-compiled binary from a Typst GitHub release.
#[derive(Debug, Clone, Parser)]
pub struct UpdateCommand {
/// Which version to update to (defaults to latest)
pub version: Option<Version>,
/// Forces a downgrade to an older version (required for downgrading)
#[clap(long, default_value_t = false)]
pub force: bool,
/// Reverts to the version from before the last update (only possible if
/// `typst update` has previously ran)
#[clap(
long,
default_value_t = false,
conflicts_with = "version",
conflicts_with = "force"
)]
pub revert: bool,
/// Custom path to the backup file created on update and used by `--revert`,
/// defaults to system-dependent location
#[clap(long = "backup-path", env = "TYPST_UPDATE_BACKUP_PATH", value_name = "FILE")]
pub backup_path: Option<PathBuf>,
}
/// Which format to use for the generated output file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum OutputFormat {
Pdf,
Png,
Svg,
}
impl Display for OutputFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
} }

View File

@ -1,8 +1,9 @@
use std::ffi::OsStr;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use chrono::{Datelike, Timelike}; 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, EcoString};
@ -12,17 +13,20 @@ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
}; };
use typst::foundations::{Datetime, Smart}; use typst::foundations::{Datetime, Smart};
use typst::layout::{Frame, Page, PageRanges}; use typst::html::HtmlDocument;
use typst::model::Document; use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span}; use typst::syntax::{FileId, Source, Span};
use typst::WorldExt; use typst::WorldExt;
use typst_pdf::{PdfOptions, PdfStandards}; use typst_pdf::{PdfOptions, PdfStandards};
use crate::args::{ use crate::args::{
CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument, CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
PdfStandard, PdfStandard, WatchCommand,
}; };
#[cfg(feature = "http-server")]
use crate::server::HtmlServer;
use crate::timings::Timer; use crate::timings::Timer;
use crate::watch::Status; use crate::watch::Status;
use crate::world::SystemWorld; use crate::world::SystemWorld;
use crate::{set_failed, terminal}; use crate::{set_failed, terminal};
@ -30,34 +34,73 @@ use crate::{set_failed, terminal};
type CodespanResult<T> = Result<T, CodespanError>; type CodespanResult<T> = Result<T, CodespanError>;
type CodespanError = codespan_reporting::files::Error; type CodespanError = codespan_reporting::files::Error;
impl CompileCommand { /// Execute a compilation command.
/// The output path. pub fn compile(timer: &mut Timer, command: &CompileCommand) -> StrResult<()> {
pub fn output(&self) -> Output { let mut config = CompileConfig::new(command)?;
self.output.clone().unwrap_or_else(|| { let mut world =
let Input::Path(path) = &self.common.input else { SystemWorld::new(&command.args.input, &command.args.world, &command.args.process)
panic!("output must be specified when input is from stdin, as guarded by the CLI"); .map_err(|err| eco_format!("{err}"))?;
}; timer.record(&mut world, |world| compile_once(world, &mut config))?
Output::Path(path.with_extension(
match self.output_format().unwrap_or(OutputFormat::Pdf) {
OutputFormat::Pdf => "pdf",
OutputFormat::Png => "png",
OutputFormat::Svg => "svg",
},
))
})
} }
/// The format to use for generated output, either specified by the user or inferred from the extension. /// A preprocessed `CompileCommand`.
/// pub struct CompileConfig {
/// Will return `Err` if the format was not specified and could not be inferred. /// Whether we are watching.
pub fn output_format(&self) -> StrResult<OutputFormat> { pub watching: bool,
Ok(if let Some(specified) = self.format { /// Path to input Typst file or stdin.
pub input: Input,
/// Path to output file (PDF, PNG, SVG, or HTML).
pub output: Output,
/// The format of the output file.
pub output_format: OutputFormat,
/// Which pages to export.
pub pages: Option<PageRanges>,
/// The document's creation date formatted as a UNIX timestamp.
pub creation_timestamp: Option<DateTime<Utc>>,
/// The format to emit diagnostics in.
pub diagnostic_format: DiagnosticFormat,
/// Opens the output file with the default viewer or a specific program after
/// compilation.
pub open: Option<Option<String>>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce
/// conformance with.
pub pdf_standards: PdfStandards,
/// A path to write a Makefile rule describing the current compilation.
pub make_deps: Option<PathBuf>,
/// The PPI (pixels per inch) to use for PNG export.
pub ppi: f32,
/// The export cache for images, used for caching output files in `typst
/// watch` sessions with images.
pub export_cache: ExportCache,
/// Server for `typst watch` to HTML.
#[cfg(feature = "http-server")]
pub server: Option<HtmlServer>,
}
impl CompileConfig {
/// Preprocess a `CompileCommand`, producing a compilation config.
pub fn new(command: &CompileCommand) -> StrResult<Self> {
Self::new_impl(&command.args, None)
}
/// Preprocess a `WatchCommand`, producing a compilation config.
pub fn watching(command: &WatchCommand) -> StrResult<Self> {
Self::new_impl(&command.args, Some(command))
}
/// The shared implementation of [`CompileConfig::new`] and
/// [`CompileConfig::watching`].
fn new_impl(args: &CompileArgs, watch: Option<&WatchCommand>) -> StrResult<Self> {
let input = args.input.clone();
let output_format = if let Some(specified) = args.format {
specified specified
} else if let Some(Output::Path(output)) = &self.output { } else if let Some(Output::Path(output)) = &args.output {
match output.extension() { match output.extension() {
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf, Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png, Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg, Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
_ => bail!( _ => bail!(
"could not infer output format for path {}.\n\ "could not infer output format for path {}.\n\
consider providing the format manually with `--format/-f`", consider providing the format manually with `--format/-f`",
@ -66,23 +109,28 @@ impl CompileCommand {
} }
} else { } else {
OutputFormat::Pdf OutputFormat::Pdf
}) };
}
/// The ranges of the pages to be exported as specified by the user. let output = args.output.clone().unwrap_or_else(|| {
/// let Input::Path(path) = &input else {
/// This returns `None` if all pages should be exported. panic!("output must be specified when input is from stdin, as guarded by the CLI");
pub fn exported_page_ranges(&self) -> Option<PageRanges> { };
self.pages.as_ref().map(|export_ranges| { Output::Path(path.with_extension(
PageRanges::new( match output_format {
export_ranges.iter().map(PageRangeArgument::to_range).collect(), OutputFormat::Pdf => "pdf",
) OutputFormat::Png => "png",
}) OutputFormat::Svg => "svg",
} OutputFormat::Html => "html",
},
))
});
/// The PDF standards to try to conform with. let pages = args.pages.as_ref().map(|export_ranges| {
pub fn pdf_standards(&self) -> StrResult<PdfStandards> { PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
let list = self });
let pdf_standards = {
let list = args
.pdf_standard .pdf_standard
.iter() .iter()
.map(|standard| match standard { .map(|standard| match standard {
@ -90,19 +138,36 @@ impl CompileCommand {
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
PdfStandards::new(&list) PdfStandards::new(&list)?
} };
}
/// Execute a compilation command. #[cfg(feature = "http-server")]
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { let server = match watch {
// Only meant for input validation Some(command)
_ = command.output_format()?; if output_format == OutputFormat::Html && !command.server.no_serve =>
{
Some(HtmlServer::new(&input, &command.server)?)
}
_ => None,
};
let mut world = Ok(Self {
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?; watching: watch.is_some(),
timer.record(&mut world, |world| compile_once(world, &mut command, false))??; input,
Ok(()) output,
output_format,
pages,
pdf_standards,
creation_timestamp: args.world.creation_timestamp,
make_deps: args.make_deps.clone(),
ppi: args.ppi,
diagnostic_format: args.process.diagnostic_format,
open: args.open.clone(),
export_cache: ExportCache::new(),
#[cfg(feature = "http-server")]
server,
})
}
} }
/// Compile a single time. /// Compile a single time.
@ -111,56 +176,44 @@ pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
#[typst_macros::time(name = "compile once")] #[typst_macros::time(name = "compile once")]
pub fn compile_once( pub fn compile_once(
world: &mut SystemWorld, world: &mut SystemWorld,
command: &mut CompileCommand, config: &mut CompileConfig,
watching: bool,
) -> StrResult<()> { ) -> StrResult<()> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
if watching { if config.watching {
Status::Compiling.print(command).unwrap(); Status::Compiling.print(config).unwrap();
} }
let Warned { output, warnings } = typst::compile(world); let Warned { output, warnings } = compile_and_export(world, config);
let result = output.and_then(|document| export(world, &document, command, watching));
match result { match output {
// Export the PDF / PNG. // Export the PDF / PNG.
Ok(()) => { Ok(()) => {
let duration = start.elapsed(); let duration = start.elapsed();
if watching { if config.watching {
if warnings.is_empty() { if warnings.is_empty() {
Status::Success(duration).print(command).unwrap(); Status::Success(duration).print(config).unwrap();
} else { } else {
Status::PartialSuccess(duration).print(command).unwrap(); Status::PartialSuccess(duration).print(config).unwrap();
} }
} }
print_diagnostics(world, &[], &warnings, command.common.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, command)?; write_make_deps(world, config)?;
open_output(config)?;
if let Some(open) = command.open.take() {
if let Output::Path(file) = command.output() {
open_file(open.as_deref(), &file)?;
}
}
} }
// Print diagnostics. // Print diagnostics.
Err(errors) => { Err(errors) => {
set_failed(); set_failed();
if watching { if config.watching {
Status::Error.print(command).unwrap(); Status::Error.print(config).unwrap();
} }
print_diagnostics( print_diagnostics(world, &errors, &warnings, config.diagnostic_format)
world,
&errors,
&warnings,
command.common.diagnostic_format,
)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
} }
} }
@ -168,39 +221,67 @@ pub fn compile_once(
Ok(()) Ok(())
} }
/// Export into the target format. /// Compile and then export the document.
fn export( fn compile_and_export(
world: &mut SystemWorld, world: &mut SystemWorld,
document: &Document, config: &mut CompileConfig,
command: &CompileCommand, ) -> Warned<SourceResult<()>> {
watching: bool, match config.output_format {
) -> SourceResult<()> { OutputFormat::Html => {
match command.output_format().at(Span::detached())? { let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
OutputFormat::Png => { let result = output.and_then(|document| export_html(&document, config));
export_image(world, document, command, watching, ImageExportFormat::Png) Warned { output: result, warnings }
}
_ => {
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
let result = output.and_then(|document| export_paged(&document, config));
Warned { output: result, warnings }
}
}
}
/// Export to HTML.
fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<()> {
let html = typst_html::html(document)?;
let result = config.output.write(html.as_bytes());
#[cfg(feature = "http-server")]
if let Some(server) = &config.server {
server.update(html);
}
result
.map_err(|err| eco_format!("failed to write HTML file ({err})"))
.at(Span::detached()) .at(Span::detached())
} }
/// Export to a paged target format.
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
match config.output_format {
OutputFormat::Pdf => export_pdf(document, config),
OutputFormat::Png => {
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
}
OutputFormat::Svg => { OutputFormat::Svg => {
export_image(world, document, command, watching, ImageExportFormat::Svg) export_image(document, config, ImageExportFormat::Svg).at(Span::detached())
.at(Span::detached())
} }
OutputFormat::Pdf => export_pdf(document, command), OutputFormat::Html => unreachable!(),
} }
} }
/// Export to a PDF. /// Export to a PDF.
fn export_pdf(document: &Document, command: &CompileCommand) -> SourceResult<()> { fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
let options = PdfOptions { let options = PdfOptions {
ident: Smart::Auto, ident: Smart::Auto,
timestamp: convert_datetime( timestamp: convert_datetime(
command.common.creation_timestamp.unwrap_or_else(chrono::Utc::now), config.creation_timestamp.unwrap_or_else(chrono::Utc::now),
), ),
page_ranges: command.exported_page_ranges(), page_ranges: config.pages.clone(),
standards: command.pdf_standards().at(Span::detached())?, standards: config.pdf_standards.clone(),
}; };
let buffer = typst_pdf::pdf(document, &options)?; let buffer = typst_pdf::pdf(document, &options)?;
command config
.output() .output
.write(&buffer) .write(&buffer)
.map_err(|err| eco_format!("failed to write PDF file ({err})")) .map_err(|err| eco_format!("failed to write PDF file ({err})"))
.at(Span::detached())?; .at(Span::detached())?;
@ -228,36 +309,31 @@ enum ImageExportFormat {
/// Export to one or multiple images. /// Export to one or multiple images.
fn export_image( fn export_image(
world: &mut SystemWorld, document: &PagedDocument,
document: &Document, config: &CompileConfig,
command: &CompileCommand,
watching: bool,
fmt: ImageExportFormat, fmt: ImageExportFormat,
) -> StrResult<()> { ) -> StrResult<()> {
let output = command.output();
// Determine whether we have indexable templates in output // Determine whether we have indexable templates in output
let can_handle_multiple = match output { let can_handle_multiple = match config.output {
Output::Stdout => false, Output::Stdout => false,
Output::Path(ref output) => { Output::Path(ref output) => {
output_template::has_indexable_template(output.to_str().unwrap_or_default()) output_template::has_indexable_template(output.to_str().unwrap_or_default())
} }
}; };
let exported_page_ranges = command.exported_page_ranges();
let exported_pages = document let exported_pages = document
.pages .pages
.iter() .iter()
.enumerate() .enumerate()
.filter(|(i, _)| { .filter(|(i, _)| {
exported_page_ranges.as_ref().map_or(true, |exported_page_ranges| { config.pages.as_ref().map_or(true, |exported_page_ranges| {
exported_page_ranges.includes_page_index(*i) exported_page_ranges.includes_page_index(*i)
}) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !can_handle_multiple && exported_pages.len() > 1 { if !can_handle_multiple && exported_pages.len() > 1 {
let err = match output { let err = match config.output {
Output::Stdout => "to stdout", Output::Stdout => "to stdout",
Output::Path(_) => { Output::Path(_) => {
"without a page number template ({p}, {0p}) in the output path" "without a page number template ({p}, {0p}) in the output path"
@ -266,15 +342,13 @@ fn export_image(
bail!("cannot export multiple images {err}"); bail!("cannot export multiple images {err}");
} }
let cache = world.export_cache();
// The results are collected in a `Vec<()>` which does not allocate. // The results are collected in a `Vec<()>` which does not allocate.
exported_pages exported_pages
.par_iter() .par_iter()
.map(|(i, page)| { .map(|(i, page)| {
// Use output with converted path. // Use output with converted path.
let output = match output { let output = match &config.output {
Output::Path(ref path) => { Output::Path(path) => {
let storage; let storage;
let path = if can_handle_multiple { let path = if can_handle_multiple {
storage = output_template::format( storage = output_template::format(
@ -290,7 +364,10 @@ fn export_image(
// If we are not watching, don't use the cache. // If we are not watching, don't use the cache.
// If the frame is in the cache, skip it. // If the frame is in the cache, skip it.
// If the file does not exist, always create it. // If the file does not exist, always create it.
if watching && cache.is_cached(*i, &page.frame) && path.exists() { if config.watching
&& config.export_cache.is_cached(*i, &page.frame)
&& path.exists()
{
return Ok(()); return Ok(());
} }
@ -299,7 +376,7 @@ fn export_image(
Output::Stdout => Output::Stdout, Output::Stdout => Output::Stdout,
}; };
export_image_page(command, page, &output, fmt)?; export_image_page(config, page, &output, fmt)?;
Ok(()) Ok(())
}) })
.collect::<Result<Vec<()>, EcoString>>()?; .collect::<Result<Vec<()>, EcoString>>()?;
@ -338,14 +415,14 @@ mod output_template {
/// Export single image. /// Export single image.
fn export_image_page( fn export_image_page(
command: &CompileCommand, config: &CompileConfig,
page: &Page, page: &Page,
output: &Output, output: &Output,
fmt: ImageExportFormat, fmt: ImageExportFormat,
) -> StrResult<()> { ) -> StrResult<()> {
match fmt { match fmt {
ImageExportFormat::Png => { ImageExportFormat::Png => {
let pixmap = typst_render::render(page, command.ppi / 72.0); let pixmap = typst_render::render(page, config.ppi / 72.0);
let buf = pixmap let buf = pixmap
.encode_png() .encode_png()
.map_err(|err| eco_format!("failed to encode PNG file ({err})"))?; .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?;
@ -409,12 +486,12 @@ 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, command: &CompileCommand) -> StrResult<()> { fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> {
let Some(ref make_deps_path) = command.make_deps else { return Ok(()) }; let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
let Output::Path(output_path) = command.output() else { let Output::Path(output_path) = &config.output else {
bail!("failed to create make dependencies file because output was stdout") bail!("failed to create make dependencies file because output was stdout")
}; };
let Ok(output_path) = output_path.into_os_string().into_string() else { let Some(output_path) = output_path.as_os_str().to_str() 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")
}; };
@ -452,13 +529,13 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu
fn write( fn write(
make_deps_path: &Path, make_deps_path: &Path,
output_path: String, output_path: &str,
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)?;
file.write_all(munge(&output_path).as_bytes())?; 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 Some(dependency) =
@ -484,21 +561,37 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu
}) })
} }
/// Opens the given file using: /// Opens the output if desired.
/// - The default file viewer if `open` is `None`. fn open_output(config: &mut CompileConfig) -> StrResult<()> {
/// - The given viewer provided by `open` if it is `Some`. let Some(viewer) = config.open.take() else { return Ok(()) };
///
/// If the file could not be opened, an error is returned. #[cfg(feature = "http-server")]
fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { if let Some(server) = &config.server {
let url = format!("http://{}", server.addr());
return open_path(OsStr::new(&url), viewer.as_deref());
}
// Can't open stdout.
let Output::Path(path) = &config.output else { return Ok(()) };
// Some resource openers require the path to be canonicalized. // Some resource openers require the path to be canonicalized.
let path = path let path = path
.canonicalize() .canonicalize()
.map_err(|err| eco_format!("failed to canonicalize path ({err})"))?; .map_err(|err| eco_format!("failed to canonicalize path ({err})"))?;
if let Some(app) = open {
open::with_detached(&path, app) open_path(path.as_os_str(), viewer.as_deref())
.map_err(|err| eco_format!("failed to open file with {} ({})", app, err)) }
/// Opens the given file using:
///
/// - The default file viewer if `app` is `None`.
/// - The given viewer provided by `app` if it is `Some`.
fn open_path(path: &OsStr, viewer: Option<&str>) -> StrResult<()> {
if let Some(viewer) = viewer {
open::with_detached(path, viewer)
.map_err(|err| eco_format!("failed to open file with {} ({})", viewer, err))
} else { } else {
open::that_detached(&path).map_err(|err| { open::that_detached(path).map_err(|err| {
let openers = open::commands(path) let openers = open::commands(path)
.iter() .iter()
.map(|command| command.get_program().to_string_lossy()) .map(|command| command.get_program().to_string_lossy())

View File

@ -6,8 +6,8 @@ use crate::args::FontsCommand;
/// Execute a font listing command. /// Execute a font listing command.
pub fn fonts(command: &FontsCommand) { pub fn fonts(command: &FontsCommand) {
let fonts = Fonts::searcher() let fonts = Fonts::searcher()
.include_system_fonts(!command.font_args.ignore_system_fonts) .include_system_fonts(!command.font.ignore_system_fonts)
.search_with(&command.font_args.font_paths); .search_with(&command.font.font_paths);
for (name, infos) in fonts.book.families() { for (name, infos) in fonts.book.families() {
println!("{name}"); println!("{name}");

View File

@ -15,7 +15,7 @@ use crate::package;
/// Execute an initialization command. /// Execute an initialization command.
pub fn init(command: &InitCommand) -> StrResult<()> { pub fn init(command: &InitCommand) -> StrResult<()> {
let package_storage = package::storage(&command.package_storage_args); let package_storage = package::storage(&command.package);
// Parse the package specification. If the user didn't specify the version, // Parse the package specification. If the user didn't specify the version,
// we try to figure it out automatically by downloading the package index // we try to figure it out automatically by downloading the package index

View File

@ -6,6 +6,8 @@ mod greet;
mod init; mod init;
mod package; mod package;
mod query; mod query;
#[cfg(feature = "http-server")]
mod server;
mod terminal; mod terminal;
mod timings; mod timings;
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
@ -44,6 +46,10 @@ static ARGS: LazyLock<CliArguments> = LazyLock::new(|| {
/// Entry point. /// Entry point.
fn main() -> ExitCode { fn main() -> ExitCode {
// Handle SIGPIPE
// https://stackoverflow.com/questions/65755853/simple-word-count-rust-program-outputs-valid-stdout-but-panicks-when-piped-to-he/65760807
sigpipe::reset();
let res = dispatch(); let res = dispatch();
if let Err(msg) = res { if let Err(msg) = res {
@ -56,11 +62,11 @@ fn main() -> ExitCode {
/// Execute the requested command. /// Execute the requested command.
fn dispatch() -> HintedStrResult<()> { fn dispatch() -> HintedStrResult<()> {
let timer = Timer::new(&ARGS); let mut timer = Timer::new(&ARGS);
match &ARGS.command { match &ARGS.command {
Command::Compile(command) => crate::compile::compile(timer, command.clone())?, Command::Compile(command) => crate::compile::compile(&mut timer, command)?,
Command::Watch(command) => crate::watch::watch(timer, command.clone())?, Command::Watch(command) => crate::watch::watch(&mut timer, command)?,
Command::Init(command) => crate::init::init(command)?, Command::Init(command) => crate::init::init(command)?,
Command::Query(command) => crate::query::query(command)?, Command::Query(command) => crate::query::query(command)?,
Command::Fonts(command) => crate::fonts::fonts(command), Command::Fonts(command) => crate::fonts::fonts(command),

View File

@ -1,10 +1,10 @@
use typst_kit::package::PackageStorage; use typst_kit::package::PackageStorage;
use crate::args::PackageStorageArgs; use crate::args::PackageArgs;
use crate::download; use crate::download;
/// Returns a new package storage for the given args. /// Returns a new package storage for the given args.
pub fn storage(args: &PackageStorageArgs) -> PackageStorage { pub fn storage(args: &PackageArgs) -> PackageStorage {
PackageStorage::new( PackageStorage::new(
args.package_cache_path.clone(), args.package_cache_path.clone(),
args.package_path.clone(), args.package_path.clone(),

View File

@ -3,7 +3,7 @@ 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::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::model::Document; use typst::layout::PagedDocument;
use typst::syntax::Span; use typst::syntax::Span;
use typst::World; use typst::World;
use typst_eval::{eval_string, EvalMode}; use typst_eval::{eval_string, EvalMode};
@ -15,7 +15,7 @@ use crate::world::SystemWorld;
/// Execute a query command. /// Execute a query command.
pub fn query(command: &QueryCommand) -> HintedStrResult<()> { pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
let mut world = SystemWorld::new(&command.common)?; let mut world = SystemWorld::new(&command.input, &command.world, &command.process)?;
// Reset everything and ensure that the main file is present. // Reset everything and ensure that the main file is present.
world.reset(); world.reset();
@ -29,7 +29,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
let data = retrieve(&world, command, &document)?; let data = retrieve(&world, command, &document)?;
let serialized = format(data, command)?; let serialized = format(data, command)?;
println!("{serialized}"); println!("{serialized}");
print_diagnostics(&world, &[], &warnings, command.common.diagnostic_format) print_diagnostics(&world, &[], &warnings, command.process.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
} }
@ -40,7 +40,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
&world, &world,
&errors, &errors,
&warnings, &warnings,
command.common.diagnostic_format, command.process.diagnostic_format,
) )
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
} }
@ -53,7 +53,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
fn retrieve( fn retrieve(
world: &dyn World, world: &dyn World,
command: &QueryCommand, command: &QueryCommand,
document: &Document, document: &PagedDocument,
) -> HintedStrResult<Vec<Content>> { ) -> HintedStrResult<Vec<Content>> {
let selector = eval_string( let selector = eval_string(
&typst::ROUTINES, &typst::ROUTINES,

View File

@ -0,0 +1,218 @@
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::sync::Arc;
use ecow::eco_format;
use parking_lot::{Condvar, Mutex, MutexGuard};
use tiny_http::{Header, Request, Response, StatusCode};
use typst::diag::{bail, StrResult};
use crate::args::{Input, ServerArgs};
/// Serves HTML with live reload.
pub struct HtmlServer {
addr: SocketAddr,
bucket: Arc<Bucket<String>>,
}
impl HtmlServer {
/// Create a new HTTP server that serves live HTML.
pub fn new(input: &Input, args: &ServerArgs) -> StrResult<Self> {
let reload = !args.no_reload;
let (addr, server) = start_server(args.port)?;
let placeholder = PLACEHOLDER_HTML.replace("{INPUT}", &input.to_string());
let bucket = Arc::new(Bucket::new(placeholder));
let bucket2 = bucket.clone();
std::thread::spawn(move || {
for req in server.incoming_requests() {
let _ = handle(req, reload, &bucket2);
}
});
Ok(Self { addr, bucket })
}
/// The address that we serve the HTML on.
pub fn addr(&self) -> SocketAddr {
self.addr
}
/// Updates the HTML, triggering a reload all connected browsers.
pub fn update(&self, html: String) {
self.bucket.put(html);
}
}
/// Starts a local HTTP server.
///
/// Uses the specified port or tries to find a free port in the range
/// `3000..=3005`.
fn start_server(port: Option<u16>) -> StrResult<(SocketAddr, tiny_http::Server)> {
const BASE_PORT: u16 = 3000;
let mut addr;
let mut retries = 0;
let listener = loop {
addr = SocketAddr::new(
IpAddr::V4(Ipv4Addr::LOCALHOST),
port.unwrap_or(BASE_PORT + retries),
);
match TcpListener::bind(addr) {
Ok(listener) => break listener,
Err(err) if err.kind() == io::ErrorKind::AddrInUse => {
if let Some(port) = port {
bail!("port {port} is already in use")
} else if retries < 5 {
// If the port is in use, try the next one.
retries += 1;
} else {
bail!("could not find free port for HTTP server");
}
}
Err(err) => bail!("failed to start TCP server: {err}"),
}
};
let server = tiny_http::Server::from_listener(listener, None)
.map_err(|err| eco_format!("failed to start HTTP server: {err}"))?;
Ok((addr, server))
}
/// Handles a request.
fn handle(req: Request, reload: bool, bucket: &Arc<Bucket<String>>) -> io::Result<()> {
let path = req.url();
match path {
"/" => handle_root(req, reload, bucket),
"/events" => handle_events(req, bucket.clone()),
_ => req.respond(Response::new_empty(StatusCode(404))),
}
}
/// Handles for the `/` route. Serves the compiled HTML.
fn handle_root(req: Request, reload: bool, bucket: &Bucket<String>) -> io::Result<()> {
let mut html = bucket.get().clone();
if reload {
inject_live_reload_script(&mut html);
}
req.respond(Response::new(
StatusCode(200),
vec![Header::from_bytes("Content-Type", "text/html").unwrap()],
html.as_bytes(),
Some(html.len()),
None,
))
}
/// Handler for the `/events` route.
fn handle_events(req: Request, bucket: Arc<Bucket<String>>) -> io::Result<()> {
std::thread::spawn(move || {
// When this returns an error, the client is disconnected and we can
// terminate the thread.
let _ = handle_events_blocking(req, &bucket);
});
Ok(())
}
/// Event stream for the `/events` route.
fn handle_events_blocking(req: Request, bucket: &Bucket<String>) -> io::Result<()> {
let mut writer = req.into_writer();
let writer: &mut dyn Write = &mut *writer;
// We need to write the header manually because `tiny-http` defaults to
// `Transfer-Encoding: chunked` when no `Content-Length` is provided, which
// Chrome & Safari dislike for `Content-Type: text/event-stream`.
write!(writer, "HTTP/1.1 200 OK\r\n")?;
write!(writer, "Content-Type: text/event-stream\r\n")?;
write!(writer, "Cache-Control: no-cache\r\n")?;
write!(writer, "\r\n")?;
writer.flush()?;
// If the user closes the browser tab, this loop will terminate once it
// tries to write to the dead socket for the first time.
loop {
bucket.wait();
// Trigger a server-sent event. The browser is listening to it via
// an `EventSource` listener` (see `inject_script`).
write!(writer, "event: reload\ndata:\n\n")?;
writer.flush()?;
}
}
/// Injects the live reload script into a string of HTML.
fn inject_live_reload_script(html: &mut String) {
let pos = html.rfind("</html>").unwrap_or(html.len());
html.insert_str(pos, LIVE_RELOAD_SCRIPT);
}
/// Holds data and notifies consumers when it's updated.
struct Bucket<T> {
mutex: Mutex<T>,
condvar: Condvar,
}
impl<T> Bucket<T> {
/// Creates a new bucket with initial data.
fn new(init: T) -> Self {
Self { mutex: Mutex::new(init), condvar: Condvar::new() }
}
/// Retrieves the current data in the bucket.
fn get(&self) -> MutexGuard<T> {
self.mutex.lock()
}
/// Puts new data into the bucket and notifies everyone who's currently
/// [waiting](Self::wait).
fn put(&self, data: T) {
*self.mutex.lock() = data;
self.condvar.notify_all();
}
/// Waits for new data in the bucket.
fn wait(&self) {
self.condvar.wait(&mut self.mutex.lock());
}
}
/// The initial HTML before compilation is finished.
const PLACEHOLDER_HTML: &str = "\
<html>
<head>
<title>Waiting for {INPUT}</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
color: #565565;
background: #eff0f3;
}
body > main > div {
margin-block: 16px;
text-align: center;
}
</style>
</head>
<body>
<main>
<div>Waiting for output ...</div>
<div><code>typst watch {INPUT}</code></div>
</main>
</body>
</html>
";
/// Reloads the page whenever it receives a "reload" server-sent event
/// on the `/events` route.
const LIVE_RELOAD_SCRIPT: &str = "\
<script>\
new EventSource(\"/events\")\
.addEventListener(\"reload\", () => location.reload())\
</script>\
";

View File

@ -22,8 +22,8 @@ impl Timer {
/// record timings for a specific function invocation. /// record timings for a specific function invocation.
pub fn new(args: &CliArguments) -> Timer { pub fn new(args: &CliArguments) -> Timer {
let record = match &args.command { let record = match &args.command {
Command::Compile(command) => command.timings.clone(), Command::Compile(command) => command.args.timings.clone(),
Command::Watch(command) => command.timings.clone(), Command::Watch(command) => command.args.timings.clone(),
_ => None, _ => None,
}; };

View File

@ -13,32 +13,38 @@ use same_file::is_same_file;
use typst::diag::{bail, StrResult}; use typst::diag::{bail, StrResult};
use typst::utils::format_duration; use typst::utils::format_duration;
use crate::args::{CompileCommand, Input, Output}; use crate::args::{Input, Output, WatchCommand};
use crate::compile::compile_once; use crate::compile::{compile_once, CompileConfig};
use crate::timings::Timer; use crate::timings::Timer;
use crate::world::{SystemWorld, WorldCreationError}; use crate::world::{SystemWorld, WorldCreationError};
use crate::{print_error, terminal}; use crate::{print_error, terminal};
/// Execute a watching compilation command. /// Execute a watching compilation command.
pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
let Output::Path(output) = command.output() else { let mut config = CompileConfig::watching(command)?;
let Output::Path(output) = &config.output else {
bail!("cannot write document to stdout in watch mode"); bail!("cannot write document to stdout in watch mode");
}; };
// Create a file system watcher. // Create a file system watcher.
let mut watcher = Watcher::new(output)?; let mut watcher = Watcher::new(output.clone())?;
// Create the world that serves sources, files, and fonts. // Create the world that serves sources, files, and fonts.
// Additionally, if any files do not exist, wait until they do. // Additionally, if any files do not exist, wait until they do.
let mut world = loop { let mut world = loop {
match SystemWorld::new(&command.common) { match SystemWorld::new(
&command.args.input,
&command.args.world,
&command.args.process,
) {
Ok(world) => break world, Ok(world) => break world,
Err( Err(
ref err @ (WorldCreationError::InputNotFound(ref path) ref err @ (WorldCreationError::InputNotFound(ref path)
| WorldCreationError::RootNotFound(ref path)), | WorldCreationError::RootNotFound(ref path)),
) => { ) => {
watcher.update([path.clone()])?; watcher.update([path.clone()])?;
Status::Error.print(&command).unwrap(); Status::Error.print(&config).unwrap();
print_error(&err.to_string()).unwrap(); print_error(&err.to_string()).unwrap();
watcher.wait()?; watcher.wait()?;
} }
@ -47,7 +53,7 @@ pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
}; };
// Perform initial compilation. // Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut command, true))??; timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Watch all dependencies of the initial compilation. // Watch all dependencies of the initial compilation.
watcher.update(world.dependencies())?; watcher.update(world.dependencies())?;
@ -61,7 +67,7 @@ pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
world.reset(); world.reset();
// Recompile. // Recompile.
timer.record(&mut world, |world| compile_once(world, &mut command, true))??; timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Evict the cache. // Evict the cache.
comemo::evict(10); comemo::evict(10);
@ -267,8 +273,7 @@ pub enum Status {
impl Status { impl Status {
/// Clear the terminal and render the status message. /// Clear the terminal and render the status message.
pub fn print(&self, command: &CompileCommand) -> io::Result<()> { pub fn print(&self, config: &CompileConfig) -> io::Result<()> {
let output = command.output();
let timestamp = chrono::offset::Local::now().format("%H:%M:%S"); let timestamp = chrono::offset::Local::now().format("%H:%M:%S");
let color = self.color(); let color = self.color();
@ -278,7 +283,7 @@ impl Status {
out.set_color(&color)?; out.set_color(&color)?;
write!(out, "watching")?; write!(out, "watching")?;
out.reset()?; out.reset()?;
match &command.common.input { match &config.input {
Input::Stdin => writeln!(out, " <stdin>"), Input::Stdin => writeln!(out, " <stdin>"),
Input::Path(path) => writeln!(out, " {}", path.display()), Input::Path(path) => writeln!(out, " {}", path.display()),
}?; }?;
@ -286,7 +291,15 @@ impl Status {
out.set_color(&color)?; out.set_color(&color)?;
write!(out, "writing to")?; write!(out, "writing to")?;
out.reset()?; out.reset()?;
writeln!(out, " {output}")?; writeln!(out, " {}", config.output)?;
#[cfg(feature = "http-server")]
if let Some(server) = &config.server {
out.set_color(&color)?;
write!(out, "serving at")?;
out.reset()?;
writeln!(out, " http://{}", server.addr())?;
}
writeln!(out)?; writeln!(out)?;
writeln!(out, "[{timestamp}] {}", self.message())?; writeln!(out, "[{timestamp}] {}", self.message())?;

View File

@ -17,8 +17,7 @@ use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage; use typst_kit::package::PackageStorage;
use typst_timing::timed; use typst_timing::timed;
use crate::args::{Input, SharedArgs}; use crate::args::{Feature, Input, ProcessArgs, WorldArgs};
use crate::compile::ExportCache;
use crate::download::PrintDownload; use crate::download::PrintDownload;
use crate::package; use crate::package;
@ -49,16 +48,17 @@ pub struct SystemWorld {
/// always the same within one compilation. /// always the same within one compilation.
/// Reset between compilations if not [`Now::Fixed`]. /// Reset between compilations if not [`Now::Fixed`].
now: Now, now: Now,
/// The export cache, used for caching output files in `typst watch`
/// sessions.
export_cache: ExportCache,
} }
impl SystemWorld { impl SystemWorld {
/// Create a new system world. /// Create a new system world.
pub fn new(command: &SharedArgs) -> Result<Self, WorldCreationError> { pub fn new(
input: &Input,
world_args: &WorldArgs,
process_args: &ProcessArgs,
) -> Result<Self, WorldCreationError> {
// Set up the thread pool. // Set up the thread pool.
if let Some(jobs) = command.jobs { if let Some(jobs) = process_args.jobs {
rayon::ThreadPoolBuilder::new() rayon::ThreadPoolBuilder::new()
.num_threads(jobs) .num_threads(jobs)
.use_current_thread() .use_current_thread()
@ -67,7 +67,7 @@ impl SystemWorld {
} }
// Resolve the system-global input path. // Resolve the system-global input path.
let input = match &command.input { let input = match input {
Input::Stdin => None, Input::Stdin => None,
Input::Path(path) => { Input::Path(path) => {
Some(path.canonicalize().map_err(|err| match err.kind() { Some(path.canonicalize().map_err(|err| match err.kind() {
@ -81,7 +81,7 @@ impl SystemWorld {
// Resolve the system-global root directory. // Resolve the system-global root directory.
let root = { let root = {
let path = command let path = world_args
.root .root
.as_deref() .as_deref()
.or_else(|| input.as_deref().and_then(|i| i.parent())) .or_else(|| input.as_deref().and_then(|i| i.parent()))
@ -106,23 +106,28 @@ impl SystemWorld {
let library = { let library = {
// Convert the input pairs to a dictionary. // Convert the input pairs to a dictionary.
let inputs: Dict = command let inputs: Dict = world_args
.inputs .inputs
.iter() .iter()
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value())) .map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
.collect(); .collect();
let features = let features = process_args
command.feature.iter().map(|&feature| match feature {}).collect(); .features
.iter()
.map(|&feature| match feature {
Feature::Html => typst::Feature::Html,
})
.collect();
Library::builder().with_inputs(inputs).with_features(features).build() Library::builder().with_inputs(inputs).with_features(features).build()
}; };
let fonts = Fonts::searcher() let fonts = Fonts::searcher()
.include_system_fonts(!command.font_args.ignore_system_fonts) .include_system_fonts(!world_args.font.ignore_system_fonts)
.search_with(&command.font_args.font_paths); .search_with(&world_args.font.font_paths);
let now = match command.creation_timestamp { let now = match world_args.creation_timestamp {
Some(time) => Now::Fixed(time), Some(time) => Now::Fixed(time),
None => Now::System(OnceLock::new()), None => Now::System(OnceLock::new()),
}; };
@ -135,9 +140,8 @@ impl SystemWorld {
book: LazyHash::new(fonts.book), book: LazyHash::new(fonts.book),
fonts: fonts.fonts, fonts: fonts.fonts,
slots: Mutex::new(HashMap::new()), slots: Mutex::new(HashMap::new()),
package_storage: package::storage(&command.package_storage_args), package_storage: package::storage(&world_args.package),
now, now,
export_cache: ExportCache::new(),
}) })
} }
@ -182,11 +186,6 @@ impl SystemWorld {
pub fn lookup(&self, id: FileId) -> Source { pub fn lookup(&self, id: FileId) -> Source {
self.source(id).expect("file id does not point to any source file") self.source(id).expect("file id does not point to any source file")
} }
/// Gets access to the export cache.
pub fn export_cache(&self) -> &ExportCache {
&self.export_cache
}
} }
impl World for SystemWorld { impl World for SystemWorld {

View File

@ -253,8 +253,8 @@ pub fn eval_closure(
// Handle control flow. // Handle control flow.
let output = body.eval(&mut vm)?; let output = body.eval(&mut vm)?;
match vm.flow { match vm.flow {
Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit), Some(FlowEvent::Return(_, Some(explicit), _)) => return Ok(explicit),
Some(FlowEvent::Return(_, None)) => {} Some(FlowEvent::Return(_, None, _)) => {}
Some(flow) => bail!(flow.forbidden()), Some(flow) => bail!(flow.forbidden()),
None => {} None => {}
} }
@ -391,7 +391,9 @@ fn wrap_args_in_math(
} }
Ok(Value::Content( Ok(Value::Content(
callee.display().spanned(callee_span) callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')).pack(), + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')'))
.pack()
.spanned(args.span),
)) ))
} }
@ -591,14 +593,8 @@ mod tests {
use super::*; use super::*;
#[track_caller] #[track_caller]
fn test(text: &str, result: &[&str]) { fn test(scopes: &Scopes, text: &str, result: &[&str]) {
let mut scopes = Scopes::new(None); let mut visitor = CapturesVisitor::new(Some(scopes), Capturer::Function);
scopes.top.define("f", 0);
scopes.top.define("x", 0);
scopes.top.define("y", 0);
scopes.top.define("z", 0);
let mut visitor = CapturesVisitor::new(Some(&scopes), Capturer::Function);
let root = parse(text); let root = parse(text);
visitor.visit(&root); visitor.visit(&root);
@ -611,44 +607,95 @@ mod tests {
#[test] #[test]
fn test_captures() { fn test_captures() {
let mut scopes = Scopes::new(None);
scopes.top.define("f", 0);
scopes.top.define("x", 0);
scopes.top.define("y", 0);
scopes.top.define("z", 0);
let s = &scopes;
// Let binding and function definition. // Let binding and function definition.
test("#let x = x", &["x"]); test(s, "#let x = x", &["x"]);
test("#let x; #(x + y)", &["y"]); test(s, "#let x; #(x + y)", &["y"]);
test("#let f(x, y) = x + y", &[]); test(s, "#let f(x, y) = x + y", &[]);
test("#let f(x, y) = f", &[]); test(s, "#let f(x, y) = f", &[]);
test("#let f = (x, y) => f", &["f"]); test(s, "#let f = (x, y) => f", &["f"]);
// Closure with different kinds of params. // Closure with different kinds of params.
test("#((x, y) => x + z)", &["z"]); test(s, "#((x, y) => x + z)", &["z"]);
test("#((x: y, z) => x + z)", &["y"]); test(s, "#((x: y, z) => x + z)", &["y"]);
test("#((..x) => x + y)", &["y"]); test(s, "#((..x) => x + y)", &["y"]);
test("#((x, y: x + z) => x + y)", &["x", "z"]); test(s, "#((x, y: x + z) => x + y)", &["x", "z"]);
test("#{x => x; x}", &["x"]); test(s, "#{x => x; x}", &["x"]);
// Show rule. // Show rule.
test("#show y: x => x", &["y"]); test(s, "#show y: x => x", &["y"]);
test("#show y: x => x + z", &["y", "z"]); test(s, "#show y: x => x + z", &["y", "z"]);
test("#show x: x => x", &["x"]); test(s, "#show x: x => x", &["x"]);
// For loop. // For loop.
test("#for x in y { x + z }", &["y", "z"]); test(s, "#for x in y { x + z }", &["y", "z"]);
test("#for (x, y) in y { x + y }", &["y"]); test(s, "#for (x, y) in y { x + y }", &["y"]);
test("#for x in y {} #x", &["x", "y"]); test(s, "#for x in y {} #x", &["x", "y"]);
// Import. // Import.
test("#import z: x, y", &["z"]); test(s, "#import z: x, y", &["z"]);
test("#import x + y: x, y, z", &["x", "y"]); test(s, "#import x + y: x, y, z", &["x", "y"]);
// Blocks. // Blocks.
test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); test(s, "#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
test("#[#let x = 1]#x", &["x"]); test(s, "#[#let x = 1]#x", &["x"]);
// Field access. // Field access.
test("#foo(body: 1)", &[]); test(s, "#x.y.f(z)", &["x", "z"]);
test("#(body: 1)", &[]);
test("#(body = 1)", &[]); // Parenthesized expressions.
test("#(body += y)", &["y"]); test(s, "#f(x: 1)", &["f"]);
test("#{ (body, a) = (y, 1) }", &["y"]); test(s, "#(x: 1)", &[]);
test("#(x.at(y) = 5)", &["x", "y"]) test(s, "#(x = 1)", &["x"]);
test(s, "#(x += y)", &["x", "y"]);
test(s, "#{ (x, z) = (y, 1) }", &["x", "y", "z"]);
test(s, "#(x.at(y) = 5)", &["x", "y"]);
}
#[test]
fn test_captures_in_math() {
let mut scopes = Scopes::new(None);
scopes.top.define("f", 0);
scopes.top.define("x", 0);
scopes.top.define("y", 0);
scopes.top.define("z", 0);
// Multi-letter variables are required for math.
scopes.top.define("foo", 0);
scopes.top.define("bar", 0);
scopes.top.define("x-bar", 0);
scopes.top.define("x_bar", 0);
let s = &scopes;
// Basic math identifier differences.
test(s, "$ x f(z) $", &[]); // single letters not captured.
test(s, "$ #x #f(z) $", &["f", "x", "z"]);
test(s, "$ foo f(bar) $", &["bar", "foo"]);
test(s, "$ #foo[#$bar$] $", &["bar", "foo"]);
test(s, "$ #let foo = x; foo $", &["x"]);
// Math idents don't have dashes/underscores
test(s, "$ x-y x_y foo-x x_bar $", &["bar", "foo"]);
test(s, "$ #x-bar #x_bar $", &["x-bar", "x_bar"]);
// Named-params.
test(s, "$ foo(bar: y) $", &["foo"]);
// This should be updated when we improve named-param parsing:
test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]);
// Field access in math.
test(s, "$ foo.bar $", &["foo"]);
test(s, "$ foo.x $", &["foo"]);
test(s, "$ x.foo $", &["foo"]);
test(s, "$ foo . bar $", &["bar", "foo"]);
test(s, "$ foo.x.y.bar(z) $", &["foo"]);
test(s, "$ foo.x-bar $", &["bar", "foo"]);
test(s, "$ foo.x_bar $", &["bar", "foo"]);
test(s, "$ #x_bar.x-bar $", &["x_bar"]);
} }
} }

View File

@ -1,12 +1,15 @@
use ecow::{eco_vec, EcoVec}; use ecow::{eco_vec, EcoVec};
use typst_library::diag::{bail, error, At, SourceResult}; use typst_library::diag::{bail, error, warning, At, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{ use typst_library::foundations::{
ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement,
Value, Selector, Str, Value,
}; };
use typst_library::introspection::{Counter, State};
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode};
use typst_utils::singleton;
use crate::{CapturesVisitor, Eval, Vm}; use crate::{CapturesVisitor, Eval, FlowEvent, Vm};
impl Eval for ast::Code<'_> { impl Eval for ast::Code<'_> {
type Output = Value; type Output = Value;
@ -54,7 +57,8 @@ fn eval_code<'a>(
output = ops::join(output, value).at(span)?; output = ops::join(output, value).at(span)?;
if vm.flow.is_some() { if let Some(event) = &vm.flow {
warn_for_discarded_content(&mut vm.engine, event, &output);
break; break;
} }
} }
@ -355,6 +359,29 @@ impl Eval for ast::Contextual<'_> {
}; };
let func = Func::from(closure).spanned(body.span()); let func = Func::from(closure).spanned(body.span());
Ok(ContextElem::new(func).pack()) Ok(ContextElem::new(func).pack().spanned(body.span()))
} }
} }
/// Emits a warning when we discard content while returning unconditionally.
fn warn_for_discarded_content(engine: &mut Engine, event: &FlowEvent, joined: &Value) {
let FlowEvent::Return(span, Some(_), false) = event else { return };
let Value::Content(tree) = &joined else { return };
let selector = singleton!(
Selector,
Selector::Or(eco_vec![State::select_any(), Counter::select_any()])
);
let mut warning = warning!(
*span,
"this return unconditionally discards the content before it";
hint: "try omitting the `return` to automatically join all values"
);
if tree.query_first(selector).is_some() {
warning.hint("state/counter updates are content that must end up in the document to have an effect");
}
engine.sink.warn(warning);
}

View File

@ -17,8 +17,8 @@ pub enum FlowEvent {
/// Skip the remainder of the current iteration in a loop. /// Skip the remainder of the current iteration in a loop.
Continue(Span), Continue(Span),
/// Stop execution of a function early, optionally returning an explicit /// Stop execution of a function early, optionally returning an explicit
/// value. /// value. The final boolean indicates whether the return was conditional.
Return(Span, Option<Value>), Return(Span, Option<Value>, bool),
} }
impl FlowEvent { impl FlowEvent {
@ -31,7 +31,7 @@ impl FlowEvent {
Self::Continue(span) => { Self::Continue(span) => {
error!(span, "cannot continue outside of loop") error!(span, "cannot continue outside of loop")
} }
Self::Return(span, _) => { Self::Return(span, _, _) => {
error!(span, "cannot return outside of function") error!(span, "cannot return outside of function")
} }
} }
@ -43,13 +43,20 @@ impl Eval for ast::Conditional<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let condition = self.condition(); let condition = self.condition();
if condition.eval(vm)?.cast::<bool>().at(condition.span())? { let output = if condition.eval(vm)?.cast::<bool>().at(condition.span())? {
self.if_body().eval(vm) self.if_body().eval(vm)?
} else if let Some(else_body) = self.else_body() { } else if let Some(else_body) = self.else_body() {
else_body.eval(vm) else_body.eval(vm)?
} else { } else {
Ok(Value::None) Value::None
};
// Mark the return as conditional.
if let Some(FlowEvent::Return(_, _, conditional)) = &mut vm.flow {
*conditional = true;
} }
Ok(output)
} }
} }
@ -95,6 +102,11 @@ impl Eval for ast::WhileLoop<'_> {
vm.flow = flow; vm.flow = flow;
} }
// Mark the return as conditional.
if let Some(FlowEvent::Return(_, _, conditional)) = &mut vm.flow {
*conditional = true;
}
Ok(output) Ok(output)
} }
} }
@ -168,6 +180,11 @@ impl Eval for ast::ForLoop<'_> {
vm.flow = flow; vm.flow = flow;
} }
// Mark the return as conditional.
if let Some(FlowEvent::Return(_, _, conditional)) = &mut vm.flow {
*conditional = true;
}
Ok(output) Ok(output)
} }
} }
@ -200,7 +217,7 @@ impl Eval for ast::FuncReturn<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.body().map(|body| body.eval(vm)).transpose()?; let value = self.body().map(|body| body.eval(vm)).transpose()?;
if vm.flow.is_none() { if vm.flow.is_none() {
vm.flow = Some(FlowEvent::Return(self.span(), value)); vm.flow = Some(FlowEvent::Return(self.span(), value, false));
} }
Ok(Value::None) Ok(Value::None)
} }

View File

@ -148,7 +148,8 @@ pub fn eval_string(
EvalMode::Math => Value::Content( EvalMode::Math => Value::Content(
EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?) EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?)
.with_block(false) .with_block(false)
.pack(), .pack()
.spanned(span),
), ),
}; };

View File

@ -11,6 +11,7 @@ use typst_library::text::{
LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem, LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem,
}; };
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode};
use typst_utils::PicoStr;
use crate::{Eval, Vm}; use crate::{Eval, Vm};
@ -122,7 +123,7 @@ impl Eval for ast::Escape<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get().into()))) Ok(Value::Symbol(Symbol::single(self.get())))
} }
} }
@ -130,7 +131,7 @@ impl Eval for ast::Shorthand<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get().into()))) Ok(Value::Symbol(Symbol::single(self.get())))
} }
} }
@ -204,7 +205,7 @@ impl Eval for ast::Label<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Label(Label::new(self.get()))) Ok(Value::Label(Label::new(PicoStr::intern(self.get()))))
} }
} }
@ -212,7 +213,7 @@ impl Eval for ast::Ref<'_> {
type Output = Content; type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let target = Label::new(self.target()); let target = Label::new(PicoStr::intern(self.target()));
let mut elem = RefElem::new(target); let mut elem = RefElem::new(target);
if let Some(supplement) = self.supplement() { if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content( elem.push_supplement(Smart::Custom(Some(Supplement::Content(

View File

@ -32,7 +32,7 @@ impl Eval for ast::MathShorthand<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get().into()))) Ok(Value::Symbol(Symbol::single(self.get())))
} }
} }

View File

@ -0,0 +1,26 @@
[package]
name = "typst-html"
description = "Typst's HTML exporter."
version = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
[dependencies]
typst-library = { workspace = true }
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
typst-svg = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
[lints]
workspace = true

View File

@ -0,0 +1,146 @@
use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode};
use typst_library::layout::Frame;
use typst_syntax::Span;
/// Encodes an HTML document into a string.
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
let mut w = Writer { pretty: true, ..Writer::default() };
w.buf.push_str("<!DOCTYPE html>");
write_indent(&mut w);
write_element(&mut w, &document.root)?;
Ok(w.buf)
}
#[derive(Default)]
struct Writer {
buf: String,
/// current indentation level
level: usize,
/// pretty printing enabled?
pretty: bool,
}
/// Write a newline and indent, if pretty printing is enabled.
fn write_indent(w: &mut Writer) {
if w.pretty {
w.buf.push('\n');
for _ in 0..w.level {
w.buf.push_str(" ");
}
}
}
/// Encode an HTML node into the writer.
fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
match node {
HtmlNode::Tag(_) => {}
HtmlNode::Text(text, span) => write_text(w, text, *span)?,
HtmlNode::Element(element) => write_element(w, element)?,
HtmlNode::Frame(frame) => write_frame(w, frame),
}
Ok(())
}
/// Encode plain text into the writer.
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
for c in text.chars() {
if charsets::is_valid_in_normal_element_text(c) {
w.buf.push(c);
} else {
write_escape(w, c).at(span)?;
}
}
Ok(())
}
/// Encode one element into the write.
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.buf.push('<');
w.buf.push_str(&element.tag.resolve());
for (attr, value) in &element.attrs.0 {
w.buf.push(' ');
w.buf.push_str(&attr.resolve());
w.buf.push('=');
w.buf.push('"');
for c in value.chars() {
if charsets::is_valid_in_attribute_value(c) {
w.buf.push(c);
} else {
write_escape(w, c).at(element.span)?;
}
}
w.buf.push('"');
}
w.buf.push('>');
if tag::is_void(element.tag) {
return Ok(());
}
let pretty = w.pretty;
if !element.children.is_empty() {
w.pretty &= is_pretty(element);
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_child = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(element) => is_pretty(element),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_child {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_child;
}
w.level -= 1;
write_indent(w)
}
w.pretty = pretty;
w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve());
w.buf.push('>');
Ok(())
}
/// Whether the element should be pretty-printed.
fn is_pretty(element: &HtmlElement) -> bool {
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
}
/// Escape a character.
fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
// See <https://html.spec.whatwg.org/multipage/syntax.html#syntax-charref>
match c {
'&' => w.buf.push_str("&amp;"),
'<' => w.buf.push_str("&lt;"),
'>' => w.buf.push_str("&gt;"),
'"' => w.buf.push_str("&quot;"),
'\'' => w.buf.push_str("&apos;"),
c if charsets::is_w3c_text_char(c) && c != '\r' => {
write!(w.buf, "&#x{:x};", c as u32).unwrap()
}
_ => bail!("the character {} cannot be encoded in HTML", c.repr()),
}
Ok(())
}
/// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &Frame) {
// FIXME: This string replacement is obviously a hack.
let svg = typst_svg::svg_frame(frame)
.replace("<svg class", "<svg style=\"overflow: visible;\" class");
w.buf.push_str(&svg);
}

View File

@ -0,0 +1,315 @@
//! Typst's HTML exporter.
mod encode;
pub use self::encode::html;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode,
};
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
use typst_library::layout::{Abs, Axes, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World;
use typst_syntax::Span;
/// Produce an HTML document from content.
///
/// This first performs root-level realization and then turns the resulting
/// elements into HTML.
#[typst_macros::time(name = "html document")]
pub fn html_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
html_document_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
styles,
)
}
/// The internal implementation of `html_document`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_document_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument(&mut info),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector })
}
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlFragment,
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
handle_list(&mut engine, &mut locator, children.iter().copied())
}
/// Convert children into HTML nodes.
fn handle_list<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert a child into HTML node(s).
fn handle(
engine: &mut Engine,
child: &Content,
locator: &mut SplitLocator,
styles: StyleChain,
output: &mut Vec<HtmlNode>,
) -> SourceResult<()> {
if let Some(elem) = child.to_packed::<TagElem>() {
output.push(HtmlNode::Tag(elem.tag.clone()));
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
let mut children = vec![];
if let Some(body) = elem.body(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
if tag::is_void(elem.tag) && !children.is_empty() {
bail!(elem.span(), "HTML void elements may not have children");
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs(styles).clone(),
children,
span: elem.span(),
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() {
let children = handle_list(engine, locator, elem.children.iter(&styles))?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// FIXME: Very incomplete and hacky, but makes boxes kind fulfill their
// purpose for now.
if let Some(body) = elem.body(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.extend(children);
}
} else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {
output.push(HtmlNode::text(elem.text.clone(), elem.span()));
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
output.push(HtmlNode::text(
if elem.double(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::set_target(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
locator,
styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?;
output.push(HtmlNode::Frame(frame));
} else {
engine.sink.warn(warning!(
child.span(),
"{} was ignored during HTML export",
child.elem().name()
));
}
Ok(())
}
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
/// supplying a suitable `<head>`.
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
let body = match classify_output(output)? {
OutputKind::Html(element) => return Ok(element),
OutputKind::Body(body) => body,
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
};
Ok(HtmlElement::new(tag::html)
.with_children(vec![head_element(info).into(), body.into()]))
}
/// Generate a `<head>` element.
fn head_element(info: &DocumentInfo) -> HtmlElement {
let mut children = vec![];
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "viewport")
.with_attr(attr::content, "width=device-width, initial-scale=1")
.into(),
);
if let Some(title) = &info.title {
children.push(
HtmlElement::new(tag::title)
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
.into(),
);
}
if let Some(description) = &info.description {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "description")
.with_attr(attr::content, description.clone())
.into(),
);
}
HtmlElement::new(tag::head).with_children(children)
}
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len();
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) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag
),
_ => {}
}
}
Ok(OutputKind::Leafs(output))
}
/// What kinds of output the user generated.
enum OutputKind {
/// The user generated their own `<html>` element. We do not need to supply
/// one.
Html(HtmlElement),
/// The user generate their own `<body>` element. We do not need to supply
/// one, but need supply the `<html>` element.
Body(HtmlElement),
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
Leafs(Vec<HtmlNode>),
}

View File

@ -1,7 +1,8 @@
use comemo::Track; use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec}; use ecow::{eco_vec, EcoString, EcoVec};
use typst::foundations::{Label, Styles, Value}; use typst::foundations::{Label, Styles, Value};
use typst::model::{BibliographyElem, Document}; use typst::layout::PagedDocument;
use typst::model::BibliographyElem;
use typst::syntax::{ast, LinkedNode, SyntaxKind}; use typst::syntax::{ast, LinkedNode, SyntaxKind};
use crate::IdeWorld; use crate::IdeWorld;
@ -36,7 +37,7 @@ pub fn analyze_expr(
} }
} }
return typst::trace(world.upcast(), node.span()); return typst::trace::<PagedDocument>(world.upcast(), node.span());
} }
}; };
@ -65,7 +66,9 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option<Value
/// - All labels and descriptions for them, if available /// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after /// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography. /// belong to a bibliography.
pub fn analyze_labels(document: &Document) -> (Vec<(Label, Option<EcoString>)>, usize) { pub fn analyze_labels(
document: &PagedDocument,
) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![]; let mut output = vec![];
// Labels in the document. // Labels in the document.
@ -88,9 +91,7 @@ pub fn analyze_labels(document: &Document) -> (Vec<(Label, Option<EcoString>)>,
let split = output.len(); let split = output.len();
// Bibliography keys. // Bibliography keys.
for (key, detail) in BibliographyElem::keys(document.introspector.track()) { output.extend(BibliographyElem::keys(document.introspector.track()));
output.push((Label::new(key.as_str()), detail));
}
(output, split) (output, split)
} }

View File

@ -9,8 +9,7 @@ use typst::foundations::{
fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr, fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr,
StyleChain, Styles, Type, Value, StyleChain, Styles, Type, Value,
}; };
use typst::layout::{Alignment, Dir}; use typst::layout::{Alignment, Dir, PagedDocument};
use typst::model::Document;
use typst::syntax::ast::AstNode; use typst::syntax::ast::AstNode;
use typst::syntax::{ use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source, ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source,
@ -38,7 +37,7 @@ use crate::{analyze_expr, analyze_import, analyze_labels, named_items, IdeWorld}
/// when the document is available. /// when the document is available.
pub fn autocomplete( pub fn autocomplete(
world: &dyn IdeWorld, world: &dyn IdeWorld,
document: Option<&Document>, document: Option<&PagedDocument>,
source: &Source, source: &Source,
cursor: usize, cursor: usize,
explicit: bool, explicit: bool,
@ -1063,7 +1062,7 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
/// Context for autocompletion. /// Context for autocompletion.
struct CompletionContext<'a> { struct CompletionContext<'a> {
world: &'a (dyn IdeWorld + 'a), world: &'a (dyn IdeWorld + 'a),
document: Option<&'a Document>, document: Option<&'a PagedDocument>,
text: &'a str, text: &'a str,
before: &'a str, before: &'a str,
after: &'a str, after: &'a str,
@ -1079,7 +1078,7 @@ impl<'a> CompletionContext<'a> {
/// Create a new autocompletion context. /// Create a new autocompletion context.
fn new( fn new(
world: &'a (dyn IdeWorld + 'a), world: &'a (dyn IdeWorld + 'a),
document: Option<&'a Document>, document: Option<&'a PagedDocument>,
source: &'a Source, source: &'a Source,
leaf: &'a LinkedNode<'a>, leaf: &'a LinkedNode<'a>,
cursor: usize, cursor: usize,
@ -1254,11 +1253,11 @@ impl<'a> CompletionContext<'a> {
eco_format!( eco_format!(
"{}{}{}", "{}{}{}",
if open { "<" } else { "" }, if open { "<" } else { "" },
label.as_str(), label.resolve(),
if close { ">" } else { "" } if close { ">" } else { "" }
) )
}), }),
label: label.as_str().into(), label: label.resolve().as_str().into(),
detail, detail,
}); });
} }
@ -1507,7 +1506,7 @@ impl BracketMode {
mod tests { mod tests {
use std::collections::BTreeSet; use std::collections::BTreeSet;
use typst::model::Document; use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::World; use typst::World;
@ -1607,7 +1606,7 @@ mod tests {
fn test_full( fn test_full(
world: &TestWorld, world: &TestWorld,
source: &Source, source: &Source,
doc: Option<&Document>, doc: Option<&PagedDocument>,
cursor: isize, cursor: isize,
) -> Response { ) -> Response {
autocomplete(world, doc, source, source.cursor(cursor), true) autocomplete(world, doc, source, source.cursor(cursor), true)

View File

@ -1,6 +1,7 @@
use typst::foundations::{Label, Selector, Value}; use typst::foundations::{Label, Selector, Value};
use typst::model::Document; use typst::layout::PagedDocument;
use typst::syntax::{ast, LinkedNode, Side, Source, Span}; use typst::syntax::{ast, LinkedNode, Side, Source, Span};
use typst::utils::PicoStr;
use crate::utils::globals; use crate::utils::globals;
use crate::{ use crate::{
@ -24,7 +25,7 @@ pub enum Definition {
/// when the document is available. /// when the document is available.
pub fn definition( pub fn definition(
world: &dyn IdeWorld, world: &dyn IdeWorld,
document: Option<&Document>, document: Option<&PagedDocument>,
source: &Source, source: &Source,
cursor: usize, cursor: usize,
side: Side, side: Side,
@ -71,7 +72,7 @@ pub fn definition(
// Try to jump to the referenced content. // Try to jump to the referenced content.
DerefTarget::Ref(node) => { DerefTarget::Ref(node) => {
let label = Label::new(node.cast::<ast::Ref>()?.target()); let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target()));
let selector = Selector::Label(label); let selector = Selector::Label(label);
let elem = document?.introspector.query_first(&selector)?; let elem = document?.introspector.query_first(&selector)?;
return Some(Definition::Span(elem.span())); return Some(Definition::Span(elem.span()));

View File

@ -1,7 +1,7 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use typst::layout::{Frame, FrameItem, Point, Position, Size}; use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
use typst::model::{Destination, Document, 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::Geometry;
use typst::WorldExt; use typst::WorldExt;
@ -30,7 +30,7 @@ impl Jump {
/// Determine where to jump to based on a click in a frame. /// Determine where to jump to based on a click in a frame.
pub fn jump_from_click( pub fn jump_from_click(
world: &dyn IdeWorld, world: &dyn IdeWorld,
document: &Document, document: &PagedDocument,
frame: &Frame, frame: &Frame,
click: Point, click: Point,
) -> Option<Jump> { ) -> Option<Jump> {
@ -110,7 +110,7 @@ pub fn jump_from_click(
/// Find the output location in the document for a cursor position. /// Find the output location in the document for a cursor position.
pub fn jump_from_cursor( pub fn jump_from_cursor(
document: &Document, document: &PagedDocument,
source: &Source, source: &Source,
cursor: usize, cursor: usize,
) -> Vec<Position> { ) -> Vec<Position> {

View File

@ -119,6 +119,10 @@ impl IdeWorld for TestWorld {
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] { fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
const LIST: &[(PackageSpec, Option<EcoString>)] = &[( const LIST: &[(PackageSpec, Option<EcoString>)] = &[(
PackageSpec { PackageSpec {
// NOTE: This literal, `"preview"`, should match the const, `DEFAULT_NAMESPACE`,
// defined in `crates/typst-kit/src/package.rs`. However, we should always use the
// literal here, not `DEFAULT_NAMESPACE`, so that this test fails if its value
// changes in an unexpected way.
namespace: EcoString::inline("preview"), namespace: EcoString::inline("preview"),
name: EcoString::inline("example"), name: EcoString::inline("example"),
version: PackageVersion { major: 0, minor: 1, patch: 0 }, version: PackageVersion { major: 0, minor: 1, patch: 0 },

View File

@ -4,8 +4,7 @@ use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use typst::engine::Sink; use typst::engine::Sink;
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
use typst::layout::Length; use typst::layout::{Length, PagedDocument};
use typst::model::Document;
use typst::syntax::ast::AstNode; use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
use typst::utils::{round_with_precision, Numeric}; use typst::utils::{round_with_precision, Numeric};
@ -21,7 +20,7 @@ use crate::{analyze_expr, analyze_import, analyze_labels, IdeWorld};
/// document is available. /// document is available.
pub fn tooltip( pub fn tooltip(
world: &dyn IdeWorld, world: &dyn IdeWorld,
document: Option<&Document>, document: Option<&PagedDocument>,
source: &Source, source: &Source,
cursor: usize, cursor: usize,
side: Side, side: Side,
@ -173,7 +172,7 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
} }
/// Tooltip for a hovered reference or label. /// Tooltip for a hovered reference or label.
fn label_tooltip(document: &Document, leaf: &LinkedNode) -> Option<Tooltip> { fn label_tooltip(document: &PagedDocument, leaf: &LinkedNode) -> Option<Tooltip> {
let target = match leaf.kind() { let target = match leaf.kind() {
SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'), SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'),
SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'), SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'),
@ -181,7 +180,7 @@ fn label_tooltip(document: &Document, leaf: &LinkedNode) -> Option<Tooltip> {
}; };
for (label, detail) in analyze_labels(document).0 { for (label, detail) in analyze_labels(document).0 {
if label.as_str() == target { if label.resolve().as_str() == target {
return Some(Tooltip::Text(detail?)); return Some(Tooltip::Text(detail?));
} }
} }
@ -338,6 +337,21 @@ mod tests {
fn test_tooltip_closure() { fn test_tooltip_closure() {
test("#let f(x) = x + y", 11, Side::Before) test("#let f(x) = x + y", 11, Side::Before)
.must_be_text("This closure captures `y`"); .must_be_text("This closure captures `y`");
// Same tooltip if `y` is defined first.
test("#let y = 10; #let f(x) = x + y", 24, Side::Before)
.must_be_text("This closure captures `y`");
// Names are sorted.
test("#let f(x) = x + y + z + a", 11, Side::Before)
.must_be_text("This closure captures `a`, `y`, and `z`");
// Names are de-duplicated.
test("#let f(x) = x + y + z + y", 11, Side::Before)
.must_be_text("This closure captures `y` and `z`");
// With arrow syntax.
test("#let f = (x) => x + y", 15, Side::Before)
.must_be_text("This closure captures `y`");
// No recursion with arrow syntax.
test("#let f = (x) => x + y + f", 13, Side::After)
.must_be_text("This closure captures `f` and `y`");
} }
#[test] #[test]

View File

@ -15,6 +15,9 @@ use crate::download::{Downloader, Progress};
/// The default Typst registry. /// The default Typst registry.
pub const DEFAULT_REGISTRY: &str = "https://packages.typst.org"; pub const DEFAULT_REGISTRY: &str = "https://packages.typst.org";
/// The public namespace in the default Typst registry.
pub const DEFAULT_NAMESPACE: &str = "preview";
/// The default packages sub directory within the package and package cache paths. /// The default packages sub directory within the package and package cache paths.
pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages"; pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
@ -28,7 +31,7 @@ pub struct PackageStorage {
package_path: Option<PathBuf>, package_path: Option<PathBuf>,
/// 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 preview namespace. /// The cached index of the default namespace.
index: OnceCell<Vec<PackageInfo>>, index: OnceCell<Vec<PackageInfo>>,
} }
@ -85,7 +88,7 @@ impl PackageStorage {
} }
// Download from network if it doesn't exist yet. // Download from network if it doesn't exist yet.
if spec.namespace == "preview" { if spec.namespace == DEFAULT_NAMESPACE {
self.download_package(spec, &dir, progress)?; self.download_package(spec, &dir, progress)?;
if dir.exists() { if dir.exists() {
return Ok(dir); return Ok(dir);
@ -101,8 +104,8 @@ impl PackageStorage {
&self, &self,
spec: &VersionlessPackageSpec, spec: &VersionlessPackageSpec,
) -> StrResult<PackageVersion> { ) -> StrResult<PackageVersion> {
if spec.namespace == "preview" { if spec.namespace == DEFAULT_NAMESPACE {
// For `@preview`, download the package index and find the latest // For `DEFAULT_NAMESPACE`, download the package index and find the latest
// version. // version.
self.download_index()? self.download_index()?
.iter() .iter()
@ -131,7 +134,7 @@ impl PackageStorage {
pub fn download_index(&self) -> StrResult<&[PackageInfo]> { pub fn download_index(&self) -> StrResult<&[PackageInfo]> {
self.index self.index
.get_or_try_init(|| { .get_or_try_init(|| {
let url = format!("{DEFAULT_REGISTRY}/preview/index.json"); let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
match self.downloader.download(&url) { match self.downloader.download(&url) {
Ok(response) => response.into_json().map_err(|err| { Ok(response) => response.into_json().map_err(|err| {
eco_format!("failed to parse package index: {err}") eco_format!("failed to parse package index: {err}")
@ -148,17 +151,19 @@ impl PackageStorage {
/// Download a package over the network. /// Download a package over the network.
/// ///
/// # Panics /// # Panics
/// Panics if the package spec namespace isn't `preview`. /// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`.
pub fn download_package( pub fn download_package(
&self, &self,
spec: &PackageSpec, spec: &PackageSpec,
package_dir: &Path, package_dir: &Path,
progress: &mut dyn Progress, progress: &mut dyn Progress,
) -> PackageResult<()> { ) -> PackageResult<()> {
assert_eq!(spec.namespace, "preview"); assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
let url = let url = format!(
format!("{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz", spec.name, spec.version); "{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/{}-{}.tar.gz",
spec.name, spec.version
);
let data = match self.downloader.download_with_progress(&url, progress) { let data = match self.downloader.download_with_progress(&url, progress) {
Ok(data) => data, Ok(data) => data,

View File

@ -5,7 +5,7 @@ use std::hash::Hash;
use bumpalo::boxed::Box as BumpBox; use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump; use bumpalo::Bump;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, warning, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{ use typst_library::introspection::{
@ -83,7 +83,11 @@ impl<'a> Collector<'a, '_, '_> {
hint: "try using a `#colbreak()` instead", hint: "try using a `#colbreak()` instead",
); );
} else { } else {
bail!(child.span(), "{} is not allowed here", child.func().name()); self.engine.sink.warn(warning!(
child.span(),
"{} was ignored during paged export",
child.func().name()
));
} }
} }

View File

@ -214,6 +214,13 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
} }
/// Lay out the inner contents of a column. /// Lay out the inner contents of a column.
///
/// Pending floats and footnotes are also laid out at this step. For those,
/// however, we forbid footnote migration (moving the frame containing the
/// footnote reference if the corresponding entry doesn't fit), allowing
/// the footnote invariant to be broken, as it would require handling a
/// [`Stop::Finish`] at this point, but that is exclusively handled by the
/// distributor.
fn column_contents(&mut self, regions: Regions) -> FlowResult<Frame> { fn column_contents(&mut self, regions: Regions) -> FlowResult<Frame> {
// Process pending footnotes. // Process pending footnotes.
for note in std::mem::take(&mut self.work.footnotes) { for note in std::mem::take(&mut self.work.footnotes) {
@ -222,7 +229,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
// Process pending floats. // Process pending floats.
for placed in std::mem::take(&mut self.work.floats) { for placed in std::mem::take(&mut self.work.floats) {
self.float(placed, &regions, false)?; self.float(placed, &regions, false, false)?;
} }
distribute(self, regions) distribute(self, regions)
@ -236,13 +243,21 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
/// (depending on `placed.scope`). /// (depending on `placed.scope`).
/// ///
/// When the float does not fit, it is queued into `work.floats`. The /// When the float does not fit, it is queued into `work.floats`. The
/// value of `clearance` that between the float and flow content is needed /// value of `clearance` indicates that between the float and flow content
/// --- it is set if there are already distributed items. /// is needed --- it is set if there are already distributed items.
///
/// The value of `migratable` determines whether footnotes within the float
/// should be allowed to prompt its migration if they don't fit in order to
/// respect the footnote invariant (entries in the same page as the
/// references), triggering [`Stop::Finish`]. This is usually `true` within
/// the distributor, as it can handle that particular flow event, and
/// `false` elsewhere.
pub fn float( pub fn float(
&mut self, &mut self,
placed: &'b PlacedChild<'a>, placed: &'b PlacedChild<'a>,
regions: &Regions, regions: &Regions,
clearance: bool, clearance: bool,
migratable: bool,
) -> FlowResult<()> { ) -> FlowResult<()> {
// If the float is already processed, skip it. // If the float is already processed, skip it.
let loc = placed.location(); let loc = placed.location();
@ -291,7 +306,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
} }
// Handle footnotes in the float. // Handle footnotes in the float.
self.footnotes(regions, &frame, need, false)?; self.footnotes(regions, &frame, need, false, migratable)?;
// Determine the float's vertical alignment. We can unwrap the inner // Determine the float's vertical alignment. We can unwrap the inner
// `Option` because `Custom(None)` is checked for during collection. // `Option` because `Custom(None)` is checked for during collection.
@ -326,12 +341,19 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
/// Lays out footnotes in the `frame` if this is the root flow and there are /// Lays out footnotes in the `frame` if this is the root flow and there are
/// any. The value of `breakable` indicates whether the element that /// any. The value of `breakable` indicates whether the element that
/// produced the frame is breakable. If not, the frame is treated as atomic. /// produced the frame is breakable. If not, the frame is treated as atomic.
///
/// The value of `migratable` indicates whether footnote migration should be
/// possible (at least for the first footnote found in the frame, as it is
/// forbidden for the second footnote onwards). It is usually `true` within
/// the distributor and `false` elsewhere, as the distributor can handle
/// [`Stop::Finish`] which is returned when migration is requested.
pub fn footnotes( pub fn footnotes(
&mut self, &mut self,
regions: &Regions, regions: &Regions,
frame: &Frame, frame: &Frame,
flow_need: Abs, flow_need: Abs,
breakable: bool, breakable: bool,
migratable: bool,
) -> FlowResult<()> { ) -> FlowResult<()> {
// Footnotes are only supported at the root level. // Footnotes are only supported at the root level.
if !self.config.root { if !self.config.root {
@ -352,7 +374,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
let mut relayout = false; let mut relayout = false;
let mut regions = *regions; let mut regions = *regions;
let mut migratable = !breakable && regions.may_progress(); let mut migratable = migratable && !breakable && regions.may_progress();
for (y, elem) in notes { for (y, elem) in notes {
// The amount of space used by the in-flow content that contains the // The amount of space used by the in-flow content that contains the
@ -470,7 +492,20 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
// Lay out nested footnotes. // Lay out nested footnotes.
for (_, note) in nested { for (_, note) in nested {
self.footnote(note, regions, flow_need, migratable)?; match self.footnote(note, regions, flow_need, migratable) {
// This footnote was already processed or queued.
Ok(_) => {}
// Footnotes always request a relayout when processed for the
// first time, so we ignore a relayout request since we're
// about to do so afterwards. Without this check, the first
// inner footnote interrupts processing of the following ones.
Err(Stop::Relayout(_)) => {}
// Either of
// - A `Stop::Finish` indicating that the frame's origin element
// should migrate to uphold the footnote invariant.
// - A fatal error.
err => return err,
}
} }
// Since we laid out a footnote, we need a relayout. // Since we laid out a footnote, we need a relayout.

View File

@ -240,7 +240,8 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
// Handle fractionally sized blocks. // Handle fractionally sized blocks.
if let Some(fr) = single.fr { if let Some(fr) = single.fr {
self.composer.footnotes(&self.regions, &frame, Abs::zero(), false)?; self.composer
.footnotes(&self.regions, &frame, Abs::zero(), false, true)?;
self.flush_tags(); self.flush_tags();
self.items.push(Item::Fr(fr, Some(single))); self.items.push(Item::Fr(fr, Some(single)));
return Ok(()); return Ok(());
@ -323,8 +324,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
} }
// Handle footnotes. // Handle footnotes.
self.composer self.composer.footnotes(
.footnotes(&self.regions, &frame, frame.height(), breakable)?; &self.regions,
&frame,
frame.height(),
breakable,
true,
)?;
// Push an item for the frame. // Push an item for the frame.
self.regions.size.y -= frame.height(); self.regions.size.y -= frame.height();
@ -347,11 +353,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
placed, placed,
&self.regions, &self.regions,
self.items.iter().any(|item| matches!(item, Item::Frame(..))), self.items.iter().any(|item| matches!(item, Item::Frame(..))),
true,
)?; )?;
self.regions.size.y -= weak_spacing; self.regions.size.y -= weak_spacing;
} else { } else {
let frame = placed.layout(self.composer.engine, self.regions.base())?; let frame = placed.layout(self.composer.engine, self.regions.base())?;
self.composer.footnotes(&self.regions, &frame, Abs::zero(), true)?; self.composer
.footnotes(&self.regions, &frame, Abs::zero(), true, true)?;
self.flush_tags(); self.flush_tags();
self.items.push(Item::Placed(frame, placed)); self.items.push(Item::Placed(frame, placed));
} }

View File

@ -142,7 +142,7 @@ fn layout_fragment_impl(
let arenas = Arenas::default(); let arenas = Arenas::default();
let children = (engine.routines.realize)( let children = (engine.routines.realize)(
RealizationKind::Container, RealizationKind::LayoutFragment,
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,

View File

@ -45,7 +45,7 @@ impl<T> Repeatable<T> {
} }
} }
impl<'a> GridLayouter<'a> { impl GridLayouter<'_> {
/// Layouts the header's rows. /// Layouts the header's rows.
/// Skips regions as necessary. /// Skips regions as necessary.
pub fn layout_header( pub fn layout_header(

View File

@ -85,7 +85,7 @@ pub struct CellMeasurementData<'layouter> {
pub frames_in_previous_regions: usize, pub frames_in_previous_regions: usize,
} }
impl<'a> GridLayouter<'a> { impl GridLayouter<'_> {
/// Layout a rowspan over the already finished regions, plus the current /// Layout a rowspan over the already finished regions, plus the current
/// region's frame and resolved rows, if it wasn't finished yet (because /// region's frame and resolved rows, if it wasn't finished yet (because
/// we're being called from `finish_region`, but note that this function is /// we're being called from `finish_region`, but note that this function is

View File

@ -55,6 +55,7 @@ pub fn layout_image(
elem.alt(styles), elem.alt(styles),
engine.world, engine.world,
&families(styles).collect::<Vec<_>>(), &families(styles).collect::<Vec<_>>(),
elem.flatten_text(styles),
) )
.at(span)?; .at(span)?;

View File

@ -256,8 +256,7 @@ impl<'a> Collector<'a> {
} }
fn push_text(&mut self, text: &str, styles: StyleChain<'a>) { fn push_text(&mut self, text: &str, styles: StyleChain<'a>) {
self.full.push_str(text); self.build_text(styles, |full| full.push_str(text));
self.push_segment(Segment::Text(text.len(), styles));
} }
fn build_text<F>(&mut self, styles: StyleChain<'a>, f: F) fn build_text<F>(&mut self, styles: StyleChain<'a>, f: F)
@ -266,33 +265,33 @@ impl<'a> Collector<'a> {
{ {
let prev = self.full.len(); let prev = self.full.len();
f(&mut self.full); f(&mut self.full);
let len = self.full.len() - prev; let segment_len = self.full.len() - prev;
self.push_segment(Segment::Text(len, styles));
// Merge adjacent text segments with the same styles.
if let Some(Segment::Text(last_len, last_styles)) = self.segments.last_mut() {
if *last_styles == styles {
*last_len += segment_len;
return;
}
}
self.segments.push(Segment::Text(segment_len, styles));
} }
fn push_item(&mut self, item: Item<'a>) { fn push_item(&mut self, item: Item<'a>) {
self.full.push_str(item.textual()); match (self.segments.last_mut(), &item) {
self.push_segment(Segment::Item(item));
}
fn push_segment(&mut self, segment: Segment<'a>) {
match (self.segments.last_mut(), &segment) {
// Merge adjacent text segments with the same styles.
(Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles))
if *last_styles == *styles =>
{
*last_len += *len;
}
// Merge adjacent weak spacing by taking the maximum. // Merge adjacent weak spacing by taking the maximum.
( (
Some(Segment::Item(Item::Absolute(prev_amount, true))), Some(Segment::Item(Item::Absolute(prev_amount, true))),
Segment::Item(Item::Absolute(amount, true)), Item::Absolute(amount, true),
) => { ) => {
*prev_amount = (*prev_amount).max(*amount); *prev_amount = (*prev_amount).max(*amount);
} }
_ => self.segments.push(segment), _ => {
self.full.push_str(item.textual());
self.segments.push(Segment::Item(item));
}
} }
} }
} }

View File

@ -38,7 +38,7 @@ pub struct Line<'a> {
pub dash: Option<Dash>, pub dash: Option<Dash>,
} }
impl<'a> Line<'a> { impl Line<'_> {
/// Create an empty line. /// Create an empty line.
pub fn empty() -> Self { pub fn empty() -> Self {
Self { Self {
@ -685,7 +685,7 @@ impl<'a> Deref for Items<'a> {
} }
} }
impl<'a> DerefMut for Items<'a> { impl DerefMut for Items<'_> {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 &mut self.0
} }

View File

@ -543,7 +543,12 @@ fn raw_ratio(
) -> f64 { ) -> f64 {
// Determine how much the line's spaces would need to be stretched // Determine how much the line's spaces would need to be stretched
// to make it the desired width. // to make it the desired width.
let delta = available_width - line_width; let mut delta = available_width - line_width;
// Avoid possible floating point errors in previous calculation.
if delta.approx_eq(Abs::zero()) {
delta = Abs::zero();
}
// Determine how much stretch or shrink is natural. // Determine how much stretch or shrink is natural.
let adjustability = if delta >= Abs::zero() { stretchability } else { shrinkability }; let adjustability = if delta >= Abs::zero() { stretchability } else { shrinkability };
@ -966,11 +971,13 @@ where
} }
/// Estimates the metrics for the line spanned by the range. /// Estimates the metrics for the line spanned by the range.
#[track_caller]
fn estimate(&self, range: Range) -> T { fn estimate(&self, range: Range) -> T {
self.get(range.end) - self.get(range.start) self.get(range.end) - self.get(range.start)
} }
/// Get the metric at the given byte position. /// Get the metric at the given byte position.
#[track_caller]
fn get(&self, index: usize) -> T { fn get(&self, index: usize) -> T {
match index.checked_sub(1) { match index.checked_sub(1) {
None => T::default(), None => T::default(),

View File

@ -148,7 +148,8 @@ impl MathFragment {
pub fn is_text_like(&self) -> bool { pub fn is_text_like(&self) -> bool {
match self { match self {
Self::Glyph(_) | Self::Variant(_) => self.class() != MathClass::Large, Self::Glyph(glyph) => !glyph.extended_shape,
Self::Variant(variant) => !variant.extended_shape,
MathFragment::Frame(frame) => frame.text_like, MathFragment::Frame(frame) => frame.text_like,
_ => false, _ => false,
} }
@ -247,6 +248,7 @@ pub struct GlyphFragment {
pub dests: SmallVec<[Destination; 1]>, pub dests: SmallVec<[Destination; 1]>,
pub hidden: bool, pub hidden: bool,
pub limits: Limits, pub limits: Limits,
pub extended_shape: bool,
} }
impl GlyphFragment { impl GlyphFragment {
@ -302,6 +304,7 @@ impl GlyphFragment {
span, span,
dests: LinkElem::dests_in(styles), dests: LinkElem::dests_in(styles),
hidden: HideElem::hidden_in(styles), hidden: HideElem::hidden_in(styles),
extended_shape: false,
}; };
fragment.set_id(ctx, id); fragment.set_id(ctx, id);
fragment fragment
@ -332,7 +335,8 @@ impl GlyphFragment {
let accent_attach = let accent_attach =
accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0);
if !is_extended_shape(ctx, id) { let extended_shape = is_extended_shape(ctx, id);
if !extended_shape {
width += italics; width += italics;
} }
@ -342,6 +346,7 @@ impl GlyphFragment {
self.descent = -bbox.y_min.scaled(ctx, self.font_size); self.descent = -bbox.y_min.scaled(ctx, self.font_size);
self.italics_correction = italics; self.italics_correction = italics;
self.accent_attach = accent_attach; self.accent_attach = accent_attach;
self.extended_shape = extended_shape;
} }
pub fn height(&self) -> Abs { pub fn height(&self) -> Abs {
@ -358,6 +363,7 @@ impl GlyphFragment {
math_size: self.math_size, math_size: self.math_size,
span: self.span, span: self.span,
limits: self.limits, limits: self.limits,
extended_shape: self.extended_shape,
frame: self.into_frame(), frame: self.into_frame(),
mid_stretched: None, mid_stretched: None,
} }
@ -465,6 +471,7 @@ pub struct VariantFragment {
pub span: Span, pub span: Span,
pub limits: Limits, pub limits: Limits,
pub mid_stretched: Option<bool>, pub mid_stretched: Option<bool>,
pub extended_shape: bool,
} }
impl VariantFragment { impl VariantFragment {

View File

@ -28,8 +28,21 @@ pub fn layout_lr(
} }
let mut fragments = ctx.layout_into_fragments(body, styles)?; let mut fragments = ctx.layout_into_fragments(body, styles)?;
// Ignore leading and trailing ignorant fragments.
let start_idx = fragments
.iter()
.position(|f| !f.is_ignorant())
.unwrap_or(fragments.len());
let end_idx = fragments
.iter()
.skip(start_idx)
.rposition(|f| !f.is_ignorant())
.map_or(start_idx, |i| start_idx + i + 1);
let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
let max_extent = fragments let max_extent = inner_fragments
.iter() .iter()
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
.max() .max()
@ -39,7 +52,7 @@ pub fn layout_lr(
let height = elem.size(styles); let height = elem.size(styles);
// Scale up fragments at both ends. // Scale up fragments at both ends.
match fragments.as_mut_slice() { match inner_fragments {
[one] => scale(ctx, styles, one, relative_to, height, None), [one] => scale(ctx, styles, one, relative_to, height, None),
[first, .., last] => { [first, .., last] => {
scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening)); scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
@ -49,7 +62,7 @@ pub fn layout_lr(
} }
// Handle MathFragment::Variant fragments that should be scaled up. // Handle MathFragment::Variant fragments that should be scaled up.
for fragment in &mut fragments { for fragment in inner_fragments.iter_mut() {
if let MathFragment::Variant(ref mut variant) = fragment { if let MathFragment::Variant(ref mut variant) = fragment {
if variant.mid_stretched == Some(false) { if variant.mid_stretched == Some(false) {
variant.mid_stretched = Some(true); variant.mid_stretched = Some(true);
@ -60,12 +73,19 @@ pub fn layout_lr(
// Remove weak SpacingFragment immediately after the opening or immediately // Remove weak SpacingFragment immediately after the opening or immediately
// before the closing. // before the closing.
let original_len = fragments.len();
let mut index = 0; let mut index = 0;
let opening_exists = inner_fragments
.first()
.is_some_and(|f| f.class() == MathClass::Opening);
let closing_exists = inner_fragments
.last()
.is_some_and(|f| f.class() == MathClass::Closing);
fragments.retain(|fragment| { fragments.retain(|fragment| {
let discard = (index == start_idx + 1 && opening_exists
|| index + 2 == end_idx && closing_exists)
&& matches!(fragment, MathFragment::Spacing(_, true));
index += 1; index += 1;
(index != 2 && index + 1 != original_len) !discard
|| !matches!(fragment, MathFragment::Spacing(_, true))
}); });
ctx.extend(fragments); ctx.extend(fragments);

View File

@ -127,7 +127,9 @@ fn layout_vec_body(
let denom_style = style_for_denominator(styles); let denom_style = style_for_denominator(styles);
let mut flat = vec![]; let mut flat = vec![];
for child in column { for child in column {
flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?); // 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 // 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 // to ensure that normal vectors are aligned with others unless they are

View File

@ -419,7 +419,10 @@ impl MathRunFrameBuilder {
} }
fn affects_row_height(fragment: &MathFragment) -> bool { fn affects_row_height(fragment: &MathFragment) -> bool {
!matches!(fragment, MathFragment::Align | MathFragment::Linebreak) !matches!(
fragment,
MathFragment::Align | MathFragment::Linebreak | MathFragment::Tag(_)
)
} }
/// Create the spacing between two fragments in a given style. /// Create the spacing between two fragments in a given style.

View File

@ -121,7 +121,6 @@ pub fn stack(
alternator: LeftRightAlternator, alternator: LeftRightAlternator,
minimum_ascent_descent: Option<(Abs, Abs)>, minimum_ascent_descent: Option<(Abs, Abs)>,
) -> Frame { ) -> Frame {
let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
let AlignmentResult { points, width } = alignments(&rows); let AlignmentResult { points, width } = alignments(&rows);
let rows: Vec<_> = rows let rows: Vec<_> = rows
.into_iter() .into_iter()

View File

@ -295,6 +295,7 @@ fn assemble(
span: base.span, span: base.span,
limits: base.limits, limits: base.limits,
mid_stretched: None, mid_stretched: None,
extended_shape: true,
} }
} }

View File

@ -150,8 +150,8 @@ fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
auto_italic auto_italic
&& matches!( && matches!(
c, c,
'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' | 'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' |
'∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' 'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
) )
&& matches!(variant, Sans | Serif), && matches!(variant, Sans | Serif),
); );
@ -306,6 +306,7 @@ fn latin_exception(
('e', Cal, false, _) => '', ('e', Cal, false, _) => '',
('g', Cal, false, _) => '', ('g', Cal, false, _) => '',
('o', Cal, false, _) => '', ('o', Cal, false, _) => '',
('ħ', Serif, .., true) => 'ℏ',
('ı', Serif, .., true) => '𝚤', ('ı', Serif, .., true) => '𝚤',
('ȷ', Serif, .., true) => '𝚥', ('ȷ', Serif, .., true) => '𝚥',
_ => return None, _ => return None,

View File

@ -297,7 +297,7 @@ fn layout_underoverspreader(
if let Some(annotation) = annotation { if let Some(annotation) = annotation {
let under_style = style_for_subscript(styles); let under_style = style_for_subscript(styles);
let annotation_styles = styles.chain(&under_style); let annotation_styles = styles.chain(&under_style);
rows.push(ctx.layout_into_run(annotation, annotation_styles)?); rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
} }
0 0
} }
@ -305,7 +305,7 @@ fn layout_underoverspreader(
if let Some(annotation) = annotation { if let Some(annotation) = annotation {
let over_style = style_for_superscript(styles); let over_style = style_for_superscript(styles);
let annotation_styles = styles.chain(&over_style); let annotation_styles = styles.chain(&over_style);
rows.push(ctx.layout_into_run(annotation, annotation_styles)?); rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
} }
rows.push(stretched.into()); rows.push(stretched.into());
rows.push(MathRun::new(vec![body])); rows.push(MathRun::new(vec![body]));

View File

@ -53,7 +53,7 @@ pub fn collect<'a>(
// The initial styles for the next page are ours unless this is a // The initial styles for the next page are ours unless this is a
// "boundary" pagebreak. Such a pagebreak is generated at the end of // "boundary" pagebreak. Such a pagebreak is generated at the end of
// the scope of a page set rule to ensure a page boundary. It's // the scope of a page set rule to ensure a page boundary. Its
// styles correspond to the styles _before_ the page set rule, so we // styles correspond to the styles _before_ the page set rule, so we
// don't want to apply it to a potential empty page. // don't want to apply it to a potential empty page.
if !pagebreak.boundary(styles) { if !pagebreak.boundary(styles) {

View File

@ -11,8 +11,8 @@ use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{ use typst_library::introspection::{
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem, Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
}; };
use typst_library::layout::{FrameItem, Page, Point}; use typst_library::layout::{FrameItem, Page, PagedDocument, Point};
use typst_library::model::{Document, DocumentInfo}; use typst_library::model::DocumentInfo;
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::World; use typst_library::World;
@ -26,12 +26,12 @@ use self::run::{layout_blank_page, layout_page_run, LayoutedPage};
/// elements. In contrast to [`layout_fragment`](crate::layout_fragment), /// elements. In contrast to [`layout_fragment`](crate::layout_fragment),
/// this does not take regions since the regions are defined by the page /// this does not take regions since the regions are defined by the page
/// configuration in the content and style chain. /// configuration in the content and style chain.
#[typst_macros::time(name = "document")] #[typst_macros::time(name = "layout document")]
pub fn layout_document( pub fn layout_document(
engine: &mut Engine, engine: &mut Engine,
content: &Content, content: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Document> { ) -> SourceResult<PagedDocument> {
layout_document_impl( layout_document_impl(
engine.routines, engine.routines,
engine.world, engine.world,
@ -56,7 +56,7 @@ fn layout_document_impl(
route: Tracked<Route>, route: Tracked<Route>,
content: &Content, content: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Document> { ) -> SourceResult<PagedDocument> {
let mut locator = Locator::root().split(); let mut locator = Locator::root().split();
let mut engine = Engine { let mut engine = Engine {
routines, routines,
@ -75,7 +75,7 @@ fn layout_document_impl(
let arenas = Arenas::default(); let arenas = Arenas::default();
let mut info = DocumentInfo::default(); let mut info = DocumentInfo::default();
let mut children = (engine.routines.realize)( let mut children = (engine.routines.realize)(
RealizationKind::Root(&mut info), RealizationKind::LayoutDocument(&mut info),
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,
@ -84,9 +84,9 @@ fn layout_document_impl(
)?; )?;
let pages = layout_pages(&mut engine, &mut children, locator, styles)?; let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
let introspector = Introspector::new(&pages); let introspector = Introspector::paged(&pages);
Ok(Document { pages, info, introspector }) Ok(PagedDocument { pages, info, introspector })
} }
/// Layouts the document's pages. /// Layouts the document's pages.

View File

@ -114,6 +114,10 @@ pub fn layout_path(
path.close_path(); path.close_path();
} }
if !size.is_finite() {
bail!(elem.span(), "cannot create path with infinite length");
}
// Prepare fill and stroke. // Prepare fill and stroke.
let fill = elem.fill(styles); let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles); let fill_rule = elem.fill_rule(styles);
@ -344,29 +348,48 @@ fn layout_shape(
pod.size = crate::pad::shrink(region.size, &inset); pod.size = crate::pad::shrink(region.size, &inset);
} }
// Layout the child. // If the shape is quadratic, we first measure it to determine its size
frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?; // and then layout with full expansion to force the aspect ratio and
// make sure it's really quadratic.
// If the child is a square or circle, relayout with full expansion into
// square region to make sure the result is really quadratic.
if kind.is_quadratic() { if kind.is_quadratic() {
let length = frame.size().max_by_side().min(pod.size.min_by_side()); let length = match quadratic_size(pod) {
let quad_pod = Region::new(Size::splat(length), Axes::splat(true)); Some(length) => length,
frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?; None => {
// Take as much as the child wants, but without overflowing.
crate::layout_frame(engine, child, locator.relayout(), styles, pod)?
.size()
.max_by_side()
.min(pod.size.min_by_side())
} }
};
pod = Region::new(Size::splat(length), Axes::splat(true));
}
// Layout the child.
frame = crate::layout_frame(engine, child, locator, styles, pod)?;
// Apply the inset. // Apply the inset.
if has_inset { if has_inset {
crate::pad::grow(&mut frame, &inset); crate::pad::grow(&mut frame, &inset);
} }
} else { } else {
// The default size that a shape takes on if it has no child and // The default size that a shape takes on if it has no child and no
// enough space. // forced sizes.
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(region.size);
let mut size = region.expand.select(region.size, default.min(region.size));
if kind.is_quadratic() { let size = if kind.is_quadratic() {
size = Size::splat(size.min_by_side()); Size::splat(match quadratic_size(region) {
} Some(length) => length,
None => default.min_by_side(),
})
} else {
// For each dimension, pick the region size if forced, otherwise
// use the default size (or the region size if the default
// is too large for the region).
region.expand.select(region.size, default)
};
frame = Frame::soft(size); frame = Frame::soft(size);
} }
@ -407,6 +430,24 @@ fn layout_shape(
Ok(frame) Ok(frame)
} }
/// Determines the forced size of a quadratic shape based on the region, if any.
///
/// The size is forced if at least one axis is expanded because `expand` is
/// `true` for axes whose size was manually specified by the user.
fn quadratic_size(region: Region) -> Option<Abs> {
if region.expand.x && region.expand.y {
// If both `width` and `height` are specified, we choose the
// smaller one.
Some(region.size.x.min(region.size.y))
} else if region.expand.x {
Some(region.size.x)
} else if region.expand.y {
Some(region.size.y)
} else {
None
}
}
/// Creates a new rectangle as a path. /// Creates a new rectangle as a path.
pub fn clip_rect( pub fn clip_rect(
size: Size, size: Size,

View File

@ -23,6 +23,7 @@ bitflags = { workspace = true }
bumpalo = { workspace = true } bumpalo = { workspace = true }
chinese-number = { workspace = true } chinese-number = { workspace = true }
ciborium = { workspace = true } ciborium = { workspace = true }
codex = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
csv = { workspace = true } csv = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }

View File

@ -301,6 +301,9 @@ impl Route<'_> {
/// The maximum layout nesting depth. /// The maximum layout nesting depth.
const MAX_LAYOUT_DEPTH: usize = 72; const MAX_LAYOUT_DEPTH: usize = 72;
/// The maximum HTML nesting depth.
const MAX_HTML_DEPTH: usize = 72;
/// The maximum function call nesting depth. /// The maximum function call nesting depth.
const MAX_CALL_DEPTH: usize = 80; const MAX_CALL_DEPTH: usize = 80;
@ -326,6 +329,17 @@ impl Route<'_> {
Ok(()) Ok(())
} }
/// Ensures that we are within the maximum HTML depth.
pub fn check_html_depth(&self) -> HintedStrResult<()> {
if !self.within(Route::MAX_HTML_DEPTH) {
bail!(
"maximum HTML depth exceeded";
hint: "try to reduce the amount of nesting of your HTML",
);
}
Ok(())
}
/// Ensures that we are within the maximum function call depth. /// Ensures that we are within the maximum function call depth.
pub fn check_call_depth(&self) -> StrResult<()> { pub fn check_call_depth(&self) -> StrResult<()> {
if !self.within(Route::MAX_CALL_DEPTH) { if !self.within(Route::MAX_CALL_DEPTH) {
@ -336,6 +350,7 @@ impl Route<'_> {
} }
#[comemo::track] #[comemo::track]
#[allow(clippy::needless_lifetimes)]
impl<'a> Route<'a> { impl<'a> Route<'a> {
/// Whether the given id is part of the route. /// Whether the given id is part of the route.
pub fn contains(&self, id: FileId) -> bool { pub fn contains(&self, id: FileId) -> bool {

View File

@ -187,7 +187,7 @@ impl Args {
self.items.retain(|item| { self.items.retain(|item| {
if item.name.is_some() { if item.name.is_some() {
return true; return true;
}; }
let span = item.value.span; let span = item.value.span;
let spanned = Spanned::new(std::mem::take(&mut item.value.v), span); let spanned = Spanned::new(std::mem::take(&mut item.value.v), span);
match T::from_value(spanned).at(span) { match T::from_value(spanned).at(span) {

View File

@ -815,6 +815,19 @@ impl Array {
/// ///
/// 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. /// function (if given) yields an error.
///
/// 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
/// are in lexicographic order.
///
/// ```example
/// #let array = (
/// (a: 2, b: 4),
/// (a: 1, b: 5),
/// (a: 2, b: 3),
/// )
/// #array.sorted(key: it => (it.a, it.b))
/// ```
#[func] #[func]
pub fn sorted( pub fn sorted(
self, self,

View File

@ -426,7 +426,7 @@ impl Content {
/// selector. /// selector.
/// ///
/// 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; let mut result = None;
self.traverse(&mut |element| { self.traverse(&mut |element| {
if result.is_none() && selector.matches(&element, None) { if result.is_none() && selector.matches(&element, None) {
@ -481,17 +481,20 @@ impl Content {
impl Content { impl Content {
/// Strongly emphasize this content. /// Strongly emphasize this content.
pub fn strong(self) -> Self { pub fn strong(self) -> Self {
StrongElem::new(self).pack() let span = self.span();
StrongElem::new(self).pack().spanned(span)
} }
/// Emphasize this content. /// Emphasize this content.
pub fn emph(self) -> Self { pub fn emph(self) -> Self {
EmphElem::new(self).pack() let span = self.span();
EmphElem::new(self).pack().spanned(span)
} }
/// Underline this content. /// Underline this content.
pub fn underlined(self) -> Self { pub fn underlined(self) -> Self {
UnderlineElem::new(self).pack() let span = self.span();
UnderlineElem::new(self).pack().spanned(span)
} }
/// Link the content somewhere. /// Link the content somewhere.
@ -506,17 +509,24 @@ impl Content {
/// Pad this content at the sides. /// Pad this content at the sides.
pub fn padded(self, padding: Sides<Rel<Length>>) -> Self { pub fn padded(self, padding: Sides<Rel<Length>>) -> Self {
let span = self.span();
PadElem::new(self) PadElem::new(self)
.with_left(padding.left) .with_left(padding.left)
.with_top(padding.top) .with_top(padding.top)
.with_right(padding.right) .with_right(padding.right)
.with_bottom(padding.bottom) .with_bottom(padding.bottom)
.pack() .pack()
.spanned(span)
} }
/// Transform this content's contents without affecting layout. /// Transform this content's contents without affecting layout.
pub fn moved(self, delta: Axes<Rel<Length>>) -> Self { pub fn moved(self, delta: Axes<Rel<Length>>) -> Self {
MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() let span = self.span();
MoveElem::new(self)
.with_dx(delta.x)
.with_dy(delta.y)
.pack()
.spanned(span)
} }
} }

View File

@ -317,6 +317,7 @@ impl Decimal {
}) })
.at(value.span) .at(value.span)
} }
ToDecimal::Decimal(decimal) => Ok(decimal),
} }
} }
} }
@ -429,6 +430,8 @@ impl Hash for Decimal {
/// A value that can be cast to a decimal. /// A value that can be cast to a decimal.
pub enum ToDecimal { pub enum ToDecimal {
/// A decimal to be converted to itself.
Decimal(Decimal),
/// A string with the decimal's representation. /// A string with the decimal's representation.
Str(EcoString), Str(EcoString),
/// An integer to be converted to the equivalent decimal. /// An integer to be converted to the equivalent decimal.
@ -439,7 +442,9 @@ pub enum ToDecimal {
cast! { cast! {
ToDecimal, ToDecimal,
v: Decimal => Self::Decimal(v),
v: i64 => Self::Int(v), v: i64 => Self::Int(v),
v: bool => Self::Int(v as i64),
v: f64 => Self::Float(v), v: f64 => Self::Float(v),
v: Str => Self::Str(EcoString::from(v)), v: Str => Self::Str(EcoString::from(v)),
} }

View File

@ -128,16 +128,21 @@ impl f64 {
#[default(Endianness::Little)] #[default(Endianness::Little)]
endian: Endianness, endian: Endianness,
) -> StrResult<f64> { ) -> StrResult<f64> {
// Convert slice to an array of length 8. // Convert slice to an array of length 4 or 8.
let buf: [u8; 8] = match bytes.as_ref().try_into() { if let Ok(buffer) = <[u8; 8]>::try_from(bytes.as_ref()) {
Ok(buffer) => buffer, return Ok(match endian {
Err(_) => bail!("bytes must have a length of exactly 8"), Endianness::Little => f64::from_le_bytes(buffer),
Endianness::Big => f64::from_be_bytes(buffer),
});
};
if let Ok(buffer) = <[u8; 4]>::try_from(bytes.as_ref()) {
return Ok(match endian {
Endianness::Little => f32::from_le_bytes(buffer),
Endianness::Big => f32::from_be_bytes(buffer),
} as f64);
}; };
Ok(match endian { bail!("bytes must have a length of 4 or 8");
Endianness::Little => f64::from_le_bytes(buf),
Endianness::Big => f64::from_be_bytes(buf),
})
} }
/// Converts a float to bytes. /// Converts a float to bytes.
@ -153,13 +158,25 @@ impl f64 {
#[named] #[named]
#[default(Endianness::Little)] #[default(Endianness::Little)]
endian: Endianness, endian: Endianness,
) -> Bytes { #[named]
match endian { #[default(8)]
size: u32,
) -> StrResult<Bytes> {
Ok(match size {
8 => match endian {
Endianness::Little => self.to_le_bytes(), Endianness::Little => self.to_le_bytes(),
Endianness::Big => self.to_be_bytes(), Endianness::Big => self.to_be_bytes(),
} }
.as_slice() .as_slice()
.into() .into(),
4 => match endian {
Endianness::Little => (self as f32).to_le_bytes(),
Endianness::Big => (self as f32).to_be_bytes(),
}
.as_slice()
.into(),
_ => bail!("size must be either 4 or 8"),
})
} }
} }

View File

@ -443,7 +443,7 @@ pub trait NativeFunc {
Func::from(Self::data()) Func::from(Self::data())
} }
/// Get the function data for the native Rust type. /// Get the function data for the native Rust function.
fn data() -> &'static NativeFuncData; fn data() -> &'static NativeFuncData;
} }
@ -462,6 +462,7 @@ pub struct NativeFuncData {
pub keywords: &'static [&'static str], pub keywords: &'static [&'static str],
/// Whether this function makes use of context. /// Whether this function makes use of context.
pub contextual: bool, pub contextual: bool,
/// Definitions in the scope of the function.
pub scope: LazyLock<Scope>, pub scope: LazyLock<Scope>,
/// A list of parameter information for each parameter. /// A list of parameter information for each parameter.
pub params: LazyLock<Vec<ParamInfo>>, pub params: LazyLock<Vec<ParamInfo>>,

View File

@ -11,7 +11,12 @@ use crate::foundations::{
/// ///
/// The number can be negative, zero, or positive. As Typst uses 64 bits to /// The number can be negative, zero, or positive. As Typst uses 64 bits to
/// store integers, integers cannot be smaller than `{-9223372036854775808}` or /// store integers, integers cannot be smaller than `{-9223372036854775808}` or
/// larger than `{9223372036854775807}`. /// larger than `{9223372036854775807}`. Integer literals are always positive,
/// so a negative integer such as `{-1}` is semantically the negation `-` of the
/// positive literal `1`. A positive integer greater than the maximum value and
/// a negative integer less than or equal to the minimum value cannot be
/// represented as an integer literal, and are instead parsed as a `{float}`.
/// The minimum integer value can still be obtained through integer arithmetic.
/// ///
/// The number can also be specified as hexadecimal, octal, or binary by /// The number can also be specified as hexadecimal, octal, or binary by
/// starting it with a zero followed by either `x`, `o`, or `b`. /// starting it with a zero followed by either `x`, `o`, or `b`.

View File

@ -1,7 +1,7 @@
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_utils::PicoStr; use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::foundations::{func, scope, ty, Repr}; use crate::foundations::{func, scope, ty, Repr, Str};
/// A label for an element. /// A label for an element.
/// ///
@ -45,17 +45,17 @@ use crate::foundations::{func, scope, ty, Repr};
/// Currently, labels can only be attached to elements in markup mode, not in /// Currently, labels can only be attached to elements in markup mode, not in
/// code mode. This might change in the future. /// code mode. This might change in the future.
#[ty(scope, cast)] #[ty(scope, cast)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Label(PicoStr); pub struct Label(PicoStr);
impl Label { impl Label {
/// Creates a label from a string, interning it. /// Creates a label from an interned string.
pub fn new(name: impl Into<PicoStr>) -> Self { pub fn new(name: PicoStr) -> Self {
Self(name.into()) Self(name)
} }
/// Resolves the label to a string. /// Resolves the label to a string.
pub fn as_str(&self) -> &'static str { pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve() self.0.resolve()
} }
@ -71,15 +71,15 @@ impl Label {
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The name of the label. /// The name of the label.
name: PicoStr, name: Str,
) -> Label { ) -> Label {
Self(name) Self(PicoStr::intern(name.as_str()))
} }
} }
impl Repr for Label { impl Repr for Label {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
eco_format!("<{}>", self.as_str()) eco_format!("<{}>", self.resolve())
} }
} }

View File

@ -31,6 +31,8 @@ mod selector;
mod str; mod str;
mod styles; mod styles;
mod symbol; mod symbol;
#[path = "target.rs"]
mod target_;
mod ty; mod ty;
mod value; mod value;
mod version; mod version;
@ -61,6 +63,7 @@ pub use self::selector::*;
pub use self::str::*; pub use self::str::*;
pub use self::styles::*; pub use self::styles::*;
pub use self::symbol::*; pub use self::symbol::*;
pub use self::target_::*;
pub use self::ty::*; pub use self::ty::*;
pub use self::value::*; pub use self::value::*;
pub use self::version::*; pub use self::version::*;
@ -79,6 +82,7 @@ use typst_syntax::Spanned;
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::routines::EvalMode; use crate::routines::EvalMode;
use crate::{Feature, Features};
/// Foundational types and functions. /// Foundational types and functions.
/// ///
@ -88,7 +92,7 @@ use crate::routines::EvalMode;
pub static FOUNDATIONS: Category; pub static FOUNDATIONS: Category;
/// Hook up all `foundations` definitions. /// Hook up all `foundations` definitions.
pub(super) fn define(global: &mut Scope, inputs: Dict) { pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.category(FOUNDATIONS); global.category(FOUNDATIONS);
global.define_type::<bool>(); global.define_type::<bool>();
global.define_type::<i64>(); global.define_type::<i64>();
@ -116,6 +120,9 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) {
global.define_func::<assert>(); global.define_func::<assert>();
global.define_func::<eval>(); global.define_func::<eval>();
global.define_func::<style>(); global.define_func::<style>();
if features.is_enabled(Feature::Html) {
global.define_func::<target>();
}
global.define_module(calc::module()); global.define_module(calc::module());
global.define_module(sys::module(inputs)); global.define_module(sys::module(inputs));
} }

View File

@ -7,7 +7,6 @@ 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 typst_utils::PicoStr;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
@ -647,7 +646,7 @@ impl Repr for str {
'\0' => r.push_str(r"\u{0}"), '\0' => r.push_str(r"\u{0}"),
'\'' => r.push('\''), '\'' => r.push('\''),
'"' => r.push_str(r#"\""#), '"' => r.push_str(r#"\""#),
_ => c.escape_debug().for_each(|c| r.push(c)), _ => r.extend(c.escape_debug()),
} }
} }
r.push('"'); r.push('"');
@ -655,6 +654,12 @@ impl Repr for str {
} }
} }
impl Repr for char {
fn repr(&self) -> EcoString {
EcoString::from(*self).repr()
}
}
impl Add for Str { impl Add for Str {
type Output = Self; type Output = Self;
@ -753,12 +758,6 @@ cast! {
v: Str => v.into(), v: Str => v.into(),
} }
cast! {
PicoStr,
self => Value::Str(self.resolve().into()),
v: Str => v.as_str().into(),
}
cast! { cast! {
String, String,
self => Value::Str(self.into()), self => Value::Str(self.into()),
@ -784,7 +783,7 @@ cast! {
.map_err(|_| "bytes are not valid utf-8")? .map_err(|_| "bytes are not valid utf-8")?
.into() .into()
), ),
v: Label => Self::Str(v.as_str().into()), v: Label => Self::Str(v.resolve().as_str().into()),
v: Type => Self::Str(v.long_name().into()), v: Type => Self::Str(v.long_name().into()),
v: Str => Self::Str(v), v: Str => Self::Str(v),
} }

View File

@ -1,6 +1,3 @@
#[doc(inline)]
pub use typst_macros::symbols;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fmt::{self, Debug, Display, Formatter, Write}; use std::fmt::{self, Debug, Display, Formatter, Write};
@ -8,10 +5,10 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::{Span, Spanned}; use typst_syntax::{is_ident, Span, Spanned};
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::foundations::{cast, func, scope, ty, Array, Func}; use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _};
/// A Unicode symbol. /// A Unicode symbol.
/// ///
@ -46,73 +43,90 @@ use crate::foundations::{cast, func, scope, ty, Array, Func};
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Symbol(Repr); pub struct Symbol(Repr);
/// The character of a symbol, possibly with a function.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct SymChar(char, Option<fn() -> Func>);
/// The internal representation. /// The internal representation.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
enum Repr { enum Repr {
Single(SymChar), /// A native symbol that has no named variant.
Const(&'static [(&'static str, SymChar)]), Single(char),
Multi(Arc<(List, EcoString)>), /// A native symbol with multiple named variants.
Complex(&'static [(&'static str, char)]),
/// A symbol with multiple named variants, where some modifiers may have
/// been applied. Also used for symbols defined at runtime by the user with
/// no modifier applied.
Modified(Arc<(List, EcoString)>),
} }
/// A collection of symbols. /// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
enum List { enum List {
Static(&'static [(&'static str, SymChar)]), Static(&'static [(&'static str, char)]),
Runtime(Box<[(EcoString, SymChar)]>), Runtime(Box<[(EcoString, char)]>),
} }
impl Symbol { impl Symbol {
/// Create a new symbol from a single character. /// Create a new symbol from a single character.
pub const fn single(c: SymChar) -> Self { pub const fn single(c: char) -> Self {
Self(Repr::Single(c)) Self(Repr::Single(c))
} }
/// Create a symbol with a static variant list. /// Create a symbol with a static variant list.
#[track_caller] #[track_caller]
pub const fn list(list: &'static [(&'static str, SymChar)]) -> Self { pub const fn list(list: &'static [(&'static str, char)]) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Const(list)) Self(Repr::Complex(list))
} }
/// Create a symbol with a runtime variant list. /// Create a symbol with a runtime variant list.
#[track_caller] #[track_caller]
pub fn runtime(list: Box<[(EcoString, SymChar)]>) -> Self { pub fn runtime(list: Box<[(EcoString, char)]>) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Multi(Arc::new((List::Runtime(list), EcoString::new())))) Self(Repr::Modified(Arc::new((List::Runtime(list), EcoString::new()))))
} }
/// Get the symbol's char. /// Get the symbol's character.
pub fn get(&self) -> char { pub fn get(&self) -> char {
self.sym().char()
}
/// Resolve the symbol's `SymChar`.
pub fn sym(&self) -> SymChar {
match &self.0 { match &self.0 {
Repr::Single(c) => *c, Repr::Single(c) => *c,
Repr::Const(_) => find(self.variants(), "").unwrap(), Repr::Complex(_) => find(self.variants(), "").unwrap(),
Repr::Multi(arc) => find(self.variants(), &arc.1).unwrap(), Repr::Modified(arc) => find(self.variants(), &arc.1).unwrap(),
} }
} }
/// Try to get the function associated with the symbol, if any. /// Try to get the function associated with the symbol, if any.
pub fn func(&self) -> StrResult<Func> { pub fn func(&self) -> StrResult<Func> {
self.sym() match self.get() {
.func() '⌈' => Ok(crate::math::ceil::func()),
.ok_or_else(|| eco_format!("symbol {self} is not callable")) '⌊' => Ok(crate::math::floor::func()),
'' => Ok(crate::math::accent::dash::func()),
'⋅' | '\u{0307}' => Ok(crate::math::accent::dot::func()),
'¨' => Ok(crate::math::accent::dot_double::func()),
'\u{20db}' => Ok(crate::math::accent::dot_triple::func()),
'\u{20dc}' => Ok(crate::math::accent::dot_quad::func()),
'' => Ok(crate::math::accent::tilde::func()),
'´' => Ok(crate::math::accent::acute::func()),
'˝' => Ok(crate::math::accent::acute_double::func()),
'˘' => Ok(crate::math::accent::breve::func()),
'ˇ' => Ok(crate::math::accent::caron::func()),
'^' => Ok(crate::math::accent::hat::func()),
'`' => Ok(crate::math::accent::grave::func()),
'¯' => Ok(crate::math::accent::macron::func()),
'○' => Ok(crate::math::accent::circle::func()),
'→' => Ok(crate::math::accent::arrow::func()),
'←' => Ok(crate::math::accent::arrow_l::func()),
'↔' => Ok(crate::math::accent::arrow_l_r::func()),
'⇀' => Ok(crate::math::accent::harpoon::func()),
'↼' => Ok(crate::math::accent::harpoon_lt::func()),
_ => bail!("symbol {self} is not callable"),
}
} }
/// Apply a modifier to the symbol. /// Apply a modifier to the symbol.
pub fn modified(mut self, modifier: &str) -> StrResult<Self> { pub fn modified(mut self, modifier: &str) -> StrResult<Self> {
if let Repr::Const(list) = self.0 { if let Repr::Complex(list) = self.0 {
self.0 = Repr::Multi(Arc::new((List::Static(list), EcoString::new()))); self.0 = Repr::Modified(Arc::new((List::Static(list), EcoString::new())));
} }
if let Repr::Multi(arc) = &mut self.0 { if let Repr::Modified(arc) = &mut self.0 {
let (list, modifiers) = Arc::make_mut(arc); let (list, modifiers) = Arc::make_mut(arc);
if !modifiers.is_empty() { if !modifiers.is_empty() {
modifiers.push('.'); modifiers.push('.');
@ -127,11 +141,11 @@ impl Symbol {
} }
/// The characters that are covered by this symbol. /// The characters that are covered by this symbol.
pub fn variants(&self) -> impl Iterator<Item = (&str, SymChar)> { pub fn variants(&self) -> impl Iterator<Item = (&str, char)> {
match &self.0 { match &self.0 {
Repr::Single(c) => Variants::Single(Some(*c).into_iter()), Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
Repr::Const(list) => Variants::Static(list.iter()), Repr::Complex(list) => Variants::Static(list.iter()),
Repr::Multi(arc) => arc.0.variants(), Repr::Modified(arc) => arc.0.variants(),
} }
} }
@ -139,7 +153,7 @@ impl Symbol {
pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ { pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ {
let mut set = BTreeSet::new(); let mut set = BTreeSet::new();
let modifiers = match &self.0 { let modifiers = match &self.0 {
Repr::Multi(arc) => arc.1.as_str(), Repr::Modified(arc) => arc.1.as_str(),
_ => "", _ => "",
}; };
for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { for modifier in self.variants().flat_map(|(name, _)| name.split('.')) {
@ -192,7 +206,14 @@ impl Symbol {
if list.iter().any(|(prev, _)| &v.0 == prev) { if list.iter().any(|(prev, _)| &v.0 == prev) {
bail!(span, "duplicate variant"); bail!(span, "duplicate variant");
} }
list.push((v.0, SymChar::pure(v.1))); if !v.0.is_empty() {
for modifier in v.0.split('.') {
if !is_ident(modifier) {
bail!(span, "invalid symbol modifier: {}", modifier.repr());
}
}
}
list.push((v.0, v.1));
} }
Ok(Symbol::runtime(list.into_boxed_slice())) Ok(Symbol::runtime(list.into_boxed_slice()))
} }
@ -204,40 +225,12 @@ impl Display for Symbol {
} }
} }
impl SymChar {
/// Create a symbol character without a function.
pub const fn pure(c: char) -> Self {
Self(c, None)
}
/// Create a symbol character with a function.
pub const fn with_func(c: char, func: fn() -> Func) -> Self {
Self(c, Some(func))
}
/// Get the character of the symbol.
pub const fn char(&self) -> char {
self.0
}
/// Get the function associated with the symbol.
pub fn func(&self) -> Option<Func> {
self.1.map(|f| f())
}
}
impl From<char> for SymChar {
fn from(c: char) -> Self {
SymChar(c, None)
}
}
impl Debug for Repr { impl Debug for Repr {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Single(c) => Debug::fmt(c, f), Self::Single(c) => Debug::fmt(c, f),
Self::Const(list) => list.fmt(f), Self::Complex(list) => list.fmt(f),
Self::Multi(lists) => lists.fmt(f), Self::Modified(lists) => lists.fmt(f),
} }
} }
} }
@ -286,20 +279,20 @@ cast! {
let mut iter = array.into_iter(); let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) { match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?),
_ => Err("point array must contain exactly two entries")?, _ => Err("variant array must contain exactly two entries")?,
} }
}, },
} }
/// Iterator over variants. /// Iterator over variants.
enum Variants<'a> { enum Variants<'a> {
Single(std::option::IntoIter<SymChar>), Single(std::option::IntoIter<char>),
Static(std::slice::Iter<'static, (&'static str, SymChar)>), Static(std::slice::Iter<'static, (&'static str, char)>),
Runtime(std::slice::Iter<'a, (EcoString, SymChar)>), Runtime(std::slice::Iter<'a, (EcoString, char)>),
} }
impl<'a> Iterator for Variants<'a> { impl<'a> Iterator for Variants<'a> {
type Item = (&'a str, SymChar); type Item = (&'a str, char);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self {
@ -312,9 +305,9 @@ impl<'a> Iterator for Variants<'a> {
/// Find the best symbol from the list. /// Find the best symbol from the list.
fn find<'a>( fn find<'a>(
variants: impl Iterator<Item = (&'a str, SymChar)>, variants: impl Iterator<Item = (&'a str, char)>,
modifiers: &str, modifiers: &str,
) -> Option<SymChar> { ) -> Option<char> {
let mut best = None; let mut best = None;
let mut best_score = None; let mut best_score = None;

View File

@ -0,0 +1,38 @@
use comemo::Tracked;
use crate::diag::HintedStrResult;
use crate::foundations::{elem, func, Cast, Context};
/// The compilation target.
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)]
pub enum Target {
/// The target that is used for paged, fully laid-out content.
#[default]
Paged,
/// The target that is used for HTML export.
Html,
}
impl Target {
/// Whether this is the HTML target.
pub fn is_html(self) -> bool {
self == Self::Html
}
}
/// This element exists solely to host the `target` style chain field.
/// It is never constructed and not visible to users.
#[elem]
pub struct TargetElem {
/// The compilation target.
pub target: Target,
}
/// Returns the current compilation target.
#[func(contextual)]
pub fn target(
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?))
}

View File

@ -199,6 +199,7 @@ pub trait NativeType {
pub struct NativeTypeData { pub struct NativeTypeData {
/// The type's normal name (e.g. `str`), as exposed to Typst. /// The type's normal name (e.g. `str`), as exposed to Typst.
pub name: &'static str, pub name: &'static str,
/// The type's long name (e.g. `string`), for error messages.
pub long_name: &'static str, pub long_name: &'static str,
/// The function's title case name (e.g. `String`). /// The function's title case name (e.g. `String`).
pub title: &'static str, pub title: &'static str,
@ -208,6 +209,7 @@ pub struct NativeTypeData {
pub keywords: &'static [&'static str], pub keywords: &'static [&'static str],
/// The constructor for this type. /// The constructor for this type.
pub constructor: LazyLock<Option<&'static NativeFuncData>>, pub constructor: LazyLock<Option<&'static NativeFuncData>>,
/// Definitions in the scope of the type.
pub scope: LazyLock<Scope>, pub scope: LazyLock<Scope>,
} }

View File

@ -0,0 +1,625 @@
use std::fmt::{self, Debug, Display, Formatter};
use ecow::{EcoString, EcoVec};
use typst_syntax::Span;
use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{cast, Dict, Repr, Str};
use crate::introspection::{Introspector, Tag};
use crate::layout::Frame;
use crate::model::DocumentInfo;
/// An HTML document.
#[derive(Debug, Clone)]
pub struct HtmlDocument {
/// The document's root HTML element.
pub root: HtmlElement,
/// Details about the document.
pub info: DocumentInfo,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
}
/// A child of an HTML element.
#[derive(Debug, Clone, Hash)]
pub enum HtmlNode {
/// An introspectable element that produced something within this node.
Tag(Tag),
/// Plain text.
Text(EcoString, Span),
/// Another element.
Element(HtmlElement),
/// A frame that will be displayed as an embedded SVG.
Frame(Frame),
}
impl HtmlNode {
/// Create a plain text node.
pub fn text(text: impl Into<EcoString>, span: Span) -> Self {
Self::Text(text.into(), span)
}
}
impl From<HtmlElement> for HtmlNode {
fn from(element: HtmlElement) -> Self {
Self::Element(element)
}
}
/// An HTML element.
#[derive(Debug, Clone, Hash)]
pub struct HtmlElement {
/// The HTML tag.
pub tag: HtmlTag,
/// The element's attributes.
pub attrs: HtmlAttrs,
/// The element's children.
pub children: Vec<HtmlNode>,
/// The span from which the element originated, if any.
pub span: Span,
}
impl HtmlElement {
/// Create a new, blank element without attributes or children.
pub fn new(tag: HtmlTag) -> Self {
Self {
tag,
attrs: HtmlAttrs::default(),
children: vec![],
span: Span::detached(),
}
}
/// Attach children to the element.
///
/// Note: This overwrites potential previous children.
pub fn with_children(mut self, children: Vec<HtmlNode>) -> Self {
self.children = children;
self
}
/// Add an atribute to the element.
pub fn with_attr(mut self, key: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.push(key, value);
self
}
/// Attach a span to the element.
pub fn spanned(mut self, span: Span) -> Self {
self.span = span;
self
}
}
/// The tag of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlTag(PicoStr);
impl HtmlTag {
/// Intern an HTML tag string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("tag name must not be empty");
}
if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) {
bail!("the character {} is not valid in a tag name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlTag`.
///
/// Should only be used in const contexts because it can panic.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("tag name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
if !bytes[i].is_ascii_alphanumeric() {
panic!("constant tag name must be ASCII alphanumeric");
}
i += 1;
}
Self(PicoStr::constant(string))
}
/// Resolves the tag to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the tag into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlTag {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "<{}>", self.resolve())
}
}
cast! {
HtmlTag,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Attributes of an HTML element.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
impl HtmlAttrs {
/// Add an attribute.
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.push((attr, value.into()));
}
}
cast! {
HtmlAttrs,
self => self.0
.into_iter()
.map(|(key, value)| (key.resolve().as_str().into(), value.into_value()))
.collect::<Dict>()
.into_value(),
values: Dict => Self(values
.into_iter()
.map(|(k, v)| {
let attr = HtmlAttr::intern(&k)?;
let value = v.cast::<EcoString>()?;
Ok((attr, value))
})
.collect::<HintedStrResult<_>>()?),
}
/// An attribute of an HTML.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr);
impl HtmlAttr {
/// Intern an HTML attribute string at runtime.
pub fn intern(string: &str) -> StrResult<Self> {
if string.is_empty() {
bail!("attribute name must not be empty");
}
if let Some(c) =
string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c))
{
bail!("the character {} is not valid in an attribute name", c.repr());
}
Ok(Self(PicoStr::intern(string)))
}
/// Creates a compile-time constant `HtmlAttr`.
///
/// Should only be used in const contexts because it can panic.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
panic!("attribute name must not be empty");
}
let bytes = string.as_bytes();
let mut i = 0;
while i < bytes.len() {
if !bytes[i].is_ascii_alphanumeric() {
panic!("constant attribute name must be ASCII alphanumeric");
}
i += 1;
}
Self(PicoStr::constant(string))
}
/// Resolves the attribute to a string.
pub fn resolve(self) -> ResolvedPicoStr {
self.0.resolve()
}
/// Turns the attribute into its inner interned string.
pub const fn into_inner(self) -> PicoStr {
self.0
}
}
impl Debug for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for HtmlAttr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.resolve())
}
}
cast! {
HtmlAttr,
self => self.0.resolve().as_str().into_value(),
v: Str => Self::intern(&v)?,
}
/// Defines syntactical properties of HTML tags, attributes, and text.
pub mod charsets {
/// Check whether a character is in a tag name.
pub const fn is_valid_in_tag_name(c: char) -> bool {
c.is_ascii_alphanumeric()
}
/// Check whether a character is valid in an attribute name.
pub const fn is_valid_in_attribute_name(c: char) -> bool {
match c {
// These are forbidden.
'\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false,
c if is_whatwg_control_char(c) => false,
c if is_whatwg_non_char(c) => false,
// _Everything_ else is allowed, including U+2029 paragraph
// separator. Go wild.
_ => true,
}
}
/// Check whether a character can be an used in an attribute value without
/// escaping.
///
/// See <https://html.spec.whatwg.org/multipage/syntax.html#attributes-2>
pub const fn is_valid_in_attribute_value(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Quotation marks are not allowed in double-quote-delimited attribute
// values.
'"' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check whether a character can be an used in normal text without
/// escaping.
pub const fn is_valid_in_normal_element_text(c: char) -> bool {
match c {
// Ampersands are sometimes legal (i.e. when they are not _ambiguous
// ampersands_) but it is not worth the trouble to check for that.
'&' => false,
// Less-than signs are not allowed in text.
'<' => false,
// All other text characters are allowed.
c => is_w3c_text_char(c),
}
}
/// Check if something is valid text in HTML.
pub const fn is_w3c_text_char(c: char) -> bool {
match c {
// Non-characters are obviously not text characters.
c if is_whatwg_non_char(c) => false,
// Control characters are disallowed, except for whitespace.
c if is_whatwg_control_char(c) => c.is_ascii_whitespace(),
// Everything else is allowed.
_ => true,
}
}
const fn is_whatwg_non_char(c: char) -> bool {
match c {
'\u{fdd0}'..='\u{fdef}' => true,
// Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive).
c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true,
_ => false,
}
}
const fn is_whatwg_control_char(c: char) -> bool {
match c {
// C0 control characters.
'\u{00}'..='\u{1f}' => true,
// Other control characters.
'\u{7f}'..='\u{9f}' => true,
_ => false,
}
}
}
/// Predefined constants for HTML tags.
pub mod tag {
use super::HtmlTag;
macro_rules! tags {
($($tag:ident)*) => {
$(#[allow(non_upper_case_globals)]
pub const $tag: HtmlTag = HtmlTag::constant(
stringify!($tag)
);)*
}
}
tags! {
a
abbr
address
area
article
aside
audio
b
base
bdi
bdo
blockquote
body
br
button
canvas
caption
cite
code
col
colgroup
data
datalist
dd
del
details
dfn
dialog
div
dl
dt
em
embed
fieldset
figcaption
figure
footer
form
h1
h2
h3
h4
h5
h6
head
header
hgroup
hr
html
i
iframe
img
input
ins
kbd
label
legend
li
link
main
map
mark
menu
meta
meter
nav
noscript
object
ol
optgroup
option
output
p
param
picture
pre
progress
q
rp
rt
ruby
s
samp
script
search
section
select
slot
small
source
span
strong
style
sub
summary
sup
table
tbody
td
template
textarea
tfoot
th
thead
time
title
tr
track
u
ul
var
video
wbr
}
/// Whether nodes with the tag have the CSS property `display: block` by
/// default.
///
/// If this is true, then pretty-printing can insert spaces around such
/// nodes and around the contents of such nodes.
///
/// However, when users change the properties of such tags via CSS, the
/// insertion of whitespace may actually impact the visual output; for
/// example, <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how
/// adding CSS rules to `<p>` can make it sensitive to whitespace. In such
/// cases, users should disable pretty-printing.
pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::html
| self::head
| self::body
| self::article
| self::aside
| self::h1
| self::h2
| self::h3
| self::h4
| self::h5
| self::h6
| self::hgroup
| self::nav
| self::section
| self::dd
| self::dl
| self::dt
| self::menu
| self::ol
| self::ul
| self::address
| self::blockquote
| self::dialog
| self::div
| self::fieldset
| self::figure
| self::figcaption
| self::footer
| self::form
| self::header
| self::hr
| self::legend
| self::main
| self::p
| self::pre
| self::search
)
}
/// Whether the element is inline-level as opposed to being block-level.
///
/// Not sure whether this distinction really makes sense. But we somehow
/// need to decide what to put into automatic paragraphs. A `<strong>`
/// should merged into a paragraph created by realization, but a `<div>`
/// shouldn't.
///
/// <https://www.w3.org/TR/html401/struct/global.html#block-inline>
/// <https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content>
/// <https://github.com/orgs/mdn/discussions/353>
pub fn is_inline_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::abbr
| self::a
| self::bdi
| self::b
| self::br
| self::bdo
| self::code
| self::cite
| self::dfn
| self::data
| self::i
| self::em
| self::mark
| self::kbd
| self::rp
| self::q
| self::ruby
| self::rt
| self::samp
| self::s
| self::span
| self::small
| self::sub
| self::strong
| self::time
| self::sup
| self::var
| self::u
)
}
/// Whether this is a void tag whose associated element may not have a
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
}
/// Predefined constants for HTML attributes.
///
/// Note: These are very incomplete.
pub mod attr {
use super::HtmlAttr;
macro_rules! attrs {
($($attr:ident)*) => {
$(#[allow(non_upper_case_globals)]
pub const $attr: HtmlAttr = HtmlAttr::constant(
stringify!($attr)
);)*
}
}
attrs! {
charset
content
href
name
value
}
}

View File

@ -0,0 +1,59 @@
//! HTML output.
mod dom;
pub use self::dom::*;
use ecow::EcoString;
use crate::foundations::{category, elem, Category, Content, Module, Scope};
/// HTML output.
#[category]
pub static HTML: Category;
/// Create a module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.category(HTML);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
Module::new("html", html)
}
/// A HTML element that can contain Typst content.
#[elem(name = "elem")]
pub struct HtmlElem {
/// The element's tag.
#[required]
pub tag: HtmlTag,
/// The element's attributes.
#[borrowed]
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl HtmlElem {
/// Add an atribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.get_or_insert_with(Default::default).push(attr, value);
self
}
}
/// An element that forces its contents to be laid out.
///
/// Integrates content that requires layout (e.g. a plot) into HTML output
/// by turning it into an inline SVG.
#[elem]
pub struct FrameElem {
/// The contents that shall be laid out.
#[positional]
#[required]
pub body: Content,
}

View File

@ -393,6 +393,11 @@ impl Counter {
let context = Context::new(Some(location), styles); let context = Context::new(Some(location), styles);
state.display(engine, context.track(), &numbering) state.display(engine, context.track(), &numbering)
} }
/// Selects all state updates.
pub fn select_any() -> Selector {
CounterUpdateElem::elem().select()
}
} }
#[scope] #[scope]

View File

@ -10,6 +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::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;
@ -47,9 +48,15 @@ type Pair = (Content, Position);
impl Introspector { impl Introspector {
/// Creates an introspector for a page list. /// Creates an introspector for a page list.
#[typst_macros::time(name = "introspect")] #[typst_macros::time(name = "introspect pages")]
pub fn new(pages: &[Page]) -> Self { pub fn paged(pages: &[Page]) -> Self {
IntrospectorBuilder::new().build(pages) IntrospectorBuilder::new().build_paged(pages)
}
/// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")]
pub fn html(root: &HtmlElement) -> Self {
IntrospectorBuilder::new().build_html(root)
} }
/// Iterates over all locatable elements. /// Iterates over all locatable elements.
@ -346,6 +353,7 @@ impl Clone for QueryCache {
/// Builds the introspector. /// Builds the introspector.
#[derive(Default)] #[derive(Default)]
struct IntrospectorBuilder { struct IntrospectorBuilder {
pages: usize,
page_numberings: Vec<Option<Numbering>>, page_numberings: Vec<Option<Numbering>>,
page_supplements: Vec<Content>, page_supplements: Vec<Content>,
seen: HashSet<Location>, seen: HashSet<Location>,
@ -361,46 +369,37 @@ impl IntrospectorBuilder {
Self::default() Self::default()
} }
/// Build the introspector. /// Build an introspector for a page list.
fn build(mut self, pages: &[Page]) -> Introspector { fn build_paged(mut self, pages: &[Page]) -> Introspector {
self.pages = pages.len();
self.page_numberings.reserve(pages.len()); self.page_numberings.reserve(pages.len());
self.page_supplements.reserve(pages.len()); self.page_supplements.reserve(pages.len());
// Discover all elements. // Discover all elements.
let mut root = Vec::new(); let mut elems = Vec::new();
for (i, page) in pages.iter().enumerate() { for (i, page) in pages.iter().enumerate() {
self.page_numberings.push(page.numbering.clone()); self.page_numberings.push(page.numbering.clone());
self.page_supplements.push(page.supplement.clone()); self.page_supplements.push(page.supplement.clone());
self.discover( self.discover_in_frame(
&mut root, &mut elems,
&page.frame, &page.frame,
NonZeroUsize::new(1 + i).unwrap(), NonZeroUsize::new(1 + i).unwrap(),
Transform::identity(), Transform::identity(),
); );
} }
self.locations.reserve(self.seen.len()); self.finalize(elems)
// Save all pairs and their descendants in the correct order.
let mut elems = Vec::with_capacity(self.seen.len());
for pair in root {
self.visit(&mut elems, pair);
} }
Introspector { /// Build an introspector for an HTML document.
pages: pages.len(), fn build_html(mut self, root: &HtmlElement) -> Introspector {
page_numberings: self.page_numberings, let mut elems = Vec::new();
page_supplements: self.page_supplements, self.discover_in_html(&mut elems, root);
elems, self.finalize(elems)
keys: self.keys,
locations: self.locations,
labels: self.labels,
queries: QueryCache::default(),
}
} }
/// Processes the tags in the frame. /// Processes the tags in the frame.
fn discover( fn discover_in_frame(
&mut self, &mut self,
sink: &mut Vec<Pair>, sink: &mut Vec<Pair>,
frame: &Frame, frame: &Frame,
@ -416,27 +415,83 @@ impl IntrospectorBuilder {
if let Some(parent) = group.parent { if let Some(parent) = group.parent {
let mut nested = vec![]; let mut nested = vec![];
self.discover(&mut nested, &group.frame, page, ts); self.discover_in_frame(&mut nested, &group.frame, page, ts);
self.insertions.insert(parent, nested); self.insertions.insert(parent, nested);
} else { } else {
self.discover(sink, &group.frame, page, ts); self.discover_in_frame(sink, &group.frame, page, ts);
} }
} }
FrameItem::Tag(Tag::Start(elem)) => { FrameItem::Tag(tag) => {
let loc = elem.location().unwrap(); self.discover_in_tag(
if self.seen.insert(loc) { sink,
let point = pos.transform(ts); tag,
sink.push((elem.clone(), Position { page, point })); Position { page, point: pos.transform(ts) },
} );
}
FrameItem::Tag(Tag::End(loc, key)) => {
self.keys.insert(*key, *loc);
} }
_ => {} _ => {}
} }
} }
} }
/// 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 {
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::Frame(frame) => self.discover_in_frame(
sink,
frame,
NonZeroUsize::ONE,
Transform::identity(),
),
}
}
}
/// Handle a tag.
fn discover_in_tag(&mut self, sink: &mut Vec<Pair>, tag: &Tag, position: Position) {
match tag {
Tag::Start(elem) => {
let loc = elem.location().unwrap();
if self.seen.insert(loc) {
sink.push((elem.clone(), position));
}
}
Tag::End(loc, key) => {
self.keys.insert(*key, *loc);
}
}
}
/// Build a complete introspector with all acceleration structures from a
/// list of top-level pairs.
fn finalize(mut self, root: Vec<Pair>) -> Introspector {
self.locations.reserve(self.seen.len());
// Save all pairs and their descendants in the correct order.
let mut elems = Vec::with_capacity(self.seen.len());
for pair in root {
self.visit(&mut elems, pair);
}
Introspector {
pages: self.pages,
page_numberings: self.page_numberings,
page_supplements: self.page_supplements,
elems,
keys: self.keys,
locations: self.locations,
labels: self.labels,
queries: QueryCache::default(),
}
}
/// Saves a pair and all its descendants into `elems` and populates the /// Saves a pair and all its descendants into `elems` and populates the
/// acceleration structures. /// acceleration structures.
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) { fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {

View File

@ -203,6 +203,7 @@ impl<'a> Locator<'a> {
} }
#[comemo::track] #[comemo::track]
#[allow(clippy::needless_lifetimes)]
impl<'a> Locator<'a> { impl<'a> Locator<'a> {
/// Resolves the locator based on its local and the outer information. /// Resolves the locator based on its local and the outer information.
fn resolve(&self) -> Resolved { fn resolve(&self) -> Resolved {

View File

@ -261,6 +261,11 @@ impl State {
fn selector(&self) -> Selector { fn selector(&self) -> Selector {
select_where!(StateUpdateElem, Key => self.key.clone()) select_where!(StateUpdateElem, Key => self.key.clone())
} }
/// Selects all state updates.
pub fn select_any() -> Selector {
StateUpdateElem::elem().select()
}
} }
#[scope] #[scope]
@ -280,6 +285,7 @@ impl State {
/// Retrieves the value of the state at the current location. /// Retrieves the value of the state at the current location.
/// ///
/// This is equivalent to `{state.at(here())}`. /// This is equivalent to `{state.at(here())}`.
#[typst_macros::time(name = "state.get", span = span)]
#[func(contextual)] #[func(contextual)]
pub fn get( pub fn get(
&self, &self,
@ -303,6 +309,7 @@ impl State {
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this /// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
/// function also works without a known context if the `selector` is a /// function also works without a known context if the `selector` is a
/// location. This behaviour will be removed in a future release. /// location. This behaviour will be removed in a future release.
#[typst_macros::time(name = "state.at", span = span)]
#[func(contextual)] #[func(contextual)]
pub fn at( pub fn at(
&self, &self,

View File

@ -520,8 +520,7 @@ pub enum FrameItem {
Image(Image, Size, Span), Image(Image, Size, Span),
/// An internal or external link to a destination. /// An internal or external link to a destination.
Link(Destination, Size), Link(Destination, Size),
/// An introspectable element that produced something within this frame /// An introspectable element that produced something within this frame.
/// alongside its key.
Tag(Tag), Tag(Tag),
} }

View File

@ -12,11 +12,12 @@ use crate::foundations::{
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
NativeElement, Set, Smart, StyleChain, Value, NativeElement, Set, Smart, StyleChain, Value,
}; };
use crate::introspection::Introspector;
use crate::layout::{ use crate::layout::{
Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel,
Sides, SpecificAlignment, Sides, SpecificAlignment,
}; };
use crate::model::Numbering; use crate::model::{DocumentInfo, Numbering};
use crate::text::LocalName; use crate::text::LocalName;
use crate::visualize::{Color, Paint}; use crate::visualize::{Color, Paint};
@ -451,6 +452,17 @@ impl PagebreakElem {
} }
} }
/// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone)]
pub struct PagedDocument {
/// The document's finished pages.
pub pages: Vec<Page>,
/// Details about the document.
pub info: DocumentInfo,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
}
/// A finished page. /// A finished page.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Page { pub struct Page {
@ -942,3 +954,14 @@ papers! {
(PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9") (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9")
(PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3") (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3")
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_paged_document_is_send_and_sync() {
fn ensure_send_and_sync<T: Send + Sync>() {}
ensure_send_and_sync::<PagedDocument>();
}
}

View File

@ -15,6 +15,7 @@ extern crate self as typst_library;
pub mod diag; pub mod diag;
pub mod engine; pub mod engine;
pub mod foundations; pub mod foundations;
pub mod html;
pub mod introspection; pub mod introspection;
pub mod layout; pub mod layout;
pub mod loading; pub mod loading;
@ -193,7 +194,7 @@ impl LibraryBuilder {
pub fn build(self) -> Library { pub fn build(self) -> Library {
let math = math::module(); let math = math::module();
let inputs = self.inputs.unwrap_or_default(); let inputs = self.inputs.unwrap_or_default();
let global = global(math.clone(), inputs); let global = global(math.clone(), inputs, &self.features);
let std = Value::Module(global.clone()); let std = Value::Module(global.clone());
Library { Library {
global, global,
@ -231,12 +232,14 @@ impl FromIterator<Feature> for Features {
/// An in-development feature that should be enabled. /// An in-development feature that should be enabled.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[non_exhaustive] #[non_exhaustive]
pub enum Feature {} pub enum Feature {
Html,
}
/// Construct the module with global definitions. /// Construct the module with global definitions.
fn global(math: Module, inputs: Dict) -> Module { fn global(math: Module, inputs: Dict, features: &Features) -> Module {
let mut global = Scope::deduplicating(); let mut global = Scope::deduplicating();
self::foundations::define(&mut global, inputs); self::foundations::define(&mut global, inputs, features);
self::model::define(&mut global); self::model::define(&mut global);
self::text::define(&mut global); self::text::define(&mut global);
global.reset_category(); global.reset_category();
@ -246,6 +249,10 @@ fn global(math: Module, inputs: Dict) -> Module {
self::introspection::define(&mut global); self::introspection::define(&mut global);
self::loading::define(&mut global); self::loading::define(&mut global);
self::symbols::define(&mut global); self::symbols::define(&mut global);
global.reset_category();
if features.is_enabled(Feature::Html) {
global.define_module(self::html::module());
}
prelude(&mut global); prelude(&mut global);
Module::new("global", global) Module::new("global", global)
} }

View File

@ -207,9 +207,7 @@ pub fn module() -> Module {
math.define("wide", HElem::new(WIDE.into()).pack()); math.define("wide", HElem::new(WIDE.into()).pack());
// Symbols. // Symbols.
for (name, symbol) in crate::symbols::SYM { crate::symbols::define_math(&mut math);
math.define(*name, symbol.clone());
}
Module::new("math", math) Module::new("math", math)
} }

View File

@ -179,16 +179,13 @@ impl BibliographyElem {
} }
/// Find all bibliography keys. /// Find all bibliography keys.
pub fn keys( pub fn keys(introspector: Tracked<Introspector>) -> Vec<(Label, Option<EcoString>)> {
introspector: Tracked<Introspector>,
) -> Vec<(EcoString, Option<EcoString>)> {
let mut vec = vec![]; let mut vec = vec![];
for elem in introspector.query(&Self::elem().select()).iter() { for elem in introspector.query(&Self::elem().select()).iter() {
let this = elem.to_packed::<Self>().unwrap(); let this = elem.to_packed::<Self>().unwrap();
for entry in this.bibliography().entries() { for (key, entry) in this.bibliography().iter() {
let key = entry.key().into();
let detail = entry.title().map(|title| title.value.to_str().into()); let detail = entry.title().map(|title| title.value.to_str().into());
vec.push((key, detail)) vec.push((Label::new(key), detail))
} }
} }
vec vec
@ -341,7 +338,7 @@ impl Bibliography {
}; };
for entry in library { for entry in library {
match map.entry(entry.key().into()) { match map.entry(PicoStr::intern(entry.key())) {
indexmap::map::Entry::Vacant(vacant) => { indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry); vacant.insert(entry);
} }
@ -366,8 +363,8 @@ impl Bibliography {
self.map.contains_key(&key.into()) self.map.contains_key(&key.into())
} }
fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> { fn iter(&self) -> impl Iterator<Item = (PicoStr, &hayagriva::Entry)> {
self.map.values() self.map.iter().map(|(&k, v)| (k, v))
} }
} }
@ -661,7 +658,7 @@ impl<'a> Generator<'a> {
errors.push(error!( errors.push(error!(
child.span(), child.span(),
"key `{}` does not exist in the bibliography", "key `{}` does not exist in the bibliography",
key.as_str() key.resolve()
)); ));
continue; continue;
}; };
@ -775,7 +772,9 @@ impl<'a> Generator<'a> {
let mut output = std::mem::take(&mut self.failures); let mut output = std::mem::take(&mut self.failures);
for (info, citation) in self.infos.iter().zip(&rendered.citations) { for (info, citation) in self.infos.iter().zip(&rendered.citations) {
let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone(); let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone();
let link = |i: usize| links.get(info.subinfos.get(i)?.key.as_str()).copied(); let link = |i: usize| {
links.get(info.subinfos.get(i)?.key.resolve().as_str()).copied()
};
let renderer = ElemRenderer { let renderer = ElemRenderer {
routines: self.routines, routines: self.routines,
@ -820,7 +819,7 @@ impl<'a> Generator<'a> {
let mut first_occurrences = HashMap::new(); let mut first_occurrences = HashMap::new();
for info in &self.infos { for info in &self.infos {
for subinfo in &info.subinfos { for subinfo in &info.subinfos {
let key = subinfo.key.as_str(); let key = subinfo.key.resolve();
first_occurrences.entry(key).or_insert(info.location); first_occurrences.entry(key).or_insert(info.location);
} }
} }

View File

@ -6,8 +6,6 @@ use crate::foundations::{
cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain, cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain,
Styles, Value, Styles, Value,
}; };
use crate::introspection::Introspector;
use crate::layout::Page;
/// The root element of a document and its metadata. /// The root element of a document and its metadata.
/// ///
@ -39,6 +37,10 @@ pub struct DocumentElem {
#[ghost] #[ghost]
pub author: Author, pub author: Author,
/// The document's description.
#[ghost]
pub description: Option<Content>,
/// The document's keywords. /// The document's keywords.
#[ghost] #[ghost]
pub keywords: Keywords, pub keywords: Keywords,
@ -86,17 +88,6 @@ cast! {
v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?), v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
} }
/// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone)]
pub struct Document {
/// The document's finished pages.
pub pages: Vec<Page>,
/// Details about the document.
pub info: DocumentInfo,
/// Provides the ability to execute queries on the document.
pub introspector: Introspector,
}
/// Details about the document. /// Details about the document.
#[derive(Debug, Default, Clone, PartialEq, Hash)] #[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct DocumentInfo { pub struct DocumentInfo {
@ -104,6 +95,8 @@ pub struct DocumentInfo {
pub title: Option<EcoString>, pub title: Option<EcoString>,
/// The document's author. /// The document's author.
pub author: Vec<EcoString>, pub author: Vec<EcoString>,
/// The document's description.
pub description: Option<EcoString>,
/// The document's keywords. /// The document's keywords.
pub keywords: Vec<EcoString>, pub keywords: Vec<EcoString>,
/// The document's creation date. /// The document's creation date.
@ -124,6 +117,10 @@ impl DocumentInfo {
if has(<DocumentElem as Fields>::Enum::Author) { if has(<DocumentElem as Fields>::Enum::Author) {
self.author = DocumentElem::author_in(chain).0; self.author = DocumentElem::author_in(chain).0;
} }
if has(<DocumentElem as Fields>::Enum::Description) {
self.description =
DocumentElem::description_in(chain).map(|content| content.plain_text());
}
if has(<DocumentElem as Fields>::Enum::Keywords) { if has(<DocumentElem as Fields>::Enum::Keywords) {
self.keywords = DocumentElem::keywords_in(chain).0; self.keywords = DocumentElem::keywords_in(chain).0;
} }
@ -132,14 +129,3 @@ impl DocumentInfo {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_document_is_send_and_sync() {
fn ensure_send_and_sync<T: Send + Sync>() {}
ensure_send_and_sync::<Document>();
}
}

View File

@ -1,6 +1,9 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain}; use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::text::{ItalicToggle, TextElem}; use crate::text::{ItalicToggle, TextElem};
/// Emphasizes content by toggling italics. /// Emphasizes content by toggling italics.
@ -35,7 +38,15 @@ pub struct EmphElem {
impl Show for Packed<EmphElem> { impl Show for Packed<EmphElem> {
#[typst_macros::time(name = "emph", span = self.span())] #[typst_macros::time(name = "emph", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body().clone().styled(TextElem::set_emph(ItalicToggle(true)))) let body = self.body.clone();
Ok(if TargetElem::target_in(styles).is_html() {
HtmlElem::new(tag::em)
.with_body(Some(body))
.pack()
.spanned(self.span())
} else {
body.styled(TextElem::set_emph(ItalicToggle(true)))
})
} }
} }

View File

@ -1,13 +1,15 @@
use std::str::FromStr; use std::str::FromStr;
use ecow::eco_format;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
Styles, Styles, TargetElem,
}; };
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
@ -214,6 +216,19 @@ impl EnumElem {
impl Show for Packed<EnumElem> { impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ol)
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
li.with_body(Some(item.body.clone())).pack().spanned(item.span())
}))))
.pack()
.spanned(self.span()));
}
let mut realized = let mut realized =
BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum) BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum)
.pack() .pack()

View File

@ -9,8 +9,9 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector,
Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
}; };
use crate::html::{tag, HtmlElem};
use crate::introspection::{ use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location, Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
}; };
@ -257,7 +258,7 @@ impl Synthesize for Packed<FigureElem> {
// Determine the figure's kind. // Determine the figure's kind.
let kind = elem.kind(styles).unwrap_or_else(|| { let kind = elem.kind(styles).unwrap_or_else(|| {
elem.body() elem.body()
.query_first(Selector::can::<dyn Figurable>()) .query_first(&Selector::can::<dyn Figurable>())
.map(|elem| FigureKind::Elem(elem.func())) .map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
}); });
@ -289,7 +290,7 @@ impl Synthesize for Packed<FigureElem> {
let descendant = match kind { let descendant = match kind {
FigureKind::Elem(func) => elem FigureKind::Elem(func) => elem
.body() .body()
.query_first(Selector::Elem(func, None)) .query_first(&Selector::Elem(func, None))
.map(Cow::Owned), .map(Cow::Owned),
FigureKind::Name(_) => None, FigureKind::Name(_) => None,
}; };
@ -307,6 +308,7 @@ impl Synthesize for Packed<FigureElem> {
// Fill the figure's caption. // Fill the figure's caption.
let mut caption = elem.caption(styles); let mut caption = elem.caption(styles);
if let Some(caption) = &mut caption { if let Some(caption) = &mut caption {
caption.synthesize(engine, styles)?;
caption.push_kind(kind.clone()); caption.push_kind(kind.clone());
caption.push_supplement(supplement.clone()); caption.push_supplement(supplement.clone());
caption.push_numbering(numbering.clone()); caption.push_numbering(numbering.clone());
@ -326,15 +328,30 @@ impl Synthesize for Packed<FigureElem> {
impl Show for Packed<FigureElem> { impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())] #[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body().clone(); let target = TargetElem::target_in(styles);
let mut realized = self.body.clone();
// Build the caption, if any. // Build the caption, if any.
if let Some(caption) = self.caption(styles) { if let Some(caption) = self.caption(styles) {
let v = VElem::new(self.gap(styles).into()).with_weak(true).pack(); let (first, second) = match caption.position(styles) {
realized = match caption.position(styles) { OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Top => caption.pack() + v + realized, OuterVAlignment::Bottom => (realized, caption.pack()),
OuterVAlignment::Bottom => realized + v + caption.pack(),
}; };
let mut seq = Vec::with_capacity(3);
seq.push(first);
if !target.is_html() {
let v = VElem::new(self.gap(styles).into()).with_weak(true);
seq.push(v.pack().spanned(self.span()))
}
seq.push(second);
realized = Content::sequence(seq)
}
if target.is_html() {
return Ok(HtmlElem::new(tag::figure)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
} }
// Wrap the contents in a block. // Wrap the contents in a block.
@ -607,6 +624,13 @@ impl Show for Packed<FigureCaption> {
realized = supplement + numbers + self.get_separator(styles) + realized; realized = supplement + numbers + self.get_separator(styles) + realized;
} }
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::figcaption)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
}
Ok(realized) Ok(realized)
} }
} }

View File

@ -194,7 +194,7 @@ cast! {
/// before any page content, typically at the very start of the document. /// before any page content, typically at the very start of the document.
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)] #[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
pub struct FootnoteEntry { pub struct FootnoteEntry {
/// The footnote for this entry. It's location can be used to determine /// The footnote for this entry. Its location can be used to determine
/// the footnote counter state. /// the footnote counter state.
/// ///
/// ```example /// ```example

View File

@ -6,8 +6,9 @@ use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize, Styles, Synthesize, TargetElem,
}; };
use crate::html::{tag, HtmlElem};
use crate::introspection::{ use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
}; };
@ -216,6 +217,8 @@ impl Synthesize for Packed<HeadingElem> {
impl Show for Packed<HeadingElem> { impl Show for Packed<HeadingElem> {
#[typst_macros::time(name = "heading", span = self.span())] #[typst_macros::time(name = "heading", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let html = TargetElem::target_in(styles).is_html();
const SPACING_TO_NUMBERING: Em = Em::new(0.3); const SPACING_TO_NUMBERING: Em = Em::new(0.3);
let span = self.span(); let span = self.span();
@ -233,7 +236,7 @@ impl Show for Packed<HeadingElem> {
.display_at_loc(engine, location, styles, numbering)? .display_at_loc(engine, location, styles, numbering)?
.spanned(span); .spanned(span);
if hanging_indent.is_auto() { if hanging_indent.is_auto() && !html {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
// We don't have a locator for the numbering here, so we just // We don't have a locator for the numbering here, so we just
@ -251,19 +254,31 @@ impl Show for Packed<HeadingElem> {
indent = size.x + SPACING_TO_NUMBERING.resolve(styles); indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
} }
realized = numbering let spacing = if html {
+ HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack() SpaceElem::shared().clone()
+ realized; } else {
HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
};
realized = numbering + spacing + realized;
} }
if indent != Abs::zero() { if indent != Abs::zero() && !html {
realized = realized.styled(ParElem::set_hanging_indent(indent.into())); realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
} }
Ok(BlockElem::new() Ok(if html {
.with_body(Some(BlockBody::Content(realized))) // HTML's h1 is closer to a title element. There should only be one.
.pack() // Meanwhile, a level 1 Typst heading is a section heading. For this
.spanned(span)) // reason, levels are offset by one: A Typst level 1 heading becomes
// a `<h2>`.
let level = self.resolve_level(styles);
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level.get().min(5) - 1];
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
} else {
let realized = BlockBody::Content(realized);
BlockElem::new().with_body(Some(realized)).pack().spanned(span)
})
} }
} }

View File

@ -3,11 +3,13 @@ use std::ops::Deref;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, warning, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Content, Label, Packed, Repr, Show, Smart, StyleChain, cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain,
TargetElem,
}; };
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::{Hyphenate, TextElem};
@ -99,8 +101,25 @@ impl LinkElem {
impl Show for Packed<LinkElem> { impl Show for Packed<LinkElem> {
#[typst_macros::time(name = "link", span = self.span())] #[typst_macros::time(name = "link", span = self.span())]
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body().clone(); let body = self.body().clone();
let dest = self.dest();
Ok(if TargetElem::target_in(styles).is_html() {
if let LinkTarget::Dest(Destination::Url(url)) = dest {
HtmlElem::new(tag::a)
.with_attr(attr::href, url.clone().into_inner())
.with_body(Some(body))
.pack()
.spanned(self.span())
} else {
engine.sink.warn(warning!(
self.span(),
"non-URL links are not yet supported by HTML export"
));
body
}
} else {
let linked = match self.dest() { let linked = match self.dest() {
LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Dest(dest) => body.linked(dest.clone()),
LinkTarget::Label(label) => { LinkTarget::Label(label) => {
@ -110,7 +129,8 @@ impl Show for Packed<LinkElem> {
} }
}; };
Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))) linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))
})
} }
} }

View File

@ -4,8 +4,9 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show,
Smart, StyleChain, Styles, Value, Smart, StyleChain, Styles, TargetElem, Value,
}; };
use crate::html::{tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length, VElem}; use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::ParElem; use crate::model::ParElem;
use crate::text::TextElem; use crate::text::TextElem;
@ -140,6 +141,18 @@ impl ListElem {
impl Show for Packed<ListElem> { impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
HtmlElem::new(tag::li)
.with_body(Some(item.body.clone()))
.pack()
.spanned(item.span())
}))))
.pack()
.spanned(self.span()));
}
let mut realized = let mut realized =
BlockElem::multi_layouter(self.clone(), engine.routines.layout_list) BlockElem::multi_layouter(self.clone(), engine.routines.layout_list)
.pack() .pack()

View File

@ -60,8 +60,9 @@ pub fn numbering(
/// Defines how the numbering works. /// Defines how the numbering works.
/// ///
/// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `α`, `Α`, `一`, `壹`, /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `α`, `Α`, `一`, `壹`,
/// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `①`, and `⓵`. They are /// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `١`, `۱`, `१`, `১`, `ক`,
/// replaced by the number in the sequence, preserving the original case. /// `①`, and `⓵`. They are replaced by the number in the sequence,
/// preserving the original case.
/// ///
/// The `*` character means that symbols should be used to count, in the /// The `*` character means that symbols should be used to count, in the
/// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six /// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six

View File

@ -248,7 +248,7 @@ impl Show for Packed<OutlineElem> {
)?; )?;
// Add the overridable outline entry, followed by a line break. // Add the overridable outline entry, followed by a line break.
seq.push(entry.pack()); seq.push(entry.pack().spanned(self.span()));
seq.push(LinebreakElem::shared().clone()); seq.push(LinebreakElem::shared().clone());
ancestors.push(elem); ancestors.push(elem);
@ -332,15 +332,18 @@ impl OutlineIndent {
} }
if !ancestors.is_empty() { if !ancestors.is_empty() {
seq.push(HideElem::new(hidden).pack()); seq.push(HideElem::new(hidden).pack().spanned(span));
seq.push(SpaceElem::shared().clone()); seq.push(SpaceElem::shared().clone().spanned(span));
} }
} }
// Length => indent with some fixed spacing per level // Length => indent with some fixed spacing per level
Some(Smart::Custom(OutlineIndent::Rel(length))) => { Some(Smart::Custom(OutlineIndent::Rel(length))) => {
seq.push( seq.push(
HElem::new(Spacing::Rel(*length)).pack().repeat(ancestors.len()), HElem::new(Spacing::Rel(*length))
.pack()
.spanned(span)
.repeat(ancestors.len()),
); );
} }
@ -535,7 +538,7 @@ impl Show for Packed<OutlineEntry> {
); );
seq.push(SpaceElem::shared().clone()); seq.push(SpaceElem::shared().clone());
} else { } else {
seq.push(HElem::new(Fr::one().into()).pack()); seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span()));
} }
// Add the page number. // Add the page number.

View File

@ -1,6 +1,9 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain}; use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::text::{TextElem, WeightDelta}; use crate::text::{TextElem, WeightDelta};
/// Strongly emphasizes content by increasing the font weight. /// Strongly emphasizes content by increasing the font weight.
@ -40,9 +43,14 @@ pub struct StrongElem {
impl Show for Packed<StrongElem> { impl Show for Packed<StrongElem> {
#[typst_macros::time(name = "strong", span = self.span())] #[typst_macros::time(name = "strong", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self let body = self.body.clone();
.body() Ok(if TargetElem::target_in(styles).is_html() {
.clone() HtmlElem::new(tag::strong)
.styled(TextElem::set_delta(WeightDelta(self.delta(styles))))) .with_body(Some(body))
.pack()
.spanned(self.span())
} else {
body.styled(TextElem::set_delta(WeightDelta(self.delta(styles))))
})
} }
} }

View File

@ -4,8 +4,9 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
Styles, Styles, TargetElem,
}; };
use crate::html::{tag, HtmlElem};
use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem}; use crate::model::{ListItemLike, ListLike, ParElem};
use crate::text::TextElem; use crate::text::TextElem;
@ -114,6 +115,26 @@ impl TermsElem {
impl Show for Packed<TermsElem> { impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|item| {
[
HtmlElem::new(tag::dt)
.with_body(Some(item.term.clone()))
.pack()
.spanned(item.term.span()),
HtmlElem::new(tag::dd)
.with_body(Some(item.description.clone()))
.pack()
.spanned(item.description.span()),
]
},
))))
.pack());
}
let separator = self.separator(styles); let separator = self.separator(styles);
let indent = self.indent(styles); let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles); let hanging_indent = self.hanging_indent(styles);
@ -127,7 +148,7 @@ impl Show for Packed<TermsElem> {
let pad = hanging_indent + indent; let pad = hanging_indent + indent;
let unpad = (!hanging_indent.is_zero()) let unpad = (!hanging_indent.is_zero())
.then(|| HElem::new((-hanging_indent).into()).pack()); .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span));
let mut children = vec![]; let mut children = vec![];
for child in self.children().iter() { for child in self.children().iter() {
@ -149,12 +170,16 @@ impl Show for Packed<TermsElem> {
let mut realized = StackElem::new(children) let mut realized = StackElem::new(children)
.with_spacing(Some(gutter.into())) .with_spacing(Some(gutter.into()))
.pack() .pack()
.spanned(span)
.padded(padding); .padded(padding);
if self.tight(styles) { if self.tight(styles) {
let leading = ParElem::leading_in(styles); let leading = ParElem::leading_in(styles);
let spacing = let spacing = VElem::new(leading.into())
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); .with_weak(true)
.with_attach(true)
.pack()
.spanned(span);
realized = spacing + realized; realized = spacing + realized;
} }

View File

@ -16,10 +16,11 @@ use crate::foundations::{
use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::introspection::{Introspector, Locator, SplitLocator};
use crate::layout::{ use crate::layout::{
Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem, Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem,
Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size, SkewElem, StackElem, PagedDocument, Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size,
SkewElem, StackElem,
}; };
use crate::math::EquationElem; use crate::math::EquationElem;
use crate::model::{Document, DocumentInfo, EnumElem, ListElem, TableElem}; use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem};
use crate::visualize::{ use crate::visualize::{
CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
SquareElem, SquareElem,
@ -85,13 +86,6 @@ routines! {
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Vec<Pair<'a>>> ) -> SourceResult<Vec<Pair<'a>>>
/// Layout content into a document.
fn layout_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<Document>
/// Lays out content into multiple regions. /// Lays out content into multiple regions.
fn layout_fragment( fn layout_fragment(
engine: &mut Engine, engine: &mut Engine,
@ -342,11 +336,16 @@ pub enum EvalMode {
/// Defines what kind of realization we are performing. /// Defines what kind of realization we are performing.
pub enum RealizationKind<'a> { pub enum RealizationKind<'a> {
/// This the root realization for the document. Requires a mutable reference /// This the root realization for layout. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules. /// to document metadata that will be filled from `set document` rules.
Root(&'a mut DocumentInfo), LayoutDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`). /// A nested realization in a container (e.g. a `block`).
Container, LayoutFragment,
/// This the root realization for HTML. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
HtmlDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`).
HtmlFragment,
/// A realization within math. /// A realization within math.
Math, Math,
} }

View File

@ -0,0 +1,49 @@
//! Modifiable symbols.
use crate::foundations::{category, Category, Module, Scope, Symbol, Value};
/// These two modules give names to symbols and emoji to make them easy to
/// insert with a normal keyboard. Alternatively, you can also always directly
/// enter Unicode symbols into your text and formulas. In addition to the
/// symbols listed below, math mode defines `dif` and `Dif`. These are not
/// normal symbol values because they also affect spacing and font style.
#[category]
pub static SYMBOLS: Category;
impl From<codex::Module> for Scope {
fn from(module: codex::Module) -> Scope {
let mut scope = Self::new();
extend_scope_from_codex_module(&mut scope, module);
scope
}
}
impl From<codex::Symbol> for Symbol {
fn from(symbol: codex::Symbol) -> Self {
match symbol {
codex::Symbol::Single(c) => Symbol::single(c),
codex::Symbol::Multi(list) => Symbol::list(list),
}
}
}
fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) {
for (name, definition) in module.iter() {
let value = match definition {
codex::Def::Symbol(s) => Value::Symbol(s.into()),
codex::Def::Module(m) => Value::Module(Module::new(name, m.into())),
};
scope.define(name, value);
}
}
/// Hook up all `symbol` definitions.
pub(super) fn define(global: &mut Scope) {
global.category(SYMBOLS);
extend_scope_from_codex_module(global, codex::ROOT);
}
/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module.
pub(super) fn define_math(math: &mut Scope) {
extend_scope_from_codex_module(math, codex::SYM);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
//! Modifiable symbols.
mod emoji;
mod sym;
pub use self::emoji::*;
pub use self::sym::*;
use crate::foundations::{category, Category, Scope};
/// These two modules give names to symbols and emoji to make them easy to
/// insert with a normal keyboard. Alternatively, you can also always directly
/// enter Unicode symbols into your text and formulas. In addition to the
/// symbols listed below, math mode defines `dif` and `Dif`. These are not
/// normal symbol values because they also affect spacing and font style.
#[category]
pub static SYMBOLS: Category;
/// Hook up all `symbol` definitions.
pub(super) fn define(global: &mut Scope) {
global.category(SYMBOLS);
global.define_module(sym());
global.define_module(emoji());
}

File diff suppressed because it is too large Load Diff

View File

@ -182,8 +182,7 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! {
"NewCM10-Regular" => Exception::new() "NewCM10-Regular" => Exception::new()
.family("New Computer Modern"), .family("New Computer Modern"),
"NewCMMath-Bold" => Exception::new() "NewCMMath-Bold" => Exception::new()
.family("New Computer Modern Math") .family("New Computer Modern Math"),
.weight(700),
"NewCMMath-Book" => Exception::new() "NewCMMath-Book" => Exception::new()
.family("New Computer Modern Math") .family("New Computer Modern Math")
.weight(450), .weight(450),

View File

@ -14,8 +14,9 @@ macro_rules! translation {
}; };
} }
const TRANSLATIONS: [(&str, &str); 36] = [ const TRANSLATIONS: [(&str, &str); 38] = [
translation!("ar"), translation!("ar"),
translation!("bg"),
translation!("ca"), translation!("ca"),
translation!("cs"), translation!("cs"),
translation!("da"), translation!("da"),
@ -23,6 +24,7 @@ const TRANSLATIONS: [(&str, &str); 36] = [
translation!("en"), translation!("en"),
translation!("es"), translation!("es"),
translation!("et"), translation!("et"),
translation!("eu"),
translation!("fi"), translation!("fi"),
translation!("fr"), translation!("fr"),
translation!("gl"), translation!("gl"),
@ -60,7 +62,9 @@ pub struct Lang([u8; 3], u8);
impl Lang { impl Lang {
pub const ALBANIAN: Self = Self(*b"sq ", 2); pub const ALBANIAN: Self = Self(*b"sq ", 2);
pub const ARABIC: Self = Self(*b"ar ", 2); pub const ARABIC: Self = Self(*b"ar ", 2);
pub const BASQUE: Self = Self(*b"eu ", 2);
pub const BOKMÅL: Self = Self(*b"nb ", 2); pub const BOKMÅL: Self = Self(*b"nb ", 2);
pub const BULGARIAN: Self = Self(*b"bg ", 2);
pub const CATALAN: Self = Self(*b"ca ", 2); pub const CATALAN: Self = Self(*b"ca ", 2);
pub const CHINESE: Self = Self(*b"zh ", 2); pub const CHINESE: Self = Self(*b"zh ", 2);
pub const CROATIAN: Self = Self(*b"hr ", 2); pub const CROATIAN: Self = Self(*b"hr ", 2);

View File

@ -495,7 +495,9 @@ pub struct TextElem {
/// ///
/// Hyphenation is generally avoided by placing the whole word on the next /// Hyphenation is generally avoided by placing the whole word on the next
/// line, so a higher hyphenation cost can result in awkward justification /// line, so a higher hyphenation cost can result in awkward justification
/// spacing. /// spacing. Note: Hyphenation costs will only be applied when the
/// [`linebreaks`]($par.linebreaks) are set to "optimized". (For example
/// by default implied by [`justify`]($par.justify).)
/// ///
/// Runts are avoided by placing more or fewer words on previous lines, so a /// Runts are avoided by placing more or fewer words on previous lines, so a
/// higher runt cost can result in more awkward in justification spacing. /// higher runt cost can result in more awkward in justification spacing.

View File

@ -14,8 +14,9 @@ use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed,
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, Value,
}; };
use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::model::{Figurable, ParElem}; use crate::model::{Figurable, ParElem};
use crate::text::{ use crate::text::{
@ -451,6 +452,14 @@ impl Show for Packed<RawElem> {
} }
let mut realized = Content::sequence(seq); let mut realized = Content::sequence(seq);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::pre)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
}
if self.block(styles) { if self.block(styles) {
// Align the text before inserting it into the block. // Align the text before inserting it into the block.
realized = realized.aligned(self.align(styles).into()); realized = realized.aligned(self.align(styles).into());

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