Compare commits

...

22 Commits

Author SHA1 Message Date
Laurenz
8dce676dcd Version bump 2025-02-19 11:13:25 +01:00
Laurenz
c02cb70f27 Update changelog (#5894) 2025-02-19 11:07:46 +01:00
Matthew Toohey
0a534f2c0e --make-deps fixes (#5873) 2025-02-18 19:19:16 +01:00
ᡥᠠᡳᡤᡳᠶᠠ ᡥᠠᠯᠠ·ᠨᡝᡴᠣ 猫
de16a2ced1 HTML export: Use <code> for inline RawElem (#5884) 2025-02-18 11:25:46 +01:00
Laurenz
d48708c5d5 More robust SVG auto-detection (#5878) 2025-02-17 12:49:15 +01:00
Laurenz
e294fe85a5
Bring back type/str compatibility for 0.13, with warnings and hints (#5877) 2025-02-17 11:52:11 +01:00
Laurenz
2f1a5ab914 Remove Linux Libertine warning (#5876) 2025-02-16 15:13:42 +01:00
Ana Gelez
c247dbc42d Lazy parsing of the package index (#5851) 2025-02-12 17:12:01 +01:00
+merlan #flirora
c259545c6e Gradient::repeat: Fix floating-point error in stop calculation (#5837) 2025-02-12 13:53:05 +01:00
+merlan #flirora
e470ccff19 Update documentation for float.{to-bits, from-bits} (#5836) 2025-02-12 13:53:05 +01:00
Laurenz
024bbb2b46 Fix autocomplete and jumps in math (#5849) 2025-02-11 12:09:33 +01:00
Laurenz
93fe02b457 Bump typst-assets 2025-02-10 16:36:30 +01:00
Laurenz
9c3ecf43a0 Respect par constructor arguments (#5842) 2025-02-10 16:28:49 +01:00
TwoF1nger
ab5e356d81 Add smart quotes for Bulgarian (#5807) 2025-02-10 16:28:49 +01:00
Malo
88f88016e0 Add warning for pdf.embed elem used with HTML (#5829) 2025-02-10 16:28:49 +01:00
PgBiel
72060d0142 Don't crash on image with zero DPI (#5835) 2025-02-10 16:28:48 +01:00
Laurenz
20dd19c64e Fix unnecessary import rename warning (#5828) 2025-02-06 22:16:07 +01:00
Laurenz
f64d029fe6 Document removals in changelog (#5827) 2025-02-06 22:16:07 +01:00
Laurenz
c417b17442 Fix docs outline for nested definitions (#5823) 2025-02-06 11:24:19 +01:00
Malo
c2316b9a3e Documentation fixes and improvements (#5816) 2025-02-06 11:24:19 +01:00
Laurenz
d8b79b5b9b Autocomplete content methods (#5822) 2025-02-06 11:24:19 +01:00
Laurenz
56d8188c61 Release Candidate 1 2025-02-05 15:49:19 +01:00
62 changed files with 1009 additions and 488 deletions

50
Cargo.lock generated
View File

@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]] [[package]]
name = "typst" name = "typst"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2752,12 +2752,13 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.12.0" version = "0.13.0"
source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1051c56bbbf74d31ea6c6b1661e62fa0ebb8104403ee53f6dcd321600426e0b6"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -2802,12 +2803,12 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.12.0" version = "0.13.0"
source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c" source = "git+https://github.com/typst/typst-dev-assets?tag=v0.13.0#61aebe9575a5abff889f76d73c7b01dc8e17e340"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"clap", "clap",
"ecow", "ecow",
@ -2830,7 +2831,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-eval" name = "typst-eval"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2848,7 +2849,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-fuzz" name = "typst-fuzz"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"comemo", "comemo",
"libfuzzer-sys", "libfuzzer-sys",
@ -2860,7 +2861,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-html" name = "typst-html"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2874,7 +2875,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-ide" name = "typst-ide"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2891,7 +2892,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-kit" name = "typst-kit"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"dirs", "dirs",
"ecow", "ecow",
@ -2901,6 +2902,8 @@ dependencies = [
"native-tls", "native-tls",
"once_cell", "once_cell",
"openssl", "openssl",
"serde",
"serde_json",
"tar", "tar",
"typst-assets", "typst-assets",
"typst-library", "typst-library",
@ -2912,7 +2915,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-layout" name = "typst-layout"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"az", "az",
"bumpalo", "bumpalo",
@ -2942,7 +2945,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-library" name = "typst-library"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"az", "az",
"bitflags 2.8.0", "bitflags 2.8.0",
@ -2964,6 +2967,7 @@ dependencies = [
"kamadak-exif", "kamadak-exif",
"kurbo", "kurbo",
"lipsum", "lipsum",
"memchr",
"palette", "palette",
"phf", "phf",
"png", "png",
@ -3001,7 +3005,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-macros" name = "typst-macros"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -3011,7 +3015,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-pdf" name = "typst-pdf"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"base64", "base64",
@ -3037,7 +3041,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-realize" name = "typst-realize"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bumpalo", "bumpalo",
@ -3053,7 +3057,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-render" name = "typst-render"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"comemo", "comemo",
@ -3069,7 +3073,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-svg" name = "typst-svg"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"base64", "base64",
"comemo", "comemo",
@ -3087,7 +3091,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-syntax" name = "typst-syntax"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"ecow", "ecow",
"serde", "serde",
@ -3103,7 +3107,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-tests" name = "typst-tests"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"clap", "clap",
"comemo", "comemo",
@ -3128,7 +3132,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-timing" name = "typst-timing"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
@ -3138,7 +3142,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-utils" name = "typst-utils"
version = "0.12.0" version = "0.13.0"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"portable-atomic", "portable-atomic",

View File

@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.12.0" version = "0.13.0"
rust-version = "1.80" # also change in ci.yml rust-version = "1.80" # also change in ci.yml
authors = ["The Typst Project Developers"] authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
@ -16,24 +16,24 @@ keywords = ["typst"]
readme = "README.md" readme = "README.md"
[workspace.dependencies] [workspace.dependencies]
typst = { path = "crates/typst", version = "0.12.0" } typst = { path = "crates/typst", version = "0.13.0" }
typst-cli = { path = "crates/typst-cli", version = "0.12.0" } typst-cli = { path = "crates/typst-cli", version = "0.13.0" }
typst-eval = { path = "crates/typst-eval", version = "0.12.0" } typst-eval = { path = "crates/typst-eval", version = "0.13.0" }
typst-html = { path = "crates/typst-html", version = "0.12.0" } typst-html = { path = "crates/typst-html", version = "0.13.0" }
typst-ide = { path = "crates/typst-ide", version = "0.12.0" } typst-ide = { path = "crates/typst-ide", version = "0.13.0" }
typst-kit = { path = "crates/typst-kit", version = "0.12.0" } typst-kit = { path = "crates/typst-kit", version = "0.13.0" }
typst-layout = { path = "crates/typst-layout", version = "0.12.0" } typst-layout = { path = "crates/typst-layout", version = "0.13.0" }
typst-library = { path = "crates/typst-library", version = "0.12.0" } typst-library = { path = "crates/typst-library", version = "0.13.0" }
typst-macros = { path = "crates/typst-macros", version = "0.12.0" } typst-macros = { path = "crates/typst-macros", version = "0.13.0" }
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" } typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" }
typst-realize = { path = "crates/typst-realize", version = "0.12.0" } typst-realize = { path = "crates/typst-realize", version = "0.13.0" }
typst-render = { path = "crates/typst-render", version = "0.12.0" } typst-render = { path = "crates/typst-render", version = "0.13.0" }
typst-svg = { path = "crates/typst-svg", version = "0.12.0" } typst-svg = { path = "crates/typst-svg", version = "0.13.0" }
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" }
typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.13.0" }
typst-utils = { path = "crates/typst-utils", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.13.0" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } typst-assets = "0.13.0"
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.0" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -73,6 +73,7 @@ kamadak-exif = "0.6"
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"
memchr = "2"
miniz_oxide = "0.8" miniz_oxide = "0.8"
native-tls = "0.2" native-tls = "0.2"
notify = "8" notify = "8"

View File

@ -6,8 +6,9 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono::{DateTime, Datelike, Timelike, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term; use codespan_reporting::term;
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use parking_lot::RwLock; use parking_lot::RwLock;
use pathdiff::diff_paths;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
@ -188,7 +189,7 @@ pub fn compile_once(
match output { match output {
// Export the PDF / PNG. // Export the PDF / PNG.
Ok(()) => { Ok(outputs) => {
let duration = start.elapsed(); let duration = start.elapsed();
if config.watching { if config.watching {
@ -202,7 +203,7 @@ pub fn compile_once(
print_diagnostics(world, &[], &warnings, config.diagnostic_format) print_diagnostics(world, &[], &warnings, config.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
write_make_deps(world, config)?; write_make_deps(world, config, outputs)?;
open_output(config)?; open_output(config)?;
} }
@ -226,12 +227,15 @@ pub fn compile_once(
fn compile_and_export( fn compile_and_export(
world: &mut SystemWorld, world: &mut SystemWorld,
config: &mut CompileConfig, config: &mut CompileConfig,
) -> Warned<SourceResult<()>> { ) -> Warned<SourceResult<Vec<Output>>> {
match config.output_format { match config.output_format {
OutputFormat::Html => { OutputFormat::Html => {
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world); let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
let result = output.and_then(|document| export_html(&document, config)); let result = output.and_then(|document| export_html(&document, config));
Warned { output: result, warnings } Warned {
output: result.map(|()| vec![config.output.clone()]),
warnings,
}
} }
_ => { _ => {
let Warned { output, warnings } = typst::compile::<PagedDocument>(world); let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
@ -257,9 +261,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<
} }
/// Export to a paged target format. /// Export to a paged target format.
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { fn export_paged(
document: &PagedDocument,
config: &CompileConfig,
) -> SourceResult<Vec<Output>> {
match config.output_format { match config.output_format {
OutputFormat::Pdf => export_pdf(document, config), OutputFormat::Pdf => {
export_pdf(document, config).map(|()| vec![config.output.clone()])
}
OutputFormat::Png => { OutputFormat::Png => {
export_image(document, config, ImageExportFormat::Png).at(Span::detached()) export_image(document, config, ImageExportFormat::Png).at(Span::detached())
} }
@ -327,7 +336,7 @@ fn export_image(
document: &PagedDocument, document: &PagedDocument,
config: &CompileConfig, config: &CompileConfig,
fmt: ImageExportFormat, fmt: ImageExportFormat,
) -> StrResult<()> { ) -> StrResult<Vec<Output>> {
// Determine whether we have indexable templates in output // Determine whether we have indexable templates in output
let can_handle_multiple = match config.output { let can_handle_multiple = match config.output {
Output::Stdout => false, Output::Stdout => false,
@ -383,7 +392,7 @@ fn export_image(
&& config.export_cache.is_cached(*i, &page.frame) && config.export_cache.is_cached(*i, &page.frame)
&& path.exists() && path.exists()
{ {
return Ok(()); return Ok(Output::Path(path.to_path_buf()));
} }
Output::Path(path.to_owned()) Output::Path(path.to_owned())
@ -392,11 +401,9 @@ fn export_image(
}; };
export_image_page(config, page, &output, fmt)?; export_image_page(config, page, &output, fmt)?;
Ok(()) Ok(output)
}) })
.collect::<Result<Vec<()>, EcoString>>()?; .collect::<StrResult<Vec<Output>>>()
Ok(())
} }
mod output_template { mod output_template {
@ -501,14 +508,25 @@ impl ExportCache {
/// Writes a Makefile rule describing the relationship between the output and /// Writes a Makefile rule describing the relationship between the output and
/// its dependencies to the path specified by the --make-deps argument, if it /// its dependencies to the path specified by the --make-deps argument, if it
/// was provided. /// was provided.
fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { fn write_make_deps(
world: &mut SystemWorld,
config: &CompileConfig,
outputs: Vec<Output>,
) -> StrResult<()> {
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
let Output::Path(output_path) = &config.output else { let Ok(output_paths) = outputs
bail!("failed to create make dependencies file because output was stdout") .into_iter()
}; .filter_map(|o| match o {
let Some(output_path) = output_path.as_os_str().to_str() else { Output::Path(path) => Some(path.into_os_string().into_string()),
Output::Stdout => None,
})
.collect::<Result<Vec<_>, _>>()
else {
bail!("failed to create make dependencies file because output path was not valid unicode") bail!("failed to create make dependencies file because output path was not valid unicode")
}; };
if output_paths.is_empty() {
bail!("failed to create make dependencies file because output was stdout")
}
// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
// perfect as some special characters can't be escaped. // perfect as some special characters can't be escaped.
@ -522,6 +540,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
res.push('$'); res.push('$');
slashes = 0; slashes = 0;
} }
':' => {
res.push('\\');
slashes = 0;
}
' ' | '\t' => { ' ' | '\t' => {
// `munge`'s source contains a comment here that says: "A // `munge`'s source contains a comment here that says: "A
// space or tab preceded by 2N+1 backslashes represents N // space or tab preceded by 2N+1 backslashes represents N
@ -544,18 +566,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
fn write( fn write(
make_deps_path: &Path, make_deps_path: &Path,
output_path: &str, output_paths: Vec<String>,
root: PathBuf, root: PathBuf,
dependencies: impl Iterator<Item = PathBuf>, dependencies: impl Iterator<Item = PathBuf>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut file = File::create(make_deps_path)?; let mut file = File::create(make_deps_path)?;
let current_dir = std::env::current_dir()?;
let relative_root = diff_paths(&root, &current_dir).unwrap_or(root.clone());
file.write_all(munge(output_path).as_bytes())?; for (i, output_path) in output_paths.into_iter().enumerate() {
if i != 0 {
file.write_all(b" ")?;
}
file.write_all(munge(&output_path).as_bytes())?;
}
file.write_all(b":")?; file.write_all(b":")?;
for dependency in dependencies { for dependency in dependencies {
let Some(dependency) = let relative_dependency = match dependency.strip_prefix(&root) {
dependency.strip_prefix(&root).unwrap_or(&dependency).to_str() Ok(root_relative_dependency) => {
else { relative_root.join(root_relative_dependency)
}
Err(_) => dependency,
};
let Some(relative_dependency) = relative_dependency.to_str() else {
// Silently skip paths that aren't valid unicode so we still // Silently skip paths that aren't valid unicode so we still
// produce a rule that will work for the other paths that can be // produce a rule that will work for the other paths that can be
// processed. // processed.
@ -563,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
}; };
file.write_all(b" ")?; file.write_all(b" ")?;
file.write_all(munge(dependency).as_bytes())?; file.write_all(munge(relative_dependency).as_bytes())?;
} }
file.write_all(b"\n")?; file.write_all(b"\n")?;
Ok(()) Ok(())
} }
write(make_deps_path, output_path, world.root().to_owned(), world.dependencies()) write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
.map_err(|err| { .map_err(|err| {
eco_format!("failed to create make dependencies file due to IO error ({err})") eco_format!("failed to create make dependencies file due to IO error ({err})")
}) })

View File

@ -55,7 +55,7 @@ fn eval_code<'a>(
_ => expr.eval(vm)?, _ => expr.eval(vm)?,
}; };
output = ops::join(output, value).at(span)?; output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?;
if let Some(event) = &vm.flow { if let Some(event) = &vm.flow {
warn_for_discarded_content(&mut vm.engine, event, &output); warn_for_discarded_content(&mut vm.engine, event, &output);

View File

@ -83,7 +83,8 @@ impl Eval for ast::WhileLoop<'_> {
} }
let value = body.eval(vm)?; let value = body.eval(vm)?;
output = ops::join(output, value).at(body.span())?; let span = body.span();
output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?;
match vm.flow { match vm.flow {
Some(FlowEvent::Break(_)) => { Some(FlowEvent::Break(_)) => {
@ -129,7 +130,9 @@ impl Eval for ast::ForLoop<'_> {
let body = self.body(); let body = self.body();
let value = body.eval(vm)?; let value = body.eval(vm)?;
output = ops::join(output, value).at(body.span())?; let span = body.span();
output =
ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?;
match vm.flow { match vm.flow {
Some(FlowEvent::Break(_)) => { Some(FlowEvent::Break(_)) => {

View File

@ -44,11 +44,10 @@ impl Eval for ast::ModuleImport<'_> {
} }
// If there is a rename, import the source itself under that name. // If there is a rename, import the source itself under that name.
let bare_name = self.bare_name();
let new_name = self.new_name(); let new_name = self.new_name();
if let Some(new_name) = new_name { if let Some(new_name) = new_name {
if let Ok(source_name) = &bare_name { if let ast::Expr::Ident(ident) = self.source() {
if source_name == new_name.as_str() { if ident.as_str() == new_name.as_str() {
// Warn on `import x as x` // Warn on `import x as x`
vm.engine.sink.warn(warning!( vm.engine.sink.warn(warning!(
new_name.span(), new_name.span(),
@ -57,6 +56,7 @@ impl Eval for ast::ModuleImport<'_> {
} }
} }
// Define renamed module on the scope.
vm.define(new_name, source.clone()); vm.define(new_name, source.clone());
} }

View File

@ -1,4 +1,4 @@
use typst_library::diag::{At, HintedStrResult, SourceResult}; use typst_library::diag::{At, DeprecationSink, HintedStrResult, SourceResult};
use typst_library::foundations::{ops, IntoValue, Value}; use typst_library::foundations::{ops, IntoValue, Value};
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode};
@ -23,22 +23,22 @@ impl Eval for ast::Binary<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
match self.op() { match self.op() {
ast::BinOp::Add => apply_binary(self, vm, ops::add), ast::BinOp::Add => apply_binary_with_sink(self, vm, ops::add),
ast::BinOp::Sub => apply_binary(self, vm, ops::sub), ast::BinOp::Sub => apply_binary(self, vm, ops::sub),
ast::BinOp::Mul => apply_binary(self, vm, ops::mul), ast::BinOp::Mul => apply_binary(self, vm, ops::mul),
ast::BinOp::Div => apply_binary(self, vm, ops::div), ast::BinOp::Div => apply_binary(self, vm, ops::div),
ast::BinOp::And => apply_binary(self, vm, ops::and), ast::BinOp::And => apply_binary(self, vm, ops::and),
ast::BinOp::Or => apply_binary(self, vm, ops::or), ast::BinOp::Or => apply_binary(self, vm, ops::or),
ast::BinOp::Eq => apply_binary(self, vm, ops::eq), ast::BinOp::Eq => apply_binary_with_sink(self, vm, ops::eq),
ast::BinOp::Neq => apply_binary(self, vm, ops::neq), ast::BinOp::Neq => apply_binary_with_sink(self, vm, ops::neq),
ast::BinOp::Lt => apply_binary(self, vm, ops::lt), ast::BinOp::Lt => apply_binary(self, vm, ops::lt),
ast::BinOp::Leq => apply_binary(self, vm, ops::leq), ast::BinOp::Leq => apply_binary(self, vm, ops::leq),
ast::BinOp::Gt => apply_binary(self, vm, ops::gt), ast::BinOp::Gt => apply_binary(self, vm, ops::gt),
ast::BinOp::Geq => apply_binary(self, vm, ops::geq), ast::BinOp::Geq => apply_binary(self, vm, ops::geq),
ast::BinOp::In => apply_binary(self, vm, ops::in_), ast::BinOp::In => apply_binary_with_sink(self, vm, ops::in_),
ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in), ast::BinOp::NotIn => apply_binary_with_sink(self, vm, ops::not_in),
ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)), ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)),
ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add), ast::BinOp::AddAssign => apply_assignment_with_sink(self, vm, ops::add),
ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub), ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub),
ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul), ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul),
ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div), ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div),
@ -65,6 +65,18 @@ fn apply_binary(
op(lhs, rhs).at(binary.span()) op(lhs, rhs).at(binary.span())
} }
/// Apply a basic binary operation, with the possiblity of deprecations.
fn apply_binary_with_sink(
binary: ast::Binary,
vm: &mut Vm,
op: impl Fn(Value, Value, &mut dyn DeprecationSink) -> HintedStrResult<Value>,
) -> SourceResult<Value> {
let span = binary.span();
let lhs = binary.lhs().eval(vm)?;
let rhs = binary.rhs().eval(vm)?;
op(lhs, rhs, &mut (&mut vm.engine, span)).at(span)
}
/// Apply an assignment operation. /// Apply an assignment operation.
fn apply_assignment( fn apply_assignment(
binary: ast::Binary, binary: ast::Binary,
@ -89,3 +101,23 @@ fn apply_assignment(
*location = op(lhs, rhs).at(binary.span())?; *location = op(lhs, rhs).at(binary.span())?;
Ok(Value::None) Ok(Value::None)
} }
/// Apply an assignment operation, with the possiblity of deprecations.
fn apply_assignment_with_sink(
binary: ast::Binary,
vm: &mut Vm,
op: fn(Value, Value, &mut dyn DeprecationSink) -> HintedStrResult<Value>,
) -> SourceResult<Value> {
let rhs = binary.rhs().eval(vm)?;
let location = binary.lhs().access(vm)?;
let lhs = std::mem::take(&mut *location);
let mut sink = vec![];
let span = binary.span();
*location = op(lhs, rhs, &mut (&mut sink, span)).at(span)?;
if !sink.is_empty() {
for warning in sink {
vm.engine.sink.warn(warning);
}
}
Ok(Value::None)
}

View File

@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
} }
// Behind existing atom or identifier: "$a|$" or "$abc|$". // Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { if matches!(
ctx.leaf.kind(),
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent
) {
ctx.from = ctx.leaf.offset(); ctx.from = ctx.leaf.offset();
math_completions(ctx); math_completions(ctx);
return true; return true;
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Behind an expression plus dot: "emoji.|". // Behind an expression plus dot: "emoji.|".
if_chain! { if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot if ctx.leaf.kind() == SyntaxKind::Dot
|| (ctx.leaf.kind() == SyntaxKind::Text || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
&& ctx.leaf.text() == "."); && ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor; if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling(); if let Some(prev) = ctx.leaf.prev_sibling();
@ -398,7 +401,17 @@ fn field_access_completions(
value: &Value, value: &Value,
styles: &Option<Styles>, styles: &Option<Styles>,
) { ) {
for (name, binding) in value.ty().scope().iter() { let scopes = {
let ty = value.ty().scope();
let elem = match value {
Value::Content(content) => Some(content.elem().scope()),
_ => None,
};
elem.into_iter().chain(Some(ty))
};
// Autocomplete methods from the element's or type's scope.
for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
ctx.call_completion(name.clone(), binding.read()); ctx.call_completion(name.clone(), binding.read());
} }
@ -1747,4 +1760,25 @@ mod tests {
.must_include(["this", "that"]) .must_include(["this", "that"])
.must_exclude(["*", "figure"]); .must_exclude(["*", "figure"]);
} }
#[test]
fn test_autocomplete_type_methods() {
test("#\"hello\".", -1).must_include(["len", "contains"]);
}
#[test]
fn test_autocomplete_content_methods() {
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
.must_include(["indented", "body", "page"]);
}
#[test]
fn test_autocomplete_symbol_variants() {
test("#sym.arrow.", -1)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
test("$ arrow. $", -3)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
}
} }

View File

@ -73,7 +73,10 @@ pub fn jump_from_click(
let Some(id) = span.id() else { continue }; let Some(id) = span.id() else { continue };
let source = world.source(id).ok()?; let source = world.source(id).ok()?;
let node = source.find(span)?; let node = source.find(span)?;
let pos = if node.kind() == SyntaxKind::Text { let pos = if matches!(
node.kind(),
SyntaxKind::Text | SyntaxKind::MathText
) {
let range = node.range(); let range = node.range();
let mut offset = range.start + usize::from(span_offset); let mut offset = range.start + usize::from(span_offset);
if (click.x - pos.x) > width / 2.0 { if (click.x - pos.x) > width / 2.0 {
@ -115,7 +118,7 @@ pub fn jump_from_cursor(
cursor: usize, cursor: usize,
) -> Vec<Position> { ) -> Vec<Position> {
fn is_text(node: &LinkedNode) -> bool { fn is_text(node: &LinkedNode) -> bool {
node.get().kind() == SyntaxKind::Text matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
} }
let root = LinkedNode::new(source.root()); let root = LinkedNode::new(source.root());
@ -261,6 +264,11 @@ mod tests {
test_click(s, point(21.0, 12.0), cursor(56)); test_click(s, point(21.0, 12.0), cursor(56));
} }
#[test]
fn test_jump_from_click_math() {
test_click("$a + b$", point(28.0, 14.0), cursor(5));
}
#[test] #[test]
fn test_jump_from_cursor() { fn test_jump_from_cursor() {
let s = "*Hello* #box[ABC] World"; let s = "*Hello* #box[ABC] World";
@ -268,6 +276,11 @@ mod tests {
test_cursor(s, 14, pos(1, 37.55, 16.58)); test_cursor(s, 14, pos(1, 37.55, 16.58));
} }
#[test]
fn test_jump_from_cursor_math() {
test_cursor("$a + b$", -3, pos(1, 27.51, 16.83));
}
#[test] #[test]
fn test_backlink() { fn test_backlink() {
let s = "#footnote[Hi]"; let s = "#footnote[Hi]";

View File

@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true }
fontdb = { workspace = true, optional = true } fontdb = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true } native-tls = { workspace = true, optional = true }
once_cell = { workspace = true } once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tar = { workspace = true, optional = true } tar = { workspace = true, optional = true }
ureq = { workspace = true, optional = true } ureq = { workspace = true, optional = true }

View File

@ -5,10 +5,9 @@ use std::path::{Path, PathBuf};
use ecow::eco_format; use ecow::eco_format;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::Deserialize;
use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
use typst_syntax::package::{ use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
};
use crate::download::{Downloader, Progress}; use crate::download::{Downloader, Progress};
@ -32,7 +31,7 @@ pub struct PackageStorage {
/// The downloader used for fetching the index and packages. /// The downloader used for fetching the index and packages.
downloader: Downloader, downloader: Downloader,
/// The cached index of the default namespace. /// The cached index of the default namespace.
index: OnceCell<Vec<PackageInfo>>, index: OnceCell<Vec<serde_json::Value>>,
} }
impl PackageStorage { impl PackageStorage {
@ -42,6 +41,18 @@ impl PackageStorage {
package_cache_path: Option<PathBuf>, package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>, package_path: Option<PathBuf>,
downloader: Downloader, downloader: Downloader,
) -> Self {
Self::with_index(package_cache_path, package_path, downloader, OnceCell::new())
}
/// Creates a new package storage with a pre-defined index.
///
/// Useful for testing.
fn with_index(
package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>,
downloader: Downloader,
index: OnceCell<Vec<serde_json::Value>>,
) -> Self { ) -> Self {
Self { Self {
package_cache_path: package_cache_path.or_else(|| { package_cache_path: package_cache_path.or_else(|| {
@ -51,7 +62,7 @@ impl PackageStorage {
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
}), }),
downloader, downloader,
index: OnceCell::new(), index,
} }
} }
@ -109,6 +120,7 @@ impl PackageStorage {
// version. // version.
self.download_index()? self.download_index()?
.iter() .iter()
.filter_map(|value| MinimalPackageInfo::deserialize(value).ok())
.filter(|package| package.name == spec.name) .filter(|package| package.name == spec.name)
.map(|package| package.version) .map(|package| package.version)
.max() .max()
@ -131,7 +143,7 @@ impl PackageStorage {
} }
/// Download the package index. The result of this is cached for efficiency. /// Download the package index. The result of this is cached for efficiency.
pub fn download_index(&self) -> StrResult<&[PackageInfo]> { pub fn download_index(&self) -> StrResult<&[serde_json::Value]> {
self.index self.index
.get_or_try_init(|| { .get_or_try_init(|| {
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
@ -186,3 +198,54 @@ impl PackageStorage {
}) })
} }
} }
/// Minimal information required about a package to determine its latest
/// version.
#[derive(Deserialize)]
struct MinimalPackageInfo {
name: String,
version: PackageVersion,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lazy_deser_index() {
let storage = PackageStorage::with_index(
None,
None,
Downloader::new("typst/test"),
OnceCell::with_value(vec![
serde_json::json!({
"name": "charged-ieee",
"version": "0.1.0",
"entrypoint": "lib.typ",
}),
serde_json::json!({
"name": "unequivocal-ams",
// This version number is currently not valid, so this package
// can't be parsed.
"version": "0.2.0-dev",
"entrypoint": "lib.typ",
}),
]),
);
let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec {
namespace: "preview".into(),
name: "charged-ieee".into(),
});
assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 }));
let ams_version = storage.determine_latest_version(&VersionlessPackageSpec {
namespace: "preview".into(),
name: "unequivocal-ams".into(),
});
assert_eq!(
ams_version,
Err("failed to find package @preview/unequivocal-ams".into())
)
}
}

View File

@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> {
styles, styles,
self.base, self.base,
self.expand, self.expand,
None,
)? )?
.into_frames(); .into_frames();
@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag)); self.output.push(Child::Tag(&elem.tag));
} }
self.lines(lines, styles); let leading = ParElem::leading_in(styles);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] { for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().unwrap(); let elem = c.to_packed::<TagElem>().unwrap();
@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
)? )?
.into_frames(); .into_frames();
let spacing = ParElem::spacing_in(styles); let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.lines(lines, styles); self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive; self.par_situation = ParSituation::Consecutive;
@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
} }
/// Collect laid-out lines. /// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) { fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles); let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let costs = TextElem::costs_in(styles); let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans. // Determine whether to prevent widow and orphans.

View File

@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
mode: FlowMode, mode: FlowMode,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow. // Prepare configuration that is shared across the whole flow.
let config = Config { let config = configuration(shared, regions, columns, column_gutter, mode);
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
}
regions.next();
}
Ok(Fragment::frames(finished))
}
/// Determine the flow's configuration.
fn configuration<'x>(
shared: StyleChain<'x>,
regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
mode: FlowMode,
) -> Config<'x> {
Config {
mode, mode,
shared, shared,
columns: { columns: {
@ -235,39 +278,7 @@ pub fn layout_flow<'a>(
) )
}, },
}), }),
};
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
}
regions.next();
} }
Ok(Fragment::frames(finished))
} }
/// The work that is left to do by flow layout. /// The work that is left to do by flow layout.

View File

@ -95,6 +95,8 @@ pub fn layout_image(
} else { } else {
// If neither is forced, take the natural image size at the image's // If neither is forced, take the natural image size at the image's
// DPI bounded by the available space. // DPI bounded by the available space.
//
// Division by DPI is fine since it's guaranteed to be positive.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new( Size::new(

View File

@ -2,10 +2,8 @@ use typst_library::diag::warning;
use typst_library::foundations::{Packed, Resolve}; use typst_library::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
Spacing,
}; };
use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair; use typst_library::routines::Pair;
use typst_library::text::{ use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
@ -123,40 +121,20 @@ pub fn collect<'a>(
children: &[Pair<'a>], children: &[Pair<'a>],
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, config: &Config,
region: Size, region: Size,
situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len()); let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new(); let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(styles); if !config.first_line_indent.is_zero() {
collector.push_item(Item::Absolute(config.first_line_indent, false));
collector.spans.push(1, Span::detached());
}
if let Some(situation) = situation { if !config.hanging_indent.is_zero() {
let first_line_indent = ParElem::first_line_indent_in(styles); collector.push_item(Item::Absolute(-config.hanging_indent, false));
if !first_line_indent.amount.is_zero() collector.spans.push(1, Span::detached());
&& match situation {
// First-line indent for the first paragraph after a list bullet
// just looks bad.
ParSituation::First => first_line_indent.all && !in_list(styles),
ParSituation::Consecutive => true,
ParSituation::Other => first_line_indent.all,
}
&& AlignElem::alignment_in(styles).resolve(styles).x
== outer_dir.start().into()
{
collector.push_item(Item::Absolute(
first_line_indent.amount.resolve(styles),
false,
));
collector.spans.push(1, Span::detached());
}
let hang = ParElem::hanging_indent_in(styles);
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached());
}
} }
for &(child, styles) in children { for &(child, styles) in children {
@ -167,7 +145,7 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| { collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles); let dir = TextElem::dir_in(styles);
if dir != outer_dir { if dir != config.dir {
// Insert "Explicit Directional Embedding". // Insert "Explicit Directional Embedding".
match dir { match dir {
Dir::LTR => full.push_str(LTR_EMBEDDING), Dir::LTR => full.push_str(LTR_EMBEDDING),
@ -182,7 +160,7 @@ pub fn collect<'a>(
full.push_str(&elem.text); full.push_str(&elem.text);
} }
if dir != outer_dir { if dir != config.dir {
// Insert "Pop Directional Formatting". // Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING); full.push_str(POP_EMBEDDING);
} }
@ -265,16 +243,6 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans)) Ok((collector.full, collector.segments, collector.spans))
} }
/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
}
/// Collects segments. /// Collects segments.
struct Collector<'a> { struct Collector<'a> {
full: String, full: String,

View File

@ -9,7 +9,6 @@ pub fn finalize(
engine: &mut Engine, engine: &mut Engine,
p: &Preparation, p: &Preparation,
lines: &[Line], lines: &[Line],
styles: StyleChain,
region: Size, region: Size,
expand: bool, expand: bool,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
@ -19,9 +18,10 @@ pub fn finalize(
let width = if !region.x.is_finite() let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero())) || (!expand && lines.iter().all(|line| line.fr().is_zero()))
{ {
region region.x.min(
.x p.config.hanging_indent
.min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + lines.iter().map(|line| line.width).max().unwrap_or_default(),
)
} else { } else {
region.x region.x
}; };
@ -29,7 +29,7 @@ pub fn finalize(
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
lines lines
.iter() .iter()
.map(|line| commit(engine, p, line, width, region.y, locator, styles)) .map(|line| commit(engine, p, line, width, region.y, locator))
.collect::<SourceResult<_>>() .collect::<SourceResult<_>>()
.map(Fragment::frames) .map(Fragment::frames)
} }

View File

@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag}; use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::{ParLine, ParLineMarker}; use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem}; use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric; use typst_utils::Numeric;
@ -135,7 +134,7 @@ pub fn line<'a>(
// Whether the line is justified. // Whether the line is justified.
let justify = full.ends_with(LINE_SEPARATOR) let justify = full.ends_with(LINE_SEPARATOR)
|| (p.justify && breakpoint != Breakpoint::Mandatory); || (p.config.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes. // Process dashes.
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
@ -157,14 +156,14 @@ pub fn line<'a>(
// Add a hyphen at the line start, if a previous dash should be repeated. // Add a hyphen at the line start, if a previous dash should be repeated.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() { if let Some(shaped) = items.first_text_mut() {
shaped.prepend_hyphen(engine, p.fallback); shaped.prepend_hyphen(engine, p.config.fallback);
} }
} }
// Add a hyphen at the line end, if we ended on a soft hyphen. // Add a hyphen at the line end, if we ended on a soft hyphen.
if dash == Some(Dash::Soft) { if dash == Some(Dash::Soft) {
if let Some(shaped) = items.last_text_mut() { if let Some(shaped) = items.last_text_mut() {
shaped.push_hyphen(engine, p.fallback); shaped.push_hyphen(engine, p.config.fallback);
} }
} }
@ -234,13 +233,13 @@ where
{ {
// If there is nothing bidirectional going on, skip reordering. // If there is nothing bidirectional going on, skip reordering.
let Some(bidi) = &p.bidi else { let Some(bidi) = &p.bidi else {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
}; };
// The bidi crate panics for empty lines. // The bidi crate panics for empty lines.
if range.is_empty() { if range.is_empty() {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
} }
@ -308,13 +307,13 @@ fn collect_range<'a>(
/// punctuation marks at line start or line end. /// punctuation marks at line start or line end.
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
if text.starts_with(BEGIN_PUNCT_PAT) if text.starts_with(BEGIN_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
{ {
adjust_cj_at_line_start(p, items); adjust_cj_at_line_start(p, items);
} }
if text.ends_with(END_PUNCT_PAT) if text.ends_with(END_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
{ {
adjust_cj_at_line_end(p, items); adjust_cj_at_line_end(p, items);
} }
@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().0; let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink); glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& glyph.x_offset > Em::zero()
{
// If the first glyph is a CJK character adjusted by // If the first glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width. // [`add_cjk_latin_spacing`], restore the original width.
let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let punct = shaped.glyphs.to_mut().last_mut().unwrap(); let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink); punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one() && (glyph.x_advance - glyph.x_offset) > Em::one()
{ {
@ -424,16 +426,15 @@ pub fn commit(
width: Abs, width: Abs,
full: Abs, full: Abs,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang; let mut remaining = width - line.width - p.config.hanging_indent;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// We always build the line from left to right. In an LTR paragraph, we must // We always build the line from left to right. In an LTR paragraph, we must
// thus add the hanging indent to the offset. In an RTL paragraph, the // thus add the hanging indent to the offset. In an RTL paragraph, the
// hanging indent arises naturally due to the line width. // hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR { if p.config.dir == Dir::LTR {
offset += p.hang; offset += p.config.hanging_indent;
} }
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
@ -554,11 +555,13 @@ pub fn commit(
let mut output = Frame::soft(size); let mut output = Frame::soft(size);
output.set_baseline(top); output.set_baseline(top);
add_par_line_marker(&mut output, styles, engine, locator, top); if let Some(marker) = &p.config.numbering_marker {
add_par_line_marker(&mut output, marker, engine, locator, top);
}
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + p.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
} }
@ -575,26 +578,18 @@ pub fn commit(
/// number in the margin, is aligned to the line's baseline. /// number in the margin, is aligned to the line's baseline.
fn add_par_line_marker( fn add_par_line_marker(
output: &mut Frame, output: &mut Frame,
styles: StyleChain, marker: &Packed<ParLineMarker>,
engine: &mut Engine, engine: &mut Engine,
locator: &mut SplitLocator, locator: &mut SplitLocator,
top: Abs, top: Abs,
) { ) {
let Some(numbering) = ParLine::numbering_in(styles) else { return };
let margin = ParLine::number_margin_in(styles);
let align = ParLine::number_align_in(styles);
// Delay resolving the number clearance until line numbers are laid out to
// avoid inconsistent spacing depending on varying font size.
let clearance = ParLine::number_clearance_in(styles);
// Elements in tags must have a location for introspection to work. We do // Elements in tags must have a location for introspection to work. We do
// the work here instead of going through all of the realization process // the work here instead of going through all of the realization process
// just for this, given we don't need to actually place the marker as we // just for this, given we don't need to actually place the marker as we
// manually search for it in the frame later (when building a root flow, // manually search for it in the frame later (when building a root flow,
// where line numbers can be displayed), so we just need it to be in a tag // where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location). // and to be valid (to have a location).
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); let mut marker = marker.clone();
let key = typst_utils::hash128(&marker); let key = typst_utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key); let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc); marker.set_location(loc);
@ -606,7 +601,7 @@ fn add_par_line_marker(
// line's general baseline. However, the line number will still need to // line's general baseline. However, the line number will still need to
// manually adjust its own 'y' position based on its own baseline. // manually adjust its own 'y' position based on its own baseline.
let pos = Point::with_y(top); let pos = Point::with_y(top);
output.push(pos, FrameItem::Tag(Tag::Start(marker))); output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
output.push(pos, FrameItem::Tag(Tag::End(loc, key))); output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
} }

View File

@ -110,15 +110,7 @@ pub fn linebreak<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
width: Abs, width: Abs,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let linebreaks = p.linebreaks.unwrap_or_else(|| { match p.config.linebreaks {
if p.justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
match linebreaks {
Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Simple => linebreak_simple(engine, p, width),
Linebreaks::Optimized => linebreak_optimized(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width),
} }
@ -384,7 +376,7 @@ fn linebreak_optimized_approximate(
// Whether the line is justified. This is not 100% accurate w.r.t // Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough. // to line()'s behaviour, but good enough.
let justify = p.justify && breakpoint != Breakpoint::Mandatory; let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
// We don't really know whether the line naturally ends with a dash // We don't really know whether the line naturally ends with a dash
// here, so we can miss that case, but it's ok, since all of this // here, so we can miss that case, but it's ok, since all of this
@ -573,7 +565,7 @@ fn raw_ratio(
// calculate the extra amount. Also, don't divide by zero. // calculate the extra amount. Also, don't divide by zero.
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
// Normalize the amount by half the em size. // Normalize the amount by half the em size.
ratio = 1.0 + extra_stretch / (p.size / 2.0); ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
} }
// The min value must be < MIN_RATIO, but how much smaller doesn't matter // The min value must be < MIN_RATIO, but how much smaller doesn't matter
@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
return; return;
} }
let hyphenate = p.hyphenate != Some(false); let hyphenate = p.config.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed(); let lb = LINEBREAK_DATA.as_borrowed();
let segmenter = match p.lang { let segmenter = match p.config.lang {
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
_ => &SEGMENTER, _ => &SEGMENTER,
}; };
@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset. /// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool { fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.hyphenate p.config.hyphenate.unwrap_or_else(|| {
.or_else(|| { let (_, item) = p.get(offset);
let (_, item) = p.get(offset); match item.text() {
let styles = item.text()?.styles; Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
Some(TextElem::hyphenate_in(styles)) None => false,
}) }
.unwrap_or(false) })
} }
/// The text language at the given offset. /// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> { fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.lang.or_else(|| { let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; let styles = item.text()?.styles;
Some(TextElem::lang_in(styles)) Some(TextElem::lang_in(styles))
@ -865,13 +857,13 @@ impl CostMetrics {
fn compute(p: &Preparation) -> Self { fn compute(p: &Preparation) -> Self {
Self { Self {
// When justifying, we may stretch spaces below their natural width. // When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// Approximate hyphen width for estimates. // Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size), approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
// Costs. // Costs.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
} }
} }

View File

@ -13,12 +13,17 @@ pub use self::box_::layout_box;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Fragment, Size}; use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::ParElem; use typst_library::model::{
EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
TermsElem,
};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{Costs, Lang, TextElem};
use typst_library::World; use typst_library::World;
use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper}; use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate; use self::deco::decorate;
@ -98,7 +103,7 @@ fn layout_par_impl(
styles, styles,
)?; )?;
layout_inline( layout_inline_impl(
&mut engine, &mut engine,
&children, &children,
&mut locator, &mut locator,
@ -106,33 +111,134 @@ fn layout_par_impl(
region, region,
expand, expand,
Some(situation), Some(situation),
&ConfigBase {
justify: elem.justify(styles),
linebreaks: elem.linebreaks(styles),
first_line_indent: elem.first_line_indent(styles),
hanging_indent: elem.hanging_indent(styles),
},
) )
} }
/// Lays out realized content with inline layout. /// Lays out realized content with inline layout.
#[allow(clippy::too_many_arguments)]
pub fn layout_inline<'a>( pub fn layout_inline<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair<'a>], children: &[Pair<'a>],
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, shared: StyleChain<'a>,
region: Size,
expand: bool,
) -> SourceResult<Fragment> {
layout_inline_impl(
engine,
children,
locator,
shared,
region,
expand,
None,
&ConfigBase {
justify: ParElem::justify_in(shared),
linebreaks: ParElem::linebreaks_in(shared),
first_line_indent: ParElem::first_line_indent_in(shared),
hanging_indent: ParElem::hanging_indent_in(shared),
},
)
}
/// The internal implementation of [`layout_inline`].
#[allow(clippy::too_many_arguments)]
fn layout_inline_impl<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
shared: StyleChain<'a>,
region: Size, region: Size,
expand: bool, expand: bool,
par: Option<ParSituation>, par: Option<ParSituation>,
base: &ConfigBase,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole inline layout.
let config = configuration(base, children, shared, par);
// Collect all text into one string for BiDi analysis. // Collect all text into one string for BiDi analysis.
let (text, segments, spans) = let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
collect(children, engine, locator, styles, region, par)?;
// Perform BiDi analysis and performs some preparation steps before we // Perform BiDi analysis and performs some preparation steps before we
// proceed to line breaking. // proceed to line breaking.
let p = prepare(engine, children, &text, segments, spans, styles, par)?; let p = prepare(engine, &config, &text, segments, spans)?;
// Break the text into lines. // Break the text into lines.
let lines = linebreak(engine, &p, region.x - p.hang); let lines = linebreak(engine, &p, region.x - config.hanging_indent);
// Turn the selected lines into frames. // Turn the selected lines into frames.
finalize(engine, &p, &lines, styles, region, expand, locator) finalize(engine, &p, &lines, region, expand, locator)
}
/// Determine the inline layout's configuration.
fn configuration(
base: &ConfigBase,
children: &[Pair],
shared: StyleChain,
situation: Option<ParSituation>,
) -> Config {
let justify = base.justify;
let font_size = TextElem::size_in(shared);
let dir = TextElem::dir_in(shared);
Config {
justify,
linebreaks: base.linebreaks.unwrap_or_else(|| {
if justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
}),
first_line_indent: {
let FirstLineIndent { amount, all } = base.first_line_indent;
if !amount.is_zero()
&& match situation {
// First-line indent for the first paragraph after a list
// bullet just looks bad.
Some(ParSituation::First) => all && !in_list(shared),
Some(ParSituation::Consecutive) => true,
Some(ParSituation::Other) => all,
None => false,
}
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
{
amount.at(font_size)
} else {
Abs::zero()
}
},
hanging_indent: if situation.is_some() {
base.hanging_indent
} else {
Abs::zero()
},
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
Packed::new(ParLineMarker::new(
numbering,
ParLine::number_align_in(shared),
ParLine::number_margin_in(shared),
// Delay resolving the number clearance until line numbers are
// laid out to avoid inconsistent spacing depending on varying
// font size.
ParLine::number_clearance_in(shared),
))
}),
align: AlignElem::alignment_in(shared).fix(dir).x,
font_size,
dir,
hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
.map(|uniform| uniform.unwrap_or(justify)),
lang: shared_get(children, shared, TextElem::lang_in),
fallback: TextElem::fallback_in(shared),
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
costs: TextElem::costs_in(shared),
}
} }
/// Distinguishes between a few different kinds of paragraphs. /// Distinguishes between a few different kinds of paragraphs.
@ -148,3 +254,66 @@ pub enum ParSituation {
/// Any other kind of paragraph. /// Any other kind of paragraph.
Other, Other,
} }
/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
struct ConfigBase {
justify: bool,
linebreaks: Smart<Linebreaks>,
first_line_indent: FirstLineIndent,
hanging_indent: Abs,
}
/// Shared configuration for the whole inline layout.
struct Config {
/// Whether to justify text.
justify: bool,
/// How to determine line breaks.
linebreaks: Linebreaks,
/// The indent the first line of a paragraph should have.
first_line_indent: Abs,
/// The indent that all but the first line of a paragraph should have.
hanging_indent: Abs,
/// Configuration for line numbering.
numbering_marker: Option<Packed<ParLineMarker>>,
/// The resolved horizontal alignment.
align: FixedAlignment,
/// The text size.
font_size: Abs,
/// The dominant direction.
dir: Dir,
/// A uniform hyphenation setting (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
hyphenate: Option<bool>,
/// The text language (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
lang: Option<Lang>,
/// Whether font fallback is enabled.
fallback: bool,
/// Whether to add spacing between CJK and Latin characters.
cjk_latin_spacing: bool,
/// Costs for various layout decisions.
costs: Costs,
}
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
}

