Merge branch 'main' into warn-suppression

This commit is contained in:
PgBiel 2024-07-15 11:55:30 -03:00 committed by GitHub
commit 5b05e2df15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
120 changed files with 3441 additions and 1672 deletions

View File

@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.74.0 - uses: dtolnay/rust-toolchain@1.77.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check --workspace - run: cargo check --workspace
@ -64,7 +64,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master - uses: dtolnay/rust-toolchain@master
with: with:
toolchain: nightly-2023-09-13 toolchain: nightly-2024-06-01
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo install --locked cargo-fuzz@0.12.0 - run: cargo install --locked cargo-fuzz@0.12.0
- run: cd tests/fuzz && cargo fuzz build --dev - run: cd tests/fuzz && cargo fuzz build --dev

861
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package] [workspace.package]
version = "0.11.0" version = "0.11.0"
rust-version = "1.74" # also change in ci.yml rust-version = "1.77" # also change in ci.yml
authors = ["The Typst Project Developers"] authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
homepage = "https://typst.app" homepage = "https://typst.app"
@ -26,8 +26,8 @@ typst-svg = { path = "crates/typst-svg", version = "0.11.0" }
typst-syntax = { path = "crates/typst-syntax", version = "0.11.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.11.0" }
typst-timing = { path = "crates/typst-timing", version = "0.11.0" } typst-timing = { path = "crates/typst-timing", version = "0.11.0" }
typst-utils = { path = "crates/typst-utils", version = "0.11.0" } typst-utils = { path = "crates/typst-utils", version = "0.11.0" }
typst-assets = "0.11.0" typst-assets = { git = "https://github.com/typst/typst-assets", rev = "4ee794c" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "48a924d9de82b631bc775124a69384c8d860db04" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "48a924d" }
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
bitflags = { version = "2", features = ["serde"] } bitflags = { version = "2", features = ["serde"] }
@ -49,7 +49,7 @@ flate2 = "1"
fontdb = { version = "0.18", default-features = false } fontdb = { version = "0.18", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"
hayagriva = "0.5.3" hayagriva = "0.5.3"
heck = "0.4" heck = "0.5"
hypher = "0.1.4" hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] } icu_properties = { version = "1.4", features = ["serde"] }
icu_provider = { version = "1.4", features = ["sync"] } icu_provider = { version = "1.4", features = ["sync"] }
@ -57,7 +57,7 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4" icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] } icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1" if_chain = "1"
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
kamadak-exif = "0.5" kamadak-exif = "0.5"
kurbo = "0.11" kurbo = "0.11"
@ -109,7 +109,7 @@ time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
tiny-skia = "0.11" tiny-skia = "0.11"
toml = { version = "0.8", default-features = false, features = ["parse", "display"] } toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
ttf-parser = "0.21.0" ttf-parser = "0.21.0"
two-face = { version = "0.3.0", default-features = false, features = ["syntect-fancy"] } two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] }
typed-arena = "2" typed-arena = "2"
unicode-bidi = "0.3.13" unicode-bidi = "0.3.13"
unicode-ident = "1.0" unicode-ident = "1.0"
@ -121,13 +121,13 @@ unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
usvg = { version = "0.42", default-features = false, features = ["text"] } usvg = { version = "0.42", default-features = false, features = ["text"] }
walkdir = "2" walkdir = "2"
wasmi = "0.31.0" wasmi = "0.34.0"
xmlparser = "0.13.5" xmlparser = "0.13.5"
xmlwriter = "0.1.0" xmlwriter = "0.1.0"
xmp-writer = "0.2" xmp-writer = "0.2"
xz2 = "0.1" xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1" yaml-front-matter = "0.1"
zip = { version = "0.6", default-features = false, features = ["deflate"] } zip = { version = "2", default-features = false, features = ["deflate"] }
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 2 opt-level = 2

View File

@ -85,3 +85,13 @@ vendor-openssl = ["openssl/vendored"]
[lints] [lints]
workspace = true workspace = true
# The following metadata is used by `cargo-binstall`, and should be synchronized
# with `.github/workflows/release.yml`.
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/typst-{ target }{ archive-suffix }"
bin-dir = "typst-{ target }/typst{ binary-ext }"
pkg-fmt = "txz"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-fmt = "zip"

View File

@ -5,7 +5,7 @@ use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::builder::ValueParser; use clap::builder::{TypedValueParser, ValueParser};
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum}; use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum};
use semver::Version; use semver::Version;
@ -77,7 +77,7 @@ pub struct CompileCommand {
/// must be present if the source document renders to multiple pages. Use `{p}` for page /// must be present if the source document renders to multiple pages. Use `{p}` for page
/// numbers, `{0p}` for zero padded page numbers and `{t}` for page count. For example, /// numbers, `{0p}` for zero padded page numbers and `{t}` for page count. For example,
/// `page-{0p}-of-{t}.png` creates `page-01-of-10.png`, `page-02-of-10.png` and so on. /// `page-{0p}-of-{t}.png` creates `page-01-of-10.png`, `page-02-of-10.png` and so on.
#[clap(required_if_eq("input", "-"), value_parser = ValueParser::new(output_value_parser))] #[clap(required_if_eq("input", "-"), value_parser = make_output_value_parser())]
pub output: Option<Output>, pub output: Option<Output>,
/// Which pages to export. When unspecified, all document pages are exported. /// Which pages to export. When unspecified, all document pages are exported.
@ -100,9 +100,11 @@ pub struct CompileCommand {
#[arg(long = "format", short = 'f')] #[arg(long = "format", short = 'f')]
pub format: Option<OutputFormat>, pub format: Option<OutputFormat>,
/// Opens the output file using the default viewer after compilation. /// Opens the output file with the default viewer or a specific program after
/// Ignored if output is stdout /// compilation
#[arg(long = "open")] ///
/// Ignored if output is stdout.
#[arg(long = "open", value_name = "VIEWER")]
pub open: Option<Option<String>>, pub open: Option<Option<String>>,
/// The PPI (pixels per inch) to use for PNG export /// The PPI (pixels per inch) to use for PNG export
@ -175,7 +177,7 @@ pub enum SerializationFormat {
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct SharedArgs { pub struct SharedArgs {
/// Path to input Typst file. Use `-` to read input from stdin /// Path to input Typst file. Use `-` to read input from stdin
#[clap(value_parser = input_value_parser)] #[clap(value_parser = make_input_value_parser())]
pub input: Input, pub input: Input,
/// Configures the project root (for absolute paths) /// Configures the project root (for absolute paths)
@ -277,7 +279,8 @@ impl Display for Output {
} }
/// The clap value parser used by `SharedArgs.input` /// The clap value parser used by `SharedArgs.input`
fn input_value_parser(value: &str) -> Result<Input, clap::error::Error> { fn make_input_value_parser() -> impl TypedValueParser<Value = Input> {
clap::builder::OsStringValueParser::new().try_map(|value| {
if value.is_empty() { if value.is_empty() {
Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) Err(clap::Error::new(clap::error::ErrorKind::InvalidValue))
} else if value == "-" { } else if value == "-" {
@ -285,10 +288,12 @@ fn input_value_parser(value: &str) -> Result<Input, clap::error::Error> {
} else { } else {
Ok(Input::Path(value.into())) Ok(Input::Path(value.into()))
} }
})
} }
/// The clap value parser used by `CompileCommand.output` /// The clap value parser used by `CompileCommand.output`
fn output_value_parser(value: &str) -> Result<Output, clap::error::Error> { fn make_output_value_parser() -> impl TypedValueParser<Value = Output> {
clap::builder::OsStringValueParser::new().try_map(|value| {
// Empty value also handled by clap for `Option<Output>` // Empty value also handled by clap for `Option<Output>`
if value.is_empty() { if value.is_empty() {
Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) Err(clap::Error::new(clap::error::ErrorKind::InvalidValue))
@ -297,6 +302,7 @@ fn output_value_parser(value: &str) -> Result<Output, clap::error::Error> {
} else { } else {
Ok(Output::Path(value.into())) Ok(Output::Path(value.into()))
} }
})
} }
/// Parses key/value pairs split by the first equal sign. /// Parses key/value pairs split by the first equal sign.

View File

@ -5,16 +5,16 @@ use std::path::{Path, PathBuf};
use chrono::{Datelike, Timelike}; use chrono::{Datelike, Timelike};
use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term; use codespan_reporting::term;
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, EcoString};
use parking_lot::RwLock; use parking_lot::RwLock;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{bail, FileError, Severity, SourceDiagnostic, StrResult, Warned}; use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned};
use typst::foundations::{Datetime, Smart}; use typst::foundations::{Datetime, Smart};
use typst::layout::{Frame, PageRanges}; use typst::layout::{Frame, PageRanges};
use typst::model::Document; use typst::model::Document;
use typst::syntax::{FileId, Source, Span}; use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color; use typst::visualize::Color;
use typst::{World, WorldExt}; use typst::WorldExt;
use crate::args::{ use crate::args::{
CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument,
@ -96,21 +96,6 @@ pub fn compile_once(
Status::Compiling.print(command).unwrap(); Status::Compiling.print(command).unwrap();
} }
if let Err(errors) = world
.source(world.main())
.map_err(|err| hint_invalid_main_file(err, &command.common.input))
{
set_failed();
if watching {
Status::Error.print(command).unwrap();
}
print_diagnostics(world, &errors, &[], command.common.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
return Ok(());
}
let Warned { output, warnings } = typst::compile(world); let Warned { output, warnings } = typst::compile(world);
match output { match output {
@ -472,60 +457,30 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu
/// Opens the given file using: /// Opens the given file using:
/// - The default file viewer if `open` is `None`. /// - The default file viewer if `open` is `None`.
/// - The given viewer provided by `open` if it is `Some`. /// - The given viewer provided by `open` if it is `Some`.
///
/// If the file could not be opened, an error is returned.
fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> {
// Some resource openers require the path to be canonicalized.
let path = path
.canonicalize()
.map_err(|err| eco_format!("failed to canonicalize path ({err})"))?;
if let Some(app) = open { if let Some(app) = open {
open::with_in_background(path, app); open::with_detached(&path, app)
.map_err(|err| eco_format!("failed to open file with {} ({})", app, err))
} else { } else {
open::that_in_background(path); open::that_detached(&path).map_err(|err| {
let openers = open::commands(path)
.iter()
.map(|command| command.get_program().to_string_lossy())
.collect::<Vec<_>>()
.join(", ");
eco_format!(
"failed to open file with any of these resource openers: {} ({})",
openers,
err,
)
})
} }
Ok(())
}
/// Adds useful hints when the main source file couldn't be read
/// and returns the final diagnostic.
fn hint_invalid_main_file(
file_error: FileError,
input: &Input,
) -> EcoVec<SourceDiagnostic> {
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8);
let mut diagnostic =
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
// Attempt to provide helpful hints for UTF-8 errors.
// Perhaps the user mistyped the filename.
// For example, they could have written "file.pdf" instead of
// "file.typ".
if is_utf8_error {
if let Input::Path(path) = input {
let extension = path.extension();
if extension.is_some_and(|extension| extension == "typ") {
// No hints if the file is already a .typ file.
// The file is indeed just invalid.
return eco_vec![diagnostic];
}
match extension {
Some(extension) => {
diagnostic.hint(eco_format!(
"a file with the `.{}` extension is not usually a Typst file",
extension.to_string_lossy()
));
}
None => {
diagnostic
.hint("a file without an extension is not usually a Typst file");
}
};
if path.with_extension("typ").exists() {
diagnostic.hint("check if you meant to use the `.typ` extension instead");
}
}
}
eco_vec![diagnostic]
} }
/// Print diagnostic messages to the terminal. /// Print diagnostic messages to the terminal.

View File

@ -192,8 +192,8 @@ impl World for SystemWorld {
&self.book &self.book
} }
fn main(&self) -> Source { fn main(&self) -> FileId {
self.source(self.main).unwrap() self.main
} }
fn source(&self, id: FileId) -> FileResult<Source> { fn source(&self, id: FileId) -> FileResult<Source> {

View File

@ -49,7 +49,6 @@ pub fn analyze_expr(
pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> { pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
// Use span in the node for resolving imports with relative paths. // Use span in the node for resolving imports with relative paths.
let source_span = source.span(); let source_span = source.span();
let (source, _) = analyze_expr(world, source).into_iter().next()?; let (source, _) = analyze_expr(world, source).into_iter().next()?;
if source.scope().is_some() { if source.scope().is_some() {
return Some(source); return Some(source);
@ -73,6 +72,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
Scopes::new(Some(world.library())), Scopes::new(Some(world.library())),
Span::detached(), Span::detached(),
); );
typst::eval::import(&mut vm, source, source_span, true) typst::eval::import(&mut vm, source, source_span, true)
.ok() .ok()
.map(Value::Module) .map(Value::Module)

View File

@ -17,8 +17,10 @@ use typst::visualize::Color;
use typst::World; use typst::World;
use unscanny::Scanner; use unscanny::Scanner;
use crate::analyze::{analyze_expr, analyze_import, analyze_labels}; use crate::{
use crate::{plain_docs_sentence, summarize_font_family}; analyze_expr, analyze_import, analyze_labels, named_items, plain_docs_sentence,
summarize_font_family,
};
/// Autocomplete a cursor position in a source file. /// Autocomplete a cursor position in a source file.
/// ///
@ -334,6 +336,13 @@ fn math_completions(ctx: &mut CompletionContext) {
/// Complete field accesses. /// Complete field accesses.
fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Used to determine whether trivia nodes are allowed before '.'.
// During an inline expression in markup mode trivia nodes exit the inline expression.
let in_markup: bool = matches!(
ctx.leaf.parent_kind(),
None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref)
);
// Behind an expression plus dot: "emoji.|". // Behind an expression plus dot: "emoji.|".
if_chain! { if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot if ctx.leaf.kind() == SyntaxKind::Dot
@ -341,6 +350,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
&& ctx.leaf.text() == "."); && ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor; if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling(); if let Some(prev) = ctx.leaf.prev_sibling();
if !in_markup || prev.range().end == ctx.leaf.range().start;
if prev.is::<ast::Expr>(); if prev.is::<ast::Expr>();
if prev.parent_kind() != Some(SyntaxKind::Markup) || if prev.parent_kind() != Some(SyntaxKind::Markup) ||
prev.prev_sibling_kind() == Some(SyntaxKind::Hash); prev.prev_sibling_kind() == Some(SyntaxKind::Hash);
@ -376,12 +386,12 @@ fn field_access_completions(
value: &Value, value: &Value,
styles: &Option<Styles>, styles: &Option<Styles>,
) { ) {
for (name, value) in value.ty().scope().iter() { for (name, value, _) in value.ty().scope().iter() {
ctx.value_completion(Some(name.clone()), value, true, None); ctx.value_completion(Some(name.clone()), value, true, None);
} }
if let Some(scope) = value.scope() { if let Some(scope) = value.scope() {
for (name, value) in scope.iter() { for (name, value, _) in scope.iter() {
ctx.value_completion(Some(name.clone()), value, true, None); ctx.value_completion(Some(name.clone()), value, true, None);
} }
} }
@ -547,7 +557,7 @@ fn import_item_completions<'a>(
ctx.snippet_completion("*", "*", "Import everything."); ctx.snippet_completion("*", "*", "Import everything.");
} }
for (name, value) in scope.iter() { for (name, value, _) in scope.iter() {
if existing.iter().all(|item| item.original_name().as_str() != name) { if existing.iter().all(|item| item.original_name().as_str() != name) {
ctx.value_completion(Some(name.clone()), value, false, None); ctx.value_completion(Some(name.clone()), value, false, None);
} }
@ -1319,62 +1329,12 @@ impl<'a> CompletionContext<'a> {
/// Filters the global/math scope with the given filter. /// Filters the global/math scope with the given filter.
fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) { fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
let mut defined = BTreeSet::new(); let mut defined = BTreeSet::new();
named_items(self.world, self.leaf.clone(), |name| {
let mut ancestor = Some(self.leaf.clone()); if name.value().as_ref().map_or(true, &filter) {
while let Some(node) = &ancestor { defined.insert(name.name().clone());
let mut sibling = Some(node.clone());
while let Some(node) = &sibling {
if let Some(v) = node.cast::<ast::LetBinding>() {
for ident in v.kind().bindings() {
defined.insert(ident.get().clone());
}
}
if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports();
match imports {
None | Some(ast::Imports::Wildcard) => {
if let Some(value) = node
.children()
.find(|child| child.is::<ast::Expr>())
.and_then(|source| analyze_import(self.world, &source))
{
if imports.is_none() {
defined.extend(value.name().map(Into::into));
} else if let Some(scope) = value.scope() {
for (name, _) in scope.iter() {
defined.insert(name.clone());
}
}
}
}
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
defined.insert(item.bound_name().get().clone());
}
}
}
}
sibling = node.prev_sibling();
}
if let Some(parent) = node.parent() {
if let Some(v) = parent.cast::<ast::ForLoop>() {
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
let pattern = v.pattern();
for ident in pattern.bindings() {
defined.insert(ident.get().clone());
}
}
}
ancestor = Some(parent.clone());
continue;
}
break;
} }
None::<()>
});
let in_math = matches!( let in_math = matches!(
self.leaf.parent_kind(), self.leaf.parent_kind(),
@ -1385,7 +1345,7 @@ impl<'a> CompletionContext<'a> {
); );
let scope = if in_math { self.math } else { self.global }; let scope = if in_math { self.math } else { self.global };
for (name, value) in scope.iter() { for (name, value, _) in scope.iter() {
if filter(value) && !defined.contains(name) { if filter(value) && !defined.contains(name) {
self.value_completion(Some(name.clone()), value, parens, None); self.value_completion(Some(name.clone()), value, parens, None);
} }
@ -1433,6 +1393,16 @@ mod tests {
test("#().", 4, &["insert", "remove", "len", "all"], &["foo"]); test("#().", 4, &["insert", "remove", "len", "all"], &["foo"]);
} }
#[test]
fn test_whitespace_in_autocomplete() {
//Check that extra space before '.' is handled correctly.
test("#() .", 5, &[], &["insert", "remove", "len", "all"]);
test("#{() .}", 6, &["insert", "remove", "len", "all"], &["foo"]);
test("#() .a", 6, &[], &["insert", "remove", "len", "all"]);
test("#{() .a}", 7, &["at", "any", "all"], &["foo"]);
}
#[test] #[test]
fn test_before_window_char_boundary() { fn test_before_window_char_boundary() {
// Check that the `before_window` doesn't slice into invalid byte // Check that the `before_window` doesn't slice into invalid byte

View File

@ -0,0 +1,262 @@
use ecow::EcoString;
use typst::foundations::{Label, Module, Selector, Value};
use typst::model::Document;
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Side, Source, Span, SyntaxKind};
use typst::World;
use crate::{analyze_import, deref_target, named_items, DerefTarget, NamedItem};
/// Find the definition of the item under the cursor.
///
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the definition search. Label definitions, for instance, are only generated
/// when the document is available.
pub fn definition(
world: &dyn World,
document: Option<&Document>,
source: &Source,
cursor: usize,
side: Side,
) -> Option<Definition> {
let root = LinkedNode::new(source.root());
let leaf = root.leaf_at(cursor, side)?;
let mut use_site = match deref_target(leaf.clone())? {
DerefTarget::VarAccess(node) | DerefTarget::Callee(node) => node,
DerefTarget::IncludePath(path) | DerefTarget::ImportPath(path) => {
let import_item =
analyze_import(world, &path).and_then(|v| v.cast::<Module>().ok())?;
return Some(Definition::module(&import_item, path.span(), Span::detached()));
}
DerefTarget::Ref(r) => {
let label = Label::new(r.cast::<ast::Ref>()?.target());
let sel = Selector::Label(label);
let elem = document?.introspector.query_first(&sel)?;
let span = elem.span();
return Some(Definition {
kind: DefinitionKind::Label,
name: label.as_str().into(),
value: Some(Value::Label(label)),
span,
name_span: Span::detached(),
});
}
DerefTarget::Label(..) | DerefTarget::Code(..) => {
return None;
}
};
let mut has_path = false;
while let Some(node) = use_site.cast::<ast::FieldAccess>() {
has_path = true;
use_site = use_site.find(node.target().span())?;
}
let name = use_site.cast::<ast::Ident>()?.get().clone();
let src = named_items(world, use_site, |item: NamedItem| {
if *item.name() != name {
return None;
}
match item {
NamedItem::Var(name) => {
let name_span = name.span();
let span = find_let_binding(source, name_span);
Some(Definition::item(name.get().clone(), span, name_span, None))
}
NamedItem::Fn(name) => {
let name_span = name.span();
let span = find_let_binding(source, name_span);
Some(
Definition::item(name.get().clone(), span, name_span, None)
.with_kind(DefinitionKind::Function),
)
}
NamedItem::Module(item, site) => Some(Definition::module(
item,
site.span(),
matches!(site.kind(), SyntaxKind::Ident)
.then_some(site.span())
.unwrap_or_else(Span::detached),
)),
NamedItem::Import(name, name_span, value) => Some(Definition::item(
name.clone(),
Span::detached(),
name_span,
value.cloned(),
)),
}
});
let src = src.or_else(|| {
let in_math = matches!(
leaf.parent_kind(),
Some(SyntaxKind::Equation)
| Some(SyntaxKind::Math)
| Some(SyntaxKind::MathFrac)
| Some(SyntaxKind::MathAttach)
);
let library = world.library();
let scope = if in_math { library.math.scope() } else { library.global.scope() };
for (item_name, value, span) in scope.iter() {
if *item_name == name {
return Some(Definition::item(
name,
span,
Span::detached(),
Some(value.clone()),
));
}
}
None
})?;
(!has_path).then_some(src)
}
/// A definition of some item.
#[derive(Debug, Clone)]
pub struct Definition {
/// The name of the definition.
pub name: EcoString,
/// The kind of the definition.
pub kind: DefinitionKind,
/// An instance of the definition, if available.
pub value: Option<Value>,
/// The source span of the entire definition. May be detached if unknown.
pub span: Span,
/// The span of the definition's name. May be detached if unknown.
pub name_span: Span,
}
impl Definition {
fn item(name: EcoString, span: Span, name_span: Span, value: Option<Value>) -> Self {
Self {
name,
kind: match value {
Some(Value::Func(_)) => DefinitionKind::Function,
_ => DefinitionKind::Variable,
},
value,
span,
name_span,
}
}
fn module(module: &Module, span: Span, name_span: Span) -> Self {
Definition {
name: module.name().clone(),
kind: DefinitionKind::Module,
value: Some(Value::Module(module.clone())),
span,
name_span,
}
}
fn with_kind(self, kind: DefinitionKind) -> Self {
Self { kind, ..self }
}
}
/// A kind of item that is definition.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum DefinitionKind {
/// ```plain
/// let foo;
/// ^^^^^^^^ span
/// ^^^ name_span
/// ```
Variable,
/// ```plain
/// let foo(it) = it;
/// ^^^^^^^^^^^^^^^^^ span
/// ^^^ name_span
/// ```
Function,
/// Case 1
/// ```plain
/// import "foo.typ": *
/// ^^^^^^^^^ span
/// name_span is detached
/// ```
///
/// Case 2
/// ```plain
/// import "foo.typ" as bar: *
/// span ^^^
/// name_span ^^^
/// ```
Module,
/// ```plain
/// <foo>
/// ^^^^^ span
/// name_span is detached
/// ```
Label,
}
fn find_let_binding(source: &Source, name_span: Span) -> Span {
let node = LinkedNode::new(source.root());
std::iter::successors(node.find(name_span).as_ref(), |n| n.parent())
.find(|n| matches!(n.kind(), SyntaxKind::LetBinding))
.map(|s| s.span())
.unwrap_or_else(Span::detached)
}
#[cfg(test)]
mod tests {
use std::ops::Range;
use typst::foundations::{IntoValue, Label, NativeElement, Value};
use typst::syntax::Side;
use typst::WorldExt;
use super::{definition, DefinitionKind as Kind};
use crate::tests::TestWorld;
#[track_caller]
fn test<T>(
text: &str,
cursor: usize,
name: &str,
kind: Kind,
value: Option<T>,
range: Option<Range<usize>>,
) where
T: IntoValue,
{
let world = TestWorld::new(text);
let doc = typst::compile(&world).output.ok();
let actual = definition(&world, doc.as_ref(), &world.main, cursor, Side::After)
.map(|d| (d.kind, d.name, world.range(d.span), d.value));
assert_eq!(
actual,
Some((kind, name.into(), range, value.map(IntoValue::into_value)))
);
}
#[test]
fn test_definition() {
test("#let x; #x", 9, "x", Kind::Variable, None::<Value>, Some(1..6));
test("#let x() = {}; #x", 16, "x", Kind::Function, None::<Value>, Some(1..13));
test(
"#table",
1,
"table",
Kind::Function,
Some(typst::model::TableElem::elem()),
None,
);
test(
"#figure[] <hi> See @hi",
21,
"hi",
Kind::Label,
Some(Label::new("hi")),
Some(1..9),
);
}
}

View File

@ -2,12 +2,16 @@
mod analyze; mod analyze;
mod complete; mod complete;
mod definition;
mod jump; mod jump;
mod matchers;
mod tooltip; mod tooltip;
pub use self::analyze::analyze_labels; pub use self::analyze::{analyze_expr, analyze_import, analyze_labels};
pub use self::complete::{autocomplete, Completion, CompletionKind}; pub use self::complete::{autocomplete, Completion, CompletionKind};
pub use self::definition::{definition, Definition, DefinitionKind};
pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; pub use self::jump::{jump_from_click, jump_from_cursor, Jump};
pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem};
pub use self::tooltip::{tooltip, Tooltip}; pub use self::tooltip::{tooltip, Tooltip};
use std::fmt::Write; use std::fmt::Write;
@ -135,8 +139,8 @@ mod tests {
&self.base.book &self.base.book
} }
fn main(&self) -> Source { fn main(&self) -> FileId {
self.main.clone() self.main.id()
} }
fn source(&self, id: FileId) -> FileResult<Source> { fn source(&self, id: FileId) -> FileResult<Source> {

View File

@ -0,0 +1,270 @@
use ecow::EcoString;
use typst::foundations::{Module, Value};
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode};
use typst::World;
use crate::analyze_import;
/// Find the named items starting from the given position.
pub fn named_items<T>(
world: &dyn World,
position: LinkedNode,
mut recv: impl FnMut(NamedItem) -> Option<T>,
) -> Option<T> {
let mut ancestor = Some(position);
while let Some(node) = &ancestor {
let mut sibling = Some(node.clone());
while let Some(node) = &sibling {
if let Some(v) = node.cast::<ast::LetBinding>() {
let kind = if matches!(v.kind(), ast::LetBindingKind::Closure(..)) {
NamedItem::Fn
} else {
NamedItem::Var
};
for ident in v.kind().bindings() {
if let Some(res) = recv(kind(ident)) {
return Some(res);
}
}
}
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();
// 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);
}
}
}
// Seeing the imported items.
match imports {
// ```plain
// import "foo";
// ```
None => {}
// ```plain
// 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(res) = recv(item) {
return Some(res);
}
}
}
}
// ```plain
// import "foo": items;
// ```
Some(ast::Imports::Items(items)) => {
for item in items.iter() {
let original = item.original_name();
let bound = item.bound_name();
let scope = source.and_then(|(value, _)| value.scope());
let span = scope
.and_then(|s| s.get_span(&original))
.unwrap_or(Span::detached())
.or(bound.span());
let value = scope.and_then(|s| s.get(&original));
if let Some(res) =
recv(NamedItem::Import(bound.get(), span, value))
{
return Some(res);
}
}
}
}
}
sibling = node.prev_sibling();
}
if let Some(parent) = node.parent() {
if let Some(v) = parent.cast::<ast::ForLoop>() {
if node.prev_sibling_kind() != Some(SyntaxKind::In) {
let pattern = v.pattern();
for ident in pattern.bindings() {
if let Some(res) = recv(NamedItem::Var(ident)) {
return Some(res);
}
}
}
}
ancestor = Some(parent.clone());
continue;
}
break;
}
None
}
/// An item that is named.
pub enum NamedItem<'a> {
/// A variable item.
Var(ast::Ident<'a>),
/// A function item.
Fn(ast::Ident<'a>),
/// A (imported) module item.
Module(&'a Module, &'a SyntaxNode),
/// An imported item.
Import(&'a EcoString, Span, Option<&'a Value>),
}
impl<'a> NamedItem<'a> {
pub(crate) fn name(&self) -> &'a EcoString {
match self {
NamedItem::Var(ident) => ident.get(),
NamedItem::Fn(ident) => ident.get(),
NamedItem::Module(value, _) => value.name(),
NamedItem::Import(name, _, _) => name,
}
}
pub(crate) fn value(&self) -> Option<Value> {
match self {
NamedItem::Var(..) | NamedItem::Fn(..) => None,
NamedItem::Module(value, _) => Some(Value::Module((*value).clone())),
NamedItem::Import(_, _, value) => value.cloned(),
}
}
}
/// Categorize an expression into common classes IDE functionality can operate
/// on.
pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
// Move to the first ancestor that is an expression.
let mut ancestor = node;
while !ancestor.is::<ast::Expr>() {
ancestor = ancestor.parent()?.clone();
}
// Identify convenient expression kinds.
let expr_node = ancestor;
let expr = expr_node.cast::<ast::Expr>()?;
Some(match expr {
ast::Expr::Label(..) => DerefTarget::Label(expr_node),
ast::Expr::Ref(..) => DerefTarget::Ref(expr_node),
ast::Expr::FuncCall(call) => {
DerefTarget::Callee(expr_node.find(call.callee().span())?)
}
ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?),
ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => {
DerefTarget::VarAccess(expr_node)
}
ast::Expr::Str(..) => {
let parent = expr_node.parent()?;
if parent.kind() == SyntaxKind::ModuleImport {
DerefTarget::ImportPath(expr_node)
} else if parent.kind() == SyntaxKind::ModuleInclude {
DerefTarget::IncludePath(expr_node)
} else {
DerefTarget::Code(expr_node.kind(), expr_node)
}
}
_ if expr.hash()
|| matches!(expr_node.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) =>
{
DerefTarget::Code(expr_node.kind(), expr_node)
}
_ => return None,
})
}
/// Classes of expressions that can be operated on by IDE functionality.
#[derive(Debug, Clone)]
pub enum DerefTarget<'a> {
/// A label expression.
Label(LinkedNode<'a>),
/// A reference expression.
Ref(LinkedNode<'a>),
/// A variable access expression.
///
/// It can be either an identifier or a field access.
VarAccess(LinkedNode<'a>),
/// A function call expression.
Callee(LinkedNode<'a>),
/// An import path expression.
ImportPath(LinkedNode<'a>),
/// An include path expression.
IncludePath(LinkedNode<'a>),
/// Any code expression.
Code(SyntaxKind, LinkedNode<'a>),
}
#[cfg(test)]
mod tests {
use typst::syntax::{LinkedNode, Side};
use crate::{named_items, tests::TestWorld};
#[track_caller]
fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool {
let world = TestWorld::new(text);
let src = world.main.clone();
let node = LinkedNode::new(src.root());
let leaf = node.leaf_at(cursor, Side::After).unwrap();
let res = named_items(&world, leaf, |s| {
if containing == s.name() {
return Some(true);
}
None
});
res.unwrap_or_default()
}
#[test]
fn test_simple_named_items() {
// Has named items
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a"));
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a"));
// Doesn't have named items
assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
}
#[test]
fn test_import_named_items() {
// Cannot test much.
assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a"));
}
}

View File

@ -11,14 +11,13 @@ use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
use typst::utils::{round_2, Numeric}; use typst::utils::{round_2, Numeric};
use typst::World; use typst::World;
use crate::analyze::{analyze_expr, analyze_labels}; use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family};
use crate::{plain_docs_sentence, summarize_font_family};
/// Describe the item under the cursor. /// Describe the item under the cursor.
/// ///
/// Passing a `document` (from a previous compilation) is optional, but enhances /// Passing a `document` (from a previous compilation) is optional, but enhances
/// the autocompletions. Label completions, for instance, are only generated /// the tooltips. Label tooltips, for instance, are only generated when the
/// when the document is available. /// document is available.
pub fn tooltip( pub fn tooltip(
world: &dyn World, world: &dyn World,
document: Option<&Document>, document: Option<&Document>,
@ -127,7 +126,7 @@ fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
let captures = visitor.finish(); let captures = visitor.finish();
let mut names: Vec<_> = let mut names: Vec<_> =
captures.iter().map(|(name, _)| eco_format!("`{name}`")).collect(); captures.iter().map(|(name, ..)| eco_format!("`{name}`")).collect();
if names.is_empty() { if names.is_empty() {
return None; return None;
} }

View File

@ -92,7 +92,7 @@ pub struct Builder<'a, R = ()> {
state: State, state: State,
/// Stack of saved graphic states. /// Stack of saved graphic states.
saves: Vec<State>, saves: Vec<State>,
/// Wheter any stroke or fill was not totally opaque. /// Whether any stroke or fill was not totally opaque.
uses_opacities: bool, uses_opacities: bool,
/// All clickable links that are present in this content. /// All clickable links that are present in this content.
links: Vec<(Destination, Rect)>, links: Vec<(Destination, Rect)>,
@ -129,7 +129,7 @@ struct State {
/// The color space of the current fill paint. /// The color space of the current fill paint.
fill_space: Option<Name<'static>>, fill_space: Option<Name<'static>>,
/// The current external graphic state. /// The current external graphic state.
external_graphics_state: Option<ExtGState>, external_graphics_state: ExtGState,
/// The current stroke paint. /// The current stroke paint.
stroke: Option<FixedStroke>, stroke: Option<FixedStroke>,
/// The color space of the current stroke paint. /// The color space of the current stroke paint.
@ -148,7 +148,7 @@ impl State {
font: None, font: None,
fill: None, fill: None,
fill_space: None, fill_space: None,
external_graphics_state: None, external_graphics_state: ExtGState::default(),
stroke: None, stroke: None,
stroke_space: None, stroke_space: None,
text_rendering_mode: TextRenderingMode::Fill, text_rendering_mode: TextRenderingMode::Fill,
@ -191,12 +191,13 @@ impl Builder<'_, ()> {
} }
fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) { fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) {
let current_state = self.state.external_graphics_state.as_ref(); let current_state = &self.state.external_graphics_state;
if current_state != Some(graphics_state) { if current_state != graphics_state {
let index = self.resources.ext_gs.insert(*graphics_state); let index = self.resources.ext_gs.insert(*graphics_state);
let name = eco_format!("Gs{index}"); let name = eco_format!("Gs{index}");
self.content.set_parameters(Name(name.as_bytes())); self.content.set_parameters(Name(name.as_bytes()));
self.state.external_graphics_state = *graphics_state;
if graphics_state.uses_opacities() { if graphics_state.uses_opacities() {
self.uses_opacities = true; self.uses_opacities = true;
} }
@ -204,29 +205,27 @@ impl Builder<'_, ()> {
} }
fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) {
let stroke_opacity = stroke let get_opacity = |paint: &Paint| {
.map(|stroke| {
let color = match &stroke.paint {
Paint::Solid(color) => *color,
Paint::Gradient(_) | Paint::Pattern(_) => return 255,
};
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
})
.unwrap_or(255);
let fill_opacity = fill
.map(|paint| {
let color = match paint { let color = match paint {
Paint::Solid(color) => *color, Paint::Solid(color) => *color,
Paint::Gradient(_) | Paint::Pattern(_) => return 255, Paint::Gradient(_) | Paint::Pattern(_) => return 255,
}; };
color.alpha().map_or(255, |v| (v * 255.0).round() as u8) color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
}) };
.unwrap_or(255);
let stroke_opacity = stroke.map_or(255, |stroke| get_opacity(&stroke.paint));
let fill_opacity = fill.map_or(255, get_opacity);
self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity });
} }
fn reset_opacities(&mut self) {
self.set_external_graphics_state(&ExtGState {
stroke_opacity: 255,
fill_opacity: 255,
});
}
pub fn transform(&mut self, transform: Transform) { pub fn transform(&mut self, transform: Transform) {
let Transform { sx, ky, kx, sy, tx, ty } = transform; let Transform { sx, ky, kx, sy, tx, ty } = transform;
self.state.transform = self.state.transform.pre_concat(transform); self.state.transform = self.state.transform.pre_concat(transform);
@ -542,6 +541,8 @@ fn write_color_glyphs(ctx: &mut Builder, pos: Point, text: TextItemView) {
let mut last_font = None; let mut last_font = None;
ctx.reset_opacities();
ctx.content.begin_text(); ctx.content.begin_text();
ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
// So that the next call to ctx.set_font() will change the font to one that // So that the next call to ctx.set_font() will change the font to one that
@ -671,6 +672,8 @@ fn write_image(ctx: &mut Builder, x: f32, y: f32, image: &Image, size: Size) {
image image
}); });
ctx.reset_opacities();
let name = eco_format!("Im{index}"); let name = eco_format!("Im{index}");
let w = size.x.to_f32(); let w = size.x.to_f32();
let h = size.y.to_f32(); let h = size.y.to_f32();

View File

@ -17,6 +17,7 @@ typst-utils = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
toml = { workspace = true }
unicode-ident = { workspace = true } unicode-ident = { workspace = true }
unicode-math-class = { workspace = true } unicode-math-class = { workspace = true }
unicode-script = { workspace = true } unicode-script = { workspace = true }

View File

@ -91,6 +91,11 @@ impl FileId {
Self::new(self.package().cloned(), self.vpath().join(path)) Self::new(self.package().cloned(), self.vpath().join(path))
} }
/// The same file location, but with a different extension.
pub fn with_extension(&self, extension: &str) -> Self {
Self::new(self.package().cloned(), self.vpath().with_extension(extension))
}
/// Construct from a raw number. /// Construct from a raw number.
pub(crate) const fn from_raw(v: u16) -> Self { pub(crate) const fn from_raw(v: u16) -> Self {
Self(v) Self(v)

View File

@ -650,7 +650,7 @@ impl Lexer<'_> {
Some('-') if !s.at(['-', '?']) => {} Some('-') if !s.at(['-', '?']) => {}
Some('.') if !s.at("..") => {} Some('.') if !s.at("..") => {}
Some('h') if !s.at("ttp://") && !s.at("ttps://") => {} Some('h') if !s.at("ttp://") && !s.at("ttps://") => {}
Some('@') if !s.at(is_id_start) => {} Some('@') if !s.at(is_valid_in_label_literal) => {}
_ => break, _ => break,
} }

View File

@ -1,37 +1,101 @@
//! Package manifest parsing. //! Package manifest parsing.
use std::collections::BTreeMap;
use std::fmt::{self, Debug, Display, Formatter}; use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::de::IgnoredAny;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use unscanny::Scanner; use unscanny::Scanner;
use crate::is_ident; use crate::is_ident;
/// A type alias for a map of key-value pairs used to collect unknown fields
/// where values are completely discarded.
pub type UnknownFields = BTreeMap<EcoString, IgnoredAny>;
/// A parsed package manifest. /// A parsed package manifest.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] ///
/// The `unknown_fields` contains fields which were found but not expected.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PackageManifest { pub struct PackageManifest {
/// Details about the package itself. /// Details about the package itself.
pub package: PackageInfo, pub package: PackageInfo,
/// Details about the template, if the package is one. /// Details about the template, if the package is one.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub template: Option<TemplateInfo>, pub template: Option<TemplateInfo>,
/// The tools section for third-party configuration.
#[serde(default)]
pub tool: ToolInfo,
/// All parsed but unknown fields, this can be used for validation.
#[serde(flatten, skip_serializing)]
pub unknown_fields: UnknownFields,
}
/// The `[tool]` key in the manifest. This field can be used to retrieve
/// 3rd-party tool configuration.
///
// # Examples
/// ```
/// # use serde::{Deserialize, Serialize};
/// # use ecow::EcoString;
/// # use typst_syntax::package::PackageManifest;
/// #[derive(Debug, PartialEq, Serialize, Deserialize)]
/// struct MyTool {
/// key: EcoString,
/// }
///
/// let mut manifest: PackageManifest = toml::from_str(r#"
/// [package]
/// name = "package"
/// version = "0.1.0"
/// entrypoint = "src/lib.typ"
///
/// [tool.my-tool]
/// key = "value"
/// "#)?;
///
/// let my_tool = manifest
/// .tool
/// .sections
/// .remove("my-tool")
/// .ok_or("tool.my-tool section missing")?;
/// let my_tool = MyTool::deserialize(my_tool)?;
///
/// assert_eq!(my_tool, MyTool { key: "value".into() });
/// # Ok::<_, Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ToolInfo {
/// Any fields parsed in the tool section.
#[serde(flatten)]
pub sections: BTreeMap<EcoString, toml::Table>,
} }
/// The `[template]` key in the manifest. /// The `[template]` key in the manifest.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] ///
/// The `unknown_fields` contains fields which were found but not expected.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemplateInfo { pub struct TemplateInfo {
/// The path of the starting point within the package. /// The directory within the package that contains the files that should be
/// copied into the user's new project directory.
pub path: EcoString, pub path: EcoString,
/// The path of the entrypoint relative to the starting point's `path`. /// A path relative to the template's path that points to the file serving
/// as the compilation target.
pub entrypoint: EcoString, pub entrypoint: EcoString,
/// A path relative to the package's root that points to a PNG or lossless
/// WebP thumbnail for the template.
pub thumbnail: EcoString,
/// All parsed but unknown fields, this can be used for validation.
#[serde(flatten, skip_serializing)]
pub unknown_fields: UnknownFields,
} }
/// The `[package]` key in the manifest. /// The `[package]` key in the manifest.
/// ///
/// More fields are specified, but they are not relevant to the compiler. /// The `unknown_fields` contains fields which were found but not expected.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PackageInfo { pub struct PackageInfo {
/// The name of the package within its namespace. /// The name of the package within its namespace.
pub name: EcoString, pub name: EcoString,
@ -39,8 +103,42 @@ pub struct PackageInfo {
pub version: PackageVersion, pub version: PackageVersion,
/// The path of the entrypoint into the package. /// The path of the entrypoint into the package.
pub entrypoint: EcoString, pub entrypoint: EcoString,
/// A list of the package's authors.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<EcoString>,
/// The package's license.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<EcoString>,
/// A short description of the package.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<EcoString>,
/// A link to the package's web presence.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub homepage: Option<EcoString>,
/// A link to the repository where this package is developed.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<EcoString>,
/// An array of search keywords for the package.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub keywords: Vec<EcoString>,
/// An array with up to three of the predefined categories to help users
/// discover the package.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub categories: Vec<EcoString>,
/// An array of disciplines defining the target audience for which the
/// package is useful.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disciplines: Vec<EcoString>,
/// The minimum required compiler version for the package. /// The minimum required compiler version for the package.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<VersionBound>, pub compiler: Option<VersionBound>,
/// An array of globs specifying files that should not be part of the
/// published bundle.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude: Vec<EcoString>,
/// All parsed but unknown fields, this can be used for validation.
#[serde(flatten, skip_serializing)]
pub unknown_fields: UnknownFields,
} }
impl PackageManifest { impl PackageManifest {
@ -423,4 +521,97 @@ mod tests {
assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap())); assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap()));
assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap())); assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap()));
} }
#[test]
fn minimal_manifest() {
assert_eq!(
toml::from_str::<PackageManifest>(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
"#
),
Ok(PackageManifest {
package: PackageInfo {
name: "package".into(),
version: PackageVersion { major: 0, minor: 1, patch: 0 },
entrypoint: "src/lib.typ".into(),
authors: vec![],
license: None,
description: None,
homepage: None,
repository: None,
keywords: vec![],
categories: vec![],
disciplines: vec![],
compiler: None,
exclude: vec![],
unknown_fields: BTreeMap::new(),
},
template: None,
tool: ToolInfo { sections: BTreeMap::new() },
unknown_fields: BTreeMap::new(),
})
);
}
#[test]
fn tool_section() {
// NOTE: tool section must be table of tables, but we can't easily
// compare the error structurally
assert!(toml::from_str::<PackageManifest>(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
[tool]
not-table = "str"
"#
)
.is_err());
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct MyTool {
key: EcoString,
}
let mut manifest: PackageManifest = toml::from_str(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
[tool.my-tool]
key = "value"
"#,
)
.unwrap();
let my_tool = manifest.tool.sections.remove("my-tool").unwrap();
let my_tool = MyTool::deserialize(my_tool).unwrap();
assert_eq!(my_tool, MyTool { key: "value".into() });
}
#[test]
fn unknown_keys() {
let manifest: PackageManifest = toml::from_str(
r#"
[package]
name = "package"
version = "0.1.0"
entrypoint = "src/lib.typ"
[unknown]
"#,
)
.unwrap();
assert!(manifest.unknown_fields.contains_key("unknown"));
}
} }

