Merge remote-tracking branch 'typst/0.13'

This commit is contained in:
stelzo 2025-03-08 16:49:36 +01:00
commit 7a02240264
No known key found for this signature in database
GPG Key ID: FC4EF89052319374
330 changed files with 12942 additions and 3770 deletions

1205
Cargo.lock generated

File diff suppressed because it is too large Load Diff

5022
Cargo.lock.bak Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"]
resolver = "2"
[workspace.package]
version = "0.12.0"
version = "0.13.1"
rust-version = "1.80" # also change in ci.yml
authors = ["The Typst Project Developers"]
edition = "2021"
@ -16,24 +16,24 @@ keywords = ["typst"]
readme = "README.md"
[workspace.dependencies]
typst = { path = "crates/typst", version = "0.12.0" }
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
typst-html = { path = "crates/typst-html", version = "0.12.0" }
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
typst-library = { path = "crates/typst-library", version = "0.12.0" }
typst-macros = { path = "crates/typst-macros", version = "0.12.0" }
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" }
typst-realize = { path = "crates/typst-realize", version = "0.12.0" }
typst-render = { path = "crates/typst-render", version = "0.12.0" }
typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
typst-timing = { path = "crates/typst-timing", version = "0.12.0" }
typst-utils = { path = "crates/typst-utils", version = "0.12.0" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" }
typst = { path = "crates/typst", version = "0.13.1" }
typst-cli = { path = "crates/typst-cli", version = "0.13.1" }
typst-eval = { path = "crates/typst-eval", version = "0.13.1" }
typst-html = { path = "crates/typst-html", version = "0.13.1" }
typst-ide = { path = "crates/typst-ide", version = "0.13.1" }
typst-kit = { path = "crates/typst-kit", version = "0.13.1" }
typst-layout = { path = "crates/typst-layout", version = "0.13.1" }
typst-library = { path = "crates/typst-library", version = "0.13.1" }
typst-macros = { path = "crates/typst-macros", version = "0.13.1" }
typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" }
typst-realize = { path = "crates/typst-realize", version = "0.13.1" }
typst-render = { path = "crates/typst-render", version = "0.13.1" }
typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = "0.13.1"
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.1" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -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"
gix = "0.68.0"
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,7 +124,7 @@ 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"
@ -134,11 +135,11 @@ 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

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

View File

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

View File

@ -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,11 +460,9 @@ 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.
@ -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);
}
}

View File

@ -55,7 +55,7 @@ fn eval_code<'a>(
_ => expr.eval(vm)?,
};
output = ops::join(output, value).at(span)?;
output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?;
if let Some(event) = &vm.flow {
warn_for_discarded_content(&mut vm.engine, event, &output);
@ -99,6 +99,7 @@ impl Eval for ast::Expr<'_> {
Self::Term(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),
@ -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

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

View File

@ -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());
}
}
}

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

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

View File

@ -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;
@ -20,10 +20,11 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
#[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,
}
@ -88,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;
@ -118,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,23 @@ 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.
for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
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 +427,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 +465,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,
})
}
}
_ => {}
}
}
@ -551,9 +554,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());
}
}
}
@ -856,13 +859,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,
@ -1474,7 +1475,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);
}
@ -1758,4 +1760,25 @@ mod tests {
.must_include(["this", "that"])
.must_exclude(["*", "figure"]);
}
#[test]
fn test_autocomplete_type_methods() {
test("#\"hello\".", -1).must_include(["len", "contains"]);
}
#[test]
fn test_autocomplete_content_methods() {
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
.must_include(["indented", "body", "page"]);
}
#[test]
fn test_autocomplete_symbol_variants() {
test("#sym.arrow.", -1)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
test("$ arrow. $", -3)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
}
}

View File

@ -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,24 +93,26 @@ pub fn named_items<T>(
// ```
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
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));
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 (span, value) = item.path().iter().fold(
(bound.span(), source.map(|(value, _)| value)),
|(span, value), path_ident| {
let scope = value.and_then(|v| v.scope());
let span = scope
.and_then(|s| s.get_span(&path_ident))
.unwrap_or(Span::detached())
.or(span);
let value = scope.and_then(|s| s.get(&path_ident));
(span, value)
},
);
if let Some(res) =
recv(NamedItem::Import(bound.get(), span, value))
{
let item = NamedItem::Import(bound.get(), span, value);
if let Some(res) = recv(item) {
return Some(res);
}
}
@ -175,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>),
}
@ -186,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,
}
}
@ -194,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(),
}
}
@ -202,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,
}
}
@ -356,7 +362,17 @@ 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\"")

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};
@ -206,7 +206,12 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Toolti
};
// 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 }
gix = { workspace = true, optional = true, features = ["worktree-mutation", "blocking-network-client"] }

View File

