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