View File

@ -395,11 +395,6 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
continue; continue;
} }
// Separate primes and superscripts to different attachments.
if primed && p.current() == SyntaxKind::Hat {
p.wrap(m, SyntaxKind::MathAttach);
}
let Some((kind, stop, assoc, mut prec)) = math_op(p.current()) else { let Some((kind, stop, assoc, mut prec)) = math_op(p.current()) else {
// No attachments, so we need to wrap primes as attachment. // No attachments, so we need to wrap primes as attachment.
if primed { if primed {
@ -431,7 +426,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
math_expr_prec(p, prec, stop); math_expr_prec(p, prec, stop);
math_unparen(p, m2); math_unparen(p, m2);
if p.eat_if(SyntaxKind::Underscore) || (!primed && p.eat_if(SyntaxKind::Hat)) { if p.eat_if(SyntaxKind::Underscore) || p.eat_if(SyntaxKind::Hat) {
let m3 = p.marker(); let m3 = p.marker();
math_expr_prec(p, prec, SyntaxKind::End); math_expr_prec(p, prec, SyntaxKind::End);
math_unparen(p, m3); math_unparen(p, m3);

View File

@ -85,6 +85,11 @@ impl VirtualPath {
Self::new(path) Self::new(path)
} }
} }
/// The same path, but with a different extension.
pub fn with_extension(&self, extension: &str) -> Self {
Self(self.0.with_extension(extension))
}
} }
impl Debug for VirtualPath { impl Debug for VirtualPath {

View File

@ -83,6 +83,15 @@ impl Span {
self.0.get() & ((1 << Self::BITS) - 1) self.0.get() & ((1 << Self::BITS) - 1)
} }
/// Return `other` if `self` is detached and `self` otherwise.
pub fn or(self, other: Self) -> Self {
if self.is_detached() {
other
} else {
self
}
}
/// Resolve a file location relative to this span's source. /// Resolve a file location relative to this span's source.
pub fn resolve_path(self, path: &str) -> Result<FileId, EcoString> { pub fn resolve_path(self, path: &str) -> Result<FileId, EcoString> {
let Some(file) = self.id() else { let Some(file) = self.id() else {

View File

@ -1,7 +1,10 @@
use comemo::{Tracked, TrackedMut}; use comemo::{Tracked, TrackedMut};
use ecow::{eco_format, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use crate::diag::{bail, error, At, HintedStrResult, SourceResult, Trace, Tracepoint}; use crate::diag::{
bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult,
Trace, Tracepoint,
};
use crate::engine::{Engine, Sink, Traced}; use crate::engine::{Engine, Sink, Traced};
use crate::eval::{Access, Eval, FlowEvent, Route, Vm}; use crate::eval::{Access, Eval, FlowEvent, Route, Vm};
use crate::foundations::{ use crate::foundations::{
@ -10,7 +13,7 @@ use crate::foundations::{
}; };
use crate::introspection::Introspector; use crate::introspection::Introspector;
use crate::math::LrElem; use crate::math::LrElem;
use crate::syntax::ast::{self, AstNode}; use crate::syntax::ast::{self, AstNode, Ident};
use crate::syntax::{Span, Spanned, SyntaxNode}; use crate::syntax::{Span, Spanned, SyntaxNode};
use crate::text::TextElem; use crate::text::TextElem;
use crate::utils::LazyHash; use crate::utils::LazyHash;
@ -32,135 +35,25 @@ impl Eval for ast::FuncCall<'_> {
} }
// Try to evaluate as a call to an associated function or field. // Try to evaluate as a call to an associated function or field.
let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { let (callee, args) = if let ast::Expr::FieldAccess(access) = callee {
let target = access.target(); let target = access.target();
let target_span = target.span();
let field = access.field(); let field = access.field();
let field_span = field.span(); match eval_field_call(target, field, args, span, vm)? {
FieldCall::Normal(callee, args) => (callee, args),
let target = if is_mutating_method(&field) { FieldCall::Resolved(value) => return Ok(value),
let mut args = args.eval(vm)?.spanned(span);
let target = target.access(vm)?;
// Only arrays and dictionaries have mutable methods.
if matches!(target, Value::Array(_) | Value::Dict(_)) {
args.span = span;
let point = || Tracepoint::Call(Some(field.get().clone()));
return call_method_mut(target, &field, args, span).trace(
vm.world(),
point,
span,
);
}
target.clone()
} else {
access.target().eval(vm)?
};
let mut args = args.eval(vm)?.spanned(span);
// Handle plugins.
if let Value::Plugin(plugin) = &target {
let bytes = args.all::<Bytes>()?;
args.finish()?;
return Ok(plugin.call(&field, bytes).at(span)?.into_value());
}
// Prioritize associated functions on the value's type (i.e.,
// methods) over its fields. A function call on a field is only
// allowed for functions, types, modules (because they are scopes),
// and symbols (because they have modifiers).
//
// For dictionaries, it is not allowed because it would be ambiguous
// (prioritizing associated functions would make an addition of a
// new associated function a breaking change and prioritizing fields
// would break associated functions for certain dictionaries).
if let Some(callee) = target.ty().scope().get(&field) {
let this = Arg {
span: target_span,
name: None,
value: Spanned::new(target, target_span),
};
args.span = span;
args.items.insert(0, this);
(callee.clone(), args)
} else if matches!(
target,
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
) {
(target.field(&field).at(field_span)?, args)
} else {
let mut error = error!(
field_span,
"type {} has no method `{}`",
target.ty(),
field.as_str()
);
let mut field_hint = || {
if target.field(&field).is_ok() {
error.hint(eco_format!(
"did you mean to access the field `{}`?",
field.as_str()
));
}
};
match target {
Value::Dict(ref dict) => {
if matches!(dict.get(&field), Ok(Value::Func(_))) {
error.hint(eco_format!(
"to call the function stored in the dictionary, surround \
the field access with parentheses, e.g. `(dict.{})(..)`",
field.as_str(),
));
} else {
field_hint();
}
}
_ => field_hint(),
}
bail!(error);
} }
} else { } else {
// Function call order: we evaluate the callee before the arguments.
(callee.eval(vm)?, args.eval(vm)?.spanned(span)) (callee.eval(vm)?, args.eval(vm)?.spanned(span))
}; };
let func_result = callee.clone().cast::<Func>(); let func_result = callee.clone().cast::<Func>();
if in_math && func_result.is_err() { if in_math && func_result.is_err() {
// For non-functions in math, we wrap the arguments in parentheses. return wrap_args_in_math(callee, callee_span, args, trailing_comma);
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += TextElem::packed(',');
}
body += arg;
}
if trailing_comma {
body += TextElem::packed(',');
}
return Ok(Value::Content(
callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')'))
.pack(),
));
} }
let func = func_result let func = func_result
.map_err(|mut err| { .map_err(|err| hint_if_shadowed_std(vm, &self.callee(), err))
if let ast::Expr::Ident(ident) = self.callee() {
let ident = ident.get();
if vm.scopes.check_std_shadowed(ident) {
err.hint(eco_format!(
"use `std.{}` to access the shadowed standard library function",
ident,
));
}
}
err
})
.at(callee_span)?; .at(callee_span)?;
let point = || Tracepoint::Call(func.name().map(Into::into)); let point = || Tracepoint::Call(func.name().map(Into::into));
@ -371,6 +264,108 @@ pub(crate) fn call_closure(
Ok(output) Ok(output)
} }
/// This used only as the return value of `eval_field_call`.
/// - `Normal` means that we have a function to call and the arguments to call it with.
/// - `Resolved` means that we have already resolved the call and have the value.
enum FieldCall {
Normal(Value, Args),
Resolved(Value),
}
/// Evaluate a field call's callee and arguments.
///
/// This follows the normal function call order: we evaluate the callee before the
/// arguments.
///
/// Prioritize associated functions on the value's type (e.g., methods) over its fields.
/// A function call on a field is only allowed for functions, types, modules (because
/// they are scopes), and symbols (because they have modifiers or associated functions).
///
/// For dictionaries, it is not allowed because it would be ambiguous - prioritizing
/// associated functions would make an addition of a new associated function a breaking
/// change and prioritizing fields would break associated functions for certain
/// dictionaries.
fn eval_field_call(
target_expr: ast::Expr,
field: Ident,
args: ast::Args,
span: Span,
vm: &mut Vm,
) -> SourceResult<FieldCall> {
// Evaluate the field-call's target and overall arguments.
let (target, mut args) = if is_mutating_method(&field) {
// If `field` looks like a mutating method, we evaluate the arguments first,
// because `target_expr.access(vm)` mutably borrows the `vm`, so that we can't
// evaluate the arguments after it.
let args = args.eval(vm)?.spanned(span);
// However, this difference from the normal call order is not observable because
// expressions like `(1, arr.len(), 2, 3).push(arr.pop())` evaluate the target to
// a temporary which we disallow mutation on (returning an error).
// Theoretically this could be observed if a method matching `is_mutating_method`
// was added to some type in the future and we didn't update this function.
match target_expr.access(vm)? {
// Only arrays and dictionaries have mutable methods.
target @ (Value::Array(_) | Value::Dict(_)) => {
let value = call_method_mut(target, &field, args, span);
let point = || Tracepoint::Call(Some(field.get().clone()));
return Ok(FieldCall::Resolved(value.trace(vm.world(), point, span)?));
}
target => (target.clone(), args),
}
} else {
let target = target_expr.eval(vm)?;
let args = args.eval(vm)?.spanned(span);
(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) {
args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.clone(), args))
} 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())?;
Ok(FieldCall::Normal(value, args))
} else {
// Otherwise we cannot call this field.
bail!(missing_field_call_error(target, field))
}
}
/// 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());
match target {
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
error.hint(eco_format!(
"to call the function stored in the dictionary, surround \
the field access with parentheses, e.g. `(dict.{})(..)`",
field.as_str(),
));
}
_ if target.field(&field).is_ok() => {
error.hint(eco_format!(
"did you mean to access the field `{}`?",
field.as_str(),
));
}
_ => {}
}
error
}
/// Check if the expression is in a math context.
fn in_math(expr: ast::Expr) -> bool { fn in_math(expr: ast::Expr) -> bool {
match expr { match expr {
ast::Expr::MathIdent(_) => true, ast::Expr::MathIdent(_) => true,
@ -379,6 +374,46 @@ fn in_math(expr: ast::Expr) -> bool {
} }
} }
/// For non-functions in math, we wrap the arguments in parentheses.
fn wrap_args_in_math(
callee: Value,
callee_span: Span,
mut args: Args,
trailing_comma: bool,
) -> SourceResult<Value> {
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += TextElem::packed(',');
}
body += arg;
}
if trailing_comma {
body += TextElem::packed(',');
}
Ok(Value::Content(
callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')).pack(),
))
}
/// Provide a hint if the callee is a shadowed standard library function.
fn hint_if_shadowed_std(
vm: &mut Vm,
callee: &ast::Expr,
mut err: HintedString,
) -> HintedString {
if let ast::Expr::Ident(ident) = callee {
let ident = ident.get();
if vm.scopes.check_std_shadowed(ident) {
err.hint(eco_format!(
"use `std.{ident}` to access the shadowed standard library function",
));
}
}
err
}
/// A visitor that determines which variables to capture for a closure. /// A visitor that determines which variables to capture for a closure.
pub struct CapturesVisitor<'a> { pub struct CapturesVisitor<'a> {
external: Option<&'a Scopes<'a>>, external: Option<&'a Scopes<'a>>,
@ -410,9 +445,11 @@ impl<'a> CapturesVisitor<'a> {
// Identifiers that shouldn't count as captures because they // Identifiers that shouldn't count as captures because they
// actually bind a new name are handled below (individually through // actually bind a new name are handled below (individually through
// the expressions that contain them). // the expressions that contain them).
Some(ast::Expr::Ident(ident)) => self.capture(&ident, Scopes::get), Some(ast::Expr::Ident(ident)) => {
self.capture(ident.get(), ident.span(), Scopes::get)
}
Some(ast::Expr::MathIdent(ident)) => { Some(ast::Expr::MathIdent(ident)) => {
self.capture(&ident, Scopes::get_in_math) self.capture(ident.get(), ident.span(), Scopes::get_in_math)
} }
// Code and content blocks create a scope. // Code and content blocks create a scope.
@ -520,13 +557,14 @@ impl<'a> CapturesVisitor<'a> {
/// Bind a new internal variable. /// Bind a new internal variable.
fn bind(&mut self, ident: ast::Ident) { fn bind(&mut self, ident: ast::Ident) {
self.internal.top.define(ident.get().clone(), Value::None); self.internal.top.define_ident(ident, Value::None);
} }
/// Capture a variable if it isn't internal. /// Capture a variable if it isn't internal.
fn capture( fn capture(
&mut self, &mut self,
ident: &str, ident: &EcoString,
span: Span,
getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>,
) { ) {
if self.internal.get(ident).is_err() { if self.internal.get(ident).is_err() {
@ -538,7 +576,12 @@ impl<'a> CapturesVisitor<'a> {
return; return;
}; };
self.captures.define_captured(ident, value.clone(), self.capturer); self.captures.define_captured(
ident.clone(),
value.clone(),
self.capturer,
span,
);
} }
} }
} }
@ -561,7 +604,7 @@ mod tests {
visitor.visit(&root); visitor.visit(&root);
let captures = visitor.finish(); let captures = visitor.finish();
let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); let mut names: Vec<_> = captures.iter().map(|(k, ..)| k).collect();
names.sort(); names.sort();
assert_eq!(names, result); assert_eq!(names, result);