@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use crate::package_downloads::{Downloader, PackageDownloader, Progress};
use ecow::eco_format;
use once_cell::sync::OnceCell;
use typst_library::diag::{PackageError, PackageResult, StrResult};
use typst_syntax::package::{
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
@ -15,6 +16,9 @@ pub const DEFAULT_PACKAGES_SUBDIR: &str = "typst/packages";
/// The default vendor sub directory within the project root.
pub const DEFAULT_VENDOR_SUBDIR: &str = "vendor";
/// The public namespace in the default Typst registry.
pub const DEFAULT_NAMESPACE: &str = "preview";
/// Holds information about where packages should be stored and downloads them
/// on demand, if possible.
#[derive(Debug)]
@ -116,24 +120,6 @@ impl PackageStorage {
&self,
spec: &VersionlessPackageSpec,
) -> StrResult<PackageVersion> {
// Same logical flow as per package download. Check package path, then check online.
// Do not check in the data directory because the latter is not intended for storage
// of local packages.
let subdir = format!("{}/{}", spec.namespace, spec.name);
let res = self
.package_path
.iter()
.flat_map(|dir| std::fs::read_dir(dir.join(&subdir)).ok())
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter_map(|path| path.file_name()?.to_string_lossy().parse().ok())
.max();
if let Some(version) = res {
return Ok(version);
}
self.download_index(spec)?
.iter()
.filter(|package| package.name == spec.name)

View File

@ -20,13 +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,
@ -34,6 +37,7 @@ pub fn collect<'a>(
locator: Locator<'a>,
base: Size,
expand: bool,
mode: FlowMode,
) -> SourceResult<Vec<Child<'a>>> {
Collector {
engine,
@ -43,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.
@ -57,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));
@ -95,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 {
@ -110,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 =
@ -166,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
@ -219,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`].

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

@ -10,7 +10,8 @@ use typst_library::layout::{
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.
@ -49,15 +50,26 @@ pub fn layout_image(
}
// Construct the image itself.
let image = Image::with_fonts(
data.clone(),
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(
@ -129,10 +143,10 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
.to_lowercase();
match ext.as_str() {
"png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
"jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
"gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
"svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
"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()),
_ => {}
}
}

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

@ -29,7 +29,7 @@ use crate::modifiers::{FrameModifiers, FrameModify};
/// 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,
@ -66,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
@ -405,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()) {

View File

@ -17,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

@ -6,7 +6,7 @@ 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::GridLayouter;
@ -22,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()
@ -41,11 +42,17 @@ pub fn layout_list(
let mut locator = locator.split();
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()),
));
}
@ -78,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()
@ -124,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

@ -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::{
@ -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());

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;
@ -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

@ -13,6 +13,7 @@ use typst_library::math::{EquationElem, MathSize};
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};
@ -275,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 {
@ -629,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};
@ -29,15 +30,7 @@ pub fn layout_lr(
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);

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()),
@ -641,7 +644,7 @@ 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,
@ -688,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

@ -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::{
@ -22,54 +22,66 @@ pub fn layout_text(
) -> SourceResult<()> {
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

@ -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

@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> {
self.last_point = point;
self.last_control_from = point;
self.is_started = true;
self.is_empty = true;
}
/// Add a line segment.
@ -1281,7 +1282,7 @@ impl ControlPoints {
}
}
/// Helper to draw arcs with bezier curves.
/// Helper to draw arcs with zier curves.
trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&mut self, start: Point, center: Point, end: Point);
@ -1305,7 +1306,7 @@ impl CurveExt for Curve {
}
}
/// Get the control points for a bezier curve that approximates a circular arc for
/// Get the control points for a zier curve that approximates a circular arc for
/// a start point, an end point and a center of the circle whose arc connects
/// the two.
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {

View File

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

View File

@ -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,47 @@ 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(&mut self, message: &str);
/// Emits the given deprecation message into this sink, with the given
/// hints.
fn emit_with_hints(&mut self, message: &str, hints: &[&str]);
}
impl DeprecationSink for () {
fn emit(&mut self, _: &str) {}
fn emit_with_hints(&mut self, _: &str, _: &[&str]) {}
}
impl DeprecationSink for (&mut Vec<SourceDiagnostic>, Span) {
fn emit(&mut self, message: &str) {
self.0.push(SourceDiagnostic::warning(self.1, message));
}
fn emit_with_hints(&mut self, message: &str, hints: &[&str]) {
self.0.push(
SourceDiagnostic::warning(self.1, message)
.with_hints(hints.iter().copied().map(Into::into)),
);
}
}
impl DeprecationSink for (&mut Engine<'_>, Span) {
fn emit(&mut self, message: &str) {
self.0.sink.warn(SourceDiagnostic::warning(self.1, message));
}
fn emit_with_hints(&mut self, message: &str, hints: &[&str]) {
self.0.sink.warn(
SourceDiagnostic::warning(self.1, message)
.with_hints(hints.iter().copied().map(Into::into)),
);
}
}
/// A part of a diagnostic's [trace](SourceDiagnostic::trace).
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Tracepoint {

View File

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

View File

@ -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,6 +160,13 @@ impl f64 {
#[named]
#[default(Endianness::Little)]
endian: Endianness,
/// The size of the resulting bytes.
///
/// This must be either 4 or 8. The call will return the
/// representation of this float in either
/// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32
/// (single-precision) or binary64 (double-precision) format
/// depending on the provided size.
#[named]
#[default(8)]
size: u32,

View File

@ -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)
@ -410,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,
}
}
@ -425,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 {
@ -466,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(),
@ -525,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

@ -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.
@ -300,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

@ -5,8 +5,10 @@ use std::cmp::Ordering;
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::diag::{bail, DeprecationSink, HintedStrResult, StrResult};
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;
@ -19,7 +21,7 @@ macro_rules! mismatch {
}
/// Join a value with another value.
pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
pub fn join(lhs: Value, rhs: Value, sink: &mut dyn DeprecationSink) -> StrResult<Value> {
use Value::*;
Ok(match (lhs, rhs) {
(a, None) => a,
@ -30,13 +32,24 @@ 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),
// Type compatibility.
(Type(a), Str(b)) => {
warn_type_str_join(sink);
Str(format_str!("{a}{b}"))
}
(Str(a), Type(b)) => {
warn_type_str_join(sink);
Str(format_str!("{a}{b}"))
}
(a, b) => mismatch!("cannot join {} with {}", a, b),
})
}
@ -86,7 +99,11 @@ pub fn neg(value: Value) -> HintedStrResult<Value> {
}
/// Compute the sum of two values.
pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
pub fn add(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
use Value::*;
Ok(match (lhs, rhs) {
(a, None) => a,
@ -130,10 +147,10 @@ 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),
@ -154,6 +171,16 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Datetime(a), Duration(b)) => Datetime(a + b),
(Duration(a), Datetime(b)) => Datetime(b + a),
// Type compatibility.
(Type(a), Str(b)) => {
warn_type_str_add(sink);
Str(format_str!("{a}{b}"))
}
(Str(a), Type(b)) => {
warn_type_str_add(sink);
Str(format_str!("{a}{b}"))
}
(Dyn(a), Dyn(b)) => {
// Alignments can be summed.
if let (Some(&a), Some(&b)) =
@ -392,13 +419,21 @@ pub fn or(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
}
/// Compute whether two values are equal.
pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
Ok(Value::Bool(equal(&lhs, &rhs)))
pub fn eq(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
Ok(Value::Bool(equal(&lhs, &rhs, sink)))
}
/// Compute whether two values are unequal.
pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
Ok(Value::Bool(!equal(&lhs, &rhs)))
pub fn neq(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
Ok(Value::Bool(!equal(&lhs, &rhs, sink)))
}
macro_rules! comparison {
@ -417,7 +452,7 @@ comparison!(gt, ">", Ordering::Greater);
comparison!(geq, ">=", Ordering::Greater | Ordering::Equal);
/// Determine whether two values are equal.
pub fn equal(lhs: &Value, rhs: &Value) -> bool {
pub fn equal(lhs: &Value, rhs: &Value, sink: &mut dyn DeprecationSink) -> bool {
use Value::*;
match (lhs, rhs) {
// Compare reflexively.
@ -445,7 +480,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,
@ -462,6 +496,12 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
rat == rel.rel && rel.abs.is_zero()
}
// Type compatibility.
(Type(ty), Str(str)) | (Str(str), Type(ty)) => {
warn_type_str_equal(sink, str);
ty.compat_name() == str.as_str()
}
_ => false,
}
}
@ -533,8 +573,12 @@ fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult<Ordering> {
}
/// Test whether one value is "in" another one.
pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
pub fn in_(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs, sink) {
Ok(Value::Bool(b))
} else {
mismatch!("cannot apply 'in' to {} and {}", lhs, rhs)
@ -542,8 +586,12 @@ pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
}
/// Test whether one value is "not in" another one.
pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
pub fn not_in(
lhs: Value,
rhs: Value,
sink: &mut dyn DeprecationSink,
) -> HintedStrResult<Value> {
if let Some(b) = contains(&lhs, &rhs, sink) {
Ok(Value::Bool(!b))
} else {
mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs)
@ -551,13 +599,27 @@ pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
}
/// Test for containment.
pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
pub fn contains(
lhs: &Value,
rhs: &Value,
sink: &mut dyn DeprecationSink,
) -> Option<bool> {
use Value::*;
match (lhs, rhs) {
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
(Str(a), Dict(b)) => Some(b.contains(a)),
(a, Array(b)) => Some(b.contains(a.clone())),
(a, Array(b)) => Some(b.contains_impl(a, sink)),
// Type compatibility.
(Type(a), Str(b)) => {
warn_type_in_str(sink);
Some(b.as_str().contains(a.compat_name()))
}
(Type(a), Dict(b)) => {
warn_type_in_dict(sink);
Some(b.contains(a.compat_name()))
}
_ => Option::None,
}
@ -567,3 +629,90 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
fn too_large() -> &'static str {
"value is too large"
}
#[cold]
fn warn_type_str_add(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"adding strings and types is deprecated",
&["convert the type to a string with `str` first"],
);
}
#[cold]
fn warn_type_str_join(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"joining strings and types is deprecated",
&["convert the type to a string with `str` first"],
);
}
#[cold]
fn warn_type_str_equal(sink: &mut dyn DeprecationSink, s: &str) {
// Only warn if `s` looks like a type name to prevent false positives.
if is_compat_type_name(s) {
sink.emit_with_hints(
"comparing strings with types is deprecated",
&[
"compare with the literal type instead",
"this comparison will always return `false` in future Typst releases",
],
);
}
}
#[cold]
fn warn_type_in_str(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"checking whether a type is contained in a string is deprecated",
&["this compatibility behavior only exists because `type` used to return a string"],
);
}
#[cold]
fn warn_type_in_dict(sink: &mut dyn DeprecationSink) {
sink.emit_with_hints(
"checking whether a type is contained in a dictionary is deprecated",
&["this compatibility behavior only exists because `type` used to return a string"],
);
}
fn is_compat_type_name(s: &str) -> bool {
matches!(
s,
"boolean"
| "alignment"
| "angle"
| "arguments"
| "array"
| "bytes"
| "color"
| "content"
| "counter"
| "datetime"
| "decimal"
| "dictionary"
| "direction"
| "duration"
| "float"
| "fraction"
| "function"
| "gradient"
| "integer"
| "label"
| "length"
| "location"
| "module"
| "pattern"
| "ratio"
| "regex"
| "relative length"
| "selector"
| "state"
| "string"
| "stroke"
| "symbol"
| "tiling"
| "type"
| "version"
)
}

View File

@ -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::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::loading::{DataSource, Load};
/// #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::loading::{DataSource, Load};
/// 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::loading::{DataSource, Load};
/// 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,72 +136,145 @@ use crate::loading::{DataSource, Load};
/// 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(
engine: &mut Engine,
/// A path to a WebAssembly file or raw WebAssembly bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>,
) -> SourceResult<Plugin> {
let data = source.load(engine.world)?;
Plugin::new(data).at(source.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(
@ -188,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.
@ -260,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,39 +515,63 @@ impl Plugin {
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();
@ -346,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, mut 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

@ -776,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.

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.
///
@ -425,3 +428,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,7 +28,49 @@ 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(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,
};
@ -44,6 +44,16 @@ use crate::foundations::{
/// #type(int) \
/// #type(type)
/// ```
///
/// # Compatibility
/// In Typst 0.7 and lower, the `type` function returned a string instead of a
/// type. Compatibility with the old way will remain until Typst 0.14 to give
/// package authors time to upgrade.
///
/// - Checks like `{int == "integer"}` evaluate to `{true}`
/// - Adding/joining a type and string will yield a string
/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}`
/// if the dictionary has a string key matching the type's name
#[ty(scope, cast)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Type(Static<NativeTypeData>);
@ -94,10 +104,23 @@ 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}`"),
}
}
}
// Type compatibility.
impl Type {
/// The type's backward-compatible name.
pub fn compat_name(&self) -> &str {
self.long_name()
}
}
@ -136,7 +159,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(),
}
}
@ -307,7 +292,8 @@ impl Repr for Value {
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
ops::equal(self, other)
// No way to emit deprecation warnings here :(
ops::equal(self, other, &mut ())
}
}
@ -350,7 +336,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),
}
}
@ -656,7 +641,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 +656,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 +714,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

@ -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

@ -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

@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke};
/// An inline-level container that sizes content.
///
/// All elements except inline math, text, and boxes are block-level and cannot
/// occur inside of a paragraph. The box function can be used to integrate such
/// elements into a paragraph. Boxes take the size of their contents by default
/// but can also be sized explicitly.
/// occur inside of a [paragraph]($par). The box function can be used to
/// integrate such elements into a paragraph. Boxes take the size of their
/// contents by default but can also be sized explicitly.
///
/// # Example
/// ```example
@ -184,6 +184,10 @@ pub enum InlineItem {
/// Such a container can be used to separate content, size it, and give it a
/// background or border.
///
/// Blocks are also the primary way to control whether text becomes part of a
/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph)
/// for more details.
///
/// # Examples
/// With a block, you can give a background to content while still allowing it
/// to break across multiple pages.

View File

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

View File