View File

@ -1,9 +1,4 @@
use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Dir, Em};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::routines::Pair;
use typst_library::text::{Costs, Lang, TextElem};
use typst_utils::SliceExt;
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*; use super::*;
@ -17,6 +12,8 @@ use super::*;
pub struct Preparation<'a> { pub struct Preparation<'a> {
/// The full text. /// The full text.
pub text: &'a str, pub text: &'a str,
/// Configuration for inline layout.
pub config: &'a Config,
/// Bidirectional text embedding levels. /// Bidirectional text embedding levels.
/// ///
/// This is `None` if all text directions are uniform (all the base /// This is `None` if all text directions are uniform (all the base
@ -28,28 +25,6 @@ pub struct Preparation<'a> {
pub indices: Vec<usize>, pub indices: Vec<usize>,
/// The span mapper. /// The span mapper.
pub spans: SpanMapper, pub spans: SpanMapper,
/// Whether to hyphenate if it's the same for all children.
pub hyphenate: Option<bool>,
/// Costs for various layout decisions.
pub costs: Costs,
/// The dominant direction.
pub dir: Dir,
/// The text language if it's the same for all children.
pub lang: Option<Lang>,
/// The resolved horizontal alignment.
pub align: FixedAlignment,
/// Whether to justify text.
pub justify: bool,
/// Hanging indent to apply.
pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled.
pub fallback: bool,
/// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>,
/// The text size.
pub size: Abs,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
#[typst_macros::time] #[typst_macros::time]
pub fn prepare<'a>( pub fn prepare<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair<'a>], config: &'a Config,
text: &'a str, text: &'a str,
segments: Vec<Segment<'a>>, segments: Vec<Segment<'a>>,
spans: SpanMapper, spans: SpanMapper,
styles: StyleChain<'a>,
situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles); let default_level = match config.dir {
let default_level = match dir {
Dir::RTL => BidiLevel::rtl(), Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(), _ => BidiLevel::ltr(),
}; };
@ -124,51 +96,20 @@ pub fn prepare<'a>(
indices.extend(range.clone().map(|_| i)); indices.extend(range.clone().map(|_| i));
} }
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); if config.cjk_latin_spacing {
if cjk_latin_spacing {
add_cjk_latin_spacing(&mut items); add_cjk_latin_spacing(&mut items);
} }
// Only apply hanging indent to real paragraphs.
let hang = if situation.is_some() {
ParElem::hanging_indent_in(styles)
} else {
Abs::zero()
};
Ok(Preparation { Ok(Preparation {
config,
text, text,
bidi: is_bidi.then_some(bidi), bidi: is_bidi.then_some(bidi),
items, items,
indices, indices,
spans, spans,
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: shared_get(children, styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang,
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
size: TextElem::size_in(styles),
}) })
} }
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Add some spacing between Han characters and western characters. See /// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode /// in Horizontal Written Mode