View File

@ -31,7 +31,7 @@ impl Eval for ast::ModuleImport<'_> {
} }
} }
if let Some(new_name) = &new_name { if let Some(new_name) = new_name {
if let ast::Expr::Ident(ident) = self.source() { if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() { if ident.as_str() == new_name.as_str() {
// Warn on `import x as x` // Warn on `import x as x`
@ -44,7 +44,7 @@ impl Eval for ast::ModuleImport<'_> {
} }
// Define renamed module on the scope. // Define renamed module on the scope.
vm.scopes.top.define(new_name.as_str(), source.clone()); vm.scopes.top.define_ident(new_name, source.clone());
} }
let scope = source.scope().unwrap(); let scope = source.scope().unwrap();
@ -57,8 +57,8 @@ impl Eval for ast::ModuleImport<'_> {
} }
} }
Some(ast::Imports::Wildcard) => { Some(ast::Imports::Wildcard) => {
for (var, value) in scope.iter() { for (var, value, span) in scope.iter() {
vm.scopes.top.define(var.clone(), value.clone()); vm.scopes.top.define_spanned(var.clone(), value.clone(), span);
} }
} }
Some(ast::Imports::Items(items)) => { Some(ast::Imports::Items(items)) => {

View File

@ -54,7 +54,11 @@ impl Eval for ast::MathAttach<'_> {
if let Some(expr) = self.top() { if let Some(expr) = self.top() {
elem.push_t(Some(expr.eval_display(vm)?)); elem.push_t(Some(expr.eval_display(vm)?));
} else if let Some(primes) = self.primes() { }
// Always attach primes in scripts style (not limits style),
// i.e. at the top-right corner.
if let Some(primes) = self.primes() {
elem.push_tr(Some(primes.eval(vm)?)); elem.push_tr(Some(primes.eval(vm)?));
} }

View File

@ -86,7 +86,7 @@ pub fn eval(
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy(); .to_string_lossy();
Ok(Module::new(name, vm.scopes.top).with_content(output)) Ok(Module::new(name, vm.scopes.top).with_content(output).with_file_id(id))
} }
/// Evaluate a string as code and return the resulting value. /// Evaluate a string as code and return the resulting value.

View File

@ -47,7 +47,7 @@ impl<'a> Vm<'a> {
if self.inspected == Some(var.span()) { if self.inspected == Some(var.span()) {
self.trace(value.clone()); self.trace(value.clone());
} }
self.scopes.top.define(var.get().clone(), value); self.scopes.top.define_ident(var, value);
} }
/// Trace a value. /// Trace a value.

View File