@ -64,17 +64,11 @@ pub use self::spacing::*;
pub use self::stack::*;
pub use self::transform::*;
use crate::foundations::{category, Category, Scope};
/// Arranging elements on the page in different ways.
///
/// By combining layout functions, you can create complex and automatic layouts.
#[category]
pub static LAYOUT: Category;
use crate::foundations::Scope;
/// Hook up all `layout` definitions.
pub fn define(global: &mut Scope) {
global.category(LAYOUT);
global.start_category(crate::Category::Layout);
global.define_type::<Length>();
global.define_type::<Angle>();
global.define_type::<Ratio>();
@ -103,4 +97,5 @@ pub fn define(global: &mut Scope) {
global.define_elem::<HideElem>();
global.define_func::<measure>();
global.define_func::<layout>();
global.reset_category();
}

View File

@ -270,7 +270,7 @@ pub struct PageElem {
/// margin: (top: 32pt, bottom: 20pt),
/// header: [
/// #set text(8pt)
/// #smallcaps[Typst Academcy]
/// #smallcaps[Typst Academy]
/// #h(1fr) _Exercise Sheet 3_
/// ],
/// )

View File

@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length};
/// Space may be inserted between the instances of the body parameter, so be
/// sure to adjust the [`justify`]($repeat.justify) parameter accordingly.
///
/// Errors if there no bounds on the available space, as it would create
/// Errors if there are no bounds on the available space, as it would create
/// infinite content.
///
/// # Example

View File

@ -29,11 +29,12 @@ pub mod visualize;
use std::ops::{Deref, Range};
use serde::{Deserialize, Serialize};
use typst_syntax::{FileId, Source, Span};
use typst_utils::{LazyHash, SmallBitSet};
use crate::diag::FileResult;
use crate::foundations::{Array, Bytes, Datetime, Dict, Module, Scope, Styles, Value};
use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles};
use crate::layout::{Alignment, Dir};
use crate::text::{Font, FontBook};
use crate::visualize::Color;
@ -148,7 +149,7 @@ pub struct Library {
/// everything else configurable via set and show rules).
pub styles: Styles,
/// The standard library as a value. Used to provide the `std` variable.
pub std: Value,
pub std: Binding,
/// In-development features that were enabled.
pub features: Features,
}
@ -196,12 +197,11 @@ impl LibraryBuilder {
let math = math::module();
let inputs = self.inputs.unwrap_or_default();
let global = global(math.clone(), inputs, &self.features);
let std = Value::Module(global.clone());
Library {
global,
global: global.clone(),
math,
styles: Styles::new(),
std,
std: Binding::detached(global),
features: self.features,
}
}
@ -237,31 +237,72 @@ pub enum Feature {
Html,
}
/// A group of related standard library definitions.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Category {
Foundations,
Introspection,
Layout,
DataLoading,
Math,
Model,
Symbols,
Text,
Visualize,
Pdf,
Html,
Svg,
Png,
}
impl Category {
/// The kebab-case name of the category.
pub fn name(&self) -> &'static str {
match self {
Self::Foundations => "foundations",
Self::Introspection => "introspection",
Self::Layout => "layout",
Self::DataLoading => "data-loading",
Self::Math => "math",
Self::Model => "model",
Self::Symbols => "symbols",
Self::Text => "text",
Self::Visualize => "visualize",
Self::Pdf => "pdf",
Self::Html => "html",
Self::Svg => "svg",
Self::Png => "png",
}
}
}
/// Construct the module with global definitions.
fn global(math: Module, inputs: Dict, features: &Features) -> Module {
let mut global = Scope::deduplicating();
self::foundations::define(&mut global, inputs, features);
self::model::define(&mut global);
self::text::define(&mut global);
global.reset_category();
global.define_module(math);
self::layout::define(&mut global);
self::visualize::define(&mut global);
self::introspection::define(&mut global);
self::loading::define(&mut global);
self::symbols::define(&mut global);
self::pdf::define(&mut global);
global.reset_category();
global.define("math", math);
global.define("pdf", self::pdf::module());
if features.is_enabled(Feature::Html) {
global.define_module(self::html::module());
global.define("html", self::html::module());
}
prelude(&mut global);
Module::new("global", global)
}
/// Defines scoped values that are globally available, too.
fn prelude(global: &mut Scope) {
global.reset_category();
global.define("black", Color::BLACK);
global.define("gray", Color::GRAY);
global.define("silver", Color::SILVER);

View File

@ -20,9 +20,7 @@ use crate::loading::{DataSource, Load};
#[func(scope, title = "CBOR")]
pub fn cbor(
engine: &mut Engine,
/// A path to a CBOR file or raw CBOR bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
@ -34,10 +32,8 @@ pub fn cbor(
#[scope]
impl cbor {
/// Reads structured data from CBOR bytes.
///
/// This function is deprecated. The [`cbor`] function now accepts bytes
/// directly.
#[func(title = "Decode CBOR")]
#[deprecated = "`cbor.decode` is deprecated, directly pass bytes to `cbor` instead"]
pub fn decode(
engine: &mut Engine,
/// CBOR data.

View File

@ -26,9 +26,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "CSV")]
pub fn csv(
engine: &mut Engine,
/// Path to a CSV file or raw CSV bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a CSV file or raw CSV bytes.
source: Spanned<DataSource>,
/// The delimiter that separates columns in the CSV file.
/// Must be a single ASCII character.
@ -96,10 +94,8 @@ pub fn csv(
#[scope]
impl csv {
/// Reads structured data from a CSV string/bytes.
///
/// This function is deprecated. The [`csv`] function now accepts bytes
/// directly.
#[func(title = "Decode CSV")]
#[deprecated = "`csv.decode` is deprecated, directly pass bytes to `csv` instead"]
pub fn decode(
engine: &mut Engine,
/// CSV data.
@ -136,18 +132,10 @@ impl Default for Delimiter {
cast! {
Delimiter,
self => self.0.into_value(),
v: EcoString => {
let mut chars = v.chars();
let first = chars.next().ok_or("delimiter must not be empty")?;
if chars.next().is_some() {
bail!("delimiter must be a single character");
}
if !first.is_ascii() {
bail!("delimiter must be an ASCII character");
}
Self(first)
c: char => if c.is_ascii() {
Self(c)
} else {
bail!("delimiter must be an ASCII character")
},
}

View File

@ -51,9 +51,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "JSON")]
pub fn json(
engine: &mut Engine,
/// Path to a JSON file or raw JSON bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a JSON file or raw JSON bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
@ -65,10 +63,8 @@ pub fn json(
#[scope]
impl json {
/// Reads structured data from a JSON string/bytes.
///
/// This function is deprecated. The [`json`] function now accepts bytes
/// directly.
#[func(title = "Decode JSON")]
#[deprecated = "`json.decode` is deprecated, directly pass bytes to `json` instead"]
pub fn decode(
engine: &mut Engine,
/// JSON data.

View File

@ -29,19 +29,12 @@ pub use self::yaml_::*;
use crate::diag::{At, SourceResult};
use crate::foundations::OneOrMultiple;
use crate::foundations::{cast, category, Bytes, Category, Scope, Str};
use crate::foundations::{cast, Bytes, Scope, Str};
use crate::World;
/// Data loading from external files.
///
/// These functions help you with loading and embedding data, for example from
/// the results of an experiment.
#[category]
pub static DATA_LOADING: Category;
/// Hook up all `data-loading` definitions.
pub(super) fn define(global: &mut Scope) {
global.category(DATA_LOADING);
global.start_category(crate::Category::DataLoading);
global.define_func::<read>();
global.define_func::<csv>();
global.define_func::<json>();
@ -49,6 +42,7 @@ pub(super) fn define(global: &mut Scope) {
global.define_func::<yaml>();
global.define_func::<cbor>();
global.define_func::<xml>();
global.reset_category();
}
/// Something we can retrieve byte data from.

View File

@ -29,9 +29,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "TOML")]
pub fn toml(
engine: &mut Engine,
/// A path to a TOML file or raw TOML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a TOML file or raw TOML bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
@ -44,10 +42,8 @@ pub fn toml(
#[scope]
impl toml {
/// Reads structured data from a TOML string/bytes.
///
/// This function is deprecated. The [`toml`] function now accepts bytes
/// directly.
#[func(title = "Decode TOML")]
#[deprecated = "`toml.decode` is deprecated, directly pass bytes to `toml` instead"]
pub fn decode(
engine: &mut Engine,
/// TOML data.

View File

@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable};
/// let author = find-child(elem, "author")
/// let pars = find-child(elem, "content")
///
/// heading(title.children.first())
/// [= #title.children.first()]
/// text(10pt, weight: "medium")[
/// Published by
/// #author.children.first()
/// ]
///
/// for p in pars.children {
/// if (type(p) == "dictionary") {
/// if type(p) == dictionary {
/// parbreak()
/// p.children.first()
/// }
@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable};
///
/// #let data = xml("example.xml")
/// #for elem in data.first().children {
/// if (type(elem) == "dictionary") {
/// if type(elem) == dictionary {
/// article(elem)
/// }
/// }
@ -58,9 +58,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "XML")]
pub fn xml(
engine: &mut Engine,
/// A path to an XML file or raw XML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to an XML file or raw XML bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
@ -77,10 +75,8 @@ pub fn xml(
#[scope]
impl xml {
/// Reads structured data from an XML string/bytes.
///
/// This function is deprecated. The [`xml`] function now accepts bytes
/// directly.
#[func(title = "Decode XML")]
#[deprecated = "`xml.decode` is deprecated, directly pass bytes to `xml` instead"]
pub fn decode(
engine: &mut Engine,
/// XML data.

View File

@ -41,9 +41,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "YAML")]
pub fn yaml(
engine: &mut Engine,
/// A path to a YAML file or raw YAML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
/// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
source: Spanned<DataSource>,
) -> SourceResult<Value> {
let data = source.load(engine.world)?;
@ -55,10 +53,8 @@ pub fn yaml(
#[scope]
impl yaml {
/// Reads structured data from a YAML string/bytes.
///
/// This function is deprecated. The [`yaml`] function now accepts bytes
/// directly.
#[func(title = "Decode YAML")]
#[deprecated = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead"]
pub fn decode(
engine: &mut Engine,
/// YAML data.

View File

@ -1,8 +1,7 @@
use crate::diag::bail;
use crate::foundations::{cast, elem, func, Content, NativeElement, Value};
use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel};
use crate::math::Mathy;
use crate::text::TextElem;
/// Attaches an accent to a base.
///
@ -142,8 +141,8 @@ cast! {
Accent,
self => self.0.into_value(),
v: char => Self::new(v),
v: Content => match v.to_packed::<TextElem>() {
Some(elem) => Value::Str(elem.text.clone().into()).cast()?,
None => bail!("expected text"),
v: Content => match v.to_packed::<SymbolElem>() {
Some(elem) => Self::new(elem.text),
None => bail!("expected a symbol"),
},
}

View File

@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// A mathematical equation.
///
/// Can be displayed inline with text or as a separate block.
/// Can be displayed inline with text or as a separate block. An equation
/// becomes block-level through the presence of at least one space after the
/// opening dollar sign and one space before the closing dollar sign.
///
/// # Example
/// ```example
@ -229,35 +231,20 @@ impl Refable for Packed<EquationElem> {
}
impl Outlinable for Packed<EquationElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.block(StyleChain::default()) {
return Ok(None);
}
let Some(numbering) = self.numbering() else {
return Ok(None);
};
// After synthesis, this should always be custom content.
let mut supplement = match (**self).supplement(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
};
fn outlined(&self) -> bool {
self.block(StyleChain::default()) && self.numbering().is_some()
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement += TextElem::packed("\u{a0}");
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
let numbers = self.counter().display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
Ok(Some(supplement + numbers))
fn body(&self) -> Content {
Content::empty()
}
}

View File

@ -1,7 +1,6 @@
use crate::foundations::{elem, func, Content, NativeElement};
use crate::foundations::{elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel};
use crate::math::Mathy;
use crate::text::TextElem;
/// Scales delimiters.
///
@ -19,7 +18,7 @@ pub struct LrElem {
#[parse(
let mut arguments = args.all::<Content>()?.into_iter();
let mut body = arguments.next().unwrap_or_default();
arguments.for_each(|arg| body += TextElem::packed(',') + arg);
arguments.for_each(|arg| body += SymbolElem::packed(',') + arg);
body
)]
pub body: Content,
@ -125,9 +124,9 @@ fn delimited(
) -> Content {
let span = body.span();
let mut elem = LrElem::new(Content::sequence([
TextElem::packed(left),
SymbolElem::packed(left),
body,
TextElem::packed(right),
SymbolElem::packed(right),
]));
// Push size only if size is provided
if let Some(size) = size {

View File

@ -1,6 +1,6 @@
use smallvec::{smallvec, SmallVec};
use typst_syntax::Spanned;
use typst_utils::Numeric;
use typst_utils::{default_math_class, Numeric};
use unicode_math_class::MathClass;
use crate::diag::{bail, At, HintedStrResult, StrResult};
@ -292,7 +292,7 @@ impl Delimiter {
pub fn char(c: char) -> StrResult<Self> {
if !matches!(
unicode_math_class::class(c),
default_math_class(c),
Some(MathClass::Opening | MathClass::Closing | MathClass::Fence),
) {
bail!("invalid delimiter: \"{}\"", c)
@ -311,7 +311,7 @@ impl Delimiter {
Some(']') => Self(Some('[')),
Some('{') => Self(Some('}')),
Some('}') => Self(Some('{')),
Some(c) => match unicode_math_class::class(c) {
Some(c) => match default_math_class(c) {
Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)),
Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)),
_ => Self(Some(c)),

View File

@ -27,119 +27,10 @@ pub use self::underover::*;
use typst_utils::singleton;
use unicode_math_class::MathClass;
use crate::foundations::{
category, elem, Category, Content, Module, NativeElement, Scope,
};
use crate::foundations::{elem, Content, Module, NativeElement, Scope};
use crate::layout::{Em, HElem};
use crate::text::TextElem;
/// Typst has special [syntax]($syntax/#math) and library functions to typeset
/// mathematical formulas. Math formulas can be displayed inline with text or as
/// separate blocks. They will be typeset into their own block if they start and
/// end with at least one space (e.g. `[$ x^2 $]`).
///
/// # Variables
/// In math, single letters are always displayed as is. Multiple letters,
/// however, are interpreted as variables and functions. To display multiple
/// letters verbatim, you can place them into quotes and to access single letter
/// variables, you can use the [hash syntax]($scripting/#expressions).
///
/// ```example
/// $ A = pi r^2 $
/// $ "area" = pi dot "radius"^2 $
/// $ cal(A) :=
/// { x in RR | x "is natural" } $
/// #let x = 5
/// $ #x < 17 $
/// ```
///
/// # Symbols
/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like
/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in
/// different variants. You can select between different variants by applying
/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of
/// shorthand sequences like `=>` that approximate a symbol. When such a
/// shorthand exists, the symbol's documentation lists it.
///
/// ```example
/// $ x < y => x gt.eq.not y $
/// ```
///
/// # Line Breaks
/// Formulas can also contain line breaks. Each line can contain one or multiple
/// _alignment points_ (`&`) which are then aligned.
///
/// ```example
/// $ sum_(k=0)^n k
/// &= 1 + ... + n \
/// &= (n(n+1)) / 2 $
/// ```
///
/// # Function calls
/// Math mode supports special function calls without the hash prefix. In these
/// "math calls", the argument list works a little differently than in code:
///
/// - Within them, Typst is still in "math mode". Thus, you can write math
/// directly into them, but need to use hash syntax to pass code expressions
/// (except for strings, which are available in the math syntax).
/// - They support positional and named arguments, as well as argument
/// spreading.
/// - They don't support trailing content blocks.
/// - They provide additional syntax for 2-dimensional argument lists. The
/// semicolon (`;`) merges preceding arguments separated by commas into an
/// array argument.
///
/// ```example
/// $ frac(a^2, 2) $
/// $ vec(1, 2, delim: "[") $
/// $ mat(1, 2; 3, 4) $
/// $ mat(..#range(1, 5).chunks(2)) $
/// $ lim_x =
/// op("lim", limits: #true)_x $
/// ```
///
/// To write a verbatim comma or semicolon in a math call, escape it with a
/// backslash. The colon on the other hand is only recognized in a special way
/// if directly preceded by an identifier, so to display it verbatim in those
/// cases, you can just insert a space before it.
///
/// Functions calls preceded by a hash are normal code function calls and not
/// affected by these rules.
///
/// # Alignment
/// When equations include multiple _alignment points_ (`&`), this creates
/// blocks of alternatingly right- and left-aligned columns. In the example
/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is
/// left-aligned. The word "given" is also left-aligned because `&&` creates two
/// alignment points in a row, alternating the alignment twice. `& &` and `&&`
/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned
/// because just one `&` precedes it. Each alignment point simply alternates
/// between right-aligned/left-aligned.
///
/// ```example
/// $ (3x + y) / 7 &= 9 && "given" \
/// 3x + y &= 63 & "multiply by 7" \
/// 3x &= 63 - y && "subtract y" \
/// x &= 21 - y/3 & "divide by 3" $
/// ```
///
/// # Math fonts
/// You can set the math font by with a [show-set rule]($styling/#show-rules) as
/// demonstrated below. Note that only special OpenType math fonts are suitable
/// for typesetting maths.
///
/// ```example
/// #show math.equation: set text(font: "Fira Math")
/// $ sum_(i in NN) 1 + i $
/// ```
///
/// # Math module
/// All math functions are part of the `math` [module]($scripting/#modules),
/// which is available by default in equations. Outside of equations, they can
/// be accessed with the `math.` prefix.
#[category]
pub static MATH: Category;
// Spacings.
pub const THIN: Em = Em::new(1.0 / 6.0);
pub const MEDIUM: Em = Em::new(2.0 / 9.0);
@ -150,7 +41,7 @@ pub const WIDE: Em = Em::new(2.0);
/// Create a module with all math definitions.
pub fn module() -> Module {
let mut math = Scope::deduplicating();
math.category(MATH);
math.start_category(crate::Category::Math);
math.define_elem::<EquationElem>();
math.define_elem::<TextElem>();
math.define_elem::<LrElem>();

View File

@ -1,6 +1,6 @@
use ecow::EcoString;
use crate::foundations::{elem, Content, NativeElement, Scope};
use crate::foundations::{elem, Content, NativeElement, Scope, SymbolElem};
use crate::layout::HElem;
use crate::math::{upright, Mathy, THIN};
use crate::text::TextElem;
@ -17,9 +17,9 @@ use crate::text::TextElem;
/// # Predefined Operators { #predefined }
/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`,
/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`,
/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
/// `gcd`, `lcm`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`,
/// `limsup`, `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`,
/// `sinc`, `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
#[elem(title = "Text Operator", Mathy)]
pub struct OpElem {
/// The operator's text.
@ -38,6 +38,7 @@ macro_rules! ops {
let operator = EcoString::from(ops!(@name $name $(: $value)?));
math.define(
stringify!($name),
// Latex also uses their equivalent of `TextElem` here.
OpElem::new(TextElem::new(operator).into())
.with_limits(ops!(@limit $($tts)*))
.pack()
@ -46,7 +47,7 @@ macro_rules! ops {
let dif = |d| {
HElem::new(THIN.into()).with_weak(true).pack()
+ upright(TextElem::packed(d))
+ upright(SymbolElem::packed(d))
};
math.define("dif", dif('d'));
math.define("Dif", dif('D'));
@ -75,6 +76,7 @@ ops! {
dim,
exp,
gcd (limits),
lcm (limits),
hom,
id,
im,

View File

@ -17,7 +17,7 @@ use hayagriva::{
use indexmap::IndexMap;
use smallvec::{smallvec, SmallVec};
use typst_syntax::{Span, Spanned};
use typst_utils::{ManuallyHash, NonZeroExt, PicoStr};
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
@ -29,7 +29,7 @@ use crate::foundations::{
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sizing, TrackSizings, VElem,
Sides, Sizing, TrackSizings,
};
use crate::loading::{DataSource, Load};
use crate::model::{
@ -38,7 +38,8 @@ use crate::model::{
};
use crate::routines::{EvalMode, Routines};
use crate::text::{
FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta,
FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem,
WeightDelta,
};
use crate::World;
@ -205,19 +206,20 @@ impl Show for Packed<BibliographyElem> {
const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5);
let span = self.span();
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span()),
.spanned(span),
);
}
let span = self.span();
let works = Works::generate(engine).at(span)?;
let references = works
.references
@ -225,10 +227,9 @@ impl Show for Packed<BibliographyElem> {
.ok_or("CSL style is not suitable for bibliographies")
.at(span)?;
let row_gutter = ParElem::spacing_in(styles);
let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack();
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let row_gutter = ParElem::spacing_in(styles);
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(GridChild::Item(GridItem::Cell(
@ -245,23 +246,27 @@ impl Show for Packed<BibliographyElem> {
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
.pack()
.spanned(self.span()),
.spanned(span),
);
} else {
for (i, (_, reference)) in references.iter().enumerate() {
if i > 0 {
seq.push(row_gutter_elem.clone());
}
seq.push(reference.clone());
for (_, reference) in references {
let realized = reference.clone();
let block = if works.hanging_indent {
let body = HElem::new((-INDENT).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(INDENT.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
seq.push(block.pack().spanned(span));
}
}
let mut content = Content::sequence(seq);
if works.hanging_indent {
content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
}
Ok(content)
Ok(Content::sequence(seq))
}
}
@ -1046,7 +1051,8 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con
match format.font_variant {
citationberg::FontVariant::Normal => {}
citationberg::FontVariant::SmallCaps => {
content = content.styled(TextElem::set_smallcaps(true));
content =
content.styled(TextElem::set_smallcaps(Some(Smallcaps::Minuscules)));
}
}

View File

@ -9,9 +9,11 @@ use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlAttr, HtmlElem};
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
use crate::model::{
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
};
/// A numbered list.
///
@ -226,22 +228,29 @@ impl EnumElem {
impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
let mut elem = HtmlElem::new(tag::ol);
if self.reversed(styles) {
elem =
elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed");
elem = elem.with_attr(attr::reversed, "reversed");
}
return Ok(elem
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
li.with_body(Some(item.body.clone())).pack().spanned(item.span())
}))))
.pack()
.spanned(self.span()));
if let Some(n) = self.start(styles).custom() {
elem = elem.with_attr(attr::start, eco_format!("{n}"));
}
let body = Content::sequence(self.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
li.with_body(Some(body)).pack().spanned(item.span())
}));
return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
}
let mut realized =
@ -249,7 +258,7 @@ impl Show for Packed<EnumElem> {
.pack()
.spanned(self.span());
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();

View File

@ -19,7 +19,9 @@ use crate::layout::{
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
PlaceElem, PlacementScope, VAlignment, VElem,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::model::{
Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement,
};
use crate::text::{Lang, Region, TextElem};
use crate::visualize::ImageElem;
@ -156,6 +158,7 @@ pub struct FigureElem {
pub scope: PlacementScope,
/// The figure's caption.
#[borrowed]
pub caption: Option<Packed<FigureCaption>>,
/// The kind of figure this is.
@ -305,7 +308,7 @@ impl Synthesize for Packed<FigureElem> {
));
// Fill the figure's caption.
let mut caption = elem.caption(styles);
let mut caption = elem.caption(styles).clone();
if let Some(caption) = &mut caption {
caption.synthesize(engine, styles)?;
caption.push_kind(kind.clone());
@ -327,11 +330,12 @@ impl Synthesize for Packed<FigureElem> {
impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let target = TargetElem::target_in(styles);
let mut realized = self.body.clone();
// Build the caption, if any.
if let Some(caption) = self.caption(styles) {
if let Some(caption) = self.caption(styles).clone() {
let (first, second) = match caption.position(styles) {
OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Bottom => (realized, caption.pack()),
@ -340,24 +344,27 @@ impl Show for Packed<FigureElem> {
seq.push(first);
if !target.is_html() {
let v = VElem::new(self.gap(styles).into()).with_weak(true);
seq.push(v.pack().spanned(self.span()))
seq.push(v.pack().spanned(span))
}
seq.push(second);
realized = Content::sequence(seq)
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
if target.is_html() {
return Ok(HtmlElem::new(tag::figure)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
.spanned(span));
}
// Wrap the contents in a block.
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span());
.spanned(span);
// Wrap in a float.
if let Some(align) = self.placement(styles) {
@ -366,10 +373,10 @@ impl Show for Packed<FigureElem> {
.with_scope(self.scope(styles))
.with_float(true)
.pack()
.spanned(self.span());
.spanned(span);
} else if self.scope(styles) == PlacementScope::Parent {
bail!(
self.span(),
span,
"parent-scoped placement is only available for floating figures";
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
);
@ -423,46 +430,26 @@ impl Refable for Packed<FigureElem> {
}
impl Outlinable for Packed<FigureElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
&& (self.caption(StyleChain::default()).is_some()
|| self.numbering().is_some())
}
fn prefix(&self, numbers: Content) -> Content {
let supplement = self.supplement();
if !supplement.is_empty() {
supplement + TextElem::packed('\u{a0}') + numbers
} else {
numbers
}
}
let Some(caption) = self.caption(StyleChain::default()) else {
return Ok(None);
};
let mut realized = caption.body.clone();
if let (
Smart::Custom(Some(Supplement::Content(mut supplement))),
Some(Some(counter)),
Some(numbering),
) = (
(**self).supplement(StyleChain::default()).clone(),
(**self).counter(),
self.numbering(),
) {
let numbers = counter.display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
let separator = caption.get_separator(StyleChain::default());
realized = supplement + numbers + separator + caption.body.clone();
}
Ok(Some(realized))
fn body(&self) -> Content {
self.caption(StyleChain::default())
.as_ref()
.map(|caption| caption.body.clone())
.unwrap_or_default()
}
}
@ -623,14 +610,17 @@ impl Show for Packed<FigureCaption> {
realized = supplement + numbers + self.get_separator(styles) + realized;
}
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::figcaption)
Ok(if TargetElem::target_in(styles).is_html() {
HtmlElem::new(tag::figcaption)
.with_body(Some(realized))
.pack()
.spanned(self.span()));
}
Ok(realized)
.spanned(self.span())
} else {
BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span())
})
}
}

View File

@ -310,11 +310,9 @@ impl Show for Packed<FootnoteEntry> {
impl ShowSet for Packed<FootnoteEntry> {
fn show_set(&self, _: StyleChain) -> Styles {
let text_size = Em::new(0.85);
let leading = Em::new(0.5);
let mut out = Styles::new();
out.set(ParElem::set_leading(leading.into()));
out.set(TextElem::set_size(TextSize(text_size.into())));
out.set(ParElem::set_leading(Em::new(0.5).into()));
out.set(TextElem::set_size(TextSize(Em::new(0.85).into())));
out
}
}

View File

@ -1,7 +1,7 @@
use std::num::NonZeroUsize;
use ecow::eco_format;
use typst_utils::NonZeroExt;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{warning, SourceResult};
use crate::engine::Engine;
@ -13,8 +13,8 @@ use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region};
use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
/// A section heading.
@ -264,10 +264,6 @@ impl Show for Packed<HeadingElem> {
realized = numbering + spacing + realized;
}
if indent != Abs::zero() && !html {
realized = realized.styled(ParElem::set_hanging_indent(indent.into()));
}
Ok(if html {
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
@ -294,8 +290,17 @@ impl Show for Packed<HeadingElem> {
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
}
} else {
let realized = BlockBody::Content(realized);
BlockElem::new().with_body(Some(realized)).pack().spanned(span)
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
block.pack().spanned(span)
})
}
}
@ -351,32 +356,21 @@ impl Refable for Packed<HeadingElem> {
}
impl Outlinable for Packed<HeadingElem> {
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>> {
if !self.outlined(StyleChain::default()) {
return Ok(None);
}
let mut content = self.body.clone();
if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() {
let numbers = Counter::of(HeadingElem::elem()).display_at_loc(
engine,
self.location().unwrap(),
styles,
numbering,
)?;
content = numbers + SpaceElem::shared().clone() + content;
};
Ok(Some(content))
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
}
fn level(&self) -> NonZeroUsize {
(**self).resolve_level(StyleChain::default())
}
fn prefix(&self, numbers: Content) -> Content {
numbers
}
fn body(&self) -> Content {
self.body.clone()
}
}
impl LocalName for Packed<HeadingElem> {

View File

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

View File

@ -8,7 +8,7 @@ use crate::foundations::{
};
use crate::html::{tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::ParElem;
use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem;
/// A bullet list.
@ -141,11 +141,18 @@ impl ListElem {
impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li)
.with_body(Some(item.body.clone()))
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
@ -158,7 +165,7 @@ impl Show for Packed<ListElem> {
.pack()
.spanned(self.span());
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing =
VElem::new(leading.into()).with_weak(true).with_attach(true).pack();

View File

@ -40,19 +40,11 @@ pub use self::strong::*;
pub use self::table::*;
pub use self::terms::*;
use crate::foundations::{category, Category, Scope};
/// Document structuring.
///
/// Here, you can find functions to structure your document and interact with
/// that structure. This includes section headings, figures, bibliography
/// management, cross-referencing and more.
#[category]
pub static MODEL: Category;
use crate::foundations::Scope;
/// Hook up all `model` definitions.
pub fn define(global: &mut Scope) {
global.category(MODEL);
global.start_category(crate::Category::Model);
global.define_elem::<DocumentElem>();
global.define_elem::<RefElem>();
global.define_elem::<LinkElem>();
@ -72,4 +64,5 @@ pub fn define(global: &mut Scope) {
global.define_elem::<EmphElem>();
global.define_elem::<StrongElem>();
global.define_func::<numbering>();
global.reset_category();
}

View File

@ -1,50 +1,61 @@
use std::num::NonZeroUsize;
use std::str::FromStr;
use comemo::Track;
use comemo::{Track, Tracked};
use smallvec::SmallVec;
use typst_syntax::Span;
use typst_utils::NonZeroExt;
use typst_utils::{Get, NonZeroExt};
use crate::diag::{bail, At, SourceResult};
use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, select_where, Content, Context, Func, LocatableSelector,
NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles,
};
use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
};
use crate::introspection::{Counter, CounterKey, Locatable};
use crate::layout::{
BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing,
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides,
};
use crate::model::{
Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable,
};
use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
use crate::text::{LocalName, SpaceElem, TextElem};
/// A table of contents, figures, or other elements.
///
/// This function generates a list of all occurrences of an element in the
/// document, up to a given depth. The element's numbering and page number will
/// be displayed in the outline alongside its title or caption. By default this
/// generates a table of contents.
/// document, up to a given [`depth`]($outline.depth). The element's numbering
/// and page number will be displayed in the outline alongside its title or
/// caption.
///
/// # Example
/// ```example
/// #set heading(numbering: "1.")
/// #outline()
///
/// = Introduction
/// #lorem(5)
///
/// = Prior work
/// = Methods
/// == Setup
/// #lorem(10)
/// ```
///
/// # Alternative outlines
/// In its default configuration, this function generates a table of contents.
/// By setting the `target` parameter, the outline can be used to generate a
/// list of other kinds of elements than headings. In the example below, we list
/// all figures containing images by setting `target` to `{figure.where(kind:
/// image)}`. We could have also set it to just `figure`, but then the list
/// would also include figures containing tables or other material. For more
/// details on the `where` selector, [see here]($function.where).
/// list of other kinds of elements than headings.
///
/// In the example below, we list all figures containing images by setting
/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set
/// it to `{figure.where(kind: table)}` to generate a list of tables.
///
/// We could also set it to just `figure`, without using a [`where`]($function.where)
/// selector, but then the list would contain _all_ figures, be it ones
/// containing images, tables, or other material.
///
/// ```example
/// #outline(
@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem};
/// ```
///
/// # Styling the outline
/// The outline element has several options for customization, such as its
/// `title` and `indent` parameters. If desired, however, it is possible to have
/// more control over the outline's look and style through the
/// [`outline.entry`]($outline.entry) element.
#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)]
/// At the most basic level, you can style the outline by setting properties on
/// it and its entries. This way, you can customize the outline's
/// [title]($outline.title), how outline entries are
/// [indented]($outline.indent), and how the space between an entry's text and
/// its page number should be [filled]($outline.entry.fill).
///
/// Richer customization is possible through configuration of the outline's
/// [entries]($outline.entry). The outline generates one entry for each outlined
/// element.
///
/// ## Spacing the entries { #entry-spacing }
/// Outline entries are [blocks]($block), so you can adjust the spacing between
/// them with normal block-spacing rules:
///
/// ```example
/// #show outline.entry.where(
/// level: 1
/// ): set block(above: 1.2em)
///
/// #outline()
///
/// = About ACME Corp.
/// == History
/// === Origins
/// = Products
/// == ACME Tools
/// ```
///
/// ## Building an outline entry from its parts { #building-an-entry }
/// For full control, you can also write a transformational show rule on
/// `outline.entry`. However, the logic for properly formatting and indenting
/// outline entries is quite complex and the outline entry itself only contains
/// two fields: The level and the outlined element.
///
/// For this reason, various helper functions are provided. You can mix and
/// match these to compose an entry from just the parts you like.
///
/// The default show rule for an outline entry looks like this[^1]:
/// ```typ
/// #show outline.entry: it => link(
/// it.element.location(),
/// it.indented(it.prefix(), it.inner()),
/// )
/// ```
///
/// - The [`indented`]($outline.entry.indented) function takes an optional
/// prefix and inner content and automatically applies the proper indentation
/// to it, such that different entries align nicely and long headings wrap
/// properly.
///
/// - The [`prefix`]($outline.entry.prefix) function formats the element's
/// numbering (if any). It also appends a supplement for certain elements.
///
/// - The [`inner`]($outline.entry.inner) function combines the element's
/// [`body`]($outline.entry.body), the filler, and the
/// [`page` number]($outline.entry.page).
///
/// You can use these individual functions to format the outline entry in
/// different ways. Let's say, you'd like to fully remove the filler and page
/// numbers. To achieve this, you could write a show rule like this:
///
/// ```example
/// #show outline.entry: it => link(
/// it.element.location(),
/// // Keep just the body, dropping
/// // the fill and the page.
/// it.indented(it.prefix(), it.body()),
/// )
///
/// #outline()
///
/// = About ACME Corp.
/// == History
/// ```
///
/// [^1]: The outline of equations is the exception to this rule as it does not
/// have a body and thus does not use indented layout.
#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)]
pub struct OutlineElem {
/// The title of the outline.
///
/// - When set to `{auto}`, an appropriate title for the
/// [text language]($text.lang) will be used. This is the default.
/// [text language]($text.lang) will be used.
/// - When set to `{none}`, the outline will not have a title.
/// - A custom title can be set by passing content.
///
@ -79,8 +163,10 @@ pub struct OutlineElem {
/// The type of element to include in the outline.
///
/// To list figures containing a specific kind of element, like a table, you
/// can write `{figure.where(kind: table)}`.
/// To list figures containing a specific kind of element, like an image or
/// a table, you can specify the desired kind in a [`where`]($function.where)
/// selector. See the section on [alternative outlines]($outline/#alternative-outlines)
/// for more details.
///
/// ```example
/// #outline(
@ -97,7 +183,7 @@ pub struct OutlineElem {
/// caption: [Experiment results],
/// )
/// ```
#[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))]
#[default(LocatableSelector(HeadingElem::elem().select()))]
#[borrowed]
pub target: LocatableSelector,
@ -121,21 +207,22 @@ pub struct OutlineElem {
/// How to indent the outline's entries.
///
/// - `{none}`: No indent
/// - `{auto}`: Indents the numbering of the nested entry with the title of
/// its parent entry. This only has an effect if the entries are numbered
/// (e.g., via [heading numbering]($heading.numbering)).
/// - [Relative length]($relative): Indents the item by this length
/// multiplied by its nesting level. Specifying `{2em}`, for instance,
/// would indent top-level headings (not nested) by `{0em}`, second level
/// - `{auto}`: Indents the numbering/prefix of a nested entry with the
/// title of its parent entry. If the entries are not numbered (e.g., via
/// [heading numbering]($heading.numbering)), this instead simply inserts
/// a fixed amount of `{1.2em}` indent per level.
///
/// - [Relative length]($relative): Indents the entry by the specified
/// length per nesting level. Specifying `{2em}`, for instance, would
/// indent top-level headings by `{0em}` (not nested), second level
/// headings by `{2em}` (nested once), third-level headings by `{4em}`
/// (nested twice) and so on.
/// - [Function]($function): You can completely customize this setting with
/// a function. That function receives the nesting level as a parameter
/// (starting at 0 for top-level headings/elements) and can return a
/// relative length or content making up the indent. For example,
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while
/// `{n => [→ ] * n}` would indent with one arrow per nesting level.
///
/// - [Function]($function): You can further customize this setting with a
/// function. That function receives the nesting level as a parameter
/// (starting at 0 for top-level headings/elements) and should return a
/// (relative) length. For example, `{n => n * 2em}` would be equivalent
/// to just specifying `{2em}`.
///
/// ```example
/// #set heading(numbering: "1.a.")
@ -150,11 +237,6 @@ pub struct OutlineElem {
/// indent: 2em,
/// )
///
/// #outline(
/// title: [Contents (Function)],
/// indent: n => [→ ] * n,
/// )
///
/// = About ACME Corp.
/// == History
/// === Origins
@ -163,20 +245,7 @@ pub struct OutlineElem {
/// == Products
/// #lorem(10)
/// ```
#[default(None)]
#[borrowed]
pub indent: Option<Smart<OutlineIndent>>,
/// Content to fill the space between the title and the page number. Can be
/// set to `{none}` to disable filling.
///
/// ```example
/// #outline(fill: line(length: 100%))
///
/// = A New Beginning
/// ```
#[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
pub fill: Option<Content>,
pub indent: Smart<OutlineIndent>,
}
#[scope]
@ -188,79 +257,51 @@ impl OutlineElem {
impl Show for Packed<OutlineElem> {
#[typst_macros::time(name = "outline", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![ParbreakElem::shared().clone()];
let span = self.span();
// Build the outline title.
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span()))
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(self.span()),
.spanned(span),
);
}
let indent = self.indent(styles);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
let mut ancestors: Vec<&Content> = vec![];
let elems = engine.introspector.query(&self.target(styles).0);
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
for elem in &elems {
let Some(entry) = OutlineEntry::from_outlinable(
engine,
self.span(),
elem.clone(),
self.fill(styles),
styles,
)?
else {
continue;
// Build the outline entries.
for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
if depth < entry.level {
continue;
let level = outlinable.level();
if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span));
}
// Deals with the ancestors of the current element.
// This is only applicable for elements with a hierarchy/level.
while ancestors
.last()
.and_then(|ancestor| ancestor.with::<dyn Outlinable>())
.is_some_and(|last| last.level() >= entry.level)
{
ancestors.pop();
}
OutlineIndent::apply(
indent,
engine,
&ancestors,
&mut seq,
styles,
self.span(),
)?;
// Add the overridable outline entry, followed by a line break.
seq.push(entry.pack().spanned(self.span()));
seq.push(LinebreakElem::shared().clone());
ancestors.push(elem);
}
seq.push(ParbreakElem::shared().clone());
Ok(Content::sequence(seq))
}
}
impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, _: StyleChain) -> Styles {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(HeadingElem::set_outlined(false));
out.set(HeadingElem::set_numbering(None));
out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
out.set(ParElem::set_justify(false));
out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into())));
// Makes the outline itself available to its entries. Should be
// superseded by a proper ancestry mechanism in the future.
out.set(OutlineEntry::set_parent(Some(self.clone())));
out
}
}
@ -269,93 +310,29 @@ impl LocalName for Packed<OutlineElem> {
const KEY: &'static str = "outline";
}
/// Marks an element as being able to be outlined. This is used to implement the
/// `#outline()` element.
pub trait Outlinable: Refable {
/// Produce an outline item for this element.
fn outline(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Option<Content>>;
/// Returns the nesting level of this element.
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
}
/// Defines how an outline is indented.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum OutlineIndent {
Rel(Rel<Length>),
/// Indents by the specified length per level.
Rel(Rel),
/// Resolve the indent for a specific level through the given function.
Func(Func),
}
impl OutlineIndent {
fn apply(
indent: &Option<Smart<Self>>,
/// Resolve the indent for an entry with the given level.
fn resolve(
&self,
engine: &mut Engine,
ancestors: &Vec<&Content>,
seq: &mut Vec<Content>,
styles: StyleChain,
context: Tracked<Context>,
level: NonZeroUsize,
span: Span,
) -> SourceResult<()> {
match indent {
// 'none' | 'false' => no indenting
None => {}
// 'auto' | 'true' => use numbering alignment for indenting
Some(Smart::Auto) => {
// Add hidden ancestors numberings to realize the indent.
let mut hidden = Content::empty();
for ancestor in ancestors {
let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
if let Some(numbering) = ancestor_outlinable.numbering() {
let numbers = ancestor_outlinable.counter().display_at_loc(
engine,
ancestor.location().unwrap(),
styles,
numbering,
)?;
hidden += numbers + SpaceElem::shared().clone();
};
}
if !ancestors.is_empty() {
seq.push(HideElem::new(hidden).pack().spanned(span));
seq.push(SpaceElem::shared().clone().spanned(span));
}
}
// Length => indent with some fixed spacing per level
Some(Smart::Custom(OutlineIndent::Rel(length))) => {
seq.push(
HElem::new(Spacing::Rel(*length))
.pack()
.spanned(span)
.repeat(ancestors.len()),
);
}
// Function => call function with the current depth and take
// the returned content
Some(Smart::Custom(OutlineIndent::Func(func))) => {
let depth = ancestors.len();
let LengthOrContent(content) = func
.call(engine, Context::new(None, Some(styles)).track(), [depth])?
.cast()
.at(span)?;
if !content.is_empty() {
seq.push(content);
}
}
};
Ok(())
) -> SourceResult<Rel> {
let depth = level.get() - 1;
match self {
Self::Rel(length) => Ok(*length * depth as f64),
Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span),
}
}
}
@ -365,46 +342,33 @@ cast! {
Self::Rel(v) => v.into_value(),
Self::Func(v) => v.into_value()
},
v: Rel<Length> => OutlineIndent::Rel(v),
v: Func => OutlineIndent::Func(v),
v: Rel<Length> => Self::Rel(v),
v: Func => Self::Func(v),
}
struct LengthOrContent(Content);
/// Marks an element as being able to be outlined.
pub trait Outlinable: Refable {
/// Whether this element should be included in the outline.
fn outlined(&self) -> bool;
cast! {
LengthOrContent,
v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
v: Content => Self(v),
/// The nesting level of this element.
fn level(&self) -> NonZeroUsize {
NonZeroUsize::ONE
}
/// Constructs the default prefix given the formatted numbering.
fn prefix(&self, numbers: Content) -> Content;
/// The body of the entry.
fn body(&self) -> Content;
}
/// Represents each entry line in an outline, including the reference to the
/// outlined element, its page number, and the filler content between both.
/// Represents an entry line in an outline.
///
/// This element is intended for use with show rules to control the appearance
/// of outlines. To customize an entry's line, you can build it from scratch by
/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the
/// entry.
///
/// ```example
/// #set heading(numbering: "1.")
///
/// #show outline.entry.where(
/// level: 1
/// ): it => {
/// v(12pt, weak: true)
/// strong(it)
/// }
///
/// #outline(indent: auto)
///
/// = Introduction
/// = Background
/// == History
/// == State of the Art
/// = Analysis
/// == Setup
/// ```
#[elem(name = "entry", title = "Outline Entry", Show)]
/// With show-set and show rules on outline entries, you can richly customize
/// the outline's appearance. See the
/// [section on styling the outline]($outline/#styling-the-outline) for details.
#[elem(scope, name = "entry", title = "Outline Entry", Show)]
pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries.
@ -412,90 +376,206 @@ pub struct OutlineEntry {
pub level: NonZeroUsize,
/// The element this entry refers to. Its location will be available
/// through the [`location`]($content.location) method on content
/// through the [`location`]($content.location) method on the content
/// and can be [linked]($link) to.
#[required]
pub element: Content,
/// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this would be its number followed
/// by the heading's title, for example.
#[required]
pub body: Content,
/// The content used to fill the space between the element's outline and
/// its page number, as defined by the outline element this entry is
/// located in. When `{none}`, empty space is inserted in that gap instead.
/// Content to fill the space between the title and the page number. Can be
/// set to `{none}` to disable filling.
///
/// Note that, when using show rules to override outline entries, it is
/// recommended to wrap the filling content in a [`box`] with fractional
/// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely
/// as many `-` characters as necessary to fill a particular gap.
#[required]
/// The `fill` will be placed into a fractionally sized box that spans the
/// space between the entry's body and the page number. When using show
/// rules to override outline entries, it is thus recommended to wrap the
/// fill in a [`box`] with fractional width, i.e.
/// `{box(width: 1fr, it.fill}`.
///
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
/// to tweak the visual weight of the fill.
///
/// ```example
/// #set outline.entry(fill: line(length: 100%))
/// #outline()
///
/// = A New Beginning
/// ```
#[borrowed]
#[default(Some(
RepeatElem::new(TextElem::packed("."))
.with_gap(Em::new(0.15).into())
.pack()
))]
pub fill: Option<Content>,
/// The page number of the element this entry links to, formatted with the
/// numbering set for the referenced page.
#[required]
pub page: Content,
}
impl OutlineEntry {
/// Generates an OutlineEntry from the given element, if possible (errors if
/// the element does not implement `Outlinable`). If the element should not
/// be outlined (e.g. heading with 'outlined: false'), does not generate an
/// entry instance (returns `Ok(None)`).
fn from_outlinable(
engine: &mut Engine,
span: Span,
elem: Content,
fill: Option<Content>,
styles: StyleChain,
) -> SourceResult<Option<Self>> {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let Some(body) = outlinable.outline(engine, styles)? else {
return Ok(None);
};
let location = elem.location().unwrap();
let page_numbering = engine
.introspector
.page_numbering(location)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
let page = Counter::new(CounterKey::Page).display_at_loc(
engine,
location,
styles,
&page_numbering,
)?;
Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
}
/// Lets outline entries access the outline they are part of. This is a bit
/// of a hack and should be superseded by a proper ancestry mechanism.
#[ghost]
#[internal]
pub parent: Option<Packed<OutlineElem>>,
}
impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![];
let elem = &self.element;
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let context = Context::new(None, Some(styles));
let context = context.track();
// In case a user constructs an outline entry with an arbitrary element.
let Some(location) = elem.location() else {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
bail!(
self.span(), "{} must have a location", elem.func().name();
hint: "try using a query or a show rule to customize the outline.entry instead",
)
} else {
bail!(self.span(), "cannot outline {}", elem.func().name())
let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?;
let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
}
}
#[scope]
impl OutlineEntry {
/// A helper function for producing an indented entry layout: Lays out a
/// prefix and the rest of the entry in an indent-aware way.
///
/// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the
/// inner content of all entries at level `N` is aligned with the prefix of
/// all entries at level `N + 1`, leaving at least `gap` space between the
/// prefix and inner parts. Furthermore, the `inner` contents of all entries
/// at the same level are aligned.
///
/// If the outline's indent is a fixed value or a function, the prefixes are
/// indented, but the inner contents are simply inset from the prefix by the
/// specified `gap`, rather than aligning outline-wide.
#[func(contextual)]
pub fn indented(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
/// The `prefix` is aligned with the `inner` content of entries that
/// have level one less.
///
/// In the default show rule, this is just `it.prefix()`, but it can be
/// freely customized.
prefix: Option<Content>,
/// The formatted inner content of the entry.
///
/// In the default show rule, this is just `it.inner()`, but it can be
/// freely customized.
inner: Content,
/// The gap between the prefix and the inner content.
#[named]
#[default(Em::new(0.5).into())]
gap: Length,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let outline = Self::parent_in(styles)
.ok_or("must be called within the context of an outline")
.at(span)?;
let outline_loc = outline.location().unwrap();
let prefix_width = prefix
.as_ref()
.map(|prefix| measure_prefix(engine, prefix, outline_loc, styles))
.transpose()?;
let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles));
let indent = outline.indent(styles);
let (base_indent, hanging_indent) = match &indent {
Smart::Auto => compute_auto_indents(
engine.introspector,
outline_loc,
styles,
self.level,
prefix_inset,
),
Smart::Custom(amount) => {
let base = amount.resolve(engine, context, self.level, span)?;
(base, prefix_inset)
}
};
let body = if let (
Some(prefix),
Some(prefix_width),
Some(prefix_inset),
Some(hanging_indent),
) = (prefix, prefix_width, prefix_inset, hanging_indent)
{
// Save information about our prefix that other outline entries
// can query for (within `compute_auto_indent`) to align
// themselves).
let mut seq = Vec::with_capacity(5);
if indent.is_auto() {
seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack());
}
// Dedent the prefix by the amount of hanging indent and then skip
// ahead so that the inner contents are aligned.
seq.extend([
HElem::new((-hanging_indent).into()).pack(),
prefix,
HElem::new((hanging_indent - prefix_width).into()).pack(),
inner,
]);
Content::sequence(seq)
} else {
inner
};
let inset = Sides::default().with(
TextElem::dir_in(styles).start(),
Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())),
);
Ok(BlockElem::new()
.with_inset(inset)
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span))
}
/// Formats the element's numbering (if any).
///
/// This also appends the element's supplement in case of figures or
/// equations. For instance, it would output `1.1` for a heading, but
/// `Figure 1` for a figure, as is usual for outlines.
#[func(contextual)]
pub fn prefix(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Option<Content>> {
let outlinable = self.outlinable().at(span)?;
let Some(numbering) = outlinable.numbering() else { return Ok(None) };
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbers =
outlinable.counter().display_at_loc(engine, loc, styles, numbering)?;
Ok(Some(outlinable.prefix(numbers)))
}
/// Creates the default inner content of the entry.
///
/// This includes the body, the fill, and page number.
#[func(contextual)]
pub fn inner(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let styles = context.styles().at(span)?;
let mut seq = vec![];
// Isolate the entry body in RTL because the page number is typically
// LTR. I'm not sure whether LTR should conceptually also be isolated,
// but in any case we don't do it for now because the text shaping
@ -511,32 +591,174 @@ impl Show for Packed<OutlineEntry> {
seq.push(TextElem::packed("\u{202B}"));
}
seq.push(self.body.clone().linked(Destination::Location(location)));
seq.push(self.body().at(span)?);
if rtl {
// "Pop Directional Formatting"
seq.push(TextElem::packed("\u{202C}"));
}
// Add filler symbols between the section name and page number.
if let Some(filler) = &self.fill {
// Add the filler between the section name and page number.
if let Some(filler) = self.fill(styles) {
seq.push(SpaceElem::shared().clone());
seq.push(
BoxElem::new()
.with_body(Some(filler.clone()))
.with_width(Fr::one().into())
.pack()
.spanned(self.span()),
.spanned(span),
);
seq.push(SpaceElem::shared().clone());
} else {
seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span()));
seq.push(HElem::new(Fr::one().into()).pack().spanned(span));
}
// Add the page number.
let page = self.page.clone().linked(Destination::Location(location));
seq.push(page);
// Add the page number. The word joiner in front ensures that the page
// number doesn't stand alone in its line.
seq.push(TextElem::packed("\u{2060}"));
seq.push(self.page(engine, context, span)?);
Ok(Content::sequence(seq))
}
/// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this is its
/// [`body`]($heading.body); for a figure a caption and for equations, it is
/// empty.
#[func]
pub fn body(&self) -> StrResult<Content> {
Ok(self.outlinable()?.body())
}
/// The page number of this entry's element, formatted with the numbering
/// set for the referenced page.
#[func(contextual)]
pub fn page(
&self,
engine: &mut Engine,
context: Tracked<Context>,
span: Span,
) -> SourceResult<Content> {
let loc = self.element_location().at(span)?;
let styles = context.styles().at(span)?;
let numbering = engine
.introspector
.page_numbering(loc)
.cloned()
.unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into());
Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering)
}
}
impl OutlineEntry {
fn outlinable(&self) -> StrResult<&dyn Outlinable> {
self.element
.with::<dyn Outlinable>()
.ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
}
fn element_location(&self) -> HintedStrResult<Location> {
let elem = &self.element;
elem.location().ok_or_else(|| {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
error!(
"{} must have a location", elem.func().name();
hint: "try using a show rule to customize the outline.entry instead",
)
} else {
error!("cannot outline {}", elem.func().name())
}
})
}
}
cast! {
OutlineEntry,
v: Content => v.unpack::<Self>().map_err(|_| "expected outline entry")?
}
/// Measures the width of a prefix.
fn measure_prefix(
engine: &mut Engine,
prefix: &Content,
loc: Location,
styles: StyleChain,
) -> SourceResult<Abs> {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
let link = LocatorLink::measure(loc);
Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)?
.width())
}
/// Compute the base indent and hanging indent for an auto-indented outline
/// entry of the given level, with the given prefix inset.
fn compute_auto_indents(
introspector: Tracked<Introspector>,
outline_loc: Location,
styles: StyleChain,
level: NonZeroUsize,
prefix_inset: Option<Abs>,
) -> (Rel, Option<Abs>) {
let indents = query_prefix_widths(introspector, outline_loc);
let fallback = Em::new(1.2).resolve(styles);
let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback);
let last = level.get() - 1;
let base: Abs = (0..last).map(get).sum();
let hang = prefix_inset.map(|p| p.max(get(last)));
(base.into(), hang)
}
/// Determines the maximum prefix inset (prefix width + gap) at each outline
/// level, for the outline with the given `loc`. Levels for which there is no
/// information available yield `None`.
#[comemo::memoize]
fn query_prefix_widths(
introspector: Tracked<Introspector>,
outline_loc: Location,
) -> SmallVec<[Option<Abs>; 4]> {
let mut widths = SmallVec::<[Option<Abs>; 4]>::new();
let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc));
for elem in &elems {
let info = elem.to_packed::<PrefixInfo>().unwrap();
let level = info.level.get();
if widths.len() < level {
widths.resize(level, None);
}
widths[level - 1].get_or_insert(info.inset).set_max(info.inset);
}
widths
}
/// Helper type for introspection-based prefix alignment.
#[elem(Construct, Locatable, Show)]
struct PrefixInfo {
/// The location of the outline this prefix is part of. This is used to
/// scope prefix computations to a specific outline.
#[required]
key: Location,
/// The level of this prefix's entry.
#[required]
#[internal]
level: NonZeroUsize,
/// The width of the prefix, including the gap.
#[required]
#[internal]
inset: Abs,
}
impl Construct for PrefixInfo {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
impl Show for Packed<PrefixInfo> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -1,22 +1,78 @@
use std::fmt::{self, Debug, Formatter};
use typst_utils::singleton;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart,
StyleVec, Unlabellable,
cast, dict, elem, scope, Args, Cast, Construct, Content, Dict, NativeElement, Packed,
Smart, Unlabellable, Value,
};
use crate::introspection::{Count, CounterUpdate, Locatable};
use crate::layout::{Em, HAlignment, Length, OuterHAlignment};
use crate::model::Numbering;
/// Arranges text, spacing and inline-level elements into a paragraph.
/// A logical subdivison of textual content.
///
/// Although this function is primarily used in set rules to affect paragraph
/// properties, it can also be used to explicitly render its argument onto a
/// paragraph of its own.
/// Typst automatically collects _inline-level_ elements into paragraphs.
/// Inline-level elements include [text], [horizontal spacing]($h),
/// [boxes]($box), and [inline equations]($math.equation).
///
/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]).
/// Paragraphs are also automatically interrupted by any block-level element
/// (like [`block`], [`place`], or anything that shows itself as one of these).
///
/// The `par` element is primarily used in set rules to affect paragraph
/// properties, but it can also be used to explicitly display its argument as a
/// paragraph of its own. Then, the paragraph's body may not contain any
/// block-level content.
///
/// # Boxes and blocks
/// As explained above, usually paragraphs only contain inline-level content.
/// However, you can integrate any kind of block-level content into a paragraph
/// by wrapping it in a [`box`].
///
/// Conversely, you can separate inline-level content from a paragraph by
/// wrapping it in a [`block`]. In this case, it will not become part of any
/// paragraph at all. Read the following section for an explanation of why that
/// matters and how it differs from just adding paragraph breaks around the
/// content.
///
/// # What becomes a paragraph?
/// When you add inline-level content to your document, Typst will automatically
/// wrap it in paragraphs. However, a typical document also contains some text
/// that is not semantically part of a paragraph, for example in a heading or
/// caption.
///
/// The rules for when Typst wraps inline-level content in a paragraph are as
/// follows:
///
/// - All text at the root of a document is wrapped in paragraphs.
///
/// - Text in a container (like a `block`) is only wrapped in a paragraph if the
/// container holds any block-level content. If all of the contents are
/// inline-level, no paragraph is created.
///
/// In the laid-out document, it's not immediately visible whether text became
/// part of a paragraph. However, it is still important for various reasons:
///
/// - Certain paragraph styling like `first-line-indent` will only apply to
/// proper paragraphs, not any text. Similarly, `par` show rules of course
/// only trigger on paragraphs.
///
/// - A proper distinction between paragraphs and other text helps people who
/// rely on assistive technologies (such as screen readers) navigate and
/// understand the document properly. Currently, this only applies to HTML
/// export since Typst does not yet output accessible PDFs, but support for
/// this is planned for the near future.
///
/// - HTML export will generate a `<p>` tag only for paragraphs.
///
/// When creating custom reusable components, you can and should take charge
/// over whether Typst creates paragraphs. By wrapping text in a [`block`]
/// instead of just adding paragraph breaks around it, you can force the absence
/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a
/// container, you can force it to become a paragraph even if it's just one
/// word. This is, for example, what [non-`tight`]($list.tight) lists do to
/// force their items to become paragraphs.
///
/// # Example
/// ```example
@ -37,7 +93,7 @@ use crate::model::Numbering;
/// let $a$ be the smallest of the
/// three integers. Then, we ...
/// ```
#[elem(scope, title = "Paragraph", Debug, Construct)]
#[elem(scope, title = "Paragraph")]
pub struct ParElem {
/// The spacing between lines.
///
@ -53,7 +109,6 @@ pub struct ParElem {
/// distribution of the top- and bottom-edge values affects the bounds of
/// the first and last line.
#[resolve]
#[ghost]
#[default(Em::new(0.65).into())]
pub leading: Length,
@ -68,7 +123,6 @@ pub struct ParElem {
/// takes precedence over the paragraph spacing. Headings, for instance,
/// reduce the spacing below them by default for a better look.
#[resolve]
#[ghost]
#[default(Em::new(1.2).into())]
pub spacing: Length,
@ -81,7 +135,6 @@ pub struct ParElem {
/// Note that the current [alignment]($align.alignment) still has an effect
/// on the placement of the last line except if it ends with a
/// [justified line break]($linebreak.justify).
#[ghost]
#[default(false)]
pub justify: bool,
@ -106,35 +159,66 @@ pub struct ParElem {
/// challenging to break in a visually
/// pleasing way.
/// ```
#[ghost]
pub linebreaks: Smart<Linebreaks>,
/// The indent the first line of a paragraph should have.
///
/// Only the first line of a consecutive paragraph will be indented (not
/// the first one in a block or on the page).
/// By default, only the first line of a consecutive paragraph will be
/// indented (not the first one in the document or container, and not
/// paragraphs immediately following other block-level elements).
///
/// If you want to indent all paragraphs instead, you can pass a dictionary
/// containing the `amount` of indent as a length and the pair
/// `{all: true}`. When `all` is omitted from the dictionary, it defaults to
/// `{false}`.
///
/// By typographic convention, paragraph breaks are indicated either by some
/// space between paragraphs or by indented first lines. Consider reducing
/// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading)
/// when using this property (e.g. using `[#set par(spacing: 0.65em)]`).
#[ghost]
pub first_line_indent: Length,
/// space between paragraphs or by indented first lines. Consider
/// - reducing the [paragraph `spacing`]($par.spacing) to the
/// [`leading`]($par.leading) using `{set par(spacing: 0.65em)}`
/// - increasing the [block `spacing`]($block.spacing) (which inherits the
/// paragraph spacing by default) to the original paragraph spacing using
/// `{set block(spacing: 1.2em)}`
///
/// ```example
/// #set block(spacing: 1.2em)
/// #set par(
/// first-line-indent: 1.5em,
/// spacing: 0.65em,
/// )
///
/// The first paragraph is not affected
/// by the indent.
///
/// But the second paragraph is.
///
/// #line(length: 100%)
///
/// #set par(first-line-indent: (
/// amount: 1.5em,
/// all: true,
/// ))
///
/// Now all paragraphs are affected
/// by the first line indent.
///
/// Even the first one.
/// ```
pub first_line_indent: FirstLineIndent,
/// The indent all but the first line of a paragraph should have.
#[ghost]
/// The indent that all but the first line of a paragraph should have.
///
/// ```example
/// #set par(hanging-indent: 1em)
///
/// #lorem(15)
/// ```
#[resolve]
pub hanging_indent: Length,
/// The contents of the paragraph.
#[external]
#[required]
pub body: Content,
/// The paragraph's children.
#[internal]
#[variadic]
pub children: StyleVec,
}
#[scope]
@ -143,28 +227,6 @@ impl ParElem {
type ParLine;
}
impl Construct for ParElem {
fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph
// element. Instead, it just ensures that the passed content lives in a
// separate paragraph and styles it.
let styles = Self::set(engine, args)?;
let body = args.expect::<Content>("body")?;
Ok(Content::sequence([
ParbreakElem::shared().clone(),
body.styled_with_map(styles),
ParbreakElem::shared().clone(),
]))
}
}
impl Debug for ParElem {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Par ")?;
self.children.fmt(f)
}
}
/// How to determine line breaks in a paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Linebreaks {
@ -177,6 +239,36 @@ pub enum Linebreaks {
Optimized,
}
/// Configuration for first line indent.
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)]
pub struct FirstLineIndent {
/// The amount of indent.
pub amount: Length,
/// Whether to indent all paragraphs, not just consecutive ones.
pub all: bool,
}
cast! {
FirstLineIndent,
self => Value::Dict(self.into()),
amount: Length => Self { amount, all: false },
mut dict: Dict => {
let amount = dict.take("amount")?.cast()?;
let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false);
dict.finish(&["amount", "all"])?;
Self { amount, all }
},
}
impl From<FirstLineIndent> for Dict {
fn from(indent: FirstLineIndent) -> Self {
dict! {
"amount" => indent.amount,
"all" => indent.all,
}
}
}
/// A paragraph break.
///
/// This starts a new paragraph. Especially useful when used within code like

