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

View File

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

View File

@ -6,8 +6,9 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Timelike, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term;
use ecow::{eco_format, EcoString};
use ecow::eco_format;
use parking_lot::RwLock;
use pathdiff::diff_paths;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
@ -188,7 +189,7 @@ pub fn compile_once(
match output {
// Export the PDF / PNG.
Ok(()) => {
Ok(outputs) => {
let duration = start.elapsed();
if config.watching {
@ -202,7 +203,7 @@ pub fn compile_once(
print_diagnostics(world, &[], &warnings, config.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
write_make_deps(world, config)?;
write_make_deps(world, config, outputs)?;
open_output(config)?;
}
@ -226,12 +227,15 @@ pub fn compile_once(
fn compile_and_export(
world: &mut SystemWorld,
config: &mut CompileConfig,
) -> Warned<SourceResult<()>> {
) -> Warned<SourceResult<Vec<Output>>> {
match config.output_format {
OutputFormat::Html => {
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
let result = output.and_then(|document| export_html(&document, config));
Warned { output: result, warnings }
Warned {
output: result.map(|()| vec![config.output.clone()]),
warnings,
}
}
_ => {
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.
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
fn export_paged(
document: &PagedDocument,
config: &CompileConfig,
) -> SourceResult<Vec<Output>> {
match config.output_format {
OutputFormat::Pdf => export_pdf(document, config),
OutputFormat::Pdf => {
export_pdf(document, config).map(|()| vec![config.output.clone()])
}
OutputFormat::Png => {
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
}
@ -327,7 +336,7 @@ fn export_image(
document: &PagedDocument,
config: &CompileConfig,
fmt: ImageExportFormat,
) -> StrResult<()> {
) -> StrResult<Vec<Output>> {
// Determine whether we have indexable templates in output
let can_handle_multiple = match config.output {
Output::Stdout => false,
@ -383,7 +392,7 @@ fn export_image(
&& config.export_cache.is_cached(*i, &page.frame)
&& path.exists()
{
return Ok(());
return Ok(Output::Path(path.to_path_buf()));
}
Output::Path(path.to_owned())
@ -392,11 +401,9 @@ fn export_image(
};
export_image_page(config, page, &output, fmt)?;
Ok(())
Ok(output)
})
.collect::<Result<Vec<()>, EcoString>>()?;
Ok(())
.collect::<StrResult<Vec<Output>>>()
}
mod output_template {
@ -501,14 +508,25 @@ impl ExportCache {
/// Writes a Makefile rule describing the relationship between the output and
/// its dependencies to the path specified by the --make-deps argument, if it
/// was provided.
fn write_make_deps(world: &mut SystemWorld, 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 Output::Path(output_path) = &config.output else {
bail!("failed to create make dependencies file because output was stdout")
};
let Some(output_path) = output_path.as_os_str().to_str() else {
let Ok(output_paths) = outputs
.into_iter()
.filter_map(|o| match o {
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")
};
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
// 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('$');
slashes = 0;
}
':' => {
res.push('\\');
slashes = 0;
}
' ' | '\t' => {
// `munge`'s source contains a comment here that says: "A
// 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(
make_deps_path: &Path,
output_path: &str,
output_paths: Vec<String>,
root: PathBuf,
dependencies: impl Iterator<Item = PathBuf>,
) -> io::Result<()> {
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":")?;
for dependency in dependencies {
let Some(dependency) =
dependency.strip_prefix(&root).unwrap_or(&dependency).to_str()
else {
let relative_dependency = match dependency.strip_prefix(&root) {
Ok(root_relative_dependency) => {
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
// produce a rule that will work for the other paths that can be
// processed.
@ -563,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
};
file.write_all(b" ")?;
file.write_all(munge(dependency).as_bytes())?;
file.write_all(munge(relative_dependency).as_bytes())?;
}
file.write_all(b"\n")?;
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| {
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)?,
};
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 {
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)?;
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 {
Some(FlowEvent::Break(_)) => {
@ -129,7 +130,9 @@ impl Eval for ast::ForLoop<'_> {
let body = self.body();
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 {
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.
let bare_name = self.bare_name();
let new_name = self.new_name();
if let Some(new_name) = new_name {
if let Ok(source_name) = &bare_name {
if source_name == new_name.as_str() {
if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() {
// Warn on `import x as x`
vm.engine.sink.warn(warning!(
new_name.span(),
@ -57,6 +56,7 @@ impl Eval for ast::ModuleImport<'_> {
}
}
// Define renamed module on the scope.
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_syntax::ast::{self, AstNode};
@ -23,22 +23,22 @@ impl Eval for ast::Binary<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
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::Mul => apply_binary(self, vm, ops::mul),
ast::BinOp::Div => apply_binary(self, vm, ops::div),
ast::BinOp::And => apply_binary(self, vm, ops::and),
ast::BinOp::Or => apply_binary(self, vm, ops::or),
ast::BinOp::Eq => apply_binary(self, vm, ops::eq),
ast::BinOp::Neq => apply_binary(self, vm, ops::neq),
ast::BinOp::Eq => apply_binary_with_sink(self, vm, ops::eq),
ast::BinOp::Neq => apply_binary_with_sink(self, vm, ops::neq),
ast::BinOp::Lt => apply_binary(self, vm, ops::lt),
ast::BinOp::Leq => apply_binary(self, vm, ops::leq),
ast::BinOp::Gt => apply_binary(self, vm, ops::gt),
ast::BinOp::Geq => apply_binary(self, vm, ops::geq),
ast::BinOp::In => apply_binary(self, vm, ops::in_),
ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in),
ast::BinOp::In => apply_binary_with_sink(self, vm, ops::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::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::MulAssign => apply_assignment(self, vm, ops::mul),
ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div),
@ -65,6 +65,18 @@ fn apply_binary(
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.
fn apply_assignment(
binary: ast::Binary,
@ -89,3 +101,23 @@ fn apply_assignment(
*location = op(lhs, rhs).at(binary.span())?;
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|$".
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();
math_completions(ctx);
return true;
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Behind an expression plus dot: "emoji.|".
if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot
|| (ctx.leaf.kind() == SyntaxKind::Text
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
&& ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling();
@ -398,7 +401,17 @@ fn field_access_completions(
value: &Value,
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());
}
@ -1747,4 +1760,25 @@ mod tests {
.must_include(["this", "that"])
.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 source = world.source(id).ok()?;
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 mut offset = range.start + usize::from(span_offset);
if (click.x - pos.x) > width / 2.0 {
@ -115,7 +118,7 @@ pub fn jump_from_cursor(
cursor: usize,
) -> Vec<Position> {
fn is_text(node: &LinkedNode) -> bool {
node.get().kind() == SyntaxKind::Text
matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
}
let root = LinkedNode::new(source.root());
@ -261,6 +264,11 @@ mod tests {
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]
fn test_jump_from_cursor() {
let s = "*Hello* #box[ABC] World";
@ -268,6 +276,11 @@ mod tests {
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]
fn test_backlink() {
let s = "#footnote[Hi]";

View File

@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true }
fontdb = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true }
once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tar = { 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 once_cell::sync::OnceCell;
use serde::Deserialize;
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
use typst_syntax::package::{
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
};
use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
use crate::download::{Downloader, Progress};
@ -32,7 +31,7 @@ pub struct PackageStorage {
/// The downloader used for fetching the index and packages.
downloader: Downloader,
/// The cached index of the default namespace.
index: OnceCell<Vec<PackageInfo>>,
index: OnceCell<Vec<serde_json::Value>>,
}
impl PackageStorage {
@ -42,6 +41,18 @@ impl PackageStorage {
package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>,
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 {
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))
}),
downloader,
index: OnceCell::new(),
index,
}
}
@ -109,6 +120,7 @@ impl PackageStorage {
// version.
self.download_index()?
.iter()
.filter_map(|value| MinimalPackageInfo::deserialize(value).ok())
.filter(|package| package.name == spec.name)
.map(|package| package.version)
.max()
@ -131,7 +143,7 @@ impl PackageStorage {
}
/// 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
.get_or_try_init(|| {
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,
self.base,
self.expand,
None,
)?
.into_frames();
@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
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..] {
let elem = c.to_packed::<TagElem>().unwrap();
@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
)?
.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.lines(lines, styles);
self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive;
@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
}
/// 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 leading = ParElem::leading_in(styles);
let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans.

View File

@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
mode: FlowMode,
) -> SourceResult<Fragment> {
// 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,
shared,
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.

View File

@ -95,6 +95,8 @@ pub fn layout_image(
} else {
// If neither is forced, take the natural image size at the image's
// 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 natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new(

View File

@ -2,10 +2,8 @@ use typst_library::diag::warning;
use typst_library::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
Spacing,
Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
};
use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair;
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
@ -123,41 +121,21 @@ pub fn collect<'a>(
children: &[Pair<'a>],
engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>,
config: &Config,
region: Size,
situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(styles);
if let Some(situation) = situation {
let first_line_indent = ParElem::first_line_indent_in(styles);
if !first_line_indent.amount.is_zero()
&& 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,
));
if !config.first_line_indent.is_zero() {
collector.push_item(Item::Absolute(config.first_line_indent, 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));
if !config.hanging_indent.is_zero() {
collector.push_item(Item::Absolute(-config.hanging_indent, false));
collector.spans.push(1, Span::detached());
}
}
for &(child, styles) in children {
let prev_len = collector.full.len();
@ -167,7 +145,7 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles);
if dir != outer_dir {
if dir != config.dir {
// Insert "Explicit Directional Embedding".
match dir {
Dir::LTR => full.push_str(LTR_EMBEDDING),
@ -182,7 +160,7 @@ pub fn collect<'a>(
full.push_str(&elem.text);
}
if dir != outer_dir {
if dir != config.dir {
// Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING);
}
@ -265,16 +243,6 @@ pub fn collect<'a>(
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.
struct Collector<'a> {
full: String,

View File

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

View File

@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine;
use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag};
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_utils::Numeric;
@ -135,7 +134,7 @@ pub fn line<'a>(
// Whether the line is justified.
let justify = full.ends_with(LINE_SEPARATOR)
|| (p.justify && breakpoint != Breakpoint::Mandatory);
|| (p.config.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes.
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.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
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.
if dash == Some(Dash::Soft) {
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.
let Some(bidi) = &p.bidi else {
f(range, p.dir == Dir::RTL);
f(range, p.config.dir == Dir::RTL);
return;
};
// The bidi crate panics for empty lines.
if range.is_empty() {
f(range, p.dir == Dir::RTL);
f(range, p.config.dir == Dir::RTL);
return;
}
@ -308,13 +307,13 @@ fn collect_range<'a>(
/// punctuation marks at line start or line end.
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
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);
}
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);
}
@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink);
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
// [`add_cjk_latin_spacing`], restore the original width.
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();
punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing
} else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one()
{
@ -424,16 +426,15 @@ pub fn commit(
width: Abs,
full: Abs,
locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> 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();
// 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
// hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR {
offset += p.hang;
if p.config.dir == Dir::LTR {
offset += p.config.hanging_indent;
}
// Handle hanging punctuation to the left.
@ -554,11 +555,13 @@ pub fn commit(
let mut output = Frame::soft(size);
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.
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();
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.
fn add_par_line_marker(
output: &mut Frame,
styles: StyleChain,
marker: &Packed<ParLineMarker>,
engine: &mut Engine,
locator: &mut SplitLocator,
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
// 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
// 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
// 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 loc = locator.next_location(engine.introspector, key);
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
// manually adjust its own 'y' position based on its own baseline.
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)));
}

View File

@ -110,15 +110,7 @@ pub fn linebreak<'a>(
p: &'a Preparation<'a>,
width: Abs,
) -> Vec<Line<'a>> {
let linebreaks = p.linebreaks.unwrap_or_else(|| {
if p.justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
match linebreaks {
match p.config.linebreaks {
Linebreaks::Simple => linebreak_simple(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
// 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
// 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.
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
// 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
@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
return;
}
let hyphenate = p.hyphenate != Some(false);
let hyphenate = p.config.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed();
let segmenter = match p.lang {
let segmenter = match p.config.lang {
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
_ => &SEGMENTER,
};
@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.hyphenate
.or_else(|| {
p.config.hyphenate.unwrap_or_else(|| {
let (_, item) = p.get(offset);
let styles = item.text()?.styles;
Some(TextElem::hyphenate_in(styles))
match item.text() {
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
None => false,
}
})
.unwrap_or(false)
}
/// The text language at the given offset.
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 styles = item.text()?.styles;
Some(TextElem::lang_in(styles))
@ -865,13 +857,13 @@ impl CostMetrics {
fn compute(p: &Preparation) -> Self {
Self {
// When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 },
min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// 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.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().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 typst_library::diag::SourceResult;
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::layout::{Fragment, Size};
use typst_library::model::ParElem;
use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::{
EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
TermsElem,
};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{Costs, Lang, TextElem};
use typst_library::World;
use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
@ -98,7 +103,7 @@ fn layout_par_impl(
styles,
)?;
layout_inline(
layout_inline_impl(
&mut engine,
&children,
&mut locator,
@ -106,33 +111,134 @@ fn layout_par_impl(
region,
expand,
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.
#[allow(clippy::too_many_arguments)]
pub fn layout_inline<'a>(
engine: &mut Engine,
children: &[Pair<'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,
expand: bool,
par: Option<ParSituation>,
base: &ConfigBase,
) -> 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.
let (text, segments, spans) =
collect(children, engine, locator, styles, region, par)?;
let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
// Perform BiDi analysis and performs some preparation steps before we
// 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.
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.
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.
@ -148,3 +254,66 @@ pub enum ParSituation {
/// Any other kind of paragraph.
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::{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 typst_library::layout::{Dir, Em};
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
@ -17,6 +12,8 @@ use super::*;
pub struct Preparation<'a> {
/// The full text.
pub text: &'a str,
/// Configuration for inline layout.
pub config: &'a Config,
/// Bidirectional text embedding levels.
///
/// This is `None` if all text directions are uniform (all the base
@ -28,28 +25,6 @@ pub struct Preparation<'a> {
pub indices: Vec<usize>,
/// The span mapper.
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> {
@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
#[typst_macros::time]
pub fn prepare<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
config: &'a Config,
text: &'a str,
segments: Vec<Segment<'a>>,
spans: SpanMapper,
styles: StyleChain<'a>,
situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles);
let default_level = match dir {
let default_level = match config.dir {
Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(),
};
@ -124,51 +96,20 @@ pub fn prepare<'a>(
indices.extend(range.clone().map(|_| i));
}
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
if cjk_latin_spacing {
if config.cjk_latin_spacing {
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 {
config,
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
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
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode

View File

@ -107,7 +107,6 @@ fn layout_inline_text(
styles,
Size::splat(Abs::inf()),
false,
None,
)?
.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 {
fn arc(&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
/// the two.
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 }
kurbo = { workspace = true }
lipsum = { workspace = true }
memchr = { workspace = true }
palette = { workspace = true }
phf = { 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.
pub trait DeprecationSink {
/// 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 () {
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) {
/// Emits the deprecation message as a warning.
fn emit(self, message: &str) {
fn emit(&mut self, message: &str) {
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).

View File

@ -9,7 +9,9 @@ use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
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::foundations::{
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())
}
/// 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]
@ -290,10 +297,12 @@ impl Array {
#[func]
pub fn contains(
&self,
engine: &mut Engine,
span: Span,
/// The value to search for.
value: Value,
) -> bool {
self.0.contains(&value)
self.contains_impl(&value, &mut (engine, span))
}
/// Searches for an item for which the given function returns `{true}` and
@ -576,6 +585,8 @@ impl Array {
#[func]
pub fn sum(
self,
engine: &mut Engine,
span: Span,
/// What to return if the array is empty. Must be set if the array can
/// be empty.
#[named]
@ -587,7 +598,7 @@ impl Array {
.or(default)
.ok_or("cannot calculate sum of empty array with no default")?;
for item in iter {
acc = ops::add(acc, item)?;
acc = ops::add(acc, item, &mut (&mut *engine, span))?;
}
Ok(acc)
}
@ -686,6 +697,8 @@ impl Array {
#[func]
pub fn join(
self,
engine: &mut Engine,
span: Span,
/// A value to insert between each item of the array.
#[default]
separator: Option<Value>,
@ -701,13 +714,18 @@ impl Array {
for (i, value) in self.into_iter().enumerate() {
if i > 0 {
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 {
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)
@ -862,13 +880,14 @@ impl Array {
self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
/// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by.
#[named]
key: Option<Func>,
) -> SourceResult<Array> {
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
// evaluation to not excessively reevaluate the `key`.
Some(f) => f.call(engine, context, [x]),
@ -879,14 +898,18 @@ impl Array {
// 1. We would like to preserve the order of the elements.
// 2. We cannot hash arbitrary `Value`.
'outer: for value in self {
let key = key_of(value.clone())?;
let key = key_of(&mut *engine, value.clone())?;
if out.is_empty() {
out.push(value);
continue;
}
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;
}
}

View File

@ -110,7 +110,7 @@ impl f64 {
f64::signum(self)
}
/// Converts bytes to a float.
/// Interprets bytes as a float.
///
/// ```example
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
@ -120,8 +120,10 @@ impl f64 {
pub fn from_bytes(
/// The bytes that should be converted to a float.
///
/// Must be of length exactly 8 so that the result fits into a 64-bit
/// float.
/// Must have a length of either 4 or 8. The bytes are then
/// 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,
/// The endianness of the conversion.
#[named]
@ -158,6 +160,13 @@ impl f64 {
#[named]
#[default(Endianness::Little)]
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]
#[default(8)]
size: u32,

View File

@ -5,7 +5,7 @@ use std::cmp::Ordering;
use ecow::eco_format;
use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::diag::{bail, DeprecationSink, HintedStrResult, StrResult};
use crate::foundations::{
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
};
@ -21,7 +21,7 @@ macro_rules! mismatch {
}
/// 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::*;
Ok(match (lhs, rhs) {
(a, None) => a,
@ -39,6 +39,17 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(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),
})
}
@ -88,7 +99,11 @@ pub fn neg(value: Value) -> HintedStrResult<Value> {
}
/// 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::*;
Ok(match (lhs, rhs) {
(a, None) => a,
@ -156,6 +171,16 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Datetime(a), Duration(b)) => Datetime(a + b),
(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)) => {
// Alignments can be summed.
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.
pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
Ok(Value::Bool(equal(&lhs, &rhs)))
pub fn eq(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
Ok(Value::Bool(equal(&lhs, &rhs, sink)))
}
/// Compute whether two values are unequal.
pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
Ok(Value::Bool(!equal(&lhs, &rhs)))
pub fn neq(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
Ok(Value::Bool(!equal(&lhs, &rhs, sink)))
}
macro_rules! comparison {
@ -419,7 +452,7 @@ comparison!(gt, ">", Ordering::Greater);
comparison!(geq, ">=", Ordering::Greater | Ordering::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::*;
match (lhs, rhs) {
// Compare reflexively.
@ -463,6 +496,12 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
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,
}
}
@ -534,8 +573,12 @@ fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult<Ordering> {
}
/// Test whether one value is "in" another one.
pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
pub fn in_(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs, sink) {
Ok(Value::Bool(b))
} else {
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.
pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
pub fn not_in(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs, sink) {
Ok(Value::Bool(!b))
} else {
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.
pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
pub fn contains(
lhs: &Value,
rhs: &Value,
sink: &mut dyn DeprecationSink,
) -> Option<bool> {
use Value::*;
match (lhs, rhs) {
(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)),
(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,
}
@ -568,3 +629,46 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
fn too_large() -> &'static str {
"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)]
pub fn plugin(
engine: &mut Engine,
/// A path to a WebAssembly file or raw WebAssembly bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
source: Spanned<DataSource>,
) -> SourceResult<Module> {
let data = source.load(engine.world)?;

View File

@ -300,7 +300,7 @@ impl Binding {
/// As the `sink`
/// - pass `()` to ignore the message.
/// - 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 {
sink.emit(message);
}

View File

@ -44,6 +44,16 @@ use crate::foundations::{
/// #type(int) \
/// #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)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
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]
impl Type {
/// Determines a value's type.

View File

@ -292,7 +292,8 @@ impl Repr for Value {
impl PartialEq for Value {
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")]
pub fn cbor(
engine: &mut Engine,
/// A path to a CBOR file or raw CBOR bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -623,7 +623,7 @@ impl OutlineEntry {
/// The content which is displayed in place of the referred element at 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.
#[func]
pub fn body(&self) -> StrResult<Content> {

View File

@ -1,9 +1,12 @@
use ecow::EcoString;
use typst_library::foundations::Target;
use typst_syntax::Spanned;
use crate::diag::{At, SourceResult};
use crate::diag::{warning, At, SourceResult};
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::World;
@ -32,12 +35,10 @@ use crate::World;
/// embedded file conforms to PDF/A-1 or PDF/A-2.
#[elem(Show, Locatable)]
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
/// in the following argument.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
#[required]
#[parse(
let Spanned { v: path, span } =
@ -80,7 +81,12 @@ pub struct 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())
}
}

View File

@ -51,7 +51,6 @@ use crate::foundations::{
};
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
use crate::math::{EquationElem, MathSize};
use crate::model::ParElem;
use crate::visualize::{Color, Paint, RelativeTo, Stroke};
use crate::World;
@ -504,9 +503,8 @@ pub struct TextElem {
/// enabling hyphenation can
/// improve justification.
/// ```
#[resolve]
#[ghost]
pub hyphenate: Hyphenate,
pub hyphenate: Smart<bool>,
/// 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
@ -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.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
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>) {
let book = engine.world.book();
for family in &list.v {
let found = 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 {
if !book.contains_family(family.as_str()) {
engine.sink.warn(warning!(
list.span,
"unknown font family: {}",

View File

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

View File

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

View File

@ -10,12 +10,12 @@ use crate::foundations::{
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
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.
/// - Move elements move the cursor without drawing.
/// - 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
/// 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
/// 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}`.
///
/// # Example
@ -88,7 +88,7 @@ pub struct CurveElem {
#[fold]
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.
#[variadic]
pub components: Vec<CurveComponent>,
@ -225,7 +225,7 @@ pub struct CurveLine {
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.
///
/// ```example
@ -245,9 +245,9 @@ pub struct CurveLine {
/// ```
#[elem(name = "quad", title = "Curve Quadratic Segment")]
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.
/// - If `{none}`, the control point defaults to `end`, and the curve will
/// be a straight line.
@ -272,7 +272,7 @@ pub struct CurveQuad {
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.
///
/// ```example
@ -388,7 +388,7 @@ pub enum CloseMode {
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)]
pub struct Curve(pub Vec<CurveItem>);

View File

@ -582,12 +582,11 @@ impl Gradient {
let mut stops = stops
.iter()
.map(move |&(color, offset)| {
let t = i as f64 / n as f64;
let r = offset.get();
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 {
(color, Ratio::new(t + r / n as f64))
(color, Ratio::new((i as f64 + r) / n as f64))
}
})
.collect::<Vec<_>>();
@ -1230,7 +1229,7 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
};
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();

View File

@ -46,10 +46,11 @@ use crate::text::LocalName;
/// ```
#[elem(scope, Show, LocalName, Figurable)]
pub struct ImageElem {
/// A path to an image file or raw bytes making up an image in one of the
/// supported [formats]($image.format).
/// A [path]($syntax/#paths) to an image file or raw bytes making up an
/// 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
/// #let original = read("diagram.svg")
@ -397,8 +398,7 @@ impl ImageFormat {
return Some(Self::Raster(RasterFormat::Exchange(format)));
}
// SVG or compressed SVG.
if data.starts_with(b"<svg") || data.starts_with(&[0x1f, 0x8b]) {
if is_svg(data) {
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.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VectorFormat {

View File

@ -160,6 +160,8 @@ impl RasterImage {
}
/// The image's pixel density in pixels per inch, if known.
///
/// This is guaranteed to be positive.
pub fn dpi(&self) -> Option<f64> {
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.
///
/// 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> {
// 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
@ -341,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option<f64> {
exif.and_then(exif_dpi)
.or_else(|| jpeg_dpi(data))
.or_else(|| png_dpi(data))
.filter(|&dpi| dpi > 0.0)
}
/// 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::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
@ -59,8 +59,8 @@ pub struct PathElem {
#[fold]
pub stroke: Smart<Option<Stroke>>,
/// Whether to close this path with one last bezier curve. This curve will
/// takes into account the adjacent control points. If you want to close
/// Whether to close this path with one last zier curve. This curve will
/// 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
/// start point.
#[default(false)]

View File

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

View File

@ -1,9 +1,9 @@
---
title: Unreleased changes planned for 0.13.0
description: Changes slated to appear in Typst 0.13.0
title: 0.13.0
description: Changes in Typst 0.13.0
---
# Unreleased
# Version 0.13.0 (February 19, 2025)
## Highlights
- 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)
for generating images from within Typst
- 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
multi-threaded
- 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
- 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
- Removed support for booleans and content in [`outline.indent`]
- The [`outline`] function was fully reworked to improve its out-of-the-box
behavior **(Breaking change)**
- [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 panic with [`path`] of infinite length
- 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
SVGs
- Fixed a crash for images with a DPI value of zero
- Fixed floating-point error in [`gradient.repeat`]
- Auto-detection of image formats from a raw buffer now has support for SVGs
## Scripting
- 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 merging of attachments when the base is a nested equation
- Fixed resolving of contextual (em-based) text sizes within math
- Fixed spacing around ⊥
- Fixed spacing around up tacks ()
## Bibliography
- 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
anymore
- 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
- Added support for [`raw`] syntax highlighting for a few new languages: CFML,
NSIS, and WGSL
- 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
- 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
## 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
- 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
- Fixed self-update not being aware about certain target architectures
- 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
- New
@ -312,8 +317,20 @@ feature flag.
functions directly accepting both paths and bytes
- The `sect` and its variants in favor of `inter`, and `integral.sect` in favor
of `integral.inter`
- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`)
which was temporarily introduced in Typst 0.8 **(Breaking change)**
- The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`)
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
- The `typst::compile` function is now generic and can return either a
@ -322,3 +339,6 @@ feature flag.
feature is enabled
- Increased minimum supported Rust version to 1.80
- 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.
## 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.11.1]($changelog/0.11.1)
- [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
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.
- If your document is only viewed at a distance, e.g. a poster, you may choose a
smaller value than 300.

View File

@ -550,8 +550,6 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
.collect(),
});
}
outline.extend(scope_outline(&model.scope));
} else {
outline.extend(model.params.iter().map(|param| OutlineItem {
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
}
/// 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() {
return None;
}
Some(OutlineItem {
id: "definitions".into(),
name: "Definitions".into(),
children: scope
let dash = if id_base.is_empty() { "" } else { "-" };
let id = eco_format!("{id_base}{dash}definitions");
let children = scope
.iter()
.map(|func| {
let id = urlify(&eco_format!("definitions-{}", func.name));
let id = urlify(&eco_format!("{id}-{}", func.name));
let children = func_outline(func, &id);
OutlineItem { id, name: func.title.into(), children }
})
.collect(),
})
.collect();
Some(OutlineItem { id, name: "Definitions".into(), children })
}
/// 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
}

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)
.map(|n| numbering.apply(engine, context, &[n]))
.collect::<SourceResult<Array>>()?
.join(Some('\n'.into_value()), None)
.join(engine, span, Some('\n'.into_value()), None)
.at(span)
}

View File

@ -2,6 +2,60 @@
#test(type(1), int)
#test(type(ltr), direction)
#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 ---
// Let the error message report the type name.

View File

@ -322,6 +322,20 @@ A
#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 ---
// 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

View File

@ -255,6 +255,10 @@
// Warning: 17-21 unnecessary import rename to same name
#import enum as enum
--- import-rename-necessary ---
#import "module.typ" as module: a
#test(module.a, a)
--- import-rename-unnecessary-mixed ---
// Warning: 17-21 unnecessary import rename to same name
#import enum as enum: item
@ -263,10 +267,6 @@
// Warning: 31-35 unnecessary import rename to same name
#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 "modul" + "e.typ" as module
#test(module.b, 1)

View File

@ -77,11 +77,6 @@ I
#let var = text(font: ("list-of", "nonexistent-fonts"))[don't]
#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 ---
#let t = tiling(

View File

@ -658,3 +658,11 @@ $ A = mat(
height: 10pt,
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]
)
--- 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(
bytes((
@ -152,8 +163,8 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
#image("path/does/not/exist")
--- image-bad-format ---
// Error: 2-22 unknown image format
#image("./image.typ")
// Error: 2-37 unknown image format
#image("/assets/plugins/hello.wasm")
--- image-bad-svg ---
// Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4)