Merge branch 'main' into array-sorted-by

This commit is contained in:
Malo 2025-02-28 17:19:12 +01:00 committed by GitHub
commit 22ac489d67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
431 changed files with 11020 additions and 5756 deletions

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0
- uses: dtolnay/rust-toolchain@1.85.0
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --no-run
- run: cargo test --workspace --no-fail-fast
@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0
- uses: dtolnay/rust-toolchain@1.85.0
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.80.0
- uses: dtolnay/rust-toolchain@1.83.0
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspace

View File

@ -44,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0
- uses: dtolnay/rust-toolchain@1.85.0
with:
target: ${{ matrix.target }}

933
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@ default-members = ["crates/typst-cli"]
resolver = "2"
[workspace.package]
version = "0.12.0"
rust-version = "1.80" # also change in ci.yml
version = "0.13.0"
rust-version = "1.83" # also change in ci.yml
authors = ["The Typst Project Developers"]
edition = "2021"
homepage = "https://typst.app"
@ -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 = "b07d156" }
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 = { git = "https://github.com/typst/typst-assets", rev = "fa0f8a4" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "61aebe9" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -47,19 +47,19 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1"
clap_mangen = "0.2.10"
codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "343a9b1" }
codex = "0.1.1"
color-print = "0.3.6"
comemo = "0.4"
csv = "1"
ctrlc = "3.4.1"
dirs = "5"
dirs = "6"
ecow = { version = "0.2", features = ["serde"] }
env_proxy = "0.4"
flate2 = "1"
fontdb = { version = "0.21", default-features = false }
fs_extra = "1.3"
glidesort = "0.1.2"
hayagriva = "0.8"
hayagriva = "0.8.1"
heck = "0.5"
hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] }
@ -68,15 +68,16 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1"
image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] }
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] }
kamadak-exif = "0.5"
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 = "6"
notify = "8"
once_cell = "1"
open = "5.0.1"
openssl = "0.10"
@ -84,7 +85,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pdf-writer = "0.12"
pdf-writer = "0.12.1"
phf = { version = "0.11", features = ["macros"] }
pixglyph = "0.5.1"
png = "0.17"
@ -123,21 +124,23 @@ tiny_http = "0.12"
tiny-skia = "0.11"
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
ttf-parser = "0.24.1"
two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] }
two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] }
typed-arena = "2"
unicode-bidi = "0.3.18"
unicode-ident = "1.0"
unicode-math-class = "0.1"
unicode-script = "0.5"
unicode-normalization = "0.1.24"
unicode-segmentation = "1"
unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
usvg = { version = "0.43", default-features = false, features = ["text"] }
walkdir = "2"
wasmi = "0.39.0"
wasmi = "0.40.0"
web-sys = "0.3"
xmlparser = "0.13.5"
xmlwriter = "0.1.0"
xmp-writer = "0.3"
xmp-writer = "0.3.1"
xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1"
zip = { version = "2", default-features = false, features = ["deflate"] }

View File

@ -5,19 +5,19 @@
<p align="center">
<a href="https://typst.app/docs/">
<img alt="Documentation" src="https://img.shields.io/website?down_message=offline&label=docs&up_color=007aff&up_message=online&url=https%3A%2F%2Ftypst.app%2Fdocs"
/></a>
></a>
<a href="https://typst.app/">
<img alt="Typst App" src="https://img.shields.io/website?down_message=offline&label=typst.app&up_color=239dad&up_message=online&url=https%3A%2F%2Ftypst.app"
/></a>
></a>
<a href="https://discord.gg/2uDybryKPe">
<img alt="Discord Server" src="https://img.shields.io/discord/1054443721975922748?color=5865F2&label=discord&labelColor=555"
/></a>
></a>
<a href="https://github.com/typst/typst/blob/main/LICENSE">
<img alt="Apache-2 License" src="https://img.shields.io/badge/license-Apache%202-brightgreen"
/></a>
></a>
<a href="https://typst.app/jobs/">
<img alt="Jobs at Typst" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ftypst.app%2Fassets%2Fdata%2Fshields.json&query=%24.jobs.text&label=jobs&color=%23A561FF&cacheSeconds=1800"
/></a>
></a>
</p>
Typst is a new markup-based typesetting system that is designed to be as powerful
@ -39,7 +39,7 @@ A [gentle introduction][tutorial] to Typst is available in our documentation.
However, if you want to see the power of Typst encapsulated in one image, here
it is:
<p align="center">
<img alt="Example" width="900" src="https://user-images.githubusercontent.com/17899797/228031796-ced0e452-fcee-4ae9-92da-b9287764ff25.png"/>
<img alt="Example" width="900" src="https://user-images.githubusercontent.com/17899797/228031796-ced0e452-fcee-4ae9-92da-b9287764ff25.png">
</p>

View File

@ -473,6 +473,9 @@ pub enum PdfStandard {
/// PDF/A-2b.
#[value(name = "a-2b")]
A_2b,
/// PDF/A-3b.
#[value(name = "a-3b")]
A_3b,
}
display_possible_values!(PdfStandard);

View File

@ -6,8 +6,9 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Timelike, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term;
use ecow::{eco_format, EcoString};
use ecow::eco_format;
use parking_lot::RwLock;
use pathdiff::diff_paths;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
@ -136,6 +137,7 @@ impl CompileConfig {
.map(|standard| match standard {
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
})
.collect::<Vec<_>>();
PdfStandards::new(&list)?
@ -187,7 +189,7 @@ pub fn compile_once(
match output {
// Export the PDF / PNG.
Ok(()) => {
Ok(outputs) => {
let duration = start.elapsed();
if config.watching {
@ -201,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)?;
}
@ -225,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);
@ -256,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())
}
@ -326,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,
@ -340,7 +350,7 @@ fn export_image(
.iter()
.enumerate()
.filter(|(i, _)| {
config.pages.as_ref().map_or(true, |exported_page_ranges| {
config.pages.as_ref().is_none_or(|exported_page_ranges| {
exported_page_ranges.includes_page_index(*i)
})
})
@ -382,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())
@ -391,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 {
@ -500,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.
@ -521,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
@ -543,18 +566,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
fn write(
make_deps_path: &Path,
output_path: &str,
output_paths: Vec<String>,
root: PathBuf,
dependencies: impl Iterator<Item = PathBuf>,
) -> io::Result<()> {
let mut file = File::create(make_deps_path)?;
let current_dir = std::env::current_dir()?;
let relative_root = diff_paths(&root, &current_dir).unwrap_or(root.clone());
file.write_all(munge(output_path).as_bytes())?;
for (i, output_path) in output_paths.into_iter().enumerate() {
if i != 0 {
file.write_all(b" ")?;
}
file.write_all(munge(&output_path).as_bytes())?;
}
file.write_all(b":")?;
for dependency in dependencies {
let Some(dependency) =
dependency.strip_prefix(&root).unwrap_or(&dependency).to_str()
else {
let relative_dependency = match dependency.strip_prefix(&root) {
Ok(root_relative_dependency) => {
relative_root.join(root_relative_dependency)
}
Err(_) => dependency,
};
let Some(relative_dependency) = relative_dependency.to_str() else {
// Silently skip paths that aren't valid unicode so we still
// produce a rule that will work for the other paths that can be
// processed.
@ -562,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
};
file.write_all(b" ")?;
file.write_all(munge(dependency).as_bytes())?;
file.write_all(munge(relative_dependency).as_bytes())?;
}
file.write_all(b"\n")?;
Ok(())
}
write(make_deps_path, output_path, world.root().to_owned(), world.dependencies())
write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
.map_err(|err| {
eco_format!("failed to create make dependencies file due to IO error ({err})")
})

View File

@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Watch all dependencies of the initial compilation.
watcher.update(world.dependencies())?;
// Recompile whenever something relevant happens.
loop {
// Watch all dependencies of the most recent compilation.
watcher.update(world.dependencies())?;
// Wait until anything relevant happens.
watcher.wait()?;
@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Evict the cache.
comemo::evict(10);
// Adjust the file watching.
watcher.update(world.dependencies())?;
}
}
@ -204,6 +201,10 @@ impl Watcher {
let event = event
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
if !is_relevant_event_kind(&event.kind) {
continue;
}
// Workaround for notify-rs' implicit unwatch on remove/rename
// (triggered by some editors when saving files) with the
// inotify backend. By keeping track of the potentially
@ -224,7 +225,17 @@ impl Watcher {
}
}
relevant |= self.is_event_relevant(&event);
// Don't recompile because the output file changed.
// FIXME: This doesn't work properly for multifile image export.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
continue;
}
relevant = true;
}
// If we found a relevant event or if any of the missing files now
@ -234,32 +245,23 @@ impl Watcher {
}
}
}
}
/// Whether a watch event is relevant for compilation.
fn is_event_relevant(&self, event: &notify::Event) -> bool {
// Never recompile because the output file changed.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
return false;
}
match &event.kind {
notify::EventKind::Any => true,
notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true,
notify::EventKind::Modify(kind) => match kind {
notify::event::ModifyKind::Any => true,
notify::event::ModifyKind::Data(_) => true,
notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
notify::event::ModifyKind::Other => false,
},
notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false,
}
/// Whether a kind of watch event is relevant for compilation.
fn is_relevant_event_kind(kind: &notify::EventKind) -> bool {
match kind {
notify::EventKind::Any => true,
notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true,
notify::EventKind::Modify(kind) => match kind {
notify::event::ModifyKind::Any => true,
notify::event::ModifyKind::Data(_) => true,
notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
notify::event::ModifyKind::Other => false,
},
notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false,
}
}

View File

@ -305,7 +305,7 @@ impl FileSlot {
) -> FileResult<Bytes> {
self.file.get_or_init(
|| read(self.id, project_root, package_storage),
|data, _| Ok(data.into()),
|data, _| Ok(Bytes::new(data)),
)
}
}

View File

@ -30,12 +30,14 @@ impl Access for ast::Ident<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
let span = self.span();
if vm.inspected == Some(span) {
if let Ok(value) = vm.scopes.get(&self).cloned() {
vm.trace(value);
if let Ok(binding) = vm.scopes.get(&self) {
vm.trace(binding.read().clone());
}
}
let value = vm.scopes.get_mut(&self).at(span)?;
Ok(value)
vm.scopes
.get_mut(&self)
.and_then(|b| b.write().map_err(Into::into))
.at(span)
}
}

View File