View File

@ -2,13 +2,14 @@ use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles,
StyleChain, Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{
Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
};
use crate::model::{CitationForm, CiteElem};
use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget};
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
/// Displays a quote alongside an optional attribution.
@ -158,6 +159,7 @@ impl Show for Packed<QuoteElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body.clone();
let block = self.block(styles);
let html = TargetElem::target_in(styles).is_html();
if self.quotes(styles) == Smart::Custom(true) || !block {
let quotes = SmartQuotes::get(
@ -171,50 +173,69 @@ impl Show for Packed<QuoteElem> {
let Depth(depth) = QuoteElem::depth_in(styles);
let double = depth % 2 == 0;
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
if !html {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
realized = Content::sequence([hole.clone(), realized, hole]);
}
realized = Content::sequence([
TextElem::packed(quotes.open(double)),
hole.clone(),
realized,
hole,
TextElem::packed(quotes.close(double)),
])
.styled(QuoteElem::set_depth(Depth(1)));
}
let attribution = self.attribution(styles);
if block {
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span());
if let Some(attribution) = self.attribution(styles).as_ref() {
let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()];
match attribution {
Attribution::Content(content) => {
seq.push(content.clone());
}
Attribution::Label(label) => {
seq.push(
CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(self.span()),
);
realized = if html {
let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized));
if let Some(Attribution::Content(attribution)) = attribution {
if let Some(link) = attribution.to_packed::<LinkElem>() {
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
elem = elem.with_attr(attr::cite, url.clone().into_inner());
}
}
}
elem.pack()
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack()
}
.spanned(self.span());
// Use v(0.9em, weak: true) bring the attribution closer to the
// quote.
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v + Content::sequence(seq).aligned(Alignment::END);
if let Some(attribution) = attribution.as_ref() {
let attribution = match attribution {
Attribution::Content(content) => content.clone(),
Attribution::Label(label) => CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(self.span()),
};
let attribution = Content::sequence([
TextElem::packed('—'),
SpaceElem::shared().clone(),
attribution,
]);
if html {
realized += attribution;
} else {
// Bring the attribution a bit closer to the quote.
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v;
realized += BlockElem::new()
.with_body(Some(BlockBody::Content(attribution)))
.pack()
.aligned(Alignment::END);
}
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = self.attribution(styles) {
if !html {
realized = PadElem::new(realized).pack();
}
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone()
+ CiteElem::new(*label).pack().spanned(self.span());
}

View File

@ -7,7 +7,11 @@ use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
TargetElem,
};
use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use crate::introspection::Locator;
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use crate::layout::{
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides,
@ -258,11 +262,65 @@ impl TableElem {
type TableFooter;
}
fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
let cell = cell.body.clone();
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
let mut attrs = HtmlAttrs::default();
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
if let Some(colspan) = span(cell.colspan(styles)) {
attrs.push(attr::colspan, colspan);
}
if let Some(rowspan) = span(cell.rowspan(styles)) {
attrs.push(attr::rowspan, rowspan);
}
HtmlElem::new(tag)
.with_body(Some(cell.body.clone()))
.with_attrs(attrs)
.pack()
.spanned(cell.span())
}
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
let tr = |tag, row: &[Entry]| {
let row = row
.iter()
.flat_map(|entry| entry.as_cell())
.map(|cell| show_cell_html(tag, cell, styles));
elem(tag::tr, Content::sequence(row))
};
let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.unwrap().start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
});
let header = grid.header.map(|hd| {
let rows = rows.drain(..hd.unwrap().end);
elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))
});
let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row)));
if header.is_some() || footer.is_some() {
body = elem(tag::tbody, body);
}
let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
elem(tag::table, Content::sequence(content))
}
impl Show for Packed<TableElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table)
.pack()
.spanned(self.span()))
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(if TargetElem::target_in(styles).is_html() {
// TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
// How can we find out whether locator is actually used?
let locator = Locator::root();
show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
} else {
BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
}
.spanned(self.span()))
}
}

