mirror of
https://github.com/typst/typst
synced 2025-06-08 13:16:24 +08:00
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:
commit
4ad90c1f38
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.82.0
|
||||
- uses: dtolnay/rust-toolchain@1.83.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo test --workspace --no-run
|
||||
- run: cargo test --workspace --no-fail-fast
|
||||
@ -59,11 +59,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.82.0
|
||||
- uses: dtolnay/rust-toolchain@1.83.0
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo clippy --workspace --all-targets --all-features
|
||||
- run: cargo clippy --workspace --all-targets --no-default-features
|
||||
- run: cargo fmt --check --all
|
||||
- run: cargo doc --workspace --no-deps
|
||||
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.82.0
|
||||
- uses: dtolnay/rust-toolchain@1.83.0
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
|
76
Cargo.lock
generated
76
Cargo.lock
generated
@ -123,6 +123,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
@ -296,6 +302,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
@ -409,6 +421,11 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/typst/codex?rev=343a9b1#343a9b199430681ba3ca0e2242097c6419492d55"
|
||||
|
||||
[[package]]
|
||||
name = "color-print"
|
||||
version = "0.3.6"
|
||||
@ -919,9 +936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "hayagriva"
|
||||
@ -950,6 +967,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hypher"
|
||||
version = "0.1.5"
|
||||
@ -1192,7 +1215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown 0.15.2",
|
||||
"rayon",
|
||||
"serde",
|
||||
]
|
||||
@ -1368,13 +1391,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
|
||||
checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2355,6 +2377,15 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "sigpipe"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5584bfb3e0d348139d8210285e39f6d2f8a1902ac06de343e06357d1d763d8e6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
@ -2697,6 +2728,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
@ -2790,6 +2833,7 @@ dependencies = [
|
||||
"comemo",
|
||||
"ecow",
|
||||
"typst-eval",
|
||||
"typst-html",
|
||||
"typst-layout",
|
||||
"typst-library",
|
||||
"typst-macros",
|
||||
@ -2802,7 +2846,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typst-assets"
|
||||
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]]
|
||||
name = "typst-cli"
|
||||
@ -2830,11 +2874,14 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_yaml 0.9.34+deprecated",
|
||||
"shell-escape",
|
||||
"sigpipe",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tiny_http",
|
||||
"toml",
|
||||
"typst",
|
||||
"typst-eval",
|
||||
"typst-html",
|
||||
"typst-kit",
|
||||
"typst-macros",
|
||||
"typst-pdf",
|
||||
@ -2902,6 +2949,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "typst-ide"
|
||||
version = "0.12.0"
|
||||
@ -2979,6 +3040,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"chinese-number",
|
||||
"ciborium",
|
||||
"codex",
|
||||
"comemo",
|
||||
"csv",
|
||||
"ecow",
|
||||
|
@ -19,6 +19,7 @@ readme = "README.md"
|
||||
typst = { path = "crates/typst", 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-html = { path = "crates/typst-html", 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-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-timing = { path = "crates/typst-timing", 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" }
|
||||
arrayvec = "0.7.4"
|
||||
az = "1.2"
|
||||
@ -46,6 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.2.1"
|
||||
clap_mangen = "0.2.10"
|
||||
codespan-reporting = "0.11"
|
||||
codex = { git = "https://github.com/typst/codex", rev = "343a9b1" }
|
||||
color-print = "0.3.6"
|
||||
comemo = "0.4"
|
||||
csv = "1"
|
||||
@ -104,6 +106,7 @@ serde = { version = "1.0.184", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
shell-escape = "0.1.5"
|
||||
sigpipe = "0.1"
|
||||
siphasher = "1"
|
||||
smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] }
|
||||
stacker = "0.1.15"
|
||||
@ -115,6 +118,7 @@ tar = "0.4"
|
||||
tempfile = "3.7.0"
|
||||
thin-vec = "0.2.13"
|
||||
time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
|
||||
tiny_http = "0.12"
|
||||
tiny-skia = "0.11"
|
||||
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
|
||||
ttf-parser = "0.24.1"
|
||||
|
@ -20,6 +20,7 @@ doc = false
|
||||
[dependencies]
|
||||
typst = { workspace = true }
|
||||
typst-eval = { workspace = true }
|
||||
typst-html = { workspace = true }
|
||||
typst-kit = { workspace = true }
|
||||
typst-macros = { workspace = true }
|
||||
typst-pdf = { workspace = true }
|
||||
@ -46,8 +47,10 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shell-escape = { workspace = true }
|
||||
sigpipe = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tiny_http = { workspace = true, optional = true }
|
||||
toml = { workspace = true }
|
||||
ureq = { workspace = true }
|
||||
xz2 = { workspace = true, optional = true }
|
||||
@ -62,11 +65,14 @@ color-print = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["embed-fonts"]
|
||||
default = ["embed-fonts", "http-server"]
|
||||
|
||||
# Embeds some fonts into the binary, see typst-kit
|
||||
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.
|
||||
self-update = ["dep:self-replace", "dep:xz2", "dep:zip"]
|
||||
|
||||
|
@ -33,7 +33,7 @@ const AFTER_HELP: &str = color_print::cstr!("\
|
||||
<s>Forum for questions:</> https://forum.typst.app/
|
||||
");
|
||||
|
||||
/// The Typst compiler
|
||||
/// The Typst compiler.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[clap(
|
||||
name = "typst",
|
||||
@ -44,24 +44,16 @@ const AFTER_HELP: &str = color_print::cstr!("\
|
||||
max_term_width = 80,
|
||||
)]
|
||||
pub struct CliArguments {
|
||||
/// The command to run
|
||||
/// The command to run.
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
|
||||
/// Set when to use color.
|
||||
/// auto = use color if a capable terminal is detected
|
||||
#[clap(
|
||||
long,
|
||||
value_name = "WHEN",
|
||||
require_equals = true,
|
||||
num_args = 0..=1,
|
||||
default_value = "auto",
|
||||
default_missing_value = "always",
|
||||
)]
|
||||
/// Whether to use color. When set to `auto` if the terminal to supports it.
|
||||
#[clap(long, default_value_t = ColorChoice::Auto, default_missing_value = "always")]
|
||||
pub color: ColorChoice,
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
@ -69,109 +61,53 @@ pub struct CliArguments {
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
#[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")]
|
||||
Compile(CompileCommand),
|
||||
|
||||
/// Watches an input file and recompiles on changes
|
||||
/// Watches an input file and recompiles on changes.
|
||||
#[command(visible_alias = "w")]
|
||||
Watch(CompileCommand),
|
||||
Watch(WatchCommand),
|
||||
|
||||
/// Initializes a new project from a template
|
||||
/// Initializes a new project from a template.
|
||||
Init(InitCommand),
|
||||
|
||||
/// Processes an input file to extract provided metadata
|
||||
/// Processes an input file to extract provided metadata.
|
||||
Query(QueryCommand),
|
||||
|
||||
/// Lists all discovered fonts in system and custom font paths
|
||||
/// Lists all discovered fonts in system and custom font paths.
|
||||
Fonts(FontsCommand),
|
||||
|
||||
/// Self update the Typst CLI
|
||||
/// Self update the Typst CLI.
|
||||
#[cfg_attr(not(feature = "self-update"), clap(hide = true))]
|
||||
Update(UpdateCommand),
|
||||
}
|
||||
|
||||
/// Compiles an input file into a supported output format
|
||||
/// Compiles an input file into a supported output format.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct CompileCommand {
|
||||
/// Shared arguments
|
||||
/// Arguments for compilation.
|
||||
#[clap(flatten)]
|
||||
pub common: SharedArgs,
|
||||
|
||||
/// 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>,
|
||||
pub args: CompileArgs,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Compiles an input file into a supported output format.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct WatchCommand {
|
||||
/// Arguments for compilation.
|
||||
#[clap(flatten)]
|
||||
pub args: CompileArgs,
|
||||
|
||||
/// Arguments for the HTTP server.
|
||||
#[cfg(feature = "http-server")]
|
||||
#[clap(flatten)]
|
||||
pub server: ServerArgs,
|
||||
}
|
||||
|
||||
/// Initializes a new project from a template
|
||||
/// Initializes a new project from a template.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
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
|
||||
/// specified, Typst will default to the latest version.
|
||||
@ -179,34 +115,34 @@ pub struct InitCommand {
|
||||
/// Supports both local and published templates.
|
||||
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>,
|
||||
|
||||
/// Arguments related to storage of packages in the system
|
||||
/// Arguments related to storage of packages in the system.
|
||||
#[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)]
|
||||
pub struct QueryCommand {
|
||||
/// Shared arguments
|
||||
#[clap(flatten)]
|
||||
pub common: SharedArgs,
|
||||
/// Path to input Typst file. Use `-` to read input from stdin.
|
||||
#[clap(value_parser = input_value_parser(), value_hint = ValueHint::FilePath)]
|
||||
pub input: Input,
|
||||
|
||||
/// Defines which elements to retrieve
|
||||
/// Defines which elements to retrieve.
|
||||
pub selector: String,
|
||||
|
||||
/// Extracts just one field from all retrieved elements
|
||||
/// Extracts just one field from all retrieved elements.
|
||||
#[clap(long = "field")]
|
||||
pub field: Option<String>,
|
||||
|
||||
/// Expects and retrieves exactly one element
|
||||
/// Expects and retrieves exactly one element.
|
||||
#[clap(long = "one", default_value = "false")]
|
||||
pub one: bool,
|
||||
|
||||
/// The format to serialize in
|
||||
#[clap(long = "format", default_value = "json")]
|
||||
/// The format to serialize in.
|
||||
#[clap(long = "format", default_value_t)]
|
||||
pub format: SerializationFormat,
|
||||
|
||||
/// Whether to pretty-print the serialized output.
|
||||
@ -214,38 +150,153 @@ pub struct QueryCommand {
|
||||
/// Only applies to JSON format.
|
||||
#[clap(long)]
|
||||
pub pretty: bool,
|
||||
|
||||
/// World arguments.
|
||||
#[clap(flatten)]
|
||||
pub world: WorldArgs,
|
||||
|
||||
/// Processing arguments.
|
||||
#[clap(flatten)]
|
||||
pub process: ProcessArgs,
|
||||
}
|
||||
|
||||
// Output file format for query command
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
|
||||
pub enum SerializationFormat {
|
||||
Json,
|
||||
Yaml,
|
||||
/// Lists all discovered fonts in system and custom font paths.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct FontsCommand {
|
||||
/// Common font arguments.
|
||||
#[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)]
|
||||
pub struct SharedArgs {
|
||||
/// Path to input Typst file. Use `-` to read input from stdin
|
||||
#[clap(value_parser = make_input_value_parser(), value_hint = ValueHint::FilePath)]
|
||||
pub struct CompileArgs {
|
||||
/// Path to input Typst file. Use `-` to read input from stdin.
|
||||
#[clap(value_parser = input_value_parser(), value_hint = ValueHint::FilePath)]
|
||||
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")]
|
||||
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(
|
||||
long = "input",
|
||||
value_name = "key=value",
|
||||
action = ArgAction::Append,
|
||||
value_parser = ValueParser::new(parse_input_pair),
|
||||
value_parser = ValueParser::new(parse_sys_input_pair),
|
||||
)]
|
||||
pub inputs: Vec<(String, String)>,
|
||||
|
||||
/// Common font arguments
|
||||
/// Common font arguments.
|
||||
#[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.
|
||||
///
|
||||
@ -257,42 +308,34 @@ pub struct SharedArgs {
|
||||
value_parser = parse_source_date_epoch,
|
||||
)]
|
||||
pub creation_timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// The format to emit diagnostics in
|
||||
#[clap(
|
||||
long,
|
||||
default_value_t = DiagnosticFormat::Human,
|
||||
value_parser = clap::value_parser!(DiagnosticFormat)
|
||||
)]
|
||||
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.
|
||||
/// Arguments for configuration the process of compilation itself.
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ProcessArgs {
|
||||
/// Number of parallel jobs spawned during compilation. Defaults to number
|
||||
/// of CPUs. Setting it to 1 disables parallelism.
|
||||
#[clap(long, short)]
|
||||
pub jobs: Option<usize>,
|
||||
|
||||
/// Enables in-development features that may be changed or removed at any
|
||||
/// time.
|
||||
#[arg(long = "feature", value_delimiter = ',')]
|
||||
pub feature: Vec<Feature>,
|
||||
}
|
||||
#[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
|
||||
pub features: Vec<Feature>,
|
||||
|
||||
/// An in-development feature that may be changed or removed at any time.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
|
||||
pub enum Feature {}
|
||||
/// The format to emit diagnostics in.
|
||||
#[clap(long, default_value_t)]
|
||||
pub diagnostic_format: DiagnosticFormat,
|
||||
}
|
||||
|
||||
/// Arguments related to where packages are stored in the system.
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct PackageStorageArgs {
|
||||
/// Custom path to local packages, defaults to system-dependent location
|
||||
pub struct PackageArgs {
|
||||
/// Custom path to local packages, defaults to system-dependent location.
|
||||
#[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
|
||||
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(
|
||||
long = "package-cache-path",
|
||||
env = "TYPST_PACKAGE_CACHE_PATH",
|
||||
@ -301,13 +344,58 @@ pub struct PackageStorageArgs {
|
||||
pub package_cache_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
|
||||
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
|
||||
let timestamp: i64 = raw
|
||||
.parse()
|
||||
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?;
|
||||
DateTime::from_timestamp(timestamp, 0)
|
||||
.ok_or_else(|| "timestamp out of range".to_string())
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -319,6 +407,15 @@ pub enum Input {
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
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`
|
||||
fn make_input_value_parser() -> impl TypedValueParser<Value = Input> {
|
||||
fn input_value_parser() -> impl TypedValueParser<Value = Input> {
|
||||
clap::builder::OsStringValueParser::new().try_map(|value| {
|
||||
if value.is_empty() {
|
||||
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`
|
||||
fn make_output_value_parser() -> impl TypedValueParser<Value = Output> {
|
||||
fn output_value_parser() -> impl TypedValueParser<Value = Output> {
|
||||
clap::builder::OsStringValueParser::new().try_map(|value| {
|
||||
// Empty value also handled by clap for `Option<Output>`
|
||||
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
|
||||
/// 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
|
||||
.split_once('=')
|
||||
.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))
|
||||
}
|
||||
|
||||
/// 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 PageRangeArgument(RangeInclusive<Option<NonZeroUsize>>);
|
||||
|
||||
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)
|
||||
}
|
||||
/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
|
||||
fn parse_source_date_epoch(raw: &str) -> Result<DateTime<Utc>, String> {
|
||||
let timestamp: i64 = raw
|
||||
.parse()
|
||||
.map_err(|err| format!("timestamp must be decimal integer ({err})"))?;
|
||||
DateTime::from_timestamp(timestamp, 0)
|
||||
.ok_or_else(|| "timestamp out of range".to_string())
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::{Datelike, Timelike};
|
||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
||||
use codespan_reporting::term;
|
||||
use ecow::{eco_format, EcoString};
|
||||
@ -12,17 +13,20 @@ use typst::diag::{
|
||||
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
||||
};
|
||||
use typst::foundations::{Datetime, Smart};
|
||||
use typst::layout::{Frame, Page, PageRanges};
|
||||
use typst::model::Document;
|
||||
use typst::html::HtmlDocument;
|
||||
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
||||
use typst::syntax::{FileId, Source, Span};
|
||||
use typst::WorldExt;
|
||||
use typst_pdf::{PdfOptions, PdfStandards};
|
||||
|
||||
use crate::args::{
|
||||
CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument,
|
||||
PdfStandard,
|
||||
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
|
||||
PdfStandard, WatchCommand,
|
||||
};
|
||||
#[cfg(feature = "http-server")]
|
||||
use crate::server::HtmlServer;
|
||||
use crate::timings::Timer;
|
||||
|
||||
use crate::watch::Status;
|
||||
use crate::world::SystemWorld;
|
||||
use crate::{set_failed, terminal};
|
||||
@ -30,34 +34,73 @@ use crate::{set_failed, terminal};
|
||||
type CodespanResult<T> = Result<T, CodespanError>;
|
||||
type CodespanError = codespan_reporting::files::Error;
|
||||
|
||||
impl CompileCommand {
|
||||
/// The output path.
|
||||
pub fn output(&self) -> Output {
|
||||
self.output.clone().unwrap_or_else(|| {
|
||||
let Input::Path(path) = &self.common.input else {
|
||||
panic!("output must be specified when input is from stdin, as guarded by the CLI");
|
||||
};
|
||||
Output::Path(path.with_extension(
|
||||
match self.output_format().unwrap_or(OutputFormat::Pdf) {
|
||||
OutputFormat::Pdf => "pdf",
|
||||
OutputFormat::Png => "png",
|
||||
OutputFormat::Svg => "svg",
|
||||
},
|
||||
))
|
||||
})
|
||||
/// Execute a compilation command.
|
||||
pub fn compile(timer: &mut Timer, command: &CompileCommand) -> StrResult<()> {
|
||||
let mut config = CompileConfig::new(command)?;
|
||||
let mut world =
|
||||
SystemWorld::new(&command.args.input, &command.args.world, &command.args.process)
|
||||
.map_err(|err| eco_format!("{err}"))?;
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config))?
|
||||
}
|
||||
|
||||
/// A preprocessed `CompileCommand`.
|
||||
pub struct CompileConfig {
|
||||
/// Whether we are watching.
|
||||
pub watching: bool,
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// The format to use for generated output, either specified by the user or inferred from the extension.
|
||||
///
|
||||
/// Will return `Err` if the format was not specified and could not be inferred.
|
||||
pub fn output_format(&self) -> StrResult<OutputFormat> {
|
||||
Ok(if let Some(specified) = self.format {
|
||||
/// 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
|
||||
} else if let Some(Output::Path(output)) = &self.output {
|
||||
} else if let Some(Output::Path(output)) = &args.output {
|
||||
match output.extension() {
|
||||
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("svg") => OutputFormat::Svg,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
|
||||
_ => bail!(
|
||||
"could not infer output format for path {}.\n\
|
||||
consider providing the format manually with `--format/-f`",
|
||||
@ -66,43 +109,65 @@ impl CompileCommand {
|
||||
}
|
||||
} else {
|
||||
OutputFormat::Pdf
|
||||
};
|
||||
|
||||
let output = args.output.clone().unwrap_or_else(|| {
|
||||
let Input::Path(path) = &input else {
|
||||
panic!("output must be specified when input is from stdin, as guarded by the CLI");
|
||||
};
|
||||
Output::Path(path.with_extension(
|
||||
match output_format {
|
||||
OutputFormat::Pdf => "pdf",
|
||||
OutputFormat::Png => "png",
|
||||
OutputFormat::Svg => "svg",
|
||||
OutputFormat::Html => "html",
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
let pages = args.pages.as_ref().map(|export_ranges| {
|
||||
PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
|
||||
});
|
||||
|
||||
let pdf_standards = {
|
||||
let list = args
|
||||
.pdf_standard
|
||||
.iter()
|
||||
.map(|standard| match standard {
|
||||
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
|
||||
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
PdfStandards::new(&list)?
|
||||
};
|
||||
|
||||
#[cfg(feature = "http-server")]
|
||||
let server = match watch {
|
||||
Some(command)
|
||||
if output_format == OutputFormat::Html && !command.server.no_serve =>
|
||||
{
|
||||
Some(HtmlServer::new(&input, &command.server)?)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
watching: watch.is_some(),
|
||||
input,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/// The ranges of the pages to be exported as specified by the user.
|
||||
///
|
||||
/// This returns `None` if all pages should be exported.
|
||||
pub fn exported_page_ranges(&self) -> Option<PageRanges> {
|
||||
self.pages.as_ref().map(|export_ranges| {
|
||||
PageRanges::new(
|
||||
export_ranges.iter().map(PageRangeArgument::to_range).collect(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// The PDF standards to try to conform with.
|
||||
pub fn pdf_standards(&self) -> StrResult<PdfStandards> {
|
||||
let list = self
|
||||
.pdf_standard
|
||||
.iter()
|
||||
.map(|standard| match standard {
|
||||
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
|
||||
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
PdfStandards::new(&list)
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a compilation command.
|
||||
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||
// Only meant for input validation
|
||||
_ = command.output_format()?;
|
||||
|
||||
let mut world =
|
||||
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
|
||||
timer.record(&mut world, |world| compile_once(world, &mut command, false))??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile a single time.
|
||||
@ -111,96 +176,112 @@ pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||
#[typst_macros::time(name = "compile once")]
|
||||
pub fn compile_once(
|
||||
world: &mut SystemWorld,
|
||||
command: &mut CompileCommand,
|
||||
watching: bool,
|
||||
config: &mut CompileConfig,
|
||||
) -> StrResult<()> {
|
||||
let start = std::time::Instant::now();
|
||||
if watching {
|
||||
Status::Compiling.print(command).unwrap();
|
||||
if config.watching {
|
||||
Status::Compiling.print(config).unwrap();
|
||||
}
|
||||
|
||||
let Warned { output, warnings } = typst::compile(world);
|
||||
let result = output.and_then(|document| export(world, &document, command, watching));
|
||||
let Warned { output, warnings } = compile_and_export(world, config);
|
||||
|
||||
match result {
|
||||
match output {
|
||||
// Export the PDF / PNG.
|
||||
Ok(()) => {
|
||||
let duration = start.elapsed();
|
||||
|
||||
if watching {
|
||||
if config.watching {
|
||||
if warnings.is_empty() {
|
||||
Status::Success(duration).print(command).unwrap();
|
||||
Status::Success(duration).print(config).unwrap();
|
||||
} 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})"))?;
|
||||
|
||||
write_make_deps(world, command)?;
|
||||
|
||||
if let Some(open) = command.open.take() {
|
||||
if let Output::Path(file) = command.output() {
|
||||
open_file(open.as_deref(), &file)?;
|
||||
}
|
||||
}
|
||||
write_make_deps(world, config)?;
|
||||
open_output(config)?;
|
||||
}
|
||||
|
||||
// Print diagnostics.
|
||||
Err(errors) => {
|
||||
set_failed();
|
||||
|
||||
if watching {
|
||||
Status::Error.print(command).unwrap();
|
||||
if config.watching {
|
||||
Status::Error.print(config).unwrap();
|
||||
}
|
||||
|
||||
print_diagnostics(
|
||||
world,
|
||||
&errors,
|
||||
&warnings,
|
||||
command.common.diagnostic_format,
|
||||
)
|
||||
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
||||
print_diagnostics(world, &errors, &warnings, config.diagnostic_format)
|
||||
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export into the target format.
|
||||
fn export(
|
||||
/// Compile and then export the document.
|
||||
fn compile_and_export(
|
||||
world: &mut SystemWorld,
|
||||
document: &Document,
|
||||
command: &CompileCommand,
|
||||
watching: bool,
|
||||
) -> SourceResult<()> {
|
||||
match command.output_format().at(Span::detached())? {
|
||||
config: &mut CompileConfig,
|
||||
) -> Warned<SourceResult<()>> {
|
||||
match config.output_format {
|
||||
OutputFormat::Html => {
|
||||
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
|
||||
let result = output.and_then(|document| export_html(&document, config));
|
||||
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())
|
||||
}
|
||||
|
||||
/// 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(world, document, command, watching, ImageExportFormat::Png)
|
||||
.at(Span::detached())
|
||||
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
|
||||
}
|
||||
OutputFormat::Svg => {
|
||||
export_image(world, document, command, watching, ImageExportFormat::Svg)
|
||||
.at(Span::detached())
|
||||
export_image(document, config, ImageExportFormat::Svg).at(Span::detached())
|
||||
}
|
||||
OutputFormat::Pdf => export_pdf(document, command),
|
||||
OutputFormat::Html => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Export to a PDF.
|
||||
fn export_pdf(document: &Document, command: &CompileCommand) -> SourceResult<()> {
|
||||
fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
|
||||
let options = PdfOptions {
|
||||
ident: Smart::Auto,
|
||||
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(),
|
||||
standards: command.pdf_standards().at(Span::detached())?,
|
||||
page_ranges: config.pages.clone(),
|
||||
standards: config.pdf_standards.clone(),
|
||||
};
|
||||
let buffer = typst_pdf::pdf(document, &options)?;
|
||||
command
|
||||
.output()
|
||||
config
|
||||
.output
|
||||
.write(&buffer)
|
||||
.map_err(|err| eco_format!("failed to write PDF file ({err})"))
|
||||
.at(Span::detached())?;
|
||||
@ -228,36 +309,31 @@ enum ImageExportFormat {
|
||||
|
||||
/// Export to one or multiple images.
|
||||
fn export_image(
|
||||
world: &mut SystemWorld,
|
||||
document: &Document,
|
||||
command: &CompileCommand,
|
||||
watching: bool,
|
||||
document: &PagedDocument,
|
||||
config: &CompileConfig,
|
||||
fmt: ImageExportFormat,
|
||||
) -> StrResult<()> {
|
||||
let output = command.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::Path(ref output) => {
|
||||
output_template::has_indexable_template(output.to_str().unwrap_or_default())
|
||||
}
|
||||
};
|
||||
|
||||
let exported_page_ranges = command.exported_page_ranges();
|
||||
|
||||
let exported_pages = document
|
||||
.pages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.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)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !can_handle_multiple && exported_pages.len() > 1 {
|
||||
let err = match output {
|
||||
let err = match config.output {
|
||||
Output::Stdout => "to stdout",
|
||||
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}");
|
||||
}
|
||||
|
||||
let cache = world.export_cache();
|
||||
|
||||
// The results are collected in a `Vec<()>` which does not allocate.
|
||||
exported_pages
|
||||
.par_iter()
|
||||
.map(|(i, page)| {
|
||||
// Use output with converted path.
|
||||
let output = match output {
|
||||
Output::Path(ref path) => {
|
||||
let output = match &config.output {
|
||||
Output::Path(path) => {
|
||||
let storage;
|
||||
let path = if can_handle_multiple {
|
||||
storage = output_template::format(
|
||||
@ -290,7 +364,10 @@ fn export_image(
|
||||
// If we are not watching, don't use the cache.
|
||||
// If the frame is in the cache, skip 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(());
|
||||
}
|
||||
|
||||
@ -299,7 +376,7 @@ fn export_image(
|
||||
Output::Stdout => Output::Stdout,
|
||||
};
|
||||
|
||||
export_image_page(command, page, &output, fmt)?;
|
||||
export_image_page(config, page, &output, fmt)?;
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<Vec<()>, EcoString>>()?;
|
||||
@ -338,14 +415,14 @@ mod output_template {
|
||||
|
||||
/// Export single image.
|
||||
fn export_image_page(
|
||||
command: &CompileCommand,
|
||||
config: &CompileConfig,
|
||||
page: &Page,
|
||||
output: &Output,
|
||||
fmt: ImageExportFormat,
|
||||
) -> StrResult<()> {
|
||||
match fmt {
|
||||
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
|
||||
.encode_png()
|
||||
.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
|
||||
/// its dependencies to the path specified by the --make-deps argument, if it
|
||||
/// was provided.
|
||||
fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResult<()> {
|
||||
let Some(ref make_deps_path) = command.make_deps else { return Ok(()) };
|
||||
let Output::Path(output_path) = command.output() else {
|
||||
fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> {
|
||||
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
|
||||
let Output::Path(output_path) = &config.output else {
|
||||
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")
|
||||
};
|
||||
|
||||
@ -452,13 +529,13 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu
|
||||
|
||||
fn write(
|
||||
make_deps_path: &Path,
|
||||
output_path: String,
|
||||
output_path: &str,
|
||||
root: PathBuf,
|
||||
dependencies: impl Iterator<Item = PathBuf>,
|
||||
) -> io::Result<()> {
|
||||
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":")?;
|
||||
for dependency in dependencies {
|
||||
let Some(dependency) =
|
||||
@ -484,21 +561,37 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens the given file using:
|
||||
/// - The default file viewer if `open` is `None`.
|
||||
/// - The given viewer provided by `open` if it is `Some`.
|
||||
///
|
||||
/// If the file could not be opened, an error is returned.
|
||||
fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> {
|
||||
/// Opens the output if desired.
|
||||
fn open_output(config: &mut CompileConfig) -> StrResult<()> {
|
||||
let Some(viewer) = config.open.take() else { return Ok(()) };
|
||||
|
||||
#[cfg(feature = "http-server")]
|
||||
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.
|
||||
let path = path
|
||||
.canonicalize()
|
||||
.map_err(|err| eco_format!("failed to canonicalize path ({err})"))?;
|
||||
if let Some(app) = open {
|
||||
open::with_detached(&path, app)
|
||||
.map_err(|err| eco_format!("failed to open file with {} ({})", app, err))
|
||||
|
||||
open_path(path.as_os_str(), viewer.as_deref())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
open::that_detached(&path).map_err(|err| {
|
||||
open::that_detached(path).map_err(|err| {
|
||||
let openers = open::commands(path)
|
||||
.iter()
|
||||
.map(|command| command.get_program().to_string_lossy())
|
||||
|
@ -6,8 +6,8 @@ use crate::args::FontsCommand;
|
||||
/// Execute a font listing command.
|
||||
pub fn fonts(command: &FontsCommand) {
|
||||
let fonts = Fonts::searcher()
|
||||
.include_system_fonts(!command.font_args.ignore_system_fonts)
|
||||
.search_with(&command.font_args.font_paths);
|
||||
.include_system_fonts(!command.font.ignore_system_fonts)
|
||||
.search_with(&command.font.font_paths);
|
||||
|
||||
for (name, infos) in fonts.book.families() {
|
||||
println!("{name}");
|
||||
|
@ -15,7 +15,7 @@ use crate::package;
|
||||
|
||||
/// Execute an initialization command.
|
||||
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,
|
||||
// we try to figure it out automatically by downloading the package index
|
||||
|
@ -6,6 +6,8 @@ mod greet;
|
||||
mod init;
|
||||
mod package;
|
||||
mod query;
|
||||
#[cfg(feature = "http-server")]
|
||||
mod server;
|
||||
mod terminal;
|
||||
mod timings;
|
||||
#[cfg(feature = "self-update")]
|
||||
@ -44,6 +46,10 @@ static ARGS: LazyLock<CliArguments> = LazyLock::new(|| {
|
||||
|
||||
/// Entry point.
|
||||
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();
|
||||
|
||||
if let Err(msg) = res {
|
||||
@ -56,11 +62,11 @@ fn main() -> ExitCode {
|
||||
|
||||
/// Execute the requested command.
|
||||
fn dispatch() -> HintedStrResult<()> {
|
||||
let timer = Timer::new(&ARGS);
|
||||
let mut timer = Timer::new(&ARGS);
|
||||
|
||||
match &ARGS.command {
|
||||
Command::Compile(command) => crate::compile::compile(timer, command.clone())?,
|
||||
Command::Watch(command) => crate::watch::watch(timer, command.clone())?,
|
||||
Command::Compile(command) => crate::compile::compile(&mut timer, command)?,
|
||||
Command::Watch(command) => crate::watch::watch(&mut timer, command)?,
|
||||
Command::Init(command) => crate::init::init(command)?,
|
||||
Command::Query(command) => crate::query::query(command)?,
|
||||
Command::Fonts(command) => crate::fonts::fonts(command),
|
||||
|
@ -1,10 +1,10 @@
|
||||
use typst_kit::package::PackageStorage;
|
||||
|
||||
use crate::args::PackageStorageArgs;
|
||||
use crate::args::PackageArgs;
|
||||
use crate::download;
|
||||
|
||||
/// Returns a new package storage for the given args.
|
||||
pub fn storage(args: &PackageStorageArgs) -> PackageStorage {
|
||||
pub fn storage(args: &PackageArgs) -> PackageStorage {
|
||||
PackageStorage::new(
|
||||
args.package_cache_path.clone(),
|
||||
args.package_path.clone(),
|
||||
|
@ -3,7 +3,7 @@ use ecow::{eco_format, EcoString};
|
||||
use serde::Serialize;
|
||||
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
|
||||
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
||||
use typst::model::Document;
|
||||
use typst::layout::PagedDocument;
|
||||
use typst::syntax::Span;
|
||||
use typst::World;
|
||||
use typst_eval::{eval_string, EvalMode};
|
||||
@ -15,7 +15,7 @@ use crate::world::SystemWorld;
|
||||
|
||||
/// Execute a query command.
|
||||
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.
|
||||
world.reset();
|
||||
@ -29,7 +29,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
|
||||
let data = retrieve(&world, command, &document)?;
|
||||
let serialized = format(data, command)?;
|
||||
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})"))?;
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
|
||||
&world,
|
||||
&errors,
|
||||
&warnings,
|
||||
command.common.diagnostic_format,
|
||||
command.process.diagnostic_format,
|
||||
)
|
||||
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
||||
}
|
||||
@ -53,7 +53,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> {
|
||||
fn retrieve(
|
||||
world: &dyn World,
|
||||
command: &QueryCommand,
|
||||
document: &Document,
|
||||
document: &PagedDocument,
|
||||
) -> HintedStrResult<Vec<Content>> {
|
||||
let selector = eval_string(
|
||||
&typst::ROUTINES,
|
||||
|
218
crates/typst-cli/src/server.rs
Normal file
218
crates/typst-cli/src/server.rs
Normal 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>\
|
||||
";
|
@ -22,8 +22,8 @@ impl Timer {
|
||||
/// record timings for a specific function invocation.
|
||||
pub fn new(args: &CliArguments) -> Timer {
|
||||
let record = match &args.command {
|
||||
Command::Compile(command) => command.timings.clone(),
|
||||
Command::Watch(command) => command.timings.clone(),
|
||||
Command::Compile(command) => command.args.timings.clone(),
|
||||
Command::Watch(command) => command.args.timings.clone(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
@ -13,32 +13,38 @@ use same_file::is_same_file;
|
||||
use typst::diag::{bail, StrResult};
|
||||
use typst::utils::format_duration;
|
||||
|
||||
use crate::args::{CompileCommand, Input, Output};
|
||||
use crate::compile::compile_once;
|
||||
use crate::args::{Input, Output, WatchCommand};
|
||||
use crate::compile::{compile_once, CompileConfig};
|
||||
use crate::timings::Timer;
|
||||
use crate::world::{SystemWorld, WorldCreationError};
|
||||
use crate::{print_error, terminal};
|
||||
|
||||
/// Execute a watching compilation command.
|
||||
pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||
let Output::Path(output) = command.output() else {
|
||||
pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
|
||||
let mut config = CompileConfig::watching(command)?;
|
||||
|
||||
let Output::Path(output) = &config.output else {
|
||||
bail!("cannot write document to stdout in watch mode");
|
||||
};
|
||||
|
||||
// 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.
|
||||
// Additionally, if any files do not exist, wait until they do.
|
||||
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,
|
||||
Err(
|
||||
ref err @ (WorldCreationError::InputNotFound(ref path)
|
||||
| WorldCreationError::RootNotFound(ref path)),
|
||||
) => {
|
||||
watcher.update([path.clone()])?;
|
||||
Status::Error.print(&command).unwrap();
|
||||
Status::Error.print(&config).unwrap();
|
||||
print_error(&err.to_string()).unwrap();
|
||||
watcher.wait()?;
|
||||
}
|
||||
@ -47,7 +53,7 @@ pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||
};
|
||||
|
||||
// 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.
|
||||
watcher.update(world.dependencies())?;
|
||||
@ -61,7 +67,7 @@ pub fn watch(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||
world.reset();
|
||||
|
||||
// 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.
|
||||
comemo::evict(10);
|
||||
@ -267,8 +273,7 @@ pub enum Status {
|
||||
|
||||
impl Status {
|
||||
/// Clear the terminal and render the status message.
|
||||
pub fn print(&self, command: &CompileCommand) -> io::Result<()> {
|
||||
let output = command.output();
|
||||
pub fn print(&self, config: &CompileConfig) -> io::Result<()> {
|
||||
let timestamp = chrono::offset::Local::now().format("%H:%M:%S");
|
||||
let color = self.color();
|
||||
|
||||
@ -278,7 +283,7 @@ impl Status {
|
||||
out.set_color(&color)?;
|
||||
write!(out, "watching")?;
|
||||
out.reset()?;
|
||||
match &command.common.input {
|
||||
match &config.input {
|
||||
Input::Stdin => writeln!(out, " <stdin>"),
|
||||
Input::Path(path) => writeln!(out, " {}", path.display()),
|
||||
}?;
|
||||
@ -286,7 +291,15 @@ impl Status {
|
||||
out.set_color(&color)?;
|
||||
write!(out, "writing to")?;
|
||||
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, "[{timestamp}] {}", self.message())?;
|
||||
|
@ -17,8 +17,7 @@ use typst_kit::fonts::{FontSlot, Fonts};
|
||||
use typst_kit::package::PackageStorage;
|
||||
use typst_timing::timed;
|
||||
|
||||
use crate::args::{Input, SharedArgs};
|
||||
use crate::compile::ExportCache;
|
||||
use crate::args::{Feature, Input, ProcessArgs, WorldArgs};
|
||||
use crate::download::PrintDownload;
|
||||
use crate::package;
|
||||
|
||||
@ -49,16 +48,17 @@ pub struct SystemWorld {
|
||||
/// always the same within one compilation.
|
||||
/// Reset between compilations if not [`Now::Fixed`].
|
||||
now: Now,
|
||||
/// The export cache, used for caching output files in `typst watch`
|
||||
/// sessions.
|
||||
export_cache: ExportCache,
|
||||
}
|
||||
|
||||
impl SystemWorld {
|
||||
/// 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.
|
||||
if let Some(jobs) = command.jobs {
|
||||
if let Some(jobs) = process_args.jobs {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(jobs)
|
||||
.use_current_thread()
|
||||
@ -67,7 +67,7 @@ impl SystemWorld {
|
||||
}
|
||||
|
||||
// Resolve the system-global input path.
|
||||
let input = match &command.input {
|
||||
let input = match input {
|
||||
Input::Stdin => None,
|
||||
Input::Path(path) => {
|
||||
Some(path.canonicalize().map_err(|err| match err.kind() {
|
||||
@ -81,7 +81,7 @@ impl SystemWorld {
|
||||
|
||||
// Resolve the system-global root directory.
|
||||
let root = {
|
||||
let path = command
|
||||
let path = world_args
|
||||
.root
|
||||
.as_deref()
|
||||
.or_else(|| input.as_deref().and_then(|i| i.parent()))
|
||||
@ -106,23 +106,28 @@ impl SystemWorld {
|
||||
|
||||
let library = {
|
||||
// Convert the input pairs to a dictionary.
|
||||
let inputs: Dict = command
|
||||
let inputs: Dict = world_args
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
|
||||
.collect();
|
||||
|
||||
let features =
|
||||
command.feature.iter().map(|&feature| match feature {}).collect();
|
||||
let features = process_args
|
||||
.features
|
||||
.iter()
|
||||
.map(|&feature| match feature {
|
||||
Feature::Html => typst::Feature::Html,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Library::builder().with_inputs(inputs).with_features(features).build()
|
||||
};
|
||||
|
||||
let fonts = Fonts::searcher()
|
||||
.include_system_fonts(!command.font_args.ignore_system_fonts)
|
||||
.search_with(&command.font_args.font_paths);
|
||||
.include_system_fonts(!world_args.font.ignore_system_fonts)
|
||||
.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),
|
||||
None => Now::System(OnceLock::new()),
|
||||
};
|
||||
@ -135,9 +140,8 @@ impl SystemWorld {
|
||||
book: LazyHash::new(fonts.book),
|
||||
fonts: fonts.fonts,
|
||||
slots: Mutex::new(HashMap::new()),
|
||||
package_storage: package::storage(&command.package_storage_args),
|
||||
package_storage: package::storage(&world_args.package),
|
||||
now,
|
||||
export_cache: ExportCache::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -182,11 +186,6 @@ impl SystemWorld {
|
||||
pub fn lookup(&self, id: FileId) -> Source {
|
||||
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 {
|
||||
|
@ -253,8 +253,8 @@ pub fn eval_closure(
|
||||
// Handle control flow.
|
||||
let output = body.eval(&mut vm)?;
|
||||
match vm.flow {
|
||||
Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit),
|
||||
Some(FlowEvent::Return(_, None)) => {}
|
||||
Some(FlowEvent::Return(_, Some(explicit), _)) => return Ok(explicit),
|
||||
Some(FlowEvent::Return(_, None, _)) => {}
|
||||
Some(flow) => bail!(flow.forbidden()),
|
||||
None => {}
|
||||
}
|
||||
@ -391,7 +391,9 @@ fn wrap_args_in_math(
|
||||
}
|
||||
Ok(Value::Content(
|
||||
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::*;
|
||||
|
||||
#[track_caller]
|
||||
fn test(text: &str, result: &[&str]) {
|
||||
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 mut visitor = CapturesVisitor::new(Some(&scopes), Capturer::Function);
|
||||
fn test(scopes: &Scopes, text: &str, result: &[&str]) {
|
||||
let mut visitor = CapturesVisitor::new(Some(scopes), Capturer::Function);
|
||||
let root = parse(text);
|
||||
visitor.visit(&root);
|
||||
|
||||
@ -611,44 +607,95 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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.
|
||||
test("#let x = x", &["x"]);
|
||||
test("#let x; #(x + y)", &["y"]);
|
||||
test("#let f(x, y) = x + y", &[]);
|
||||
test("#let f(x, y) = f", &[]);
|
||||
test("#let f = (x, y) => f", &["f"]);
|
||||
test(s, "#let x = x", &["x"]);
|
||||
test(s, "#let x; #(x + y)", &["y"]);
|
||||
test(s, "#let f(x, y) = x + y", &[]);
|
||||
test(s, "#let f(x, y) = f", &[]);
|
||||
test(s, "#let f = (x, y) => f", &["f"]);
|
||||
|
||||
// Closure with different kinds of params.
|
||||
test("#((x, y) => x + z)", &["z"]);
|
||||
test("#((x: y, z) => x + z)", &["y"]);
|
||||
test("#((..x) => x + y)", &["y"]);
|
||||
test("#((x, y: x + z) => x + y)", &["x", "z"]);
|
||||
test("#{x => x; x}", &["x"]);
|
||||
test(s, "#((x, y) => x + z)", &["z"]);
|
||||
test(s, "#((x: y, z) => x + z)", &["y"]);
|
||||
test(s, "#((..x) => x + y)", &["y"]);
|
||||
test(s, "#((x, y: x + z) => x + y)", &["x", "z"]);
|
||||
test(s, "#{x => x; x}", &["x"]);
|
||||
|
||||
// Show rule.
|
||||
test("#show y: x => x", &["y"]);
|
||||
test("#show y: x => x + z", &["y", "z"]);
|
||||
test("#show x: x => x", &["x"]);
|
||||
test(s, "#show y: x => x", &["y"]);
|
||||
test(s, "#show y: x => x + z", &["y", "z"]);
|
||||
test(s, "#show x: x => x", &["x"]);
|
||||
|
||||
// For loop.
|
||||
test("#for x in y { x + z }", &["y", "z"]);
|
||||
test("#for (x, y) in y { x + y }", &["y"]);
|
||||
test("#for x in y {} #x", &["x", "y"]);
|
||||
test(s, "#for x in y { x + z }", &["y", "z"]);
|
||||
test(s, "#for (x, y) in y { x + y }", &["y"]);
|
||||
test(s, "#for x in y {} #x", &["x", "y"]);
|
||||
|
||||
// Import.
|
||||
test("#import z: x, y", &["z"]);
|
||||
test("#import x + y: x, y, z", &["x", "y"]);
|
||||
test(s, "#import z: x, y", &["z"]);
|
||||
test(s, "#import x + y: x, y, z", &["x", "y"]);
|
||||
|
||||
// Blocks.
|
||||
test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
|
||||
test("#[#let x = 1]#x", &["x"]);
|
||||
test(s, "#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
|
||||
test(s, "#[#let x = 1]#x", &["x"]);
|
||||
|
||||
// Field access.
|
||||
test("#foo(body: 1)", &[]);
|
||||
test("#(body: 1)", &[]);
|
||||
test("#(body = 1)", &[]);
|
||||
test("#(body += y)", &["y"]);
|
||||
test("#{ (body, a) = (y, 1) }", &["y"]);
|
||||
test("#(x.at(y) = 5)", &["x", "y"])
|
||||
test(s, "#x.y.f(z)", &["x", "z"]);
|
||||
|
||||
// Parenthesized expressions.
|
||||
test(s, "#f(x: 1)", &["f"]);
|
||||
test(s, "#(x: 1)", &[]);
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
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::{
|
||||
ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str,
|
||||
Value,
|
||||
ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement,
|
||||
Selector, Str, Value,
|
||||
};
|
||||
use typst_library::introspection::{Counter, State};
|
||||
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<'_> {
|
||||
type Output = Value;
|
||||
@ -54,7 +57,8 @@ fn eval_code<'a>(
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -355,6 +359,29 @@ impl Eval for ast::Contextual<'_> {
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ pub enum FlowEvent {
|
||||
/// Skip the remainder of the current iteration in a loop.
|
||||
Continue(Span),
|
||||
/// Stop execution of a function early, optionally returning an explicit
|
||||
/// value.
|
||||
Return(Span, Option<Value>),
|
||||
/// value. The final boolean indicates whether the return was conditional.
|
||||
Return(Span, Option<Value>, bool),
|
||||
}
|
||||
|
||||
impl FlowEvent {
|
||||
@ -31,7 +31,7 @@ impl FlowEvent {
|
||||
Self::Continue(span) => {
|
||||
error!(span, "cannot continue outside of loop")
|
||||
}
|
||||
Self::Return(span, _) => {
|
||||
Self::Return(span, _, _) => {
|
||||
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> {
|
||||
let condition = self.condition();
|
||||
if condition.eval(vm)?.cast::<bool>().at(condition.span())? {
|
||||
self.if_body().eval(vm)
|
||||
let output = if condition.eval(vm)?.cast::<bool>().at(condition.span())? {
|
||||
self.if_body().eval(vm)?
|
||||
} else if let Some(else_body) = self.else_body() {
|
||||
else_body.eval(vm)
|
||||
else_body.eval(vm)?
|
||||
} 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;
|
||||
}
|
||||
|
||||
// Mark the return as conditional.
|
||||
if let Some(FlowEvent::Return(_, _, conditional)) = &mut vm.flow {
|
||||
*conditional = true;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
@ -168,6 +180,11 @@ impl Eval for ast::ForLoop<'_> {
|
||||
vm.flow = flow;
|
||||
}
|
||||
|
||||
// Mark the return as conditional.
|
||||
if let Some(FlowEvent::Return(_, _, conditional)) = &mut vm.flow {
|
||||
*conditional = true;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
@ -200,7 +217,7 @@ impl Eval for ast::FuncReturn<'_> {
|
||||
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
let value = self.body().map(|body| body.eval(vm)).transpose()?;
|
||||
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)
|
||||
}
|
||||
|
@ -148,7 +148,8 @@ pub fn eval_string(
|
||||
EvalMode::Math => Value::Content(
|
||||
EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?)
|
||||
.with_block(false)
|
||||
.pack(),
|
||||
.pack()
|
||||
.spanned(span),
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -11,6 +11,7 @@ use typst_library::text::{
|
||||
LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem,
|
||||
};
|
||||
use typst_syntax::ast::{self, AstNode};
|
||||
use typst_utils::PicoStr;
|
||||
|
||||
use crate::{Eval, Vm};
|
||||
|
||||
@ -122,7 +123,7 @@ impl Eval for ast::Escape<'_> {
|
||||
type Output = Value;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
if let Some(supplement) = self.supplement() {
|
||||
elem.push_supplement(Smart::Custom(Some(Supplement::Content(
|
||||
|
@ -32,7 +32,7 @@ impl Eval for ast::MathShorthand<'_> {
|
||||
type Output = Value;
|
||||
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Value::Symbol(Symbol::single(self.get().into())))
|
||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
|
26
crates/typst-html/Cargo.toml
Normal file
26
crates/typst-html/Cargo.toml
Normal 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
|
146
crates/typst-html/src/encode.rs
Normal file
146
crates/typst-html/src/encode.rs
Normal 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("&"),
|
||||
'<' => w.buf.push_str("<"),
|
||||
'>' => w.buf.push_str(">"),
|
||||
'"' => w.buf.push_str("""),
|
||||
'\'' => w.buf.push_str("'"),
|
||||
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);
|
||||
}
|
315
crates/typst-html/src/lib.rs
Normal file
315
crates/typst-html/src/lib.rs
Normal 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>),
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
use comemo::Track;
|
||||
use ecow::{eco_vec, EcoString, EcoVec};
|
||||
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 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
|
||||
/// - A split offset: All labels before this offset belong to nodes, all after
|
||||
/// 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![];
|
||||
|
||||
// Labels in the document.
|
||||
@ -88,9 +91,7 @@ pub fn analyze_labels(document: &Document) -> (Vec<(Label, Option<EcoString>)>,
|
||||
let split = output.len();
|
||||
|
||||
// Bibliography keys.
|
||||
for (key, detail) in BibliographyElem::keys(document.introspector.track()) {
|
||||
output.push((Label::new(key.as_str()), detail));
|
||||
}
|
||||
output.extend(BibliographyElem::keys(document.introspector.track()));
|
||||
|
||||
(output, split)
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ use typst::foundations::{
|
||||
fields_on, repr, AutoValue, CastInfo, Func, Label, NoneValue, ParamInfo, Repr,
|
||||
StyleChain, Styles, Type, Value,
|
||||
};
|
||||
use typst::layout::{Alignment, Dir};
|
||||
use typst::model::Document;
|
||||
use typst::layout::{Alignment, Dir, PagedDocument};
|
||||
use typst::syntax::ast::AstNode;
|
||||
use typst::syntax::{
|
||||
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.
|
||||
pub fn autocomplete(
|
||||
world: &dyn IdeWorld,
|
||||
document: Option<&Document>,
|
||||
document: Option<&PagedDocument>,
|
||||
source: &Source,
|
||||
cursor: usize,
|
||||
explicit: bool,
|
||||
@ -1063,7 +1062,7 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
|
||||
/// Context for autocompletion.
|
||||
struct CompletionContext<'a> {
|
||||
world: &'a (dyn IdeWorld + 'a),
|
||||
document: Option<&'a Document>,
|
||||
document: Option<&'a PagedDocument>,
|
||||
text: &'a str,
|
||||
before: &'a str,
|
||||
after: &'a str,
|
||||
@ -1079,7 +1078,7 @@ impl<'a> CompletionContext<'a> {
|
||||
/// Create a new autocompletion context.
|
||||
fn new(
|
||||
world: &'a (dyn IdeWorld + 'a),
|
||||
document: Option<&'a Document>,
|
||||
document: Option<&'a PagedDocument>,
|
||||
source: &'a Source,
|
||||
leaf: &'a LinkedNode<'a>,
|
||||
cursor: usize,
|
||||
@ -1254,11 +1253,11 @@ impl<'a> CompletionContext<'a> {
|
||||
eco_format!(
|
||||
"{}{}{}",
|
||||
if open { "<" } else { "" },
|
||||
label.as_str(),
|
||||
label.resolve(),
|
||||
if close { ">" } else { "" }
|
||||
)
|
||||
}),
|
||||
label: label.as_str().into(),
|
||||
label: label.resolve().as_str().into(),
|
||||
detail,
|
||||
});
|
||||
}
|
||||
@ -1507,7 +1506,7 @@ impl BracketMode {
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use typst::model::Document;
|
||||
use typst::layout::PagedDocument;
|
||||
use typst::syntax::{FileId, Source, VirtualPath};
|
||||
use typst::World;
|
||||
|
||||
@ -1607,7 +1606,7 @@ mod tests {
|
||||
fn test_full(
|
||||
world: &TestWorld,
|
||||
source: &Source,
|
||||
doc: Option<&Document>,
|
||||
doc: Option<&PagedDocument>,
|
||||
cursor: isize,
|
||||
) -> Response {
|
||||
autocomplete(world, doc, source, source.cursor(cursor), true)
|
||||
|
@ -1,6 +1,7 @@
|
||||
use typst::foundations::{Label, Selector, Value};
|
||||
use typst::model::Document;
|
||||
use typst::layout::PagedDocument;
|
||||
use typst::syntax::{ast, LinkedNode, Side, Source, Span};
|
||||
use typst::utils::PicoStr;
|
||||
|
||||
use crate::utils::globals;
|
||||
use crate::{
|
||||
@ -24,7 +25,7 @@ pub enum Definition {
|
||||
/// when the document is available.
|
||||
pub fn definition(
|
||||
world: &dyn IdeWorld,
|
||||
document: Option<&Document>,
|
||||
document: Option<&PagedDocument>,
|
||||
source: &Source,
|
||||
cursor: usize,
|
||||
side: Side,
|
||||
@ -71,7 +72,7 @@ pub fn definition(
|
||||
|
||||
// Try to jump to the referenced content.
|
||||
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 elem = document?.introspector.query_first(&selector)?;
|
||||
return Some(Definition::Span(elem.span()));
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use typst::layout::{Frame, FrameItem, Point, Position, Size};
|
||||
use typst::model::{Destination, Document, Url};
|
||||
use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
|
||||
use typst::model::{Destination, Url};
|
||||
use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind};
|
||||
use typst::visualize::Geometry;
|
||||
use typst::WorldExt;
|
||||
@ -30,7 +30,7 @@ impl Jump {
|
||||
/// Determine where to jump to based on a click in a frame.
|
||||
pub fn jump_from_click(
|
||||
world: &dyn IdeWorld,
|
||||
document: &Document,
|
||||
document: &PagedDocument,
|
||||
frame: &Frame,
|
||||
click: Point,
|
||||
) -> Option<Jump> {
|
||||
@ -110,7 +110,7 @@ pub fn jump_from_click(
|
||||
|
||||
/// Find the output location in the document for a cursor position.
|
||||
pub fn jump_from_cursor(
|
||||
document: &Document,
|
||||
document: &PagedDocument,
|
||||
source: &Source,
|
||||
cursor: usize,
|
||||
) -> Vec<Position> {
|
||||
|
@ -119,6 +119,10 @@ impl IdeWorld for TestWorld {
|
||||
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
||||
const LIST: &[(PackageSpec, Option<EcoString>)] = &[(
|
||||
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"),
|
||||
name: EcoString::inline("example"),
|
||||
version: PackageVersion { major: 0, minor: 1, patch: 0 },
|
||||
|
@ -4,8 +4,7 @@ use ecow::{eco_format, EcoString};
|
||||
use if_chain::if_chain;
|
||||
use typst::engine::Sink;
|
||||
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
|
||||
use typst::layout::Length;
|
||||
use typst::model::Document;
|
||||
use typst::layout::{Length, PagedDocument};
|
||||
use typst::syntax::ast::AstNode;
|
||||
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
||||
use typst::utils::{round_with_precision, Numeric};
|
||||
@ -21,7 +20,7 @@ use crate::{analyze_expr, analyze_import, analyze_labels, IdeWorld};
|
||||
/// document is available.
|
||||
pub fn tooltip(
|
||||
world: &dyn IdeWorld,
|
||||
document: Option<&Document>,
|
||||
document: Option<&PagedDocument>,
|
||||
source: &Source,
|
||||
cursor: usize,
|
||||
side: Side,
|
||||
@ -173,7 +172,7 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
SyntaxKind::RefMarker => leaf.text().trim_start_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 {
|
||||
if label.as_str() == target {
|
||||
if label.resolve().as_str() == target {
|
||||
return Some(Tooltip::Text(detail?));
|
||||
}
|
||||
}
|
||||
@ -338,6 +337,21 @@ mod tests {
|
||||
fn test_tooltip_closure() {
|
||||
test("#let f(x) = x + y", 11, Side::Before)
|
||||
.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]
|
||||
|
@ -15,6 +15,9 @@ use crate::download::{Downloader, Progress};
|
||||
/// The default Typst registry.
|
||||
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.
|
||||
pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
|
||||
|
||||
@ -28,7 +31,7 @@ pub struct PackageStorage {
|
||||
package_path: Option<PathBuf>,
|
||||
/// The downloader used for fetching the index and packages.
|
||||
downloader: Downloader,
|
||||
/// The cached index of the preview namespace.
|
||||
/// The cached index of the default namespace.
|
||||
index: OnceCell<Vec<PackageInfo>>,
|
||||
}
|
||||
|
||||
@ -85,7 +88,7 @@ impl PackageStorage {
|
||||
}
|
||||
|
||||
// Download from network if it doesn't exist yet.
|
||||
if spec.namespace == "preview" {
|
||||
if spec.namespace == DEFAULT_NAMESPACE {
|
||||
self.download_package(spec, &dir, progress)?;
|
||||
if dir.exists() {
|
||||
return Ok(dir);
|
||||
@ -101,8 +104,8 @@ impl PackageStorage {
|
||||
&self,
|
||||
spec: &VersionlessPackageSpec,
|
||||
) -> StrResult<PackageVersion> {
|
||||
if spec.namespace == "preview" {
|
||||
// For `@preview`, download the package index and find the latest
|
||||
if spec.namespace == DEFAULT_NAMESPACE {
|
||||
// For `DEFAULT_NAMESPACE`, download the package index and find the latest
|
||||
// version.
|
||||
self.download_index()?
|
||||
.iter()
|
||||
@ -131,7 +134,7 @@ impl PackageStorage {
|
||||
pub fn download_index(&self) -> StrResult<&[PackageInfo]> {
|
||||
self.index
|
||||
.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) {
|
||||
Ok(response) => response.into_json().map_err(|err| {
|
||||
eco_format!("failed to parse package index: {err}")
|
||||
@ -148,17 +151,19 @@ impl PackageStorage {
|
||||
/// Download a package over the network.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the package spec namespace isn't `preview`.
|
||||
/// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`.
|
||||
pub fn download_package(
|
||||
&self,
|
||||
spec: &PackageSpec,
|
||||
package_dir: &Path,
|
||||
progress: &mut dyn Progress,
|
||||
) -> PackageResult<()> {
|
||||
assert_eq!(spec.namespace, "preview");
|
||||
assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
|
||||
|
||||
let url =
|
||||
format!("{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz", spec.name, spec.version);
|
||||
let url = format!(
|
||||
"{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/{}-{}.tar.gz",
|
||||
spec.name, spec.version
|
||||
);
|
||||
|
||||
let data = match self.downloader.download_with_progress(&url, progress) {
|
||||
Ok(data) => data,
|
||||
|
@ -5,7 +5,7 @@ use std::hash::Hash;
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
use bumpalo::Bump;
|
||||
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::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||
use typst_library::introspection::{
|
||||
@ -83,7 +83,11 @@ impl<'a> Collector<'a, '_, '_> {
|
||||
hint: "try using a `#colbreak()` instead",
|
||||
);
|
||||
} 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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,6 +214,13 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// Process pending footnotes.
|
||||
for note in std::mem::take(&mut self.work.footnotes) {
|
||||
@ -222,7 +229,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
|
||||
// Process pending floats.
|
||||
for placed in std::mem::take(&mut self.work.floats) {
|
||||
self.float(placed, ®ions, false)?;
|
||||
self.float(placed, ®ions, false, false)?;
|
||||
}
|
||||
|
||||
distribute(self, regions)
|
||||
@ -236,13 +243,21 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
/// (depending on `placed.scope`).
|
||||
///
|
||||
/// 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
|
||||
/// --- it is set if there are already distributed items.
|
||||
/// value of `clearance` indicates that between the float and flow content
|
||||
/// 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(
|
||||
&mut self,
|
||||
placed: &'b PlacedChild<'a>,
|
||||
regions: &Regions,
|
||||
clearance: bool,
|
||||
migratable: bool,
|
||||
) -> FlowResult<()> {
|
||||
// If the float is already processed, skip it.
|
||||
let loc = placed.location();
|
||||
@ -291,7 +306,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
}
|
||||
|
||||
// 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
|
||||
// `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
|
||||
/// any. The value of `breakable` indicates whether the element that
|
||||
/// 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(
|
||||
&mut self,
|
||||
regions: &Regions,
|
||||
frame: &Frame,
|
||||
flow_need: Abs,
|
||||
breakable: bool,
|
||||
migratable: bool,
|
||||
) -> FlowResult<()> {
|
||||
// Footnotes are only supported at the root level.
|
||||
if !self.config.root {
|
||||
@ -352,7 +374,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
|
||||
let mut relayout = false;
|
||||
let mut regions = *regions;
|
||||
let mut migratable = !breakable && regions.may_progress();
|
||||
let mut migratable = migratable && !breakable && regions.may_progress();
|
||||
|
||||
for (y, elem) in notes {
|
||||
// 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.
|
||||
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.
|
||||
|
@ -240,7 +240,8 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
|
||||
|
||||
// Handle fractionally sized blocks.
|
||||
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.items.push(Item::Fr(fr, Some(single)));
|
||||
return Ok(());
|
||||
@ -323,8 +324,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
|
||||
}
|
||||
|
||||
// Handle footnotes.
|
||||
self.composer
|
||||
.footnotes(&self.regions, &frame, frame.height(), breakable)?;
|
||||
self.composer.footnotes(
|
||||
&self.regions,
|
||||
&frame,
|
||||
frame.height(),
|
||||
breakable,
|
||||
true,
|
||||
)?;
|
||||
|
||||
// Push an item for the frame.
|
||||
self.regions.size.y -= frame.height();
|
||||
@ -347,11 +353,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
|
||||
placed,
|
||||
&self.regions,
|
||||
self.items.iter().any(|item| matches!(item, Item::Frame(..))),
|
||||
true,
|
||||
)?;
|
||||
self.regions.size.y -= weak_spacing;
|
||||
} else {
|
||||
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.items.push(Item::Placed(frame, placed));
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ fn layout_fragment_impl(
|
||||
|
||||
let arenas = Arenas::default();
|
||||
let children = (engine.routines.realize)(
|
||||
RealizationKind::Container,
|
||||
RealizationKind::LayoutFragment,
|
||||
&mut engine,
|
||||
&mut locator,
|
||||
&arenas,
|
||||
|
@ -45,7 +45,7 @@ impl<T> Repeatable<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GridLayouter<'a> {
|
||||
impl GridLayouter<'_> {
|
||||
/// Layouts the header's rows.
|
||||
/// Skips regions as necessary.
|
||||
pub fn layout_header(
|
||||
|
@ -85,7 +85,7 @@ pub struct CellMeasurementData<'layouter> {
|
||||
pub frames_in_previous_regions: usize,
|
||||
}
|
||||
|
||||
impl<'a> GridLayouter<'a> {
|
||||
impl GridLayouter<'_> {
|
||||
/// Layout a rowspan over the already finished regions, plus the current
|
||||
/// region's frame and resolved rows, if it wasn't finished yet (because
|
||||
/// we're being called from `finish_region`, but note that this function is
|
||||
|
@ -55,6 +55,7 @@ pub fn layout_image(
|
||||
elem.alt(styles),
|
||||
engine.world,
|
||||
&families(styles).collect::<Vec<_>>(),
|
||||
elem.flatten_text(styles),
|
||||
)
|
||||
.at(span)?;
|
||||
|
||||
|
@ -256,8 +256,7 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
|
||||
fn push_text(&mut self, text: &str, styles: StyleChain<'a>) {
|
||||
self.full.push_str(text);
|
||||
self.push_segment(Segment::Text(text.len(), styles));
|
||||
self.build_text(styles, |full| full.push_str(text));
|
||||
}
|
||||
|
||||
fn build_text<F>(&mut self, styles: StyleChain<'a>, f: F)
|
||||
@ -266,33 +265,33 @@ impl<'a> Collector<'a> {
|
||||
{
|
||||
let prev = self.full.len();
|
||||
f(&mut self.full);
|
||||
let len = self.full.len() - prev;
|
||||
self.push_segment(Segment::Text(len, styles));
|
||||
let segment_len = self.full.len() - prev;
|
||||
|
||||
// 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>) {
|
||||
self.full.push_str(item.textual());
|
||||
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;
|
||||
}
|
||||
|
||||
match (self.segments.last_mut(), &item) {
|
||||
// Merge adjacent weak spacing by taking the maximum.
|
||||
(
|
||||
Some(Segment::Item(Item::Absolute(prev_amount, true))),
|
||||
Segment::Item(Item::Absolute(amount, true)),
|
||||
Item::Absolute(amount, true),
|
||||
) => {
|
||||
*prev_amount = (*prev_amount).max(*amount);
|
||||
}
|
||||
|
||||
_ => self.segments.push(segment),
|
||||
_ => {
|
||||
self.full.push_str(item.textual());
|
||||
self.segments.push(Segment::Item(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ pub struct Line<'a> {
|
||||
pub dash: Option<Dash>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
impl Line<'_> {
|
||||
/// Create an empty line.
|
||||
pub fn empty() -> 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 {
|
||||
&mut self.0
|
||||
}
|
||||
|
@ -543,7 +543,12 @@ fn raw_ratio(
|
||||
) -> f64 {
|
||||
// Determine how much the line's spaces would need to be stretched
|
||||
// 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.
|
||||
let adjustability = if delta >= Abs::zero() { stretchability } else { shrinkability };
|
||||
@ -966,11 +971,13 @@ where
|
||||
}
|
||||
|
||||
/// Estimates the metrics for the line spanned by the range.
|
||||
#[track_caller]
|
||||
fn estimate(&self, range: Range) -> T {
|
||||
self.get(range.end) - self.get(range.start)
|
||||
}
|
||||
|
||||
/// Get the metric at the given byte position.
|
||||
#[track_caller]
|
||||
fn get(&self, index: usize) -> T {
|
||||
match index.checked_sub(1) {
|
||||
None => T::default(),
|
||||
|
@ -148,7 +148,8 @@ impl MathFragment {
|
||||
|
||||
pub fn is_text_like(&self) -> bool {
|
||||
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,
|
||||
_ => false,
|
||||
}
|
||||
@ -247,6 +248,7 @@ pub struct GlyphFragment {
|
||||
pub dests: SmallVec<[Destination; 1]>,
|
||||
pub hidden: bool,
|
||||
pub limits: Limits,
|
||||
pub extended_shape: bool,
|
||||
}
|
||||
|
||||
impl GlyphFragment {
|
||||
@ -302,6 +304,7 @@ impl GlyphFragment {
|
||||
span,
|
||||
dests: LinkElem::dests_in(styles),
|
||||
hidden: HideElem::hidden_in(styles),
|
||||
extended_shape: false,
|
||||
};
|
||||
fragment.set_id(ctx, id);
|
||||
fragment
|
||||
@ -332,7 +335,8 @@ impl GlyphFragment {
|
||||
let accent_attach =
|
||||
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;
|
||||
}
|
||||
|
||||
@ -342,6 +346,7 @@ impl GlyphFragment {
|
||||
self.descent = -bbox.y_min.scaled(ctx, self.font_size);
|
||||
self.italics_correction = italics;
|
||||
self.accent_attach = accent_attach;
|
||||
self.extended_shape = extended_shape;
|
||||
}
|
||||
|
||||
pub fn height(&self) -> Abs {
|
||||
@ -358,6 +363,7 @@ impl GlyphFragment {
|
||||
math_size: self.math_size,
|
||||
span: self.span,
|
||||
limits: self.limits,
|
||||
extended_shape: self.extended_shape,
|
||||
frame: self.into_frame(),
|
||||
mid_stretched: None,
|
||||
}
|
||||
@ -465,6 +471,7 @@ pub struct VariantFragment {
|
||||
pub span: Span,
|
||||
pub limits: Limits,
|
||||
pub mid_stretched: Option<bool>,
|
||||
pub extended_shape: bool,
|
||||
}
|
||||
|
||||
impl VariantFragment {
|
||||
|
@ -28,8 +28,21 @@ pub fn layout_lr(
|
||||
}
|
||||
|
||||
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 max_extent = fragments
|
||||
let max_extent = inner_fragments
|
||||
.iter()
|
||||
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
|
||||
.max()
|
||||
@ -39,7 +52,7 @@ pub fn layout_lr(
|
||||
let height = elem.size(styles);
|
||||
|
||||
// Scale up fragments at both ends.
|
||||
match fragments.as_mut_slice() {
|
||||
match inner_fragments {
|
||||
[one] => scale(ctx, styles, one, relative_to, height, None),
|
||||
[first, .., last] => {
|
||||
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.
|
||||
for fragment in &mut fragments {
|
||||
for fragment in inner_fragments.iter_mut() {
|
||||
if let MathFragment::Variant(ref mut variant) = fragment {
|
||||
if variant.mid_stretched == Some(false) {
|
||||
variant.mid_stretched = Some(true);
|
||||
@ -60,12 +73,19 @@ pub fn layout_lr(
|
||||
|
||||
// Remove weak SpacingFragment immediately after the opening or immediately
|
||||
// before the closing.
|
||||
let original_len = fragments.len();
|
||||
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| {
|
||||
let discard = (index == start_idx + 1 && opening_exists
|
||||
|| index + 2 == end_idx && closing_exists)
|
||||
&& matches!(fragment, MathFragment::Spacing(_, true));
|
||||
index += 1;
|
||||
(index != 2 && index + 1 != original_len)
|
||||
|| !matches!(fragment, MathFragment::Spacing(_, true))
|
||||
!discard
|
||||
});
|
||||
|
||||
ctx.extend(fragments);
|
||||
|
@ -127,7 +127,9 @@ fn layout_vec_body(
|
||||
let denom_style = style_for_denominator(styles);
|
||||
let mut flat = vec![];
|
||||
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
|
||||
// to ensure that normal vectors are aligned with others unless they are
|
||||
|
@ -419,7 +419,10 @@ impl MathRunFrameBuilder {
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -121,7 +121,6 @@ pub fn stack(
|
||||
alternator: LeftRightAlternator,
|
||||
minimum_ascent_descent: Option<(Abs, Abs)>,
|
||||
) -> Frame {
|
||||
let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
|
||||
let AlignmentResult { points, width } = alignments(&rows);
|
||||
let rows: Vec<_> = rows
|
||||
.into_iter()
|
||||
|
@ -295,6 +295,7 @@ fn assemble(
|
||||
span: base.span,
|
||||
limits: base.limits,
|
||||
mid_stretched: None,
|
||||
extended_shape: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,8 +150,8 @@ fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
|
||||
auto_italic
|
||||
&& matches!(
|
||||
c,
|
||||
'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' |
|
||||
'∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
|
||||
'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' |
|
||||
'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
|
||||
)
|
||||
&& matches!(variant, Sans | Serif),
|
||||
);
|
||||
@ -306,6 +306,7 @@ fn latin_exception(
|
||||
('e', Cal, false, _) => 'ℯ',
|
||||
('g', Cal, false, _) => 'ℊ',
|
||||
('o', Cal, false, _) => 'ℴ',
|
||||
('ħ', Serif, .., true) => 'ℏ',
|
||||
('ı', Serif, .., true) => '𝚤',
|
||||
('ȷ', Serif, .., true) => '𝚥',
|
||||
_ => return None,
|
||||
|
@ -297,7 +297,7 @@ fn layout_underoverspreader(
|
||||
if let Some(annotation) = annotation {
|
||||
let under_style = style_for_subscript(styles);
|
||||
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
|
||||
}
|
||||
@ -305,7 +305,7 @@ fn layout_underoverspreader(
|
||||
if let Some(annotation) = annotation {
|
||||
let over_style = style_for_superscript(styles);
|
||||
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(MathRun::new(vec![body]));
|
||||
|
@ -53,7 +53,7 @@ pub fn collect<'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
|
||||
// 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
|
||||
// don't want to apply it to a potential empty page.
|
||||
if !pagebreak.boundary(styles) {
|
||||
|
@ -11,8 +11,8 @@ use typst_library::foundations::{Content, StyleChain};
|
||||
use typst_library::introspection::{
|
||||
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
|
||||
};
|
||||
use typst_library::layout::{FrameItem, Page, Point};
|
||||
use typst_library::model::{Document, DocumentInfo};
|
||||
use typst_library::layout::{FrameItem, Page, PagedDocument, Point};
|
||||
use typst_library::model::DocumentInfo;
|
||||
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||
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),
|
||||
/// this does not take regions since the regions are defined by the page
|
||||
/// configuration in the content and style chain.
|
||||
#[typst_macros::time(name = "document")]
|
||||
#[typst_macros::time(name = "layout document")]
|
||||
pub fn layout_document(
|
||||
engine: &mut Engine,
|
||||
content: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Document> {
|
||||
) -> SourceResult<PagedDocument> {
|
||||
layout_document_impl(
|
||||
engine.routines,
|
||||
engine.world,
|
||||
@ -56,7 +56,7 @@ fn layout_document_impl(
|
||||
route: Tracked<Route>,
|
||||
content: &Content,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<Document> {
|
||||
) -> SourceResult<PagedDocument> {
|
||||
let mut locator = Locator::root().split();
|
||||
let mut engine = Engine {
|
||||
routines,
|
||||
@ -75,7 +75,7 @@ fn layout_document_impl(
|
||||
let arenas = Arenas::default();
|
||||
let mut info = DocumentInfo::default();
|
||||
let mut children = (engine.routines.realize)(
|
||||
RealizationKind::Root(&mut info),
|
||||
RealizationKind::LayoutDocument(&mut info),
|
||||
&mut engine,
|
||||
&mut locator,
|
||||
&arenas,
|
||||
@ -84,9 +84,9 @@ fn layout_document_impl(
|
||||
)?;
|
||||
|
||||
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.
|
||||
|
@ -114,6 +114,10 @@ pub fn layout_path(
|
||||
path.close_path();
|
||||
}
|
||||
|
||||
if !size.is_finite() {
|
||||
bail!(elem.span(), "cannot create path with infinite length");
|
||||
}
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = elem.fill(styles);
|
||||
let fill_rule = elem.fill_rule(styles);
|
||||
@ -344,29 +348,48 @@ fn layout_shape(
|
||||
pod.size = crate::pad::shrink(region.size, &inset);
|
||||
}
|
||||
|
||||
// Layout the child.
|
||||
frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?;
|
||||
|
||||
// If the child is a square or circle, relayout with full expansion into
|
||||
// square region to make sure the result is really quadratic.
|
||||
// If the shape is quadratic, we first measure it to determine its size
|
||||
// and then layout with full expansion to force the aspect ratio and
|
||||
// make sure it's really quadratic.
|
||||
if kind.is_quadratic() {
|
||||
let length = frame.size().max_by_side().min(pod.size.min_by_side());
|
||||
let quad_pod = Region::new(Size::splat(length), Axes::splat(true));
|
||||
frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?;
|
||||
let length = match quadratic_size(pod) {
|
||||
Some(length) => length,
|
||||
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.
|
||||
if has_inset {
|
||||
crate::pad::grow(&mut frame, &inset);
|
||||
}
|
||||
} else {
|
||||
// The default size that a shape takes on if it has no child and
|
||||
// enough space.
|
||||
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
|
||||
let mut size = region.expand.select(region.size, default.min(region.size));
|
||||
if kind.is_quadratic() {
|
||||
size = Size::splat(size.min_by_side());
|
||||
}
|
||||
// The default size that a shape takes on if it has no child and no
|
||||
// forced sizes.
|
||||
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(region.size);
|
||||
|
||||
let size = if kind.is_quadratic() {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -407,6 +430,24 @@ fn layout_shape(
|
||||
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.
|
||||
pub fn clip_rect(
|
||||
size: Size,
|
||||
|
@ -23,6 +23,7 @@ bitflags = { workspace = true }
|
||||
bumpalo = { workspace = true }
|
||||
chinese-number = { workspace = true }
|
||||
ciborium = { workspace = true }
|
||||
codex = { workspace = true }
|
||||
comemo = { workspace = true }
|
||||
csv = { workspace = true }
|
||||
ecow = { workspace = true }
|
||||
|
@ -301,6 +301,9 @@ impl Route<'_> {
|
||||
/// The maximum layout nesting depth.
|
||||
const MAX_LAYOUT_DEPTH: usize = 72;
|
||||
|
||||
/// The maximum HTML nesting depth.
|
||||
const MAX_HTML_DEPTH: usize = 72;
|
||||
|
||||
/// The maximum function call nesting depth.
|
||||
const MAX_CALL_DEPTH: usize = 80;
|
||||
|
||||
@ -326,6 +329,17 @@ impl Route<'_> {
|
||||
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.
|
||||
pub fn check_call_depth(&self) -> StrResult<()> {
|
||||
if !self.within(Route::MAX_CALL_DEPTH) {
|
||||
@ -336,6 +350,7 @@ impl Route<'_> {
|
||||
}
|
||||
|
||||
#[comemo::track]
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
impl<'a> Route<'a> {
|
||||
/// Whether the given id is part of the route.
|
||||
pub fn contains(&self, id: FileId) -> bool {
|
||||
|
@ -187,7 +187,7 @@ impl Args {
|
||||
self.items.retain(|item| {
|
||||
if item.name.is_some() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
let span = item.value.span;
|
||||
let spanned = Spanned::new(std::mem::take(&mut item.value.v), span);
|
||||
match T::from_value(spanned).at(span) {
|
||||
|
@ -815,6 +815,19 @@ impl Array {
|
||||
///
|
||||
/// Returns an error if two values could not be compared or if the key
|
||||
/// 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]
|
||||
pub fn sorted(
|
||||
self,
|
||||
|
@ -426,7 +426,7 @@ impl Content {
|
||||
/// selector.
|
||||
///
|
||||
/// Elements produced in `show` rules will not be included in the results.
|
||||
pub fn query_first(&self, selector: Selector) -> Option<Content> {
|
||||
pub fn query_first(&self, selector: &Selector) -> Option<Content> {
|
||||
let mut result = None;
|
||||
self.traverse(&mut |element| {
|
||||
if result.is_none() && selector.matches(&element, None) {
|
||||
@ -481,17 +481,20 @@ impl Content {
|
||||
impl Content {
|
||||
/// Strongly emphasize this content.
|
||||
pub fn strong(self) -> Self {
|
||||
StrongElem::new(self).pack()
|
||||
let span = self.span();
|
||||
StrongElem::new(self).pack().spanned(span)
|
||||
}
|
||||
|
||||
/// Emphasize this content.
|
||||
pub fn emph(self) -> Self {
|
||||
EmphElem::new(self).pack()
|
||||
let span = self.span();
|
||||
EmphElem::new(self).pack().spanned(span)
|
||||
}
|
||||
|
||||
/// Underline this content.
|
||||
pub fn underlined(self) -> Self {
|
||||
UnderlineElem::new(self).pack()
|
||||
let span = self.span();
|
||||
UnderlineElem::new(self).pack().spanned(span)
|
||||
}
|
||||
|
||||
/// Link the content somewhere.
|
||||
@ -506,17 +509,24 @@ impl Content {
|
||||
|
||||
/// Pad this content at the sides.
|
||||
pub fn padded(self, padding: Sides<Rel<Length>>) -> Self {
|
||||
let span = self.span();
|
||||
PadElem::new(self)
|
||||
.with_left(padding.left)
|
||||
.with_top(padding.top)
|
||||
.with_right(padding.right)
|
||||
.with_bottom(padding.bottom)
|
||||
.pack()
|
||||
.spanned(span)
|
||||
}
|
||||
|
||||
/// Transform this content's contents without affecting layout.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,6 +317,7 @@ impl Decimal {
|
||||
})
|
||||
.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.
|
||||
pub enum ToDecimal {
|
||||
/// A decimal to be converted to itself.
|
||||
Decimal(Decimal),
|
||||
/// A string with the decimal's representation.
|
||||
Str(EcoString),
|
||||
/// An integer to be converted to the equivalent decimal.
|
||||
@ -439,7 +442,9 @@ pub enum ToDecimal {
|
||||
|
||||
cast! {
|
||||
ToDecimal,
|
||||
v: Decimal => Self::Decimal(v),
|
||||
v: i64 => Self::Int(v),
|
||||
v: bool => Self::Int(v as i64),
|
||||
v: f64 => Self::Float(v),
|
||||
v: Str => Self::Str(EcoString::from(v)),
|
||||
}
|
||||
|
@ -128,16 +128,21 @@ impl f64 {
|
||||
#[default(Endianness::Little)]
|
||||
endian: Endianness,
|
||||
) -> StrResult<f64> {
|
||||
// Convert slice to an array of length 8.
|
||||
let buf: [u8; 8] = match bytes.as_ref().try_into() {
|
||||
Ok(buffer) => buffer,
|
||||
Err(_) => bail!("bytes must have a length of exactly 8"),
|
||||
// Convert slice to an array of length 4 or 8.
|
||||
if let Ok(buffer) = <[u8; 8]>::try_from(bytes.as_ref()) {
|
||||
return Ok(match endian {
|
||||
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 {
|
||||
Endianness::Little => f64::from_le_bytes(buf),
|
||||
Endianness::Big => f64::from_be_bytes(buf),
|
||||
})
|
||||
bail!("bytes must have a length of 4 or 8");
|
||||
}
|
||||
|
||||
/// Converts a float to bytes.
|
||||
@ -153,13 +158,25 @@ impl f64 {
|
||||
#[named]
|
||||
#[default(Endianness::Little)]
|
||||
endian: Endianness,
|
||||
) -> Bytes {
|
||||
match endian {
|
||||
Endianness::Little => self.to_le_bytes(),
|
||||
Endianness::Big => self.to_be_bytes(),
|
||||
}
|
||||
.as_slice()
|
||||
.into()
|
||||
#[named]
|
||||
#[default(8)]
|
||||
size: u32,
|
||||
) -> StrResult<Bytes> {
|
||||
Ok(match size {
|
||||
8 => match endian {
|
||||
Endianness::Little => self.to_le_bytes(),
|
||||
Endianness::Big => self.to_be_bytes(),
|
||||
}
|
||||
.as_slice()
|
||||
.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"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,7 +443,7 @@ pub trait NativeFunc {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -462,6 +462,7 @@ pub struct NativeFuncData {
|
||||
pub keywords: &'static [&'static str],
|
||||
/// Whether this function makes use of context.
|
||||
pub contextual: bool,
|
||||
/// Definitions in the scope of the function.
|
||||
pub scope: LazyLock<Scope>,
|
||||
/// A list of parameter information for each parameter.
|
||||
pub params: LazyLock<Vec<ParamInfo>>,
|
||||
|
@ -11,7 +11,12 @@ use crate::foundations::{
|
||||
///
|
||||
/// The number can be negative, zero, or positive. As Typst uses 64 bits to
|
||||
/// 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
|
||||
/// starting it with a zero followed by either `x`, `o`, or `b`.
|
||||
|
@ -1,7 +1,7 @@
|
||||
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.
|
||||
///
|
||||
@ -45,17 +45,17 @@ use crate::foundations::{func, scope, ty, Repr};
|
||||
/// Currently, labels can only be attached to elements in markup mode, not in
|
||||
/// code mode. This might change in the future.
|
||||
#[ty(scope, cast)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Label(PicoStr);
|
||||
|
||||
impl Label {
|
||||
/// Creates a label from a string, interning it.
|
||||
pub fn new(name: impl Into<PicoStr>) -> Self {
|
||||
Self(name.into())
|
||||
/// Creates a label from an interned string.
|
||||
pub fn new(name: PicoStr) -> Self {
|
||||
Self(name)
|
||||
}
|
||||
|
||||
/// Resolves the label to a string.
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
pub fn resolve(self) -> ResolvedPicoStr {
|
||||
self.0.resolve()
|
||||
}
|
||||
|
||||
@ -71,15 +71,15 @@ impl Label {
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The name of the label.
|
||||
name: PicoStr,
|
||||
name: Str,
|
||||
) -> Label {
|
||||
Self(name)
|
||||
Self(PicoStr::intern(name.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Repr for Label {
|
||||
fn repr(&self) -> EcoString {
|
||||
eco_format!("<{}>", self.as_str())
|
||||
eco_format!("<{}>", self.resolve())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,8 @@ mod selector;
|
||||
mod str;
|
||||
mod styles;
|
||||
mod symbol;
|
||||
#[path = "target.rs"]
|
||||
mod target_;
|
||||
mod ty;
|
||||
mod value;
|
||||
mod version;
|
||||
@ -61,6 +63,7 @@ pub use self::selector::*;
|
||||
pub use self::str::*;
|
||||
pub use self::styles::*;
|
||||
pub use self::symbol::*;
|
||||
pub use self::target_::*;
|
||||
pub use self::ty::*;
|
||||
pub use self::value::*;
|
||||
pub use self::version::*;
|
||||
@ -79,6 +82,7 @@ use typst_syntax::Spanned;
|
||||
use crate::diag::{bail, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::routines::EvalMode;
|
||||
use crate::{Feature, Features};
|
||||
|
||||
/// Foundational types and functions.
|
||||
///
|
||||
@ -88,7 +92,7 @@ use crate::routines::EvalMode;
|
||||
pub static FOUNDATIONS: Category;
|
||||
|
||||
/// 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.define_type::<bool>();
|
||||
global.define_type::<i64>();
|
||||
@ -116,6 +120,9 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) {
|
||||
global.define_func::<assert>();
|
||||
global.define_func::<eval>();
|
||||
global.define_func::<style>();
|
||||
if features.is_enabled(Feature::Html) {
|
||||
global.define_func::<target>();
|
||||
}
|
||||
global.define_module(calc::module());
|
||||
global.define_module(sys::module(inputs));
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ use comemo::Tracked;
|
||||
use ecow::EcoString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typst_syntax::{Span, Spanned};
|
||||
use typst_utils::PicoStr;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
@ -647,7 +646,7 @@ impl Repr for str {
|
||||
'\0' => r.push_str(r"\u{0}"),
|
||||
'\'' => r.push('\''),
|
||||
'"' => r.push_str(r#"\""#),
|
||||
_ => c.escape_debug().for_each(|c| r.push(c)),
|
||||
_ => r.extend(c.escape_debug()),
|
||||
}
|
||||
}
|
||||
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 {
|
||||
type Output = Self;
|
||||
|
||||
@ -753,12 +758,6 @@ cast! {
|
||||
v: Str => v.into(),
|
||||
}
|
||||
|
||||
cast! {
|
||||
PicoStr,
|
||||
self => Value::Str(self.resolve().into()),
|
||||
v: Str => v.as_str().into(),
|
||||
}
|
||||
|
||||
cast! {
|
||||
String,
|
||||
self => Value::Str(self.into()),
|
||||
@ -784,7 +783,7 @@ cast! {
|
||||
.map_err(|_| "bytes are not valid utf-8")?
|
||||
.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: Str => Self::Str(v),
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
#[doc(inline)]
|
||||
pub use typst_macros::symbols;
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::{self, Debug, Display, Formatter, Write};
|
||||
@ -8,10 +5,10 @@ use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use serde::{Serialize, Serializer};
|
||||
use typst_syntax::{Span, Spanned};
|
||||
use typst_syntax::{is_ident, Span, Spanned};
|
||||
|
||||
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.
|
||||
///
|
||||
@ -46,73 +43,90 @@ use crate::foundations::{cast, func, scope, ty, Array, Func};
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
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.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
enum Repr {
|
||||
Single(SymChar),
|
||||
Const(&'static [(&'static str, SymChar)]),
|
||||
Multi(Arc<(List, EcoString)>),
|
||||
/// A native symbol that has no named variant.
|
||||
Single(char),
|
||||
/// 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.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
enum List {
|
||||
Static(&'static [(&'static str, SymChar)]),
|
||||
Runtime(Box<[(EcoString, SymChar)]>),
|
||||
Static(&'static [(&'static str, char)]),
|
||||
Runtime(Box<[(EcoString, char)]>),
|
||||
}
|
||||
|
||||
impl Symbol {
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Create a symbol with a static variant list.
|
||||
#[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());
|
||||
Self(Repr::Const(list))
|
||||
Self(Repr::Complex(list))
|
||||
}
|
||||
|
||||
/// Create a symbol with a runtime variant list.
|
||||
#[track_caller]
|
||||
pub fn runtime(list: Box<[(EcoString, SymChar)]>) -> Self {
|
||||
pub fn runtime(list: Box<[(EcoString, char)]>) -> Self {
|
||||
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 {
|
||||
self.sym().char()
|
||||
}
|
||||
|
||||
/// Resolve the symbol's `SymChar`.
|
||||
pub fn sym(&self) -> SymChar {
|
||||
match &self.0 {
|
||||
Repr::Single(c) => *c,
|
||||
Repr::Const(_) => find(self.variants(), "").unwrap(),
|
||||
Repr::Multi(arc) => find(self.variants(), &arc.1).unwrap(),
|
||||
Repr::Complex(_) => find(self.variants(), "").unwrap(),
|
||||
Repr::Modified(arc) => find(self.variants(), &arc.1).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get the function associated with the symbol, if any.
|
||||
pub fn func(&self) -> StrResult<Func> {
|
||||
self.sym()
|
||||
.func()
|
||||
.ok_or_else(|| eco_format!("symbol {self} is not callable"))
|
||||
match self.get() {
|
||||
'⌈' => Ok(crate::math::ceil::func()),
|
||||
'⌊' => 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.
|
||||
pub fn modified(mut self, modifier: &str) -> StrResult<Self> {
|
||||
if let Repr::Const(list) = self.0 {
|
||||
self.0 = Repr::Multi(Arc::new((List::Static(list), EcoString::new())));
|
||||
if let Repr::Complex(list) = self.0 {
|
||||
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);
|
||||
if !modifiers.is_empty() {
|
||||
modifiers.push('.');
|
||||
@ -127,11 +141,11 @@ impl 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 {
|
||||
Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
|
||||
Repr::Const(list) => Variants::Static(list.iter()),
|
||||
Repr::Multi(arc) => arc.0.variants(),
|
||||
Repr::Complex(list) => Variants::Static(list.iter()),
|
||||
Repr::Modified(arc) => arc.0.variants(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +153,7 @@ impl Symbol {
|
||||
pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ {
|
||||
let mut set = BTreeSet::new();
|
||||
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('.')) {
|
||||
@ -192,7 +206,14 @@ impl Symbol {
|
||||
if list.iter().any(|(prev, _)| &v.0 == prev) {
|
||||
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()))
|
||||
}
|
||||
@ -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 {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Single(c) => Debug::fmt(c, f),
|
||||
Self::Const(list) => list.fmt(f),
|
||||
Self::Multi(lists) => lists.fmt(f),
|
||||
Self::Complex(list) => list.fmt(f),
|
||||
Self::Modified(lists) => lists.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -286,20 +279,20 @@ cast! {
|
||||
let mut iter = array.into_iter();
|
||||
match (iter.next(), iter.next(), iter.next()) {
|
||||
(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.
|
||||
enum Variants<'a> {
|
||||
Single(std::option::IntoIter<SymChar>),
|
||||
Static(std::slice::Iter<'static, (&'static str, SymChar)>),
|
||||
Runtime(std::slice::Iter<'a, (EcoString, SymChar)>),
|
||||
Single(std::option::IntoIter<char>),
|
||||
Static(std::slice::Iter<'static, (&'static str, char)>),
|
||||
Runtime(std::slice::Iter<'a, (EcoString, char)>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Variants<'a> {
|
||||
type Item = (&'a str, SymChar);
|
||||
type Item = (&'a str, char);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
@ -312,9 +305,9 @@ impl<'a> Iterator for Variants<'a> {
|
||||
|
||||
/// Find the best symbol from the list.
|
||||
fn find<'a>(
|
||||
variants: impl Iterator<Item = (&'a str, SymChar)>,
|
||||
variants: impl Iterator<Item = (&'a str, char)>,
|
||||
modifiers: &str,
|
||||
) -> Option<SymChar> {
|
||||
) -> Option<char> {
|
||||
let mut best = None;
|
||||
let mut best_score = None;
|
||||
|
||||
|
38
crates/typst-library/src/foundations/target.rs
Normal file
38
crates/typst-library/src/foundations/target.rs
Normal 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()?))
|
||||
}
|
@ -199,6 +199,7 @@ pub trait NativeType {
|
||||
pub struct NativeTypeData {
|
||||
/// The type's normal name (e.g. `str`), as exposed to Typst.
|
||||
pub name: &'static str,
|
||||
/// The type's long name (e.g. `string`), for error messages.
|
||||
pub long_name: &'static str,
|
||||
/// The function's title case name (e.g. `String`).
|
||||
pub title: &'static str,
|
||||
@ -208,6 +209,7 @@ pub struct NativeTypeData {
|
||||
pub keywords: &'static [&'static str],
|
||||
/// The constructor for this type.
|
||||
pub constructor: LazyLock<Option<&'static NativeFuncData>>,
|
||||
/// Definitions in the scope of the type.
|
||||
pub scope: LazyLock<Scope>,
|
||||
}
|
||||
|
||||
|
625
crates/typst-library/src/html/dom.rs
Normal file
625
crates/typst-library/src/html/dom.rs
Normal 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
|
||||
}
|
||||
}
|
59
crates/typst-library/src/html/mod.rs
Normal file
59
crates/typst-library/src/html/mod.rs
Normal 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,
|
||||
}
|
@ -393,6 +393,11 @@ impl Counter {
|
||||
let context = Context::new(Some(location), styles);
|
||||
state.display(engine, context.track(), &numbering)
|
||||
}
|
||||
|
||||
/// Selects all state updates.
|
||||
pub fn select_any() -> Selector {
|
||||
CounterUpdateElem::elem().select()
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
|
@ -10,6 +10,7 @@ use typst_utils::NonZeroExt;
|
||||
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::foundations::{Content, Label, Repr, Selector};
|
||||
use crate::html::{HtmlElement, HtmlNode};
|
||||
use crate::introspection::{Location, Tag};
|
||||
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
|
||||
use crate::model::Numbering;
|
||||
@ -47,9 +48,15 @@ type Pair = (Content, Position);
|
||||
|
||||
impl Introspector {
|
||||
/// Creates an introspector for a page list.
|
||||
#[typst_macros::time(name = "introspect")]
|
||||
pub fn new(pages: &[Page]) -> Self {
|
||||
IntrospectorBuilder::new().build(pages)
|
||||
#[typst_macros::time(name = "introspect pages")]
|
||||
pub fn paged(pages: &[Page]) -> Self {
|
||||
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.
|
||||
@ -346,6 +353,7 @@ impl Clone for QueryCache {
|
||||
/// Builds the introspector.
|
||||
#[derive(Default)]
|
||||
struct IntrospectorBuilder {
|
||||
pages: usize,
|
||||
page_numberings: Vec<Option<Numbering>>,
|
||||
page_supplements: Vec<Content>,
|
||||
seen: HashSet<Location>,
|
||||
@ -361,46 +369,37 @@ impl IntrospectorBuilder {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Build the introspector.
|
||||
fn build(mut self, pages: &[Page]) -> Introspector {
|
||||
/// Build an introspector for a page list.
|
||||
fn build_paged(mut self, pages: &[Page]) -> Introspector {
|
||||
self.pages = pages.len();
|
||||
self.page_numberings.reserve(pages.len());
|
||||
self.page_supplements.reserve(pages.len());
|
||||
|
||||
// Discover all elements.
|
||||
let mut root = Vec::new();
|
||||
let mut elems = Vec::new();
|
||||
for (i, page) in pages.iter().enumerate() {
|
||||
self.page_numberings.push(page.numbering.clone());
|
||||
self.page_supplements.push(page.supplement.clone());
|
||||
self.discover(
|
||||
&mut root,
|
||||
self.discover_in_frame(
|
||||
&mut elems,
|
||||
&page.frame,
|
||||
NonZeroUsize::new(1 + i).unwrap(),
|
||||
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 {
|
||||
pages: pages.len(),
|
||||
page_numberings: self.page_numberings,
|
||||
page_supplements: self.page_supplements,
|
||||
elems,
|
||||
keys: self.keys,
|
||||
locations: self.locations,
|
||||
labels: self.labels,
|
||||
queries: QueryCache::default(),
|
||||
}
|
||||
/// Build an introspector for an HTML document.
|
||||
fn build_html(mut self, root: &HtmlElement) -> Introspector {
|
||||
let mut elems = Vec::new();
|
||||
self.discover_in_html(&mut elems, root);
|
||||
self.finalize(elems)
|
||||
}
|
||||
|
||||
/// Processes the tags in the frame.
|
||||
fn discover(
|
||||
fn discover_in_frame(
|
||||
&mut self,
|
||||
sink: &mut Vec<Pair>,
|
||||
frame: &Frame,
|
||||
@ -416,27 +415,83 @@ impl IntrospectorBuilder {
|
||||
|
||||
if let Some(parent) = group.parent {
|
||||
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);
|
||||
} else {
|
||||
self.discover(sink, &group.frame, page, ts);
|
||||
self.discover_in_frame(sink, &group.frame, page, ts);
|
||||
}
|
||||
}
|
||||
FrameItem::Tag(Tag::Start(elem)) => {
|
||||
let loc = elem.location().unwrap();
|
||||
if self.seen.insert(loc) {
|
||||
let point = pos.transform(ts);
|
||||
sink.push((elem.clone(), Position { page, point }));
|
||||
}
|
||||
}
|
||||
FrameItem::Tag(Tag::End(loc, key)) => {
|
||||
self.keys.insert(*key, *loc);
|
||||
FrameItem::Tag(tag) => {
|
||||
self.discover_in_tag(
|
||||
sink,
|
||||
tag,
|
||||
Position { page, point: pos.transform(ts) },
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// acceleration structures.
|
||||
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {
|
||||
|
@ -203,6 +203,7 @@ impl<'a> Locator<'a> {
|
||||
}
|
||||
|
||||
#[comemo::track]
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
impl<'a> Locator<'a> {
|
||||
/// Resolves the locator based on its local and the outer information.
|
||||
fn resolve(&self) -> Resolved {
|
||||
|
@ -261,6 +261,11 @@ impl State {
|
||||
fn selector(&self) -> Selector {
|
||||
select_where!(StateUpdateElem, Key => self.key.clone())
|
||||
}
|
||||
|
||||
/// Selects all state updates.
|
||||
pub fn select_any() -> Selector {
|
||||
StateUpdateElem::elem().select()
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -280,6 +285,7 @@ impl State {
|
||||
/// Retrieves the value of the state at the current location.
|
||||
///
|
||||
/// This is equivalent to `{state.at(here())}`.
|
||||
#[typst_macros::time(name = "state.get", span = span)]
|
||||
#[func(contextual)]
|
||||
pub fn get(
|
||||
&self,
|
||||
@ -303,6 +309,7 @@ impl State {
|
||||
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
|
||||
/// function also works without a known context if the `selector` is a
|
||||
/// location. This behaviour will be removed in a future release.
|
||||
#[typst_macros::time(name = "state.at", span = span)]
|
||||
#[func(contextual)]
|
||||
pub fn at(
|
||||
&self,
|
||||
|
@ -520,8 +520,7 @@ pub enum FrameItem {
|
||||
Image(Image, Size, Span),
|
||||
/// An internal or external link to a destination.
|
||||
Link(Destination, Size),
|
||||
/// An introspectable element that produced something within this frame
|
||||
/// alongside its key.
|
||||
/// An introspectable element that produced something within this frame.
|
||||
Tag(Tag),
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,12 @@ use crate::foundations::{
|
||||
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
|
||||
NativeElement, Set, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::introspection::Introspector;
|
||||
use crate::layout::{
|
||||
Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel,
|
||||
Sides, SpecificAlignment,
|
||||
};
|
||||
use crate::model::Numbering;
|
||||
use crate::model::{DocumentInfo, Numbering};
|
||||
use crate::text::LocalName;
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page {
|
||||
@ -942,3 +954,14 @@ papers! {
|
||||
(PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9")
|
||||
(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>();
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ extern crate self as typst_library;
|
||||
pub mod diag;
|
||||
pub mod engine;
|
||||
pub mod foundations;
|
||||
pub mod html;
|
||||
pub mod introspection;
|
||||
pub mod layout;
|
||||
pub mod loading;
|
||||
@ -193,7 +194,7 @@ impl LibraryBuilder {
|
||||
pub fn build(self) -> Library {
|
||||
let math = math::module();
|
||||
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());
|
||||
Library {
|
||||
global,
|
||||
@ -231,12 +232,14 @@ impl FromIterator<Feature> for Features {
|
||||
/// An in-development feature that should be enabled.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Feature {}
|
||||
pub enum Feature {
|
||||
Html,
|
||||
}
|
||||
|
||||
/// 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();
|
||||
self::foundations::define(&mut global, inputs);
|
||||
self::foundations::define(&mut global, inputs, features);
|
||||
self::model::define(&mut global);
|
||||
self::text::define(&mut global);
|
||||
global.reset_category();
|
||||
@ -246,6 +249,10 @@ fn global(math: Module, inputs: Dict) -> Module {
|
||||
self::introspection::define(&mut global);
|
||||
self::loading::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);
|
||||
Module::new("global", global)
|
||||
}
|
||||
|
@ -207,9 +207,7 @@ pub fn module() -> Module {
|
||||
math.define("wide", HElem::new(WIDE.into()).pack());
|
||||
|
||||
// Symbols.
|
||||
for (name, symbol) in crate::symbols::SYM {
|
||||
math.define(*name, symbol.clone());
|
||||
}
|
||||
crate::symbols::define_math(&mut math);
|
||||
|
||||
Module::new("math", math)
|
||||
}
|
||||
|
@ -179,16 +179,13 @@ impl BibliographyElem {
|
||||
}
|
||||
|
||||
/// Find all bibliography keys.
|
||||
pub fn keys(
|
||||
introspector: Tracked<Introspector>,
|
||||
) -> Vec<(EcoString, Option<EcoString>)> {
|
||||
pub fn keys(introspector: Tracked<Introspector>) -> Vec<(Label, Option<EcoString>)> {
|
||||
let mut vec = vec![];
|
||||
for elem in introspector.query(&Self::elem().select()).iter() {
|
||||
let this = elem.to_packed::<Self>().unwrap();
|
||||
for entry in this.bibliography().entries() {
|
||||
let key = entry.key().into();
|
||||
for (key, entry) in this.bibliography().iter() {
|
||||
let detail = entry.title().map(|title| title.value.to_str().into());
|
||||
vec.push((key, detail))
|
||||
vec.push((Label::new(key), detail))
|
||||
}
|
||||
}
|
||||
vec
|
||||
@ -341,7 +338,7 @@ impl Bibliography {
|
||||
};
|
||||
|
||||
for entry in library {
|
||||
match map.entry(entry.key().into()) {
|
||||
match map.entry(PicoStr::intern(entry.key())) {
|
||||
indexmap::map::Entry::Vacant(vacant) => {
|
||||
vacant.insert(entry);
|
||||
}
|
||||
@ -366,8 +363,8 @@ impl Bibliography {
|
||||
self.map.contains_key(&key.into())
|
||||
}
|
||||
|
||||
fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> {
|
||||
self.map.values()
|
||||
fn iter(&self) -> impl Iterator<Item = (PicoStr, &hayagriva::Entry)> {
|
||||
self.map.iter().map(|(&k, v)| (k, v))
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,7 +658,7 @@ impl<'a> Generator<'a> {
|
||||
errors.push(error!(
|
||||
child.span(),
|
||||
"key `{}` does not exist in the bibliography",
|
||||
key.as_str()
|
||||
key.resolve()
|
||||
));
|
||||
continue;
|
||||
};
|
||||
@ -775,7 +772,9 @@ impl<'a> Generator<'a> {
|
||||
let mut output = std::mem::take(&mut self.failures);
|
||||
for (info, citation) in self.infos.iter().zip(&rendered.citations) {
|
||||
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 {
|
||||
routines: self.routines,
|
||||
@ -820,7 +819,7 @@ impl<'a> Generator<'a> {
|
||||
let mut first_occurrences = HashMap::new();
|
||||
for info in &self.infos {
|
||||
for subinfo in &info.subinfos {
|
||||
let key = subinfo.key.as_str();
|
||||
let key = subinfo.key.resolve();
|
||||
first_occurrences.entry(key).or_insert(info.location);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ use crate::foundations::{
|
||||
cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain,
|
||||
Styles, Value,
|
||||
};
|
||||
use crate::introspection::Introspector;
|
||||
use crate::layout::Page;
|
||||
|
||||
/// The root element of a document and its metadata.
|
||||
///
|
||||
@ -39,6 +37,10 @@ pub struct DocumentElem {
|
||||
#[ghost]
|
||||
pub author: Author,
|
||||
|
||||
/// The document's description.
|
||||
#[ghost]
|
||||
pub description: Option<Content>,
|
||||
|
||||
/// The document's keywords.
|
||||
#[ghost]
|
||||
pub keywords: Keywords,
|
||||
@ -86,17 +88,6 @@ cast! {
|
||||
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.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
||||
pub struct DocumentInfo {
|
||||
@ -104,6 +95,8 @@ pub struct DocumentInfo {
|
||||
pub title: Option<EcoString>,
|
||||
/// The document's author.
|
||||
pub author: Vec<EcoString>,
|
||||
/// The document's description.
|
||||
pub description: Option<EcoString>,
|
||||
/// The document's keywords.
|
||||
pub keywords: Vec<EcoString>,
|
||||
/// The document's creation date.
|
||||
@ -124,6 +117,10 @@ impl DocumentInfo {
|
||||
if has(<DocumentElem as Fields>::Enum::Author) {
|
||||
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) {
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
use crate::diag::SourceResult;
|
||||
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};
|
||||
|
||||
/// Emphasizes content by toggling italics.
|
||||
@ -35,7 +38,15 @@ pub struct EmphElem {
|
||||
|
||||
impl Show for Packed<EmphElem> {
|
||||
#[typst_macros::time(name = "emph", span = self.span())]
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.body().clone().styled(TextElem::set_emph(ItalicToggle(true))))
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use ecow::eco_format;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
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::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
|
||||
|
||||
@ -214,6 +216,19 @@ impl EnumElem {
|
||||
|
||||
impl Show for Packed<EnumElem> {
|
||||
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 =
|
||||
BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum)
|
||||
.pack()
|
||||
|
@ -9,8 +9,9 @@ use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
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::{
|
||||
Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
|
||||
};
|
||||
@ -257,7 +258,7 @@ impl Synthesize for Packed<FigureElem> {
|
||||
// Determine the figure's kind.
|
||||
let kind = elem.kind(styles).unwrap_or_else(|| {
|
||||
elem.body()
|
||||
.query_first(Selector::can::<dyn Figurable>())
|
||||
.query_first(&Selector::can::<dyn Figurable>())
|
||||
.map(|elem| FigureKind::Elem(elem.func()))
|
||||
.unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
|
||||
});
|
||||
@ -289,7 +290,7 @@ impl Synthesize for Packed<FigureElem> {
|
||||
let descendant = match kind {
|
||||
FigureKind::Elem(func) => elem
|
||||
.body()
|
||||
.query_first(Selector::Elem(func, None))
|
||||
.query_first(&Selector::Elem(func, None))
|
||||
.map(Cow::Owned),
|
||||
FigureKind::Name(_) => None,
|
||||
};
|
||||
@ -307,6 +308,7 @@ impl Synthesize for Packed<FigureElem> {
|
||||
// Fill the figure's caption.
|
||||
let mut caption = elem.caption(styles);
|
||||
if let Some(caption) = &mut caption {
|
||||
caption.synthesize(engine, styles)?;
|
||||
caption.push_kind(kind.clone());
|
||||
caption.push_supplement(supplement.clone());
|
||||
caption.push_numbering(numbering.clone());
|
||||
@ -326,15 +328,30 @@ impl Synthesize for Packed<FigureElem> {
|
||||
impl Show for Packed<FigureElem> {
|
||||
#[typst_macros::time(name = "figure", span = self.span())]
|
||||
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.
|
||||
if let Some(caption) = self.caption(styles) {
|
||||
let v = VElem::new(self.gap(styles).into()).with_weak(true).pack();
|
||||
realized = match caption.position(styles) {
|
||||
OuterVAlignment::Top => caption.pack() + v + realized,
|
||||
OuterVAlignment::Bottom => realized + v + caption.pack(),
|
||||
let (first, second) = match caption.position(styles) {
|
||||
OuterVAlignment::Top => (caption.pack(), realized),
|
||||
OuterVAlignment::Bottom => (realized, 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.
|
||||
@ -607,6 +624,13 @@ impl Show for Packed<FigureCaption> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ cast! {
|
||||
/// before any page content, typically at the very start of the document.
|
||||
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
|
||||
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.
|
||||
///
|
||||
/// ```example
|
||||
|
@ -6,8 +6,9 @@ use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
|
||||
Styles, Synthesize,
|
||||
Styles, Synthesize, TargetElem,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::introspection::{
|
||||
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
|
||||
};
|
||||
@ -216,6 +217,8 @@ impl Synthesize for Packed<HeadingElem> {
|
||||
impl Show for Packed<HeadingElem> {
|
||||
#[typst_macros::time(name = "heading", span = self.span())]
|
||||
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);
|
||||
|
||||
let span = self.span();
|
||||
@ -233,7 +236,7 @@ impl Show for Packed<HeadingElem> {
|
||||
.display_at_loc(engine, location, styles, numbering)?
|
||||
.spanned(span);
|
||||
|
||||
if hanging_indent.is_auto() {
|
||||
if hanging_indent.is_auto() && !html {
|
||||
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
realized = numbering
|
||||
+ HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
|
||||
+ realized;
|
||||
let spacing = if html {
|
||||
SpaceElem::shared().clone()
|
||||
} 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()));
|
||||
}
|
||||
|
||||
Ok(BlockElem::new()
|
||||
.with_body(Some(BlockBody::Content(realized)))
|
||||
.pack()
|
||||
.spanned(span))
|
||||
Ok(if html {
|
||||
// HTML's h1 is closer to a title element. There should only be one.
|
||||
// Meanwhile, a level 1 Typst heading is a section heading. For this
|
||||
// reason, levels are offset by one: A Typst level 1 heading becomes
|
||||
// a `<h2>`.
|
||||
let level = 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,13 @@ use std::ops::Deref;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
use crate::diag::{bail, warning, At, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
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::layout::Position;
|
||||
use crate::text::{Hyphenate, TextElem};
|
||||
@ -99,18 +101,36 @@ impl LinkElem {
|
||||
|
||||
impl Show for Packed<LinkElem> {
|
||||
#[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 linked = match self.dest() {
|
||||
LinkTarget::Dest(dest) => body.linked(dest.clone()),
|
||||
LinkTarget::Label(label) => {
|
||||
let elem = engine.introspector.query_label(*label).at(self.span())?;
|
||||
let dest = Destination::Location(elem.location().unwrap());
|
||||
body.clone().linked(dest)
|
||||
}
|
||||
};
|
||||
let dest = self.dest();
|
||||
|
||||
Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))))
|
||||
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() {
|
||||
LinkTarget::Dest(dest) => body.linked(dest.clone()),
|
||||
LinkTarget::Label(label) => {
|
||||
let elem = engine.introspector.query_label(*label).at(self.span())?;
|
||||
let dest = Destination::Location(elem.location().unwrap());
|
||||
body.clone().linked(dest)
|
||||
}
|
||||
};
|
||||
|
||||
linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,9 @@ use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
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::model::ParElem;
|
||||
use crate::text::TextElem;
|
||||
@ -140,6 +141,18 @@ impl ListElem {
|
||||
|
||||
impl Show for Packed<ListElem> {
|
||||
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 =
|
||||
BlockElem::multi_layouter(self.clone(), engine.routines.layout_list)
|
||||
.pack()
|
||||
|
@ -60,8 +60,9 @@ pub fn numbering(
|
||||
/// Defines how the numbering works.
|
||||
///
|
||||
/// **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
|
||||
/// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six
|
||||
|
@ -248,7 +248,7 @@ impl Show for Packed<OutlineElem> {
|
||||
)?;
|
||||
|
||||
// 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());
|
||||
|
||||
ancestors.push(elem);
|
||||
@ -332,15 +332,18 @@ impl OutlineIndent {
|
||||
}
|
||||
|
||||
if !ancestors.is_empty() {
|
||||
seq.push(HideElem::new(hidden).pack());
|
||||
seq.push(SpaceElem::shared().clone());
|
||||
seq.push(HideElem::new(hidden).pack().spanned(span));
|
||||
seq.push(SpaceElem::shared().clone().spanned(span));
|
||||
}
|
||||
}
|
||||
|
||||
// Length => indent with some fixed spacing per level
|
||||
Some(Smart::Custom(OutlineIndent::Rel(length))) => {
|
||||
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());
|
||||
} else {
|
||||
seq.push(HElem::new(Fr::one().into()).pack());
|
||||
seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span()));
|
||||
}
|
||||
|
||||
// Add the page number.
|
||||
|
@ -1,6 +1,9 @@
|
||||
use crate::diag::SourceResult;
|
||||
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};
|
||||
|
||||
/// Strongly emphasizes content by increasing the font weight.
|
||||
@ -40,9 +43,14 @@ pub struct StrongElem {
|
||||
impl Show for Packed<StrongElem> {
|
||||
#[typst_macros::time(name = "strong", span = self.span())]
|
||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self
|
||||
.body()
|
||||
.clone()
|
||||
.styled(TextElem::set_delta(WeightDelta(self.delta(styles)))))
|
||||
let body = self.body.clone();
|
||||
Ok(if TargetElem::target_in(styles).is_html() {
|
||||
HtmlElem::new(tag::strong)
|
||||
.with_body(Some(body))
|
||||
.pack()
|
||||
.spanned(self.span())
|
||||
} else {
|
||||
body.styled(TextElem::set_delta(WeightDelta(self.delta(styles))))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
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::model::{ListItemLike, ListLike, ParElem};
|
||||
use crate::text::TextElem;
|
||||
@ -114,6 +115,26 @@ impl TermsElem {
|
||||
|
||||
impl Show for Packed<TermsElem> {
|
||||
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 indent = self.indent(styles);
|
||||
let hanging_indent = self.hanging_indent(styles);
|
||||
@ -127,7 +148,7 @@ impl Show for Packed<TermsElem> {
|
||||
|
||||
let pad = hanging_indent + indent;
|
||||
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![];
|
||||
for child in self.children().iter() {
|
||||
@ -149,12 +170,16 @@ impl Show for Packed<TermsElem> {
|
||||
let mut realized = StackElem::new(children)
|
||||
.with_spacing(Some(gutter.into()))
|
||||
.pack()
|
||||
.spanned(span)
|
||||
.padded(padding);
|
||||
|
||||
if self.tight(styles) {
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let spacing =
|
||||
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();
|
||||
let spacing = VElem::new(leading.into())
|
||||
.with_weak(true)
|
||||
.with_attach(true)
|
||||
.pack()
|
||||
.spanned(span);
|
||||
realized = spacing + realized;
|
||||
}
|
||||
|
||||
|
@ -16,10 +16,11 @@ use crate::foundations::{
|
||||
use crate::introspection::{Introspector, Locator, SplitLocator};
|
||||
use crate::layout::{
|
||||
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::model::{Document, DocumentInfo, EnumElem, ListElem, TableElem};
|
||||
use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem};
|
||||
use crate::visualize::{
|
||||
CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
|
||||
SquareElem,
|
||||
@ -85,13 +86,6 @@ routines! {
|
||||
styles: StyleChain<'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.
|
||||
fn layout_fragment(
|
||||
engine: &mut Engine,
|
||||
@ -342,11 +336,16 @@ pub enum EvalMode {
|
||||
|
||||
/// Defines what kind of realization we are performing.
|
||||
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.
|
||||
Root(&'a mut DocumentInfo),
|
||||
LayoutDocument(&'a mut DocumentInfo),
|
||||
/// 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.
|
||||
Math,
|
||||
}
|
||||
|
49
crates/typst-library/src/symbols.rs
Normal file
49
crates/typst-library/src/symbols.rs
Normal 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
@ -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
@ -182,8 +182,7 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! {
|
||||
"NewCM10-Regular" => Exception::new()
|
||||
.family("New Computer Modern"),
|
||||
"NewCMMath-Bold" => Exception::new()
|
||||
.family("New Computer Modern Math")
|
||||
.weight(700),
|
||||
.family("New Computer Modern Math"),
|
||||
"NewCMMath-Book" => Exception::new()
|
||||
.family("New Computer Modern Math")
|
||||
.weight(450),
|
||||
|
@ -14,8 +14,9 @@ macro_rules! translation {
|
||||
};
|
||||
}
|
||||
|
||||
const TRANSLATIONS: [(&str, &str); 36] = [
|
||||
const TRANSLATIONS: [(&str, &str); 38] = [
|
||||
translation!("ar"),
|
||||
translation!("bg"),
|
||||
translation!("ca"),
|
||||
translation!("cs"),
|
||||
translation!("da"),
|
||||
@ -23,6 +24,7 @@ const TRANSLATIONS: [(&str, &str); 36] = [
|
||||
translation!("en"),
|
||||
translation!("es"),
|
||||
translation!("et"),
|
||||
translation!("eu"),
|
||||
translation!("fi"),
|
||||
translation!("fr"),
|
||||
translation!("gl"),
|
||||
@ -60,7 +62,9 @@ pub struct Lang([u8; 3], u8);
|
||||
impl Lang {
|
||||
pub const ALBANIAN: Self = Self(*b"sq ", 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 BULGARIAN: Self = Self(*b"bg ", 2);
|
||||
pub const CATALAN: Self = Self(*b"ca ", 2);
|
||||
pub const CHINESE: Self = Self(*b"zh ", 2);
|
||||
pub const CROATIAN: Self = Self(*b"hr ", 2);
|
||||
|
@ -495,7 +495,9 @@ pub struct TextElem {
|
||||
///
|
||||
/// Hyphenation is generally avoided by placing the whole word on the next
|
||||
/// 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
|
||||
/// higher runt cost can result in more awkward in justification spacing.
|
||||
|
@ -14,8 +14,9 @@ use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
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::model::{Figurable, ParElem};
|
||||
use crate::text::{
|
||||
@ -451,6 +452,14 @@ impl Show for Packed<RawElem> {
|
||||
}
|
||||
|
||||
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) {
|
||||
// Align the text before inserting it into the block.
|
||||
realized = realized.aligned(self.align(styles).into());
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user