Compare commits

...

39 Commits

Author SHA1 Message Date
Laurenz
8ace67d942 Version bump 2025-03-07 11:13:08 +01:00
Laurenz
81e9bc7c8f 0.13.1 changelog (#6025) 2025-03-07 11:10:26 +01:00
Laurenz
381ff0cc2c Mark breaking symbol changes as breaking in 0.13.0 changelog (#6024) 2025-03-07 10:22:12 +01:00
Malo
393be881f8 Mention that sym.ohm was removed in the 0.13.0 changelog (#6017)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-07 10:22:12 +01:00
Laurenz
74826fc6ec Replace par function call in tutorial (#6023) 2025-03-07 09:53:39 +01:00
Laurenz
fe94b2b54f Hotfix for labels on symbols (#6015) 2025-03-07 09:53:39 +01:00
Andrew Voynov
e0074dfc01 Make array.chunks example more readable (#5975) 2025-03-06 16:30:59 +01:00
F2011
d97967dd40 Correct typo (#5971) 2025-03-06 16:30:59 +01:00
Tijme
9c41234574 Fix docs example with type/string comparison (#5987) 2025-03-06 16:30:59 +01:00
Emmanuel Lesueur
59569cbf61 Fix curve with multiple non-closed components. (#5963) 2025-02-26 21:21:15 +01:00
Laurenz
d04f014fc6 Fix paper name in page setup guide (#5956) 2025-02-26 21:21:15 +01:00
Laurenz
4a78a7d082
Fix false positive for type/str comparison warning (#5957) 2025-02-25 17:00:21 +01:00
aodenis
a754be513d Fix high CPU usage due to inotify watch triggering itself (#5905)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-25 15:28:14 +01:00
Laurenz
7d4010afad Fix introspection of HTML root sibling metadata (#5953) 2025-02-25 15:28:14 +01:00
Sharzy
4893eb501e HTML export: fix elem counting on classify_output (#5910)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-25 15:28:14 +01:00
Laurenz
20d4f8135a Fix comparison of Func and NativeFuncData (#5943) 2025-02-25 15:28:14 +01:00
PgBiel
a998775edc Fix HTML export of table with gutter (#5920) 2025-02-25 15:28:14 +01:00
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
84 changed files with 1311 additions and 553 deletions

50
Cargo.lock generated
View File

@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typst"
version = "0.12.0"
version = "0.13.1"
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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5bf0cc3c2265502b51fcb73147cc7c951ceb694507195b93c2ab0b901abb902"
[[package]]
name = "typst-cli"
version = "0.12.0"
version = "0.13.1"
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.1"
source = "git+https://github.com/typst/typst-dev-assets?tag=v0.13.1#9879589f4b3247b12c5e694d0d7fa86d4d8a198e"
[[package]]
name = "typst-docs"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"clap",
"ecow",
@ -2830,7 +2831,7 @@ dependencies = [
[[package]]
name = "typst-eval"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2848,7 +2849,7 @@ dependencies = [
[[package]]
name = "typst-fuzz"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"comemo",
"libfuzzer-sys",
@ -2860,7 +2861,7 @@ dependencies = [
[[package]]
name = "typst-html"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2874,7 +2875,7 @@ dependencies = [
[[package]]
name = "typst-ide"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"comemo",
"ecow",
@ -2891,7 +2892,7 @@ dependencies = [
[[package]]
name = "typst-kit"
version = "0.12.0"
version = "0.13.1"
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.1"
dependencies = [
"az",
"bumpalo",
@ -2942,7 +2945,7 @@ dependencies = [
[[package]]
name = "typst-library"
version = "0.12.0"
version = "0.13.1"
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.1"
dependencies = [
"heck",
"proc-macro2",
@ -3011,7 +3015,7 @@ dependencies = [
[[package]]
name = "typst-pdf"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"arrayvec",
"base64",
@ -3037,7 +3041,7 @@ dependencies = [
[[package]]
name = "typst-realize"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"arrayvec",
"bumpalo",
@ -3053,7 +3057,7 @@ dependencies = [
[[package]]
name = "typst-render"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"bytemuck",
"comemo",
@ -3069,7 +3073,7 @@ dependencies = [
[[package]]
name = "typst-svg"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"base64",
"comemo",
@ -3087,7 +3091,7 @@ dependencies = [
[[package]]
name = "typst-syntax"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"ecow",
"serde",
@ -3103,7 +3107,7 @@ dependencies = [
[[package]]
name = "typst-tests"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"clap",
"comemo",
@ -3128,7 +3132,7 @@ dependencies = [
[[package]]
name = "typst-timing"
version = "0.12.0"
version = "0.13.1"
dependencies = [
"parking_lot",
"serde",
@ -3138,7 +3142,7 @@ dependencies = [
[[package]]
name = "typst-utils"
version = "0.12.0"
version = "0.13.1"
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.1"
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.1" }
typst-cli = { path = "crates/typst-cli", version = "0.13.1" }
typst-eval = { path = "crates/typst-eval", version = "0.13.1" }
typst-html = { path = "crates/typst-html", version = "0.13.1" }
typst-ide = { path = "crates/typst-ide", version = "0.13.1" }
typst-kit = { path = "crates/typst-kit", version = "0.13.1" }
typst-layout = { path = "crates/typst-layout", version = "0.13.1" }
typst-library = { path = "crates/typst-library", version = "0.13.1" }
typst-macros = { path = "crates/typst-macros", version = "0.13.1" }
typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" }
typst-realize = { path = "crates/typst-realize", version = "0.13.1" }
typst-render = { path = "crates/typst-render", version = "0.13.1" }
typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = "0.13.1"
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.1" }
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

@ -204,6 +204,10 @@ impl Watcher {
let event = event
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
if !is_relevant_event_kind(&event.kind) {
continue;
}
// Workaround for notify-rs' implicit unwatch on remove/rename
// (triggered by some editors when saving files) with the
// inotify backend. By keeping track of the potentially
@ -224,7 +228,17 @@ impl Watcher {
}
}
relevant |= self.is_event_relevant(&event);
// Don't recompile because the output file changed.
// FIXME: This doesn't work properly for multifile image export.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
continue;
}
relevant = true;
}
// If we found a relevant event or if any of the missing files now
@ -234,32 +248,23 @@ impl Watcher {
}
}
}
}
/// Whether a watch event is relevant for compilation.
fn is_event_relevant(&self, event: &notify::Event) -> bool {
// Never recompile because the output file changed.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
return false;
}
match &event.kind {
notify::EventKind::Any => true,
notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true,
notify::EventKind::Modify(kind) => match kind {
notify::event::ModifyKind::Any => true,
notify::event::ModifyKind::Data(_) => true,
notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
notify::event::ModifyKind::Other => false,
},
notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false,
}
/// Whether a kind of watch event is relevant for compilation.
fn is_relevant_event_kind(kind: &notify::EventKind) -> bool {
match kind {
notify::EventKind::Any => true,
notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true,
notify::EventKind::Modify(kind) => match kind {
notify::event::ModifyKind::Any => true,
notify::event::ModifyKind::Data(_) => true,
notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
notify::event::ModifyKind::Other => false,
},
notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false,
}
}

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

@ -83,8 +83,8 @@ fn html_document_impl(
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector })
}
@ -307,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len();
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, len) {
match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag
elem.tag,
),
_ => {}
}

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,40 +121,20 @@ 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 !config.first_line_indent.is_zero() {
collector.push_item(Item::Absolute(config.first_line_indent, false));
collector.spans.push(1, Span::detached());
}
if let Some(situation) = situation {
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,
));
collector.spans.push(1, Span::detached());
}
let hang = ParElem::hanging_indent_in(styles);
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached());
}
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 {
@ -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(|| {
let (_, item) = p.get(offset);
let styles = item.text()?.styles;
Some(TextElem::hyphenate_in(styles))
})
.unwrap_or(false)
p.config.hyphenate.unwrap_or_else(|| {
let (_, item) = p.get(offset);
match item.text() {
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
None => 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

@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> {
self.last_point = point;
self.last_control_from = point;
self.is_started = true;
self.is_empty = true;
}
/// Add a line segment.
@ -1281,7 +1282,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 +1306,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)
@ -751,7 +769,7 @@ impl Array {
///
/// ```example
/// #let array = (1, 2, 3, 4, 5, 6, 7, 8)
/// #array.chunks(3)
/// #array.chunks(3) \
/// #array.chunks(3, exact: true)
/// ```
#[func]
@ -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

@ -437,10 +437,10 @@ impl PartialEq for Func {
}
}
impl PartialEq<&NativeFuncData> for Func {
fn eq(&self, other: &&NativeFuncData) -> bool {
impl PartialEq<&'static NativeFuncData> for Func {
fn eq(&self, other: &&'static NativeFuncData) -> bool {
match &self.repr {
Repr::Native(native) => native.function == other.function,
Repr::Native(native) => *native == Static(*other),
_ => false,
}
}

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, str);
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,90 @@ 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, s: &str) {
// Only warn if `s` looks like a type name to prevent false positives.
if is_compat_type_name(s) {
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"],
);
}
fn is_compat_type_name(s: &str) -> bool {
matches!(
s,
"boolean"
| "alignment"
| "angle"
| "arguments"
| "array"
| "bytes"
| "color"
| "content"
| "counter"
| "datetime"
| "decimal"
| "dictionary"
| "direction"
| "duration"
| "float"
| "fraction"
| "function"
| "gradient"
| "integer"
| "label"
| "length"
| "location"
| "module"
| "pattern"
| "ratio"
| "regex"
| "relative length"
| "selector"
| "state"
| "string"
| "stroke"
| "symbol"
| "tiling"
| "type"
| "version"
)
}

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

@ -10,7 +10,7 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector};
use crate::html::{HtmlElement, HtmlNode};
use crate::html::HtmlNode;
use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering;
@ -55,8 +55,8 @@ impl Introspector {
/// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")]
pub fn html(root: &HtmlElement) -> Self {
IntrospectorBuilder::new().build_html(root)
pub fn html(output: &[HtmlNode]) -> Self {
IntrospectorBuilder::new().build_html(output)
}
/// Iterates over all locatable elements.
@ -392,9 +392,9 @@ impl IntrospectorBuilder {
}
/// Build an introspector for an HTML document.
fn build_html(mut self, root: &HtmlElement) -> Introspector {
fn build_html(mut self, output: &[HtmlNode]) -> Introspector {
let mut elems = Vec::new();
self.discover_in_html(&mut elems, root);
self.discover_in_html(&mut elems, output);
self.finalize(elems)
}
@ -434,16 +434,16 @@ impl IntrospectorBuilder {
}
/// Processes the tags in the HTML element.
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) {
for child in &elem.children {
match child {
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, nodes: &[HtmlNode]) {
for node in nodes {
match node {
HtmlNode::Tag(tag) => self.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
),
HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => self.discover_in_html(sink, elem),
HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame(
sink,
frame,

View File

@ -1526,11 +1526,7 @@ impl<'a> CellGrid<'a> {
self.entry(x, y).map(|entry| match entry {
Entry::Cell(_) => Axes::new(x, y),
Entry::Merged { parent } => {
let c = if self.has_gutter {
1 + self.cols.len() / 2
} else {
self.cols.len()
};
let c = self.non_gutter_column_count();
let factor = if self.has_gutter { 2 } else { 1 };
Axes::new(factor * (*parent % c), factor * (*parent / c))
}
@ -1602,6 +1598,21 @@ impl<'a> CellGrid<'a> {
cell.rowspan.get()
}
}
#[inline]
pub fn non_gutter_column_count(&self) -> usize {
if self.has_gutter {
// Calculation: With gutters, we have
// 'cols = 2 * (non-gutter cols) - 1', since there is a gutter
// column between each regular column. Therefore,
// 'floor(cols / 2)' will be equal to
// 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1',
// so 'non-gutter cols = 1 + floor(cols / 2)'.
1 + self.cols.len() / 2
} else {
self.cols.len()
}
}
}
/// Given a cell's requested x and y, the vector with the resolved cell

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

@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable};
/// let author = find-child(elem, "author")
/// let pars = find-child(elem, "content")
///
/// heading(title.children.first())
/// [= #title.children.first()]
/// text(10pt, weight: "medium")[
/// Published by
/// #author.children.first()
/// ]
///
/// for p in pars.children {
/// if (type(p) == "dictionary") {
/// if type(p) == dictionary {
/// parbreak()
/// p.children.first()
/// }
@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable};
///
/// #let data = xml("example.xml")
/// #for elem in data.first().children {
/// if (type(elem) == "dictionary") {
/// if type(elem) == dictionary {
/// article(elem)
/// }
/// }
@ -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

@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect();
let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
let tr = |tag, row: &[Entry]| {
let row = row

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,10 +446,14 @@ impl Show for Packed<RawElem> {
let mut realized = Content::sequence(seq);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::pre)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
return Ok(HtmlElem::new(if self.block(styles) {
tag::pre
} else {
tag::code
})
.with_body(Some(realized))
.pack()
.spanned(self.span()));
}
if self.block(styles) {
@ -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

@ -326,7 +326,10 @@ fn visit_math_rules<'a>(
// Symbols in non-math content transparently convert to `TextElem` so we
// don't have to handle them in non-math layout.
if let Some(elem) = content.to_packed::<SymbolElem>() {
let text = TextElem::packed(elem.text).spanned(elem.span());
let mut text = TextElem::packed(elem.text).spanned(elem.span());
if let Some(label) = elem.label() {
text.set_label(label);
}
visit(s, s.store(text), styles)?;
return Ok(true);
}

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
@ -289,20 +294,20 @@ feature flag.
`errorbar.diamond.stroked`, `errorbar.diamond.filled`,
`errorbar.circle.stroked`, `errorbar.circle.filled`
- `numero`
- `Omega.inv`
- Renamed
- Renamed **(Breaking change)**
- `ohm.inv` to `Omega.inv`
- Changed codepoint
- Changed codepoint **(Breaking change)**
- `angle.l.double` from `《` to `⟪`
- `angle.r.double` from `》` to `⟫`
- `angstrom` from U+212B (`Å`) to U+00C5 (`Å`)
- Deprecated
- `sect` and all its variants in favor of `inter`
- `integral.sect` in favor of `integral.inter`
- Removed
- Removed **(Breaking change)**
- `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math)
- `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math)
- `kelvin` in favor of just K (`[$upright(K)$]` in math)
- `ohm` in favor of `Omega`
## Deprecations
- The [`path`] function in favor of the [`curve`] function
@ -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" />

29
docs/changelog/0.13.1.md Normal file
View File

@ -0,0 +1,29 @@
---
title: 0.13.1
description: Changes in Typst 0.13.1
---
# Version 0.13.1 (March 7, 2025)
## Command Line Interface
- Fixed high CPU usage for `typst watch` on Linux. Depending on the project
size, CPU usage would spike for varying amounts of time. This bug appeared
with 0.13.0 due to a behavioral change in the inotify file watching backend.
## HTML export
- Fixed export of tables with [gutters]($table.gutter)
- Fixed usage of `<html>` and `<body>` element within [context]
- Fixed querying of [metadata] next to `<html>` and `<body>` element
## Visualization
- Fixed [curves]($curve) with multiple non-closed components
## Introspection
- Fixed a regression where labelled [symbols]($symbol) could not be
[queried]($query) by label
## Deprecations
- Fixed false positives in deprecation warnings for type/str comparisons
## Contributors
<contributors from="v0.13.0" to="v0.13.1" />

View File

@ -10,7 +10,8 @@ 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.1]($changelog/0.13.1)
- [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

@ -447,7 +447,7 @@ document.
To let a function style your whole document, the show rule processes everything
that comes after it and calls the function specified after the colon with the
result as an argument. The `.with` part is a _method_ that takes the `conf`
function and pre-configures some if its arguments before passing it on to the
function and pre-configures some of its arguments before passing it on to the
show rule.
</div>

View File

@ -56,7 +56,7 @@ requirements with examples.
Typst's default page size is A4 paper. Depending on your region and your use
case, you will want to change this. You can do this by using the
[`{page}`]($page) set rule and passing it a string argument to use a common page
size. Options include the complete ISO 216 series (e.g. `"iso-a4"`, `"iso-c2"`),
size. Options include the complete ISO 216 series (e.g. `"a4"` and `"iso-c2"`),
customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the
reference for the [page's paper argument]($page.paper) to learn about all
available options.

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

@ -188,6 +188,7 @@ fn changelog_pages(resolver: &dyn Resolver) -> PageModel {
let mut page = md_page(resolver, resolver.base(), load!("changelog/welcome.md"));
let base = format!("{}changelog/", resolver.base());
page.children = vec![
md_page(resolver, &base, load!("changelog/0.13.1.md")),
md_page(resolver, &base, load!("changelog/0.13.0.md")),
md_page(resolver, &base, load!("changelog/0.12.0.md")),
md_page(resolver, &base, load!("changelog/0.11.1.md")),
@ -550,8 +551,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 +559,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
.iter()
.map(|func| {
let id = urlify(&eco_format!("definitions-{}", func.name));
let children = func_outline(func, &id);
OutlineItem { id, name: func.title.into(), children }
})
.collect(),
})
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!("{id}-{}", func.name));
let children = func_outline(func, &id);
OutlineItem { id, name: func.title.into(), children }
})
.collect();
Some(OutlineItem { id, name: "Definitions".into(), children })
}
/// Create a page for a group of functions.
@ -687,7 +689,7 @@ fn type_outline(model: &TypeModel) -> Vec<OutlineItem> {
});
}
outline.extend(scope_outline(&model.scope));
outline.extend(scope_outline(&model.scope, ""));
outline
}

View File

@ -13,11 +13,11 @@ your report using Typst's styling system.
As we have seen in the previous chapter, Typst has functions that _insert_
content (e.g. the [`image`] function) and others that _manipulate_ content that
they received as arguments (e.g. the [`align`] function). The first impulse you
might have when you want, for example, to justify the report, could be to look
might have when you want, for example, to change the font, could be to look
for a function that does that and wrap the complete document in it.
```example
#par(justify: true)[
#text(font: "New Computer Modern")[
= Background
In the case of glaciers, fluid
dynamics principles can be used
@ -37,9 +37,9 @@ do in Typst, there is special syntax for it: Instead of putting the content
inside of the argument list, you can write it in square brackets directly after
the normal arguments, saving on punctuation.
As seen above, that works. The [`par`] function justifies all paragraphs within
it. However, wrapping the document in countless functions and applying styles
selectively and in-situ can quickly become cumbersome.
As seen above, that works. With the [`text`] function, we can adjust the font
for all text within it. However, wrapping the document in countless functions
and applying styles selectively and in-situ can quickly become cumbersome.
Fortunately, Typst has a more elegant solution. With _set rules,_ you can apply
style properties to all occurrences of some kind of content. You write a set
@ -47,7 +47,9 @@ rule by entering the `{set}` keyword, followed by the name of the function whose
properties you want to set, and a list of arguments in parentheses.
```example
#set par(justify: true)
#set text(
font: "New Computer Modern"
)
= Background
In the case of glaciers, fluid

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html></html>

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html>Hi</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

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,62 @@
#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)
// This is not a warning.
#test(type(10) in ("any", str, int), 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.

15
tests/suite/html/elem.typ Normal file
View File

@ -0,0 +1,15 @@
--- html-elem-alone-context html ---
#context html.elem("html")
--- html-elem-not-alone html ---
// Error: 2-19 `<html>` element must be the only element in the document
#html.elem("html")
Text
--- html-elem-metadata html ---
#html.elem("html", context {
let val = query(<l>).first().value
test(val, "Hi")
val
})
#metadata("Hi") <l>

View File

@ -30,3 +30,30 @@
[row],
),
)
--- col-gutter-table html ---
#table(
columns: 3,
column-gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
--- row-gutter-table html ---
#table(
columns: 3,
row-gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
--- col-row-gutter-table html ---
#table(
columns: 3,
gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)

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

@ -151,3 +151,7 @@
--- symbol-sect-deprecated ---
// Warning: 5-9 `sect` is deprecated, use `inter` instead
$ A sect B = A inter B $
--- issue-5930-symbol-label ---
#emoji.face<lab>
#context test(query(<lab>).first().text, "😀")

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

@ -38,6 +38,16 @@
curve.close(mode: "smooth"),
)
--- curve-multiple-non-closed ---
#curve(
stroke: 2pt,
curve.line((20pt, 0pt)),
curve.move((0pt, 10pt)),
curve.line((20pt, 10pt)),
curve.move((0pt, 20pt)),
curve.line((20pt, 20pt)),
)
--- curve-line ---
#curve(
fill: purple,

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)