View File

@ -1,4 +1,4 @@
use typst_utils::Numeric;
use typst_utils::{Get, Numeric};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
@ -7,8 +7,8 @@ use crate::foundations::{
Styles, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem};
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
use crate::text::TextElem;
/// A list of terms and their descriptions.
@ -105,6 +105,11 @@ pub struct TermsElem {
/// ```
#[variadic]
pub children: Vec<Packed<TermItem>>,
/// Whether we are currently within a term list.
#[internal]
#[ghost]
pub within: bool,
}
#[scope]
@ -116,17 +121,25 @@ impl TermsElem {
impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|item| {
// Text in wide term lists shall always turn into paragraphs.
let mut description = item.description.clone();
if !tight {
description += ParbreakElem::shared();
}
[
HtmlElem::new(tag::dt)
.with_body(Some(item.term.clone()))
.pack()
.spanned(item.term.span()),
HtmlElem::new(tag::dd)
.with_body(Some(item.description.clone()))
.with_body(Some(description))
.pack()
.spanned(item.description.span()),
]
@ -139,7 +152,7 @@ impl Show for Packed<TermsElem> {
let indent = self.indent(styles);
let hanging_indent = self.hanging_indent(styles);
let gutter = self.spacing(styles).unwrap_or_else(|| {
if self.tight(styles) {
if tight {
ParElem::leading_in(styles).into()
} else {
ParElem::spacing_in(styles).into()
@ -157,23 +170,25 @@ impl Show for Packed<TermsElem> {
seq.push(child.term.clone().strong());
seq.push((*separator).clone());
seq.push(child.description.clone());
// Text in wide term lists shall always turn into paragraphs.
if !tight {
seq.push(ParbreakElem::shared().clone());
}
children.push(StackChild::Block(Content::sequence(seq)));
}
let mut padding = Sides::default();
if TextElem::dir_in(styles) == Dir::LTR {
padding.left = pad.into();
} else {
padding.right = pad.into();
}
let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into());
let mut realized = StackElem::new(children)
.with_spacing(Some(gutter.into()))
.pack()
.spanned(span)
.padded(padding);
.padded(padding)
.styled(TermsElem::set_within(true));
if self.tight(styles) {
if tight {
let leading = ParElem::leading_in(styles);
let spacing = VElem::new(leading.into())
.with_weak(true)

View File

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

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