@ -76,6 +76,18 @@ impl Args {
self.items.iter().filter(|slot| slot.name.is_none()).count() self.items.iter().filter(|slot| slot.name.is_none()).count()
} }
/// Insert a positional argument at a specific index.
pub fn insert(&mut self, index: usize, span: Span, value: Value) {
self.items.insert(
index,
Arg {
span: self.span,
name: None,
value: Spanned::new(value, span),
},
)
}
/// Push a positional argument. /// Push a positional argument.
pub fn push(&mut self, span: Span, value: Value) { pub fn push(&mut self, span: Span, value: Value) {
self.items.push(Arg { self.items.push(Arg {

View File

@ -259,7 +259,7 @@ impl Array {
.ok_or_else(|| out_of_bounds_no_default(index, self.len())) .ok_or_else(|| out_of_bounds_no_default(index, self.len()))
} }
/// Extracts a subslice of the array. Fails with an error if the start or /// Extracts a subslice of the array. Fails with an error if the start or end
/// index is out of bounds. /// index is out of bounds.
#[func] #[func]
pub fn slice( pub fn slice(

View File

@ -127,7 +127,7 @@ impl Bytes {
.ok_or_else(|| out_of_bounds_no_default(index, self.len())) .ok_or_else(|| out_of_bounds_no_default(index, self.len()))
} }
/// Extracts a subslice of the bytes. Fails with an error if the start or /// Extracts a subslice of the bytes. Fails with an error if the start or end
/// index is out of bounds. /// index is out of bounds.
#[func] #[func]
pub fn slice( pub fn slice(

View File

@ -261,7 +261,7 @@ pub struct ToDict(Dict);
cast! { cast! {
ToDict, 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, v, _)| (Str::from(k.clone()), v.clone())).collect()),
} }
impl Debug for Dict { impl Debug for Dict {

View File

@ -290,7 +290,7 @@ pub fn eval(
let dict = scope; let dict = scope;
let mut scope = Scope::new(); let mut scope = Scope::new();
for (key, value) in dict { for (key, value) in dict {
scope.define(key, value); scope.define_spanned(key, value, span);
} }
crate::eval::eval_string(engine.world, &text, span, mode, scope) crate::eval::eval_string(engine.world, &text, span, mode, scope)
} }

View File

@ -5,6 +5,7 @@ use ecow::{eco_format, EcoString};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::foundations::{repr, ty, Content, Scope, Value}; use crate::foundations::{repr, ty, Content, Scope, Value};
use crate::syntax::FileId;
/// An evaluated module, either built-in or resulting from a file. /// An evaluated module, either built-in or resulting from a file.
/// ///
@ -43,6 +44,8 @@ struct Repr {
scope: Scope, scope: Scope,
/// The module's layoutable contents. /// The module's layoutable contents.
content: Content, content: Content,
/// The id of the file which defines the module, if any.
file_id: Option<FileId>,
} }
impl Module { impl Module {
@ -50,7 +53,7 @@ impl Module {
pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self { pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self {
Self { Self {
name: name.into(), name: name.into(),
inner: Arc::new(Repr { scope, content: Content::empty() }), inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
} }
} }
@ -72,6 +75,12 @@ impl Module {
self self
} }
/// Update the module's file id.
pub fn with_file_id(mut self, file_id: FileId) -> Self {
Arc::make_mut(&mut self.inner).file_id = Some(file_id);
self
}
/// Get the module's name. /// Get the module's name.
pub fn name(&self) -> &EcoString { pub fn name(&self) -> &EcoString {
&self.name &self.name
@ -82,6 +91,13 @@ impl Module {
&self.inner.scope &self.inner.scope
} }
/// Access the module's file id.
///
/// Some modules are not associated with a file, like the built-in modules.
pub fn file_id(&self) -> Option<FileId> {
self.inner.file_id
}
/// Access the module's scope, mutably. /// Access the module's scope, mutably.
pub fn scope_mut(&mut self) -> &mut Scope { pub fn scope_mut(&mut self) -> &mut Scope {
&mut Arc::make_mut(&mut self.inner).scope &mut Arc::make_mut(&mut self.inner).scope

View File

@ -234,12 +234,12 @@ impl Plugin {
let ty = func.ty(store.as_context()); let ty = func.ty(store.as_context());
// Check function signature. // Check function signature.
if ty.params().iter().any(|&v| v != wasmi::core::ValueType::I32) { if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) {
bail!( bail!(
"plugin function `{name}` has a parameter that is not a 32-bit integer" "plugin function `{name}` has a parameter that is not a 32-bit integer"
); );
} }
if ty.results() != [wasmi::core::ValueType::I32] { if ty.results() != [wasmi::core::ValType::I32] {
bail!("plugin function `{name}` does not return exactly one 32-bit integer"); bail!("plugin function `{name}` does not return exactly one 32-bit integer");
} }
@ -257,14 +257,14 @@ impl Plugin {
// Collect the lengths of the argument buffers. // Collect the lengths of the argument buffers.
let lengths = args let lengths = args
.iter() .iter()
.map(|a| wasmi::Value::I32(a.len() as i32)) .map(|a| wasmi::Val::I32(a.len() as i32))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Store the input data. // Store the input data.
store.data_mut().args = args; store.data_mut().args = args;
// Call the function. // Call the function.
let mut code = wasmi::Value::I32(-1); let mut code = wasmi::Val::I32(-1);
func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code))
.map_err(|err| eco_format!("plugin panicked: {err}"))?; .map_err(|err| eco_format!("plugin panicked: {err}"))?;
if let Some(MemoryError { offset, length, write }) = if let Some(MemoryError { offset, length, write }) =
@ -281,8 +281,8 @@ impl Plugin {
// Parse the functions return value. // Parse the functions return value.
match code { match code {
wasmi::Value::I32(0) => {} wasmi::Val::I32(0) => {}
wasmi::Value::I32(1) => match std::str::from_utf8(&output) { wasmi::Val::I32(1) => match std::str::from_utf8(&output) {
Ok(message) => bail!("plugin errored with: {message}"), Ok(message) => bail!("plugin errored with: {message}"),
Err(_) => { Err(_) => {
bail!("plugin errored, but did not return a valid error message") bail!("plugin errored, but did not return a valid error message")

View File

@ -9,6 +9,8 @@ use crate::foundations::{
Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData,
NativeType, Type, Value, NativeType, Type, Value,
}; };
use crate::syntax::ast::{self, AstNode};
use crate::syntax::Span;
use crate::utils::Static; use crate::utils::Static;
use crate::Library; use crate::Library;
@ -152,6 +154,23 @@ impl Scope {
/// Bind a value to a name. /// Bind a value to a name.
#[track_caller] #[track_caller]
pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { 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(); let name = name.into();
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -159,8 +178,24 @@ impl Scope {
panic!("duplicate definition: {name}"); panic!("duplicate definition: {name}");
} }
self.map self.map.insert(
.insert(name, Slot::new(value.into_value(), Kind::Normal, self.category)); 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. /// Define a native function through a Rust type that shadows the function.
@ -191,19 +226,6 @@ impl Scope {
self.define(module.name().clone(), module); self.define(module.name().clone(), module);
} }
/// Define a captured, immutable binding.
pub fn define_captured(
&mut self,
var: impl Into<EcoString>,
value: impl IntoValue,
capturer: Capturer,
) {
self.map.insert(
var.into(),
Slot::new(value.into_value(), Kind::Captured(capturer), self.category),
);
}
/// Try to access a variable immutably. /// Try to access a variable immutably.
pub fn get(&self, var: &str) -> Option<&Value> { pub fn get(&self, var: &str) -> Option<&Value> {
self.map.get(var).map(Slot::read) self.map.get(var).map(Slot::read)
@ -217,14 +239,19 @@ impl Scope {
.map(|res| res.map_err(HintedString::from)) .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. /// Get the category of a definition.
pub fn get_category(&self, var: &str) -> Option<Category> { pub fn get_category(&self, var: &str) -> Option<Category> {
self.map.get(var)?.category self.map.get(var)?.category
} }
/// Iterate over all definitions. /// Iterate over all definitions.
pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value)> { pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value, Span)> {
self.map.iter().map(|(k, v)| (k, v.read())) self.map.iter().map(|(k, v)| (k, v.read(), v.span))
} }
} }
@ -264,6 +291,8 @@ struct Slot {
value: Value, value: Value,
/// The kind of slot, determines how the value can be accessed. /// The kind of slot, determines how the value can be accessed.
kind: Kind, kind: Kind,
/// A span associated with the stored value.
span: Span,
/// The category of the slot. /// The category of the slot.
category: Option<Category>, category: Option<Category>,
} }
@ -288,8 +317,8 @@ pub enum Capturer {
impl Slot { impl Slot {
/// Create a new slot. /// Create a new slot.
fn new(value: Value, kind: Kind, category: Option<Category>) -> Self { fn new(value: Value, span: Span, kind: Kind, category: Option<Category>) -> Self {
Self { value, kind, category } Self { value, span, kind, category }
} }
/// Read the value. /// Read the value.

View File

@ -116,6 +116,6 @@ impl Tag {
impl Debug for Tag { impl Debug for Tag {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Tag({:?})", self.elem) write!(f, "Tag({:?})", self.elem.elem().name())
} }
} }

View File

@ -47,51 +47,7 @@ impl Packed<FlowElem> {
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
if !regions.size.x.is_finite() && regions.expand.x { FlowLayouter::new(engine, self, locator, &styles, regions).layout()
bail!(self.span(), "cannot expand into infinite width");
}
if !regions.size.y.is_finite() && regions.expand.y {
bail!(self.span(), "cannot expand into infinite height");
}
// Check whether we have just a single multiple-layoutable element. In
// that case, we do not set `expand.y` to `false`, but rather keep it at
// its original value (since that element can take the full space).
//
// Consider the following code: `block(height: 5cm, pad(10pt, align(bottom, ..)))`
// Thanks to the code below, the expansion will be passed all the way
// through the block & pad and reach the innermost flow, so that things
// are properly bottom-aligned.
let mut alone = false;
if let [child] = self.children().elements() {
alone = child.is::<BlockElem>();
}
let mut layouter = FlowLayouter::new(locator, styles, regions, alone);
for (child, styles) in self.children().chain(&styles) {
if let Some(elem) = child.to_packed::<TagElem>() {
layouter.layout_tag(elem);
} else if child.is::<FlushElem>() {
layouter.flush(engine)?;
} else if let Some(elem) = child.to_packed::<VElem>() {
layouter.layout_spacing(engine, elem, styles)?;
} else if let Some(elem) = child.to_packed::<ParElem>() {
layouter.layout_par(engine, elem, styles)?;
} else if let Some(elem) = child.to_packed::<BlockElem>() {
layouter.layout_block(engine, elem, styles)?;
} else if let Some(placed) = child.to_packed::<PlaceElem>() {
layouter.layout_placed(engine, placed, styles)?;
} else if child.is::<ColbreakElem>() {
if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
{
layouter.finish_region(engine, true)?;
}
} else {
bail!(child.span(), "unexpected flow child");
}
}
layouter.finish(engine)
} }
} }
@ -103,13 +59,17 @@ impl Debug for FlowElem {
} }
/// Performs flow layout. /// Performs flow layout.
struct FlowLayouter<'a> { struct FlowLayouter<'a, 'e> {
/// The engine.
engine: &'a mut Engine<'e>,
/// The children that will be arranged into a flow.
flow: &'a Packed<FlowElem>,
/// Whether this is the root flow. /// Whether this is the root flow.
root: bool, root: bool,
/// Provides unique locations to the flow's children. /// Provides unique locations to the flow's children.
locator: SplitLocator<'a>, locator: SplitLocator<'a>,
/// The shared styles. /// The shared styles.
styles: StyleChain<'a>, styles: &'a StyleChain<'a>,
/// The regions to layout children into. /// The regions to layout children into.
regions: Regions<'a>, regions: Regions<'a>,
/// Whether the flow should expand to fill the region. /// Whether the flow should expand to fill the region.
@ -124,7 +84,7 @@ struct FlowLayouter<'a> {
/// Spacing and layouted blocks for the current region. /// Spacing and layouted blocks for the current region.
items: Vec<FlowItem>, items: Vec<FlowItem>,
/// A queue of tags that will be attached to the next frame. /// A queue of tags that will be attached to the next frame.
pending_tags: Vec<Tag>, pending_tags: Vec<&'a Tag>,
/// A queue of floating elements. /// A queue of floating elements.
pending_floats: Vec<FlowItem>, pending_floats: Vec<FlowItem>,
/// Whether we have any footnotes in the current region. /// Whether we have any footnotes in the current region.
@ -157,18 +117,27 @@ enum FlowItem {
align: Axes<FixedAlignment>, align: Axes<FixedAlignment>,
/// Whether the frame sticks to the item after it (for orphan prevention). /// Whether the frame sticks to the item after it (for orphan prevention).
sticky: bool, sticky: bool,
/// Whether the frame is movable; that is, kept together with its footnotes. /// Whether the frame is movable; that is, kept together with its
/// footnotes.
/// ///
/// This is true for frames created by paragraphs and [`LayoutSingle`] elements. /// This is true for frames created by paragraphs and
/// [`BlockElem::single_layouter`] elements.
movable: bool, movable: bool,
}, },
/// An absolutely placed frame. /// An absolutely placed frame.
Placed { Placed {
/// The layouted content.
frame: Frame, frame: Frame,
/// Where to place the content horizontally.
x_align: FixedAlignment, x_align: FixedAlignment,
/// Where to place the content vertically.
y_align: Smart<Option<FixedAlignment>>, y_align: Smart<Option<FixedAlignment>>,
/// A translation to apply to the content.
delta: Axes<Rel<Abs>>, delta: Axes<Rel<Abs>>,
/// Whether the content floats --- i.e. collides with in-flow content.
float: bool, float: bool,
/// The amount of space that needs to be kept between the placed content
/// and in-flow content. Only relevant if `float` is `true`.
clearance: Abs, clearance: Abs,
}, },
/// A footnote frame (can also be the separator). /// A footnote frame (can also be the separator).
@ -193,24 +162,41 @@ impl FlowItem {
} }
} }
impl<'a> FlowLayouter<'a> { impl<'a, 'e> FlowLayouter<'a, 'e> {
/// Create a new flow layouter. /// Create a new flow layouter.
fn new( fn new(
engine: &'a mut Engine<'e>,
flow: &'a Packed<FlowElem>,
locator: Locator<'a>, locator: Locator<'a>,
styles: StyleChain<'a>, styles: &'a StyleChain<'a>,
mut regions: Regions<'a>, mut regions: Regions<'a>,
alone: bool,
) -> Self { ) -> Self {
let expand = regions.expand; // Check whether we have just a single multiple-layoutable element. In
let root = std::mem::replace(&mut regions.root, false); // that case, we do not set `expand.y` to `false`, but rather keep it at
// its original value (since that element can take the full space).
//
// Consider the following code: `block(height: 5cm, pad(10pt,
// align(bottom, ..)))`. Thanks to the code below, the expansion will be
// passed all the way through the block & pad and reach the innermost
// flow, so that things are properly bottom-aligned.
let mut alone = false;
if let [child] = flow.children.elements() {
alone = child.is::<BlockElem>();
}
// Disable vertical expansion when there are multiple or not directly // Disable vertical expansion when there are multiple or not directly
// layoutable children. // layoutable children.
let expand = regions.expand;
if !alone { if !alone {
regions.expand.y = false; regions.expand.y = false;
} }
// The children aren't root.
let root = std::mem::replace(&mut regions.root, false);
Self { Self {
engine,
flow,
root, root,
locator: locator.split(), locator: locator.split(),
styles, styles,
@ -223,52 +209,84 @@ impl<'a> FlowLayouter<'a> {
pending_floats: vec![], pending_floats: vec![],
has_footnotes: false, has_footnotes: false,
footnote_config: FootnoteConfig { footnote_config: FootnoteConfig {
separator: FootnoteEntry::separator_in(styles), separator: FootnoteEntry::separator_in(*styles),
clearance: FootnoteEntry::clearance_in(styles), clearance: FootnoteEntry::clearance_in(*styles),
gap: FootnoteEntry::gap_in(styles), gap: FootnoteEntry::gap_in(*styles),
}, },
finished: vec![], finished: vec![],
} }
} }
/// Layout the flow.
fn layout(mut self) -> SourceResult<Fragment> {
for (child, styles) in self.flow.children.chain(self.styles) {
if let Some(elem) = child.to_packed::<TagElem>() {
self.handle_tag(elem);
} else if let Some(elem) = child.to_packed::<VElem>() {
self.handle_v(elem, styles)?;
} else if let Some(elem) = child.to_packed::<ColbreakElem>() {
self.handle_colbreak(elem)?;
} else if let Some(elem) = child.to_packed::<ParElem>() {
self.handle_par(elem, styles)?;
} else if let Some(elem) = child.to_packed::<BlockElem>() {
self.handle_block(elem, styles)?;
} else if let Some(elem) = child.to_packed::<PlaceElem>() {
self.handle_place(elem, styles)?;
} else if let Some(elem) = child.to_packed::<FlushElem>() {
self.handle_flush(elem)?;
} else {
bail!(child.span(), "unexpected flow child");
}
}
self.finish()
}
/// Place explicit metadata into the flow. /// Place explicit metadata into the flow.
fn layout_tag(&mut self, elem: &Packed<TagElem>) { fn handle_tag(&mut self, elem: &'a Packed<TagElem>) {
self.pending_tags.push(elem.tag.clone()); self.pending_tags.push(&elem.tag);
} }
/// Layout vertical spacing. /// Layout vertical spacing.
fn layout_spacing( fn handle_v(&mut self, v: &'a Packed<VElem>, styles: StyleChain) -> SourceResult<()> {
&mut self, self.handle_item(match v.amount {
engine: &mut Engine,
v: &Packed<VElem>,
styles: StyleChain,
) -> SourceResult<()> {
self.layout_item(
engine,
match v.amount() {
Spacing::Rel(rel) => FlowItem::Absolute( Spacing::Rel(rel) => FlowItem::Absolute(
// Resolve the spacing relative to the current base height.
rel.resolve(styles).relative_to(self.initial.y), rel.resolve(styles).relative_to(self.initial.y),
v.weakness(styles) > 0, v.weakness(styles) > 0,
), ),
Spacing::Fr(fr) => FlowItem::Fractional(*fr), Spacing::Fr(fr) => FlowItem::Fractional(fr),
}, })
) }
/// Layout a column break.
fn handle_colbreak(&mut self, _: &'a Packed<ColbreakElem>) -> SourceResult<()> {
// If there is still an available region, skip to it.
// TODO: Turn this into a region abstraction.
if !self.regions.backlog.is_empty() || self.regions.last.is_some() {
self.finish_region(true)?;
}
Ok(())
} }
/// Layout a paragraph. /// Layout a paragraph.
fn layout_par( fn handle_par(
&mut self, &mut self,
engine: &mut Engine, par: &'a Packed<ParElem>,
par: &Packed<ParElem>,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Fetch properties.
let align = AlignElem::alignment_in(styles).resolve(styles); let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles); let leading = ParElem::leading_in(styles);
// Layout the paragraph into lines. This only depends on the base size,
// not on the Y position.
let consecutive = self.last_was_par; let consecutive = self.last_was_par;
let locator = self.locator.next(&par.span());
let lines = par let lines = par
.layout( .layout(
engine, self.engine,
self.locator.next(&par.span()), locator,
styles, styles,
consecutive, consecutive,
self.regions.base(), self.regions.base(),
@ -280,39 +298,26 @@ impl<'a> FlowLayouter<'a> {
// previous sticky frame to the next region (if available) // previous sticky frame to the next region (if available)
if let Some(first) = lines.first() { if let Some(first) = lines.first() {
while !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { while !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
let mut sticky = self.items.len(); let in_last = self.finish_region_with_migration()?;
for (i, item) in self.items.iter().enumerate().rev() {
match *item {
FlowItem::Absolute(_, _) => {}
FlowItem::Frame { sticky: true, .. } => sticky = i,
_ => break,
}
}
let carry: Vec<_> = self.items.drain(sticky..).collect();
self.finish_region(engine, false)?;
let in_last = self.regions.in_last();
for item in carry {
self.layout_item(engine, item)?;
}
if in_last { if in_last {
break; break;
} }
} }
} }
// Layout the lines.
for (i, mut frame) in lines.into_iter().enumerate() { for (i, mut frame) in lines.into_iter().enumerate() {
if i > 0 { if i > 0 {
self.layout_item(engine, FlowItem::Absolute(leading, true))?; self.handle_item(FlowItem::Absolute(leading, true))?;
} }
self.drain_tag(&mut frame); self.drain_tag(&mut frame);
self.layout_item( self.handle_item(FlowItem::Frame {
engine, frame,
FlowItem::Frame { frame, align, sticky: false, movable: true }, align,
)?; sticky: false,
movable: true,
})?;
} }
self.last_was_par = true; self.last_was_par = true;
@ -320,56 +325,54 @@ impl<'a> FlowLayouter<'a> {
} }
/// Layout into multiple regions. /// Layout into multiple regions.
fn layout_block( fn handle_block(
&mut self, &mut self,
engine: &mut Engine,
block: &'a Packed<BlockElem>, block: &'a Packed<BlockElem>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Temporarily delegate rootness to the columns. // Fetch properties.
let sticky = block.sticky(styles);
let align = AlignElem::alignment_in(styles).resolve(styles);
// If the block is "rootable" it may host footnotes. In that case, we
// defer rootness to it temporarily. We disable our own rootness to
// prevent duplicate footnotes.
let is_root = self.root; let is_root = self.root;
if is_root && block.rootable(styles) { if is_root && block.rootable(styles) {
self.root = false; self.root = false;
self.regions.root = true; self.regions.root = true;
} }
if self.regions.is_full() {
// Skip directly if region is already full. // Skip directly if region is already full.
self.finish_region(engine, false)?; if self.regions.is_full() {
self.finish_region(false)?;
} }
// Layout the block itself. // Layout the block itself.
let sticky = block.sticky(styles);
let fragment = block.layout( let fragment = block.layout(
engine, self.engine,
self.locator.next(&block.span()), self.locator.next(&block.span()),
styles, styles,
self.regions, self.regions,
)?; )?;
// How to align the block.
let align = AlignElem::alignment_in(styles).resolve(styles);
let mut notes = Vec::new(); let mut notes = Vec::new();
for (i, mut frame) in fragment.into_iter().enumerate() { for (i, mut frame) in fragment.into_iter().enumerate() {
// Find footnotes in the frame. // Find footnotes in the frame.
if self.root { if self.root {
find_footnotes(&mut notes, &frame); collect_footnotes(&mut notes, &frame);
} }
if i > 0 { if i > 0 {
self.finish_region(engine, false)?; self.finish_region(false)?;
} }
self.drain_tag(&mut frame); self.drain_tag(&mut frame);
frame.post_process(styles); frame.post_process(styles);
self.layout_item( self.handle_item(FlowItem::Frame { frame, align, sticky, movable: false })?;
engine,
FlowItem::Frame { frame, align, sticky, movable: false },
)?;
} }
self.try_handle_footnotes(engine, notes)?; self.try_handle_footnotes(notes)?;
self.root = is_root; self.root = is_root;
self.regions.root = false; self.regions.root = false;
@ -379,50 +382,56 @@ impl<'a> FlowLayouter<'a> {
} }
/// Layout a placed element. /// Layout a placed element.
fn layout_placed( fn handle_place(
&mut self, &mut self,
engine: &mut Engine, placed: &'a Packed<PlaceElem>,
placed: &Packed<PlaceElem>,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Fetch properties.
let float = placed.float(styles); let float = placed.float(styles);
let clearance = placed.clearance(styles); let clearance = placed.clearance(styles);
let alignment = placed.alignment(styles); let alignment = placed.alignment(styles);
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles); let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
let x_align = alignment.map_or(FixedAlignment::Center, |align| { let x_align = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles) align.x().unwrap_or_default().resolve(styles)
}); });
let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles))); let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let mut frame = placed let mut frame = placed
.layout( .layout(
engine, self.engine,
self.locator.next(&placed.span()), self.locator.next(&placed.span()),
styles, styles,
self.regions.base(), self.regions.base(),
)? )?
.into_frame(); .into_frame();
frame.post_process(styles); frame.post_process(styles);
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
self.layout_item(engine, item) self.handle_item(FlowItem::Placed {
frame,
x_align,
y_align,
delta,
float,
clearance,
})
} }
/// Attach currently pending metadata to the frame. /// Lays out all floating elements before continuing with other content.
fn drain_tag(&mut self, frame: &mut Frame) { fn handle_flush(&mut self, _: &'a Packed<FlushElem>) -> SourceResult<()> {
if !self.pending_tags.is_empty() && !frame.is_empty() { for item in std::mem::take(&mut self.pending_floats) {
frame.prepend_multiple( self.handle_item(item)?;
self.pending_tags
.drain(..)
.map(|tag| (Point::zero(), FrameItem::Tag(tag))),
);
} }
while !self.pending_floats.is_empty() {
self.finish_region(false)?;
}
Ok(())
} }
/// Layout a finished frame. /// Layout a finished frame.
fn layout_item( fn handle_item(&mut self, mut item: FlowItem) -> SourceResult<()> {
&mut self,
engine: &mut Engine,
mut item: FlowItem,
) -> SourceResult<()> {
match item { match item {
FlowItem::Absolute(v, weak) => { FlowItem::Absolute(v, weak) => {
if weak if weak
@ -439,24 +448,24 @@ impl<'a> FlowLayouter<'a> {
FlowItem::Frame { ref frame, movable, .. } => { FlowItem::Frame { ref frame, movable, .. } => {
let height = frame.height(); let height = frame.height();
while !self.regions.size.y.fits(height) && !self.regions.in_last() { while !self.regions.size.y.fits(height) && !self.regions.in_last() {
self.finish_region(engine, false)?; self.finish_region(false)?;
} }
let in_last = self.regions.in_last(); let in_last = self.regions.in_last();
self.regions.size.y -= height; self.regions.size.y -= height;
if self.root && movable { if self.root && movable {
let mut notes = Vec::new(); let mut notes = Vec::new();
find_footnotes(&mut notes, frame); collect_footnotes(&mut notes, frame);
self.items.push(item); self.items.push(item);
// When we are already in_last, we can directly force the // When we are already in_last, we can directly force the
// footnotes. // footnotes.
if !self.handle_footnotes(engine, &mut notes, true, in_last)? { if !self.handle_footnotes(&mut notes, true, in_last)? {
let item = self.items.pop(); let item = self.items.pop();
self.finish_region(engine, false)?; self.finish_region(false)?;
self.items.extend(item); self.items.extend(item);
self.regions.size.y -= height; self.regions.size.y -= height;
self.handle_footnotes(engine, &mut notes, true, true)?; self.handle_footnotes(&mut notes, true, true)?;
} }
return Ok(()); return Ok(());
} }
@ -504,8 +513,8 @@ impl<'a> FlowLayouter<'a> {
// Find footnotes in the frame. // Find footnotes in the frame.
if self.root { if self.root {
let mut notes = vec![]; let mut notes = vec![];
find_footnotes(&mut notes, frame); collect_footnotes(&mut notes, frame);
self.try_handle_footnotes(engine, notes)?; self.try_handle_footnotes(notes)?;
} }
} }
FlowItem::Footnote(_) => {} FlowItem::Footnote(_) => {}
@ -515,12 +524,49 @@ impl<'a> FlowLayouter<'a> {
Ok(()) Ok(())
} }
/// Attach currently pending metadata to the frame.
fn drain_tag(&mut self, frame: &mut Frame) {
if !self.pending_tags.is_empty() && !frame.is_empty() {
frame.prepend_multiple(
self.pending_tags
.drain(..)
.map(|tag| (Point::zero(), FrameItem::Tag(tag.clone()))),
);
}
}
/// Finisht the region, migrating all sticky items to the next one.
///
/// Returns whether we migrated into a last region.
fn finish_region_with_migration(&mut self) -> SourceResult<bool> {
// Find the suffix of sticky items.
let mut sticky = self.items.len();
for (i, item) in self.items.iter().enumerate().rev() {
match *item {
FlowItem::Absolute(_, _) => {}
FlowItem::Frame { sticky: true, .. } => sticky = i,
_ => break,
}
}
let carry: Vec<_> = self.items.drain(sticky..).collect();
self.finish_region(false)?;
let in_last = self.regions.in_last();
for item in carry {
self.handle_item(item)?;
}
Ok(in_last)
}
/// Finish the frame for one region. /// Finish the frame for one region.
/// ///
/// Set `force` to `true` to allow creating a frame for out-of-flow elements /// Set `force` to `true` to allow creating a frame for out-of-flow elements
/// only (this is used to force the creation of a frame in case the /// only (this is used to force the creation of a frame in case the
/// remaining elements are all out-of-flow). /// remaining elements are all out-of-flow).
fn finish_region(&mut self, engine: &mut Engine, force: bool) -> SourceResult<()> { fn finish_region(&mut self, force: bool) -> SourceResult<()> {
// Early return if we don't have any relevant items.
if !force if !force
&& !self.items.is_empty() && !self.items.is_empty()
&& self.items.iter().all(FlowItem::is_out_of_flow) && self.items.iter().all(FlowItem::is_out_of_flow)
@ -585,6 +631,13 @@ impl<'a> FlowLayouter<'a> {
size.y = self.initial.y; size.y = self.initial.y;
} }
if !self.regions.size.x.is_finite() && self.expand.x {
bail!(self.flow.span(), "cannot expand into infinite width");
}
if !self.regions.size.y.is_finite() && self.expand.y {
bail!(self.flow.span(), "cannot expand into infinite height");
}
let mut output = Frame::soft(size); let mut output = Frame::soft(size);
let mut ruler = FixedAlignment::Start; let mut ruler = FixedAlignment::Start;
let mut float_top_offset = Abs::zero(); let mut float_top_offset = Abs::zero();
@ -653,7 +706,9 @@ impl<'a> FlowLayouter<'a> {
if force && !self.pending_tags.is_empty() { if force && !self.pending_tags.is_empty() {
let pos = Point::with_y(offset); let pos = Point::with_y(offset);
output.push_multiple( output.push_multiple(
self.pending_tags.drain(..).map(|tag| (pos, FrameItem::Tag(tag))), self.pending_tags
.drain(..)
.map(|tag| (pos, FrameItem::Tag(tag.clone()))),
); );
} }
@ -665,62 +720,42 @@ impl<'a> FlowLayouter<'a> {
// Try to place floats into the next region. // Try to place floats into the next region.
for item in std::mem::take(&mut self.pending_floats) { for item in std::mem::take(&mut self.pending_floats) {
self.layout_item(engine, item)?; self.handle_item(item)?;
}
Ok(())
}
/// Lays out all floating elements before continuing with other content.
fn flush(&mut self, engine: &mut Engine) -> SourceResult<()> {
for item in std::mem::take(&mut self.pending_floats) {
self.layout_item(engine, item)?;
}
while !self.pending_floats.is_empty() {
self.finish_region(engine, false)?;
} }
Ok(()) Ok(())
} }
/// Finish layouting and return the resulting fragment. /// Finish layouting and return the resulting fragment.
fn finish(mut self, engine: &mut Engine) -> SourceResult<Fragment> { fn finish(mut self) -> SourceResult<Fragment> {
if self.expand.y { if self.expand.y {
while !self.regions.backlog.is_empty() { while !self.regions.backlog.is_empty() {
self.finish_region(engine, true)?; self.finish_region(true)?;
} }
} }
self.finish_region(engine, true)?; self.finish_region(true)?;
while !self.items.is_empty() { while !self.items.is_empty() {
self.finish_region(engine, true)?; self.finish_region(true)?;
} }
Ok(Fragment::frames(self.finished)) Ok(Fragment::frames(self.finished))
} }
}
impl FlowLayouter<'_> {
/// Tries to process all footnotes in the frame, placing them /// Tries to process all footnotes in the frame, placing them
/// in the next region if they could not be placed in the current /// in the next region if they could not be placed in the current
/// one. /// one.
fn try_handle_footnotes( fn try_handle_footnotes(
&mut self, &mut self,
engine: &mut Engine,
mut notes: Vec<Packed<FootnoteElem>>, mut notes: Vec<Packed<FootnoteElem>>,
) -> SourceResult<()> { ) -> SourceResult<()> {
// When we are already in_last, we can directly force the // When we are already in_last, we can directly force the
// footnotes. // footnotes.
if self.root if self.root
&& !self.handle_footnotes( && !self.handle_footnotes(&mut notes, false, self.regions.in_last())?
engine,
&mut notes,
false,
self.regions.in_last(),
)?
{ {
self.finish_region(engine, false)?; self.finish_region(false)?;
self.handle_footnotes(engine, &mut notes, false, true)?; self.handle_footnotes(&mut notes, false, true)?;
} }
Ok(()) Ok(())
} }
@ -731,7 +766,6 @@ impl FlowLayouter<'_> {
/// regions. /// regions.
fn handle_footnotes( fn handle_footnotes(
&mut self, &mut self,
engine: &mut Engine,
notes: &mut Vec<Packed<FootnoteElem>>, notes: &mut Vec<Packed<FootnoteElem>>,
movable: bool, movable: bool,
force: bool, force: bool,
@ -750,16 +784,16 @@ impl FlowLayouter<'_> {
} }
if !self.has_footnotes { if !self.has_footnotes {
self.layout_footnote_separator(engine)?; self.layout_footnote_separator()?;
} }
self.regions.size.y -= self.footnote_config.gap; self.regions.size.y -= self.footnote_config.gap;
let frames = FootnoteEntry::new(notes[k].clone()) let frames = FootnoteEntry::new(notes[k].clone())
.pack() .pack()
.layout( .layout(
engine, self.engine,
Locator::synthesize(notes[k].location().unwrap()), Locator::synthesize(notes[k].location().unwrap()),
self.styles, *self.styles,
self.regions.with_root(false), self.regions.with_root(false),
)? )?
.into_frames(); .into_frames();
@ -780,10 +814,10 @@ impl FlowLayouter<'_> {
let prev = notes.len(); let prev = notes.len();
for (i, frame) in frames.into_iter().enumerate() { for (i, frame) in frames.into_iter().enumerate() {
find_footnotes(notes, &frame); collect_footnotes(notes, &frame);
if i > 0 { if i > 0 {
self.finish_region(engine, false)?; self.finish_region(false)?;
self.layout_footnote_separator(engine)?; self.layout_footnote_separator()?;
self.regions.size.y -= self.footnote_config.gap; self.regions.size.y -= self.footnote_config.gap;
} }
self.regions.size.y -= frame.height(); self.regions.size.y -= frame.height();
@ -804,14 +838,14 @@ impl FlowLayouter<'_> {
} }
/// Layout and save the footnote separator, typically a line. /// Layout and save the footnote separator, typically a line.
fn layout_footnote_separator(&mut self, engine: &mut Engine) -> SourceResult<()> { fn layout_footnote_separator(&mut self) -> SourceResult<()> {
let expand = Axes::new(self.regions.expand.x, false); let expand = Axes::new(self.regions.expand.x, false);
let pod = Regions::one(self.regions.base(), expand); let pod = Regions::one(self.regions.base(), expand);
let separator = &self.footnote_config.separator; let separator = &self.footnote_config.separator;
// FIXME: Shouldn't use `root()` here. // FIXME: Shouldn't use `root()` here.
let mut frame = separator let mut frame = separator
.layout(engine, Locator::root(), self.styles, pod)? .layout(self.engine, Locator::root(), *self.styles, pod)?
.into_frame(); .into_frame();
frame.size_mut().y += self.footnote_config.clearance; frame.size_mut().y += self.footnote_config.clearance;
frame.translate(Point::with_y(self.footnote_config.clearance)); frame.translate(Point::with_y(self.footnote_config.clearance));
@ -824,11 +858,11 @@ impl FlowLayouter<'_> {
} }
} }
/// Finds all footnotes in the frame. /// Collect all footnotes in a frame.
fn find_footnotes(notes: &mut Vec<Packed<FootnoteElem>>, frame: &Frame) { fn collect_footnotes(notes: &mut Vec<Packed<FootnoteElem>>, frame: &Frame) {
for (_, item) in frame.items() { for (_, item) in frame.items() {
match item { match item {
FrameItem::Group(group) => find_footnotes(notes, &group.frame), FrameItem::Group(group) => collect_footnotes(notes, &group.frame),
FrameItem::Tag(tag) FrameItem::Tag(tag)
if !notes.iter().any(|note| note.location() == tag.elem.location()) => if !notes.iter().any(|note| note.location() == tag.elem.location()) =>
{ {

View File

@ -79,7 +79,7 @@ impl<'a> Item<'a> {
} }
/// The natural layouted width of the item. /// The natural layouted width of the item.
pub fn width(&self) -> Abs { pub fn natural_width(&self) -> Abs {
match self { match self {
Self::Text(shaped) => shaped.width, Self::Text(shaped) => shaped.width,
Self::Absolute(v, _) => *v, Self::Absolute(v, _) => *v,
@ -201,7 +201,7 @@ pub fn collect<'a>(
); );
let peeked = iter.peek().and_then(|(child, _)| { let peeked = iter.peek().and_then(|(child, _)| {
if let Some(elem) = child.to_packed::<TextElem>() { if let Some(elem) = child.to_packed::<TextElem>() {
elem.text().chars().next() elem.text().chars().find(|c| !is_default_ignorable(*c))
} else if child.is::<SmartQuoteElem>() { } else if child.is::<SmartQuoteElem>() {
Some('"') Some('"')
} else if child.is::<SpaceElem>() } else if child.is::<SpaceElem>()
@ -302,7 +302,7 @@ impl<'a> Collector<'a> {
} }
fn push_segment(&mut self, segment: Segment<'a>, is_quote: bool) { fn push_segment(&mut self, segment: Segment<'a>, is_quote: bool) {
if let Some(last) = self.full.chars().last() { if let Some(last) = self.full.chars().rev().find(|c| !is_default_ignorable(*c)) {
self.quoter.last(last, is_quote); self.quoter.last(last, is_quote);
} }

View File

@ -1,11 +1,18 @@
use unicode_bidi::BidiInfo; use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use super::*; use super::*;
use crate::engine::Engine; use crate::engine::Engine;
use crate::layout::{Abs, Em, Fr, Frame, FrameItem, Point}; use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use crate::text::TextElem; use crate::text::{Lang, TextElem};
use crate::utils::Numeric; use crate::utils::Numeric;
const SHY: char = '\u{ad}';
const HYPHEN: char = '-';
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 /// A layouted line, consisting of a sequence of layouted paragraph items that
/// are mostly borrowed from the preparation phase. This type enables you to /// 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 /// measure the size of a line in a range before committing to building the
@ -16,20 +23,9 @@ use crate::utils::Numeric;
/// line, respectively. But even those can partially reuse previous results when /// line, respectively. But even those can partially reuse previous results when
/// the break index is safe-to-break per rustybuzz. /// the break index is safe-to-break per rustybuzz.
pub struct Line<'a> { pub struct Line<'a> {
/// Bidi information about the paragraph. /// The items the line is made of.
pub bidi: &'a BidiInfo<'a>, pub items: Items<'a>,
/// The trimmed range the line spans in the paragraph. /// The exact natural width of the line.
pub trimmed: Range,
/// The untrimmed end where the line ends.
pub end: usize,
/// A reshaped text item if the line sliced up a text item at the start.
pub first: Option<Item<'a>>,
/// Inner items which don't need to be reprocessed.
pub inner: &'a [Item<'a>],
/// A reshaped text item if the line sliced up a text item at the end. If
/// there is only one text item, this takes precedence over `first`.
pub last: Option<Item<'a>>,
/// The width of the line.
pub width: Abs, pub width: Abs,
/// Whether the line should be justified. /// Whether the line should be justified.
pub justify: bool, pub justify: bool,
@ -39,45 +35,27 @@ pub struct Line<'a> {
} }
impl<'a> Line<'a> { impl<'a> Line<'a> {
/// Iterate over the line's items. /// Create an empty line.
pub fn items(&self) -> impl Iterator<Item = &Item<'a>> { pub fn empty() -> Self {
self.first.iter().chain(self.inner).chain(&self.last) Self {
items: Items::new(),
width: Abs::zero(),
justify: false,
dash: None,
} }
/// Return items that intersect the given `text_range`.
pub fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
let mut cursor = self.trimmed.start;
let mut start = 0;
let mut end = 0;
for (i, item) in self.items().enumerate() {
if cursor <= text_range.start {
start = i;
}
let len = item.textual_len();
if cursor < text_range.end || cursor + len <= text_range.end {
end = i + 1;
} else {
break;
}
cursor += len;
}
self.items().skip(start).take(end - start)
} }
/// How many glyphs are in the text where we can insert additional /// How many glyphs are in the text where we can insert additional
/// space when encountering underfull lines. /// space when encountering underfull lines.
pub fn justifiables(&self) -> usize { pub fn justifiables(&self) -> usize {
let mut count = 0; let mut count = 0;
for shaped in self.items().filter_map(Item::text) { for shaped in self.items.iter().filter_map(Item::text) {
count += shaped.justifiables(); count += shaped.justifiables();
} }
// CJK character at line end should not be adjusted. // CJK character at line end should not be adjusted.
if self if self
.items() .items
.last() .last()
.and_then(Item::text) .and_then(Item::text)
.map(|s| s.cjk_justifiable_at_last()) .map(|s| s.cjk_justifiable_at_last())
@ -89,19 +67,37 @@ impl<'a> Line<'a> {
count count
} }
/// How much can the line stretch /// How much the line can stretch.
pub fn stretchability(&self) -> Abs { pub fn stretchability(&self) -> Abs {
self.items().filter_map(Item::text).map(|s| s.stretchability()).sum() self.items
.iter()
.filter_map(Item::text)
.map(|s| s.stretchability())
.sum()
} }
/// How much can the line shrink /// How much the line can shrink.
pub fn shrinkability(&self) -> Abs { pub fn shrinkability(&self) -> Abs {
self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum() self.items
.iter()
.filter_map(Item::text)
.map(|s| s.shrinkability())
.sum()
}
/// Whether the line has items with negative width.
pub fn has_negative_width_items(&self) -> bool {
self.items.iter().any(|item| match item {
Item::Absolute(amount, _) => *amount < Abs::zero(),
Item::Frame(frame, _) => frame.width() < Abs::zero(),
_ => false,
})
} }
/// The sum of fractions in the line. /// The sum of fractions in the line.
pub fn fr(&self) -> Fr { pub fn fr(&self) -> Fr {
self.items() self.items
.iter()
.filter_map(|item| match item { .filter_map(|item| match item {
Item::Fractional(fr, _) => Some(*fr), Item::Fractional(fr, _) => Some(*fr),
_ => None, _ => None,
@ -113,232 +109,299 @@ impl<'a> Line<'a> {
/// A dash at the end of a line. /// A dash at the end of a line.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Dash { pub enum Dash {
/// A hyphen added to break a word. /// A soft hyphen added to break a word.
SoftHyphen, Soft,
/// Regular hyphen, present in a compound word, e.g. beija-flor. /// A regular hyphen, present in a compound word, e.g. beija-flor.
HardHyphen, Hard,
/// An em dash. /// Another kind of dash. Only relevant for cost computation.
Long, Other,
/// An en dash.
Short,
} }
/// Create a line which spans the given range. /// Create a line which spans the given range.
pub fn line<'a>( pub fn line<'a>(
engine: &Engine, engine: &Engine,
p: &'a Preparation, p: &'a Preparation,
mut range: Range, range: Range,
breakpoint: Breakpoint, breakpoint: Breakpoint,
prepend_hyphen: bool, pred: Option<&Line>,
) -> Line<'a> { ) -> Line<'a> {
let end = range.end; // The line's full text.
let mut justify = let full = &p.text[range.clone()];
p.justify && end < p.bidi.text.len() && breakpoint != Breakpoint::Mandatory;
if range.is_empty() { // Whether the line is justified.
return Line { let justify = full.ends_with(LINE_SEPARATOR)
bidi: &p.bidi, || (p.justify && breakpoint != Breakpoint::Mandatory);
end,
trimmed: range,
first: None,
inner: &[],
last: None,
width: Abs::zero(),
justify,
dash: None,
};
}
// Slice out the relevant items. // Process dashes.
let (mut expanded, mut inner) = p.slice(range.clone()); let dash = if breakpoint == Breakpoint::Hyphen || full.ends_with(SHY) {
let mut width = Abs::zero(); Some(Dash::Soft)
} else if full.ends_with(HYPHEN) {
// Weak space (`Absolute(_, true)`) is removed at the end of the line Some(Dash::Hard)
while let Some((Item::Absolute(_, true), before)) = inner.split_last() { } else if full.ends_with([EN_DASH, EM_DASH]) {
inner = before; Some(Dash::Other)
range.end -= 1;
expanded.end -= 1;
}
// Weak space (`Absolute(_, true)`) is removed at the beginning of the line
while let Some((Item::Absolute(_, true), after)) = inner.split_first() {
inner = after;
range.start += 1;
expanded.end += 1;
}
// Reshape the last item if it's split in half or hyphenated.
let mut last = None;
let mut dash = None;
if let Some((Item::Text(shaped), before)) = inner.split_last() {
// Compute the range we want to shape, trimming whitespace at the
// end of the line.
let base = expanded.end - shaped.text.len();
let start = range.start.max(base);
let text = &p.bidi.text[start..range.end];
// U+200B ZERO WIDTH SPACE is used to provide a line break opportunity,
// we want to trim it too.
let trimmed = text.trim_end().trim_end_matches('\u{200B}');
range.end = start + trimmed.len();
// Deal with hyphens, dashes and justification.
let shy = trimmed.ends_with('\u{ad}');
let hyphen = breakpoint == Breakpoint::Hyphen;
dash = if hyphen || shy {
Some(Dash::SoftHyphen)
} else if trimmed.ends_with('-') {
Some(Dash::HardHyphen)
} else if trimmed.ends_with('') {
Some(Dash::Short)
} else if trimmed.ends_with('—') {
Some(Dash::Long)
} else { } else {
None None
}; };
justify |= text.ends_with('\u{2028}');
// Deal with CJK punctuation at line ends. // Trim the line at the end, if necessary for this breakpoint.
let gb_style = cjk_punct_style(shaped.lang, shaped.region); let trim = range.start + breakpoint.trim(full).len();
let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT)
|| (p.cjk_latin_spacing && trimmed.ends_with(is_of_cj_script));
// Usually, we don't want to shape an empty string because: // Collect the items for the line.
// - We don't want the height of trimmed whitespace in a different font let mut items = collect_items(engine, p, range, trim);
// to be considered for the line height.
// - Even if it's in the same font, its unnecessary. // 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)) {
// There is one exception though. When the whole line is empty, we need if let Some(shaped) = items.first_text_mut() {
// the shaped empty string to make the line the appropriate height. That shaped.prepend_hyphen(engine, p.fallback);
// is the case exactly if the string is empty and there are no other }
// items in the line. }
if hyphen
|| start + shaped.text.len() > range.end // Add a hyphen at the line end, if we ended on a soft hyphen.
|| maybe_adjust_last_glyph if dash == Some(Dash::Soft) {
|| prepend_hyphen if let Some(shaped) = items.last_text_mut() {
shaped.push_hyphen(engine, p.fallback);
}
}
// Deal with CJ characters at line boundaries.
adjust_cj_at_line_boundaries(p, full, &mut items);
// Compute the line's width.
let width = items.iter().map(Item::natural_width).sum();
Line { items, width, justify, dash }
}
/// Collects / reshapes all items for the line with the given `range`.
///
/// The `trim` defines an end position to which text items are trimmed. For
/// example, the `range` may span "hello\n", but the `trim` specifies that the
/// linebreak is trimmed.
///
/// We do not factor the `trim` diredctly into the `range` because we still want
/// to keep non-text items after the trim (e.g. tags).
fn collect_items<'a>(
engine: &Engine,
p: &'a Preparation,
range: Range,
trim: usize,
) -> Items<'a> {
let mut items = Items::new();
let mut fallback = None;
// Collect the items for each consecutively ordered run.
reorder(p, range.clone(), |subrange, rtl| {
let from = items.len();
collect_range(engine, p, subrange, trim, &mut items, &mut fallback);
if rtl {
items.reorder(from);
}
});
// Trim weak spacing at the start of the line.
let prefix = items
.iter()
.take_while(|item| matches!(item, Item::Absolute(_, true)))
.count();
if prefix > 0 {
items.drain(..prefix);
}
// Trim weak spacing at the end of the line.
while matches!(items.last(), Some(Item::Absolute(_, true))) {
items.pop();
}
// Add fallback text to expand the line height, if necessary.
if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback {
items.push(fallback);
}
}
items
}
/// Calls `f` for the the BiDi-reordered ranges of a line.
fn reorder<F>(p: &Preparation, range: Range, mut f: F)
where
F: FnMut(Range, bool),
{ {
if hyphen || start < range.end || before.is_empty() { // If there is nothing bidirectional going on, skip reordering.
let mut reshaped = shaped.reshape(engine, &p.spans, start..range.end); let Some(bidi) = &p.bidi else {
if hyphen || shy { f(range, p.dir == Dir::RTL);
reshaped.push_hyphen(engine, p.fallback); return;
};
// The bidi crate panics for empty lines.
if range.is_empty() {
f(range, p.dir == Dir::RTL);
return;
} }
if let Some(last_glyph) = reshaped.glyphs.last() { // Find the paragraph that contains the line.
if last_glyph.is_cjk_left_aligned_punctuation(gb_style) { let para = bidi
// If the last glyph is a CJK punctuation, we want to .paragraphs
// shrink it. See Requirements for Chinese Text Layout, .iter()
// Section 3.1.6.3 Compression of punctuation marks at .find(|para| para.range.contains(&range.start))
// line start or line end .unwrap();
let shrink_amount = last_glyph.shrinkability().1;
let punct = reshaped.glyphs.to_mut().last_mut().unwrap(); // Compute the reordered ranges in visual order (left to right).
punct.shrink_right(shrink_amount); let (levels, runs) = bidi.visual_runs(para, range.clone());
reshaped.width -= shrink_amount.at(reshaped.size);
} else if p.cjk_latin_spacing // Call `f` for each run.
&& last_glyph.is_cj_script() for run in runs {
&& (last_glyph.x_advance - last_glyph.x_offset) > Em::one() let rtl = levels[run.start].is_rtl();
f(run, rtl)
}
}
/// Collects / reshapes all items for the given `subrange` with continous
/// direction.
fn collect_range<'a>(
engine: &Engine,
p: &'a Preparation,
range: Range,
trim: usize,
items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>,
) {
for (subrange, item) in p.slice(range.clone()) {
// All non-text items are just kept, they can't be split.
let Item::Text(shaped) = item else {
items.push(item);
continue;
};
// The intersection range of the item, the subrange, and the line's
// trimming.
let sliced =
range.start.max(subrange.start)..range.end.min(subrange.end).min(trim);
// Whether the item is split by the line.
let split = subrange.start < sliced.start || sliced.end < subrange.end;
if sliced.is_empty() {
// When there is no text, still keep this as a fallback item, which
// we can use to force a non-zero line-height when the line doesn't
// contain any other text.
*fallback = Some(ItemEntry::from(Item::Text(shaped.empty())));
} else if split {
// When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped));
} else {
// When the item is fully contained, just keep it.
items.push(item);
}
}
}
/// Add spacing around punctuation marks for CJ glyphs at line boundaries.
///
/// See Requirements for Chinese Text Layout, Section 3.1.6.3 Compression of
/// 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))
{ {
// If the last glyph is a CJK character adjusted by adjust_cj_at_line_start(p, items);
// [`add_cjk_latin_spacing`], restore the original
// width.
let shrink_amount =
last_glyph.x_advance - last_glyph.x_offset - Em::one();
let glyph = reshaped.glyphs.to_mut().last_mut().unwrap();
glyph.x_advance -= shrink_amount;
glyph.adjustability.shrinkability.1 = Em::zero();
reshaped.width -= shrink_amount.at(reshaped.size);
}
} }
width += reshaped.width; if text.ends_with(END_PUNCT_PAT)
last = Some(Item::Text(reshaped)); || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script))
}
inner = before;
}
}
// Deal with CJ characters at line starts.
let text = &p.bidi.text[range.start..end];
let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script));
// Reshape the start item if it's split in half.
let mut first = None;
if let Some((Item::Text(shaped), after)) = inner.split_first() {
// Compute the range we want to shape.
let base = expanded.start;
let end = range.end.min(base + shaped.text.len());
// Reshape if necessary.
if range.start + shaped.text.len() > end
|| maybe_adjust_first_glyph
|| prepend_hyphen
{ {
// If the range is empty, we don't want to push an empty text item. adjust_cj_at_line_end(p, items);
if range.start < end {
let reshaped = shaped.reshape(engine, &p.spans, range.start..end);
width += reshaped.width;
first = Some(Item::Text(reshaped));
}
inner = after;
} }
} }
if prepend_hyphen { /// Add spacing around punctuation marks for CJ glyphs at the line start.
let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
if let Some(reshaped) = reshaped { let Some(shaped) = items.first_text_mut() else { return };
let width_before = reshaped.width; let Some(glyph) = shaped.glyphs.first() else { return };
reshaped.prepend_hyphen(engine, p.fallback);
width += reshaped.width - width_before;
}
}
if maybe_adjust_first_glyph { if glyph.is_cjk_right_aligned_punctuation() {
let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut);
if let Some(reshaped) = reshaped {
if let Some(first_glyph) = reshaped.glyphs.first() {
if first_glyph.is_cjk_right_aligned_punctuation() {
// If the first glyph is a CJK punctuation, we want to // If the first glyph is a CJK punctuation, we want to
// shrink it. // shrink it.
let shrink_amount = first_glyph.shrinkability().0; let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink_amount); glyph.shrink_left(shrink);
let amount_abs = shrink_amount.at(reshaped.size); shaped.width -= shrink.at(shaped.size);
reshaped.width -= amount_abs; } else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() {
width -= amount_abs;
} else if p.cjk_latin_spacing
&& first_glyph.is_cj_script()
&& first_glyph.x_offset > Em::zero()
{
// If the first glyph is a CJK character adjusted by // If the first glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width. // [`add_cjk_latin_spacing`], restore the original width.
let shrink_amount = first_glyph.x_offset; let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); let shrink = glyph.x_offset;
glyph.x_advance -= shrink_amount; glyph.x_advance -= shrink;
glyph.x_offset = Em::zero(); glyph.x_offset = Em::zero();
glyph.adjustability.shrinkability.0 = Em::zero(); glyph.adjustability.shrinkability.0 = Em::zero();
let amount_abs = shrink_amount.at(reshaped.size); shaped.width -= shrink.at(shaped.size);
reshaped.width -= amount_abs;
width -= amount_abs;
}
}
} }
} }
// Measure the inner items. /// Add spacing around punctuation marks for CJ glyphs at the line end.
for item in inner { fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
width += item.width(); let Some(shaped) = items.last_text_mut() else { return };
let Some(glyph) = shaped.glyphs.last() else { return };
// Deal with CJK punctuation at line ends.
let style = cjk_punct_style(shaped.lang, shaped.region);
if glyph.is_cjk_left_aligned_punctuation(style) {
// If the last glyph is a CJK punctuation, we want to
// shrink it.
let shrink = glyph.shrinkability().1;
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
&& glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one()
{
// If the last glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width.
let shrink = glyph.x_advance - glyph.x_offset - Em::one();
let glyph = shaped.glyphs.to_mut().last_mut().unwrap();
glyph.x_advance -= shrink;
glyph.adjustability.shrinkability.1 = Em::zero();
shaped.width -= shrink.at(shaped.size);
}
} }
Line { /// Whether a hyphen should be inserted at the start of the next line.
bidi: &p.bidi, fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
trimmed: range, // If the predecessor line does not end with a `Dash::Hard`, we shall
end, // not place a hyphen at the start of the next line.
first, if pred_line.dash != Some(Dash::Hard) {
inner, return false;
last, }
width,
justify, // The hyphen should repeat only in the languages that require that feature.
dash, // For more information see the discussion at https://github.com/typst/typst/issues/3235
let Some(Item::Text(shaped)) = pred_line.items.last() else { return false };
match shaped.lang {
// - Lower Sorbian: see https://dolnoserbski.de/ortografija/psawidla/K3
// - Czech: see https://prirucka.ujc.cas.cz/?id=164
// - Croatian: see http://pravopis.hr/pravilo/spojnica/68/
// - Polish: see https://www.ortograf.pl/zasady-pisowni/lacznik-zasady-pisowni
// - Portuguese: see https://www2.senado.leg.br/bdsf/bitstream/handle/id/508145/000997415.pdf (Base XX)
// - Slovak: see https://www.zones.sk/studentske-prace/gramatika/10620-pravopis-rozdelovanie-slov/
Lang::LOWER_SORBIAN
| Lang::CZECH
| Lang::CROATIAN
| Lang::POLISH
| Lang::PORTUGUESE
| Lang::SLOVAK => true,
// In Spanish the hyphen is required only if the word next to hyphen is
// not capitalized. Otherwise, the hyphen must not be repeated.
//
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()),
_ => false,
} }
} }
@ -354,18 +417,19 @@ pub fn commit(
let mut remaining = width - line.width - p.hang; let mut remaining = width - line.width - p.hang;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// Reorder the line from logical to visual order. // We always build the line from left to right. In an LTR paragraph, we must
let (reordered, starts_rtl) = reorder(line); // thus add the hanging indent to the offset. When the paragraph is RTL, the
if !starts_rtl { // hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR {
offset += p.hang; offset += p.hang;
} }
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
if let Some(Item::Text(text)) = reordered.first() { if let Some(Item::Text(text)) = line.items.first() {
if let Some(glyph) = text.glyphs.first() { if let Some(glyph) = text.glyphs.first() {
if !text.dir.is_positive() if !text.dir.is_positive()
&& TextElem::overhang_in(text.styles) && TextElem::overhang_in(text.styles)
&& (reordered.len() > 1 || text.glyphs.len() > 1) && (line.items.len() > 1 || text.glyphs.len() > 1)
{ {
let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
offset -= amount; offset -= amount;
@ -375,11 +439,11 @@ pub fn commit(
} }
// Handle hanging punctuation to the right. // Handle hanging punctuation to the right.
if let Some(Item::Text(text)) = reordered.last() { if let Some(Item::Text(text)) = line.items.last() {
if let Some(glyph) = text.glyphs.last() { if let Some(glyph) = text.glyphs.last() {
if text.dir.is_positive() if text.dir.is_positive()
&& TextElem::overhang_in(text.styles) && TextElem::overhang_in(text.styles)
&& (reordered.len() > 1 || text.glyphs.len() > 1) && (line.items.len() > 1 || text.glyphs.len() > 1)
{ {
let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
remaining += amount; remaining += amount;
@ -397,16 +461,16 @@ pub fn commit(
let mut extra_justification = Abs::zero(); let mut extra_justification = Abs::zero();
let shrinkability = line.shrinkability(); let shrinkability = line.shrinkability();
let stretch = line.stretchability(); let stretchability = line.stretchability();
if remaining < Abs::zero() && shrinkability > Abs::zero() && shrink { if remaining < Abs::zero() && shrinkability > Abs::zero() && shrink {
// Attempt to reduce the length of the line, using shrinkability. // Attempt to reduce the length of the line, using shrinkability.
justification_ratio = (remaining / shrinkability).max(-1.0); justification_ratio = (remaining / shrinkability).max(-1.0);
remaining = (remaining + shrinkability).min(Abs::zero()); remaining = (remaining + shrinkability).min(Abs::zero());
} else if line.justify && fr.is_zero() { } else if line.justify && fr.is_zero() {
// Attempt to increase the length of the line, using stretchability. // Attempt to increase the length of the line, using stretchability.
if stretch > Abs::zero() { if stretchability > Abs::zero() {
justification_ratio = (remaining / stretch).min(1.0); justification_ratio = (remaining / stretchability).min(1.0);
remaining = (remaining - stretch).max(Abs::zero()); remaining = (remaining - stretchability).max(Abs::zero());
} }
let justifiables = line.justifiables(); let justifiables = line.justifiables();
@ -422,7 +486,7 @@ pub fn commit(
// Build the frames and determine the height and baseline. // Build the frames and determine the height and baseline.
let mut frames = vec![]; let mut frames = vec![];
for item in reordered { for item in line.items.iter() {
let mut push = |offset: &mut Abs, frame: Frame| { let mut push = |offset: &mut Abs, frame: Frame| {
let width = frame.width(); let width = frame.width();
top.set_max(frame.baseline()); top.set_max(frame.baseline());
@ -449,8 +513,12 @@ pub fn commit(
} }
} }
Item::Text(shaped) => { Item::Text(shaped) => {
let mut frame = let mut frame = shaped.build(
shaped.build(engine, justification_ratio, extra_justification); engine,
&p.spans,
justification_ratio,
extra_justification,
);
frame.post_process(shaped.styles); frame.post_process(shaped.styles);
push(&mut offset, frame); push(&mut offset, frame);
} }
@ -488,46 +556,6 @@ pub fn commit(
Ok(output) Ok(output)
} }
/// Return a line's items in visual order.
fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) {
let mut reordered = vec![];
// The bidi crate doesn't like empty lines.
if line.trimmed.is_empty() {
return (line.slice(line.trimmed.clone()).collect(), false);
}
// Find the paragraph that contains the line.
let para = line
.bidi
.paragraphs
.iter()
.find(|para| para.range.contains(&line.trimmed.start))
.unwrap();
// Compute the reordered ranges in visual order (left to right).
let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
let starts_rtl = levels.first().is_some_and(|level| level.is_rtl());
// Collect the reordered items.
for run in runs {
// Skip reset L1 runs because handling them would require reshaping
// again in some cases.
if line.bidi.levels[run.start] != levels[run.start] {
continue;
}
let prev = reordered.len();
reordered.extend(line.slice(run.clone()));
if levels[run.start].is_rtl() {
reordered[prev..].reverse();
}
}
(reordered, starts_rtl)
}
/// How much a character should hang into the end margin. /// How much a character should hang into the end margin.
/// ///
/// For more discussion, see: /// For more discussion, see:
@ -548,3 +576,119 @@ fn overhang(c: char) -> f64 {
_ => 0.0, _ => 0.0,
} }
} }
/// A collection of owned or borrowed paragraph items.
pub struct Items<'a>(Vec<ItemEntry<'a>>);
impl<'a> Items<'a> {
/// Create empty items.
pub fn new() -> Self {
Self(vec![])
}
/// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) {
self.0.push(entry.into());
}
/// Iterate over the items
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.0.iter().map(|item| &**item)
}
/// Access the first item.
pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item)
}
/// Access the last item.
pub fn last(&self) -> Option<&Item<'a>> {
self.0.last().map(|item| &**item)
}
/// Access the first item mutably, if it is text.
pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.first_mut()?.text_mut()
}
/// Access the last item mutably, if it is text.
pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.last_mut()?.text_mut()
}
/// Reorder the items starting at the given index to RTL.
pub fn reorder(&mut self, from: usize) {
self.0[from..].reverse()
}
}
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
impl<'a> Deref for Items<'a> {
type Target = Vec<ItemEntry<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> DerefMut for Items<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// A reference to or a boxed item.
pub enum ItemEntry<'a> {
Ref(&'a Item<'a>),
Box(Box<Item<'a>>),
}
impl<'a> ItemEntry<'a> {
fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
match self {
Self::Ref(item) => {
let text = item.text()?;
*self = Self::Box(Box::new(Item::Text(text.clone())));
match self {
Self::Box(item) => item.text_mut(),
_ => unreachable!(),
}
}
Self::Box(item) => item.text_mut(),
}
}
}
impl<'a> Deref for ItemEntry<'a> {
type Target = Item<'a>;
fn deref(&self) -> &Self::Target {
match self {
Self::Ref(item) => item,
Self::Box(item) => item,
}
}
}
impl Debug for ItemEntry<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
(**self).fmt(f)
}
}
impl<'a> From<&'a Item<'a>> for ItemEntry<'a> {
fn from(item: &'a Item<'a>) -> Self {
Self::Ref(item)
}
}
impl<'a> From<Item<'a>> for ItemEntry<'a> {
fn from(item: Item<'a>) -> Self {
Self::Box(Box::new(item))
}
}

View File

@ -1,42 +1,61 @@
use icu_properties::maps::CodePointMapData; use std::ops::{Add, Sub};
use icu_properties::maps::{CodePointMapData, CodePointMapDataBorrowed};
use icu_properties::sets::CodePointSetData;
use icu_properties::LineBreak; use icu_properties::LineBreak;
use icu_provider::AsDeserializingBufferProvider; use icu_provider::AsDeserializingBufferProvider;
use icu_provider_adapters::fork::ForkByKeyProvider; use icu_provider_adapters::fork::ForkByKeyProvider;
use icu_provider_blob::BlobDataProvider; use icu_provider_blob::BlobDataProvider;
use icu_segmenter::LineSegmenter; use icu_segmenter::LineSegmenter;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use unicode_segmentation::UnicodeSegmentation;
use super::*; use super::*;
use crate::engine::Engine; use crate::engine::Engine;
use crate::layout::Abs; use crate::layout::{Abs, Em};
use crate::model::Linebreaks; use crate::model::Linebreaks;
use crate::syntax::link_prefix; use crate::syntax::link_prefix;
use crate::text::{Lang, TextElem}; use crate::text::{Lang, TextElem};
/// The cost of a line or paragraph layout.
type Cost = f64;
// Cost parameters.
const DEFAULT_HYPH_COST: Cost = 0.5;
const DEFAULT_RUNT_COST: Cost = 0.5;
const CONSECUTIVE_DASH_COST: Cost = 0.3;
const MAX_COST: Cost = 1_000_000.0;
const MIN_RATIO: f64 = -1.0;
const MIN_APPROX_RATIO: f64 = -0.5;
const BOUND_EPS: f64 = 1e-3;
/// The ICU blob data.
fn blob() -> BlobDataProvider {
BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap()
}
/// The general line break segmenter. /// The general line break segmenter.
static SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { static SEGMENTER: Lazy<LineSegmenter> =
let provider = Lazy::new(|| LineSegmenter::try_new_lstm_with_buffer_provider(&blob()).unwrap());
BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap();
LineSegmenter::try_new_lstm_with_buffer_provider(&provider).unwrap()
});
/// The line break segmenter for Chinese/Japanese text. /// The line break segmenter for Chinese/Japanese text.
static CJ_SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { static CJ_SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| {
let provider =
BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap();
let cj_blob = let cj_blob =
BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU_CJ_SEGMENT) BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU_CJ_SEGMENT)
.unwrap(); .unwrap();
let cj_provider = ForkByKeyProvider::new(cj_blob, provider); let cj_provider = ForkByKeyProvider::new(cj_blob, blob());
LineSegmenter::try_new_lstm_with_buffer_provider(&cj_provider).unwrap() LineSegmenter::try_new_lstm_with_buffer_provider(&cj_provider).unwrap()
}); });
/// The Unicode line break properties for each code point. /// The Unicode line break properties for each code point.
static LINEBREAK_DATA: Lazy<CodePointMapData<LineBreak>> = Lazy::new(|| { static LINEBREAK_DATA: Lazy<CodePointMapData<LineBreak>> = Lazy::new(|| {
let provider = icu_properties::maps::load_line_break(&blob().as_deserializing()).unwrap()
BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap(); });
let deser_provider = provider.as_deserializing();
icu_properties::maps::load_line_break(&deser_provider).unwrap() /// The set of Unicode default ignorables.
static DEFAULT_IGNORABLE_DATA: Lazy<CodePointSetData> = Lazy::new(|| {
icu_properties::sets::load_default_ignorable_code_point(&blob().as_deserializing())
.unwrap()
}); });
/// A line break opportunity. /// A line break opportunity.
@ -50,6 +69,37 @@ pub enum Breakpoint {
Hyphen, Hyphen,
} }
impl Breakpoint {
/// Trim a line before this breakpoint.
pub fn trim(self, line: &str) -> &str {
// Trim default ignorables.
let ignorable = DEFAULT_IGNORABLE_DATA.as_borrowed();
let line = line.trim_end_matches(|c| ignorable.contains(c));
match self {
// Trim whitespace.
Self::Normal => line.trim_end_matches(char::is_whitespace),
// Trim linebreaks.
Self::Mandatory => {
let lb = LINEBREAK_DATA.as_borrowed();
line.trim_end_matches(|c| {
matches!(
lb.get(c),
LineBreak::MandatoryBreak
| LineBreak::CarriageReturn
| LineBreak::LineFeed
| LineBreak::NextLine
)
})
}
// Trim nothing further.
Self::Hyphen => line,
}
}
}
/// Breaks the paragraph into lines. /// Breaks the paragraph into lines.
pub fn linebreak<'a>( pub fn linebreak<'a>(
engine: &Engine, engine: &Engine,
@ -84,10 +134,8 @@ fn linebreak_simple<'a>(
let mut last = None; let mut last = None;
breakpoints(p, |end, breakpoint| { breakpoints(p, |end, breakpoint| {
let prepend_hyphen = lines.last().map(should_repeat_hyphen).unwrap_or(false);
// Compute the line and its size. // Compute the line and its size.
let mut attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); let mut attempt = line(engine, p, start..end, breakpoint, lines.last());
// If the line doesn't fit anymore, we push the last fitting attempt // If the line doesn't fit anymore, we push the last fitting attempt
// into the stack and rebuild the line from the attempt's end. The // into the stack and rebuild the line from the attempt's end. The
@ -96,7 +144,7 @@ fn linebreak_simple<'a>(
if let Some((last_attempt, last_end)) = last.take() { if let Some((last_attempt, last_end)) = last.take() {
lines.push(last_attempt); lines.push(last_attempt);
start = last_end; start = last_end;
attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); attempt = line(engine, p, start..end, breakpoint, lines.last());
} }
} }
@ -142,79 +190,77 @@ fn linebreak_optimized<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
width: Abs, width: Abs,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
/// The cost of a line or paragraph layout. let metrics = CostMetrics::compute(p);
type Cost = f64;
/// An entry in the dynamic programming table. // Determines the exact costs of a likely good layout through Knuth-Plass
// with approximate metrics. We can use this cost as an upper bound to prune
// the search space in our proper optimization pass below.
let upper_bound = linebreak_optimized_approximate(engine, p, width, &metrics);
// Using the upper bound, perform exact optimized linebreaking.
linebreak_optimized_bounded(engine, p, width, &metrics, upper_bound)
}
/// Performs line breaking in optimized Knuth-Plass style, but with an upper
/// bound on the cost. This allows us to skip many parts of the search space.
#[typst_macros::time]
fn linebreak_optimized_bounded<'a>(
engine: &Engine,
p: &'a Preparation<'a>,
width: Abs,
metrics: &CostMetrics,
upper_bound: Cost,
) -> Vec<Line<'a>> {
/// An entry in the dynamic programming table for paragraph optimization.
struct Entry<'a> { struct Entry<'a> {
pred: usize, pred: usize,
total: Cost, total: Cost,
line: Line<'a>, line: Line<'a>,
end: usize,
} }
// Cost parameters.
const DEFAULT_HYPH_COST: Cost = 0.5;
const DEFAULT_RUNT_COST: Cost = 0.5;
const CONSECUTIVE_DASH_COST: Cost = 0.3;
const MAX_COST: Cost = 1_000_000.0;
const MIN_RATIO: f64 = -1.0;
let hyph_cost = DEFAULT_HYPH_COST * p.costs.hyphenation().get();
let runt_cost = DEFAULT_RUNT_COST * p.costs.runt().get();
// Dynamic programming table. // Dynamic programming table.
let mut active = 0; let mut table = vec![Entry { pred: 0, total: 0.0, line: Line::empty(), end: 0 }];
let mut table = vec![Entry {
pred: 0, let mut active = 0;
total: 0.0, let mut prev_end = 0;
line: line(engine, p, 0..0, Breakpoint::Mandatory, false),
}];
let em = p.size;
let mut lines = Vec::with_capacity(16);
breakpoints(p, |end, breakpoint| { breakpoints(p, |end, breakpoint| {
let k = table.len(); // Find the optimal predecessor.
let is_end = end == p.bidi.text.len();
let mut best: Option<Entry> = None; let mut best: Option<Entry> = None;
// Find the optimal predecessor. // A lower bound for the cost of all following line attempts.
for (i, pred) in table.iter().enumerate().skip(active) { let mut line_lower_bound = None;
// Layout the line.
let start = pred.line.end;
let prepend_hyphen = should_repeat_hyphen(&pred.line);
let attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); for (pred_index, pred) in table.iter().enumerate().skip(active) {
let start = pred.end;
let unbreakable = prev_end == start;
// Determine how much the line's spaces would need to be stretched // If the minimum cost we've established for the line is already
// to make it the desired width. // too much, skip this attempt.
let delta = width - attempt.width; if line_lower_bound
// Determine how much stretch are permitted. .is_some_and(|lower| pred.total + lower > upper_bound + BOUND_EPS)
let adjust = if delta >= Abs::zero() { {
attempt.stretchability() continue;
} else {
attempt.shrinkability()
};
// Ideally, the ratio should between -1.0 and 1.0, but sometimes a
// value above 1.0 is possible, in which case the line is underfull.
let mut ratio = delta / adjust;
if ratio.is_nan() {
// The line is not stretchable, but it just fits. This often
// happens with monospace fonts and CJK texts.
ratio = 0.0;
}
if ratio > 1.0 {
// We should stretch the line above its stretchability. Now
// calculate the extra amount. Also, don't divide by zero.
let extra_stretch =
(delta - adjust) / attempt.justifiables().max(1) as f64;
// Normalize the amount by half Em size.
ratio = 1.0 + extra_stretch / (em / 2.0);
} }
// Determine the cost of the line. // Build the line.
let min_ratio = if p.justify { MIN_RATIO } else { 0.0 }; let attempt = line(engine, p, start..end, breakpoint, Some(&pred.line));
let mut cost = if ratio < min_ratio {
// The line is overfull. This is the case if // Determine the cost of the line and its stretch ratio.
let (line_ratio, line_cost) = ratio_and_cost(
p,
metrics,
width,
&pred.line,
&attempt,
end,
breakpoint,
unbreakable,
);
// If the line is overfull, we adjust the set of active candidate
// line starts. This is the case if
// - justification is on, but we'd need to shrink too much // - justification is on, but we'd need to shrink too much
// - justification is off and the line just doesn't fit // - justification is off and the line just doesn't fit
// //
@ -223,63 +269,61 @@ fn linebreak_optimized<'a>(
// earlier one (active < i), then the logically shorter line was // earlier one (active < i), then the logically shorter line was
// in fact longer (can happen with negative spacing) and we // in fact longer (can happen with negative spacing) and we
// can't trim the active set just yet. // can't trim the active set just yet.
if active == i { if line_ratio < metrics.min_ratio && active == pred_index {
active += 1; active += 1;
} }
MAX_COST
} else if breakpoint == Breakpoint::Mandatory || is_end {
// This is a mandatory break and the line is not overfull, so
// all breakpoints before this one become inactive since no line
// can span above the mandatory break.
active = k;
// - If ratio > 0, we need to stretch the line only when justify
// is needed.
// - If ratio < 0, we always need to shrink the line.
if (ratio > 0.0 && attempt.justify) || ratio < 0.0 {
ratio.powi(3).abs()
} else {
0.0
}
} else {
// Normal line with cost of |ratio^3|.
ratio.powi(3).abs()
};
// Penalize runts.
if k == i + 1 && is_end {
cost += runt_cost;
}
// Penalize hyphens.
if breakpoint == Breakpoint::Hyphen {
cost += hyph_cost;
}
// In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a,
// where r is the ratio, p=50 is the penalty, and a=3000 is
// consecutive the penalty. We divide the whole formula by 10,
// resulting (0.01 + |r|^3 + p)^2 + a, where p=0.5 and a=0.3
cost = (0.01 + cost).powi(2);
// Penalize two consecutive dashes (not necessarily hyphens) extra.
if attempt.dash.is_some() && pred.line.dash.is_some() {
cost += CONSECUTIVE_DASH_COST;
}
// The total cost of this line and its chain of predecessors. // The total cost of this line and its chain of predecessors.
let total = pred.total + cost; let total = pred.total + line_cost;
// If the line is already underfull (`line_ratio > 0`), any shorter
// slice of the line will be even more underfull. So it'll only get
// worse from here and further attempts would also have a cost
// exceeding `bound`. There is one exception: When the line has
// negative spacing, we can't know for sure, so we don't assign the
// lower bound in that case.
if line_ratio > 0.0
&& line_lower_bound.is_none()
&& !attempt.has_negative_width_items()
{
line_lower_bound = Some(line_cost);
}
// If the cost already exceeds the upper bound, we don't need to
// integrate this result into the table.
if total > upper_bound + BOUND_EPS {
continue;
}
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().map_or(true, |best| best.total >= total) {
best = Some(Entry { pred: i, total, line: attempt }); best = Some(Entry { pred: pred_index, total, line: attempt, end });
} }
} }
table.push(best.unwrap()); // If this is a mandatory break, all breakpoints before this one become
// inactive since no line can span over the mandatory break.
if breakpoint == Breakpoint::Mandatory {
active = table.len();
}
table.extend(best);
prev_end = end;
}); });
// Retrace the best path. // Retrace the best path.
let mut lines = Vec::with_capacity(16);
let mut idx = table.len() - 1; let mut idx = table.len() - 1;
// 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");
#[cfg(not(debug_assertions))]
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
}
while idx != 0 { while idx != 0 {
table.truncate(idx + 1); table.truncate(idx + 1);
let entry = table.pop().unwrap(); let entry = table.pop().unwrap();
@ -291,6 +335,293 @@ fn linebreak_optimized<'a>(
lines lines
} }
/// Runs the normal Knuth-Plass algorithm, but instead of building proper lines
/// (which is costly) to determine costs, it determines approximate costs using
/// cummulative arrays.
///
/// This results in a likely good paragraph 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]
fn linebreak_optimized_approximate(
engine: &Engine,
p: &Preparation,
width: Abs,
metrics: &CostMetrics,
) -> Cost {
// Determine the cummulative estimation metrics.
let estimates = Estimates::compute(p);
/// An entry in the dynamic programming table for paragraph optimization.
struct Entry {
pred: usize,
total: Cost,
end: usize,
unbreakable: bool,
breakpoint: Breakpoint,
}
// Dynamic programming table.
let mut table = vec![Entry {
pred: 0,
total: 0.0,
end: 0,
unbreakable: false,
breakpoint: Breakpoint::Mandatory,
}];
let mut active = 0;
let mut prev_end = 0;
breakpoints(p, |end, breakpoint| {
let at_end = end == p.text.len();
// Find the optimal predecessor.
let mut best: Option<Entry> = None;
for (pred_index, pred) in table.iter().enumerate().skip(active) {
let start = pred.end;
let unbreakable = prev_end == start;
// Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough.
let justify = p.justify && !at_end && 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
// just an estimate.
let consecutive_dash =
pred.breakpoint == Breakpoint::Hyphen && breakpoint == Breakpoint::Hyphen;
// Estimate how much the line's spaces would need to be stretched to
// make it the desired width. We trim at the end to not take into
// account trailing spaces. This is, again, only an approximation of
// the real behaviour of `line`.
let trimmed_end = start + p.text[start..end].trim_end().len();
let line_ratio = raw_ratio(
p,
width,
estimates.widths.estimate(start..trimmed_end)
+ if breakpoint == Breakpoint::Hyphen {
metrics.approx_hyphen_width
} else {
Abs::zero()
},
estimates.stretchability.estimate(start..trimmed_end),
estimates.shrinkability.estimate(start..trimmed_end),
estimates.justifiables.estimate(start..trimmed_end),
);
// Determine the line's cost.
let line_cost = raw_cost(
metrics,
breakpoint,
line_ratio,
at_end,
justify,
unbreakable,
consecutive_dash,
true,
);
// Adjust the set of active breakpoints.
// See `linebreak_optimized` for details.
if line_ratio < metrics.min_ratio && active == pred_index {
active += 1;
}
// The total cost of this line and its chain of predecessors.
let total = pred.total + line_cost;
// If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) {
best = Some(Entry {
pred: pred_index,
total,
end,
unbreakable,
breakpoint,
});
}
}
// If this is a mandatory break, all breakpoints before this one become
// inactive.
if breakpoint == Breakpoint::Mandatory {
active = table.len();
}
table.extend(best);
prev_end = end;
});
// Retrace the best path.
let mut indices = Vec::with_capacity(16);
let mut idx = table.len() - 1;
while idx != 0 {
indices.push(idx);
idx = table[idx].pred;
}
let mut pred = Line::empty();
let mut start = 0;
let mut exact = 0.0;
// The cost that we optimized was only an approximate cost, so the layout we
// got here is only likely to be good, not guaranteed to be the best. We now
// computes its exact cost as that gives us a sound upper bound for the
// proper optimization pass.
for idx in indices.into_iter().rev() {
let Entry { end, breakpoint, unbreakable, .. } = table[idx];
let attempt = line(engine, p, start..end, breakpoint, Some(&pred));
let (_, line_cost) = ratio_and_cost(
p,
metrics,
width,
&pred,
&attempt,
end,
breakpoint,
unbreakable,
);
pred = attempt;
start = end;
exact += line_cost;
}
exact
}
/// Compute the stretch ratio and cost of a line.
#[allow(clippy::too_many_arguments)]
fn ratio_and_cost(
p: &Preparation,
metrics: &CostMetrics,
available_width: Abs,
pred: &Line,
attempt: &Line,
end: usize,
breakpoint: Breakpoint,
unbreakable: bool,
) -> (f64, Cost) {
let ratio = raw_ratio(
p,
available_width,
attempt.width,
attempt.stretchability(),
attempt.shrinkability(),
attempt.justifiables(),
);
let cost = raw_cost(
metrics,
breakpoint,
ratio,
end == p.text.len(),
attempt.justify,
unbreakable,
pred.dash.is_some() && attempt.dash.is_some(),
false,
);
(ratio, cost)
}
/// Determine the stretch ratio for a line given raw metrics.
fn raw_ratio(
p: &Preparation,
available_width: Abs,
line_width: Abs,
stretchability: Abs,
shrinkability: Abs,
justifiables: usize,
) -> f64 {
// Determine how much the line's spaces would need to be stretched
// to make it the desired width.
let delta = available_width - line_width;
// Determine how much stretch is permitted.
let adjust = if delta >= Abs::zero() { stretchability } else { shrinkability };
// Ideally, the ratio should between -1.0 and 1.0.
//
// A ratio above 1.0 is possible for an underfull line, but a ratio below
// -1.0 is forbidden because the line would overflow.
let mut ratio = delta / adjust;
// The line is not stretchable, but it just fits. This often happens with
// monospace fonts and CJK texts.
if ratio.is_nan() {
ratio = 0.0;
}
if ratio > 1.0 {
// We should stretch the line above its stretchability. Now
// calculate the extra amount. Also, don't divide by zero.
let extra_stretch = (delta - adjust) / justifiables.max(1) as f64;
// Normalize the amount by half the em size.
ratio = 1.0 + extra_stretch / (p.size / 2.0);
}
ratio
}
/// Compute the cost of a line given raw metrics.
#[allow(clippy::too_many_arguments)]
fn raw_cost(
metrics: &CostMetrics,
breakpoint: Breakpoint,
ratio: f64,
at_end: bool,
justify: bool,
unbreakable: bool,
consecutive_dash: bool,
approx: bool,
) -> Cost {
// Determine the cost of the line.
let mut cost = if ratio < metrics.min_ratio(approx) {
// Overfull line always has maximum cost.
MAX_COST
} else if breakpoint == Breakpoint::Mandatory || at_end {
// - If ratio < 0, we always need to shrink the line (even the last one).
// - If ratio > 0, we need to stretch the line only when it is justified
// (last line is not justified by default even if `p.justify` is true).
if ratio < 0.0 || (ratio > 0.0 && justify) {
ratio.powi(3).abs()
} else {
0.0
}
} else {
// Normal line with cost of |ratio^3|.
ratio.powi(3).abs()
};
// Penalize runts (lone words in the last line).
if unbreakable && at_end {
cost += metrics.runt_cost;
}
// Penalize hyphenation.
if breakpoint == Breakpoint::Hyphen {
cost += metrics.hyph_cost;
}
// In the Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a,
// where r is the ratio, p=50 is the penalty, and a=3000 is
// consecutive the penalty. We divide the whole formula by 10,
// resulting (0.01 + |r|^3 + p)^2 + a, where p=0.5 and a=0.3
let mut cost = (0.01 + cost).powi(2);
// Penalize two consecutive dashes (not necessarily hyphens) extra.
if consecutive_dash {
cost += CONSECUTIVE_DASH_COST;
}
cost
}
/// Calls `f` for all possible points in the text where lines can broken. /// Calls `f` for all possible points in the text where lines can broken.
/// ///
/// Yields for each breakpoint the text index, whether the break is mandatory /// Yields for each breakpoint the text index, whether the break is mandatory
@ -300,8 +631,15 @@ fn linebreak_optimized<'a>(
/// This is an internal instead of an external iterator because it makes the /// This is an internal instead of an external iterator because it makes the
/// code much simpler and the consumers of this function don't need the /// code much simpler and the consumers of this function don't need the
/// composability and flexibility of external iteration anyway. /// composability and flexibility of external iteration anyway.
fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint)) { fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
let text = p.bidi.text; let text = p.text;
// Single breakpoint at the end for empty text.
if text.is_empty() {
f(0, Breakpoint::Mandatory);
return;
}
let hyphenate = p.hyphenate != Some(false); let hyphenate = p.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed(); let lb = LINEBREAK_DATA.as_borrowed();
let segmenter = match p.lang { let segmenter = match p.lang {
@ -324,7 +662,7 @@ fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint))
} }
} }
// Get the UAX #14 linebreak opportunities. // Get the next UAX #14 linebreak opportunity.
let Some(point) = iter.next() else { break }; let Some(point) = iter.next() else { break };
// Skip breakpoint if there is no char before it. icu4x generates one // Skip breakpoint if there is no char before it. icu4x generates one
@ -349,32 +687,44 @@ fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint))
}; };
// Hyphenate between the last and current breakpoint. // Hyphenate between the last and current breakpoint.
'hyphenate: { if hyphenate {
if !hyphenate {
break 'hyphenate;
}
// Extract a hyphenatable "word".
let word = &text[last..point].trim_end_matches(|c: char| !c.is_alphabetic());
if word.is_empty() {
break 'hyphenate;
}
let end = last + word.len();
let mut offset = last; let mut offset = last;
for segment in text[last..point].split_word_bounds() {
if !segment.is_empty() && segment.chars().all(char::is_alphabetic) {
hyphenations(p, &lb, offset, segment, &mut f);
}
offset += segment.len();
}
}
// Determine the language to hyphenate this word in. // Call `f` for the UAX #14 break opportunity.
let Some(lang) = lang_at(p, last) else { break 'hyphenate }; f(point, breakpoint);
last = point;
}
}
/// Generate breakpoints for hyphenations within a word.
fn hyphenations(
p: &Preparation,
lb: &CodePointMapDataBorrowed<LineBreak>,
mut offset: usize,
word: &str,
mut f: impl FnMut(usize, Breakpoint),
) {
let Some(lang) = lang_at(p, offset) else { return };
let end = offset + word.len();
for syllable in hypher::hyphenate(word, lang) { for syllable in hypher::hyphenate(word, lang) {
// Don't hyphenate after the final syllable.
offset += syllable.len(); offset += syllable.len();
// Don't hyphenate after the final syllable.
if offset == end { if offset == end {
continue; continue;
} }
// Filter out hyphenation opportunities where hyphenation was // Filter out hyphenation opportunities where hyphenation was actually
// actually disabled. // disabled.
if !hyphenate_at(p, offset) { if !hyphenate_at(p, offset) {
continue; continue;
} }
@ -392,13 +742,6 @@ fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint))
} }
} }
// Call `f` for the UAX #14 break opportunity.
f(point, breakpoint);
last = point;
}
}
/// Produce linebreak opportunities for a link. /// Produce linebreak opportunities for a link.
fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
#[derive(PartialEq)] #[derive(PartialEq)]
@ -433,7 +776,7 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
// - other -> other // - other -> other
// - alphabetic -> numeric // - alphabetic -> numeric
// - numeric -> alphabetic // - numeric -> alphabetic
// Never before after opening delimiters. // Never before/after opening delimiters.
if end > 0 if end > 0
&& prev != Class::Open && prev != Class::Open
&& if class == Class::Other { prev == Class::Other } else { class != prev } && if class == Class::Other { prev == Class::Other } else { class != prev }
@ -461,8 +804,9 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
fn hyphenate_at(p: &Preparation, offset: usize) -> bool { fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.hyphenate p.hyphenate
.or_else(|| { .or_else(|| {
let shaped = p.find(offset)?.text()?; let (_, item) = p.get(offset);
Some(TextElem::hyphenate_in(shaped.styles)) let styles = item.text()?.styles;
Some(TextElem::hyphenate_in(styles))
}) })
.unwrap_or(false) .unwrap_or(false)
} }
@ -470,56 +814,147 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
/// The text language at the given offset. /// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> { fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.lang.or_else(|| { let lang = p.lang.or_else(|| {
let shaped = p.find(offset)?.text()?; let (_, item) = p.get(offset);
Some(TextElem::lang_in(shaped.styles)) let styles = item.text()?.styles;
Some(TextElem::lang_in(styles))
})?; })?;
let bytes = lang.as_str().as_bytes().try_into().ok()?; let bytes = lang.as_str().as_bytes().try_into().ok()?;
hypher::Lang::from_iso(bytes) hypher::Lang::from_iso(bytes)
} }
/// Whether the hyphen should repeat at the start of the next line. /// Resolved metrics relevant for cost computation.
fn should_repeat_hyphen(pred_line: &Line) -> bool { struct CostMetrics {
// If the predecessor line does not end with a Dash::HardHyphen, we shall min_ratio: f64,
// not place a hyphen at the start of the next line. min_approx_ratio: f64,
if pred_line.dash != Some(Dash::HardHyphen) { hyph_cost: Cost,
return false; runt_cost: Cost,
approx_hyphen_width: Abs,
} }
// If there's a trimmed out space, we needn't repeat the hyphen. That's the impl CostMetrics {
// case of a text like "...kebab é a -melhor- comida que existe", where the /// Compute shared metrics for paragraph optimization.
// hyphens are a kind of emphasis marker. fn compute(p: &Preparation) -> Self {
if pred_line.trimmed.end != pred_line.end { Self {
return false; // 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 },
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
// Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size),
}
} }
// The hyphen should repeat only in the languages that require that feature. /// The minimum line ratio we allow for shrinking. For approximate layout,
// For more information see the discussion at https://github.com/typst/typst/issues/3235 /// we allow less because otherwise we get an invalid layout fairly often,
let Some(Item::Text(shape)) = pred_line.last.as_ref() else { return false }; /// which makes our bound useless.
fn min_ratio(&self, approx: bool) -> f64 {
if approx {
self.min_approx_ratio
} else {
self.min_ratio
}
}
}
match shape.lang { /// Estimated line metrics.
// - Lower Sorbian: see https://dolnoserbski.de/ortografija/psawidla/K3 ///
// - Czech: see https://prirucka.ujc.cas.cz/?id=164 /// Allows to get a quick estimate of a metric for a line between two byte
// - Croatian: see http://pravopis.hr/pravilo/spojnica/68/ /// positions.
// - Polish: see https://www.ortograf.pl/zasady-pisowni/lacznik-zasady-pisowni struct Estimates {
// - Portuguese: see https://www2.senado.leg.br/bdsf/bitstream/handle/id/508145/000997415.pdf (Base XX) widths: CummulativeVec<Abs>,
// - Slovak: see https://www.zones.sk/studentske-prace/gramatika/10620-pravopis-rozdelovanie-slov/ stretchability: CummulativeVec<Abs>,
Lang::LOWER_SORBIAN shrinkability: CummulativeVec<Abs>,
| Lang::CZECH justifiables: CummulativeVec<usize>,
| Lang::CROATIAN }
| Lang::POLISH
| Lang::PORTUGUESE impl Estimates {
| Lang::SLOVAK => true, /// Compute estimations for approximate Knuth-Plass layout.
// In Spanish the hyphen is required only if the word next to hyphen is fn compute(p: &Preparation) -> Self {
// not capitalized. Otherwise, the hyphen must not be repeated. let cap = p.text.len();
//
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española" let mut widths = CummulativeVec::with_capacity(cap);
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea let mut stretchability = CummulativeVec::with_capacity(cap);
Lang::SPANISH => pred_line.bidi.text[pred_line.end..] let mut shrinkability = CummulativeVec::with_capacity(cap);
.chars() let mut justifiables = CummulativeVec::with_capacity(cap);
.next()
.map(|c| !c.is_uppercase()) for (range, item) in p.items.iter() {
.unwrap_or(false), if let Item::Text(shaped) = item {
_ => false, for g in shaped.glyphs.iter() {
let byte_len = g.range.len();
let stretch = g.stretchability().0 + g.stretchability().1;
let shrink = g.shrinkability().0 + g.shrinkability().1;
widths.push(byte_len, g.x_advance.at(shaped.size));
stretchability.push(byte_len, stretch.at(shaped.size));
shrinkability.push(byte_len, shrink.at(shaped.size));
justifiables.push(byte_len, g.is_justifiable() as usize);
}
} else {
widths.push(range.len(), item.natural_width());
}
widths.adjust(range.end);
stretchability.adjust(range.end);
shrinkability.adjust(range.end);
justifiables.adjust(range.end);
}
Self {
widths,
stretchability,
shrinkability,
justifiables,
} }
} }
}
/// An accumulative array of a metric.
struct CummulativeVec<T> {
total: T,
summed: Vec<T>,
}
impl<T> CummulativeVec<T>
where
T: Default + Copy + Add<Output = T> + Sub<Output = T>,
{
/// Create a new instance with the given capacity.
fn with_capacity(capacity: usize) -> Self {
let total = T::default();
let mut summed = Vec::with_capacity(capacity);
summed.push(total);
Self { total, summed }
}
/// Adjust to cover the given byte length.
fn adjust(&mut self, len: usize) {
self.summed.resize(len, self.total);
}
/// Adds a new segment with the given byte length and metric.
fn push(&mut self, byte_len: usize, metric: T) {
self.total = self.total + metric;
for _ in 0..byte_len {
self.summed.push(self.total);
}
}
/// Estimates the metrics for the line spanned by the range.
fn estimate(&self, range: Range) -> T {
self.get(range.end) - self.get(range.start)
}
/// Get the metric at the given byte position.
fn get(&self, index: usize) -> T {
match index.checked_sub(1) {
None => T::default(),
Some(i) => self.summed[i],
}
}
}
/// Whether a codepoint is Unicode `Default_Ignorable`.
pub fn is_default_ignorable(c: char) -> bool {
DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c)
}

