Merge branch 'upstream-main' into krilla-port

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

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }}
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

View File

@ -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
View File

@ -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",

View File

@ -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"

View File

@ -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"]

View File

@ -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())
}

View File

@ -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())

View File

@ -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}");

View File

@ -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

View File

@ -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),

View File

@ -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(),

View File

@ -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,

View File

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

View File

@ -22,8 +22,8 @@ impl Timer {
/// record timings for a specific function invocation.
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,
};

View File

@ -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())?;

View File

@ -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 {

View File

@ -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"]);
}
}

View File

@ -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);
}

View File

@ -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)
}

View File

@ -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),
),
};

View File

@ -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(

View File

@ -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())))
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
use comemo::Track;
use 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)
}

View File

@ -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)

View File

@ -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()));

View File

@ -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> {

View File

@ -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 },

View File

@ -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]

View File

@ -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,

View File

@ -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()
));
}
}

View File

@ -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, &regions, false)?;
self.float(placed, &regions, 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.

View File

@ -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));
}

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

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

View File

@ -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));
}
}
}
}

View File

@ -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
}

View File

@ -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(),

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

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

View File

@ -150,8 +150,8 @@ fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
auto_italic
&& 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,

View File

@ -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]));

View File

@ -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) {

View File

@ -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.

View File

@ -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,

View File

@ -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 }

View File

@ -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 {

View File

@ -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) {

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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)),
}

View File

@ -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"),
})
}
}

View File

@ -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>>,

View File

@ -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`.

View File

@ -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())
}
}

View File

@ -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));
}

View File

@ -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),
}

View File

@ -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;

View File

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

View File

@ -199,6 +199,7 @@ pub trait NativeType {
pub struct NativeTypeData {
/// 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>,
}

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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 {

View File

@ -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,

View File

@ -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),
}

View File

@ -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>();
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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);
}
}

View File

@ -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>();
}
}

View File

@ -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)))
})
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
})
}
}

View File

@ -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))))
})
}
}

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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))))
})
}
}

View File

@ -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;
}

View File

@ -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,
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -182,8 +182,7 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! {
"NewCM10-Regular" => Exception::new()
.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),

View File

@ -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);

View File

@ -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.

View File

@ -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