View File

@ -107,7 +107,6 @@ fn layout_inline_text(
styles, styles,
Size::splat(Abs::inf()), Size::splat(Abs::inf()),
false, false,
None,
)? )?
.into_frame(); .into_frame();

View File

@ -1281,7 +1281,7 @@ impl ControlPoints {
} }
} }
/// Helper to draw arcs with bezier curves. /// Helper to draw arcs with zier curves.
trait CurveExt { trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point); fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&mut self, start: Point, center: Point, end: Point); fn arc_move(&mut self, start: Point, center: Point, end: Point);
@ -1305,7 +1305,7 @@ impl CurveExt for Curve {
} }
} }
/// Get the control points for a bezier curve that approximates a circular arc for /// Get the control points for a zier curve that approximates a circular arc for
/// a start point, an end point and a center of the circle whose arc connects /// a start point, an end point and a center of the circle whose arc connects
/// the two. /// the two.
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {

View File

@ -38,6 +38,7 @@ indexmap = { workspace = true }
kamadak-exif = { workspace = true } kamadak-exif = { workspace = true }
kurbo = { workspace = true } kurbo = { workspace = true }
lipsum = { workspace = true } lipsum = { workspace = true }
memchr = { workspace = true }
palette = { workspace = true } palette = { workspace = true }
phf = { workspace = true } phf = { workspace = true }
png = { workspace = true } png = { workspace = true }

View File

@ -232,18 +232,42 @@ impl From<SyntaxError> for SourceDiagnostic {
/// Destination for a deprecation message when accessing a deprecated value. /// Destination for a deprecation message when accessing a deprecated value.
pub trait DeprecationSink { pub trait DeprecationSink {
/// Emits the given deprecation message into this sink. /// Emits the given deprecation message into this sink.
fn emit(self, message: &str); fn emit(&mut self, message: &str);
/// Emits the given deprecation message into this sink, with the given
/// hints.
fn emit_with_hints(&mut self, message: &str, hints: &[&str]);
} }
impl DeprecationSink for () { impl DeprecationSink for () {
fn emit(self, _: &str) {} fn emit(&mut self, _: &str) {}
fn emit_with_hints(&mut self, _: &str, _: &[&str]) {}
}
impl DeprecationSink for (&mut Vec<SourceDiagnostic>, Span) {
fn emit(&mut self, message: &str) {
self.0.push(SourceDiagnostic::warning(self.1, message));
}
fn emit_with_hints(&mut self, message: &str, hints: &[&str]) {
self.0.push(
SourceDiagnostic::warning(self.1, message)
.with_hints(hints.iter().copied().map(Into::into)),
);
}
} }
impl DeprecationSink for (&mut Engine<'_>, Span) { impl DeprecationSink for (&mut Engine<'_>, Span) {
/// Emits the deprecation message as a warning. fn emit(&mut self, message: &str) {
fn emit(self, message: &str) {
self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); self.0.sink.warn(SourceDiagnostic::warning(self.1, message));
} }
fn emit_with_hints(&mut self, message: &str, hints: &[&str]) {
self.0.sink.warn(
SourceDiagnostic::warning(self.1, message)
.with_hints(hints.iter().copied().map(Into::into)),
);
}
} }
/// A part of a diagnostic's [trace](SourceDiagnostic::trace). /// A part of a diagnostic's [trace](SourceDiagnostic::trace).