@ -6,13 +6,12 @@ use typst_library::diag::{
};
use typst_library::engine::{Engine, Sink, Traced};
use typst_library::foundations::{
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
NativeElement, Scope, Scopes, Value,
Arg, Args, Binding, Capturer, Closure, Content, Context, Func, NativeElement, Scope,
Scopes, SymbolElem, Value,
};
use typst_library::introspection::Introspector;
use typst_library::math::LrElem;
use typst_library::routines::Routines;
use typst_library::text::TextElem;
use typst_library::World;
use typst_syntax::ast::{self, AstNode, Ident};
use typst_syntax::{Span, Spanned, SyntaxNode};
@ -197,7 +196,7 @@ pub fn eval_closure(
// Provide the closure itself for recursive calls.
if let Some(name) = name {
vm.define(name, Value::Func(func.clone()));
vm.define(name, func.clone());
}
let num_pos_args = args.to_pos().len();
@ -316,22 +315,25 @@ fn eval_field_call(
(target, args)
};
if let Value::Plugin(plugin) = &target {
// Call plugins by converting args to bytes.
let bytes = args.all::<Bytes>()?;
args.finish()?;
let value = plugin.call(&field, bytes).at(span)?.into_value();
Ok(FieldCall::Resolved(value))
} else if let Some(callee) = target.ty().scope().get(&field) {
let field_span = field.span();
let sink = (&mut vm.engine, field_span);
if let Some(callee) = target.ty().scope().get(&field) {
args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.clone(), args))
Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args))
} else if let Value::Content(content) = &target {
if let Some(callee) = content.elem().scope().get(&field) {
args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args))
} else {
bail!(missing_field_call_error(target, field))
}
} else if matches!(
target,
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
) {
// Certain value types may have their own ways to access method fields.
// e.g. `$arrow.r(v)$`, `table.cell[..]`
let value = target.field(&field).at(field.span())?;
let value = target.field(&field, sink).at(field_span)?;
Ok(FieldCall::Normal(value, args))
} else {
// Otherwise we cannot call this field.
@ -341,8 +343,20 @@ fn eval_field_call(
/// Produce an error when we cannot call the field.
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
let mut error =
error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str());
let mut error = match &target {
Value::Content(content) => error!(
field.span(),
"element {} has no method `{}`",
content.elem().name(),
field.as_str(),
),
_ => error!(
field.span(),
"type {} has no method `{}`",
target.ty(),
field.as_str()
),
};
match target {
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
@ -352,7 +366,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
field.as_str(),
));
}
_ if target.field(&field).is_ok() => {
_ if target.field(&field, ()).is_ok() => {
error.hint(eco_format!(
"did you mean to access the field `{}`?",
field.as_str(),
@ -360,6 +374,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
}
_ => {}
}
error
}
@ -382,16 +397,16 @@ fn wrap_args_in_math(
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += TextElem::packed(',');
body += SymbolElem::packed(',');
}
body += arg;
}
if trailing_comma {
body += TextElem::packed(',');
body += SymbolElem::packed(',');
}
Ok(Value::Content(
callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')'))
+ LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')'))
.pack()
.spanned(args.span),
))
@ -445,15 +460,13 @@ impl<'a> CapturesVisitor<'a> {
// Identifiers that shouldn't count as captures because they
// actually bind a new name are handled below (individually through
// the expressions that contain them).
Some(ast::Expr::Ident(ident)) => {
self.capture(ident.get(), ident.span(), Scopes::get)
}
Some(ast::Expr::Ident(ident)) => self.capture(ident.get(), Scopes::get),
Some(ast::Expr::MathIdent(ident)) => {
self.capture(ident.get(), ident.span(), Scopes::get_in_math)
self.capture(ident.get(), Scopes::get_in_math)
}
// Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => {
self.internal.enter();
for child in node.children() {
self.visit(child);
@ -503,7 +516,7 @@ impl<'a> CapturesVisitor<'a> {
// A let expression contains a binding, but that binding is only
// active after the body is evaluated.
Some(ast::Expr::Let(expr)) => {
Some(ast::Expr::LetBinding(expr)) => {
if let Some(init) = expr.init() {
self.visit(init.to_untyped());
}
@ -516,7 +529,7 @@ impl<'a> CapturesVisitor<'a> {
// A for loop contains one or two bindings in its pattern. These are
// active after the iterable is evaluated but before the body is
// evaluated.
Some(ast::Expr::For(expr)) => {
Some(ast::Expr::ForLoop(expr)) => {
self.visit(expr.iterable().to_untyped());
self.internal.enter();
@ -531,7 +544,7 @@ impl<'a> CapturesVisitor<'a> {
// An import contains items, but these are active only after the
// path is evaluated.
Some(ast::Expr::Import(expr)) => {
Some(ast::Expr::ModuleImport(expr)) => {
self.visit(expr.source().to_untyped());
if let Some(ast::Imports::Items(items)) = expr.imports() {
for item in items.iter() {
@ -557,32 +570,34 @@ impl<'a> CapturesVisitor<'a> {
/// Bind a new internal variable.
fn bind(&mut self, ident: ast::Ident) {
self.internal.top.define_ident(ident, Value::None);
// The concrete value does not matter as we only use the scoping
// mechanism of `Scopes`, not the values themselves.
self.internal
.top
.bind(ident.get().clone(), Binding::detached(Value::None));
}
/// Capture a variable if it isn't internal.
fn capture(
&mut self,
ident: &EcoString,
span: Span,
getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>,
getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Binding>,
) {
if self.internal.get(ident).is_err() {
let Some(value) = self
.external
.map(|external| getter(external, ident).ok())
.unwrap_or(Some(&Value::None))
else {
return;
};
self.captures.define_captured(
ident.clone(),
value.clone(),
self.capturer,
span,
);
if self.internal.get(ident).is_ok() {
return;
}
let binding = match self.external {
Some(external) => match getter(external, ident) {
Ok(binding) => binding.capture(self.capturer),
Err(_) => return,
},
// The external scopes are only `None` when we are doing IDE capture
// analysis, in which case the concrete value doesn't matter.
None => Binding::detached(Value::None),
};
self.captures.bind(ident.clone(), binding);
}
}
@ -685,8 +700,7 @@ mod tests {
// Named-params.
test(s, "$ foo(bar: y) $", &["foo"]);
// This should be updated when we improve named-param parsing:
test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]);
test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]);
// Field access in math.
test(s, "$ foo.bar $", &["foo"]);

View File

@ -30,7 +30,7 @@ fn eval_code<'a>(
while let Some(expr) = exprs.next() {
let span = expr.span();
let value = match expr {
ast::Expr::Set(set) => {
ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?;
if vm.flow.is_some() {
break;
@ -39,7 +39,7 @@ fn eval_code<'a>(
let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_map(styles))
}
ast::Expr::Show(show) => {
ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?;
if vm.flow.is_some() {
break;
@ -94,11 +94,12 @@ impl Eval for ast::Expr<'_> {
Self::Label(v) => v.eval(vm),
Self::Ref(v) => v.eval(vm).map(Value::Content),
Self::Heading(v) => v.eval(vm).map(Value::Content),
Self::List(v) => v.eval(vm).map(Value::Content),
Self::Enum(v) => v.eval(vm).map(Value::Content),
Self::Term(v) => v.eval(vm).map(Value::Content),
Self::ListItem(v) => v.eval(vm).map(Value::Content),
Self::EnumItem(v) => v.eval(vm).map(Value::Content),
Self::TermItem(v) => v.eval(vm).map(Value::Content),
Self::Equation(v) => v.eval(vm).map(Value::Content),
Self::Math(v) => v.eval(vm).map(Value::Content),
Self::MathText(v) => v.eval(vm).map(Value::Content),
Self::MathIdent(v) => v.eval(vm),
Self::MathShorthand(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
@ -115,8 +116,8 @@ impl Eval for ast::Expr<'_> {
Self::Float(v) => v.eval(vm),
Self::Numeric(v) => v.eval(vm),
Self::Str(v) => v.eval(vm),
Self::Code(v) => v.eval(vm),
Self::Content(v) => v.eval(vm).map(Value::Content),
Self::CodeBlock(v) => v.eval(vm),
Self::ContentBlock(v) => v.eval(vm).map(Value::Content),
Self::Array(v) => v.eval(vm).map(Value::Array),
Self::Dict(v) => v.eval(vm).map(Value::Dict),
Self::Parenthesized(v) => v.eval(vm),
@ -125,19 +126,19 @@ impl Eval for ast::Expr<'_> {
Self::Closure(v) => v.eval(vm),
Self::Unary(v) => v.eval(vm),
Self::Binary(v) => v.eval(vm),
Self::Let(v) => v.eval(vm),
Self::DestructAssign(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")),
Self::LetBinding(v) => v.eval(vm),
Self::DestructAssignment(v) => v.eval(vm),
Self::SetRule(_) => bail!(forbidden("set")),
Self::ShowRule(_) => bail!(forbidden("show")),
Self::Contextual(v) => v.eval(vm).map(Value::Content),
Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm),
Self::For(v) => v.eval(vm),
Self::Import(v) => v.eval(vm),
Self::Include(v) => v.eval(vm).map(Value::Content),
Self::Break(v) => v.eval(vm),
Self::Continue(v) => v.eval(vm),
Self::Return(v) => v.eval(vm),
Self::WhileLoop(v) => v.eval(vm),
Self::ForLoop(v) => v.eval(vm),
Self::ModuleImport(v) => v.eval(vm),
Self::ModuleInclude(v) => v.eval(vm).map(Value::Content),
Self::LoopBreak(v) => v.eval(vm),
Self::LoopContinue(v) => v.eval(vm),
Self::FuncReturn(v) => v.eval(vm),
}?
.spanned(span);
@ -153,7 +154,13 @@ impl Eval for ast::Ident<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.get(&self).cloned().at(self.span())
let span = self.span();
Ok(vm
.scopes
.get(&self)
.at(span)?
.read_checked((&mut vm.engine, span))
.clone())
}
}
@ -309,8 +316,9 @@ impl Eval for ast::FieldAccess<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.target().eval(vm)?;
let field = self.field();
let field_span = field.span();
let err = match value.field(&field).at(field.span()) {
let err = match value.field(&field, (&mut vm.engine, field_span)).at(field_span) {
Ok(value) => return Ok(value),
Err(err) => err,
};

View File

@ -4,9 +4,9 @@ use typst_library::diag::{
bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Module, Value};
use typst_library::foundations::{Binding, Content, Module, Value};
use typst_library::World;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::ast::{self, AstNode, BareImportError};
use typst_syntax::package::{PackageManifest, PackageSpec};
use typst_syntax::{FileId, Span, VirtualPath};
@ -16,11 +16,11 @@ impl Eval for ast::ModuleImport<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let source = self.source();
let source_span = source.span();
let mut source = source.eval(vm)?;
let new_name = self.new_name();
let imports = self.imports();
let source_expr = self.source();
let source_span = source_expr.span();
let mut source = source_expr.eval(vm)?;
let mut is_str = false;
match &source {
Value::Func(func) => {
@ -32,6 +32,7 @@ impl Eval for ast::ModuleImport<'_> {
Value::Module(_) => {}
Value::Str(path) => {
source = Value::Module(import(&mut vm.engine, path, source_span)?);
is_str = true;
}
v => {
bail!(
@ -42,6 +43,8 @@ impl Eval for ast::ModuleImport<'_> {
}
}
// If there is a rename, import the source itself under that name.
let new_name = self.new_name();
if let Some(new_name) = new_name {
if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() {
@ -54,21 +57,42 @@ impl Eval for ast::ModuleImport<'_> {
}
// Define renamed module on the scope.
vm.scopes.top.define_ident(new_name, source.clone());
vm.define(new_name, source.clone());
}
let scope = source.scope().unwrap();
match imports {
match self.imports() {
None => {
// Only import here if there is no rename.
if new_name.is_none() {
let name: EcoString = source.name().unwrap().into();
vm.scopes.top.define(name, source);
match self.bare_name() {
// Bare dynamic string imports are not allowed.
Ok(name)
if !is_str || matches!(source_expr, ast::Expr::Str(_)) =>
{
if matches!(source_expr, ast::Expr::Ident(_)) {
vm.engine.sink.warn(warning!(
source_expr.span(),
"this import has no effect",
));
}
vm.scopes.top.bind(name, Binding::new(source, source_span));
}
Ok(_) | Err(BareImportError::Dynamic) => bail!(
source_span, "dynamic import requires an explicit name";
hint: "you can name the import with `as`"
),
Err(BareImportError::PathInvalid) => bail!(
source_span, "module name would not be a valid identifier";
hint: "you can rename the import with `as`",
),
// Bad package spec would have failed the import already.
Err(BareImportError::PackageInvalid) => unreachable!(),
}
}
}
Some(ast::Imports::Wildcard) => {
for (var, value, span) in scope.iter() {
vm.scopes.top.define_spanned(var.clone(), value.clone(), span);
for (var, binding) in scope.iter() {
vm.scopes.top.bind(var.clone(), binding.clone());
}
}
Some(ast::Imports::Items(items)) => {
@ -78,7 +102,7 @@ impl Eval for ast::ModuleImport<'_> {
let mut scope = scope;
while let Some(component) = &path.next() {
let Some(value) = scope.get(component) else {
let Some(binding) = scope.get(component) else {
errors.push(error!(component.span(), "unresolved import"));
break;
};
@ -86,6 +110,7 @@ impl Eval for ast::ModuleImport<'_> {
if path.peek().is_some() {
// Nested import, as this is not the last component.
// This must be a submodule.
let value = binding.read();
let Some(submodule) = value.scope() else {
let error = if matches!(value, Value::Func(function) if function.scope().is_none())
{
@ -128,7 +153,7 @@ impl Eval for ast::ModuleImport<'_> {
}
}
vm.define(item.bound_name(), value.clone());
vm.bind(item.bound_name(), binding.clone());
}
}
}
@ -211,7 +236,7 @@ fn resolve_package(
// Evaluate the manifest.
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
let bytes = engine.world.file(manifest_id).at(span)?;
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?;
let string = bytes.as_str().map_err(FileError::from).at(span)?;
let manifest: PackageManifest = toml::from_str(string)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
.at(span)?;

View File

@ -33,7 +33,7 @@ fn eval_markup<'a>(
while let Some(expr) = exprs.next() {
match expr {
ast::Expr::Set(set) => {
ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?;
if vm.flow.is_some() {
break;
@ -41,7 +41,7 @@ fn eval_markup<'a>(
seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
}
ast::Expr::Show(show) => {
ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?;
if vm.flow.is_some() {
break;

View File

@ -1,11 +1,11 @@
use ecow::eco_format;
use typst_library::diag::{At, SourceResult};
use typst_library::foundations::{Content, NativeElement, Symbol, Value};
use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value};
use typst_library::math::{
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
};
use typst_library::text::TextElem;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::ast::{self, AstNode, MathTextKind};
use crate::{Eval, Vm};
@ -20,11 +20,28 @@ impl Eval for ast::Math<'_> {
}
}
impl Eval for ast::MathText<'_> {
type Output = Content;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
match self.get() {
MathTextKind::Character(c) => Ok(SymbolElem::packed(c)),
MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())),
}
}
}
impl Eval for ast::MathIdent<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.get_in_math(&self).cloned().at(self.span())
let span = self.span();
Ok(vm
.scopes
.get_in_math(&self)
.at(span)?
.read_checked((&mut vm.engine, span))
.clone())
}
}
@ -102,6 +119,7 @@ impl Eval for ast::MathRoot<'_> {
type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
// Use `TextElem` to match `MathTextKind::Number` above.
let index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
let radicand = self.radicand().eval_display(vm)?;
Ok(RootElem::new(radicand).with_index(index).pack())

View File

@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> {
let transform = self.transform();
let transform = match transform {
ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?),
ast::Expr::SetRule(set) => Transformation::Style(set.eval(vm)?),
expr => expr.eval(vm)?.cast::<Transformation>().at(transform.span())?,
};

View File

@ -1,7 +1,7 @@
use comemo::Tracked;
use typst_library::diag::warning;
use typst_library::engine::Engine;
use typst_library::foundations::{Context, IntoValue, Scopes, Value};
use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value};
use typst_library::World;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::Span;
@ -42,13 +42,23 @@ impl<'a> Vm<'a> {
self.engine.world
}
/// Define a variable in the current scope.
/// Bind a value to an identifier.
///
/// This will create a [`Binding`] with the value and the identifier's span.
pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) {
let value = value.into_value();
self.bind(var, Binding::new(value, var.span()));
}
/// Insert a binding into the current scope.
///
/// This will insert the value into the top-most scope and make it available
/// for dynamic tracing, assisting IDE functionality.
pub fn bind(&mut self, var: ast::Ident, binding: Binding) {
if self.inspected == Some(var.span()) {
self.trace(value.clone());
self.trace(binding.read().clone());
}
// This will become an error in the parser if 'is' becomes a keyword.
// This will become an error in the parser if `is` becomes a keyword.
if var.get() == "is" {
self.engine.sink.warn(warning!(
var.span(),
@ -58,7 +68,8 @@ impl<'a> Vm<'a> {
hint: "try `is_` instead"
));
}
self.scopes.top.define_ident(var, value);
self.scopes.top.bind(var.get().clone(), binding);
}
/// Trace a value.

View File

@ -2,7 +2,7 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode};
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
use typst_library::layout::Frame;
use typst_syntax::Span;
@ -12,15 +12,19 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
w.buf.push_str("<!DOCTYPE html>");
write_indent(&mut w);
write_element(&mut w, &document.root)?;
if w.pretty {
w.buf.push('\n');
}
Ok(w.buf)
}
#[derive(Default)]
struct Writer {
/// The output buffer.
buf: String,
/// current indentation level
/// The current indentation level
level: usize,
/// pretty printing enabled?
/// Whether pretty printing is enabled.
pretty: bool,
}
@ -85,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
let pretty = w.pretty;
if !element.children.is_empty() {
w.pretty &= is_pretty(element);
let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag),
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_child = match c {
let pretty_around = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(element) => is_pretty(element),
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_child {
if core::mem::take(&mut indent) || pretty_around {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_child;
indent = pretty_around;
}
w.level -= 1;
write_indent(w)
write_indent(w);
}
w.pretty = pretty;
@ -115,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
Ok(())
}
/// Whether the element should be pretty-printed.
fn is_pretty(element: &HtmlElement) -> bool {
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
/// Whether we are allowed to add an extra newline at the start and end of the
/// element's contents.
///
/// Technically, users can change CSS `display` properties such that the
/// insertion of whitespace may actually impact the visual output. For example,
/// <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how adding CSS
/// rules to `<p>` can make it sensitive to whitespace. For this reason, we
/// should also respect the `style` tag in the future.
fn allows_pretty_inside(tag: HtmlTag) -> bool {
(tag::is_block_by_default(tag) && tag != tag::pre)
|| tag::is_tabular_by_default(tag)
|| tag == tag::li
}
/// Whether newlines should be added before and after the element if the parent
/// allows it.
///
/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is
/// more subjective and depends on preference.
fn wants_pretty_around(tag: HtmlTag) -> bool {
allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre
}
/// Escape a character.

View File

@ -14,9 +14,9 @@ use typst_library::html::{
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
use typst_library::layout::{Abs, Axes, BoxElem, Region, Size};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World;
use typst_syntax::Span;
@ -83,8 +83,8 @@ fn html_document_impl(
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector })
}
@ -139,7 +139,9 @@ fn html_fragment_impl(
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlFragment,
// No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
&mut engine,
&mut locator,
&arenas,
@ -189,7 +191,8 @@ fn handle(
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() {
let children = handle_list(engine, locator, elem.children.iter(&styles))?;
let children =
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
@ -197,13 +200,34 @@ fn handle(
.into(),
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// FIXME: Very incomplete and hacky, but makes boxes kind fulfill their
// purpose for now.
// TODO: This is rather incomplete.
if let Some(body) = elem.body(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.extend(children);
output.push(
HtmlElement::new(tag::span)
.with_attr(attr::style, "display: inline-block;")
.with_children(children)
.spanned(elem.span())
.into(),
)
}
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
{
// TODO: This is rather incomplete.
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::div)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {
@ -283,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len();
let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, len) {
match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag
elem.tag,
),
_ => {}
}

View File

@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
}
// Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) {
if matches!(
ctx.leaf.kind(),
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent
) {
ctx.from = ctx.leaf.offset();
math_completions(ctx);
return true;
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Behind an expression plus dot: "emoji.|".
if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot
|| (ctx.leaf.kind() == SyntaxKind::Text
|| (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
&& ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling();
@ -398,13 +401,31 @@ fn field_access_completions(
value: &Value,
styles: &Option<Styles>,
) {
for (name, value, _) in value.ty().scope().iter() {
ctx.call_completion(name.clone(), value);
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. We only complete
// those which have a `self` parameter.
for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
let Ok(func) = binding.read().clone().cast::<Func>() else { continue };
if func
.params()
.and_then(|params| params.first())
.is_some_and(|param| param.name == "self")
{
ctx.call_completion(name.clone(), binding.read());
}
}
if let Some(scope) = value.scope() {
for (name, value, _) in scope.iter() {
ctx.call_completion(name.clone(), value);
for (name, binding) in scope.iter() {
ctx.call_completion(name.clone(), binding.read());
}
}
@ -414,7 +435,7 @@ fn field_access_completions(
// with method syntax;
// 2. We can unwrap the field's value since it's a field belonging to
// this value's type, so accessing it should not fail.
ctx.value_completion(field, &value.field(field).unwrap());
ctx.value_completion(field, &value.field(field, ()).unwrap());
}
match value {
@ -452,16 +473,6 @@ fn field_access_completions(
}
}
}
Value::Plugin(plugin) => {
for name in plugin.iter() {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: name.clone(),
apply: None,
detail: None,
})
}
}
_ => {}
}
}
@ -506,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
// "#import "path.typ": a, b, |".
if_chain! {
if let Some(prev) = ctx.leaf.prev_sibling();
if let Some(ast::Expr::Import(import)) = prev.get().cast();
if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
then {
@ -525,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if let Some(grand) = parent.parent();
if grand.kind() == SyntaxKind::ImportItems;
if let Some(great) = grand.parent();
if let Some(ast::Expr::Import(import)) = great.get().cast();
if let Some(ast::Expr::ModuleImport(import)) = great.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
then {
@ -551,9 +562,9 @@ fn import_item_completions<'a>(
ctx.snippet_completion("*", "*", "Import everything.");
}
for (name, value, _) in scope.iter() {
for (name, binding) in scope.iter() {
if existing.iter().all(|item| item.original_name().as_str() != name) {
ctx.value_completion(name.clone(), value);
ctx.value_completion(name.clone(), binding.read());
}
}
}
@ -666,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
if let Some(args) = parent.get().cast::<ast::Args>();
if let Some(grand) = parent.parent();
if let Some(expr) = grand.get().cast::<ast::Expr>();
let set = matches!(expr, ast::Expr::Set(_));
let set = matches!(expr, ast::Expr::SetRule(_));
if let Some(callee) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()),
ast::Expr::SetRule(set) => Some(set.target()),
_ => None,
};
then {
@ -817,19 +828,8 @@ fn param_value_completions<'a>(
) {
if param.name == "font" {
ctx.font_completions();
} else if param.name == "path" {
ctx.file_completions_with_extensions(match func.name() {
Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
Some("csv") => &["csv"],
Some("plugin") => &["wasm"],
Some("cbor") => &["cbor"],
Some("json") => &["json"],
Some("toml") => &["toml"],
Some("xml") => &["xml"],
Some("yaml") => &["yml", "yaml"],
Some("bibliography") => &["bib", "yml", "yaml"],
_ => &[],
});
} else if let Some(extensions) = path_completion(func, param) {
ctx.file_completions_with_extensions(extensions);
} else if func.name() == Some("figure") && param.name == "body" {
ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure.");
ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure.");
@ -838,6 +838,28 @@ fn param_value_completions<'a>(
ctx.cast_completions(&param.input);
}
/// Returns which file extensions to complete for the given parameter if any.
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
Some(match (func.name(), param.name) {
(Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
(Some("csv"), "source") => &["csv"],
(Some("plugin"), "source") => &["wasm"],
(Some("cbor"), "source") => &["cbor"],
(Some("json"), "source") => &["json"],
(Some("toml"), "source") => &["toml"],
(Some("xml"), "source") => &["xml"],
(Some("yaml"), "source") => &["yml", "yaml"],
(Some("bibliography"), "sources") => &["bib", "yml", "yaml"],
(Some("bibliography"), "style") => &["csl"],
(Some("cite"), "style") => &["csl"],
(Some("raw"), "syntaxes") => &["sublime-syntax"],
(Some("raw"), "theme") => &["tmtheme"],
(Some("embed"), "path") => &[],
(None, "path") => &[],
_ => return None,
})
}
/// Resolve a callee expression to a global function.
fn resolve_global_callee<'a>(
ctx: &CompletionContext<'a>,
@ -845,13 +867,11 @@ fn resolve_global_callee<'a>(
) -> Option<&'a Func> {
let globals = globals(ctx.world, ctx.leaf);
let value = match callee {
ast::Expr::Ident(ident) => globals.get(&ident)?,
ast::Expr::Ident(ident) => globals.get(&ident)?.read(),
ast::Expr::FieldAccess(access) => match access.target() {
ast::Expr::Ident(target) => match globals.get(&target)? {
Value::Module(module) => module.field(&access.field()).ok()?,
Value::Func(func) => func.field(&access.field()).ok()?,
_ => return None,
},
ast::Expr::Ident(target) => {
globals.get(&target)?.read().scope()?.get(&access.field())?.read()
}
_ => return None,
},
_ => return None,
@ -1443,7 +1463,7 @@ impl<'a> CompletionContext<'a> {
let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
named_items(self.world, self.leaf.clone(), |item| {
let name = item.name();
if !name.is_empty() && item.value().as_ref().map_or(true, filter) {
if !name.is_empty() && item.value().as_ref().is_none_or(filter) {
defined.insert(name.clone(), item.value());
}
@ -1463,7 +1483,8 @@ impl<'a> CompletionContext<'a> {
}
}
for (name, value, _) in globals(self.world, self.leaf).iter() {
for (name, binding) in globals(self.world, self.leaf).iter() {
let value = binding.read();
if filter(value) && !defined.contains_key(name) {
self.value_completion_full(Some(name.clone()), value, parens, None, None);
}
@ -1747,4 +1768,26 @@ mod tests {
.must_include(["this", "that"])
.must_exclude(["*", "figure"]);
}
#[test]
fn test_autocomplete_type_methods() {
test("#\"hello\".", -1).must_include(["len", "contains"]);
test("#table().", -1).must_exclude(["cell"]);
}
#[test]
fn test_autocomplete_content_methods() {
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
.must_include(["indented", "body", "page"]);
}
#[test]
fn test_autocomplete_symbol_variants() {
test("#sym.arrow.", -1)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
test("$ arrow. $", -3)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
}
}

View File

@ -55,8 +55,8 @@ pub fn definition(
}
}
if let Some(value) = globals(world, &leaf).get(&name) {
return Some(Definition::Std(value.clone()));
if let Some(binding) = globals(world, &leaf).get(&name) {
return Some(Definition::Std(binding.read().clone()));
}
}

View File

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

View File

@ -1,7 +1,7 @@
use ecow::EcoString;
use typst::foundations::{Module, Value};
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode};
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use crate::{analyze_import, IdeWorld};
@ -30,38 +30,38 @@ pub fn named_items<T>(
if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports();
let source = node
.children()
.find(|child| child.is::<ast::Expr>())
.and_then(|source: LinkedNode| {
Some((analyze_import(world, &source)?, source))
});
let source = source.as_ref();
let source = v.source();
let source_value = node
.find(source.span())
.and_then(|source| analyze_import(world, &source));
let source_value = source_value.as_ref();
let module = source_value.and_then(|value| match value {
Value::Module(module) => Some(module),
_ => None,
});
let name_and_span = match (imports, v.new_name()) {
// ```plain
// import "foo" as name
// import "foo" as name: ..
// ```
(_, Some(name)) => Some((name.get().clone(), name.span())),
// ```plain
// import "foo"
// ```
(None, None) => v.bare_name().ok().map(|name| (name, source.span())),
// ```plain
// import "foo": ..
// ```
(Some(..), None) => None,
};
// Seeing the module itself.
if let Some((value, source)) = source {
let site = match (imports, v.new_name()) {
// ```plain
// import "foo" as name;
// import "foo" as name: ..;
// ```
(_, Some(name)) => Some(name.to_untyped()),
// ```plain
// import "foo";
// ```
(None, None) => Some(source.get()),
// ```plain
// import "foo": ..;
// ```
(Some(..), None) => None,
};
if let Some((site, value)) =
site.zip(value.clone().cast::<Module>().ok())
{
if let Some(res) = recv(NamedItem::Module(&value, site)) {
return Some(res);
}
if let Some((name, span)) = name_and_span {
if let Some(res) = recv(NamedItem::Module(&name, span, module)) {
return Some(res);
}
}
@ -75,9 +75,13 @@ pub fn named_items<T>(
// import "foo": *;
// ```
Some(ast::Imports::Wildcard) => {
if let Some(scope) = source.and_then(|(value, _)| value.scope()) {
for (name, value, span) in scope.iter() {
let item = NamedItem::Import(name, span, Some(value));
if let Some(scope) = source_value.and_then(Value::scope) {
for (name, binding) in scope.iter() {
let item = NamedItem::Import(
name,
binding.span(),
Some(binding.read()),
);
if let Some(res) = recv(item) {
return Some(res);
}
@ -89,18 +93,26 @@ pub fn named_items<T>(
// ```
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
let original = item.original_name();
let bound = item.bound_name();
let scope = source.and_then(|(value, _)| value.scope());
let span = scope
.and_then(|s| s.get_span(&original))
.unwrap_or(Span::detached())
.or(bound.span());
let mut iter = item.path().iter();
let mut binding = source_value
.and_then(Value::scope)
.zip(iter.next())
.and_then(|(scope, first)| scope.get(&first));
let value = scope.and_then(|s| s.get(&original));
if let Some(res) =
recv(NamedItem::Import(bound.get(), span, value))
{
for ident in iter {
binding = binding.and_then(|binding| {
binding.read().scope()?.get(&ident)
});
}
let bound = item.bound_name();
let (span, value) = match binding {
Some(binding) => (binding.span(), Some(binding.read())),
None => (bound.span(), None),
};
let item = NamedItem::Import(bound.get(), span, value);
if let Some(res) = recv(item) {
return Some(res);
}
}
@ -169,8 +181,8 @@ pub enum NamedItem<'a> {
Var(ast::Ident<'a>),
/// A function item.
Fn(ast::Ident<'a>),
/// A (imported) module item.
Module(&'a Module, &'a SyntaxNode),
/// A (imported) module.
Module(&'a EcoString, Span, Option<&'a Module>),
/// An imported item.
Import(&'a EcoString, Span, Option<&'a Value>),
}
@ -180,7 +192,7 @@ impl<'a> NamedItem<'a> {
match self {
NamedItem::Var(ident) => ident.get(),
NamedItem::Fn(ident) => ident.get(),
NamedItem::Module(value, _) => value.name(),
NamedItem::Module(name, _, _) => name,
NamedItem::Import(name, _, _) => name,
}
}
@ -188,7 +200,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn value(&self) -> Option<Value> {
match self {
NamedItem::Var(..) | NamedItem::Fn(..) => None,
NamedItem::Module(value, _) => Some(Value::Module((*value).clone())),
NamedItem::Module(_, _, value) => value.cloned().map(Value::Module),
NamedItem::Import(_, _, value) => value.cloned(),
}
}
@ -196,7 +208,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn span(&self) -> Span {
match *self {
NamedItem::Var(name) | NamedItem::Fn(name) => name.span(),
NamedItem::Module(_, site) => site.span(),
NamedItem::Module(_, span, _) => span,
NamedItem::Import(_, span, _) => span,
}
}
@ -220,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
ast::Expr::FuncCall(call) => {
DerefTarget::Callee(expr_node.find(call.callee().span())?)
}
ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?),
ast::Expr::SetRule(set) => {
DerefTarget::Callee(expr_node.find(set.target().span())?)
}
ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => {
DerefTarget::VarAccess(expr_node)
}
@ -269,16 +283,18 @@ mod tests {
use std::borrow::Borrow;
use ecow::EcoString;
use typst::foundations::Value;
use typst::syntax::{LinkedNode, Side};
use super::named_items;
use crate::tests::{FilePos, WorldLike};
use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = Vec<EcoString>;
type Response = Vec<(EcoString, Option<Value>)>;
trait ResponseExt {
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self;
}
impl ResponseExt for Response {
@ -286,7 +302,7 @@ mod tests {
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in includes {
assert!(
self.iter().any(|v| v == item),
self.iter().any(|v| v.0 == item),
"{item:?} was not contained in {self:?}",
);
}
@ -297,12 +313,21 @@ mod tests {
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in excludes {
assert!(
!self.iter().any(|v| v == item),
!self.iter().any(|v| v.0 == item),
"{item:?} was wrongly contained in {self:?}",
);
}
self
}
#[track_caller]
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self {
assert!(
self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value),
"{name_value:?} was not contained in {self:?}",
);
self
}
}
#[track_caller]
@ -314,7 +339,7 @@ mod tests {
let leaf = node.leaf_at(cursor, Side::After).unwrap();
let mut items = vec![];
named_items(world, leaf, |s| {
items.push(s.name().clone());
items.push((s.name().clone(), s.value().clone()));
None::<()>
});
items
@ -339,6 +364,21 @@ mod tests {
#[test]
fn test_named_items_import() {
test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]);
test("#import \"foo.typ\"", 2).must_include(["foo"]);
test("#import \"foo.typ\" as bar", 2)
.must_include(["bar"])
.must_exclude(["foo"]);
}
#[test]
fn test_named_items_import_items() {
test("#import \"foo.typ\": a; #(a);", 2)
.must_include(["a"])
.must_exclude(["foo"]);
let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);")
.with_source("foo.typ", "#import \"a.typ\"")
.with_source("a.typ", "#let b = 1;");
test(&world, 2).must_include_value(("b", Some(&Value::Int(1))));
}
}

View File

@ -55,7 +55,7 @@ impl TestWorld {
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path));
let data = typst_dev_assets::get_by_name(filename).unwrap();
let bytes = Bytes::from_static(data);
let bytes = Bytes::new(data);
Arc::make_mut(&mut self.files).assets.insert(id, bytes);
self
}
@ -152,7 +152,7 @@ impl Default for TestBase {
fn default() -> Self {
let fonts: Vec<_> = typst_assets::fonts()
.chain(typst_dev_assets::fonts())
.flat_map(|data| Font::iter(Bytes::from_static(data)))
.flat_map(|data| Font::iter(Bytes::new(data)))
.collect();
Self {

View File

@ -3,7 +3,7 @@ use std::fmt::Write;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use typst::engine::Sink;
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
use typst::foundations::{repr, Binding, Capturer, CastInfo, Repr, Value};
use typst::layout::{Length, PagedDocument};
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
@ -201,12 +201,17 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Toolti
if let Some(expr) = grand_grand.cast::<ast::Expr>();
if let Some(ast::Expr::Ident(callee)) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()),
ast::Expr::SetRule(set) => Some(set.target()),
_ => None,
};
// Find metadata about the function.
if let Some(Value::Func(func)) = world.library().global.scope().get(&callee);
if let Some(Value::Func(func)) = world
.library()
.global
.scope()
.get(&callee)
.map(Binding::read);
then { (func, named) }
else { return None; }
};
@ -352,6 +357,13 @@ mod tests {
.must_be_text("This closure captures `f` and `y`");
}
#[test]
fn test_tooltip_import() {
let world = TestWorld::new("#import \"other.typ\": a, b")
.with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
test(&world, -5, Side::After).must_be_code("1");
}
#[test]
fn test_tooltip_star_import() {
let world = TestWorld::new("#import \"other.typ\": *")

View File

@ -171,7 +171,7 @@ where
self.find_iter(content.fields().iter().map(|(_, v)| v))?;
}
Value::Module(module) => {
self.find_iter(module.scope().iter().map(|(_, v, _)| v))?;
self.find_iter(module.scope().iter().map(|(_, b)| b.read()))?;
}
_ => {}
}

View File

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

View File

@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use fontdb::{Database, Source};
use typst_library::foundations::Bytes;
use typst_library::text::{Font, FontBook, FontInfo};
use typst_timing::TimingScope;
@ -52,9 +53,8 @@ impl FontSlot {
.as_ref()
.expect("`path` is not `None` if `font` is uninitialized"),
)
.ok()?
.into();
Font::new(data, self.index)
.ok()?;
Font::new(Bytes::new(data), self.index)
})
.clone()
}
@ -196,7 +196,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) {
for data in typst_assets::fonts() {
let buffer = typst_library::foundations::Bytes::from_static(data);
let buffer = Bytes::new(data);
for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone());
self.fonts.push(FontSlot {

View File

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

View File

@ -20,12 +20,16 @@ use typst_library::model::ParElem;
use typst_library::routines::{Pair, Routines};
use typst_library::text::TextElem;
use typst_library::World;
use typst_utils::SliceExt;
use super::{layout_multi_block, layout_single_block};
use super::{layout_multi_block, layout_single_block, FlowMode};
use crate::inline::ParSituation;
use crate::modifiers::layout_and_modify;
/// Collects all elements of the flow into prepared children. These are much
/// simpler to handle than the raw elements.
#[typst_macros::time]
#[allow(clippy::too_many_arguments)]
pub fn collect<'a>(
engine: &mut Engine,
bump: &'a Bump,
@ -33,6 +37,7 @@ pub fn collect<'a>(
locator: Locator<'a>,
base: Size,
expand: bool,
mode: FlowMode,
) -> SourceResult<Vec<Child<'a>>> {
Collector {
engine,
@ -42,9 +47,9 @@ pub fn collect<'a>(
base,
expand,
output: Vec::with_capacity(children.len()),
last_was_par: false,
par_situation: ParSituation::First,
}
.run()
.run(mode)
}
/// State for collection.
@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> {
expand: bool,
locator: SplitLocator<'a>,
output: Vec<Child<'a>>,
last_was_par: bool,
par_situation: ParSituation,
}
impl<'a> Collector<'a, '_, '_> {
/// Perform the collection.
fn run(mut self) -> SourceResult<Vec<Child<'a>>> {
fn run(self, mode: FlowMode) -> SourceResult<Vec<Child<'a>>> {
match mode {
FlowMode::Root | FlowMode::Block => self.run_block(),
FlowMode::Inline => self.run_inline(),
}
}
/// Perform collection for block-level children.
fn run_block(mut self) -> SourceResult<Vec<Child<'a>>> {
for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::<TagElem>() {
self.output.push(Child::Tag(&elem.tag));
@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> {
Ok(self.output)
}
/// Perform collection for inline-level children.
fn run_inline(mut self) -> SourceResult<Vec<Child<'a>>> {
// Extract leading and trailing tags.
let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::<TagElem>());
let inner = &self.children[start..end];
// Compute the shared styles, ignoring tags.
let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default();
// Layout the lines.
let lines = crate::inline::layout_inline(
self.engine,
inner,
&mut self.locator,
styles,
self.base,
self.expand,
)?
.into_frames();
for (c, _) in &self.children[..start] {
let elem = c.to_packed::<TagElem>().unwrap();
self.output.push(Child::Tag(&elem.tag));
}
let leading = ParElem::leading_in(styles);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().unwrap();
self.output.push(Child::Tag(&elem.tag));
}
Ok(self.output)
}
/// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount {
@ -109,24 +158,35 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<ParElem>,
styles: StyleChain<'a>,
) -> SourceResult<()> {
let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let spacing = ParElem::spacing_in(styles);
let costs = TextElem::costs_in(styles);
let lines = crate::layout_inline(
let lines = crate::inline::layout_par(
elem,
self.engine,
&elem.children,
self.locator.next(&elem.span()),
styles,
self.last_was_par,
self.base,
self.expand,
self.par_situation,
)?
.into_frames();
let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
self.output.push(Child::Rel(spacing.into(), 4));
self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive;
Ok(())
}
/// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles);
let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans.
let len = lines.len();
let prevent_orphans =
@ -165,11 +225,6 @@ impl<'a> Collector<'a, '_, '_> {
self.output
.push(Child::Line(self.boxed(LineChild { frame, align, need })));
}
self.output.push(Child::Rel(spacing.into(), 4));
self.last_was_par = true;
Ok(())
}
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
@ -218,7 +273,7 @@ impl<'a> Collector<'a, '_, '_> {
};
self.output.push(spacing(elem.below(styles)));
self.last_was_par = false;
self.par_situation = ParSituation::Other;
}
/// Collects a placed element into a [`PlacedChild`].
@ -377,8 +432,9 @@ fn layout_single_impl(
route: Route::extend(route),
};
layout_single_block(elem, &mut engine, locator, styles, region)
.map(|frame| frame.post_processed(styles))
layout_and_modify(styles, |styles| {
layout_single_block(elem, &mut engine, locator, styles, region)
})
}
/// A child that encapsulates a prepared breakable block.
@ -473,11 +529,8 @@ fn layout_multi_impl(
route: Route::extend(route),
};
layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| {
for frame in &mut fragment {
frame.post_process(styles);
}
fragment
layout_and_modify(styles, |styles| {
layout_multi_block(elem, &mut engine, locator, styles, regions)
})
}
@ -579,20 +632,23 @@ impl PlacedChild<'_> {
self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap();
let styles = self.styles.chain(&aligned);
let mut frame = crate::layout_frame(
engine,
&self.elem.body,
self.locator.relayout(),
self.styles.chain(&aligned),
Region::new(base, Axes::splat(false)),
)?;
let mut frame = layout_and_modify(styles, |styles| {
crate::layout_frame(
engine,
&self.elem.body,
self.locator.relayout(),
styles,
Region::new(base, Axes::splat(false)),
)
})?;
if self.float {
frame.set_parent(self.elem.location().unwrap());
}
Ok(frame.post_processed(self.styles))
Ok(frame)
})
}

View File

@ -17,7 +17,9 @@ use typst_library::model::{
use typst_syntax::Span;
use typst_utils::{NonZeroExt, Numeric};
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
use super::{
distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work,
};
/// Composes the contents of a single page/region. A region can have multiple
/// columns/subregions.
@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
migratable: bool,
) -> FlowResult<()> {
// Footnotes are only supported at the root level.
if !self.config.root {
if self.config.mode != FlowMode::Root {
return Ok(());
}

View File

@ -25,7 +25,7 @@ use typst_library::layout::{
Regions, Rel, Size,
};
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::TextElem;
use typst_library::World;
use typst_utils::{NonZeroExt, Numeric};
@ -140,9 +140,10 @@ fn layout_fragment_impl(
engine.route.check_layout_depth().at(content.span())?;
let mut kind = FragmentKind::Block;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::LayoutFragment,
RealizationKind::LayoutFragment(&mut kind),
&mut engine,
&mut locator,
&arenas,
@ -158,62 +159,45 @@ fn layout_fragment_impl(
regions,
columns,
column_gutter,
false,
kind.into(),
)
}
/// The mode a flow can be laid out in.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum FlowMode {
/// A root flow with block-level elements. Like `FlowMode::Block`, but can
/// additionally host footnotes and line numbers.
Root,
/// A flow whose children are block-level elements.
Block,
/// A flow whose children are inline-level elements.
Inline,
}
impl From<FragmentKind> for FlowMode {
fn from(value: FragmentKind) -> Self {
match value {
FragmentKind::Inline => Self::Inline,
FragmentKind::Block => Self::Block,
}
}
}
/// Lays out realized content into regions, potentially with columns.
#[allow(clippy::too_many_arguments)]
pub(crate) fn layout_flow(
pub fn layout_flow<'a>(
engine: &mut Engine,
children: &[Pair],
locator: &mut SplitLocator,
shared: StyleChain,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
shared: StyleChain<'a>,
mut regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
root: bool,
mode: FlowMode,
) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow.
let config = Config {
root,
shared,
columns: {
let mut count = columns.get();
if !regions.size.x.is_finite() {
count = 1;
}
let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared);
ColumnConfig { count, width, gutter, dir }
},
footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x,
},
line_numbers: root.then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
PageElem::height_in(shared)
} else {
PageElem::width_in(shared)
};
// Clamp below is safe (min <= max): if the font size is
// negative, we set min = max = 0; otherwise,
// `0.75 * size <= 2.5 * size` for zero and positive sizes.
(0.026 * width.unwrap_or_default()).clamp(
Em::new(0.75).resolve(shared).max(Abs::zero()),
Em::new(2.5).resolve(shared).max(Abs::zero()),
)
},
}),
};
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.
@ -225,6 +209,7 @@ pub(crate) fn layout_flow(
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
@ -247,6 +232,55 @@ pub(crate) fn layout_flow(
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: {
let mut count = columns.get();
if !regions.size.x.is_finite() {
count = 1;
}
let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared);
ColumnConfig { count, width, gutter, dir }
},
footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x,
},
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
PageElem::height_in(shared)
} else {
PageElem::width_in(shared)
};
// Clamp below is safe (min <= max): if the font size is
// negative, we set min = max = 0; otherwise,
// `0.75 * size <= 2.5 * size` for zero and positive sizes.
(0.026 * width.unwrap_or_default()).clamp(
Em::new(0.75).resolve(shared).max(Abs::zero()),
Em::new(2.5).resolve(shared).max(Abs::zero()),
)
},
}),
}
}
/// The work that is left to do by flow layout.
///
/// The lifetimes 'a and 'b are used across flow layout:
@ -318,7 +352,7 @@ impl<'a, 'b> Work<'a, 'b> {
struct Config<'x> {
/// Whether this is the root flow, which can host footnotes and line
/// numbers.
root: bool,
mode: FlowMode,
/// The styles shared by the whole flow. This is used for footnotes and line
/// numbers.
shared: StyleChain<'x>,

View File

@ -3,6 +3,7 @@ use std::fmt::Debug;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable};
use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing,
@ -13,8 +14,8 @@ use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric};
use super::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid,
LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup,
generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
LineSegment, Rowspan, UnbreakableRowGroup,
};
/// Performs grid layout.
@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> {
let size = Size::new(available, height);
let pod = Region::new(size, Axes::splat(false));
let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame();
let frame =
layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame();
resolved.set_max(frame.width() - already_covered_width);
}
@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> {
};
let frames =
cell.layout(engine, disambiguator, self.styles, pod)?.into_frames();
layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames();
// Skip the first region if one cell in it is empty. Then,
// remeasure.
@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> {
// rows.
pod.full = self.regions.full;
}
let frame = cell
.layout(engine, disambiguator, self.styles, pod)?
.into_frame();
let frame =
layout_cell(cell, engine, disambiguator, self.styles, pod)?
.into_frame();
let mut pos = pos;
if self.is_rtl {
// In the grid, cell colspans expand to the right,
@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames.
let fragment =
cell.layout(engine, disambiguator, self.styles, pod)?;
layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos;
if self.is_rtl {
@ -1375,7 +1377,7 @@ impl<'a> GridLayouter<'a> {
.footer
.as_ref()
.and_then(Repeatable::as_repeated)
.map_or(true, |footer| footer.start != header.end)
.is_none_or(|footer| footer.start != header.end)
&& self.lrows.last().is_some_and(|row| row.index() < header.end)
&& !in_last_with_offset(
self.regions,
@ -1444,7 +1446,7 @@ impl<'a> GridLayouter<'a> {
.iter_mut()
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
.filter(|rowspan| {
rowspan.max_resolved_row.map_or(true, |max_row| y > max_row)
rowspan.max_resolved_row.is_none_or(|max_row| y > max_row)
})
{
// If the first region wasn't defined yet, it will have the
@ -1492,7 +1494,7 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even
// on this row's first frame.
if laid_out_footer_start.map_or(true, |footer_start| {
if laid_out_footer_start.is_none_or(|footer_start| {
// If this is a footer row, then only lay out this rowspan
// if the rowspan is contained within the footer.
y < footer_start || rowspan.y >= footer_start
@ -1578,5 +1580,5 @@ pub(super) fn points(
/// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
regions.backlog.is_empty()
&& regions.last.map_or(true, |height| regions.size.y + offset == height)
&& regions.last.is_none_or(|height| regions.size.y + offset == height)
}

View File

@ -1,41 +1,11 @@
use std::num::NonZeroUsize;
use std::sync::Arc;
use typst_library::foundations::{AlternativeFold, Fold};
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs;
use typst_library::visualize::Stroke;
use super::{CellGrid, LinePosition, Repeatable, RowPiece};
/// Represents an explicit grid line (horizontal or vertical) specified by the
/// user.
pub struct Line {
/// The index of the track after this line. This will be the index of the
/// row a horizontal line is above of, or of the column right after a
/// vertical line.
///
/// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols`
/// or `grid.rows`, ignoring gutter tracks, as appropriate).
pub index: usize,
/// The index of the track at which this line starts being drawn.
/// This is the first column a horizontal line appears in, or the first row
/// a vertical line appears in.
///
/// Must be within `0..tracks.len()` minus gutter tracks.
pub start: usize,
/// The index after the last track through which the line is drawn.
/// Thus, the line is drawn through tracks `start..end` (note that `end` is
/// exclusive).
///
/// Must be within `1..=tracks.len()` minus gutter tracks.
/// `None` indicates the line should go all the way to the end.
pub end: Option<NonZeroUsize>,
/// The line's stroke. This is `None` when the line is explicitly used to
/// override a previously specified line.
pub stroke: Option<Arc<Stroke<Abs>>>,
/// The line's position in relation to the track with its index.
pub position: LinePosition,
}
use super::RowPiece;
/// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke.
@ -493,7 +463,7 @@ pub fn hline_stroke_at_column(
// region, we have the last index, and (as a failsafe) we don't have the
// last row of cells above us.
let use_bottom_border_stroke = !in_last_region
&& local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len())
&& local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len())
&& y == grid.rows.len();
let bottom_y =
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };
@ -588,13 +558,13 @@ pub fn hline_stroke_at_column(
#[cfg(test)]
mod test {
use std::num::NonZeroUsize;
use typst_library::foundations::Content;
use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt;
use super::super::cells::Entry;
use super::super::Cell;
use super::*;
fn sample_cell() -> Cell<'static> {

View File

@ -1,40 +1,44 @@
mod cells;
mod layouter;
mod lines;
mod repeated;
mod rowspans;
pub use self::cells::{Cell, CellGrid};
pub use self::layouter::GridLayouter;
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::eco_format;
use typst_library::diag::{SourceResult, Trace, Tracepoint};
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Fold, Packed, Smart, StyleChain};
use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length,
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides,
};
use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
use typst_library::text::TextElem;
use typst_library::visualize::{Paint, Stroke};
use typst_syntax::Span;
use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell};
use typst_library::layout::{Fragment, GridElem, Regions};
use typst_library::model::TableElem;
use self::cells::{
LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
};
use self::layouter::RowPiece;
use self::lines::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
LineSegment,
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment,
};
use self::repeated::{Footer, Header, Repeatable};
use self::rowspans::{Rowspan, UnbreakableRowGroup};
/// Layout the cell into the given regions.
///
/// The `disambiguator` indicates which instance of this cell this should be
/// layouted as. For normal cells, it is always `0`, but for headers and
/// footers, it indicates the index of the header/footer among all. See the
/// [`Locator`] docs for more details on the concepts behind this.
pub fn layout_cell(
cell: &Cell,
engine: &mut Engine,
disambiguator: usize,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut locator = cell.locator.relayout();
if disambiguator > 0 {
locator = locator.split().next_inner(disambiguator as u128);
}
crate::layout_fragment(engine, &cell.body, locator, styles, regions)
}
/// Layout the grid.
#[typst_macros::time(span = elem.span())]
pub fn layout_grid(
@ -44,54 +48,8 @@ pub fn layout_grid(
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
let children = elem.children().iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
GridChild::Item(item) => {
ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
let grid = grid_to_cellgrid(elem, engine, locator, styles)?;
GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
}
/// Layout the table.
@ -103,314 +61,6 @@ pub fn layout_table(
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
let children = elem.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
TableChild::Item(item) => {
ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
fn grid_item_to_resolvable(
item: &GridItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<GridCell>> {
match item {
GridItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
GridItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
fn table_item_to_resolvable(
item: &TableItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<TableCell>> {
match item {
TableItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
TableItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
impl ResolvableCell for Packed<TableCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the table is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on table cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}
impl ResolvableCell for Packed<GridCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the grid is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on grid cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
let grid = table_to_cellgrid(elem, engine, locator, styles)?;
GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
}

View File

@ -1,50 +1,11 @@
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::GridLayouter;
use super::rowspans::UnbreakableRowGroup;
/// A repeatable grid header. Starts at the first row.
pub struct Header {
/// The index after the last row included in this header.
pub end: usize,
}
/// A repeatable grid footer. Stops at the last row.
pub struct Footer {
/// The first row included in this footer.
pub start: usize,
}
/// A possibly repeatable grid object.
/// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable).
pub enum Repeatable<T> {
Repeated(T),
NotRepeated(T),
}
impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether
/// it repeats.
pub fn unwrap(&self) -> &T {
match self {
Self::Repeated(repeated) => repeated,
Self::NotRepeated(not_repeated) => not_repeated,
}
}
/// Returns `Some` if the value is repeated, `None` otherwise.
pub fn as_repeated(&self) -> Option<&T> {
match self {
Self::Repeated(repeated) => Some(repeated),
Self::NotRepeated(_) => None,
}
}
}
impl GridLayouter<'_> {
/// Layouts the header's rows.
/// Skips regions as necessary.

View File

@ -1,12 +1,12 @@
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::repeated::Repeatable;
use super::{Cell, GridLayouter};
use super::{layout_cell, Cell, GridLayouter};
/// All information needed to layout a single rowspan.
pub struct Rowspan {
@ -141,7 +141,7 @@ impl GridLayouter<'_> {
}
// Push the layouted frames directly into the finished frames.
let fragment = cell.layout(engine, disambiguator, self.styles, pod)?;
let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self
.finished
@ -588,7 +588,7 @@ impl GridLayouter<'_> {
measurement_data: &CellMeasurementData<'_>,
) -> bool {
if sizes.len() <= 1
&& sizes.first().map_or(true, |&first_frame_size| {
&& sizes.first().is_none_or(|&first_frame_size| {
first_frame_size <= measurement_data.height_in_this_region
})
{

View File

@ -1,16 +1,17 @@
use std::ffi::OsStr;
use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
use typst_library::diag::{warning, At, SourceResult, StrResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
};
use typst_library::loading::Readable;
use typst_library::loading::DataSource;
use typst_library::text::families;
use typst_library::visualize::{
Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat,
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
RasterImage, SvgImage, VectorFormat,
};
/// Layout the image.
@ -26,17 +27,17 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let data = elem.data();
let Derived { source, derived: data } = &elem.source;
let format = match elem.format(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
Smart::Auto => determine_format(source, data).at(span)?,
};
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
if has_foreign_object {
engine.sink.warn(warning!(
@ -49,15 +50,26 @@ pub fn layout_image(
}
// Construct the image itself.
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
elem.flatten_text(styles),
)
.at(span)?;
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
data.clone(),
format,
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.at(span)?,
),
};
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
// Determine the image's pixel aspect ratio.
let pxw = image.width();
@ -83,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(
@ -119,25 +133,23 @@ pub fn layout_image(
Ok(frame)
}
/// Determine the image format based on path and data.
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
let ext = std::path::Path::new(path)
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
/// Try to determine the image format based on the data.
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
if let DataSource::Path(path) = source {
let ext = std::path::Path::new(path.as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
Ok(match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => match &data {
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
Some(f) => ImageFormat::Raster(f),
None => bail!("unknown image format"),
},
},
})
match ext.as_str() {
"png" => return Ok(ExchangeFormat::Png.into()),
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
_ => {}
}
}
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
}

View File

@ -11,7 +11,7 @@ use typst_utils::Numeric;
use crate::flow::unbreakable_pod;
use crate::shapes::{clip_rect, fill_and_stroke};
/// Lay out a box as part of a paragraph.
/// Lay out a box as part of inline layout.
#[typst_macros::time(name = "box", span = elem.span())]
pub fn layout_box(
elem: &Packed<BoxElem>,

View File

@ -1,10 +1,10 @@
use typst_library::diag::bail;
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::routines::Pair;
use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
SpaceElem, TextElem,
@ -13,9 +13,10 @@ use typst_syntax::Span;
use typst_utils::Numeric;
use super::*;
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
// The characters by which spacing, inline content and pins are replaced in the
// paragraph's full text.
// full text.
const SPACING_REPLACE: &str = " "; // Space
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character
@ -26,7 +27,7 @@ const POP_EMBEDDING: &str = "\u{202C}";
const LTR_ISOLATE: &str = "\u{2066}";
const POP_ISOLATE: &str = "\u{2069}";
/// A prepared item in a paragraph layout.
/// A prepared item in a inline layout.
#[derive(Debug)]
pub enum Item<'a> {
/// A shaped text run with consistent style and direction.
@ -36,7 +37,7 @@ pub enum Item<'a> {
/// Fractional spacing between other items.
Fractional(Fr, Option<(&'a Packed<BoxElem>, Locator<'a>, StyleChain<'a>)>),
/// Layouted inline-level content.
Frame(Frame, StyleChain<'a>),
Frame(Frame),
/// A tag.
Tag(&'a Tag),
/// An item that is invisible and needs to be skipped, e.g. a Unicode
@ -67,7 +68,7 @@ impl<'a> Item<'a> {
match self {
Self::Text(shaped) => shaped.text,
Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE,
Self::Frame(_, _) => OBJ_REPLACE,
Self::Frame(_) => OBJ_REPLACE,
Self::Tag(_) => "",
Self::Skip(s) => s,
}
@ -83,7 +84,7 @@ impl<'a> Item<'a> {
match self {
Self::Text(shaped) => shaped.width,
Self::Absolute(v, _) => *v,
Self::Frame(frame, _) => frame.width(),
Self::Frame(frame) => frame.width(),
Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(),
Self::Skip(_) => Abs::zero(),
}
@ -112,38 +113,31 @@ impl Segment<'_> {
}
}
/// Collects all text of the paragraph into one string and a collection of
/// segments that correspond to pieces of that string. This also performs
/// string-level preprocessing like case transformations.
/// Collects all text into one string and a collection of segments that
/// correspond to pieces of that string. This also performs string-level
/// preprocessing like case transformations.
#[typst_macros::time]
pub fn collect<'a>(
children: &'a StyleVec,
children: &[Pair<'a>],
engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>,
styles: &'a StyleChain<'a>,
config: &Config,
region: Size,
consecutive: bool,
) -> 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);
let first_line_indent = ParElem::first_line_indent_in(*styles);
if !first_line_indent.is_zero()
&& consecutive
&& AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into()
{
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
if !config.first_line_indent.is_zero() {
collector.push_item(Item::Absolute(config.first_line_indent, false));
collector.spans.push(1, Span::detached());
}
let hang = ParElem::hanging_indent_in(*styles);
if !hang.is_zero() {
collector.push_item(Item::Absolute(-hang, false));
if !config.hanging_indent.is_zero() {
collector.push_item(Item::Absolute(-config.hanging_indent, false));
collector.spans.push(1, Span::detached());
}
for (child, styles) in children.iter(styles) {
for &(child, styles) in children {
let prev_len = collector.full.len();
if child.is::<SpaceElem>() {
@ -151,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),
@ -161,24 +155,23 @@ pub fn collect<'a>(
}
if let Some(case) = TextElem::case_in(styles) {
full.push_str(&case.apply(elem.text()));
full.push_str(&case.apply(&elem.text));
} else {
full.push_str(elem.text());
full.push_str(&elem.text);
}
if dir != outer_dir {
if dir != config.dir {
// Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING);
}
});
} else if let Some(elem) = child.to_packed::<HElem>() {
let amount = elem.amount();
if amount.is_zero() {
if elem.amount.is_zero() {
continue;
}
collector.push_item(match amount {
Spacing::Fr(fr) => Item::Fractional(*fr, None),
collector.push_item(match elem.amount {
Spacing::Fr(fr) => Item::Fractional(fr, None),
Spacing::Rel(rel) => Item::Absolute(
rel.resolve(styles).relative_to(region.x),
elem.weak(styles),
@ -211,8 +204,10 @@ pub fn collect<'a>(
InlineItem::Space(space, weak) => {
collector.push_item(Item::Absolute(space, weak));
}
InlineItem::Frame(frame) => {
collector.push_item(Item::Frame(frame, styles));
InlineItem::Frame(mut frame) => {
frame.modify(&FrameModifiers::get_in(styles));
apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame));
}
}
}
@ -223,13 +218,22 @@ pub fn collect<'a>(
if let Sizing::Fr(v) = elem.width(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else {
let frame = layout_box(elem, engine, loc, styles, region)?;
collector.push_item(Item::Frame(frame, styles));
let mut frame = layout_and_modify(styles, |styles| {
layout_box(elem, engine, loc, styles, region)
})?;
apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame));
}
} else if let Some(elem) = child.to_packed::<TagElem>() {
collector.push_item(Item::Tag(&elem.tag));
} else {
bail!(child.span(), "unexpected paragraph child");
// Non-paragraph inline layout should never trigger this since it
// only won't be triggered if we see any non-inline content.
engine.sink.warn(warning!(
child.span(),
"{} may not occur inside of a paragraph and was ignored",
child.func().name()
));
};
let len = collector.full.len() - prev_len;

View File

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

View File

@ -2,14 +2,14 @@ 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;
use super::*;
use crate::modifiers::layout_and_modify;
const SHY: char = '\u{ad}';
const HYPHEN: char = '-';
@ -17,12 +17,12 @@ const EN_DASH: char = '';
const EM_DASH: char = '—';
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks.
/// A layouted line, consisting of a sequence of layouted paragraph items that
/// are mostly borrowed from the preparation phase. This type enables you to
/// measure the size of a line in a range before committing to building the
/// line's frame.
/// A layouted line, consisting of a sequence of layouted inline items that are
/// mostly borrowed from the preparation phase. This type enables you to measure
/// the size of a line in a range before committing to building the line's
/// frame.
///
/// At most two paragraph items must be created individually for this line: The
/// At most two inline items must be created individually for this line: The
/// first and last one since they may be broken apart by the start or end of the
/// line, respectively. But even those can partially reuse previous results when
/// the break index is safe-to-break per rustybuzz.
@ -93,7 +93,7 @@ impl Line<'_> {
pub fn has_negative_width_items(&self) -> bool {
self.items.iter().any(|item| match item {
Item::Absolute(amount, _) => *amount < Abs::zero(),
Item::Frame(frame, _) => frame.width() < Abs::zero(),
Item::Frame(frame) => frame.width() < Abs::zero(),
_ => false,
})
}
@ -134,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) {
@ -154,16 +154,16 @@ pub fn line<'a>(
let mut items = collect_items(engine, p, range, trim);
// Add a hyphen at the line start, if a previous dash should be repeated.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) {
if pred.is_some_and(|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);
}
}
@ -233,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;
}
@ -307,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);
}
@ -331,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();
@ -358,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()
{
@ -403,12 +406,17 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
//
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()),
Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()),
_ => false,
}
}
/// Apply the current baseline shift to a frame.
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
}
/// Commit to a line and build its frame.
#[allow(clippy::too_many_arguments)]
pub fn commit(
@ -418,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. When the paragraph is RTL, the
// 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.
@ -509,10 +516,11 @@ pub fn commit(
let amount = v.share(fr, remaining);
if let Some((elem, loc, styles)) = elem {
let region = Size::new(amount, full);
let mut frame =
layout_box(elem, engine, loc.relayout(), *styles, region)?;
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), styles, region)
})?;
apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame);
} else {
offset += amount;
}
@ -524,12 +532,10 @@ pub fn commit(
justification_ratio,
extra_justification,
);
push(&mut offset, frame.post_processed(shaped.styles));
push(&mut offset, frame);
}
Item::Frame(frame, styles) => {
let mut frame = frame.clone();
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
Item::Frame(frame) => {
push(&mut offset, frame.clone());
}
Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
@ -549,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);
}
@ -570,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);
@ -601,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)));
}
@ -626,7 +626,7 @@ fn overhang(c: char) -> f64 {
}
}
/// A collection of owned or borrowed paragraph items.
/// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec<ItemEntry<'a>>);
impl<'a> Items<'a> {

View File

@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation;
use super::*;
/// The cost of a line or paragraph layout.
/// The cost of a line or inline layout.
type Cost = f64;
// Cost parameters.
@ -104,21 +104,13 @@ impl Breakpoint {
}
}
/// Breaks the paragraph into lines.
/// Breaks the text into lines.
pub fn linebreak<'a>(
engine: &Engine,
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),
}
@ -181,13 +173,12 @@ fn linebreak_simple<'a>(
/// lines with hyphens even more.
///
/// To find the layout with the minimal total cost the algorithm uses dynamic
/// programming: For each possible breakpoint it determines the optimal
/// paragraph layout _up to that point_. It walks over all possible start points
/// for a line ending at that point and finds the one for which the cost of the
/// line plus the cost of the optimal paragraph up to the start point (already
/// computed and stored in dynamic programming table) is minimal. The final
/// result is simply the layout determined for the last breakpoint at the end of
/// text.
/// programming: For each possible breakpoint, it determines the optimal layout
/// _up to that point_. It walks over all possible start points for a line
/// ending at that point and finds the one for which the cost of the line plus
/// the cost of the optimal layout up to the start point (already computed and
/// stored in dynamic programming table) is minimal. The final result is simply
/// the layout determined for the last breakpoint at the end of text.
#[typst_macros::time]
fn linebreak_optimized<'a>(
engine: &Engine,
@ -215,7 +206,7 @@ fn linebreak_optimized_bounded<'a>(
metrics: &CostMetrics,
upper_bound: Cost,
) -> Vec<Line<'a>> {
/// An entry in the dynamic programming table for paragraph optimization.
/// An entry in the dynamic programming table for inline layout optimization.
struct Entry<'a> {
pred: usize,
total: Cost,
@ -299,7 +290,7 @@ fn linebreak_optimized_bounded<'a>(
}
// If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) {
if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { pred: pred_index, total, line: attempt, end });
}
}
@ -321,7 +312,7 @@ fn linebreak_optimized_bounded<'a>(
// This should only happen if our bound was faulty. Which shouldn't happen!
if table[idx].end != p.text.len() {
#[cfg(debug_assertions)]
panic!("bounded paragraph layout is incomplete");
panic!("bounded inline layout is incomplete");
#[cfg(not(debug_assertions))]
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
@ -342,7 +333,7 @@ fn linebreak_optimized_bounded<'a>(
/// (which is costly) to determine costs, it determines approximate costs using
/// cumulative arrays.
///
/// This results in a likely good paragraph layouts, for which we then compute
/// This results in a likely good inline layouts, for which we then compute
/// the exact cost. This cost is an upper bound for proper optimized
/// linebreaking. We can use it to heavily prune the search space.
#[typst_macros::time]
@ -355,7 +346,7 @@ fn linebreak_optimized_approximate(
// Determine the cumulative estimation metrics.
let estimates = Estimates::compute(p);
/// An entry in the dynamic programming table for paragraph optimization.
/// An entry in the dynamic programming table for inline layout optimization.
struct Entry {
pred: usize,
total: Cost,
@ -385,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
@ -432,7 +423,7 @@ fn linebreak_optimized_approximate(
let total = pred.total + line_cost;
// If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) {
if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry {
pred: pred_index,
total,
@ -574,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
@ -664,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,
};
@ -831,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))
@ -862,17 +853,17 @@ struct CostMetrics {
}
impl CostMetrics {
/// Compute shared metrics for paragraph optimization.
/// Compute shared metrics for inline layout optimization.
fn compute(p: &Preparation) -> Self {
Self {
// When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 },
min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size),
approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
// Costs.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
}
}

View File

@ -13,17 +13,22 @@ 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::{StyleChain, StyleVec};
use typst_library::introspection::{Introspector, Locator, LocatorLink};
use typst_library::layout::{Fragment, Size};
use typst_library::model::ParElem;
use typst_library::routines::Routines;
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
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;
use self::finalize::finalize;
use self::line::{commit, line, Line};
use self::line::{apply_baseline_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation};
use self::shaping::{
@ -34,18 +39,18 @@ use self::shaping::{
/// Range of a substring of text.
type Range = std::ops::Range<usize>;
/// Layouts content inline.
pub fn layout_inline(
/// Layouts the paragraph.
pub fn layout_par(
elem: &Packed<ParElem>,
engine: &mut Engine,
children: &StyleVec,
locator: Locator,
styles: StyleChain,
consecutive: bool,
region: Size,
expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> {
layout_inline_impl(
children,
layout_par_impl(
elem,
engine.routines,
engine.world,
engine.introspector,
@ -54,17 +59,17 @@ pub fn layout_inline(
engine.route.track(),
locator.track(),
styles,
consecutive,
region,
expand,
situation,
)
}
/// The internal, memoized implementation of `layout_inline`.
/// The internal, memoized implementation of `layout_par`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn layout_inline_impl(
children: &StyleVec,
fn layout_par_impl(
elem: &Packed<ParElem>,
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
@ -73,12 +78,12 @@ fn layout_inline_impl(
route: Tracked<Route>,
locator: Tracked<Locator>,
styles: StyleChain,
consecutive: bool,
region: Size,
expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> {
let link = LocatorLink::new(locator);
let locator = Locator::link(&link);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
@ -88,18 +93,227 @@ fn layout_inline_impl(
route: Route::extend(route),
};
let mut locator = locator.split();
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::LayoutPar,
&mut engine,
&mut locator,
&arenas,
&elem.body,
styles,
)?;
layout_inline_impl(
&mut engine,
&children,
&mut locator,
styles,
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.
pub fn layout_inline<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'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, &mut engine, &mut locator, &styles, region, consecutive)?;
let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
// Perform BiDi analysis and then prepares paragraph layout.
let p = prepare(&mut engine, children, &text, segments, spans, styles)?;
// Perform BiDi analysis and performs some preparation steps before we
// proceed to line breaking.
let p = prepare(engine, &config, &text, segments, spans)?;
// Break the paragraph into lines.
let lines = linebreak(&engine, &p, region.x - p.hang);
// Break the text into lines.
let lines = linebreak(engine, &p, region.x - config.hanging_indent);
// Turn the selected lines into frames.
finalize(&mut engine, &p, &lines, styles, region, expand, &mut 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.
///
/// In the form `Option<ParSituation>`, `None` implies that we are creating an
/// inline layout that isn't a semantic paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ParSituation {
/// The paragraph is the first thing in the flow.
First,
/// The paragraph follows another paragraph.
Consecutive,
/// Any other kind of paragraph.
Other,
}
/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
struct ConfigBase {
justify: bool,
linebreaks: Smart<Linebreaks>,
first_line_indent: FirstLineIndent,
hanging_indent: Abs,
}
/// Shared configuration for the whole inline layout.
struct Config {
/// Whether to justify text.
justify: bool,
/// How to determine line breaks.
linebreaks: Linebreaks,
/// The indent the first line of a paragraph should have.
first_line_indent: Abs,
/// The indent that all but the first line of a paragraph should have.
hanging_indent: Abs,
/// Configuration for line numbering.
numbering_marker: Option<Packed<ParLineMarker>>,
/// The resolved horizontal alignment.
align: FixedAlignment,
/// The text size.
font_size: Abs,
/// The dominant direction.
dir: Dir,
/// A uniform hyphenation setting (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
hyphenate: Option<bool>,
/// The text language (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
lang: Option<Lang>,
/// Whether font fallback is enabled.
fallback: bool,
/// Whether to add spacing between CJK and Latin characters.
cjk_latin_spacing: bool,
/// Costs for various layout decisions.
costs: Costs,
}
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
}

View File

@ -1,23 +1,23 @@
use typst_library::foundations::{Resolve, Smart};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::text::{Costs, Lang, TextElem};
use typst_library::layout::{Dir, Em};
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
/// A paragraph representation in which children are already layouted and text
/// is already preshaped.
/// A representation in which children are already layouted and text is already
/// preshaped.
///
/// In many cases, we can directly reuse these results when constructing a line.
/// Only when a line break falls onto a text index that is not safe-to-break per
/// rustybuzz, we have to reshape that portion.
pub struct Preparation<'a> {
/// The paragraph's full text.
/// The full text.
pub text: &'a str,
/// Bidirectional text embedding levels for the paragraph.
/// Configuration for inline layout.
pub config: &'a Config,
/// Bidirectional text embedding levels.
///
/// This is `None` if the paragraph is BiDi-uniform (all the base direction).
/// This is `None` if all text directions are uniform (all the base
/// direction).
pub bidi: Option<BidiInfo<'a>>,
/// Text runs, spacing and layouted elements.
pub items: Vec<(Range, Item<'a>)>,
@ -25,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 paragraph's resolved horizontal alignment.
pub align: FixedAlignment,
/// Whether to justify the paragraph.
pub justify: bool,
/// The paragraph's hanging indent.
pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled for this paragraph.
pub fallback: bool,
/// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>,
/// The text size.
pub size: Abs,
}
impl<'a> Preparation<'a> {
@ -71,20 +49,18 @@ impl<'a> Preparation<'a> {
}
}
/// Performs BiDi analysis and then prepares paragraph layout by building a
/// Performs BiDi analysis and then prepares further layout by building a
/// representation on which we can do line breaking without layouting each and
/// every line from scratch.
#[typst_macros::time]
pub fn prepare<'a>(
engine: &mut Engine,
children: &'a StyleVec,
config: &'a Config,
text: &'a str,
segments: Vec<Segment<'a>>,
spans: SpanMapper,
styles: StyleChain<'a>,
) -> 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(),
};
@ -120,28 +96,17 @@ 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);
}
Ok(Preparation {
config,
text,
bidi: is_bidi.then_some(bidi),
items,
indices,
spans,
hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: children.shared_get(styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang: ParElem::hanging_indent_in(styles),
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
size: TextElem::size_in(styles),
})
}

View File

@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript};
use super::{decorate, Item, Range, SpanMapper};
use crate::modifiers::{FrameModifiers, FrameModify};
/// The result of shaping text.
///
@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper};
/// frame.
#[derive(Clone)]
pub struct ShapedText<'a> {
/// The start of the text in the full paragraph.
/// The start of the text in the full text.
pub base: usize,
/// The text that was shaped.
pub text: &'a str,
@ -65,9 +66,9 @@ pub struct ShapedGlyph {
pub y_offset: Em,
/// The adjustability of the glyph.
pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full paragraph. A cluster
/// is a sequence of one or multiple glyphs that cannot be separated and
/// must always be treated as a union.
/// The byte range of this glyph's cluster in the full inline layout. A
/// cluster is a sequence of one or multiple glyphs that cannot be separated
/// and must always be treated as a union.
///
/// The range values of the glyphs in a [`ShapedText`] should not overlap
/// with each other, and they should be monotonically increasing (for
@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> {
offset += width;
}
frame.modify(&FrameModifiers::get_in(self.styles));
frame
}
@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> {
/// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
///
/// The text `range` is relative to the whole paragraph.
/// The text `range` is relative to the whole inline layout.
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
let text = &self.text[text_range.start - self.base..text_range.end - self.base];
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
@ -463,7 +465,7 @@ impl<'a> ShapedText<'a> {
None
};
let mut chain = families(self.styles)
.filter(|family| family.covers().map_or(true, |c| c.is_match("-")))
.filter(|family| family.covers().is_none_or(|c| c.is_match("-")))
.map(|family| book.select(family.as_str(), self.variant))
.chain(fallback_func.iter().map(|f| f()))
.flatten();
@ -568,7 +570,7 @@ impl<'a> ShapedText<'a> {
// for the next line.
let dec = if ltr { usize::checked_sub } else { usize::checked_add };
while let Some(next) = dec(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) {
if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) {
break;
}
idx = next;
@ -810,7 +812,7 @@ fn shape_segment<'a>(
.nth(1)
.map(|(i, _)| offset + i)
.unwrap_or(text.len());
covers.map_or(true, |cov| cov.is_match(&text[offset..end]))
covers.is_none_or(|cov| cov.is_match(&text[offset..end]))
};
// Collect the shaped glyphs, doing fallback and shaping parts again with

View File

@ -6,6 +6,7 @@ mod image;
mod inline;
mod lists;
mod math;
mod modifiers;
mod pad;
mod pages;
mod repeat;
@ -16,7 +17,6 @@ mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image;
pub use self::inline::{layout_box, layout_inline};
pub use self::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad;

View File

@ -4,11 +4,12 @@ use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, CellGrid};
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
use typst_library::text::TextElem;
use crate::grid::{Cell, CellGrid, GridLayouter};
use crate::grid::GridLayouter;
/// Layout the list.
#[typst_macros::time(span = elem.span())]
@ -21,8 +22,9 @@ pub fn layout_list(
) -> SourceResult<Fragment> {
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -39,12 +41,18 @@ pub fn layout_list(
let mut cells = vec![];
let mut locator = locator.split();
for item in elem.children() {
for item in &elem.children {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
item.body.clone().styled(ListElem::set_depth(Depth(1))),
body.styled(ListElem::set_depth(Depth(1))),
locator.next(&item.body.span()),
));
}
@ -77,8 +85,9 @@ pub fn layout_enum(
let reversed = elem.reversed(styles);
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -100,7 +109,7 @@ pub fn layout_enum(
// relation to the item it refers to.
let number_align = elem.number_align(styles);
for item in elem.children() {
for item in &elem.children {
number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
@ -123,11 +132,17 @@ pub fn layout_enum(
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
body.styled(EnumElem::set_parents(smallvec![number])),
locator.next(&item.body.span()),
));
number =

View File

@ -16,7 +16,7 @@ pub fn layout_accent(
styles: StyleChain,
) -> SourceResult<()> {
let cramped = style_cramped();
let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?;
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
// Try to replace a glyph with its dotless variant.
if let MathFragment::Glyph(glyph) = &mut base {
@ -29,12 +29,12 @@ pub fn layout_accent(
let width = elem.size(styles).relative_to(base.width());
let Accent(c) = elem.accent();
let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span());
let Accent(c) = elem.accent;
let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
// Try to replace accent glyph with flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
if base.height() > flattened_base_height {
if base.ascent() > flattened_base_height {
glyph.make_flattened_accent_form(ctx);
}
@ -50,7 +50,7 @@ pub fn layout_accent(
// minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.height().min(accent_base_height);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);

View File

@ -1,10 +1,9 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
};
use typst_library::text::TextElem;
use typst_utils::OptionExt;
use super::{
@ -29,7 +28,7 @@ pub fn layout_attach(
let elem = merged.as_ref().unwrap_or(elem);
let stretch = stretch_size(styles, elem);
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
@ -95,7 +94,7 @@ pub fn layout_primes(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
match *elem.count() {
match elem.count {
count @ 1..=4 => {
let c = match count {
1 => '',
@ -104,13 +103,14 @@ pub fn layout_primes(
4 => '⁗',
_ => unreachable!(),
};
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?;
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
ctx.push(f);
}
count => {
// Custom amount of primes
let prime =
ctx.layout_into_fragment(&TextElem::packed(''), styles)?.into_frame();
let prime = ctx
.layout_into_fragment(&SymbolElem::packed(''), styles)?
.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent());
@ -134,7 +134,7 @@ pub fn layout_scripts(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(Limits::Never);
ctx.push(fragment);
Ok(())
@ -148,7 +148,7 @@ pub fn layout_limits(
styles: StyleChain,
) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(limits);
ctx.push(fragment);
Ok(())
@ -157,9 +157,9 @@ pub fn layout_limits(
/// Get the size to stretch the base to.
fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
// Extract from an EquationElem.
let mut base = elem.base();
let mut base = &elem.base;
while let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body();
base = &equation.body;
}
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))

View File

@ -16,7 +16,7 @@ pub fn layout_cancel(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let body = ctx.layout_into_fragment(elem.body(), styles)?;
let body = ctx.layout_into_fragment(&elem.body, styles)?;
// Preserve properties of body.
let body_class = body.class();

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
use typst_library::math::{BinomElem, FracElem};
use typst_library::text::TextElem;
@ -23,8 +23,8 @@ pub fn layout_frac(
layout_frac_like(
ctx,
styles,
elem.num(),
std::slice::from_ref(elem.denom()),
&elem.num,
std::slice::from_ref(&elem.denom),
false,
elem.span(),
)
@ -37,7 +37,7 @@ pub fn layout_binom(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span())
layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
}
/// Layout a fraction or binomial.
@ -80,7 +80,10 @@ fn layout_frac_like(
let denom = ctx.layout_into_frame(
&Content::sequence(
// Add a comma between each element.
denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1),
denom
.iter()
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
.skip(1),
),
styles.chain(&denom_style),
)?;

View File

@ -1,23 +1,23 @@
use std::fmt::{self, Debug, Formatter};
use rustybuzz::Feature;
use smallvec::SmallVec;
use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
use ttf_parser::opentype_layout::LayoutTable;
use ttf_parser::{GlyphId, Rect};
use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag;
use typst_library::layout::{
Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
};
use typst_library::math::{EquationElem, MathSize};
use typst_library::model::{Destination, LinkElem};
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use typst_library::visualize::Paint;
use typst_syntax::Span;
use typst_utils::default_math_class;
use unicode_math_class::MathClass;
use super::{stretch_glyph, MathContext, Scaled};
use crate::modifiers::{FrameModifiers, FrameModify};
#[derive(Debug, Clone)]
pub enum MathFragment {
@ -245,8 +245,7 @@ pub struct GlyphFragment {
pub class: MathClass,
pub math_size: MathSize,
pub span: Span,
pub dests: SmallVec<[Destination; 1]>,
pub hidden: bool,
pub modifiers: FrameModifiers,
pub limits: Limits,
pub extended_shape: bool,
}
@ -277,11 +276,7 @@ impl GlyphFragment {
span: Span,
) -> Self {
let class = EquationElem::class_in(styles)
.or_else(|| match c {
':' => Some(MathClass::Relation),
'.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal),
_ => unicode_math_class::class(c),
})
.or_else(|| default_math_class(c))
.unwrap_or(MathClass::Normal);
let mut fragment = Self {
@ -302,8 +297,7 @@ impl GlyphFragment {
accent_attach: Abs::zero(),
class,
span,
dests: LinkElem::dests_in(styles),
hidden: HideElem::hidden_in(styles),
modifiers: FrameModifiers::get_in(styles),
extended_shape: false,
};
fragment.set_id(ctx, id);
@ -390,7 +384,7 @@ impl GlyphFragment {
let mut frame = Frame::soft(size);
frame.set_baseline(self.ascent);
frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
frame.post_process_raw(self.dests, self.hidden);
frame.modify(&self.modifiers);
frame
}
@ -516,7 +510,7 @@ impl FrameFragment {
let base_ascent = frame.ascent();
let accent_attach = frame.width() / 2.0;
Self {
frame: frame.post_processed(styles),
frame: frame.modified(&FrameModifiers::get_in(styles)),
font_size: TextElem::size_in(styles),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles),
@ -632,7 +626,7 @@ pub enum Limits {
impl Limits {
/// The default limit configuration if the given character is the base.
pub fn for_char(c: char) -> Self {
match unicode_math_class::class(c) {
match default_math_class(c) {
Some(MathClass::Large) => {
if is_integral_char(c) {
Limits::Never

View File

@ -2,6 +2,7 @@ use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::{EquationElem, LrElem, MidElem};
use typst_utils::SliceExt;
use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
@ -13,32 +14,23 @@ pub fn layout_lr(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut body = elem.body();
// Extract from an EquationElem.
let mut body = &elem.body;
if let Some(equation) = body.to_packed::<EquationElem>() {
body = equation.body();
body = &equation.body;
}
// Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() {
if lr.size(styles).is_one() {
body = lr.body();
body = &lr.body;
}
}
let mut fragments = ctx.layout_into_fragments(body, styles)?;
// Ignore leading and trailing ignorant fragments.
let start_idx = fragments
.iter()
.position(|f| !f.is_ignorant())
.unwrap_or(fragments.len());
let end_idx = fragments
.iter()
.skip(start_idx)
.rposition(|f| !f.is_ignorant())
.map_or(start_idx, |i| start_idx + i + 1);
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height);
@ -100,7 +92,7 @@ pub fn layout_mid(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?;
let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments {
match fragment {

View File

@ -27,7 +27,7 @@ pub fn layout_vec(
let frame = layout_vec_body(
ctx,
styles,
elem.children(),
&elem.children,
elem.align(styles),
elem.gap(styles),
LeftRightAlternator::Right,
@ -44,7 +44,7 @@ pub fn layout_mat(
styles: StyleChain,
) -> SourceResult<()> {
let augment = elem.augment(styles);
let rows = elem.rows();
let rows = &elem.rows;
if let Some(aug) = &augment {
for &offset in &aug.hline.0 {
@ -58,7 +58,7 @@ pub fn layout_mat(
}
}
let ncols = elem.rows().first().map_or(0, |row| row.len());
let ncols = rows.first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols {
@ -97,7 +97,7 @@ pub fn layout_cases(
let frame = layout_vec_body(
ctx,
styles,
elem.children(),
&elem.children,
FixedAlignment::Start,
elem.gap(styles),
LeftRightAlternator::None,

View File

@ -17,7 +17,9 @@ use rustybuzz::Feature;
use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
};
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
use typst_library::layout::{
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
@ -200,8 +202,7 @@ pub fn layout_equation_block(
let counter = Counter::of(EquationElem::elem())
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span);
let number =
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
@ -535,6 +536,8 @@ fn layout_realized(
layout_h(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
self::text::layout_symbol(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() {
@ -615,7 +618,7 @@ fn layout_box(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let frame = (ctx.engine.routines.layout_box)(
let frame = crate::inline::layout_box(
elem,
ctx.engine,
ctx.locator.next(&elem.span()),
@ -632,7 +635,7 @@ fn layout_h(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount() {
if let Spacing::Rel(rel) = elem.amount {
if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
}
@ -641,17 +644,16 @@ fn layout_h(
}
/// Lays out a [`ClassElem`].
#[typst_macros::time(name = "math.op", span = elem.span())]
#[typst_macros::time(name = "math.class", span = elem.span())]
fn layout_class(
elem: &Packed<ClassElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let class = *elem.class();
let style = EquationElem::set_class(Some(class)).wrap();
let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?;
fragment.set_class(class);
fragment.set_limits(Limits::for_class(class));
let style = EquationElem::set_class(Some(elem.class)).wrap();
let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
fragment.set_class(elem.class);
fragment.set_limits(Limits::for_class(elem.class));
ctx.push(fragment);
Ok(())
}
@ -663,7 +665,7 @@ fn layout_op(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let fragment = ctx.layout_into_fragment(elem.text(), styles)?;
let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach();
let text_like = fragment.is_text_like();
@ -689,7 +691,7 @@ fn layout_external(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<Frame> {
(ctx.engine.routines.layout_frame)(
crate::layout_frame(
ctx.engine,
content,
ctx.locator.next(&content.span()),

View File

@ -18,7 +18,6 @@ pub fn layout_root(
styles: StyleChain,
) -> SourceResult<()> {
let index = elem.index(styles);
let radicand = elem.radicand();
let span = elem.span();
let gap = scaled!(
@ -36,7 +35,7 @@ pub fn layout_root(
let radicand = {
let cramped = style_cramped();
let styles = styles.chain(&cramped);
let run = ctx.layout_into_run(radicand, styles)?;
let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame();
if multiline {

View File

@ -10,6 +10,7 @@ use super::{
delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
VariantFragment,
};
use crate::modifiers::FrameModify;
/// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024;
@ -21,7 +22,7 @@ pub fn layout_stretch(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment(
ctx,
styles,
@ -265,7 +266,7 @@ fn assemble(
let mut frame = Frame::soft(size);
let mut offset = Abs::zero();
frame.set_baseline(baseline);
frame.post_process_raw(base.dests, base.hidden);
frame.modify(&base.modifiers);
for (fragment, advance) in selected {
let pos = match axis {

View File

@ -1,8 +1,8 @@
use std::f64::consts::SQRT_2;
use ecow::{eco_vec, EcoString};
use ecow::EcoString;
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, StyleVec};
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{
@ -20,56 +20,68 @@ pub fn layout_text(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let text = elem.text();
let text = &elem.text;
let span = elem.span();
let mut chars = text.chars();
let math_size = EquationElem::size_in(styles);
let mut dtls = ctx.dtls_table.is_some();
let fragment: MathFragment = if let Some(mut glyph) = chars
.next()
.filter(|_| chars.next().is_none())
.map(|c| dtls_char(c, &mut dtls))
.map(|c| styled_char(styles, c, true))
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
{
// A single letter that is available in the math font.
if dtls {
glyph.make_dotless_form(ctx);
}
let fragment = if text.contains(is_newline) {
layout_text_lines(text.split(is_newline), span, ctx, styles)?
} else {
layout_inline_text(text, span, ctx, styles)?
};
ctx.push(fragment);
Ok(())
}
match math_size {
MathSize::Script => {
glyph.make_script_size(ctx);
}
MathSize::ScriptScript => {
glyph.make_script_script_size(ctx);
}
_ => (),
/// Layout multiple lines of text.
fn layout_text_lines<'a>(
lines: impl Iterator<Item = &'a str>,
span: Span,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<FrameFragment> {
let mut fragments = vec![];
for (i, line) in lines.enumerate() {
if i != 0 {
fragments.push(MathFragment::Linebreak);
}
if !line.is_empty() {
fragments.push(layout_inline_text(line, span, ctx, styles)?.into());
}
}
let mut frame = MathRun::new(fragments).into_frame(styles);
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
Ok(FrameFragment::new(styles, frame))
}
if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero())
} else {
glyph.into_variant()
};
// TeXbook p 155. Large operators are always vertically centered on the axis.
variant.center_on_axis(ctx);
variant.into()
} else {
glyph.into()
}
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Numbers aren't that difficult.
/// Layout the given text string into a [`FrameFragment`] after styling all
/// characters for the math font (without auto-italics).
fn layout_inline_text(
text: &str,
span: Span,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<FrameFragment> {
if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![];
for c in text.chars() {
let c = styled_char(styles, c, false);
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
let is_single = text.chars().count() == 1;
for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false);
let mut glyph = GlyphFragment::new(ctx, styles, c, span);
if is_single {
// Duplicate what `layout_glyph` does exactly even if it's
// probably incorrect here.
match EquationElem::size_in(styles) {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
}
fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
FrameFragment::new(styles, frame).with_text_like(true).into()
Ok(FrameFragment::new(styles, frame).with_text_like(true))
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
@ -77,64 +89,96 @@ pub fn layout_text(
]
.map(|p| p.wrap());
// Anything else is handled by Typst's standard text layout.
let styles = styles.chain(&local);
let text: EcoString =
let styled_text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect();
if text.contains(is_newline) {
let mut fragments = vec![];
for (i, piece) in text.split(is_newline).enumerate() {
if i != 0 {
fragments.push(MathFragment::Linebreak);
}
if !piece.is_empty() {
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
}
}
let mut frame = MathRun::new(fragments).into_frame(styles);
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
FrameFragment::new(styles, frame).into()
} else {
layout_complex_text(&text, ctx, span, styles)?.into()
let spaced = styled_text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(styled_text).spanned(span);
// There isn't a natural width for a paragraph in a math environment;
// because it will be placed somewhere probably not at the left margin
// it will overflow. So emulate an `hbox` instead and allow the
// paragraph to extend as far as needed.
let frame = crate::inline::layout_inline(
ctx.engine,
&[(&elem, styles)],
&mut ctx.locator.next(&span).split(),
styles,
Size::splat(Abs::inf()),
false,
)?
.into_frame();
Ok(FrameFragment::new(styles, frame)
.with_class(MathClass::Alphabetic)
.with_text_like(true)
.with_spaced(spaced))
}
}
/// Layout a single character in the math font with the correct styling applied
/// (includes auto-italics).
pub fn layout_symbol(
elem: &Packed<SymbolElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
// Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass.
let (unstyled_c, dtls) = match try_dotless(elem.text) {
Some(c) if ctx.dtls_table.is_some() => (c, true),
_ => (elem.text, false),
};
let c = styled_char(styles, unstyled_c, true);
let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
None => {
// Not in the math font, fallback to normal inline text layout.
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
.into()
}
};
ctx.push(fragment);
Ok(())
}
/// Layout the given text string into a [`FrameFragment`].
fn layout_complex_text(
text: &str,
/// Layout a [`GlyphFragment`].
fn layout_glyph(
mut glyph: GlyphFragment,
dtls: bool,
ctx: &mut MathContext,
span: Span,
styles: StyleChain,
) -> SourceResult<FrameFragment> {
// There isn't a natural width for a paragraph in a math environment;
// because it will be placed somewhere probably not at the left margin
// it will overflow. So emulate an `hbox` instead and allow the paragraph
// to extend as far as needed.
let spaced = text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(text).spanned(span);
let frame = (ctx.engine.routines.layout_inline)(
ctx.engine,
&StyleVec::wrap(eco_vec![elem]),
ctx.locator.next(&span),
styles,
false,
Size::splat(Abs::inf()),
false,
)?
.into_frame();
) -> MathFragment {
if dtls {
glyph.make_dotless_form(ctx);
}
let math_size = EquationElem::size_in(styles);
match math_size {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
Ok(FrameFragment::new(styles, frame)
.with_class(MathClass::Alphabetic)
.with_text_like(true)
.with_spaced(spaced))
if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero())
} else {
glyph.into_variant()
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
variant.center_on_axis(ctx);
variant.into()
} else {
glyph.into()
}
}
/// Select the correct styled math letter.
/// Style the character by selecting the unicode codepoint for italic, bold,
/// caligraphic, etc.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
@ -353,15 +397,12 @@ fn greek_exception(
})
}
/// Switch dotless character to non dotless character for use of the dtls
/// OpenType feature.
pub fn dtls_char(c: char, dtls: &mut bool) -> char {
match (c, *dtls) {
('ı', true) => 'i',
('ȷ', true) => 'j',
_ => {
*dtls = false;
c
}
/// The non-dotless version of a dotless character that can be used with the
/// `dtls` OpenType feature.
pub fn try_dotless(c: char) -> Option<char> {
match c {
'ı' => Some('i'),
'ȷ' => Some('j'),
_ => None,
}
}

View File

@ -32,7 +32,7 @@ pub fn layout_underline(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under)
layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under)
}
/// Lays out an [`OverlineElem`].
@ -42,7 +42,7 @@ pub fn layout_overline(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over)
layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over)
}
/// Lays out an [`UnderbraceElem`].
@ -55,7 +55,7 @@ pub fn layout_underbrace(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⏟',
BRACE_GAP,
@ -74,7 +74,7 @@ pub fn layout_overbrace(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⏞',
BRACE_GAP,
@ -93,7 +93,7 @@ pub fn layout_underbracket(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⎵',
BRACKET_GAP,
@ -112,7 +112,7 @@ pub fn layout_overbracket(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⎴',
BRACKET_GAP,
@ -131,7 +131,7 @@ pub fn layout_underparen(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⏝',
PAREN_GAP,
@ -150,7 +150,7 @@ pub fn layout_overparen(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⏜',
PAREN_GAP,
@ -169,7 +169,7 @@ pub fn layout_undershell(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⏡',
SHELL_GAP,
@ -188,7 +188,7 @@ pub fn layout_overshell(
layout_underoverspreader(
ctx,
styles,
elem.body(),
&elem.body,
&elem.annotation(styles),
'⏠',
SHELL_GAP,

View File

@ -0,0 +1,110 @@
use typst_library::foundations::StyleChain;
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point};
use typst_library::model::{Destination, LinkElem};
/// Frame-level modifications resulting from styles that do not impose any
/// layout structure.
///
/// These are always applied at the highest level of style uniformity.
/// Consequently, they must be applied by all layouters that manually manage
/// styles of their children (because they can produce children with varying
/// styles). This currently includes flow, inline, and math layout.
///
/// Other layouters don't manually need to handle it because their parents that
/// result from realization will take care of it and the styles can only apply
/// to them as a whole, not part of it (since they don't manage styles).
///
/// Currently existing frame modifiers are:
/// - `HideElem::hidden`
/// - `LinkElem::dests`
#[derive(Debug, Clone)]
pub struct FrameModifiers {
/// A destination to link to.
dest: Option<Destination>,
/// Whether the contents of the frame should be hidden.
hidden: bool,
}
impl FrameModifiers {
/// Retrieve all modifications that should be applied per-frame.
pub fn get_in(styles: StyleChain) -> Self {
Self {
dest: LinkElem::current_in(styles),
hidden: HideElem::hidden_in(styles),
}
}
}
/// Applies [`FrameModifiers`].
pub trait FrameModify {
/// Apply the modifiers in-place.
fn modify(&mut self, modifiers: &FrameModifiers);
/// Apply the modifiers, and return the modified result.
fn modified(mut self, modifiers: &FrameModifiers) -> Self
where
Self: Sized,
{
self.modify(modifiers);
self
}
}
impl FrameModify for Frame {
fn modify(&mut self, modifiers: &FrameModifiers) {
if let Some(dest) = &modifiers.dest {
let size = self.size();
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
self.hide();
}
}
}
impl FrameModify for Fragment {
fn modify(&mut self, modifiers: &FrameModifiers) {
for frame in self.iter_mut() {
frame.modify(modifiers);
}
}
}
impl<T, E> FrameModify for Result<T, E>
where
T: FrameModify,
{
fn modify(&mut self, props: &FrameModifiers) {
if let Ok(inner) = self {
inner.modify(props);
}
}
}
/// Performs layout and modification in one step.
///
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,
/// but with the additional step that redundant modifiers (which are already
/// applied here) are removed from the `styles` passed to `layout`. This is used
/// for the layout of containers like `block`.
pub fn layout_and_modify<F, R>(styles: StyleChain, layout: F) -> R
where
F: FnOnce(StyleChain) -> R,
R: FrameModify,
{
let modifiers = FrameModifiers::get_in(styles);
// Disable the current link internally since it's already applied at this
// level of layout. This means we don't generate redundant nested links,
// which may bloat the output considerably.
let reset;
let outer = styles;
let mut styles = styles;
if modifiers.dest.is_some() {
reset = LinkElem::set_current(None).wrap();
styles = outer.chain(&reset);
}
layout(styles).modified(&modifiers)
}

View File

@ -23,7 +23,7 @@ pub enum Item<'a> {
/// things like tags and weak pagebreaks.
pub fn collect<'a>(
mut children: &'a mut [Pair<'a>],
mut locator: SplitLocator<'a>,
locator: &mut SplitLocator<'a>,
mut initial: StyleChain<'a>,
) -> Vec<Item<'a>> {
// The collected page-level items.

View File

@ -83,7 +83,7 @@ fn layout_document_impl(
styles,
)?;
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
let introspector = Introspector::paged(&pages);
Ok(PagedDocument { pages, info, introspector })
@ -93,7 +93,7 @@ fn layout_document_impl(
fn layout_pages<'a>(
engine: &mut Engine,
children: &'a mut [Pair<'a>],
locator: SplitLocator<'a>,
locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>,
) -> SourceResult<Vec<Page>> {
// Slice up the children into logical parts.

View File

@ -19,7 +19,7 @@ use typst_library::visualize::Paint;
use typst_library::World;
use typst_utils::Numeric;
use crate::flow::layout_flow;
use crate::flow::{layout_flow, FlowMode};
/// A mostly finished layout for one page. Needs only knowledge of its exact
/// page number to be finalized into a `Page`. (Because the margins can depend
@ -181,7 +181,7 @@ fn layout_page_run_impl(
Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles),
true,
FlowMode::Root,
)?;
// Layouts a single marginal.

View File

@ -62,7 +62,7 @@ pub fn layout_path(
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
};
let vertices = elem.vertices();
let vertices = &elem.vertices;
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero();
@ -150,7 +150,7 @@ pub fn layout_curve(
) -> SourceResult<Frame> {
let mut builder = CurveBuilder::new(region, styles);
for item in elem.components() {
for item in &elem.components {
match item {
CurveComponent::Move(element) => {
let relative = element.relative(styles);
@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> {
self.last_point = point;
self.last_control_from = point;
self.is_started = true;
self.is_empty = true;
}
/// Add a line segment.
@ -399,7 +400,7 @@ pub fn layout_polygon(
region: Region,
) -> SourceResult<Frame> {
let points: Vec<Point> = elem
.vertices()
.vertices
.iter()
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
.collect();
@ -1281,7 +1282,7 @@ impl ControlPoints {
}
}
/// Helper to draw arcs with bezier curves.
/// Helper to draw arcs with zier curves.
trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&mut self, start: Point, center: Point, end: Point);
@ -1305,7 +1306,7 @@ impl CurveExt for Curve {
}
}
/// Get the control points for a bezier curve that approximates a circular arc for
/// Get the control points for a zier curve that approximates a circular arc for
/// a start point, an end point and a center of the circle whose arc connects
/// the two.
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {

View File

@ -27,7 +27,7 @@ pub fn layout_stack(
let spacing = elem.spacing(styles);
let mut deferred = None;
for child in elem.children() {
for child in &elem.children {
match child {
StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind);
@ -36,14 +36,14 @@ pub fn layout_stack(
StackChild::Block(block) => {
// Transparently handle `h`.
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
layouter.layout_spacing(*h.amount());
layouter.layout_spacing(h.amount);
deferred = None;
continue;
}
// Transparently handle `v`.
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
layouter.layout_spacing(*v.amount());
layouter.layout_spacing(v.amount);
deferred = None;
continue;
}

View File

@ -52,7 +52,7 @@ pub fn layout_rotate(
region,
size,
styles,
elem.body(),
&elem.body,
Transform::rotate(angle),
align,
elem.reflow(styles),
@ -81,7 +81,7 @@ pub fn layout_scale(
region,
size,
styles,
elem.body(),
&elem.body,
Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles),
elem.reflow(styles),
@ -169,7 +169,7 @@ pub fn layout_skew(
region,
size,
styles,
elem.body(),
&elem.body,
Transform::skew(ax, ay),
align,
elem.reflow(styles),

View File

@ -39,6 +39,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 }
@ -61,6 +62,7 @@ ttf-parser = { workspace = true }
two-face = { workspace = true }
typed-arena = { workspace = true }
unicode-math-class = { workspace = true }
unicode-normalization = { workspace = true }
unicode-segmentation = { workspace = true }
unscanny = { workspace = true }
usvg = { workspace = true }

View File

@ -11,6 +11,7 @@ use ecow::{eco_vec, EcoVec};
use typst_syntax::package::{PackageSpec, PackageVersion};
use typst_syntax::{Span, Spanned, SyntaxError};
use crate::engine::Engine;
use crate::{World, WorldExt};
/// Early-return with a [`StrResult`] or [`SourceResult`].
@ -228,6 +229,23 @@ 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);
}
impl DeprecationSink for () {
fn emit(self, _: &str) {}
}
impl DeprecationSink for (&mut Engine<'_>, Span) {
/// Emits the deprecation message as a warning.
fn emit(self, message: &str) {
self.0.sink.warn(SourceDiagnostic::warning(self.1, message));
}
}
/// A part of a diagnostic's [trace](SourceDiagnostic::trace).
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Tracepoint {

View File

@ -312,7 +312,8 @@ impl Route<'_> {
if !self.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!(
"maximum show rule depth exceeded";
hint: "check whether the show rule matches its own output"
hint: "maybe a show rule matches its own output";
hint: "maybe there are too deeply nested elements"
);
}
Ok(())

View File

@ -1,4 +1,5 @@
use std::fmt::{self, Debug, Formatter};
use std::ops::Add;
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_syntax::{Span, Spanned};
@ -304,8 +305,6 @@ impl Args {
/// ```
#[func(constructor)]
pub fn construct(
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args,
/// The arguments to construct.
#[external]
@ -366,7 +365,7 @@ impl Debug for Args {
impl Repr for Args {
fn repr(&self) -> EcoString {
let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>();
repr::pretty_array_like(&pieces, false).into()
eco_format!("arguments{}", repr::pretty_array_like(&pieces, false))
}
}
@ -376,6 +375,21 @@ impl PartialEq for Args {
}
}
impl Add for Args {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self.items.retain(|item| {
!item.name.as_ref().is_some_and(|name| {
rhs.items.iter().any(|a| a.name.as_ref() == Some(name))
})
});
self.items.extend(rhs.items);
self.span = Span::detached();
self
}
}
/// An argument to a function call: `12` or `draw: false`.
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]

View File

@ -301,9 +301,7 @@ impl Array {
#[func]
pub fn find(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean.
searcher: Func,
@ -325,9 +323,7 @@ impl Array {
#[func]
pub fn position(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean.
searcher: Func,
@ -363,8 +359,6 @@ impl Array {
/// ```
#[func]
pub fn range(
/// The real arguments (the other arguments are just for the docs, this
/// function is a bit involved, so we parse the arguments manually).
args: &mut Args,
/// The start of the range (inclusive).
#[external]
@ -402,9 +396,7 @@ impl Array {
#[func]
pub fn filter(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean.
test: Func,
@ -427,9 +419,7 @@ impl Array {
#[func]
pub fn map(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The function to apply to each item.
mapper: Func,
@ -481,8 +471,6 @@ impl Array {
#[func]
pub fn zip(
self,
/// The real arguments (the `others` arguments are just for the docs, this
/// function is a bit involved, so we parse the positional arguments manually).
args: &mut Args,
/// Whether all arrays have to have the same length.
/// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an
@ -569,9 +557,7 @@ impl Array {
#[func]
pub fn fold(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The initial value to start with.
init: Value,
@ -631,9 +617,7 @@ impl Array {
#[func]
pub fn any(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean.
test: Func,
@ -651,9 +635,7 @@ impl Array {
#[func]
pub fn all(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean.
test: Func,
@ -831,11 +813,8 @@ impl Array {
#[func]
pub fn sorted(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// If given, applies this function to the elements in the array to
/// determine the keys to sort by.
@ -911,9 +890,7 @@ impl Array {
#[func(title = "Deduplicate")]
pub fn dedup(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by.
@ -997,9 +974,7 @@ impl Array {
#[func]
pub fn reduce(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The reducing function. Must have two parameters: One for the
/// accumulated value and one for an item.
@ -1154,6 +1129,53 @@ impl<T: FromValue, const N: usize> FromValue for SmallVec<[T; N]> {
}
}
/// One element, or multiple provided as an array.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct OneOrMultiple<T>(pub Vec<T>);
impl<T: Reflect> Reflect for OneOrMultiple<T> {
fn input() -> CastInfo {
T::input() + Array::input()
}
fn output() -> CastInfo {
T::output() + Array::output()
}
fn castable(value: &Value) -> bool {
Array::castable(value) || T::castable(value)
}
}
impl<T: IntoValue + Clone> IntoValue for OneOrMultiple<T> {
fn into_value(self) -> Value {
self.0.into_value()
}
}
impl<T: FromValue> FromValue for OneOrMultiple<T> {
fn from_value(value: Value) -> HintedStrResult<Self> {
if T::castable(&value) {
return Ok(Self(vec![T::from_value(value)?]));
}
if Array::castable(&value) {
return Ok(Self(
Array::from_value(value)?
.into_iter()
.map(|value| T::from_value(value))
.collect::<HintedStrResult<_>>()?,
));
}
Err(Self::error(&value))
}
}
impl<T> Default for OneOrMultiple<T> {
fn default() -> Self {
Self(vec![])
}
}
/// The error message when the array is empty.
#[cold]
fn array_is_empty() -> EcoString {

View File

@ -1,6 +1,8 @@
use std::borrow::Cow;
use std::any::Any;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref};
use std::str::Utf8Error;
use std::sync::Arc;
use ecow::{eco_format, EcoString};
@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value
/// #str(data.slice(1, 4))
/// ```
#[ty(scope, cast)]
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct Bytes(Arc<LazyHash<Cow<'static, [u8]>>>);
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Bytes(Arc<LazyHash<dyn Bytelike>>);
impl Bytes {
/// Create a buffer from a static byte slice.
pub fn from_static(slice: &'static [u8]) -> Self {
Self(Arc::new(LazyHash::new(Cow::Borrowed(slice))))
/// Create `Bytes` from anything byte-like.
///
/// The `data` type will directly back this bytes object. This means you can
/// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be
/// allocated.
///
/// If the type is `Vec<u8>` and the `Bytes` are unique (i.e. not cloned),
/// the vector will be reused when mutating to the `Bytes`.
///
/// If your source type is a string, prefer [`Bytes::from_string`] to
/// directly use the UTF-8 encoded string data without any copying.
pub fn new<T>(data: T) -> Self
where
T: AsRef<[u8]> + Send + Sync + 'static,
{
Self(Arc::new(LazyHash::new(data)))
}
/// Create `Bytes` from anything string-like, implicitly viewing the UTF-8
/// representation.
///
/// The `data` type will directly back this bytes object. This means you can
/// e.g. pass `String` or `EcoString` without any copying.
pub fn from_string<T>(data: T) -> Self
where
T: AsRef<str> + Send + Sync + 'static,
{
Self(Arc::new(LazyHash::new(StrWrapper(data))))
}
/// Return `true` if the length is 0.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
self.as_slice().is_empty()
}
/// Return a view into the buffer.
/// Return a view into the bytes.
pub fn as_slice(&self) -> &[u8] {
self
}
/// Return a copy of the buffer as a vector.
/// Try to view the bytes as an UTF-8 string.
///
/// If these bytes were created via `Bytes::from_string`, UTF-8 validation
/// is skipped.
pub fn as_str(&self) -> Result<&str, Utf8Error> {
self.inner().as_str()
}
/// Return a copy of the bytes as a vector.
pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec()
self.as_slice().to_vec()
}
/// Try to turn the bytes into a `Str`.
///
/// - If these bytes were created via `Bytes::from_string::<Str>`, the
/// string is cloned directly.
/// - If these bytes were created via `Bytes::from_string`, but from a
/// different type of string, UTF-8 validation is still skipped.
pub fn to_str(&self) -> Result<Str, Utf8Error> {
match self.inner().as_any().downcast_ref::<Str>() {
Some(string) => Ok(string.clone()),
None => self.as_str().map(Into::into),
}
}
/// Resolve an index or throw an out of bounds error.
@ -72,12 +121,15 @@ impl Bytes {
///
/// `index == len` is considered in bounds.
fn locate_opt(&self, index: i64) -> Option<usize> {
let len = self.as_slice().len();
let wrapped =
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
if index >= 0 { Some(index) } else { (len as i64).checked_add(index) };
wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len)
}
wrapped
.and_then(|v| usize::try_from(v).ok())
.filter(|&v| v <= self.0.len())
/// Access the inner `dyn Bytelike`.
fn inner(&self) -> &dyn Bytelike {
&**self.0
}
}
@ -106,7 +158,7 @@ impl Bytes {
/// The length in bytes.
#[func(title = "Length")]
pub fn len(&self) -> usize {
self.0.len()
self.as_slice().len()
}
/// Returns the byte at the specified index. Returns the default value if
@ -122,13 +174,13 @@ impl Bytes {
default: Option<Value>,
) -> StrResult<Value> {
self.locate_opt(index)
.and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into())))
.and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into())))
.or(default)
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
}
/// Extracts a subslice of the bytes. Fails with an error if the start or end
/// index is out of bounds.
/// Extracts a subslice of the bytes. Fails with an error if the start or
/// end index is out of bounds.
#[func]
pub fn slice(
&self,
@ -148,9 +200,17 @@ impl Bytes {
if end.is_none() {
end = count.map(|c: i64| start + c);
}
let start = self.locate(start)?;
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
Ok(self.0[start..end].into())
let slice = &self.as_slice()[start..end];
// We could hold a view into the original bytes here instead of
// making a copy, but it's unclear when that's worth it. Java
// originally did that for strings, but went back on it because a
// very small view into a very large buffer would be a sort of
// memory leak.
Ok(Bytes::new(slice.to_vec()))
}
}
@ -170,7 +230,15 @@ impl Deref for Bytes {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
self.inner().as_bytes()
}
}
impl Eq for Bytes {}
impl PartialEq for Bytes {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
}
}
@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes {
}
}
impl From<&[u8]> for Bytes {
fn from(slice: &[u8]) -> Self {
Self(Arc::new(LazyHash::new(slice.to_vec().into())))
}
}
impl From<Vec<u8>> for Bytes {
fn from(vec: Vec<u8>) -> Self {
Self(Arc::new(LazyHash::new(vec.into())))
}
}
impl Add for Bytes {
type Output = Self;
@ -207,10 +263,12 @@ impl AddAssign for Bytes {
// Nothing to do
} else if self.is_empty() {
*self = rhs;
} else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) {
Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs);
} else if let Some(vec) = Arc::get_mut(&mut self.0)
.and_then(|unique| unique.as_any_mut().downcast_mut::<Vec<u8>>())
{
vec.extend_from_slice(&rhs);
} else {
*self = Self::from([self.as_slice(), rhs.as_slice()].concat());
*self = Self::new([self.as_slice(), rhs.as_slice()].concat());
}
}
}
@ -228,20 +286,79 @@ impl Serialize for Bytes {
}
}
/// Any type that can back a byte buffer.
trait Bytelike: Send + Sync {
fn as_bytes(&self) -> &[u8];
fn as_str(&self) -> Result<&str, Utf8Error>;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T> Bytelike for T
where
T: AsRef<[u8]> + Send + Sync + 'static,
{
fn as_bytes(&self) -> &[u8] {
self.as_ref()
}
fn as_str(&self) -> Result<&str, Utf8Error> {
std::str::from_utf8(self.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Hash for dyn Bytelike {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_bytes().hash(state);
}
}
/// Makes string-like objects usable with `Bytes`.
struct StrWrapper<T>(T);
impl<T> Bytelike for StrWrapper<T>
where
T: AsRef<str> + Send + Sync + 'static,
{
fn as_bytes(&self) -> &[u8] {
self.0.as_ref().as_bytes()
}
fn as_str(&self) -> Result<&str, Utf8Error> {
Ok(self.0.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// A value that can be cast to bytes.
pub struct ToBytes(Bytes);
cast! {
ToBytes,
v: Str => Self(v.as_bytes().into()),
v: Str => Self(Bytes::from_string(v)),
v: Array => Self(v.iter()
.map(|item| match item {
Value::Int(byte @ 0..=255) => Ok(*byte as u8),
Value::Int(_) => bail!("number must be between 0 and 255"),
value => Err(<u8 as Reflect>::error(value)),
})
.collect::<Result<Vec<u8>, _>>()?
.into()
.collect::<Result<Vec<u8>, _>>()
.map(Bytes::new)?
),
v: Bytes => Self(v),
}

View File

@ -97,7 +97,6 @@ cast! {
/// ```
#[func(title = "Power")]
pub fn pow(
/// The callsite span.
span: Span,
/// The base of the power.
///
@ -159,7 +158,6 @@ pub fn pow(
/// ```
#[func(title = "Exponential")]
pub fn exp(
/// The callsite span.
span: Span,
/// The exponent of the power.
exponent: Spanned<Num>,
@ -412,7 +410,6 @@ pub fn tanh(
/// ```
#[func(title = "Logarithm")]
pub fn log(
/// The callsite span.
span: Span,
/// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned<Num>,
@ -454,7 +451,6 @@ pub fn log(
/// ```
#[func(title = "Natural Logarithm")]
pub fn ln(
/// The callsite span.
span: Span,
/// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned<Num>,
@ -782,7 +778,6 @@ pub fn round(
/// ```
#[func]
pub fn clamp(
/// The callsite span.
span: Span,
/// The number to clamp.
value: DecNum,
@ -815,7 +810,6 @@ pub fn clamp(
/// ```
#[func(title = "Minimum")]
pub fn min(
/// The callsite span.
span: Span,
/// The sequence of values from which to extract the minimum.
/// Must not be empty.
@ -833,7 +827,6 @@ pub fn min(
/// ```
#[func(title = "Maximum")]
pub fn max(
/// The callsite span.
span: Span,
/// The sequence of values from which to extract the maximum.
/// Must not be empty.
@ -911,7 +904,6 @@ pub fn odd(
/// ```
#[func(title = "Remainder")]
pub fn rem(
/// The span of the function call.
span: Span,
/// The dividend of the remainder.
dividend: DecNum,
@ -950,7 +942,6 @@ pub fn rem(
/// ```
#[func(title = "Euclidean Division")]
pub fn div_euclid(
/// The callsite span.
span: Span,
/// The dividend of the division.
dividend: DecNum,
@ -994,7 +985,6 @@ pub fn div_euclid(
/// ```
#[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])]
pub fn rem_euclid(
/// The callsite span.
span: Span,
/// The dividend of the remainder.
dividend: DecNum,
@ -1031,7 +1021,6 @@ pub fn rem_euclid(
/// ```
#[func(title = "Quotient")]
pub fn quo(
/// The span of the function call.
span: Span,
/// The dividend of the quotient.
dividend: DecNum,

View File

@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned};
use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value};
use crate::foundations::{
array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value,
};
/// Determine details of a type.
///
@ -497,3 +499,58 @@ cast! {
/// An operator that can be both unary or binary like `+`.
"vary" => MathClass::Vary,
}
/// A type that contains a user-visible source portion and something that is
/// derived from it, but not user-visible.
///
/// An example usage would be `source` being a `DataSource` and `derived` a
/// TextMate theme parsed from it. With `Derived`, we can store both parts in
/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue`
/// impls.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Derived<S, D> {
/// The source portion.
pub source: S,
/// The derived portion.
pub derived: D,
}
impl<S, D> Derived<S, D> {
/// Create a new instance from the `source` and the `derived` data.
pub fn new(source: S, derived: D) -> Self {
Self { source, derived }
}
}
impl<S: Reflect, D> Reflect for Derived<S, D> {
fn input() -> CastInfo {
S::input()
}
fn output() -> CastInfo {
S::output()
}
fn castable(value: &Value) -> bool {
S::castable(value)
}
fn error(found: &Value) -> HintedString {
S::error(found)
}
}
impl<S: IntoValue, D> IntoValue for Derived<S, D> {
fn into_value(self) -> Value {
self.source.into_value()
}
}
impl<S: Fold, D: Fold> Fold for Derived<S, D> {
fn fold(self, outer: Self) -> Self {
Self {
source: self.source.fold(outer.source),
derived: self.derived.fold(outer.derived),
}
}
}

View File

@ -9,7 +9,6 @@ use std::sync::Arc;
use comemo::Tracked;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use smallvec::smallvec;
use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
@ -500,7 +499,7 @@ impl Content {
/// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self {
self.styled(LinkElem::set_dests(smallvec![dest]))
self.styled(LinkElem::set_current(Some(dest)))
}
/// Set alignments for this content.

View File

@ -318,7 +318,6 @@ impl Datetime {
/// ```
#[func]
pub fn today(
/// The engine.
engine: &mut Engine,
/// An offset to apply to the current UTC date. If set to `{auto}`, the
/// offset will be the local offset.

View File

@ -261,7 +261,12 @@ pub struct ToDict(Dict);
cast! {
ToDict,
v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()),
v: Module => Self(v
.scope()
.iter()
.map(|(k, b)| (Str::from(k.clone()), b.read().clone()))
.collect()
),
}
impl Debug for Dict {

View File

@ -110,7 +110,7 @@ impl f64 {
f64::signum(self)
}
/// Converts bytes to a float.
/// Interprets bytes as a float.
///
/// ```example
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
@ -120,8 +120,10 @@ impl f64 {
pub fn from_bytes(
/// The bytes that should be converted to a float.
///
/// Must be of length exactly 8 so that the result fits into a 64-bit
/// float.
/// Must have a length of either 4 or 8. The bytes are then
/// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s
/// binary32 (single-precision) or binary64 (double-precision) format
/// depending on the length of the bytes.
bytes: Bytes,
/// The endianness of the conversion.
#[named]
@ -158,23 +160,26 @@ 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,
) -> StrResult<Bytes> {
Ok(match size {
8 => match endian {
8 => Bytes::new(match endian {
Endianness::Little => self.to_le_bytes(),
Endianness::Big => self.to_be_bytes(),
}
.as_slice()
.into(),
4 => match endian {
}),
4 => Bytes::new(match endian {
Endianness::Little => (self as f32).to_le_bytes(),
Endianness::Big => (self as f32).to_be_bytes(),
}
.as_slice()
.into(),
}),
_ => bail!("size must be either 4 or 8"),
})
}

View File

@ -9,11 +9,11 @@ use ecow::{eco_format, EcoString};
use typst_syntax::{ast, Span, SyntaxNode};
use typst_utils::{singleton, LazyHash, Static};
use crate::diag::{bail, SourceResult, StrResult};
use crate::diag::{bail, At, DeprecationSink, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope,
Selector, Type, Value,
cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs,
PluginFunc, Scope, Selector, Type, Value,
};
/// A mapping from argument values to a return value.
@ -151,6 +151,8 @@ enum Repr {
Element(Element),
/// A user-defined closure.
Closure(Arc<LazyHash<Closure>>),
/// A plugin WebAssembly function.
Plugin(Arc<PluginFunc>),
/// A nested function with pre-applied arguments.
With(Arc<(Func, Args)>),
}
@ -164,6 +166,7 @@ impl Func {
Repr::Native(native) => Some(native.name),
Repr::Element(elem) => Some(elem.name()),
Repr::Closure(closure) => closure.name(),
Repr::Plugin(func) => Some(func.name()),
Repr::With(with) => with.0.name(),
}
}
@ -176,6 +179,7 @@ impl Func {
Repr::Native(native) => Some(native.title),
Repr::Element(elem) => Some(elem.title()),
Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.title(),
}
}
@ -186,6 +190,7 @@ impl Func {
Repr::Native(native) => Some(native.docs),
Repr::Element(elem) => Some(elem.docs()),
Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.docs(),
}
}
@ -204,6 +209,7 @@ impl Func {
Repr::Native(native) => Some(&native.0.params),
Repr::Element(elem) => Some(elem.params()),
Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.params(),
}
}
@ -221,6 +227,7 @@ impl Func {
Some(singleton!(CastInfo, CastInfo::Type(Type::of::<Content>())))
}
Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.returns(),
}
}
@ -231,6 +238,7 @@ impl Func {
Repr::Native(native) => native.keywords,
Repr::Element(elem) => elem.keywords(),
Repr::Closure(_) => &[],
Repr::Plugin(_) => &[],
Repr::With(with) => with.0.keywords(),
}
}
@ -241,16 +249,21 @@ impl Func {
Repr::Native(native) => Some(&native.0.scope),
Repr::Element(elem) => Some(elem.scope()),
Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.scope(),
}
}
/// Get a field from this function's scope, if possible.
pub fn field(&self, field: &str) -> StrResult<&'static Value> {
pub fn field(
&self,
field: &str,
sink: impl DeprecationSink,
) -> StrResult<&'static Value> {
let scope =
self.scope().ok_or("cannot access fields on user-defined functions")?;
match scope.get(field) {
Some(field) => Ok(field),
Some(binding) => Ok(binding.read_checked(sink)),
None => match self.name() {
Some(name) => bail!("function `{name}` does not contain field `{field}`"),
None => bail!("function does not contain field `{field}`"),
@ -266,6 +279,14 @@ impl Func {
}
}
/// Extract the plugin function, if it is one.
pub fn to_plugin(&self) -> Option<&PluginFunc> {
match &self.repr {
Repr::Plugin(func) => Some(func),
_ => None,
}
}
/// Call the function with the given context and arguments.
pub fn call<A: IntoArgs>(
&self,
@ -307,6 +328,12 @@ impl Func {
context,
args,
),
Repr::Plugin(func) => {
let inputs = args.all::<Bytes>()?;
let output = func.call(inputs).at(args.span)?;
args.finish()?;
Ok(Value::Bytes(output))
}
Repr::With(with) => {
args.items = with.1.items.iter().cloned().chain(args.items).collect();
with.0.call(engine, context, args)
@ -334,8 +361,6 @@ impl Func {
#[func]
pub fn with(
self,
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args,
/// The arguments to apply to the function.
#[external]
@ -361,8 +386,6 @@ impl Func {
#[func]
pub fn where_(
self,
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args,
/// The fields to filter for.
#[variadic]
@ -414,10 +437,10 @@ impl PartialEq for Func {
}
}
impl PartialEq<&NativeFuncData> for Func {
fn eq(&self, other: &&NativeFuncData) -> bool {
impl PartialEq<&'static NativeFuncData> for Func {
fn eq(&self, other: &&'static NativeFuncData) -> bool {
match &self.repr {
Repr::Native(native) => native.function == other.function,
Repr::Native(native) => *native == Static(*other),
_ => false,
}
}
@ -429,12 +452,30 @@ impl From<Repr> for Func {
}
}
impl From<&'static NativeFuncData> for Func {
fn from(data: &'static NativeFuncData) -> Self {
Repr::Native(Static(data)).into()
}
}
impl From<Element> for Func {
fn from(func: Element) -> Self {
Repr::Element(func).into()
}
}
impl From<Closure> for Func {
fn from(closure: Closure) -> Self {
Repr::Closure(Arc::new(LazyHash::new(closure))).into()
}
}
impl From<PluginFunc> for Func {
fn from(func: PluginFunc) -> Self {
Repr::Plugin(Arc::new(func)).into()
}
}
/// A Typst function that is defined by a native Rust type that shadows a
/// native Rust function.
pub trait NativeFunc {
@ -470,12 +511,6 @@ pub struct NativeFuncData {
pub returns: LazyLock<CastInfo>,
}
impl From<&'static NativeFuncData> for Func {
fn from(data: &'static NativeFuncData) -> Self {
Repr::Native(Static(data)).into()
}
}
cast! {
&'static NativeFuncData,
self => Func::from(self).into_value(),
@ -529,12 +564,6 @@ impl Closure {
}
}
impl From<Closure> for Func {
fn from(closure: Closure) -> Self {
Repr::Closure(Arc::new(LazyHash::new(closure))).into()
}
}
cast! {
Closure,
self => Value::Func(self.into()),

View File

@ -1,6 +1,7 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError};
use ecow::{eco_format, EcoString};
use smallvec::SmallVec;
use crate::diag::{bail, StrResult};
use crate::foundations::{
@ -322,7 +323,7 @@ impl i64 {
Endianness::Little => self.to_le_bytes(),
};
let mut buf = vec![0u8; size];
let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size);
match endian {
Endianness::Big => {
// Copy the bytes from the array to the buffer, starting from
@ -339,7 +340,7 @@ impl i64 {
}
}
Bytes::from(buf)
Bytes::new(buf)
}
}

View File

@ -25,7 +25,8 @@ mod int;
mod label;
mod module;
mod none;
mod plugin;
#[path = "plugin.rs"]
mod plugin_;
mod scope;
mod selector;
mod str;
@ -56,7 +57,7 @@ pub use self::int::*;
pub use self::label::*;
pub use self::module::*;
pub use self::none::*;
pub use self::plugin::*;
pub use self::plugin_::*;
pub use self::repr::Repr;
pub use self::scope::*;
pub use self::selector::*;
@ -84,16 +85,9 @@ use crate::engine::Engine;
use crate::routines::EvalMode;
use crate::{Feature, Features};
/// Foundational types and functions.
///
/// Here, you'll find documentation for basic data types like [integers]($int)
/// and [strings]($str) as well as details about core computational functions.
#[category]
pub static FOUNDATIONS: Category;
/// Hook up all `foundations` definitions.
pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.category(FOUNDATIONS);
global.start_category(crate::Category::Foundations);
global.define_type::<bool>();
global.define_type::<i64>();
global.define_type::<f64>();
@ -114,16 +108,17 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.define_type::<Symbol>();
global.define_type::<Duration>();
global.define_type::<Version>();
global.define_type::<Plugin>();
global.define_func::<repr::repr>();
global.define_func::<panic>();
global.define_func::<assert>();
global.define_func::<eval>();
global.define_func::<plugin>();
if features.is_enabled(Feature::Html) {
global.define_func::<target>();
}
global.define_module(calc::module());
global.define_module(sys::module(inputs));
global.define("calc", calc::module());
global.define("sys", sys::module(inputs));
global.reset_category();
}
/// Fails with an error.
@ -266,7 +261,6 @@ impl assert {
/// ```
#[func(title = "Evaluate")]
pub fn eval(
/// The engine.
engine: &mut Engine,
/// A string of Typst code to evaluate.
source: Spanned<String>,
@ -301,7 +295,7 @@ pub fn eval(
let dict = scope;
let mut scope = Scope::new();
for (key, value) in dict {
scope.define_spanned(key, value, span);
scope.bind(key.into(), Binding::new(value, span));
}
(engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope)
}

View File

@ -4,17 +4,23 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString};
use typst_syntax::FileId;
use crate::diag::StrResult;
use crate::diag::{bail, DeprecationSink, StrResult};
use crate::foundations::{repr, ty, Content, Scope, Value};
/// An evaluated module, either built-in or resulting from a file.
/// An module of definitions.
///
/// You can access definitions from the module using
/// [field access notation]($scripting/#fields) and interact with it using the
/// [import and include syntaxes]($scripting/#modules). Alternatively, it is
/// possible to convert a module to a dictionary, and therefore access its
/// contents dynamically, using the
/// [dictionary constructor]($dictionary/#constructor).
/// A module
/// - be built-in
/// - stem from a [file import]($scripting/#modules)
/// - stem from a [package import]($scripting/#packages) (and thus indirectly
/// its entrypoint file)
/// - result from a call to the [plugin]($plugin) function
///
/// You can access definitions from the module using [field access
/// notation]($scripting/#fields) and interact with it using the [import and
/// include syntaxes]($scripting/#modules). Alternatively, it is possible to
/// convert a module to a dictionary, and therefore access its contents
/// dynamically, using the [dictionary constructor]($dictionary/#constructor).
///
/// # Example
/// ```example
@ -32,7 +38,7 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Module {
/// The module's name.
name: EcoString,
name: Option<EcoString>,
/// The reference-counted inner fields.
inner: Arc<Repr>,
}
@ -52,14 +58,22 @@ impl Module {
/// Create a new module.
pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self {
Self {
name: name.into(),
name: Some(name.into()),
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
}
}
/// Create a new anonymous module without a name.
pub fn anonymous(scope: Scope) -> Self {
Self {
name: None,
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
}
}
/// Update the module's name.
pub fn with_name(mut self, name: impl Into<EcoString>) -> Self {
self.name = name.into();
self.name = Some(name.into());
self
}
@ -82,8 +96,8 @@ impl Module {
}
/// Get the module's name.
pub fn name(&self) -> &EcoString {
&self.name
pub fn name(&self) -> Option<&EcoString> {
self.name.as_ref()
}
/// Access the module's scope.
@ -104,10 +118,14 @@ impl Module {
}
/// Try to access a definition in the module.
pub fn field(&self, name: &str) -> StrResult<&Value> {
self.scope().get(name).ok_or_else(|| {
eco_format!("module `{}` does not contain `{name}`", self.name())
})
pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<&Value> {
match self.scope().get(field) {
Some(binding) => Ok(binding.read_checked(sink)),
None => match &self.name {
Some(name) => bail!("module `{name}` does not contain `{field}`"),
None => bail!("module does not contain `{field}`"),
},
}
}
/// Extract the module's content.
@ -131,7 +149,10 @@ impl Debug for Module {
impl repr::Repr for Module {
fn repr(&self) -> EcoString {
eco_format!("<module {}>", self.name())
match &self.name {
Some(module) => eco_format!("<module {module}>"),
None => "<module>".into(),
}
}
}

View File

@ -6,7 +6,9 @@ use ecow::eco_format;
use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value};
use crate::foundations::{
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
};
use crate::layout::{Alignment, Length, Rel};
use crate::text::TextElem;
use crate::visualize::Stroke;
@ -30,12 +32,13 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
(Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
(Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
(a, b) => mismatch!("cannot join {} with {}", a, b),
})
}
@ -129,13 +132,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
(Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
(Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
Stroke::from_pair(color, thickness).into_value()
@ -443,7 +447,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Args(a), Args(b)) => a == b,
(Type(a), Type(b)) => a == b,
(Module(a), Module(b)) => a == b,
(Plugin(a), Plugin(b)) => a == b,
(Datetime(a), Datetime(b)) => a == b,
(Duration(a), Duration(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b,

View File

@ -4,43 +4,27 @@ use std::sync::{Arc, Mutex};
use ecow::{eco_format, EcoString};
use typst_syntax::Spanned;
use wasmi::{AsContext, AsContextMut};
use wasmi::Memory;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Bytes};
use crate::World;
use crate::foundations::{cast, func, scope, Binding, Bytes, Func, Module, Scope, Value};
use crate::loading::{DataSource, Load};
/// A WebAssembly plugin.
/// Loads a WebAssembly module.
///
/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin
/// functions may accept multiple [byte buffers]($bytes) as arguments and return
/// a single byte buffer. They should typically be wrapped in idiomatic Typst
/// functions that perform the necessary conversions between native Typst types
/// and bytes.
/// The resulting [module] will contain one Typst [function] for each function
/// export of the loaded WebAssembly module.
///
/// Plugins run in isolation from your system, which means that printing,
/// reading files, or anything like that will not be supported for security
/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit
/// shared WebAssembly library. Many compilers will use the
/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g.
/// emscripten), which allows printing, reading files, etc. This ABI will not
/// directly work with Typst. You will either need to compile to a different
/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
/// Typst WebAssembly plugins need to follow a specific
/// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be
/// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept
/// multiple [byte buffers]($bytes) as arguments and return a single byte
/// buffer. They should typically be wrapped in idiomatic Typst functions that
/// perform the necessary conversions between native Typst types and bytes.
///
/// # Plugins and Packages
/// Plugins are distributed as packages. A package can make use of a plugin
/// simply by including a WebAssembly file and loading it. Because the
/// byte-based plugin interface is quite low-level, plugins are typically
/// exposed through wrapper functions, that also live in the same package.
///
/// # Purity
/// Plugin functions must be pure: Given the same arguments, they must always
/// return the same value. The reason for this is that Typst functions must be
/// pure (which is quite fundamental to the language design) and, since Typst
/// function can call plugin functions, this requirement is inherited. In
/// particular, if a plugin function is called twice with the same arguments,
/// Typst might cache the results and call your function only once.
/// For security reasons, plugins run in isolation from your system. This means
/// that printing, reading files, or similar things are not supported.
///
/// # Example
/// ```example
@ -55,6 +39,50 @@ use crate::World;
/// #concat("hello", "world")
/// ```
///
/// Since the plugin function returns a module, it can be used with import
/// syntax:
/// ```typ
/// #import plugin("hello.wasm"): concatenate
/// ```
///
/// # Purity
/// Plugin functions **must be pure:** A plugin function call most not have any
/// observable side effects on future plugin calls and given the same arguments,
/// it must always return the same value.
///
/// The reason for this is that Typst functions must be pure (which is quite
/// fundamental to the language design) and, since Typst function can call
/// plugin functions, this requirement is inherited. In particular, if a plugin
/// function is called twice with the same arguments, Typst might cache the
/// results and call your function only once. Moreover, Typst may run multiple
/// instances of your plugin in multiple threads, with no state shared between
/// them.
///
/// Typst does not enforce plugin function purity (for efficiency reasons), but
/// calling an impure function will lead to unpredictable and irreproducible
/// results and must be avoided.
///
/// That said, mutable operations _can be_ useful for plugins that require
/// costly runtime initialization. Due to the purity requirement, such
/// initialization cannot be performed through a normal function call. Instead,
/// Typst exposes a [plugin transition API]($plugin.transition), which executes
/// a function call and then creates a derived module with new functions which
/// will observe the side effects produced by the transition call. The original
/// plugin remains unaffected.
///
/// # Plugins and Packages
/// Any Typst code can make use of a plugin simply by including a WebAssembly
/// file and loading it. However, because the byte-based plugin interface is
/// quite low-level, plugins are typically exposed through a package containing
/// the plugin and idiomatic wrapper functions.
///
/// # WASI
/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as
/// their only option (e.g. emscripten), which allows printing, reading files,
/// etc. This ABI will not directly work with Typst. You will either need to
/// compile to a different target or [stub all
/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
///
/// # Protocol
/// To be used as a plugin, a WebAssembly module must conform to the following
/// protocol:
@ -67,8 +95,8 @@ use crate::World;
/// lengths, so `usize/size_t` may be preferable), and return one 32-bit
/// integer.
///
/// - The function should first allocate a buffer `buf` of length
/// `a_1 + a_2 + ... + a_n`, and then call
/// - The function should first allocate a buffer `buf` of length `a_1 + a_2 +
/// ... + a_n`, and then call
/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`.
///
/// - The `a_1` first bytes of the buffer now constitute the first argument, the
@ -85,19 +113,21 @@ use crate::World;
/// then interpreted as an UTF-8 encoded error message.
///
/// ## Imports
/// Plugin modules need to import two functions that are provided by the runtime.
/// (Types and functions are described using WAT syntax.)
/// Plugin modules need to import two functions that are provided by the
/// runtime. (Types and functions are described using WAT syntax.)
///
/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))`
/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func
/// (param i32)))`
///
/// Writes the arguments for the current function into a plugin-allocated
/// buffer. When a plugin function is called, it
/// [receives the lengths](#exports) of its input buffers as arguments. It
/// should then allocate a buffer whose capacity is at least the sum of these
/// lengths. It should then call this function with a `ptr` to the buffer to
/// fill it with the arguments, one after another.
/// buffer. When a plugin function is called, it [receives the
/// lengths](#exports) of its input buffers as arguments. It should then
/// allocate a buffer whose capacity is at least the sum of these lengths. It
/// should then call this function with a `ptr` to the buffer to fill it with
/// the arguments, one after another.
///
/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))`
/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func
/// (param i32 i32)))`
///
/// Sends the output of the current function to the host (Typst). The first
/// parameter shall be a pointer to a buffer (`ptr`), while the second is the
@ -106,75 +136,145 @@ use crate::World;
/// interpreted as an error message, it should be encoded as UTF-8.
///
/// # Resources
/// For more resources, check out the
/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol).
/// It contains:
/// For more resources, check out the [wasm-minimal-protocol
/// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It
/// contains:
///
/// - A list of example plugin implementations and a test runner for these
/// examples
/// - Wrappers to help you write your plugin in Rust (Zig wrapper in
/// development)
/// - A stubber for WASI
#[ty(scope, cast)]
#[derive(Clone)]
pub struct Plugin(Arc<Repr>);
/// The internal representation of a plugin.
struct Repr {
/// The raw WebAssembly bytes.
bytes: Bytes,
/// The function defined by the WebAssembly module.
functions: Vec<(EcoString, wasmi::Func)>,
/// Owns all data associated with the WebAssembly module.
store: Mutex<Store>,
}
/// Owns all data associated with the WebAssembly module.
type Store = wasmi::Store<StoreData>;
/// If there was an error reading/writing memory, keep the offset + length to
/// display an error message.
struct MemoryError {
offset: u32,
length: u32,
write: bool,
}
/// The persistent store data used for communication between store and host.
#[derive(Default)]
struct StoreData {
args: Vec<Bytes>,
output: Vec<u8>,
memory_error: Option<MemoryError>,
#[func(scope)]
pub fn plugin(
engine: &mut Engine,
/// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
source: Spanned<DataSource>,
) -> SourceResult<Module> {
let data = source.load(engine.world)?;
Plugin::module(data).at(source.span)
}
#[scope]
impl Plugin {
/// Creates a new plugin from a WebAssembly file.
#[func(constructor)]
pub fn construct(
/// The engine.
engine: &mut Engine,
/// Path to a WebAssembly file.
///
/// For more details, see the [Paths section]($syntax/#paths).
path: Spanned<EcoString>,
) -> SourceResult<Plugin> {
let Spanned { v: path, span } = path;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
Plugin::new(data).at(span)
impl plugin {
/// Calls a plugin function that has side effects and returns a new module
/// with plugin functions that are guaranteed to have observed the results
/// of the mutable call.
///
/// Note that calling an impure function through a normal function call
/// (without use of the transition API) is forbidden and leads to
/// unpredictable behaviour. Read the [section on purity]($plugin/#purity)
/// for more details.
///
/// In the example below, we load the plugin `hello-mut.wasm` which exports
/// two functions: The `get()` function retrieves a global array as a
/// string. The `add(value)` function adds a value to the global array.
///
/// We call `add` via the transition API. The call `mutated.get()` on the
/// derived module will observe the addition. Meanwhile the original module
/// remains untouched as demonstrated by the `base.get()` call.
///
/// _Note:_ Due to limitations in the internal WebAssembly implementation,
/// the transition API can only guarantee to reflect changes in the plugin's
/// memory, not in WebAssembly globals. If your plugin relies on changes to
/// globals being visible after transition, you might want to avoid use of
/// the transition API for now. We hope to lift this limitation in the
/// future.
///
/// ```typ
/// #let base = plugin("hello-mut.wasm")
/// #assert.eq(base.get(), "[]")
///
/// #let mutated = plugin.transition(base.add, "hello")
/// #assert.eq(base.get(), "[]")
/// #assert.eq(mutated.get(), "[hello]")
/// ```
#[func]
pub fn transition(
/// The plugin function to call.
func: PluginFunc,
/// The byte buffers to call the function with.
#[variadic]
arguments: Vec<Bytes>,
) -> StrResult<Module> {
func.transition(arguments)
}
}
/// A function loaded from a WebAssembly plugin.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PluginFunc {
/// The underlying plugin, shared by this and the other functions.
plugin: Arc<Plugin>,
/// The name of the plugin function.
name: EcoString,
}
impl PluginFunc {
/// The name of the plugin function.
pub fn name(&self) -> &str {
&self.name
}
/// Call the WebAssembly function with the given arguments.
#[comemo::memoize]
#[typst_macros::time(name = "call plugin")]
pub fn call(&self, args: Vec<Bytes>) -> StrResult<Bytes> {
self.plugin.call(&self.name, args)
}
/// Transition a plugin and turn the result into a module.
#[comemo::memoize]
#[typst_macros::time(name = "transition plugin")]
pub fn transition(&self, args: Vec<Bytes>) -> StrResult<Module> {
self.plugin.transition(&self.name, args).map(Plugin::into_module)
}
}
cast! {
PluginFunc,
self => Value::Func(self.into()),
v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(),
}
/// A plugin with potentially multiple instances for multi-threaded
/// execution.
struct Plugin {
/// Shared by all variants of the plugin.
base: Arc<PluginBase>,
/// A pool of plugin instances.
///
/// When multiple plugin calls run concurrently due to multi-threading, we
/// create new instances whenever we run out of ones.
pool: Mutex<Vec<PluginInstance>>,
/// A snapshot that new instances should be restored to.
snapshot: Option<Snapshot>,
/// A combined hash that incorporates all function names and arguments used
/// in transitions of this plugin, such that this plugin has a deterministic
/// hash and equality check that can differentiate it from "siblings" (same
/// base, different transitions).
fingerprint: u128,
}
impl Plugin {
/// Create a new plugin from raw WebAssembly bytes.
/// Create a plugin and turn it into a module.
#[comemo::memoize]
#[typst_macros::time(name = "load plugin")]
pub fn new(bytes: Bytes) -> StrResult<Plugin> {
fn module(bytes: Bytes) -> StrResult<Module> {
Self::new(bytes).map(Self::into_module)
}
/// Create a new plugin from raw WebAssembly bytes.
fn new(bytes: Bytes) -> StrResult<Self> {
let engine = wasmi::Engine::default();
let module = wasmi::Module::new(&engine, bytes.as_slice())
.map_err(|err| format!("failed to load WebAssembly module ({err})"))?;
// Ensure that the plugin exports its memory.
if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) {
bail!("plugin does not export its memory");
}
let mut linker = wasmi::Linker::new(&engine);
linker
.func_wrap(
@ -191,58 +291,174 @@ impl Plugin {
)
.unwrap();
let mut store = Store::new(&engine, StoreData::default());
let instance = linker
.instantiate(&mut store, &module)
let base = Arc::new(PluginBase { bytes, linker, module });
let instance = PluginInstance::new(&base, None)?;
Ok(Self {
base,
snapshot: None,
fingerprint: 0,
pool: Mutex::new(vec![instance]),
})
}
/// Execute a function with access to an instsance.
fn call(&self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
// Acquire an instance from the pool (potentially creating a new one).
let mut instance = self.acquire()?;
// Execute the call on an instance from the pool. If the call fails, we
// return early and _don't_ return the instance to the pool as it might
// be irrecoverably damaged.
let output = instance.call(func, args)?;
// Return the instance to the pool.
self.pool.lock().unwrap().push(instance);
Ok(output)
}
/// Call a mutable plugin function, producing a new mutable whose functions
/// are guaranteed to be able to observe the mutation.
fn transition(&self, func: &str, args: Vec<Bytes>) -> StrResult<Plugin> {
// Derive a new transition hash from the old one and the function and arguments.
let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args));
// Execute the mutable call on an instance.
let mut instance = self.acquire()?;
// Call the function. If the call fails, we return early and _don't_
// return the instance to the pool as it might be irrecoverably damaged.
instance.call(func, args)?;
// Snapshot the instance after the mutable call.
let snapshot = instance.snapshot();
// Create a new plugin and move (this is important!) the used instance
// into it, so that the old plugin won't observe the mutation. Also
// save the snapshot so that instances that are initialized for the
// transitioned plugin's pool observe the mutation.
Ok(Self {
base: self.base.clone(),
snapshot: Some(snapshot),
fingerprint,
pool: Mutex::new(vec![instance]),
})
}
/// Acquire an instance from the pool (or create a new one).
fn acquire(&self) -> StrResult<PluginInstance> {
// Don't use match to ensure that the lock is released before we create
// a new instance.
if let Some(instance) = self.pool.lock().unwrap().pop() {
return Ok(instance);
}
PluginInstance::new(&self.base, self.snapshot.as_ref())
}
/// Turn a plugin into a Typst module containing plugin functions.
fn into_module(self) -> Module {
let shared = Arc::new(self);
// Build a scope from the collected functions.
let mut scope = Scope::new();
for export in shared.base.module.exports() {
if matches!(export.ty(), wasmi::ExternType::Func(_)) {
let name = EcoString::from(export.name());
let func = PluginFunc { plugin: shared.clone(), name: name.clone() };
scope.bind(name, Binding::detached(Func::from(func)));
}
}
Module::anonymous(scope)
}
}
impl Debug for Plugin {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Plugin(..)")
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint
}
}
impl Hash for Plugin {
fn hash<H: Hasher>(&self, state: &mut H) {
self.base.bytes.hash(state);
self.fingerprint.hash(state);
}
}
/// Shared by all pooled & transitioned variants of the plugin.
struct PluginBase {
/// The raw WebAssembly bytes.
bytes: Bytes,
/// The compiled WebAssembly module.
module: wasmi::Module,
/// A linker used to create a `Store` for execution.
linker: wasmi::Linker<CallData>,
}
/// An single plugin instance for single-threaded execution.
struct PluginInstance {
/// The underlying wasmi instance.
instance: wasmi::Instance,
/// The execution store of this concrete plugin instance.
store: wasmi::Store<CallData>,
}
/// A snapshot of a plugin instance.
struct Snapshot {
/// The number of pages in the main memory.
mem_pages: u32,
/// The data in the main memory.
mem_data: Vec<u8>,
}
impl PluginInstance {
/// Create a new execution instance of a plugin, potentially restoring
/// a snapshot.
#[typst_macros::time(name = "create plugin instance")]
fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult<PluginInstance> {
let mut store = wasmi::Store::new(base.linker.engine(), CallData::default());
let instance = base
.linker
.instantiate(&mut store, &base.module)
.and_then(|pre_instance| pre_instance.start(&mut store))
.map_err(|e| eco_format!("{e}"))?;
// Ensure that the plugin exports its memory.
if !matches!(
instance.get_export(&store, "memory"),
Some(wasmi::Extern::Memory(_))
) {
bail!("plugin does not export its memory");
let mut instance = PluginInstance { instance, store };
if let Some(snapshot) = snapshot {
instance.restore(snapshot);
}
// Collect exported functions.
let functions = instance
.exports(&store)
.filter_map(|export| {
let name = export.name().into();
export.into_func().map(|func| (name, func))
})
.collect();
Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) })))
Ok(instance)
}
/// Call the plugin function with the given `name`.
#[comemo::memoize]
#[typst_macros::time(name = "call plugin")]
pub fn call(&self, name: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
// Find the function with the given name.
let func = self
.0
.functions
.iter()
.find(|(v, _)| v == name)
.map(|&(_, func)| func)
.ok_or_else(|| {
eco_format!("plugin does not contain a function called {name}")
})?;
/// Call a plugin function with byte arguments.
fn call(&mut self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
let handle = self
.instance
.get_export(&self.store, func)
.unwrap()
.into_func()
.unwrap();
let ty = handle.ty(&self.store);
let mut store = self.0.store.lock().unwrap();
let ty = func.ty(store.as_context());
// Check function signature.
// Check function signature. Do this lazily only when a function is called
// because there might be exported functions like `_initialize` that don't
// match the schema.
if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) {
bail!(
"plugin function `{name}` has a parameter that is not a 32-bit integer"
"plugin function `{func}` has a parameter that is not a 32-bit integer"
);
}
if ty.results() != [wasmi::core::ValType::I32] {
bail!("plugin function `{name}` does not return exactly one 32-bit integer");
bail!("plugin function `{func}` does not return exactly one 32-bit integer");
}
// Check inputs.
@ -263,23 +479,26 @@ impl Plugin {
.collect::<Vec<_>>();
// Store the input data.
store.data_mut().args = args;
self.store.data_mut().args = args;
// Call the function.
let mut code = wasmi::Val::I32(-1);
func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code))
handle
.call(&mut self.store, &lengths, std::slice::from_mut(&mut code))
.map_err(|err| eco_format!("plugin panicked: {err}"))?;
if let Some(MemoryError { offset, length, write }) =
store.data_mut().memory_error.take()
self.store.data_mut().memory_error.take()
{
return Err(eco_format!(
"plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}",
"plugin tried to {kind} out of bounds: \
pointer {offset:#x} is out of bounds for {kind} of length {length}",
kind = if write { "write" } else { "read" }
));
}
// Extract the returned data.
let output = std::mem::take(&mut store.data_mut().output);
let output = std::mem::take(&mut self.store.data_mut().output);
// Parse the functions return value.
match code {
@ -293,42 +512,66 @@ impl Plugin {
_ => bail!("plugin did not respect the protocol"),
};
Ok(output.into())
Ok(Bytes::new(output))
}
/// An iterator over all the function names defined by the plugin.
pub fn iter(&self) -> impl Iterator<Item = &EcoString> {
self.0.functions.as_slice().iter().map(|(func_name, _)| func_name)
/// Creates a snapshot of this instance from which another one can be
/// initialized.
#[typst_macros::time(name = "save snapshot")]
fn snapshot(&self) -> Snapshot {
let memory = self.memory();
let mem_pages = memory.size(&self.store);
let mem_data = memory.data(&self.store).to_vec();
Snapshot { mem_pages, mem_data }
}
/// Restores the instance to a snapshot.
#[typst_macros::time(name = "restore snapshot")]
fn restore(&mut self, snapshot: &Snapshot) {
let memory = self.memory();
let current_size = memory.size(&self.store);
if current_size < snapshot.mem_pages {
memory
.grow(&mut self.store, snapshot.mem_pages - current_size)
.unwrap();
}
memory.data_mut(&mut self.store)[..snapshot.mem_data.len()]
.copy_from_slice(&snapshot.mem_data);
}
/// Retrieves a handle to the plugin's main memory.
fn memory(&self) -> Memory {
self.instance
.get_export(&self.store, "memory")
.unwrap()
.into_memory()
.unwrap()
}
}
impl Debug for Plugin {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Plugin(..)")
}
/// The persistent store data used for communication between store and host.
#[derive(Default)]
struct CallData {
/// Arguments for a current call.
args: Vec<Bytes>,
/// The results of the current call.
output: Vec<u8>,
/// A memory error that occured during execution of the current call.
memory_error: Option<MemoryError>,
}
impl repr::Repr for Plugin {
fn repr(&self) -> EcoString {
"plugin(..)".into()
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.0.bytes == other.0.bytes
}
}
impl Hash for Plugin {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.bytes.hash(state);
}
/// If there was an error reading/writing memory, keep the offset + length to
/// display an error message.
struct MemoryError {
offset: u32,
length: u32,
write: bool,
}
/// Write the arguments to the plugin function into the plugin's memory.
fn wasm_minimal_protocol_write_args_to_buffer(
mut caller: wasmi::Caller<StoreData>,
mut caller: wasmi::Caller<CallData>,
ptr: u32,
) {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
@ -349,7 +592,7 @@ fn wasm_minimal_protocol_write_args_to_buffer(
/// Extracts the output of the plugin function from the plugin's memory.
fn wasm_minimal_protocol_send_result_to_host(
mut caller: wasmi::Caller<StoreData>,
mut caller: wasmi::Caller<CallData>,
ptr: u32,
len: u32,
) {

View File

@ -1,21 +1,17 @@
#[doc(inline)]
pub use typst_macros::category;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use ecow::{eco_format, EcoString};
use indexmap::map::Entry;
use indexmap::IndexMap;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::Span;
use typst_utils::Static;
use crate::diag::{bail, HintedStrResult, HintedString, StrResult};
use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult};
use crate::foundations::{
Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData,
NativeType, Type, Value,
Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType,
Type, Value,
};
use crate::Library;
use crate::{Category, Library};
/// A stack of scopes.
#[derive(Debug, Default, Clone)]
@ -46,14 +42,14 @@ impl<'a> Scopes<'a> {
self.top = self.scopes.pop().expect("no pushed scope");
}
/// Try to access a variable immutably.
pub fn get(&self, var: &str) -> HintedStrResult<&Value> {
/// Try to access a binding immutably.
pub fn get(&self, var: &str) -> HintedStrResult<&Binding> {
std::iter::once(&self.top)
.chain(self.scopes.iter().rev())
.find_map(|scope| scope.get(var))
.or_else(|| {
self.base.and_then(|base| match base.global.scope().get(var) {
Some(value) => Some(value),
Some(binding) => Some(binding),
None if var == "std" => Some(&base.std),
None => None,
})
@ -61,14 +57,28 @@ impl<'a> Scopes<'a> {
.ok_or_else(|| unknown_variable(var))
}
/// Try to access a variable immutably in math.
pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> {
/// Try to access a binding mutably.
pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> {
std::iter::once(&mut self.top)
.chain(&mut self.scopes.iter_mut().rev())
.find_map(|scope| scope.get_mut(var))
.ok_or_else(|| {
match self.base.and_then(|base| base.global.scope().get(var)) {
Some(_) => cannot_mutate_constant(var),
_ if var == "std" => cannot_mutate_constant(var),
_ => unknown_variable(var),
}
})
}
/// Try to access a binding immutably in math.
pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> {
std::iter::once(&self.top)
.chain(self.scopes.iter().rev())
.find_map(|scope| scope.get(var))
.or_else(|| {
self.base.and_then(|base| match base.math.scope().get(var) {
Some(value) => Some(value),
Some(binding) => Some(binding),
None if var == "std" => Some(&base.std),
None => None,
})
@ -81,20 +91,6 @@ impl<'a> Scopes<'a> {
})
}
/// Try to access a variable mutably.
pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> {
std::iter::once(&mut self.top)
.chain(&mut self.scopes.iter_mut().rev())
.find_map(|scope| scope.get_mut(var))
.ok_or_else(|| {
match self.base.and_then(|base| base.global.scope().get(var)) {
Some(_) => cannot_mutate_constant(var),
_ if var == "std" => cannot_mutate_constant(var),
_ => unknown_variable(var),
}
})?
}
/// Check if an std variable is shadowed.
pub fn check_std_shadowed(&self, var: &str) -> bool {
self.base.is_some_and(|base| base.global.scope().get(var).is_some())
@ -104,63 +100,15 @@ impl<'a> Scopes<'a> {
}
}
#[cold]
fn cannot_mutate_constant(var: &str) -> HintedString {
eco_format!("cannot mutate a constant: {}", var).into()
}
/// The error message when a variable is not found.
#[cold]
fn unknown_variable(var: &str) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
if var.contains('-') {
res.hint(eco_format!(
"if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`",
if var.matches('-').count() > 1 { "s" } else { "" },
var.replace('-', " - ")
));
}
res
}
#[cold]
fn unknown_variable_math(var: &str, in_global: bool) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
if matches!(var, "none" | "auto" | "false" | "true") {
res.hint(eco_format!(
"if you meant to use a literal, try adding a hash before it: `#{var}`",
));
} else if in_global {
res.hint(eco_format!(
"`{var}` is not available directly in math, try adding a hash before it: `#{var}`",
));
} else {
res.hint(eco_format!(
"if you meant to display multiple letters as is, try adding spaces between each letter: `{}`",
var.chars()
.flat_map(|c| [' ', c])
.skip(1)
.collect::<EcoString>()
));
res.hint(eco_format!(
"or if you meant to display this as text, try placing it in quotes: `\"{var}\"`"
));
}
res
}
/// A map from binding names to values.
#[derive(Default, Clone)]
pub struct Scope {
map: IndexMap<EcoString, Slot>,
map: IndexMap<EcoString, Binding>,
deduplicate: bool,
category: Option<Category>,
}
/// Scope construction.
impl Scope {
/// Create a new empty scope.
pub fn new() -> Self {
@ -173,7 +121,7 @@ impl Scope {
}
/// Enter a new category.
pub fn category(&mut self, category: Category) {
pub fn start_category(&mut self, category: Category) {
self.category = Some(category);
}
@ -182,107 +130,87 @@ impl Scope {
self.category = None;
}
/// Bind a value to a name.
#[track_caller]
pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) {
self.define_spanned(name, value, Span::detached())
}
/// Bind a value to a name defined by an identifier.
#[track_caller]
pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) {
self.define_spanned(ident.get().clone(), value, ident.span())
}
/// Bind a value to a name.
#[track_caller]
pub fn define_spanned(
&mut self,
name: impl Into<EcoString>,
value: impl IntoValue,
span: Span,
) {
let name = name.into();
#[cfg(debug_assertions)]
if self.deduplicate && self.map.contains_key(&name) {
panic!("duplicate definition: {name}");
}
self.map.insert(
name,
Slot::new(value.into_value(), span, Kind::Normal, self.category),
);
}
/// Define a captured, immutable binding.
pub fn define_captured(
&mut self,
name: EcoString,
value: Value,
capturer: Capturer,
span: Span,
) {
self.map.insert(
name,
Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category),
);
}
/// Define a native function through a Rust type that shadows the function.
pub fn define_func<T: NativeFunc>(&mut self) {
#[track_caller]
pub fn define_func<T: NativeFunc>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Func::from(data));
self.define(data.name, Func::from(data))
}
/// Define a native function with raw function data.
pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) {
self.define(data.name, Func::from(data));
#[track_caller]
pub fn define_func_with_data(
&mut self,
data: &'static NativeFuncData,
) -> &mut Binding {
self.define(data.name, Func::from(data))
}
/// Define a native type.
pub fn define_type<T: NativeType>(&mut self) {
#[track_caller]
pub fn define_type<T: NativeType>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Type::from(data));
self.define(data.name, Type::from(data))
}
/// Define a native element.
pub fn define_elem<T: NativeElement>(&mut self) {
#[track_caller]
pub fn define_elem<T: NativeElement>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Element::from(data));
self.define(data.name, Element::from(data))
}
/// Define a module.
pub fn define_module(&mut self, module: Module) {
self.define(module.name().clone(), module);
/// Define a built-in with compile-time known name and returns a mutable
/// reference to it.
///
/// When the name isn't compile-time known, you should instead use:
/// - `Vm::bind` if you already have [`Binding`]
/// - `Vm::define` if you only have a [`Value`]
/// - [`Scope::bind`](Self::bind) if you are not operating in the context of
/// a `Vm` or if you are binding to something that is not an AST
/// identifier (e.g. when constructing a dynamic
/// [`Module`](super::Module))
#[track_caller]
pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding {
#[cfg(debug_assertions)]
if self.deduplicate && self.map.contains_key(name) {
panic!("duplicate definition: {name}");
}
let mut binding = Binding::detached(value);
binding.category = self.category;
self.bind(name.into(), binding)
}
}
/// Scope manipulation and access.
impl Scope {
/// Inserts a binding into this scope and returns a mutable reference to it.
///
/// Prefer `Vm::bind` if you are operating in the context of a `Vm`.
pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding {
match self.map.entry(name) {
Entry::Occupied(mut entry) => {
entry.insert(binding);
entry.into_mut()
}
Entry::Vacant(entry) => entry.insert(binding),
}
}
/// Try to access a variable immutably.
pub fn get(&self, var: &str) -> Option<&Value> {
self.map.get(var).map(Slot::read)
/// Try to access a binding immutably.
pub fn get(&self, var: &str) -> Option<&Binding> {
self.map.get(var)
}
/// Try to access a variable mutably.
pub fn get_mut(&mut self, var: &str) -> Option<HintedStrResult<&mut Value>> {
self.map
.get_mut(var)
.map(Slot::write)
.map(|res| res.map_err(HintedString::from))
}
/// Get the span of a definition.
pub fn get_span(&self, var: &str) -> Option<Span> {
Some(self.map.get(var)?.span)
}
/// Get the category of a definition.
pub fn get_category(&self, var: &str) -> Option<Category> {
self.map.get(var)?.category
/// Try to access a binding mutably.
pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> {
self.map.get_mut(var)
}
/// Iterate over all definitions.
pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value, Span)> {
self.map.iter().map(|(k, v)| (k, v.read(), v.span))
pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Binding)> {
self.map.iter()
}
}
@ -315,28 +243,111 @@ pub trait NativeScope {
fn scope() -> Scope;
}
/// A slot where a value is stored.
#[derive(Clone, Hash)]
struct Slot {
/// The stored value.
/// A bound value with metadata.
#[derive(Debug, Clone, Hash)]
pub struct Binding {
/// The bound value.
value: Value,
/// The kind of slot, determines how the value can be accessed.
kind: Kind,
/// A span associated with the stored value.
/// The kind of binding, determines how the value can be accessed.
kind: BindingKind,
/// A span associated with the binding.
span: Span,
/// The category of the slot.
/// The category of the binding.
category: Option<Category>,
/// A deprecation message for the definition.
deprecation: Option<&'static str>,
}
/// The different kinds of slots.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum Kind {
enum BindingKind {
/// A normal, mutable binding.
Normal,
/// A captured copy of another variable.
Captured(Capturer),
}
impl Binding {
/// Create a new binding with a span marking its definition site.
pub fn new(value: impl IntoValue, span: Span) -> Self {
Self {
value: value.into_value(),
span,
kind: BindingKind::Normal,
category: None,
deprecation: None,
}
}
/// Create a binding without a span.
pub fn detached(value: impl IntoValue) -> Self {
Self::new(value, Span::detached())
}
/// Marks this binding as deprecated, with the given `message`.
pub fn deprecated(&mut self, message: &'static str) -> &mut Self {
self.deprecation = Some(message);
self
}
/// Read the value.
pub fn read(&self) -> &Value {
&self.value
}
/// Read the value, checking for deprecation.
///
/// 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 {
if let Some(message) = self.deprecation {
sink.emit(message);
}
&self.value
}
/// Try to write to the value.
///
/// This fails if the value is a read-only closure capture.
pub fn write(&mut self) -> StrResult<&mut Value> {
match self.kind {
BindingKind::Normal => Ok(&mut self.value),
BindingKind::Captured(capturer) => bail!(
"variables from outside the {} are \
read-only and cannot be modified",
match capturer {
Capturer::Function => "function",
Capturer::Context => "context expression",
}
),
}
}
/// Create a copy of the binding for closure capturing.
pub fn capture(&self, capturer: Capturer) -> Self {
Self {
kind: BindingKind::Captured(capturer),
..self.clone()
}
}
/// A span associated with the stored value.
pub fn span(&self) -> Span {
self.span
}
/// A deprecation message for the value, if any.
pub fn deprecation(&self) -> Option<&'static str> {
self.deprecation
}
/// The category of the value, if any.
pub fn category(&self) -> Option<Category> {
self.category
}
}
/// What the variable was captured by.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Capturer {
@ -346,71 +357,56 @@ pub enum Capturer {
Context,
}
impl Slot {
/// Create a new slot.
fn new(value: Value, span: Span, kind: Kind, category: Option<Category>) -> Self {
Self { value, span, kind, category }
}
/// Read the value.
fn read(&self) -> &Value {
&self.value
}
/// Try to write to the value.
fn write(&mut self) -> StrResult<&mut Value> {
match self.kind {
Kind::Normal => Ok(&mut self.value),
Kind::Captured(capturer) => {
bail!(
"variables from outside the {} are \
read-only and cannot be modified",
match capturer {
Capturer::Function => "function",
Capturer::Context => "context expression",
}
)
}
}
}
/// The error message when trying to mutate a variable from the standard
/// library.
#[cold]
fn cannot_mutate_constant(var: &str) -> HintedString {
eco_format!("cannot mutate a constant: {}", var).into()
}
/// A group of related definitions.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Category(Static<CategoryData>);
/// The error message when a variable wasn't found.
#[cold]
fn unknown_variable(var: &str) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
impl Category {
/// Create a new category from raw data.
pub const fn from_data(data: &'static CategoryData) -> Self {
Self(Static(data))
if var.contains('-') {
res.hint(eco_format!(
"if you meant to use subtraction, \
try adding spaces around the minus sign{}: `{}`",
if var.matches('-').count() > 1 { "s" } else { "" },
var.replace('-', " - ")
));
}
/// The category's name.
pub fn name(&self) -> &'static str {
self.0.name
}
/// The type's title case name, for use in documentation (e.g. `String`).
pub fn title(&self) -> &'static str {
self.0.title
}
/// Documentation for the category.
pub fn docs(&self) -> &'static str {
self.0.docs
}
res
}
impl Debug for Category {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Category({})", self.name())
}
}
/// The error message when a variable wasn't found it math.
#[cold]
fn unknown_variable_math(var: &str, in_global: bool) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
/// Defines a category.
#[derive(Debug)]
pub struct CategoryData {
pub name: &'static str,
pub title: &'static str,
pub docs: &'static str,
if matches!(var, "none" | "auto" | "false" | "true") {
res.hint(eco_format!(
"if you meant to use a literal, \
try adding a hash before it: `#{var}`",
));
} else if in_global {
res.hint(eco_format!(
"`{var}` is not available directly in math, \
try adding a hash before it: `#{var}`",
));
} else {
res.hint(eco_format!(
"if you meant to display multiple letters as is, \
try adding spaces between each letter: `{}`",
var.chars().flat_map(|c| [' ', c]).skip(1).collect::<EcoString>()
));
res.hint(eco_format!(
"or if you meant to display this as text, \
try placing it in quotes: `\"{var}\"`"
));
}
res
}

View File

@ -7,12 +7,13 @@ use comemo::Tracked;
use ecow::EcoString;
use serde::{Deserialize, Serialize};
use typst_syntax::{Span, Spanned};
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func,
cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func,
IntoValue, Label, Repr, Type, Value, Version,
};
use crate::layout::Alignment;
@ -286,6 +287,30 @@ impl Str {
Ok(c.into())
}
/// Normalizes the string to the given Unicode normal form.
///
/// This is useful when manipulating strings containing Unicode combining
/// characters.
///
/// ```typ
/// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}")
/// #assert.eq("ſ́".normalize(form: "nfkc"), "ś")
/// ```
#[func]
pub fn normalize(
&self,
#[named]
#[default(UnicodeNormalForm::Nfc)]
form: UnicodeNormalForm,
) -> Str {
match form {
UnicodeNormalForm::Nfc => self.nfc().collect(),
UnicodeNormalForm::Nfd => self.nfd().collect(),
UnicodeNormalForm::Nfkc => self.nfkc().collect(),
UnicodeNormalForm::Nfkd => self.nfkd().collect(),
}
}
/// Whether the string contains the specified pattern.
///
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
@ -425,9 +450,7 @@ impl Str {
#[func]
pub fn replace(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The pattern to search for.
pattern: StrPattern,
@ -577,9 +600,10 @@ impl Str {
/// of the resulting parts.
///
/// When the empty string is used as a separator, it separates every
/// character in the string, along with the beginning and end of the
/// string. In practice, this means that the resulting list of parts
/// will contain the empty string at the start and end of the list.
/// character (i.e., Unicode code point) in the string, along with the
/// beginning and end of the string. In practice, this means that the
/// resulting list of parts will contain the empty string at the start
/// and end of the list.
#[func]
pub fn split(
&self,
@ -783,16 +807,31 @@ cast! {
v: f64 => Self::Str(repr::display_float(v).into()),
v: Decimal => Self::Str(format_str!("{}", v)),
v: Version => Self::Str(format_str!("{}", v)),
v: Bytes => Self::Str(
std::str::from_utf8(&v)
.map_err(|_| "bytes are not valid utf-8")?
.into()
),
v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?),
v: Label => Self::Str(v.resolve().as_str().into()),
v: Type => Self::Str(v.long_name().into()),
v: Str => Self::Str(v),
}
/// A Unicode normalization form.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum UnicodeNormalForm {
/// Canonical composition where e.g. accented letters are turned into a
/// single Unicode codepoint.
#[string("nfc")]
Nfc,
/// Canonical decomposition where e.g. accented letters are split into a
/// separate base and diacritic.
#[string("nfd")]
Nfd,
/// Like NFC, but using the Unicode compatibility decompositions.
#[string("nfkc")]
Nfkc,
/// Like NFD, but using the Unicode compatibility decompositions.
#[string("nfkd")]
Nfkd,
}
/// Convert an item of std's `match_indices` to a dictionary.
fn match_to_dict((start, text): (usize, &str)) -> Dict {
dict! {

View File

@ -12,7 +12,8 @@ use typst_utils::LazyHash;
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector,
cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr,
Selector,
};
use crate::text::{FontFamily, FontList, TextElem};
@ -775,107 +776,6 @@ impl<'a> Iterator for Links<'a> {
}
}
/// A sequence of elements with associated styles.
#[derive(Clone, PartialEq, Hash)]
pub struct StyleVec {
/// The elements themselves.
elements: EcoVec<Content>,
/// A run-length encoded list of style lists.
///
/// Each element is a (styles, count) pair. Any elements whose
/// style falls after the end of this list is considered to
/// have an empty style list.
styles: EcoVec<(Styles, usize)>,
}
impl StyleVec {
/// Create a style vector from an unstyled vector content.
pub fn wrap(elements: EcoVec<Content>) -> Self {
Self { elements, styles: EcoVec::new() }
}
/// Create a `StyleVec` from a list of content with style chains.
pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
let depth = trunk.links().count();
let mut elements = EcoVec::with_capacity(buf.len());
let mut styles = EcoVec::<(Styles, usize)>::new();
let mut last: Option<(StyleChain<'a>, usize)> = None;
for &(element, chain) in buf {
elements.push(element.clone());
if let Some((prev, run)) = &mut last {
if chain == *prev {
*run += 1;
} else {
styles.push((prev.suffix(depth), *run));
last = Some((chain, 1));
}
} else {
last = Some((chain, 1));
}
}
if let Some((last, run)) = last {
let skippable = styles.is_empty() && last == trunk;
if !skippable {
styles.push((last.suffix(depth), run));
}
}
(StyleVec { elements, styles }, trunk)
}
/// Whether there are no elements.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// The number of elements.
pub fn len(&self) -> usize {
self.elements.len()
}
/// Iterate over the contained content and style chains.
pub fn iter<'a>(
&'a self,
outer: &'a StyleChain<'_>,
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
static EMPTY: Styles = Styles::new();
self.elements
.iter()
.zip(
self.styles
.iter()
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
.chain(std::iter::repeat(&EMPTY)),
)
.map(|(element, local)| (element, outer.chain(local)))
}
/// Get a style property, but only if it is the same for all children of the
/// style vector.
pub fn shared_get<T: PartialEq>(
&self,
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
self.styles
.iter()
.all(|(local, _)| getter(styles.chain(local)) == value)
.then_some(value)
}
}
impl Debug for StyleVec {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_list().entries(&self.elements).finish()
}
}
/// A property that is resolved with other properties from the style chain.
pub trait Resolve {
/// The type of the resolved output.
@ -939,6 +839,13 @@ impl<T, const N: usize> Fold for SmallVec<[T; N]> {
}
}
impl<T> Fold for OneOrMultiple<T> {
fn fold(self, mut outer: Self) -> Self {
outer.0.extend(self.0);
outer
}
}
/// A variant of fold for foldable optional (`Option<T>`) values where an inner
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
/// implementation, with which folding with an inner `None` always returns

View File

@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128;
use crate::diag::{bail, SourceResult, StrResult};
use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _};
use crate::foundations::{
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
PlainText, Repr as _,
};
/// A Unicode symbol.
///
@ -18,6 +21,7 @@ use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as
/// be accessed using [field access notation]($scripting/#fields):
///
/// - General symbols are defined in the [`sym` module]($category/symbols/sym)
/// and are accessible without the `sym.` prefix in math mode.
/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
///
/// Moreover, you can define custom symbols with this type's constructor
@ -187,7 +191,6 @@ impl Symbol {
/// ```
#[func(constructor)]
pub fn construct(
/// The callsite span.
span: Span,
/// The variants of the symbol.
///
@ -408,7 +411,7 @@ fn find<'a>(
}
let score = (matching, Reverse(total));
if best_score.map_or(true, |b| score > b) {
if best_score.is_none_or(|b| score > b) {
best = Some(candidate.1);
best_score = Some(score);
}
@ -426,3 +429,31 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m)
}
/// A single character.
#[elem(Repr, PlainText)]
pub struct SymbolElem {
/// The symbol's character.
#[required]
pub text: char, // This is called `text` for consistency with `TextElem`.
}
impl SymbolElem {
/// Create a new packed symbol element.
pub fn packed(text: impl Into<char>) -> Content {
Self::new(text.into()).pack()
}
}
impl PlainText for Packed<SymbolElem> {
fn plain_text(&self, text: &mut EcoString) {
text.push(self.text);
}
}
impl crate::foundations::Repr for SymbolElem {
/// Use a custom repr that matches normal content.
fn repr(&self) -> EcoString {
eco_format!("[{}]", self.text)
}
}

View File

@ -3,7 +3,7 @@ use comemo::Tracked;
use crate::diag::HintedStrResult;
use crate::foundations::{elem, func, Cast, Context};
/// The compilation target.
/// The export target.
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)]
pub enum Target {
/// The target that is used for paged, fully laid-out content.
@ -28,11 +28,50 @@ pub struct TargetElem {
pub target: Target,
}
/// Returns the current compilation target.
/// Returns the current export target.
///
/// This function returns either
/// - `{"paged"}` (for PDF, PNG, and SVG export), or
/// - `{"html"}` (for HTML export).
///
/// The design of this function is not yet finalized and for this reason it is
/// guarded behind the `html` feature. Visit the [HTML documentation
/// page]($html) for more details.
///
/// # When to use it
/// This function allows you to format your document properly across both HTML
/// and paged export targets. It should primarily be used in templates and show
/// rules, rather than directly in content. This way, the document's contents
/// can be fully agnostic to the export target and content can be shared between
/// PDF and HTML export.
///
/// # Varying targets
/// This function is [contextual]($context) as the target can vary within a
/// single compilation: When exporting to HTML, the target will be `{"paged"}`
/// while within an [`html.frame`].
///
/// # Example
/// ```example
/// #let kbd(it) = context {
/// if target() == "html" {
/// html.elem("kbd", it)
/// } else {
/// set text(fill: rgb("#1f2328"))
/// let r = 3pt
/// box(
/// fill: rgb("#f6f8fa"),
/// stroke: rgb("#d1d9e0b3"),
/// outset: (y: r),
/// inset: (x: r),
/// radius: r,
/// raw(it)
/// )
/// }
/// }
///
/// Press #kbd("F1") for help.
/// ```
#[func(contextual)]
pub fn target(
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Target> {
pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?))
}

View File

@ -8,7 +8,7 @@ use std::sync::LazyLock;
use ecow::{eco_format, EcoString};
use typst_utils::Static;
use crate::diag::StrResult;
use crate::diag::{bail, DeprecationSink, StrResult};
use crate::foundations::{
cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value,
};
@ -94,10 +94,15 @@ impl Type {
}
/// Get a field from this type's scope, if possible.
pub fn field(&self, field: &str) -> StrResult<&'static Value> {
self.scope()
.get(field)
.ok_or_else(|| eco_format!("type {self} does not contain field `{field}`"))
pub fn field(
&self,
field: &str,
sink: impl DeprecationSink,
) -> StrResult<&'static Value> {
match self.scope().get(field) {
Some(binding) => Ok(binding.read_checked(sink)),
None => bail!("type {self} does not contain field `{field}`"),
}
}
}
@ -136,7 +141,7 @@ impl Repr for Type {
} else if *self == Type::of::<NoneValue>() {
"type(none)"
} else {
self.long_name()
self.short_name()
}
.into()
}

View File

@ -11,12 +11,12 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use typst_syntax::{ast, Span};
use typst_utils::ArcExt;
use crate::diag::{HintedStrResult, HintedString, StrResult};
use crate::diag::{DeprecationSink, HintedStrResult, HintedString, StrResult};
use crate::foundations::{
fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime,
Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module,
NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str,
Styles, Symbol, Type, Version,
NativeElement, NativeType, NoneValue, Reflect, Repr, Resolve, Scope, Str, Styles,
Symbol, SymbolElem, Type, Version,
};
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::text::{RawContent, RawElem, TextElem};
@ -84,8 +84,6 @@ pub enum Value {
Type(Type),
/// A module.
Module(Module),
/// A WebAssembly plugin.
Plugin(Plugin),
/// A dynamic value.
Dyn(Dynamic),
}
@ -147,7 +145,6 @@ impl Value {
Self::Args(_) => Type::of::<Args>(),
Self::Type(_) => Type::of::<Type>(),
Self::Module(_) => Type::of::<Module>(),
Self::Plugin(_) => Type::of::<Plugin>(),
Self::Dyn(v) => v.ty(),
}
}
@ -158,15 +155,15 @@ impl Value {
}
/// Try to access a field on the value.
pub fn field(&self, field: &str) -> StrResult<Value> {
pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> {
match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
Self::Version(version) => version.component(field).map(Self::Int),
Self::Dict(dict) => dict.get(field).cloned(),
Self::Content(content) => content.field_by_name(field),
Self::Type(ty) => ty.field(field).cloned(),
Self::Func(func) => func.field(field).cloned(),
Self::Module(module) => module.field(field).cloned(),
Self::Type(ty) => ty.field(field, sink).cloned(),
Self::Func(func) => func.field(field, sink).cloned(),
Self::Module(module) => module.field(field, sink).cloned(),
_ => fields::field(self, field),
}
}
@ -181,16 +178,6 @@ impl Value {
}
}
/// The name, if this is a function, type, or module.
pub fn name(&self) -> Option<&str> {
match self {
Self::Func(func) => func.name(),
Self::Type(ty) => Some(ty.short_name()),
Self::Module(module) => Some(module.name()),
_ => None,
}
}
/// Try to extract documentation for the value.
pub fn docs(&self) -> Option<&'static str> {
match self {
@ -209,7 +196,7 @@ impl Value {
Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
Self::Str(v) => TextElem::packed(v),
Self::Version(v) => TextElem::packed(eco_format!("{v}")),
Self::Symbol(v) => TextElem::packed(v.get()),
Self::Symbol(v) => SymbolElem::packed(v.get()),
Self::Content(v) => v,
Self::Module(module) => module.content(),
_ => RawElem::new(RawContent::Text(self.repr()))
@ -261,7 +248,6 @@ impl Debug for Value {
Self::Args(v) => Debug::fmt(v, f),
Self::Type(v) => Debug::fmt(v, f),
Self::Module(v) => Debug::fmt(v, f),
Self::Plugin(v) => Debug::fmt(v, f),
Self::Dyn(v) => Debug::fmt(v, f),
}
}
@ -299,7 +285,6 @@ impl Repr for Value {
Self::Args(v) => v.repr(),
Self::Type(v) => v.repr(),
Self::Module(v) => v.repr(),
Self::Plugin(v) => v.repr(),
Self::Dyn(v) => v.repr(),
}
}
@ -350,7 +335,6 @@ impl Hash for Value {
Self::Args(v) => v.hash(state),
Self::Type(v) => v.hash(state),
Self::Module(v) => v.hash(state),
Self::Plugin(v) => v.hash(state),
Self::Dyn(v) => v.hash(state),
}
}
@ -459,15 +443,15 @@ impl<'de> Visitor<'de> for ValueVisitor {
}
fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value())
Ok(Bytes::new(v.to_vec()).into_value())
}
fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value())
Ok(Bytes::new(v.to_vec()).into_value())
}
fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value())
Ok(Bytes::new(v).into_value())
}
fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
@ -656,7 +640,7 @@ primitive! { Duration: "duration", Duration }
primitive! { Content: "content",
Content,
None => Content::empty(),
Symbol(v) => TextElem::packed(v.get()),
Symbol(v) => SymbolElem::packed(v.get()),
Str(v) => TextElem::packed(v)
}
primitive! { Styles: "styles", Styles }
@ -671,7 +655,6 @@ primitive! {
primitive! { Args: "arguments", Args }
primitive! { Type: "type", Type }
primitive! { Module: "module", Module }
primitive! { Plugin: "plugin", Plugin }
impl<T: Reflect> Reflect for Arc<T> {
fn input() -> CastInfo {
@ -730,6 +713,11 @@ mod tests {
assert_eq!(value.into_value().repr(), exp);
}
#[test]
fn test_value_size() {
assert!(std::mem::size_of::<Value>() <= 32);
}
#[test]
fn test_value_debug() {
// Primitives.

View File

@ -210,7 +210,10 @@ impl HtmlAttr {
/// Creates a compile-time constant `HtmlAttr`.
///
/// Should only be used in const contexts because it can panic.
/// Must only be used in const contexts (in a constant definition or
/// explicit `const { .. }` block) because otherwise a panic for a malformed
/// attribute or not auto-internible constant will only be caught at
/// runtime.
#[track_caller]
pub const fn constant(string: &'static str) -> Self {
if string.is_empty() {
@ -472,17 +475,55 @@ pub mod tag {
wbr
}
/// Whether this is a void tag whose associated element may not have a
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
/// Whether an element is considered metadata.
pub fn is_metadata(tag: HtmlTag) -> bool {
matches!(
tag,
self::base
| self::link
| self::meta
| self::noscript
| self::script
| self::style
| self::template
| self::title
)
}
/// Whether nodes with the tag have the CSS property `display: block` by
/// default.
///
/// If this is true, then pretty-printing can insert spaces around such
/// nodes and around the contents of such nodes.
///
/// However, when users change the properties of such tags via CSS, the
/// insertion of whitespace may actually impact the visual output; for
/// example, <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how
/// adding CSS rules to `<p>` can make it sensitive to whitespace. In such
/// cases, users should disable pretty-printing.
pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
@ -569,42 +610,29 @@ pub mod tag {
)
}
/// Whether this is a void tag whose associated element may not have a
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
/// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// by default.
pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
self::table
| self::thead
| self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
| self::colgroup
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
}
/// Predefined constants for HTML attributes.
///
/// Note: These are very incomplete.
#[allow(non_upper_case_globals)]
pub mod attr {
use super::HtmlAttr;
@ -619,13 +647,18 @@ pub mod attr {
attrs! {
charset
cite
colspan
content
href
name
value
reversed
role
rowspan
start
style
value
}
#[allow(non_upper_case_globals)]
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
}

View File

@ -6,53 +6,77 @@ pub use self::dom::*;
use ecow::EcoString;
use crate::foundations::{category, elem, Category, Content, Module, Scope};
/// HTML output.
#[category]
pub static HTML: Category;
use crate::foundations::{elem, Content, Module, Scope};
/// Create a module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.category(HTML);
html.start_category(crate::Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
Module::new("html", html)
}
/// A HTML element that can contain Typst content.
/// An HTML element that can contain Typst content.
///
/// Typst's HTML export automatically generates the appropriate tags for most
/// elements. However, sometimes, it is desirable to retain more control. For
/// example, when using Typst to generate your blog, you could use this function
/// to wrap each article in an `<article>` tag.
///
/// Typst is aware of what is valid HTML. A tag and its attributes must form
/// syntactically valid HTML. Some tags, like `meta` do not accept content.
/// Hence, you must not provide a body for them. We may add more checks in the
/// future, so be sure that you are generating valid HTML when using this
/// function.
///
/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
/// you instead create them with this function, Typst will omit its own tags.
///
/// ```typ
/// #html.elem("div", attrs: (style: "background: aqua"))[
/// A div with _Typst content_ inside!
/// ]
/// ```
#[elem(name = "elem")]
pub struct HtmlElem {
/// The element's tag.
#[required]
pub tag: HtmlTag,
/// The element's attributes.
/// The element's HTML attributes.
#[borrowed]
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl HtmlElem {
/// Add an atribute to the element.
/// Add an attribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.get_or_insert_with(Default::default).push(attr, value);
self
}
}
/// An element that forces its contents to be laid out.
/// An element that lays out its content as an inline SVG.
///
/// Integrates content that requires layout (e.g. a plot) into HTML output
/// by turning it into an inline SVG.
/// Sometimes, converting Typst content to HTML is not desirable. This can be
/// the case for plots and other content that relies on positioning and styling
/// to convey its message.
///
/// This function allows you to use the Typst layout engine that would also be
/// used for PDF, SVG, and PNG export to render a part of your document exactly
/// how it would appear when exported in one of these formats. It embeds the
/// content as an inline SVG.
#[elem]
pub struct FrameElem {
/// The contents that shall be laid out.
/// The content that shall be laid out.
#[positional]
#[required]
pub body: Content,

View File

@ -428,11 +428,8 @@ impl Counter {
#[func(contextual)]
pub fn get(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
) -> SourceResult<CounterState> {
let loc = context.location().at(span)?;
@ -444,11 +441,8 @@ impl Counter {
#[func(contextual)]
pub fn display(
self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The call span of the display.
span: Span,
/// A [numbering pattern or a function]($numbering), which specifies how
/// to display the counter. If given a function, that function receives
@ -482,11 +476,8 @@ impl Counter {
#[func(contextual)]
pub fn at(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// The place at which the counter's value should be retrieved.
selector: LocatableSelector,
@ -500,11 +491,8 @@ impl Counter {
#[func(contextual)]
pub fn final_(
&self,
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The callsite span.
span: Span,
) -> SourceResult<CounterState> {
context.introspect().at(span)?;
@ -528,7 +516,6 @@ impl Counter {
#[func]
pub fn step(
self,
/// The call span of the update.
span: Span,
/// The depth at which to step the counter. Defaults to `{1}`.
#[named]
@ -545,7 +532,6 @@ impl Counter {
#[func]
pub fn update(
self,
/// The call span of the update.
span: Span,
/// If given an integer or array of integers, sets the counter to that
/// value. If given a function, that function receives the previous
@ -800,7 +786,7 @@ impl ManualPageCounter {
let Some(elem) = elem.to_packed::<CounterUpdateElem>() else {
continue;
};
if *elem.key() == CounterKey::Page {
if elem.key == CounterKey::Page {
let mut state = CounterState(smallvec![self.logical]);
state.update(engine, elem.update.clone())?;
self.logical = state.first();

View File

@ -44,9 +44,6 @@ use crate::introspection::Location;
/// ```
/// Refer to the [`selector`] type for more details on before/after selectors.
#[func(contextual)]
pub fn here(
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Location> {
pub fn here(context: Tracked<Context>) -> HintedStrResult<Location> {
context.location()
}

View File

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

View File

@ -24,9 +24,7 @@ use crate::introspection::Location;
/// ```
#[func(contextual)]
pub fn locate(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// A selector that should match exactly one element. This element will be
/// located.

View File

@ -25,24 +25,11 @@ pub use self::query_::*;
pub use self::state::*;
pub use self::tag::*;
use crate::foundations::{category, Category, Scope};
/// Interactions between document parts.
///
/// This category is home to Typst's introspection capabilities: With the
/// `counter` function, you can access and manipulate page, section, figure, and
/// equation counters or create custom ones. Meanwhile, the `query` function
/// lets you search for elements in the document to construct things like a list
/// of figures or headers which show the current chapter title.
///
/// Most of the functions are _contextual._ It is recommended to read the chapter
/// on [context] before continuing here.
#[category]
pub static INTROSPECTION: Category;
use crate::foundations::Scope;
/// Hook up all `introspection` definitions.
pub fn define(global: &mut Scope) {
global.category(INTROSPECTION);
global.start_category(crate::Category::Introspection);
global.define_type::<Location>();
global.define_type::<Counter>();
global.define_type::<State>();
@ -50,4 +37,5 @@ pub fn define(global: &mut Scope) {
global.define_func::<here>();
global.define_func::<query>();
global.define_func::<locate>();
global.reset_category();
}

View File

@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value};
/// ```
#[func(contextual)]
pub fn query(
/// The engine.
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// Can be
/// - an element function like a `heading` or `figure`,

Some files were not shown because too many files have changed in this diff Show More