mirror of
https://github.com/typst/typst
synced 2025-05-19 11:35:27 +08:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8dce676dcd | ||
|
c02cb70f27 | ||
|
0a534f2c0e | ||
|
de16a2ced1 | ||
|
d48708c5d5 | ||
|
e294fe85a5 | ||
|
2f1a5ab914 | ||
|
c247dbc42d | ||
|
c259545c6e | ||
|
e470ccff19 | ||
|
024bbb2b46 | ||
|
93fe02b457 | ||
|
9c3ecf43a0 | ||
|
ab5e356d81 | ||
|
88f88016e0 | ||
|
72060d0142 | ||
|
20dd19c64e | ||
|
f64d029fe6 | ||
|
c417b17442 | ||
|
c2316b9a3e | ||
|
d8b79b5b9b | ||
|
56d8188c61 |
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "typst"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"comemo",
|
||||
"ecow",
|
||||
@ -2752,12 +2752,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-assets"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1051c56bbbf74d31ea6c6b1661e62fa0ebb8104403ee53f6dcd321600426e0b6"
|
||||
|
||||
[[package]]
|
||||
name = "typst-cli"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@ -2802,12 +2803,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-dev-assets"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/typst/typst-dev-assets?tag=v0.13.0#61aebe9575a5abff889f76d73c7b01dc8e17e340"
|
||||
|
||||
[[package]]
|
||||
name = "typst-docs"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ecow",
|
||||
@ -2830,7 +2831,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-eval"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"comemo",
|
||||
"ecow",
|
||||
@ -2848,7 +2849,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-fuzz"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"comemo",
|
||||
"libfuzzer-sys",
|
||||
@ -2860,7 +2861,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-html"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"comemo",
|
||||
"ecow",
|
||||
@ -2874,7 +2875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-ide"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"comemo",
|
||||
"ecow",
|
||||
@ -2891,7 +2892,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-kit"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"ecow",
|
||||
@ -2901,6 +2902,8 @@ dependencies = [
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"typst-assets",
|
||||
"typst-library",
|
||||
@ -2912,7 +2915,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-layout"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"az",
|
||||
"bumpalo",
|
||||
@ -2942,7 +2945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-library"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"az",
|
||||
"bitflags 2.8.0",
|
||||
@ -2964,6 +2967,7 @@ dependencies = [
|
||||
"kamadak-exif",
|
||||
"kurbo",
|
||||
"lipsum",
|
||||
"memchr",
|
||||
"palette",
|
||||
"phf",
|
||||
"png",
|
||||
@ -3001,7 +3005,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-macros"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@ -3011,7 +3015,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-pdf"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"base64",
|
||||
@ -3037,7 +3041,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-realize"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bumpalo",
|
||||
@ -3053,7 +3057,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-render"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"comemo",
|
||||
@ -3069,7 +3073,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-svg"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"comemo",
|
||||
@ -3087,7 +3091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-syntax"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"ecow",
|
||||
"serde",
|
||||
@ -3103,7 +3107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-tests"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"comemo",
|
||||
@ -3128,7 +3132,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-timing"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
"serde",
|
||||
@ -3138,7 +3142,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typst-utils"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"portable-atomic",
|
||||
|
39
Cargo.toml
39
Cargo.toml
@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
rust-version = "1.80" # also change in ci.yml
|
||||
authors = ["The Typst Project Developers"]
|
||||
edition = "2021"
|
||||
@ -16,24 +16,24 @@ keywords = ["typst"]
|
||||
readme = "README.md"
|
||||
|
||||
[workspace.dependencies]
|
||||
typst = { path = "crates/typst", version = "0.12.0" }
|
||||
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
|
||||
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
|
||||
typst-html = { path = "crates/typst-html", version = "0.12.0" }
|
||||
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
|
||||
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
|
||||
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
|
||||
typst-library = { path = "crates/typst-library", version = "0.12.0" }
|
||||
typst-macros = { path = "crates/typst-macros", version = "0.12.0" }
|
||||
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" }
|
||||
typst-realize = { path = "crates/typst-realize", version = "0.12.0" }
|
||||
typst-render = { path = "crates/typst-render", version = "0.12.0" }
|
||||
typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
|
||||
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
|
||||
typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
|
||||
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
|
||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
|
||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" }
|
||||
typst = { path = "crates/typst", version = "0.13.0" }
|
||||
typst-cli = { path = "crates/typst-cli", version = "0.13.0" }
|
||||
typst-eval = { path = "crates/typst-eval", version = "0.13.0" }
|
||||
typst-html = { path = "crates/typst-html", version = "0.13.0" }
|
||||
typst-ide = { path = "crates/typst-ide", version = "0.13.0" }
|
||||
typst-kit = { path = "crates/typst-kit", version = "0.13.0" }
|
||||
typst-layout = { path = "crates/typst-layout", version = "0.13.0" }
|
||||
typst-library = { path = "crates/typst-library", version = "0.13.0" }
|
||||
typst-macros = { path = "crates/typst-macros", version = "0.13.0" }
|
||||
typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" }
|
||||
typst-realize = { path = "crates/typst-realize", version = "0.13.0" }
|
||||
typst-render = { path = "crates/typst-render", version = "0.13.0" }
|
||||
typst-svg = { path = "crates/typst-svg", version = "0.13.0" }
|
||||
typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" }
|
||||
typst-timing = { path = "crates/typst-timing", version = "0.13.0" }
|
||||
typst-utils = { path = "crates/typst-utils", version = "0.13.0" }
|
||||
typst-assets = "0.13.0"
|
||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.0" }
|
||||
arrayvec = "0.7.4"
|
||||
az = "1.2"
|
||||
base64 = "0.22"
|
||||
@ -73,6 +73,7 @@ kamadak-exif = "0.6"
|
||||
kurbo = "0.11"
|
||||
libfuzzer-sys = "0.4"
|
||||
lipsum = "0.9"
|
||||
memchr = "2"
|
||||
miniz_oxide = "0.8"
|
||||
native-tls = "0.2"
|
||||
notify = "8"
|
||||
|
@ -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, ¤t_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})")
|
||||
})
|
||||
|
@ -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);
|
||||
|
@ -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(_)) => {
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
@ -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]";
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -107,7 +107,6 @@ fn layout_inline_text(
|
||||
styles,
|
||||
Size::splat(Abs::inf()),
|
||||
false,
|
||||
None,
|
||||
)?
|
||||
.into_frame();
|
||||
|
||||
|
@ -1281,7 +1281,7 @@ impl ControlPoints {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to draw arcs with bezier curves.
|
||||
/// Helper to draw arcs with Bézier curves.
|
||||
trait CurveExt {
|
||||
fn arc(&mut self, start: Point, center: Point, end: Point);
|
||||
fn arc_move(&mut self, start: Point, center: Point, end: Point);
|
||||
@ -1305,7 +1305,7 @@ impl CurveExt for Curve {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the control points for a bezier curve that approximates a circular arc for
|
||||
/// Get the control points for a Bé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] {
|
||||
|
@ -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 }
|
||||
|
@ -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).
|
||||
|
@ -9,7 +9,9 @@ use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use typst_syntax::{Span, Spanned};
|
||||
|
||||
use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
||||
use crate::diag::{
|
||||
bail, At, DeprecationSink, HintedStrResult, SourceDiagnostic, SourceResult, StrResult,
|
||||
};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue,
|
||||
@ -143,6 +145,11 @@ impl Array {
|
||||
|
||||
Ok(self.iter().cloned().cycle().take(count).collect())
|
||||
}
|
||||
|
||||
/// The internal implementation of [`Array::contains`].
|
||||
pub fn contains_impl(&self, value: &Value, sink: &mut dyn DeprecationSink) -> bool {
|
||||
self.0.iter().any(|v| ops::equal(v, value, sink))
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -290,10 +297,12 @@ impl Array {
|
||||
#[func]
|
||||
pub fn contains(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
span: Span,
|
||||
/// The value to search for.
|
||||
value: Value,
|
||||
) -> bool {
|
||||
self.0.contains(&value)
|
||||
self.contains_impl(&value, &mut (engine, span))
|
||||
}
|
||||
|
||||
/// Searches for an item for which the given function returns `{true}` and
|
||||
@ -576,6 +585,8 @@ impl Array {
|
||||
#[func]
|
||||
pub fn sum(
|
||||
self,
|
||||
engine: &mut Engine,
|
||||
span: Span,
|
||||
/// What to return if the array is empty. Must be set if the array can
|
||||
/// be empty.
|
||||
#[named]
|
||||
@ -587,7 +598,7 @@ impl Array {
|
||||
.or(default)
|
||||
.ok_or("cannot calculate sum of empty array with no default")?;
|
||||
for item in iter {
|
||||
acc = ops::add(acc, item)?;
|
||||
acc = ops::add(acc, item, &mut (&mut *engine, span))?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
@ -686,6 +697,8 @@ impl Array {
|
||||
#[func]
|
||||
pub fn join(
|
||||
self,
|
||||
engine: &mut Engine,
|
||||
span: Span,
|
||||
/// A value to insert between each item of the array.
|
||||
#[default]
|
||||
separator: Option<Value>,
|
||||
@ -701,13 +714,18 @@ impl Array {
|
||||
for (i, value) in self.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
if i + 1 == len && last.is_some() {
|
||||
result = ops::join(result, last.take().unwrap())?;
|
||||
result = ops::join(
|
||||
result,
|
||||
last.take().unwrap(),
|
||||
&mut (&mut *engine, span),
|
||||
)?;
|
||||
} else {
|
||||
result = ops::join(result, separator.clone())?;
|
||||
result =
|
||||
ops::join(result, separator.clone(), &mut (&mut *engine, span))?;
|
||||
}
|
||||
}
|
||||
|
||||
result = ops::join(result, value)?;
|
||||
result = ops::join(result, value, &mut (&mut *engine, span))?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
@ -862,13 +880,14 @@ impl Array {
|
||||
self,
|
||||
engine: &mut Engine,
|
||||
context: Tracked<Context>,
|
||||
span: Span,
|
||||
/// If given, applies this function to the elements in the array to
|
||||
/// determine the keys to deduplicate by.
|
||||
#[named]
|
||||
key: Option<Func>,
|
||||
) -> SourceResult<Array> {
|
||||
let mut out = EcoVec::with_capacity(self.0.len());
|
||||
let mut key_of = |x: Value| match &key {
|
||||
let key_of = |engine: &mut Engine, x: Value| match &key {
|
||||
// NOTE: We are relying on `comemo`'s memoization of function
|
||||
// evaluation to not excessively reevaluate the `key`.
|
||||
Some(f) => f.call(engine, context, [x]),
|
||||
@ -879,14 +898,18 @@ impl Array {
|
||||
// 1. We would like to preserve the order of the elements.
|
||||
// 2. We cannot hash arbitrary `Value`.
|
||||
'outer: for value in self {
|
||||
let key = key_of(value.clone())?;
|
||||
let key = key_of(&mut *engine, value.clone())?;
|
||||
if out.is_empty() {
|
||||
out.push(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
for second in out.iter() {
|
||||
if ops::equal(&key, &key_of(second.clone())?) {
|
||||
if ops::equal(
|
||||
&key,
|
||||
&key_of(&mut *engine, second.clone())?,
|
||||
&mut (&mut *engine, span),
|
||||
) {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -5,7 +5,7 @@ use std::cmp::Ordering;
|
||||
use ecow::eco_format;
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use crate::diag::{bail, HintedStrResult, StrResult};
|
||||
use crate::diag::{bail, DeprecationSink, HintedStrResult, StrResult};
|
||||
use crate::foundations::{
|
||||
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
|
||||
};
|
||||
@ -21,7 +21,7 @@ macro_rules! mismatch {
|
||||
}
|
||||
|
||||
/// Join a value with another value.
|
||||
pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
pub fn join(lhs: Value, rhs: Value, sink: &mut dyn DeprecationSink) -> StrResult<Value> {
|
||||
use Value::*;
|
||||
Ok(match (lhs, rhs) {
|
||||
(a, None) => a,
|
||||
@ -39,6 +39,17 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
(Args(a), Args(b)) => Args(a + b),
|
||||
|
||||
// Type compatibility.
|
||||
(Type(a), Str(b)) => {
|
||||
warn_type_str_join(sink);
|
||||
Str(format_str!("{a}{b}"))
|
||||
}
|
||||
(Str(a), Type(b)) => {
|
||||
warn_type_str_join(sink);
|
||||
Str(format_str!("{a}{b}"))
|
||||
}
|
||||
|
||||
(a, b) => mismatch!("cannot join {} with {}", a, b),
|
||||
})
|
||||
}
|
||||
@ -88,7 +99,11 @@ pub fn neg(value: Value) -> HintedStrResult<Value> {
|
||||
}
|
||||
|
||||
/// Compute the sum of two values.
|
||||
pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
pub fn add(
|
||||
lhs: Value,
|
||||
rhs: Value,
|
||||
sink: &mut dyn DeprecationSink,
|
||||
) -> HintedStrResult<Value> {
|
||||
use Value::*;
|
||||
Ok(match (lhs, rhs) {
|
||||
(a, None) => a,
|
||||
@ -156,6 +171,16 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
(Datetime(a), Duration(b)) => Datetime(a + b),
|
||||
(Duration(a), Datetime(b)) => Datetime(b + a),
|
||||
|
||||
// Type compatibility.
|
||||
(Type(a), Str(b)) => {
|
||||
warn_type_str_add(sink);
|
||||
Str(format_str!("{a}{b}"))
|
||||
}
|
||||
(Str(a), Type(b)) => {
|
||||
warn_type_str_add(sink);
|
||||
Str(format_str!("{a}{b}"))
|
||||
}
|
||||
|
||||
(Dyn(a), Dyn(b)) => {
|
||||
// Alignments can be summed.
|
||||
if let (Some(&a), Some(&b)) =
|
||||
@ -394,13 +419,21 @@ pub fn or(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
}
|
||||
|
||||
/// Compute whether two values are equal.
|
||||
pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
Ok(Value::Bool(equal(&lhs, &rhs)))
|
||||
pub fn eq(
|
||||
lhs: Value,
|
||||
rhs: Value,
|
||||
sink: &mut dyn DeprecationSink,
|
||||
) -> HintedStrResult<Value> {
|
||||
Ok(Value::Bool(equal(&lhs, &rhs, sink)))
|
||||
}
|
||||
|
||||
/// Compute whether two values are unequal.
|
||||
pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
Ok(Value::Bool(!equal(&lhs, &rhs)))
|
||||
pub fn neq(
|
||||
lhs: Value,
|
||||
rhs: Value,
|
||||
sink: &mut dyn DeprecationSink,
|
||||
) -> HintedStrResult<Value> {
|
||||
Ok(Value::Bool(!equal(&lhs, &rhs, sink)))
|
||||
}
|
||||
|
||||
macro_rules! comparison {
|
||||
@ -419,7 +452,7 @@ comparison!(gt, ">", Ordering::Greater);
|
||||
comparison!(geq, ">=", Ordering::Greater | Ordering::Equal);
|
||||
|
||||
/// Determine whether two values are equal.
|
||||
pub fn equal(lhs: &Value, rhs: &Value) -> bool {
|
||||
pub fn equal(lhs: &Value, rhs: &Value, sink: &mut dyn DeprecationSink) -> bool {
|
||||
use Value::*;
|
||||
match (lhs, rhs) {
|
||||
// Compare reflexively.
|
||||
@ -463,6 +496,12 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
|
||||
rat == rel.rel && rel.abs.is_zero()
|
||||
}
|
||||
|
||||
// Type compatibility.
|
||||
(Type(ty), Str(str)) | (Str(str), Type(ty)) => {
|
||||
warn_type_str_equal(sink);
|
||||
ty.compat_name() == str.as_str()
|
||||
}
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -534,8 +573,12 @@ fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult<Ordering> {
|
||||
}
|
||||
|
||||
/// Test whether one value is "in" another one.
|
||||
pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
if let Some(b) = contains(&lhs, &rhs) {
|
||||
pub fn in_(
|
||||
lhs: Value,
|
||||
rhs: Value,
|
||||
sink: &mut dyn DeprecationSink,
|
||||
) -> HintedStrResult<Value> {
|
||||
if let Some(b) = contains(&lhs, &rhs, sink) {
|
||||
Ok(Value::Bool(b))
|
||||
} else {
|
||||
mismatch!("cannot apply 'in' to {} and {}", lhs, rhs)
|
||||
@ -543,8 +586,12 @@ pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
}
|
||||
|
||||
/// Test whether one value is "not in" another one.
|
||||
pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
if let Some(b) = contains(&lhs, &rhs) {
|
||||
pub fn not_in(
|
||||
lhs: Value,
|
||||
rhs: Value,
|
||||
sink: &mut dyn DeprecationSink,
|
||||
) -> HintedStrResult<Value> {
|
||||
if let Some(b) = contains(&lhs, &rhs, sink) {
|
||||
Ok(Value::Bool(!b))
|
||||
} else {
|
||||
mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs)
|
||||
@ -552,13 +599,27 @@ pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
}
|
||||
|
||||
/// Test for containment.
|
||||
pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
|
||||
pub fn contains(
|
||||
lhs: &Value,
|
||||
rhs: &Value,
|
||||
sink: &mut dyn DeprecationSink,
|
||||
) -> Option<bool> {
|
||||
use Value::*;
|
||||
match (lhs, rhs) {
|
||||
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
|
||||
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
|
||||
(Str(a), Dict(b)) => Some(b.contains(a)),
|
||||
(a, Array(b)) => Some(b.contains(a.clone())),
|
||||
(a, Array(b)) => Some(b.contains_impl(a, sink)),
|
||||
|
||||
// Type compatibility.
|
||||
(Type(a), Str(b)) => {
|
||||
warn_type_in_str(sink);
|
||||
Some(b.as_str().contains(a.compat_name()))
|
||||
}
|
||||
(Type(a), Dict(b)) => {
|
||||
warn_type_in_dict(sink);
|
||||
Some(b.contains(a.compat_name()))
|
||||
}
|
||||
|
||||
_ => Option::None,
|
||||
}
|
||||
@ -568,3 +629,46 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
|
||||
fn too_large() -> &'static str {
|
||||
"value is too large"
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn warn_type_str_add(sink: &mut dyn DeprecationSink) {
|
||||
sink.emit_with_hints(
|
||||
"adding strings and types is deprecated",
|
||||
&["convert the type to a string with `str` first"],
|
||||
);
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn warn_type_str_join(sink: &mut dyn DeprecationSink) {
|
||||
sink.emit_with_hints(
|
||||
"joining strings and types is deprecated",
|
||||
&["convert the type to a string with `str` first"],
|
||||
);
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn warn_type_str_equal(sink: &mut dyn DeprecationSink) {
|
||||
sink.emit_with_hints(
|
||||
"comparing strings with types is deprecated",
|
||||
&[
|
||||
"compare with the literal type instead",
|
||||
"this comparison will always return `false` in future Typst releases",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn warn_type_in_str(sink: &mut dyn DeprecationSink) {
|
||||
sink.emit_with_hints(
|
||||
"checking whether a type is contained in a string is deprecated",
|
||||
&["this compatibility behavior only exists because `type` used to return a string"],
|
||||
);
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn warn_type_in_dict(sink: &mut dyn DeprecationSink) {
|
||||
sink.emit_with_hints(
|
||||
"checking whether a type is contained in a dictionary is deprecated",
|
||||
&["this compatibility behavior only exists because `type` used to return a string"],
|
||||
);
|
||||
}
|
||||
|
@ -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)?;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 ())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)?;
|
||||
|
@ -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.
|
||||
|
@ -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)?;
|
||||
|
@ -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)?;
|
||||
|
@ -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)?;
|
||||
|
@ -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)?;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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: {}",
|
||||
|
@ -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)));
|
||||
|
@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> {
|
||||
"el" => ("‘", "’", "«", "»"),
|
||||
"he" => ("’", "’", "”", "”"),
|
||||
"hr" => ("‘", "’", "„", "”"),
|
||||
"bg" => ("’", "’", "„", "“"),
|
||||
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
|
||||
_ => default,
|
||||
};
|
||||
|
@ -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>);
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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 Bé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)]
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
title: Unreleased changes planned for 0.13.0
|
||||
description: Changes slated to appear in Typst 0.13.0
|
||||
title: 0.13.0
|
||||
description: Changes in Typst 0.13.0
|
||||
---
|
||||
|
||||
# Unreleased
|
||||
# Version 0.13.0 (February 19, 2025)
|
||||
|
||||
## Highlights
|
||||
- There is now a distinction between [proper paragraphs]($par) and just
|
||||
@ -16,7 +16,7 @@ description: Changes slated to appear in Typst 0.13.0
|
||||
- The `image` function now supports raw [pixel raster formats]($image.format)
|
||||
for generating images from within Typst
|
||||
- Functions that accept [file paths]($syntax/#paths) now also accept raw
|
||||
[bytes] instead, for full flexibility
|
||||
[bytes], for full flexibility
|
||||
- WebAssembly [plugins]($plugin) are more flexible and automatically run
|
||||
multi-threaded
|
||||
- Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`)
|
||||
@ -45,6 +45,7 @@ description: Changes slated to appear in Typst 0.13.0
|
||||
result in a warning
|
||||
- The default show rules of various built-in elements like lists, quotes, etc.
|
||||
were adjusted to ensure they produce/don't produce paragraphs as appropriate
|
||||
- Removed support for booleans and content in [`outline.indent`]
|
||||
- The [`outline`] function was fully reworked to improve its out-of-the-box
|
||||
behavior **(Breaking change)**
|
||||
- [Outline entries]($outline.entry) are now [blocks]($block) and are thus
|
||||
@ -98,8 +99,9 @@ description: Changes slated to appear in Typst 0.13.0
|
||||
- Fixed interaction of clipping and outset on [`box`] and [`block`]
|
||||
- Fixed panic with [`path`] of infinite length
|
||||
- Fixed non-solid (e.g. tiling) text fills in clipped blocks
|
||||
- Auto-detection of image formats from a raw buffer now has basic support for
|
||||
SVGs
|
||||
- Fixed a crash for images with a DPI value of zero
|
||||
- Fixed floating-point error in [`gradient.repeat`]
|
||||
- Auto-detection of image formats from a raw buffer now has support for SVGs
|
||||
|
||||
## Scripting
|
||||
- Functions that accept [file paths]($syntax/#paths) now also accept raw
|
||||
@ -155,7 +157,7 @@ description: Changes slated to appear in Typst 0.13.0
|
||||
- Fixed multi-line annotations (e.g. overbrace) changing the math baseline
|
||||
- Fixed merging of attachments when the base is a nested equation
|
||||
- Fixed resolving of contextual (em-based) text sizes within math
|
||||
- Fixed spacing around ⊥
|
||||
- Fixed spacing around up tacks (⊥)
|
||||
|
||||
## Bibliography
|
||||
- Prose and author-only citations now use editor names if the author names are
|
||||
@ -186,12 +188,12 @@ description: Changes slated to appear in Typst 0.13.0
|
||||
- [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text
|
||||
anymore
|
||||
- Fixed wrong language codes being used for Greek and Ukrainian
|
||||
- Fixed default quotes for Croatian
|
||||
- Fixed default quotes for Croatian and Bulgarian
|
||||
- Fixed crash in RTL text handling
|
||||
- Added support for [`raw`] syntax highlighting for a few new languages: CFML,
|
||||
NSIS, and WGSL
|
||||
- New font metadata exception for New Computer Modern Sans Math
|
||||
- Updated bundled New Computer Modern fonts to version 7.0
|
||||
- Updated bundled New Computer Modern fonts to version 7.0.1
|
||||
|
||||
## Layout
|
||||
- Fixed various bugs with footnotes
|
||||
@ -229,7 +231,7 @@ description: Changes slated to appear in Typst 0.13.0
|
||||
- A shebang `#!` at the very start of a file is now ignored
|
||||
|
||||
## PDF export
|
||||
- Added `pdf.embed` function for embedding arbitrary files in the exported PDF
|
||||
- Added [`pdf.embed`] function for embedding arbitrary files in the exported PDF
|
||||
- Added support for PDF/A-3b export
|
||||
- The PDF timestamp will now contain the timezone by default
|
||||
|
||||
@ -270,6 +272,9 @@ feature flag.
|
||||
- Added a live reloading HTTP server to `typst watch` when targeting HTML
|
||||
- Fixed self-update not being aware about certain target architectures
|
||||
- Fixed crash when piping `typst fonts` output to another command
|
||||
- Fixed handling of relative paths in `--make-deps` output
|
||||
- Fixed handling of multipage SVG and PNG export in `--make-deps` output
|
||||
- Colons in filenames are now correctly escaped in `--make-deps` output
|
||||
|
||||
## Symbols
|
||||
- New
|
||||
@ -312,8 +317,20 @@ feature flag.
|
||||
functions directly accepting both paths and bytes
|
||||
- The `sect` and its variants in favor of `inter`, and `integral.sect` in favor
|
||||
of `integral.inter`
|
||||
- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`)
|
||||
which was temporarily introduced in Typst 0.8 **(Breaking change)**
|
||||
- The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`)
|
||||
which was temporarily introduced in Typst 0.8 now emits warnings. It will be
|
||||
removed in Typst 0.14.
|
||||
|
||||
## Removals
|
||||
- Removed `style` function and `styles` argument of [`measure`], use a [context]
|
||||
expression instead **(Breaking change)**
|
||||
- Removed `state.display` function, use [`state.get`] instead
|
||||
**(Breaking change)**
|
||||
- Removed `location` argument of [`state.at`], [`counter.at`], and [`query`]
|
||||
**(Breaking change)**
|
||||
- Removed compatibility behavior where [`counter.display`] worked without
|
||||
[context] **(Breaking change)**
|
||||
- Removed compatibility behavior of [`locate`] **(Breaking change)**
|
||||
|
||||
## Development
|
||||
- The `typst::compile` function is now generic and can return either a
|
||||
@ -322,3 +339,6 @@ feature flag.
|
||||
feature is enabled
|
||||
- Increased minimum supported Rust version to 1.80
|
||||
- Fixed linux/arm64 Docker image
|
||||
|
||||
## Contributors
|
||||
<contributors from="v0.12.0" to="v0.13.0" />
|
||||
|
@ -10,7 +10,7 @@ forward. This section documents all changes to Typst since its initial public
|
||||
release.
|
||||
|
||||
## Versions
|
||||
- [Unreleased changes planned for Typst 0.13.0]($changelog/0.13.0)
|
||||
- [Typst 0.13.0]($changelog/0.13.0)
|
||||
- [Typst 0.12.0]($changelog/0.12.0)
|
||||
- [Typst 0.11.1]($changelog/0.11.1)
|
||||
- [Typst 0.11.0]($changelog/0.11.0)
|
||||
|
@ -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.
|
||||
|
@ -550,8 +550,6 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
outline.extend(scope_outline(&model.scope));
|
||||
} else {
|
||||
outline.extend(model.params.iter().map(|param| OutlineItem {
|
||||
id: eco_format!("{id_base}-{}", urlify(param.name)),
|
||||
@ -560,27 +558,30 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec<OutlineItem> {
|
||||
}));
|
||||
}
|
||||
|
||||
outline.extend(scope_outline(&model.scope, id_base));
|
||||
|
||||
outline
|
||||
}
|
||||
|
||||
/// Produce an outline for a function scope.
|
||||
fn scope_outline(scope: &[FuncModel]) -> Option<OutlineItem> {
|
||||
fn scope_outline(scope: &[FuncModel], id_base: &str) -> Option<OutlineItem> {
|
||||
if scope.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(OutlineItem {
|
||||
id: "definitions".into(),
|
||||
name: "Definitions".into(),
|
||||
children: scope
|
||||
.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 +688,7 @@ fn type_outline(model: &TypeModel) -> Vec<OutlineItem> {
|
||||
});
|
||||
}
|
||||
|
||||
outline.extend(scope_outline(&model.scope));
|
||||
outline.extend(scope_outline(&model.scope, ""));
|
||||
outline
|
||||
}
|
||||
|
||||
|
BIN
tests/ref/image-svg-auto-detection.png
Normal file
BIN
tests/ref/image-svg-auto-detection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 129 B |
BIN
tests/ref/issue-5831-par-constructor-args.png
Normal file
BIN
tests/ref/issue-5831-par-constructor-args.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -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)
|
||||
}
|
||||
|
@ -2,6 +2,60 @@
|
||||
#test(type(1), int)
|
||||
#test(type(ltr), direction)
|
||||
#test(type(10 / 3), float)
|
||||
#test(type(10) == int, true)
|
||||
#test(type(10) != int, false)
|
||||
|
||||
--- type-string-compatibility-add ---
|
||||
// Warning: 7-23 adding strings and types is deprecated
|
||||
// Hint: 7-23 convert the type to a string with `str` first
|
||||
#test("is " + type(10), "is integer")
|
||||
// Warning: 7-23 adding strings and types is deprecated
|
||||
// Hint: 7-23 convert the type to a string with `str` first
|
||||
#test(type(10) + " is", "integer is")
|
||||
|
||||
--- type-string-compatibility-join ---
|
||||
// Warning: 16-24 joining strings and types is deprecated
|
||||
// Hint: 16-24 convert the type to a string with `str` first
|
||||
#test({ "is "; type(10) }, "is integer")
|
||||
// Warning: 19-24 joining strings and types is deprecated
|
||||
// Hint: 19-24 convert the type to a string with `str` first
|
||||
#test({ type(10); " is" }, "integer is")
|
||||
|
||||
--- type-string-compatibility-equal ---
|
||||
// Warning: 7-28 comparing strings with types is deprecated
|
||||
// Hint: 7-28 compare with the literal type instead
|
||||
// Hint: 7-28 this comparison will always return `false` in future Typst releases
|
||||
#test(type(10) == "integer", true)
|
||||
// Warning: 7-26 comparing strings with types is deprecated
|
||||
// Hint: 7-26 compare with the literal type instead
|
||||
// Hint: 7-26 this comparison will always return `false` in future Typst releases
|
||||
#test(type(10) != "float", true)
|
||||
|
||||
--- type-string-compatibility-in-array ---
|
||||
// Warning: 7-35 comparing strings with types is deprecated
|
||||
// Hint: 7-35 compare with the literal type instead
|
||||
// Hint: 7-35 this comparison will always return `false` in future Typst releases
|
||||
#test(int in ("integer", "string"), true)
|
||||
// Warning: 7-37 comparing strings with types is deprecated
|
||||
// Hint: 7-37 compare with the literal type instead
|
||||
// Hint: 7-37 this comparison will always return `false` in future Typst releases
|
||||
#test(float in ("integer", "string"), false)
|
||||
|
||||
--- type-string-compatibility-in-str ---
|
||||
// Warning: 7-35 checking whether a type is contained in a string is deprecated
|
||||
// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string
|
||||
#test(int in "integers or strings", true)
|
||||
// Warning: 7-35 checking whether a type is contained in a string is deprecated
|
||||
// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string
|
||||
#test(str in "integers or strings", true)
|
||||
// Warning: 7-37 checking whether a type is contained in a string is deprecated
|
||||
// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string
|
||||
#test(float in "integers or strings", false)
|
||||
|
||||
--- type-string-compatibility-in-dict ---
|
||||
// Warning: 7-37 checking whether a type is contained in a dictionary is deprecated
|
||||
// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string
|
||||
#test(int in (integer: 1, string: 2), true)
|
||||
|
||||
--- issue-3110-type-constructor ---
|
||||
// Let the error message report the type name.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user