View File

@ -9,7 +9,9 @@ use serde::{Deserialize, Serialize};
use smallvec::SmallVec; use smallvec::SmallVec;
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; use crate::diag::{
bail, At, DeprecationSink, HintedStrResult, SourceDiagnostic, SourceResult, StrResult,
};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue,
@ -143,6 +145,11 @@ impl Array {
Ok(self.iter().cloned().cycle().take(count).collect()) Ok(self.iter().cloned().cycle().take(count).collect())
} }
/// The internal implementation of [`Array::contains`].
pub fn contains_impl(&self, value: &Value, sink: &mut dyn DeprecationSink) -> bool {
self.0.iter().any(|v| ops::equal(v, value, sink))
}
} }
#[scope] #[scope]
@ -290,10 +297,12 @@ impl Array {
#[func] #[func]
pub fn contains( pub fn contains(
&self, &self,
engine: &mut Engine,
span: Span,
/// The value to search for. /// The value to search for.
value: Value, value: Value,
) -> bool { ) -> bool {
self.0.contains(&value) self.contains_impl(&value, &mut (engine, span))
} }
/// Searches for an item for which the given function returns `{true}` and /// Searches for an item for which the given function returns `{true}` and
@ -576,6 +585,8 @@ impl Array {
#[func] #[func]
pub fn sum( pub fn sum(
self, self,
engine: &mut Engine,
span: Span,
/// What to return if the array is empty. Must be set if the array can /// What to return if the array is empty. Must be set if the array can
/// be empty. /// be empty.
#[named] #[named]
@ -587,7 +598,7 @@ impl Array {
.or(default) .or(default)
.ok_or("cannot calculate sum of empty array with no default")?; .ok_or("cannot calculate sum of empty array with no default")?;
for item in iter { for item in iter {
acc = ops::add(acc, item)?; acc = ops::add(acc, item, &mut (&mut *engine, span))?;
} }
Ok(acc) Ok(acc)
} }
@ -686,6 +697,8 @@ impl Array {
#[func] #[func]
pub fn join( pub fn join(
self, self,
engine: &mut Engine,
span: Span,
/// A value to insert between each item of the array. /// A value to insert between each item of the array.
#[default] #[default]
separator: Option<Value>, separator: Option<Value>,
@ -701,13 +714,18 @@ impl Array {
for (i, value) in self.into_iter().enumerate() { for (i, value) in self.into_iter().enumerate() {
if i > 0 { if i > 0 {
if i + 1 == len && last.is_some() { if i + 1 == len && last.is_some() {
result = ops::join(result, last.take().unwrap())?; result = ops::join(
result,
last.take().unwrap(),
&mut (&mut *engine, span),
)?;
} else { } else {
result = ops::join(result, separator.clone())?; result =
ops::join(result, separator.clone(), &mut (&mut *engine, span))?;
} }
} }
result = ops::join(result, value)?; result = ops::join(result, value, &mut (&mut *engine, span))?;
} }
Ok(result) Ok(result)
@ -862,13 +880,14 @@ impl Array {
self, self,
engine: &mut Engine, engine: &mut Engine,
context: Tracked<Context>, context: Tracked<Context>,
span: Span,
/// If given, applies this function to the elements in the array to /// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by. /// determine the keys to deduplicate by.
#[named] #[named]
key: Option<Func>, key: Option<Func>,
) -> SourceResult<Array> { ) -> SourceResult<Array> {
let mut out = EcoVec::with_capacity(self.0.len()); let mut out = EcoVec::with_capacity(self.0.len());
let mut key_of = |x: Value| match &key { let key_of = |engine: &mut Engine, x: Value| match &key {
// NOTE: We are relying on `comemo`'s memoization of function // NOTE: We are relying on `comemo`'s memoization of function
// evaluation to not excessively reevaluate the `key`. // evaluation to not excessively reevaluate the `key`.
Some(f) => f.call(engine, context, [x]), Some(f) => f.call(engine, context, [x]),
@ -879,14 +898,18 @@ impl Array {
// 1. We would like to preserve the order of the elements. // 1. We would like to preserve the order of the elements.
// 2. We cannot hash arbitrary `Value`. // 2. We cannot hash arbitrary `Value`.
'outer: for value in self { 'outer: for value in self {
let key = key_of(value.clone())?; let key = key_of(&mut *engine, value.clone())?;
if out.is_empty() { if out.is_empty() {
out.push(value); out.push(value);
continue; continue;
} }
for second in out.iter() { for second in out.iter() {
if ops::equal(&key, &key_of(second.clone())?) { if ops::equal(
&key,
&key_of(&mut *engine, second.clone())?,
&mut (&mut *engine, span),
) {
continue 'outer; continue 'outer;
} }
} }

View File

@ -110,7 +110,7 @@ impl f64 {
f64::signum(self) f64::signum(self)
} }
/// Converts bytes to a float. /// Interprets bytes as a float.
/// ///
/// ```example /// ```example
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
@ -120,8 +120,10 @@ impl f64 {
pub fn from_bytes( pub fn from_bytes(
/// The bytes that should be converted to a float. /// The bytes that should be converted to a float.
/// ///
/// Must be of length exactly 8 so that the result fits into a 64-bit /// Must have a length of either 4 or 8. The bytes are then
/// float. /// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s
/// binary32 (single-precision) or binary64 (double-precision) format
/// depending on the length of the bytes.
bytes: Bytes, bytes: Bytes,
/// The endianness of the conversion. /// The endianness of the conversion.
#[named] #[named]
@ -158,6 +160,13 @@ impl f64 {
#[named] #[named]
#[default(Endianness::Little)] #[default(Endianness::Little)]
endian: Endianness, endian: Endianness,
/// The size of the resulting bytes.
///
/// This must be either 4 or 8. The call will return the
/// representation of this float in either
/// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32
/// (single-precision) or binary64 (double-precision) format
/// depending on the provided size.
#[named] #[named]
#[default(8)] #[default(8)]
size: u32, size: u32,

View File

@ -5,7 +5,7 @@ use std::cmp::Ordering;
use ecow::eco_format; use ecow::eco_format;
use typst_utils::Numeric; use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, DeprecationSink, HintedStrResult, StrResult};
use crate::foundations::{ use crate::foundations::{
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value, format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
}; };
@ -21,7 +21,7 @@ macro_rules! mismatch {
} }
/// Join a value with another value. /// Join a value with another value.
pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> { pub fn join(lhs: Value, rhs: Value, sink: &mut dyn DeprecationSink) -> StrResult<Value> {
use Value::*; use Value::*;
Ok(match (lhs, rhs) { Ok(match (lhs, rhs) {
(a, None) => a, (a, None) => a,
@ -39,6 +39,17 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b), (Args(a), Args(b)) => Args(a + b),
// Type compatibility.
(Type(a), Str(b)) => {
warn_type_str_join(sink);
Str(format_str!("{a}{b}"))
}
(Str(a), Type(b)) => {
warn_type_str_join(sink);
Str(format_str!("{a}{b}"))
}
(a, b) => mismatch!("cannot join {} with {}", a, b), (a, b) => mismatch!("cannot join {} with {}", a, b),
}) })
} }
@ -88,7 +99,11 @@ pub fn neg(value: Value) -> HintedStrResult<Value> {
} }
/// Compute the sum of two values. /// Compute the sum of two values.
pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> { pub fn add(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
use Value::*; use Value::*;
Ok(match (lhs, rhs) { Ok(match (lhs, rhs) {
(a, None) => a, (a, None) => a,
@ -156,6 +171,16 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Datetime(a), Duration(b)) => Datetime(a + b), (Datetime(a), Duration(b)) => Datetime(a + b),
(Duration(a), Datetime(b)) => Datetime(b + a), (Duration(a), Datetime(b)) => Datetime(b + a),
// Type compatibility.
(Type(a), Str(b)) => {
warn_type_str_add(sink);
Str(format_str!("{a}{b}"))
}
(Str(a), Type(b)) => {
warn_type_str_add(sink);
Str(format_str!("{a}{b}"))
}
(Dyn(a), Dyn(b)) => { (Dyn(a), Dyn(b)) => {
// Alignments can be summed. // Alignments can be summed.
if let (Some(&a), Some(&b)) = if let (Some(&a), Some(&b)) =
@ -394,13 +419,21 @@ pub fn or(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
} }
/// Compute whether two values are equal. /// Compute whether two values are equal.
pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult<Value> { pub fn eq(
Ok(Value::Bool(equal(&lhs, &rhs))) lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
Ok(Value::Bool(equal(&lhs, &rhs, sink)))
} }
/// Compute whether two values are unequal. /// Compute whether two values are unequal.
pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult<Value> { pub fn neq(
Ok(Value::Bool(!equal(&lhs, &rhs))) lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
Ok(Value::Bool(!equal(&lhs, &rhs, sink)))
} }
macro_rules! comparison { macro_rules! comparison {
@ -419,7 +452,7 @@ comparison!(gt, ">", Ordering::Greater);
comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); comparison!(geq, ">=", Ordering::Greater | Ordering::Equal);
/// Determine whether two values are equal. /// Determine whether two values are equal.
pub fn equal(lhs: &Value, rhs: &Value) -> bool { pub fn equal(lhs: &Value, rhs: &Value, sink: &mut dyn DeprecationSink) -> bool {
use Value::*; use Value::*;
match (lhs, rhs) { match (lhs, rhs) {
// Compare reflexively. // Compare reflexively.
@ -463,6 +496,12 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
rat == rel.rel && rel.abs.is_zero() rat == rel.rel && rel.abs.is_zero()
} }
// Type compatibility.
(Type(ty), Str(str)) | (Str(str), Type(ty)) => {
warn_type_str_equal(sink);
ty.compat_name() == str.as_str()
}
_ => false, _ => false,
} }
} }
@ -534,8 +573,12 @@ fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult<Ordering> {
} }
/// Test whether one value is "in" another one. /// Test whether one value is "in" another one.
pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> { pub fn in_(
if let Some(b) = contains(&lhs, &rhs) { lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs, sink) {
Ok(Value::Bool(b)) Ok(Value::Bool(b))
} else { } else {
mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) mismatch!("cannot apply 'in' to {} and {}", lhs, rhs)
@ -543,8 +586,12 @@ pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
} }
/// Test whether one value is "not in" another one. /// Test whether one value is "not in" another one.
pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> { pub fn not_in(
if let Some(b) = contains(&lhs, &rhs) { lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs, sink) {
Ok(Value::Bool(!b)) Ok(Value::Bool(!b))
} else { } else {
mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs)
@ -552,13 +599,27 @@ pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
} }
/// Test for containment. /// Test for containment.
pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> { pub fn contains(
lhs: &Value,
rhs: &Value,
sink: &mut dyn DeprecationSink,
) -> Option<bool> {
use Value::*; use Value::*;
match (lhs, rhs) { match (lhs, rhs) {
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
(Str(a), Dict(b)) => Some(b.contains(a)), (Str(a), Dict(b)) => Some(b.contains(a)),
(a, Array(b)) => Some(b.contains(a.clone())), (a, Array(b)) => Some(b.contains_impl(a, sink)),
// Type compatibility.
(Type(a), Str(b)) => {
warn_type_in_str(sink);
Some(b.as_str().contains(a.compat_name()))
}
(Type(a), Dict(b)) => {
warn_type_in_dict(sink);
Some(b.contains(a.compat_name()))
}
_ => Option::None, _ => Option::None,
} }
@ -568,3 +629,46 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
fn too_large() -> &'static str { fn too_large() -> &'static str {
"value is too large" "value is too large"
} }
#[cold]
fn warn_type_str_add(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"adding strings and types is deprecated",
&["convert the type to a string with `str` first"],
);
}
#[cold]
fn warn_type_str_join(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"joining strings and types is deprecated",
&["convert the type to a string with `str` first"],
);
}
#[cold]
fn warn_type_str_equal(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"comparing strings with types is deprecated",
&[
"compare with the literal type instead",
"this comparison will always return `false` in future Typst releases",
],
);
}
#[cold]
fn warn_type_in_str(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"checking whether a type is contained in a string is deprecated",
&["this compatibility behavior only exists because `type` used to return a string"],
);
}
#[cold]
fn warn_type_in_dict(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"checking whether a type is contained in a dictionary is deprecated",
&["this compatibility behavior only exists because `type` used to return a string"],
);
}

View File

@ -148,9 +148,7 @@ use crate::loading::{DataSource, Load};
#[func(scope)] #[func(scope)]
pub fn plugin( pub fn plugin(
engine: &mut Engine, engine: &mut Engine,
/// A path to a WebAssembly file or raw WebAssembly bytes. /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Module> { ) -> SourceResult<Module> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -300,7 +300,7 @@ impl Binding {
/// As the `sink` /// As the `sink`
/// - pass `()` to ignore the message. /// - pass `()` to ignore the message.
/// - pass `(&mut engine, span)` to emit a warning into the engine. /// - pass `(&mut engine, span)` to emit a warning into the engine.
pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value { pub fn read_checked(&self, mut sink: impl DeprecationSink) -> &Value {
if let Some(message) = self.deprecation { if let Some(message) = self.deprecation {
sink.emit(message); sink.emit(message);
} }

View File

@ -44,6 +44,16 @@ use crate::foundations::{
/// #type(int) \ /// #type(int) \
/// #type(type) /// #type(type)
/// ``` /// ```
///
/// # Compatibility
/// In Typst 0.7 and lower, the `type` function returned a string instead of a
/// type. Compatibility with the old way will remain until Typst 0.14 to give
/// package authors time to upgrade.
///
/// - Checks like `{int == "integer"}` evaluate to `{true}`
/// - Adding/joining a type and string will yield a string
/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}`
/// if the dictionary has a string key matching the type's name
#[ty(scope, cast)] #[ty(scope, cast)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Type(Static<NativeTypeData>); pub struct Type(Static<NativeTypeData>);
@ -106,6 +116,14 @@ impl Type {
} }
} }
// Type compatibility.
impl Type {
/// The type's backward-compatible name.
pub fn compat_name(&self) -> &str {
self.long_name()
}
}
#[scope] #[scope]
impl Type { impl Type {
/// Determines a value's type. /// Determines a value's type.

View File

@ -292,7 +292,8 @@ impl Repr for Value {
impl PartialEq for Value { impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
ops::equal(self, other) // No way to emit deprecation warnings here :(
ops::equal(self, other, &mut ())
} }
} }

View File

@ -20,9 +20,7 @@ use crate::loading::{DataSource, Load};
#[func(scope, title = "CBOR")] #[func(scope, title = "CBOR")]
pub fn cbor( pub fn cbor(
engine: &mut Engine, engine: &mut Engine,
/// A path to a CBOR file or raw CBOR bytes. /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -26,9 +26,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "CSV")] #[func(scope, title = "CSV")]
pub fn csv( pub fn csv(
engine: &mut Engine, engine: &mut Engine,
/// Path to a CSV file or raw CSV bytes. /// A [path]($syntax/#paths) to a CSV file or raw CSV bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
/// The delimiter that separates columns in the CSV file. /// The delimiter that separates columns in the CSV file.
/// Must be a single ASCII character. /// Must be a single ASCII character.

View File

@ -51,9 +51,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "JSON")] #[func(scope, title = "JSON")]
pub fn json( pub fn json(
engine: &mut Engine, engine: &mut Engine,
/// Path to a JSON file or raw JSON bytes. /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -29,9 +29,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "TOML")] #[func(scope, title = "TOML")]
pub fn toml( pub fn toml(
engine: &mut Engine, engine: &mut Engine,
/// A path to a TOML file or raw TOML bytes. /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -58,9 +58,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "XML")] #[func(scope, title = "XML")]
pub fn xml( pub fn xml(
engine: &mut Engine, engine: &mut Engine,
/// A path to an XML file or raw XML bytes. /// A [path]($syntax/#paths) to an XML file or raw XML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -41,9 +41,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "YAML")] #[func(scope, title = "YAML")]
pub fn yaml( pub fn yaml(
engine: &mut Engine, engine: &mut Engine,
/// A path to a YAML file or raw YAML bytes. /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -11,7 +11,7 @@ use crate::foundations::{
use crate::html::{attr, tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location; use crate::introspection::Location;
use crate::layout::Position; use crate::layout::Position;
use crate::text::{Hyphenate, TextElem}; use crate::text::TextElem;
/// Links to a URL or a location in the document. /// Links to a URL or a location in the document.
/// ///
@ -138,7 +138,7 @@ impl Show for Packed<LinkElem> {
impl ShowSet for Packed<LinkElem> { impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles { fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out out
} }
} }

View File

@ -623,7 +623,7 @@ impl OutlineEntry {
/// The content which is displayed in place of the referred element at its /// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this is its /// entry in the outline. For a heading, this is its
/// [`body`]($heading.body), for a figure a caption, and for equations it is /// [`body`]($heading.body); for a figure a caption and for equations, it is
/// empty. /// empty.
#[func] #[func]
pub fn body(&self) -> StrResult<Content> { pub fn body(&self) -> StrResult<Content> {

View File

@ -1,9 +1,12 @@
use ecow::EcoString; use ecow::EcoString;
use typst_library::foundations::Target;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::{At, SourceResult}; use crate::diag::{warning, At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; use crate::foundations::{
elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem,
};
use crate::introspection::Locatable; use crate::introspection::Locatable;
use crate::World; use crate::World;
@ -32,12 +35,10 @@ use crate::World;
/// embedded file conforms to PDF/A-1 or PDF/A-2. /// embedded file conforms to PDF/A-1 or PDF/A-2.
#[elem(Show, Locatable)] #[elem(Show, Locatable)]
pub struct EmbedElem { pub struct EmbedElem {
/// Path of the file to be embedded. /// The [path]($syntax/#paths) of the file to be embedded.
/// ///
/// Must always be specified, but is only read from if no data is provided /// Must always be specified, but is only read from if no data is provided
/// in the following argument. /// in the following argument.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
#[required] #[required]
#[parse( #[parse(
let Spanned { v: path, span } = let Spanned { v: path, span } =
@ -80,7 +81,12 @@ pub struct EmbedElem {
} }
impl Show for Packed<EmbedElem> { impl Show for Packed<EmbedElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles) == Target::Html {
engine
.sink
.warn(warning!(self.span(), "embed was ignored during HTML export"));
}
Ok(Content::empty()) Ok(Content::empty())
} }
} }

View File

@ -51,7 +51,6 @@ use crate::foundations::{
}; };
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
use crate::math::{EquationElem, MathSize}; use crate::math::{EquationElem, MathSize};
use crate::model::ParElem;
use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::visualize::{Color, Paint, RelativeTo, Stroke};
use crate::World; use crate::World;
@ -504,9 +503,8 @@ pub struct TextElem {
/// enabling hyphenation can /// enabling hyphenation can
/// improve justification. /// improve justification.
/// ``` /// ```
#[resolve]
#[ghost] #[ghost]
pub hyphenate: Hyphenate, pub hyphenate: Smart<bool>,
/// The "cost" of various choices when laying out text. A higher cost means /// The "cost" of various choices when laying out text. A higher cost means
/// the layout engine will make the choice less often. Costs are specified /// the layout engine will make the choice less often. Costs are specified
@ -1110,27 +1108,6 @@ impl Resolve for TextDir {
} }
} }
/// Whether to hyphenate text.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub Smart<bool>);
cast! {
Hyphenate,
self => self.0.into_value(),
v: Smart<bool> => Self(v),
}
impl Resolve for Hyphenate {
type Output = bool;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self.0 {
Smart::Auto => ParElem::justify_in(styles),
Smart::Custom(v) => v,
}
}
}
/// A set of stylistic sets to enable. /// A set of stylistic sets to enable.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct StylisticSets(u32); pub struct StylisticSets(u32);
@ -1403,24 +1380,7 @@ pub fn is_default_ignorable(c: char) -> bool {
fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) { fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
let book = engine.world.book(); let book = engine.world.book();
for family in &list.v { for family in &list.v {
let found = book.contains_family(family.as_str()); if !book.contains_family(family.as_str()) {
if family.as_str() == "linux libertine" {
let mut warning = warning!(
list.span,
"Typst's default font has changed from Linux Libertine to its successor Libertinus Serif";
hint: "please set the font to `\"Libertinus Serif\"` instead"
);
if found {
warning.hint(
"Linux Libertine is available on your system - \
you can ignore this warning if you are sure you want to use it",
);
warning.hint("this warning will be removed in Typst 0.13");
}
engine.sink.warn(warning);
} else if !found {
engine.sink.warn(warning!( engine.sink.warn(warning!(
list.span, list.span,
"unknown font family: {}", "unknown font family: {}",

View File

@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load}; use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem}; use crate::model::{Figurable, ParElem};
use crate::text::{ use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
};
use crate::visualize::Color; use crate::visualize::Color;
use crate::World; use crate::World;
@ -448,10 +446,14 @@ impl Show for Packed<RawElem> {
let mut realized = Content::sequence(seq); let mut realized = Content::sequence(seq);
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::pre) return Ok(HtmlElem::new(if self.block(styles) {
.with_body(Some(realized)) tag::pre
.pack() } else {
.spanned(self.span())); tag::code
})
.with_body(Some(realized))
.pack()
.spanned(self.span()));
} }
if self.block(styles) { if self.block(styles) {
@ -472,7 +474,7 @@ impl ShowSet for Packed<RawElem> {
let mut out = Styles::new(); let mut out = Styles::new();
out.set(TextElem::set_overhang(false)); out.set(TextElem::set_overhang(false));
out.set(TextElem::set_lang(Lang::ENGLISH)); out.set(TextElem::set_lang(Lang::ENGLISH));
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));

View File

@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> {
"el" => ("", "", "«", "»"), "el" => ("", "", "«", "»"),
"he" => ("", "", "", ""), "he" => ("", "", "", ""),
"hr" => ("", "", "", ""), "hr" => ("", "", "", ""),
"bg" => ("", "", "", ""),
_ if lang.dir() == Dir::RTL => ("", "", "", ""), _ if lang.dir() == Dir::RTL => ("", "", "", ""),
_ => default, _ => default,
}; };

View File

@ -10,12 +10,12 @@ use crate::foundations::{
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
/// A curve consisting of movements, lines, and Beziér segments. /// A curve consisting of movements, lines, and Bézier segments.
/// ///
/// At any point in time, there is a conceptual pen or cursor. /// At any point in time, there is a conceptual pen or cursor.
/// - Move elements move the cursor without drawing. /// - Move elements move the cursor without drawing.
/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new /// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
/// position, potentially with control point for a Beziér curve. /// position, potentially with control point for a Bézier curve.
/// - Close elements draw a straight or smooth line back to the start of the /// - Close elements draw a straight or smooth line back to the start of the
/// curve or the latest preceding move segment. /// curve or the latest preceding move segment.
/// ///
@ -26,7 +26,7 @@ use crate::visualize::{FillRule, Paint, Stroke};
/// or relative to the current pen/cursor position, that is, the position where /// or relative to the current pen/cursor position, that is, the position where
/// the previous segment ended. /// the previous segment ended.
/// ///
/// Beziér curve control points can be skipped by passing `{none}` or /// Bézier curve control points can be skipped by passing `{none}` or
/// automatically mirrored from the preceding segment by passing `{auto}`. /// automatically mirrored from the preceding segment by passing `{auto}`.
/// ///
/// # Example /// # Example
@ -88,7 +88,7 @@ pub struct CurveElem {
#[fold] #[fold]
pub stroke: Smart<Option<Stroke>>, pub stroke: Smart<Option<Stroke>>,
/// The components of the curve, in the form of moves, line and Beziér /// The components of the curve, in the form of moves, line and Bézier
/// segment, and closes. /// segment, and closes.
#[variadic] #[variadic]
pub components: Vec<CurveComponent>, pub components: Vec<CurveComponent>,
@ -225,7 +225,7 @@ pub struct CurveLine {
pub relative: bool, pub relative: bool,
} }
/// Adds a quadratic Beziér curve segment from the last point to `end`, using /// Adds a quadratic Bézier curve segment from the last point to `end`, using
/// `control` as the control point. /// `control` as the control point.
/// ///
/// ```example /// ```example
@ -245,9 +245,9 @@ pub struct CurveLine {
/// ``` /// ```
#[elem(name = "quad", title = "Curve Quadratic Segment")] #[elem(name = "quad", title = "Curve Quadratic Segment")]
pub struct CurveQuad { pub struct CurveQuad {
/// The control point of the quadratic Beziér curve. /// The control point of the quadratic Bézier curve.
/// ///
/// - If `{auto}` and this segment follows another quadratic Beziér curve, /// - If `{auto}` and this segment follows another quadratic Bézier curve,
/// the previous control point will be mirrored. /// the previous control point will be mirrored.
/// - If `{none}`, the control point defaults to `end`, and the curve will /// - If `{none}`, the control point defaults to `end`, and the curve will
/// be a straight line. /// be a straight line.
@ -272,7 +272,7 @@ pub struct CurveQuad {
pub relative: bool, pub relative: bool,
} }
/// Adds a cubic Beziér curve segment from the last point to `end`, using /// Adds a cubic Bézier curve segment from the last point to `end`, using
/// `control-start` and `control-end` as the control points. /// `control-start` and `control-end` as the control points.
/// ///
/// ```example /// ```example
@ -388,7 +388,7 @@ pub enum CloseMode {
Straight, Straight,
} }
/// A curve consisting of movements, lines, and Beziér segments. /// A curve consisting of movements, lines, and Bézier segments.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Curve(pub Vec<CurveItem>); pub struct Curve(pub Vec<CurveItem>);

View File

@ -582,12 +582,11 @@ impl Gradient {
let mut stops = stops let mut stops = stops
.iter() .iter()
.map(move |&(color, offset)| { .map(move |&(color, offset)| {
let t = i as f64 / n as f64;
let r = offset.get(); let r = offset.get();
if i % 2 == 1 && mirror { if i % 2 == 1 && mirror {
(color, Ratio::new(t + (1.0 - r) / n as f64)) (color, Ratio::new((i as f64 + 1.0 - r) / n as f64))
} else { } else {
(color, Ratio::new(t + r / n as f64)) (color, Ratio::new((i as f64 + r) / n as f64))
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -1230,7 +1229,7 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
}; };
if stop.get() < last_stop { if stop.get() < last_stop {
bail!(*span, "offsets must be in strictly monotonic order"); bail!(*span, "offsets must be in monotonic order");
} }
last_stop = stop.get(); last_stop = stop.get();

View File

@ -46,10 +46,11 @@ use crate::text::LocalName;
/// ``` /// ```
#[elem(scope, Show, LocalName, Figurable)] #[elem(scope, Show, LocalName, Figurable)]
pub struct ImageElem { pub struct ImageElem {
/// A path to an image file or raw bytes making up an image in one of the /// A [path]($syntax/#paths) to an image file or raw bytes making up an
/// supported [formats]($image.format). /// image in one of the supported [formats]($image.format).
/// ///
/// For more details about paths, see the [Paths section]($syntax/#paths). /// Bytes can be used to specify raw pixel data in a row-major,
/// left-to-right, top-to-bottom format.
/// ///
/// ```example /// ```example
/// #let original = read("diagram.svg") /// #let original = read("diagram.svg")
@ -397,8 +398,7 @@ impl ImageFormat {
return Some(Self::Raster(RasterFormat::Exchange(format))); return Some(Self::Raster(RasterFormat::Exchange(format)));
} }
// SVG or compressed SVG. if is_svg(data) {
if data.starts_with(b"<svg") || data.starts_with(&[0x1f, 0x8b]) {
return Some(Self::Vector(VectorFormat::Svg)); return Some(Self::Vector(VectorFormat::Svg));
} }
@ -406,6 +406,21 @@ impl ImageFormat {
} }
} }
/// Checks whether the data looks like an SVG or a compressed SVG.
fn is_svg(data: &[u8]) -> bool {
// Check for the gzip magic bytes. This check is perhaps a bit too
// permissive as other formats than SVGZ could use gzip.
if data.starts_with(&[0x1f, 0x8b]) {
return true;
}
// If the first 2048 bytes contain the SVG namespace declaration, we assume
// that it's an SVG. Note that, if the SVG does not contain a namespace
// declaration, usvg will reject it.
let head = &data[..data.len().min(2048)];
memchr::memmem::find(head, b"http://www.w3.org/2000/svg").is_some()
}
/// A vector graphics format. /// A vector graphics format.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VectorFormat { pub enum VectorFormat {

View File

@ -160,6 +160,8 @@ impl RasterImage {
} }
/// The image's pixel density in pixels per inch, if known. /// The image's pixel density in pixels per inch, if known.
///
/// This is guaranteed to be positive.
pub fn dpi(&self) -> Option<f64> { pub fn dpi(&self) -> Option<f64> {
self.0.dpi self.0.dpi
} }
@ -334,6 +336,9 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) {
} }
/// Try to determine the DPI (dots per inch) of the image. /// Try to determine the DPI (dots per inch) of the image.
///
/// This is guaranteed to be a positive value, or `None` if invalid or
/// unspecified.
fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> { fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
// Try to extract the DPI from the EXIF metadata. If that doesn't yield // Try to extract the DPI from the EXIF metadata. If that doesn't yield
// anything, fall back to specialized procedures for extracting JPEG or PNG // anything, fall back to specialized procedures for extracting JPEG or PNG
@ -341,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
exif.and_then(exif_dpi) exif.and_then(exif_dpi)
.or_else(|| jpeg_dpi(data)) .or_else(|| jpeg_dpi(data))
.or_else(|| png_dpi(data)) .or_else(|| png_dpi(data))
.filter(|&dpi| dpi > 0.0)
} }
/// Try to get the DPI from the EXIF metadata. /// Try to get the DPI from the EXIF metadata.

View File

@ -8,7 +8,7 @@ use crate::foundations::{
use crate::layout::{Axes, BlockElem, Length, Rel}; use crate::layout::{Axes, BlockElem, Length, Rel};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
/// A path through a list of points, connected by Bezier curves. /// A path through a list of points, connected by Bézier curves.
/// ///
/// # Example /// # Example
/// ```example /// ```example
@ -59,8 +59,8 @@ pub struct PathElem {
#[fold] #[fold]
pub stroke: Smart<Option<Stroke>>, pub stroke: Smart<Option<Stroke>>,
/// Whether to close this path with one last bezier curve. This curve will /// Whether to close this path with one last zier curve. This curve will
/// takes into account the adjacent control points. If you want to close /// take into account the adjacent control points. If you want to close
/// with a straight line, simply add one last point that's the same as the /// with a straight line, simply add one last point that's the same as the
/// start point. /// start point.
#[default(false)] #[default(false)]

View File

@ -412,7 +412,7 @@ pub enum Geometry {
Line(Point), Line(Point),
/// A rectangle with its origin in the topleft corner. /// A rectangle with its origin in the topleft corner.
Rect(Size), Rect(Size),
/// A curve consisting of movements, lines, and Bezier segments. /// A curve consisting of movements, lines, and Bézier segments.
Curve(Curve), Curve(Curve),
} }

View File

@ -1,9 +1,9 @@
--- ---
title: Unreleased changes planned for 0.13.0 title: 0.13.0
description: Changes slated to appear in Typst 0.13.0 description: Changes in Typst 0.13.0
--- ---
# Unreleased # Version 0.13.0 (February 19, 2025)
## Highlights ## Highlights
- There is now a distinction between [proper paragraphs]($par) and just - There is now a distinction between [proper paragraphs]($par) and just
@ -16,7 +16,7 @@ description: Changes slated to appear in Typst 0.13.0
- The `image` function now supports raw [pixel raster formats]($image.format) - The `image` function now supports raw [pixel raster formats]($image.format)
for generating images from within Typst for generating images from within Typst
- Functions that accept [file paths]($syntax/#paths) now also accept raw - Functions that accept [file paths]($syntax/#paths) now also accept raw
[bytes] instead, for full flexibility [bytes], for full flexibility
- WebAssembly [plugins]($plugin) are more flexible and automatically run - WebAssembly [plugins]($plugin) are more flexible and automatically run
multi-threaded multi-threaded
- Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`) - Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`)
@ -45,6 +45,7 @@ description: Changes slated to appear in Typst 0.13.0
result in a warning result in a warning
- The default show rules of various built-in elements like lists, quotes, etc. - The default show rules of various built-in elements like lists, quotes, etc.
were adjusted to ensure they produce/don't produce paragraphs as appropriate were adjusted to ensure they produce/don't produce paragraphs as appropriate
- Removed support for booleans and content in [`outline.indent`]
- The [`outline`] function was fully reworked to improve its out-of-the-box - The [`outline`] function was fully reworked to improve its out-of-the-box
behavior **(Breaking change)** behavior **(Breaking change)**
- [Outline entries]($outline.entry) are now [blocks]($block) and are thus - [Outline entries]($outline.entry) are now [blocks]($block) and are thus
@ -98,8 +99,9 @@ description: Changes slated to appear in Typst 0.13.0
- Fixed interaction of clipping and outset on [`box`] and [`block`] - Fixed interaction of clipping and outset on [`box`] and [`block`]
- Fixed panic with [`path`] of infinite length - Fixed panic with [`path`] of infinite length
- Fixed non-solid (e.g. tiling) text fills in clipped blocks - Fixed non-solid (e.g. tiling) text fills in clipped blocks
- Auto-detection of image formats from a raw buffer now has basic support for - Fixed a crash for images with a DPI value of zero
SVGs - Fixed floating-point error in [`gradient.repeat`]
- Auto-detection of image formats from a raw buffer now has support for SVGs
## Scripting ## Scripting
- Functions that accept [file paths]($syntax/#paths) now also accept raw - Functions that accept [file paths]($syntax/#paths) now also accept raw
@ -155,7 +157,7 @@ description: Changes slated to appear in Typst 0.13.0
- Fixed multi-line annotations (e.g. overbrace) changing the math baseline - Fixed multi-line annotations (e.g. overbrace) changing the math baseline
- Fixed merging of attachments when the base is a nested equation - Fixed merging of attachments when the base is a nested equation
- Fixed resolving of contextual (em-based) text sizes within math - Fixed resolving of contextual (em-based) text sizes within math
- Fixed spacing around ⊥ - Fixed spacing around up tacks ()
## Bibliography ## Bibliography
- Prose and author-only citations now use editor names if the author names are - Prose and author-only citations now use editor names if the author names are
@ -186,12 +188,12 @@ description: Changes slated to appear in Typst 0.13.0
- [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text - [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text
anymore anymore
- Fixed wrong language codes being used for Greek and Ukrainian - Fixed wrong language codes being used for Greek and Ukrainian
- Fixed default quotes for Croatian - Fixed default quotes for Croatian and Bulgarian
- Fixed crash in RTL text handling - Fixed crash in RTL text handling
- Added support for [`raw`] syntax highlighting for a few new languages: CFML, - Added support for [`raw`] syntax highlighting for a few new languages: CFML,
NSIS, and WGSL NSIS, and WGSL
- New font metadata exception for New Computer Modern Sans Math - New font metadata exception for New Computer Modern Sans Math
- Updated bundled New Computer Modern fonts to version 7.0 - Updated bundled New Computer Modern fonts to version 7.0.1
## Layout ## Layout
- Fixed various bugs with footnotes - Fixed various bugs with footnotes
@ -229,7 +231,7 @@ description: Changes slated to appear in Typst 0.13.0
- A shebang `#!` at the very start of a file is now ignored - A shebang `#!` at the very start of a file is now ignored
## PDF export ## PDF export
- Added `pdf.embed` function for embedding arbitrary files in the exported PDF - Added [`pdf.embed`] function for embedding arbitrary files in the exported PDF
- Added support for PDF/A-3b export - Added support for PDF/A-3b export
- The PDF timestamp will now contain the timezone by default - The PDF timestamp will now contain the timezone by default
@ -270,6 +272,9 @@ feature flag.
- Added a live reloading HTTP server to `typst watch` when targeting HTML - Added a live reloading HTTP server to `typst watch` when targeting HTML
- Fixed self-update not being aware about certain target architectures - Fixed self-update not being aware about certain target architectures
- Fixed crash when piping `typst fonts` output to another command - Fixed crash when piping `typst fonts` output to another command
- Fixed handling of relative paths in `--make-deps` output
- Fixed handling of multipage SVG and PNG export in `--make-deps` output
- Colons in filenames are now correctly escaped in `--make-deps` output
## Symbols ## Symbols
- New - New
@ -312,8 +317,20 @@ feature flag.
functions directly accepting both paths and bytes functions directly accepting both paths and bytes
- The `sect` and its variants in favor of `inter`, and `integral.sect` in favor - The `sect` and its variants in favor of `inter`, and `integral.sect` in favor
of `integral.inter` of `integral.inter`
- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`) - The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`)
which was temporarily introduced in Typst 0.8 **(Breaking change)** which was temporarily introduced in Typst 0.8 now emits warnings. It will be
removed in Typst 0.14.
## Removals
- Removed `style` function and `styles` argument of [`measure`], use a [context]
expression instead **(Breaking change)**
- Removed `state.display` function, use [`state.get`] instead
**(Breaking change)**
- Removed `location` argument of [`state.at`], [`counter.at`], and [`query`]
**(Breaking change)**
- Removed compatibility behavior where [`counter.display`] worked without
[context] **(Breaking change)**
- Removed compatibility behavior of [`locate`] **(Breaking change)**
## Development ## Development
- The `typst::compile` function is now generic and can return either a - The `typst::compile` function is now generic and can return either a
@ -322,3 +339,6 @@ feature flag.
feature is enabled feature is enabled
- Increased minimum supported Rust version to 1.80 - Increased minimum supported Rust version to 1.80
- Fixed linux/arm64 Docker image - Fixed linux/arm64 Docker image
## Contributors
<contributors from="v0.12.0" to="v0.13.0" />

View File

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

View File

@ -11,7 +11,7 @@ the PNG you exported, you will notice a loss of quality. Typst calculates the
resolution of your PNGs based on each page's physical dimensions and the PPI. If resolution of your PNGs based on each page's physical dimensions and the PPI. If
you need guidance for choosing a PPI value, consider the following: you need guidance for choosing a PPI value, consider the following:
- A DPI value of 300 or 600 is typical for desktop printing. - A value of 300 or 600 is typical for desktop printing.
- Professional prints of detailed graphics can go up to 1200 PPI. - Professional prints of detailed graphics can go up to 1200 PPI.
- If your document is only viewed at a distance, e.g. a poster, you may choose a - If your document is only viewed at a distance, e.g. a poster, you may choose a
smaller value than 300. smaller value than 300.

View File

@ -550,8 +550,6 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
.collect(), .collect(),
}); });
} }
outline.extend(scope_outline(&model.scope));
} else { } else {
outline.extend(model.params.iter().map(|param| OutlineItem { outline.extend(model.params.iter().map(|param| OutlineItem {
id: eco_format!("{id_base}-{}", urlify(param.name)), id: eco_format!("{id_base}-{}", urlify(param.name)),
@ -560,27 +558,30 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
})); }));
} }
outline.extend(scope_outline(&model.scope, id_base));
outline outline
} }
/// Produce an outline for a function scope. /// Produce an outline for a function scope.
fn scope_outline(scope: &[FuncModel]) -> Option<OutlineItem> { fn scope_outline(scope: &[FuncModel], id_base: &str) -> Option<OutlineItem> {
if scope.is_empty() { if scope.is_empty() {
return None; return None;
} }
Some(OutlineItem { let dash = if id_base.is_empty() { "" } else { "-" };
id: "definitions".into(), let id = eco_format!("{id_base}{dash}definitions");
name: "Definitions".into(),
children: scope let children = scope
.iter() .iter()
.map(|func| { .map(|func| {
let id = urlify(&eco_format!("definitions-{}", func.name)); let id = urlify(&eco_format!("{id}-{}", func.name));
let children = func_outline(func, &id); let children = func_outline(func, &id);
OutlineItem { id, name: func.title.into(), children } OutlineItem { id, name: func.title.into(), children }
}) })
.collect(), .collect();
})
Some(OutlineItem { id, name: "Definitions".into(), children })
} }
/// Create a page for a group of functions. /// Create a page for a group of functions.
@ -687,7 +688,7 @@ fn type_outline(model: &TypeModel) -> Vec<OutlineItem> {
}); });
} }
outline.extend(scope_outline(&model.scope)); outline.extend(scope_outline(&model.scope, ""));
outline outline
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -251,6 +251,6 @@ fn lines(
(1..=count) (1..=count)
.map(|n| numbering.apply(engine, context, &[n])) .map(|n| numbering.apply(engine, context, &[n]))
.collect::<SourceResult<Array>>()? .collect::<SourceResult<Array>>()?
.join(Some('\n'.into_value()), None) .join(engine, span, Some('\n'.into_value()), None)
.at(span) .at(span)
} }

View File

@ -2,6 +2,60 @@
#test(type(1), int) #test(type(1), int)
#test(type(ltr), direction) #test(type(ltr), direction)
#test(type(10 / 3), float) #test(type(10 / 3), float)
#test(type(10) == int, true)
#test(type(10) != int, false)
--- type-string-compatibility-add ---
// Warning: 7-23 adding strings and types is deprecated
// Hint: 7-23 convert the type to a string with `str` first
#test("is " + type(10), "is integer")
// Warning: 7-23 adding strings and types is deprecated
// Hint: 7-23 convert the type to a string with `str` first
#test(type(10) + " is", "integer is")
--- type-string-compatibility-join ---
// Warning: 16-24 joining strings and types is deprecated
// Hint: 16-24 convert the type to a string with `str` first
#test({ "is "; type(10) }, "is integer")
// Warning: 19-24 joining strings and types is deprecated
// Hint: 19-24 convert the type to a string with `str` first
#test({ type(10); " is" }, "integer is")
--- type-string-compatibility-equal ---
// Warning: 7-28 comparing strings with types is deprecated
// Hint: 7-28 compare with the literal type instead
// Hint: 7-28 this comparison will always return `false` in future Typst releases
#test(type(10) == "integer", true)
// Warning: 7-26 comparing strings with types is deprecated
// Hint: 7-26 compare with the literal type instead
// Hint: 7-26 this comparison will always return `false` in future Typst releases
#test(type(10) != "float", true)
--- type-string-compatibility-in-array ---
// Warning: 7-35 comparing strings with types is deprecated
// Hint: 7-35 compare with the literal type instead
// Hint: 7-35 this comparison will always return `false` in future Typst releases
#test(int in ("integer", "string"), true)
// Warning: 7-37 comparing strings with types is deprecated
// Hint: 7-37 compare with the literal type instead
// Hint: 7-37 this comparison will always return `false` in future Typst releases
#test(float in ("integer", "string"), false)
--- type-string-compatibility-in-str ---
// Warning: 7-35 checking whether a type is contained in a string is deprecated
// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string
#test(int in "integers or strings", true)
// Warning: 7-35 checking whether a type is contained in a string is deprecated
// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string
#test(str in "integers or strings", true)
// Warning: 7-37 checking whether a type is contained in a string is deprecated
// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string
#test(float in "integers or strings", false)
--- type-string-compatibility-in-dict ---
// Warning: 7-37 checking whether a type is contained in a dictionary is deprecated
// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string
#test(int in (integer: 1, string: 2), true)
--- issue-3110-type-constructor --- --- issue-3110-type-constructor ---
// Let the error message report the type name. // Let the error message report the type name.

View File

@ -322,6 +322,20 @@ A
#context test(query(<a>).len(), 1) #context test(query(<a>).len(), 1)
--- issue-5831-par-constructor-args ---
// Make sure that all arguments are also respected in the constructor.
A
#par(
leading: 2pt,
spacing: 20pt,
justify: true,
linebreaks: "simple",
first-line-indent: (amount: 1em, all: true),
hanging-indent: 5pt,
)[
The par function has a constructor and justification.
]
--- show-par-set-block-hint --- --- show-par-set-block-hint ---
// Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore // Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore
// Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore // Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore

View File

@ -255,6 +255,10 @@
// Warning: 17-21 unnecessary import rename to same name // Warning: 17-21 unnecessary import rename to same name
#import enum as enum #import enum as enum
--- import-rename-necessary ---
#import "module.typ" as module: a
#test(module.a, a)
--- import-rename-unnecessary-mixed --- --- import-rename-unnecessary-mixed ---
// Warning: 17-21 unnecessary import rename to same name // Warning: 17-21 unnecessary import rename to same name
#import enum as enum: item #import enum as enum: item
@ -263,10 +267,6 @@
// Warning: 31-35 unnecessary import rename to same name // Warning: 31-35 unnecessary import rename to same name
#import enum as enum: item as item #import enum as enum: item as item
--- import-item-rename-unnecessary-string ---
// Warning: 25-31 unnecessary import rename to same name
#import "module.typ" as module
--- import-item-rename-unnecessary-but-ok --- --- import-item-rename-unnecessary-but-ok ---
#import "modul" + "e.typ" as module #import "modul" + "e.typ" as module
#test(module.b, 1) #test(module.b, 1)

View File

@ -77,11 +77,6 @@ I
#let var = text(font: ("list-of", "nonexistent-fonts"))[don't] #let var = text(font: ("list-of", "nonexistent-fonts"))[don't]
#var #var
--- text-font-linux-libertine ---
// Warning: 17-34 Typst's default font has changed from Linux Libertine to its successor Libertinus Serif
// Hint: 17-34 please set the font to `"Libertinus Serif"` instead
#set text(font: "Linux Libertine")
--- issue-5499-text-fill-in-clip-block --- --- issue-5499-text-fill-in-clip-block ---
#let t = tiling( #let t = tiling(

View File

@ -658,3 +658,11 @@ $ A = mat(
height: 10pt, height: 10pt,
fill: gradient.linear(violet, blue, space: cmyk) fill: gradient.linear(violet, blue, space: cmyk)
) )
--- issue-5819-gradient-repeat ---
// Ensure the gradient constructor generates monotonic stops which can be fed
// back into the gradient constructor itself.
#let my-gradient = gradient.linear(red, blue).repeat(5)
#let _ = gradient.linear(..my-gradient.stops())
#let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true)
#let _ = gradient.linear(..my-gradient2.stops())

View File

@ -65,6 +65,17 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
caption: [Bilingual text] caption: [Bilingual text]
) )
--- image-svg-auto-detection ---
#image(bytes(
```
<?xml version="1.0" encoding="utf-8"?>
<!-- An SVG -->
<svg width="200" height="150" xmlns="http://www.w3.org/2000/svg">
<rect fill="red" stroke="black" x="25" y="25" width="150" height="100"/>
</svg>
```.text
))
--- image-pixmap-rgb8 --- --- image-pixmap-rgb8 ---
#image( #image(
bytes(( bytes((
@ -152,8 +163,8 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
#image("path/does/not/exist") #image("path/does/not/exist")
--- image-bad-format --- --- image-bad-format ---
// Error: 2-22 unknown image format // Error: 2-37 unknown image format
#image("./image.typ") #image("/assets/plugins/hello.wasm")
--- image-bad-svg --- --- image-bad-svg ---
// Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4) // Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4)