View File

@ -9,8 +9,8 @@ use comemo::{Track, Tracked, TrackedMut};
use self::collect::{collect, Item, Segment, SpanMapper}; use self::collect::{collect, Item, Segment, SpanMapper};
use self::finalize::finalize; use self::finalize::finalize;
use self::line::{commit, line, Dash, Line}; use self::line::{commit, line, Line};
use self::linebreak::{linebreak, Breakpoint}; use self::linebreak::{is_default_ignorable, linebreak, Breakpoint};
use self::prepare::{prepare, Preparation}; use self::prepare::{prepare, Preparation};
use self::shaping::{ use self::shaping::{
cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText, cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText,

View File

@ -13,16 +13,24 @@ use crate::text::{Costs, Lang, TextElem};
/// Only when a line break falls onto a text index that is not safe-to-break per /// Only when a line break falls onto a text index that is not safe-to-break per
/// rustybuzz, we have to reshape that portion. /// rustybuzz, we have to reshape that portion.
pub struct Preparation<'a> { pub struct Preparation<'a> {
/// The paragraph's full text.
pub text: &'a str,
/// Bidirectional text embedding levels for the paragraph. /// Bidirectional text embedding levels for the paragraph.
pub bidi: BidiInfo<'a>, ///
/// This is `None` if the paragraph is BiDi-uniform (all the base direction).
pub bidi: Option<BidiInfo<'a>>,
/// Text runs, spacing and layouted elements. /// Text runs, spacing and layouted elements.
pub items: Vec<Item<'a>>, pub items: Vec<(Range, Item<'a>)>,
/// Maps from byte indices to item indices.
pub indices: Vec<usize>,
/// The span mapper. /// The span mapper.
pub spans: SpanMapper, pub spans: SpanMapper,
/// Whether to hyphenate if it's the same for all children. /// Whether to hyphenate if it's the same for all children.
pub hyphenate: Option<bool>, pub hyphenate: Option<bool>,
/// Costs for various layout decisions. /// Costs for various layout decisions.
pub costs: Costs, pub costs: Costs,
/// The dominant direction.
pub dir: Dir,
/// The text language if it's the same for all children. /// The text language if it's the same for all children.
pub lang: Option<Lang>, pub lang: Option<Lang>,
/// The paragraph's resolved horizontal alignment. /// The paragraph's resolved horizontal alignment.
@ -44,46 +52,18 @@ pub struct Preparation<'a> {
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
/// Find the item that contains the given `text_offset`. /// Get the item that contains the given `text_offset`.
pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> { pub fn get(&self, offset: usize) -> &(Range, Item<'a>) {
let mut cursor = 0; let idx = self.indices.get(offset).copied().unwrap_or(0);
for item in &self.items { &self.items[idx]
let end = cursor + item.textual_len();
if (cursor..end).contains(&text_offset) {
return Some(item);
}
cursor = end;
}
None
} }
/// Return the items that intersect the given `text_range`. /// Iterate over the items that intersect the given `sliced` range.
/// pub fn slice(&self, sliced: Range) -> impl Iterator<Item = &(Range, Item<'a>)> {
/// Returns the expanded range around the items and the items. let start = self.indices.get(sliced.start).copied().unwrap_or(0);
pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { self.items[start..].iter().take_while(move |(range, _)| {
let mut cursor = 0; range.start < sliced.end || range.end <= sliced.end
let mut start = 0; })
let mut end = 0;
let mut expanded = text_range.clone();
for (i, item) in self.items.iter().enumerate() {
if cursor <= text_range.start {
start = i;
expanded.start = cursor;
}
let len = item.textual_len();
if cursor < text_range.end || cursor + len <= text_range.end {
end = i + 1;
expanded.end = cursor + len;
} else {
break;
}
cursor += len;
}
(expanded, &self.items[start..end])
} }
} }
@ -99,42 +79,57 @@ pub fn prepare<'a>(
spans: SpanMapper, spans: SpanMapper,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let bidi = BidiInfo::new( let dir = TextElem::dir_in(styles);
text, let default_level = match dir {
match TextElem::dir_in(styles) { Dir::RTL => BidiLevel::rtl(),
Dir::LTR => Some(BidiLevel::ltr()), _ => BidiLevel::ltr(),
Dir::RTL => Some(BidiLevel::rtl()), };
_ => None,
}, let bidi = BidiInfo::new(text, Some(default_level));
); let is_bidi = bidi
.levels
.iter()
.any(|level| level.is_ltr() != default_level.is_ltr());
let mut cursor = 0; let mut cursor = 0;
let mut items = Vec::with_capacity(segments.len()); let mut items = Vec::with_capacity(segments.len());
// Shape the text to finalize the items. // Shape the text to finalize the items.
for segment in segments { for segment in segments {
let end = cursor + segment.textual_len(); let len = segment.textual_len();
let end = cursor + len;
let range = cursor..end;
match segment { match segment {
Segment::Text(_, styles) => { Segment::Text(_, styles) => {
shape_range(&mut items, engine, &bidi, cursor..end, &spans, styles); shape_range(&mut items, engine, text, &bidi, range, styles);
} }
Segment::Item(item) => items.push(item), Segment::Item(item) => items.push((range, item)),
} }
cursor = end; cursor = end;
} }
// Build the mapping from byte to item indices.
let mut indices = Vec::with_capacity(text.len());
for (i, (range, _)) in items.iter().enumerate() {
indices.extend(range.clone().map(|_| i));
}
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
if cjk_latin_spacing { if cjk_latin_spacing {
add_cjk_latin_spacing(&mut items); add_cjk_latin_spacing(&mut items);
} }
Ok(Preparation { Ok(Preparation {
bidi, text,
bidi: is_bidi.then_some(bidi),
items, items,
indices,
spans, spans,
hyphenate: children.shared_get(styles, TextElem::hyphenate_in), hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles), costs: TextElem::costs_in(styles),
dir,
lang: children.shared_get(styles, TextElem::lang_in), lang: children.shared_get(styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x, align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles), justify: ParElem::justify_in(styles),
@ -150,10 +145,14 @@ pub fn prepare<'a>(
/// Add some spacing between Han characters and western characters. See /// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode /// in Horizontal Written Mode
fn add_cjk_latin_spacing(items: &mut [Item]) { fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Tag(_))).peekable(); let mut items = items
.iter_mut()
.filter(|(_, x)| !matches!(x, Item::Tag(_)))
.peekable();
let mut prev: Option<&ShapedGlyph> = None; let mut prev: Option<&ShapedGlyph> = None;
while let Some(item) = items.next() { while let Some((_, item)) = items.next() {
let Some(text) = item.text_mut() else { let Some(text) = item.text_mut() else {
prev = None; prev = None;
continue; continue;
@ -168,7 +167,7 @@ fn add_cjk_latin_spacing(items: &mut [Item]) {
let next = glyphs.peek().map(|n| n as _).or_else(|| { let next = glyphs.peek().map(|n| n as _).or_else(|| {
items items
.peek() .peek()
.and_then(|i| i.text()) .and_then(|(_, i)| i.text())
.and_then(|shaped| shaped.glyphs.first()) .and_then(|shaped| shaped.glyphs.first())
}); });

View File

@ -14,7 +14,6 @@ use super::{Item, Range, SpanMapper};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{Smart, StyleChain}; use crate::foundations::{Smart, StyleChain};
use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use crate::syntax::Span;
use crate::text::{ use crate::text::{
decorate, families, features, variant, Font, FontVariant, Glyph, Lang, Region, decorate, families, features, variant, Font, FontVariant, Glyph, Lang, Region,
TextElem, TextItem, TextElem, TextItem,
@ -27,6 +26,7 @@ use crate::World;
/// This type contains owned or borrowed shaped text runs, which can be /// This type contains owned or borrowed shaped text runs, which can be
/// measured, used to reshape substrings more quickly and converted into a /// measured, used to reshape substrings more quickly and converted into a
/// frame. /// frame.
#[derive(Clone)]
pub struct ShapedText<'a> { pub struct ShapedText<'a> {
/// The start of the text in the full paragraph. /// The start of the text in the full paragraph.
pub base: usize, pub base: usize,
@ -80,8 +80,6 @@ pub struct ShapedGlyph {
pub safe_to_break: bool, pub safe_to_break: bool,
/// The first char in this glyph's cluster. /// The first char in this glyph's cluster.
pub c: char, pub c: char,
/// The source code location of the glyph and its byte offset within it.
pub span: (Span, u16),
/// Whether this glyph is justifiable for CJK scripts. /// Whether this glyph is justifiable for CJK scripts.
pub is_justifiable: bool, pub is_justifiable: bool,
/// The script of the glyph. /// The script of the glyph.
@ -214,6 +212,7 @@ impl<'a> ShapedText<'a> {
pub fn build( pub fn build(
&self, &self,
engine: &Engine, engine: &Engine,
spans: &SpanMapper,
justification_ratio: f64, justification_ratio: f64,
extra_justification: Abs, extra_justification: Abs,
) -> Frame { ) -> Frame {
@ -268,7 +267,7 @@ impl<'a> ShapedText<'a> {
// We may not be able to reach the offset completely if // We may not be able to reach the offset completely if
// it exceeds u16, but better to have a roughly correct // it exceeds u16, but better to have a roughly correct
// span offset than nothing. // span offset than nothing.
let mut span = shaped.span; let mut span = spans.span_at(shaped.range.start);
span.1 = span.1.saturating_add(span_offset.saturating_as()); span.1 = span.1.saturating_add(span_offset.saturating_as());
// |<---- a Glyph ---->| // |<---- a Glyph ---->|
@ -331,7 +330,7 @@ impl<'a> ShapedText<'a> {
} }
/// Measure the top and bottom extent of this text. /// Measure the top and bottom extent of this text.
fn measure(&self, engine: &Engine) -> (Abs, Abs) { pub fn measure(&self, engine: &Engine) -> (Abs, Abs) {
let mut top = Abs::zero(); let mut top = Abs::zero();
let mut bottom = Abs::zero(); let mut bottom = Abs::zero();
@ -409,12 +408,7 @@ impl<'a> ShapedText<'a> {
/// shaping process if possible. /// shaping process if possible.
/// ///
/// The text `range` is relative to the whole paragraph. /// The text `range` is relative to the whole paragraph.
pub fn reshape( pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
&'a self,
engine: &Engine,
spans: &SpanMapper,
text_range: Range,
) -> ShapedText<'a> {
let text = &self.text[text_range.start - self.base..text_range.end - self.base]; 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()) { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -436,7 +430,6 @@ impl<'a> ShapedText<'a> {
engine, engine,
text_range.start, text_range.start,
text, text,
spans,
self.styles, self.styles,
self.dir, self.dir,
self.lang, self.lang,
@ -445,6 +438,16 @@ impl<'a> ShapedText<'a> {
} }
} }
/// Derive an empty text run with the same properties as this one.
pub fn empty(&self) -> Self {
Self {
text: "",
width: Abs::zero(),
glyphs: Cow::Borrowed(&[]),
..*self
}
}
/// Push a hyphen to end of the text. /// Push a hyphen to end of the text.
pub fn push_hyphen(&mut self, engine: &Engine, fallback: bool) { pub fn push_hyphen(&mut self, engine: &Engine, fallback: bool) {
self.insert_hyphen(engine, fallback, Side::Right) self.insert_hyphen(engine, fallback, Side::Right)
@ -493,7 +496,6 @@ impl<'a> ShapedText<'a> {
range, range,
safe_to_break: true, safe_to_break: true,
c: '-', c: '-',
span: (Span::detached(), 0),
is_justifiable: false, is_justifiable: false,
script: Script::Common, script: Script::Common,
}; };
@ -592,11 +594,11 @@ impl Debug for ShapedText<'_> {
/// Group a range of text by BiDi level and script, shape the runs and generate /// Group a range of text by BiDi level and script, shape the runs and generate
/// items for them. /// items for them.
pub fn shape_range<'a>( pub fn shape_range<'a>(
items: &mut Vec<Item<'a>>, items: &mut Vec<(Range, Item<'a>)>,
engine: &Engine, engine: &Engine,
text: &'a str,
bidi: &BidiInfo<'a>, bidi: &BidiInfo<'a>,
range: Range, range: Range,
spans: &SpanMapper,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) { ) {
let script = TextElem::script_in(styles); let script = TextElem::script_in(styles);
@ -604,17 +606,9 @@ pub fn shape_range<'a>(
let region = TextElem::region_in(styles); let region = TextElem::region_in(styles);
let mut process = |range: Range, level: BidiLevel| { let mut process = |range: Range, level: BidiLevel| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped = shape( let shaped =
engine, shape(engine, range.start, &text[range.clone()], styles, dir, lang, region);
range.start, items.push((range, Item::Text(shaped)));
&bidi.text[range],
spans,
styles,
dir,
lang,
region,
);
items.push(Item::Text(shaped));
}; };
let mut prev_level = BidiLevel::ltr(); let mut prev_level = BidiLevel::ltr();
@ -625,14 +619,14 @@ pub fn shape_range<'a>(
// set (rather than inferred from the glyphs), we keep the script at an // set (rather than inferred from the glyphs), we keep the script at an
// unchanging `Script::Unknown` so that only level changes cause breaks. // unchanging `Script::Unknown` so that only level changes cause breaks.
for i in range.clone() { for i in range.clone() {
if !bidi.text.is_char_boundary(i) { if !text.is_char_boundary(i) {
continue; continue;
} }
let level = bidi.levels[i]; let level = bidi.levels[i];
let curr_script = match script { let curr_script = match script {
Smart::Auto => { Smart::Auto => {
bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()) text[i..].chars().next().map_or(Script::Unknown, |c| c.script())
} }
Smart::Custom(_) => Script::Unknown, Smart::Custom(_) => Script::Unknown,
}; };
@ -668,7 +662,6 @@ fn shape<'a>(
engine: &Engine, engine: &Engine,
base: usize, base: usize,
text: &'a str, text: &'a str,
spans: &SpanMapper,
styles: StyleChain<'a>, styles: StyleChain<'a>,
dir: Dir, dir: Dir,
lang: Lang, lang: Lang,
@ -677,7 +670,6 @@ fn shape<'a>(
let size = TextElem::size_in(styles); let size = TextElem::size_in(styles);
let mut ctx = ShapingContext { let mut ctx = ShapingContext {
engine, engine,
spans,
size, size,
glyphs: vec![], glyphs: vec![],
used: vec![], used: vec![],
@ -717,7 +709,6 @@ fn shape<'a>(
/// Holds shaping results and metadata common to all shaped segments. /// Holds shaping results and metadata common to all shaped segments.
struct ShapingContext<'a, 'v> { struct ShapingContext<'a, 'v> {
engine: &'a Engine<'v>, engine: &'a Engine<'v>,
spans: &'a SpanMapper,
glyphs: Vec<ShapedGlyph>, glyphs: Vec<ShapedGlyph>,
used: Vec<Font>, used: Vec<Font>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
@ -830,7 +821,6 @@ fn shape_segment<'a>(
range: start..end, range: start..end,
safe_to_break: !info.unsafe_to_break(), safe_to_break: !info.unsafe_to_break(),
c, c,
span: ctx.spans.span_at(start),
is_justifiable: is_justifiable( is_justifiable: is_justifiable(
c, c,
script, script,
@ -921,7 +911,6 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
range: start..end, range: start..end,
safe_to_break: true, safe_to_break: true,
c, c,
span: ctx.spans.span_at(start),
is_justifiable: is_justifiable( is_justifiable: is_justifiable(
c, c,
script, script,

View File

@ -106,7 +106,6 @@ pub fn define(global: &mut Scope) {
global.define_elem::<ColumnsElem>(); global.define_elem::<ColumnsElem>();
global.define_elem::<ColbreakElem>(); global.define_elem::<ColbreakElem>();
global.define_elem::<PlaceElem>(); global.define_elem::<PlaceElem>();
global.define_elem::<FlushElem>();
global.define_elem::<AlignElem>(); global.define_elem::<AlignElem>();
global.define_elem::<PadElem>(); global.define_elem::<PadElem>();
global.define_elem::<RepeatElem>(); global.define_elem::<RepeatElem>();

View File

@ -46,7 +46,7 @@ pub struct PlaceElem {
/// Floating elements are positioned at the top or bottom of the page, /// Floating elements are positioned at the top or bottom of the page,
/// displacing in-flow content. They are always placed in the in-flow /// displacing in-flow content. They are always placed in the in-flow
/// order relative to each other, as well as before any content following /// order relative to each other, as well as before any content following
/// a later [`flush`] element. /// a later [`place.flush`] element.
/// ///
/// ```example /// ```example
/// #set page(height: 150pt) /// #set page(height: 150pt)

View File

@ -60,10 +60,12 @@ use std::collections::HashSet;
use std::ops::{Deref, Range}; use std::ops::{Deref, Range};
use comemo::{Track, Tracked, Validate}; use comemo::{Track, Tracked, Validate};
use ecow::{EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_timing::{timed, TimingScope}; use typst_timing::{timed, TimingScope};
use crate::diag::{warning, FileResult, SourceDiagnostic, SourceResult, Warned}; use crate::diag::{
warning, FileError, FileResult, SourceDiagnostic, SourceResult, Warned,
};
use crate::engine::{Engine, Route, Sink, Traced}; use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{ use crate::foundations::{
Array, Bytes, Datetime, Dict, Module, Scope, StyleChain, Styles, Value, Array, Bytes, Datetime, Dict, Module, Scope, StyleChain, Styles, Value,
@ -110,13 +112,19 @@ fn compile_inner(
let library = world.library(); let library = world.library();
let styles = StyleChain::new(&library.styles); let styles = StyleChain::new(&library.styles);
// Fetch the main source file once.
let main = world.main();
let main = world
.source(main)
.map_err(|err| hint_invalid_main_file(world, err, main))?;
// First evaluate the main source file into a module. // First evaluate the main source file into a module.
let content = crate::eval::eval( let content = crate::eval::eval(
world, world,
traced, traced,
sink.track_mut(), sink.track_mut(),
Route::default().track(), Route::default().track(),
&world.main(), &main,
)? )?
.content(); .content();
@ -205,8 +213,8 @@ pub trait World: Send + Sync {
/// Metadata about all known fonts. /// Metadata about all known fonts.
fn book(&self) -> &LazyHash<FontBook>; fn book(&self) -> &LazyHash<FontBook>;
/// Access the main source file. /// Get the file id of the main source file.
fn main(&self) -> Source; fn main(&self) -> FileId;
/// Try to access the specified source file. /// Try to access the specified source file.
fn source(&self, id: FileId) -> FileResult<Source>; fn source(&self, id: FileId) -> FileResult<Source>;
@ -248,7 +256,7 @@ macro_rules! delegate_for_ptr {
self.deref().book() self.deref().book()
} }
fn main(&self) -> Source { fn main(&self) -> FileId {
self.deref().main() self.deref().main()
} }
@ -404,3 +412,48 @@ fn prelude(global: &mut Scope) {
global.define("horizon", Alignment::HORIZON); global.define("horizon", Alignment::HORIZON);
global.define("bottom", Alignment::BOTTOM); global.define("bottom", Alignment::BOTTOM);
} }
/// Adds useful hints when the main source file couldn't be read
/// and returns the final diagnostic.
fn hint_invalid_main_file(
world: Tracked<dyn World + '_>,
file_error: FileError,
input: FileId,
) -> EcoVec<SourceDiagnostic> {
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8);
let mut diagnostic =
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
// Attempt to provide helpful hints for UTF-8 errors. Perhaps the user
// mistyped the filename. For example, they could have written "file.pdf"
// instead of "file.typ".
if is_utf8_error {
let path = input.vpath();
let extension = path.as_rootless_path().extension();
if extension.is_some_and(|extension| extension == "typ") {
// No hints if the file is already a .typ file.
// The file is indeed just invalid.
return eco_vec![diagnostic];
}
match extension {
Some(extension) => {
diagnostic.hint(eco_format!(
"a file with the `.{}` extension is not usually a Typst file",
extension.to_string_lossy()
));
}
None => {
diagnostic
.hint("a file without an extension is not usually a Typst file");
}
};
if world.source(input.with_extension("typ")).is_ok() {
diagnostic.hint("check if you meant to use the `.typ` extension instead");
}
}
eco_vec![diagnostic]
}

View File

@ -52,31 +52,47 @@ pub struct AttachElem {
impl LayoutMath for Packed<AttachElem> { impl LayoutMath for Packed<AttachElem> {
#[typst_macros::time(name = "math.attach", span = self.span())] #[typst_macros::time(name = "math.attach", span = self.span())]
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>;
let layout_attachment =
|ctx: &mut MathContext, styles: StyleChain, getter: GetAttachment| {
getter(self, styles)
.map(|elem| ctx.layout_into_fragment(&elem, styles))
.transpose()
};
let base = ctx.layout_into_fragment(self.base(), styles)?; let base = ctx.layout_into_fragment(self.base(), styles)?;
let sup_style = style_for_superscript(styles); let sup_style = style_for_superscript(styles);
let tl = layout_attachment(ctx, styles.chain(&sup_style), AttachElem::tl)?; let sup_style_chain = styles.chain(&sup_style);
let tr = layout_attachment(ctx, styles.chain(&sup_style), AttachElem::tr)?; let tl = self.tl(sup_style_chain);
let t = layout_attachment(ctx, styles.chain(&sup_style), AttachElem::t)?; let tr = self.tr(sup_style_chain);
let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
let t = self.t(sup_style_chain);
let sub_style = style_for_subscript(styles); let sub_style = style_for_subscript(styles);
let bl = layout_attachment(ctx, styles.chain(&sub_style), AttachElem::bl)?; let sub_style_chain = styles.chain(&sub_style);
let br = layout_attachment(ctx, styles.chain(&sub_style), AttachElem::br)?; let bl = self.bl(sub_style_chain);
let b = layout_attachment(ctx, styles.chain(&sub_style), AttachElem::b)?; let br = self.br(sub_style_chain);
let b = self.b(sub_style_chain);
let limits = base.limits().active(styles); let limits = base.limits().active(styles);
let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) }; let (t, tr) = match (t, tr) {
(Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)),
(Some(t), None) if !limits => (None, Some(t)),
(t, tr) => (t, tr),
};
let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
layout_attachments(ctx, styles, base, [tl, t, tr, bl, b, br])
macro_rules! layout {
($content:ident, $style_chain:ident) => {
$content
.map(|elem| ctx.layout_into_fragment(&elem, $style_chain))
.transpose()
};
}
let fragments = [
layout!(tl, sup_style_chain)?,
layout!(t, sup_style_chain)?,
layout!(tr, sup_style_chain)?,
layout!(bl, sub_style_chain)?,
layout!(b, sub_style_chain)?,
layout!(br, sub_style_chain)?,
];
layout_attachments(ctx, styles, base, fragments)
} }
} }
@ -127,7 +143,7 @@ impl LayoutMath for Packed<PrimesElem> {
prime.clone(), prime.clone(),
) )
} }
ctx.push(FrameFragment::new(ctx, styles, frame)); ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true));
} }
} }
Ok(()) Ok(())
@ -245,8 +261,11 @@ fn layout_attachments(
base: MathFragment, base: MathFragment,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> { ) -> SourceResult<()> {
let (shift_up, shift_down) = let (shift_up, shift_down) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
compute_shifts_up_and_down(ctx, styles, &base, [&tl, &tr, &bl, &br]); (Abs::zero(), Abs::zero())
} else {
compute_shifts_up_and_down(ctx, styles, &base, [&tl, &tr, &bl, &br])
};
let sup_delta = Abs::zero(); let sup_delta = Abs::zero();
let sub_delta = -base.italics_correction(); let sub_delta = -base.italics_correction();
@ -271,7 +290,11 @@ fn layout_attachments(
let post_width_max = let post_width_max =
(sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width)); (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width));
let (center_frame, base_offset) = attach_top_and_bottom(ctx, styles, base, t, b); let (center_frame, base_offset) = if t.is_none() && b.is_none() {
(base.into_frame(), Abs::zero())
} else {
attach_top_and_bottom(ctx, styles, base, t, b)
};
if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) { if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) {
ctx.push(FrameFragment::new(ctx, styles, center_frame).with_class(base_class)); ctx.push(FrameFragment::new(ctx, styles, center_frame).with_class(base_class));
return Ok(()); return Ok(());
@ -327,7 +350,7 @@ fn layout_attachments(
} }
fn attach_top_and_bottom( fn attach_top_and_bottom(
ctx: &mut MathContext, ctx: &MathContext,
styles: StyleChain, styles: StyleChain,
base: MathFragment, base: MathFragment,
t: Option<MathFragment>, t: Option<MathFragment>,

View File

@ -65,13 +65,13 @@ pub struct CancelElem {
/// How much to rotate the cancel line. /// How much to rotate the cancel line.
/// ///
/// - If given an angle, the line is rotated by that angle clockwise with
/// respect to the y-axis.
/// - If `{auto}`, the line assumes the default angle; that is, along the /// - If `{auto}`, the line assumes the default angle; that is, along the
/// diagonal line of the content box. /// rising diagonal of the content box.
/// - If given an angle, the line is rotated by that angle clockwise w.r.t /// - If given a function `angle => angle`, the line is rotated, with
/// the y-axis. /// respect to the y-axis, by the angle returned by that function. The
/// - If given a function `angle => angle`, the line is rotated by the angle /// function receives the default angle as its input.
/// returned by that function. The function receives the default angle as
/// its input.
/// ///
/// ```example /// ```example
/// >>> #set page(width: 140pt) /// >>> #set page(width: 140pt)

View File

@ -71,6 +71,13 @@ impl MathFragment {
} }
} }
pub fn is_ignorant(&self) -> bool {
match self {
Self::Frame(fragment) => fragment.ignorant,
_ => false,
}
}
pub fn class(&self) -> MathClass { pub fn class(&self) -> MathClass {
match self { match self {
Self::Glyph(glyph) => glyph.class, Self::Glyph(glyph) => glyph.class,
@ -120,18 +127,19 @@ impl MathFragment {
} }
pub fn is_spaced(&self) -> bool { pub fn is_spaced(&self) -> bool {
self.class() == MathClass::Fence if self.class() == MathClass::Fence {
|| match self { return true;
MathFragment::Frame(frame) => { }
frame.spaced
&& matches!( matches!(
frame.class, self,
MathClass::Normal | MathClass::Alphabetic MathFragment::Frame(FrameFragment {
spaced: true,
class: MathClass::Normal | MathClass::Alphabetic,
..
})
) )
} }
_ => false,
}
}
pub fn is_text_like(&self) -> bool { pub fn is_text_like(&self) -> bool {
match self { match self {
@ -440,6 +448,7 @@ pub struct FrameFragment {
pub italics_correction: Abs, pub italics_correction: Abs,
pub accent_attach: Abs, pub accent_attach: Abs,
pub text_like: bool, pub text_like: bool,
pub ignorant: bool,
} }
impl FrameFragment { impl FrameFragment {
@ -458,6 +467,7 @@ impl FrameFragment {
italics_correction: Abs::zero(), italics_correction: Abs::zero(),
accent_attach, accent_attach,
text_like: false, text_like: false,
ignorant: false,
} }
} }
@ -488,6 +498,10 @@ impl FrameFragment {
pub fn with_text_like(self, text_like: bool) -> Self { pub fn with_text_like(self, text_like: bool) -> Self {
Self { text_like, ..self } Self { text_like, ..self }
} }
pub fn with_ignorant(self, ignorant: bool) -> Self {
Self { ignorant, ..self }
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -49,6 +49,7 @@ use crate::foundations::{
}; };
use crate::introspection::TagElem; use crate::introspection::TagElem;
use crate::layout::{BoxElem, Frame, FrameItem, HElem, Point, Size, Spacing, VAlignment}; use crate::layout::{BoxElem, Frame, FrameItem, HElem, Point, Size, Spacing, VAlignment};
use crate::realize::Behaviour;
use crate::realize::{process, BehavedBuilder}; use crate::realize::{process, BehavedBuilder};
use crate::text::{LinebreakElem, SpaceElem, TextElem}; use crate::text::{LinebreakElem, SpaceElem, TextElem};
@ -299,7 +300,7 @@ impl LayoutMath for Content {
if let Some(elem) = self.to_packed::<TagElem>() { if let Some(elem) = self.to_packed::<TagElem>() {
let mut frame = Frame::soft(Size::zero()); let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag(elem.tag.clone())); frame.push(Point::zero(), FrameItem::Tag(elem.tag.clone()));
ctx.push(FrameFragment::new(ctx, styles, frame)); ctx.push(FrameFragment::new(ctx, styles, frame).with_ignorant(true));
return Ok(()); return Ok(());
} }
@ -312,7 +313,15 @@ impl LayoutMath for Content {
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis); frame.set_baseline(frame.height() / 2.0 + axis);
} }
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
ctx.push(
FrameFragment::new(ctx, styles, frame)
.with_spaced(true)
.with_ignorant(matches!(
self.behaviour(),
Behaviour::Invisible | Behaviour::Ignorant
)),
);
Ok(()) Ok(())
} }

View File

@ -77,7 +77,8 @@ impl MathRun {
fragment.set_class(MathClass::Binary); fragment.set_class(MathClass::Binary);
} }
// Insert spacing between the last and this item. // Insert spacing between the last and this non-ignorant item.
if !fragment.is_ignorant() {
if let Some(i) = last { if let Some(i) = last {
if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { if let Some(s) = spacing(&resolved[i], space.take(), &fragment) {
resolved.insert(i + 1, s); resolved.insert(i + 1, s);
@ -85,6 +86,8 @@ impl MathRun {
} }
last = Some(resolved.len()); last = Some(resolved.len());
}
resolved.push(fragment); resolved.push(fragment);
} }
@ -156,10 +159,19 @@ impl MathRun {
pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment { pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment {
if self.0.len() == 1 { if self.0.len() == 1 {
self.0.into_iter().next().unwrap() return self.0.into_iter().next().unwrap();
} else {
FrameFragment::new(ctx, styles, self.into_frame(ctx, styles)).into()
} }
// Fragments without a math_size are ignored: the notion of size do not
// apply to them, so their text-likeness is meaningless.
let text_like = self
.iter()
.filter(|e| e.math_size().is_some())
.all(|e| e.is_text_like());
FrameFragment::new(ctx, styles, self.into_frame(ctx, styles))
.with_text_like(text_like)
.into()
} }
/// Returns a builder that lays out the [`MathFragment`]s into a possibly /// Returns a builder that lays out the [`MathFragment`]s into a possibly

View File

@ -93,6 +93,15 @@ impl FootnoteElem {
Self::new(FootnoteBody::Reference(label)) Self::new(FootnoteBody::Reference(label))
} }
/// Creates a new footnote referencing the footnote with the specified label,
/// with the other fields from the current footnote cloned.
pub fn into_ref(&self, label: Label) -> Self {
Self {
body: FootnoteBody::Reference(label),
..self.clone()
}
}
/// Tests if this footnote is a reference to another footnote. /// Tests if this footnote is a reference to another footnote.
pub fn is_ref(&self) -> bool { pub fn is_ref(&self) -> bool {
matches!(self.body(), FootnoteBody::Reference(_)) matches!(self.body(), FootnoteBody::Reference(_))

View File

@ -483,7 +483,7 @@ impl OutlineEntry {
impl Show for Packed<OutlineEntry> { impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())] #[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut seq = vec![]; let mut seq = vec![];
let elem = self.element(); let elem = self.element();
@ -500,7 +500,11 @@ impl Show for Packed<OutlineEntry> {
}; };
// The body text remains overridable. // The body text remains overridable.
seq.push(self.body().clone().linked(Destination::Location(location))); crate::text::isolate(
self.body().clone().linked(Destination::Location(location)),
styles,
&mut seq,
);
// Add filler symbols between the section name and page number. // Add filler symbols between the section name and page number.
if let Some(filler) = self.fill() { if let Some(filler) = self.fill() {

View File

@ -18,9 +18,9 @@ use crate::realize::StyleVec;
/// ///
/// # Example /// # Example
/// ```example /// ```example
/// #show par: set block(spacing: 0.65em)
/// #set par( /// #set par(
/// first-line-indent: 1em, /// first-line-indent: 1em,
/// spacing: 0.65em,
/// justify: true, /// justify: true,
/// ) /// )
/// ///
@ -115,8 +115,7 @@ pub struct ParElem {
/// By typographic convention, paragraph breaks are indicated either by some /// By typographic convention, paragraph breaks are indicated either by some
/// space between paragraphs or by indented first lines. Consider reducing /// space between paragraphs or by indented first lines. Consider reducing
/// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading)
/// when using this property (e.g. using /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`).
/// `[#show par: set block(spacing: 0.65em)]`).
#[ghost] #[ghost]
pub first_line_indent: Length, pub first_line_indent: Length,

View File

@ -177,8 +177,8 @@ impl Show for Packed<RefElem> {
let elem = elem.at(span)?; let elem = elem.at(span)?;
if elem.func() == FootnoteElem::elem() { if let Some(footnote) = elem.to_packed::<FootnoteElem>() {
return Ok(FootnoteElem::with_label(target).pack().spanned(span)); return Ok(footnote.into_ref(target).pack().spanned(span));
} }
let elem = elem.clone(); let elem = elem.clone();

View File

@ -480,7 +480,6 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
checkmark: ['✓', light: '🗸', heavy: ''], checkmark: ['✓', light: '🗸', heavy: ''],
crossmark: ['✗', heavy: ''], crossmark: ['✗', heavy: ''],
floral: ['❦', l: '', r: ''], floral: ['❦', l: '', r: ''],
notes: [up: '🎜', down: '🎝'],
refmark: '', refmark: '',
servicemark: '', servicemark: '',
maltese: '', maltese: '',
@ -495,6 +494,51 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
spade.stroked: '', spade.stroked: '',
], ],
// Music.
note: [
up: '🎜',
down: '🎝',
whole: '𝅝',
half: '𝅗𝅥',
quarter: '𝅘𝅥',
quarter.alt: '',
eighth: '𝅘𝅥𝅮',
eighth.alt: '',
eighth.beamed: '',
sixteenth: '𝅘𝅥𝅯',
sixteenth.beamed: '',
grace: '𝆕',
grace.slash: '𝆔',
],
rest: [
whole: '𝄻',
multiple: '𝄺',
multiple.measure: '𝄩',
half: '𝄼',
quarter: '𝄽',
eighth: '𝄾',
sixteenth: '𝄿',
],
natural: [
'♮',
t: '𝄮',
b: '𝄯',
],
flat: [
'♭',
t: '𝄬',
b: '𝄭',
double: '𝄫',
quarter: '𝄳',
],
sharp: [
'♯',
t: '𝄰',
b: '𝄱',
double: '𝄪',
quarter: '𝄲',
],
// Shapes. // Shapes.
bullet: '', bullet: '',
circle: [ circle: [

View File

@ -48,6 +48,16 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! {
// See https://corefonts.sourceforge.net/. // See https://corefonts.sourceforge.net/.
"Arial-Black" => Exception::new() "Arial-Black" => Exception::new()
.weight(900), .weight(900),
// Archivo Narrow is different from Archivo and Archivo Black. Since Archivo Black seems
// identical to Archivo weight 900, only differentiate between Archivo and Archivo Narrow.
"ArchivoNarrow-Regular" => Exception::new()
.family("Archivo Narrow"),
"ArchivoNarrow-Italic" => Exception::new()
.family("Archivo Narrow"),
"ArchivoNarrow-Bold" => Exception::new()
.family("Archivo Narrow"),
"ArchivoNarrow-BoldItalic" => Exception::new()
.family("Archivo Narrow"),
// Fandol fonts designed for Chinese typesetting. // Fandol fonts designed for Chinese typesetting.
// See https://ctan.org/tex-archive/fonts/fandol/. // See https://ctan.org/tex-archive/fonts/fandol/.
"FandolHei-Bold" => Exception::new() "FandolHei-Bold" => Exception::new()

View File

@ -1300,3 +1300,13 @@ cast! {
ret ret
}, },
} }
/// Pushes `text` wrapped in LRE/RLE + PDF to `out`.
pub(crate) fn isolate(text: Content, styles: StyleChain, out: &mut Vec<Content>) {
out.push(TextElem::packed(match TextElem::dir_in(styles) {
Dir::RTL => "\u{202B}",
_ => "\u{202A}",
}));
out.push(text);
out.push(TextElem::packed("\u{202C}"));
}

View File

@ -123,7 +123,7 @@ impl SmartQuoter {
/// Process the last seen character. /// Process the last seen character.
pub fn last(&mut self, c: char, is_quote: bool) { pub fn last(&mut self, c: char, is_quote: bool) {
self.expect_opening = is_ignorable(c) || is_opening_bracket(c); self.expect_opening = is_exterior_to_quote(c) || is_opening_bracket(c);
self.last_num = c.is_numeric(); self.last_num = c.is_numeric();
if !is_quote { if !is_quote {
self.prev_quote_type = None; self.prev_quote_type = None;
@ -150,7 +150,7 @@ impl SmartQuoter {
self.prev_quote_type = Some(double); self.prev_quote_type = Some(double);
quotes.open(double) quotes.open(double)
} else if self.quote_depth > 0 } else if self.quote_depth > 0
&& (peeked.is_ascii_punctuation() || is_ignorable(peeked)) && (peeked.is_ascii_punctuation() || is_exterior_to_quote(peeked))
{ {
self.quote_depth -= 1; self.quote_depth -= 1;
quotes.close(double) quotes.close(double)
@ -168,7 +168,7 @@ impl Default for SmartQuoter {
} }
} }
fn is_ignorable(c: char) -> bool { fn is_exterior_to_quote(c: char) -> bool {
c.is_whitespace() || is_newline(c) c.is_whitespace() || is_newline(c)
} }

View File

@ -196,7 +196,7 @@ fn layout_image(
format, format,
elem.alt(styles), elem.alt(styles),
engine.world, engine.world,
&families(styles).map(|s| s.into()).collect::<Vec<_>>(), &families(styles).collect::<Vec<_>>(),
) )
.at(span)?; .at(span)?;
@ -360,7 +360,7 @@ impl Image {
format: ImageFormat, format: ImageFormat,
alt: Option<EcoString>, alt: Option<EcoString>,
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
families: &[String], families: &[&str],
) -> StrResult<Image> { ) -> StrResult<Image> {
let kind = match format { let kind = match format {
ImageFormat::Raster(format) => { ImageFormat::Raster(format) => {

View File

@ -30,11 +30,11 @@ impl RasterImage {
/// Decode a raster image. /// Decode a raster image.
#[comemo::memoize] #[comemo::memoize]
pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> { pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> {
fn decode_with<'a, T: ImageDecoder<'a>>( fn decode_with<T: ImageDecoder>(
decoder: ImageResult<T>, decoder: ImageResult<T>,
) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> { ) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> {
let mut decoder = decoder?; let mut decoder = decoder?;
let icc = decoder.icc_profile().filter(|icc| !icc.is_empty()); let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty());
decoder.set_limits(Limits::default())?; decoder.set_limits(Limits::default())?;
let dynamic = image::DynamicImage::from_decoder(decoder)?; let dynamic = image::DynamicImage::from_decoder(decoder)?;
Ok((dynamic, icc)) Ok((dynamic, icc))

View File

@ -40,7 +40,7 @@ impl SvgImage {
pub fn with_fonts( pub fn with_fonts(
data: Bytes, data: Bytes,
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
families: &[String], families: &[&str],
) -> StrResult<SvgImage> { ) -> StrResult<SvgImage> {
let book = world.book(); let book = world.book();
let resolver = Mutex::new(FontResolver::new(world, book, families)); let resolver = Mutex::new(FontResolver::new(world, book, families));
@ -142,7 +142,7 @@ struct FontResolver<'a> {
/// The world we use to load fonts. /// The world we use to load fonts.
world: Tracked<'a, dyn World + 'a>, world: Tracked<'a, dyn World + 'a>,
/// The active list of font families at the location of the SVG. /// The active list of font families at the location of the SVG.
families: &'a [String], families: &'a [&'a str],
/// A mapping from Typst font indices to fontdb IDs. /// A mapping from Typst font indices to fontdb IDs.
to_id: HashMap<usize, Option<fontdb::ID>>, to_id: HashMap<usize, Option<fontdb::ID>>,
/// The reverse mapping. /// The reverse mapping.
@ -156,7 +156,7 @@ impl<'a> FontResolver<'a> {
fn new( fn new(
world: Tracked<'a, dyn World + 'a>, world: Tracked<'a, dyn World + 'a>,
book: &'a FontBook, book: &'a FontBook,
families: &'a [String], families: &'a [&'a str],
) -> Self { ) -> Self {
Self { Self {
book, book,
@ -191,11 +191,11 @@ impl FontResolver<'_> {
font.families() font.families()
.iter() .iter()
.filter_map(|family| match family { .filter_map(|family| match family {
usvg::FontFamily::Named(named) => Some(named), usvg::FontFamily::Named(named) => Some(named.as_str()),
// We don't support generic families at the moment. // We don't support generic families at the moment.
_ => None, _ => None,
}) })
.chain(self.families) .chain(self.families.iter().copied())
.filter_map(|named| self.book.select(&named.to_lowercase(), variant)) .filter_map(|named| self.book.select(&named.to_lowercase(), variant))
.find_map(|index| self.get_or_load(index, db)) .find_map(|index| self.get_or_load(index, db))
} }

View File

@ -593,10 +593,9 @@ The example below
```typ ```typ
#set page(margin: 1.75in) #set page(margin: 1.75in)
#set par(leading: 0.55em, first-line-indent: 1.8em, justify: true) #set par(leading: 0.55em, spacing: 0.55em, first-line-indent: 1.8em, justify: true)
#set text(font: "New Computer Modern") #set text(font: "New Computer Modern")
#show raw: set text(font: "New Computer Modern Mono") #show raw: set text(font: "New Computer Modern Mono")
#show par: set block(spacing: 0.55em)
#show heading: set block(above: 1.4em, below: 1em) #show heading: set block(above: 1.4em, below: 1em)
``` ```

View File

@ -120,7 +120,7 @@ a table listing all syntax that is available in code mode:
| Named function | `{let f(x) = 2 * x}` | [Function]($function) | | Named function | `{let f(x) = 2 * x}` | [Function]($function) |
| Set rule | `{set text(14pt)}` | [Styling]($styling/#set-rules) | | Set rule | `{set text(14pt)}` | [Styling]($styling/#set-rules) |
| Set-if rule | `{set text(..) if .. }` | [Styling]($styling/#set-rules) | | Set-if rule | `{set text(..) if .. }` | [Styling]($styling/#set-rules) |
| Show-set rule | `{show par: set block(..)}` | [Styling]($styling/#show-rules) | | Show-set rule | `{show heading: set block(..)}` | [Styling]($styling/#show-rules) |
| Show rule with function | `{show raw: it => {..}}` | [Styling]($styling/#show-rules) | | Show rule with function | `{show raw: it => {..}}` | [Styling]($styling/#show-rules) |
| Show-everything rule | `{show: columns.with(2)}` | [Styling]($styling/#show-rules) | | Show-everything rule | `{show: columns.with(2)}` | [Styling]($styling/#show-rules) |
| Context expression | `{context text.lang}` | [Context]($context) | | Context expression | `{context text.lang}` | [Context]($context) |

View File

@ -6,7 +6,7 @@ use heck::{ToKebabCase, ToTitleCase};
use pulldown_cmark as md; use pulldown_cmark as md;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typed_arena::Arena; use typed_arena::Arena;
use typst::diag::{FileResult, StrResult}; use typst::diag::{FileError, FileResult, StrResult};
use typst::foundations::{Bytes, Datetime}; use typst::foundations::{Bytes, Datetime};
use typst::layout::{Abs, Point, Size}; use typst::layout::{Abs, Point, Size};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
@ -463,12 +463,16 @@ impl World for DocWorld {
&FONTS.0 &FONTS.0
} }
fn main(&self) -> Source { fn main(&self) -> FileId {
self.0.clone() self.0.id()
} }
fn source(&self, _: FileId) -> FileResult<Source> { fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.0.id() {
Ok(self.0.clone()) Ok(self.0.clone())
} else {
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
}
} }
fn file(&self, id: FileId) -> FileResult<Bytes> { fn file(&self, id: FileId) -> FileResult<Bytes> {

View File

@ -44,8 +44,8 @@ static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| {
.module() .module()
.scope() .scope()
.iter() .iter()
.filter(|(_, v)| matches!(v, Value::Func(_))) .filter(|(_, v, _)| matches!(v, Value::Func(_)))
.map(|(k, _)| k.clone()) .map(|(k, _, _)| k.clone())
.collect(); .collect();
} }
} }
@ -249,7 +249,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
// Add values and types. // Add values and types.
let scope = module.scope(); let scope = module.scope();
for (name, value) in scope.iter() { for (name, value, _) in scope.iter() {
if scope.get_category(name) != Some(category) { if scope.get_category(name) != Some(category) {
continue; continue;
} }
@ -463,7 +463,7 @@ fn casts(
fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec<FuncModel> { fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec<FuncModel> {
scope scope
.iter() .iter()
.filter_map(|(_, value)| { .filter_map(|(_, value, _)| {
let Value::Func(func) = value else { return None }; let Value::Func(func) = value else { return None };
Some(func_model(resolver, func, &[name], true)) Some(func_model(resolver, func, &[name], true))
}) })
@ -649,7 +649,7 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> Pag
/// Produce a symbol list's model. /// Produce a symbol list's model.
fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
let mut list = vec![]; let mut list = vec![];
for (name, value) in group.module().scope().iter() { for (name, value, _) in group.module().scope().iter() {
let Value::Symbol(symbol) = value else { continue }; let Value::Symbol(symbol) = value else { continue };
let complete = |variant: &str| { let complete = |variant: &str| {
if variant.is_empty() { if variant.is_empty() {

View File

@ -39,16 +39,20 @@ impl World for FuzzWorld {
&self.book &self.book
} }
fn main(&self) -> Source { fn main(&self) -> FileId {
self.source.clone() self.source.id()
} }
fn source(&self, src: FileId) -> FileResult<Source> { fn source(&self, id: FileId) -> FileResult<Source> {
Err(FileError::NotFound(src.vpath().as_rootless_path().into())) if id == self.source.id() {
Ok(self.source.clone())
} else {
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
}
} }
fn file(&self, src: FileId) -> FileResult<Bytes> { fn file(&self, id: FileId) -> FileResult<Bytes> {
Err(FileError::NotFound(src.vpath().as_rootless_path().into())) Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
} }
fn font(&self, _: usize) -> Option<Font> { fn font(&self, _: usize) -> Option<Font> {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 850 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 B

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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