Merge typst and typst-library

This commit is contained in:
Laurenz 2023-11-23 16:25:49 +01:00
parent 76e173b78b
commit 7eebafa783
250 changed files with 9566 additions and 9027 deletions

61
Cargo.lock generated
View File

@ -2704,14 +2704,26 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
name = "typst"
version = "0.9.0"
dependencies = [
"az",
"bitflags 2.4.1",
"chinese-number",
"ciborium",
"comemo",
"csv",
"ecow",
"fontdb",
"hayagriva",
"hypher",
"icu_properties",
"icu_provider",
"icu_provider_adapters",
"icu_provider_blob",
"icu_segmenter",
"image",
"indexmap 2.0.2",
"kurbo",
"lasso",
"lipsum",
"log",
"once_cell",
"palette",
@ -2720,16 +2732,22 @@ dependencies = [
"roxmltree",
"rustybuzz",
"serde",
"serde_json",
"serde_yaml 0.9.27",
"siphasher",
"smallvec",
"stacker",
"syntect",
"time",
"toml",
"tracing",
"ttf-parser",
"typed-arena",
"typst-macros",
"typst-syntax",
"unicode-bidi",
"unicode-math-class",
"unicode-script",
"unicode-segmentation",
"usvg",
"wasmi",
@ -2771,7 +2789,6 @@ dependencies = [
"tracing-flame",
"tracing-subscriber",
"typst",
"typst-library",
"typst-pdf",
"typst-render",
"typst-svg",
@ -2795,7 +2812,6 @@ dependencies = [
"syntect",
"typed-arena",
"typst",
"typst-library",
"unicode_names2",
"unscanny",
"yaml-front-matter",
@ -2814,46 +2830,6 @@ dependencies = [
"unscanny",
]
[[package]]
name = "typst-library"
version = "0.9.0"
dependencies = [
"az",
"chinese-number",
"ciborium",
"comemo",
"csv",
"ecow",
"hayagriva",
"hypher",
"icu_properties",
"icu_provider",
"icu_provider_adapters",
"icu_provider_blob",
"icu_segmenter",
"indexmap 2.0.2",
"kurbo",
"lipsum",
"log",
"once_cell",
"roxmltree",
"rustybuzz",
"serde_json",
"serde_yaml 0.9.27",
"smallvec",
"syntect",
"time",
"toml",
"tracing",
"ttf-parser",
"typed-arena",
"typst",
"unicode-bidi",
"unicode-math-class",
"unicode-script",
"unicode-segmentation",
]
[[package]]
name = "typst-macros"
version = "0.9.0"
@ -2948,7 +2924,6 @@ dependencies = [
"tiny-skia",
"ttf-parser",
"typst",
"typst-library",
"typst-pdf",
"typst-render",
"typst-svg",

View File

@ -20,7 +20,6 @@ typst = { path = "crates/typst" }
typst-cli = { path = "crates/typst-cli" }
typst-docs = { path = "crates/typst-docs" }
typst-ide = { path = "crates/typst-ide" }
typst-library = { path = "crates/typst-library" }
typst-macros = { path = "crates/typst-macros" }
typst-pdf = { path = "crates/typst-pdf" }
typst-render = { path = "crates/typst-render" }
@ -98,7 +97,7 @@ tar = "0.4"
tempfile = "3.7.0"
time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
tiny-skia = "0.11"
toml = { version = "0.8", default-features = false, features = ["parse"] }
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
tracing = "0.1.37"
tracing-error = "0.2"
tracing-flame = "0.2.0"

View File

@ -21,7 +21,6 @@ doc = false
[dependencies]
typst = { workspace = true }
typst-library = { workspace = true }
typst-pdf = { workspace = true }
typst-render = { workspace = true }
typst-svg = { workspace = true }

View File

@ -1,9 +1,8 @@
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use semver::Version;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
use semver::Version;
/// The character typically used to separate path components
/// in environment variables.

View File

@ -4,12 +4,14 @@ use std::path::{Path, PathBuf};
use chrono::{Datelike, Timelike};
use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term::{self, termcolor};
use ecow::eco_format;
use termcolor::{ColorChoice, StandardStream};
use typst::diag::{bail, Severity, SourceDiagnostic, StrResult};
use typst::doc::Document;
use typst::eval::{eco_format, Datetime, Tracer};
use typst::geom::Color;
use typst::eval::Tracer;
use typst::foundations::Datetime;
use typst::model::Document;
use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color;
use typst::{World, WorldExt};
use crate::args::{CompileCommand, DiagnosticFormat, OutputFormat};

View File

@ -4,7 +4,7 @@ use std::path::PathBuf;
use fontdb::{Database, Source};
use typst::diag::StrResult;
use typst::font::{Font, FontBook, FontInfo, FontVariant};
use typst::text::{Font, FontBook, FontInfo, FontVariant};
use crate::args::FontsCommand;
@ -106,7 +106,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) {
let mut process = |bytes: &'static [u8]| {
let buffer = typst::eval::Bytes::from_static(bytes);
let buffer = typst::foundations::Bytes::from_static(bytes);
for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone());
self.fonts.push(FontSlot {

View File

@ -1,10 +1,13 @@
use comemo::Track;
use ecow::{eco_format, EcoString};
use serde::Serialize;
use typst::diag::{bail, StrResult};
use typst::eval::{eval_string, EvalMode, Tracer};
use typst::model::Introspector;
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::introspection::Introspector;
use typst::model::Document;
use typst::syntax::Span;
use typst::World;
use typst_library::prelude::*;
use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics;
@ -95,7 +98,7 @@ fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
.collect();
if command.one {
let Some(value) = mapped.get(0) else {
let Some(value) = mapped.first() else {
bail!("no such field found for element");
};
serialize(value, command.format)

View File

@ -6,8 +6,9 @@ use inferno::flamegraph::Options;
use tracing::metadata::LevelFilter;
use tracing_error::ErrorLayer;
use tracing_flame::{FlameLayer, FlushGuard};
use tracing_subscriber::fmt;
use tracing_subscriber::prelude::*;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{fmt, Layer};
use crate::args::{CliArguments, Command};

View File

@ -1,13 +1,12 @@
use std::env;
use std::fs;
use std::io::{Cursor, Read, Write};
use std::path::PathBuf;
use std::{env, fs};
use ecow::eco_format;
use semver::Version;
use serde::Deserialize;
use tempfile::NamedTempFile;
use typst::diag::{bail, StrResult};
use typst::eval::eco_format;
use xz2::bufread::XzDecoder;
use zip::ZipArchive;

View File

@ -3,11 +3,11 @@ use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use codespan_reporting::term::{self, termcolor};
use ecow::eco_format;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use same_file::is_same_file;
use termcolor::WriteColor;
use typst::diag::StrResult;
use typst::eval::eco_format;
use crate::args::CompileCommand;
use crate::color_stream;

View File

@ -5,13 +5,14 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Local};
use comemo::Prehashed;
use ecow::eco_format;
use typst::diag::{FileError, FileResult, StrResult};
use typst::doc::Frame;
use typst::eval::{eco_format, Bytes, Datetime, Library};
use typst::font::{Font, FontBook};
use typst::foundations::{Bytes, Datetime};
use typst::layout::Frame;
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::util::hash128;
use typst::World;
use typst::{Library, World};
use crate::args::SharedArgs;
use crate::fonts::{FontSearcher, FontSlot};
@ -75,7 +76,7 @@ impl SystemWorld {
input,
root,
main: FileId::new(None, main_path),
library: Prehashed::new(typst_library::build()),
library: Prehashed::new(Library::build()),
book: Prehashed::new(searcher.book),
fonts: searcher.fonts,
slots: RefCell::default(),

View File

@ -12,7 +12,6 @@ bench = false
[dependencies]
typst = { workspace = true }
typst-library = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
heck = { workspace = true }

View File

@ -4,7 +4,7 @@ use std::fmt::Write;
use serde::{Deserialize, Serialize};
use super::{Html, Resolver};
use crate::{Html, Resolver};
/// Build HTML detailing the contributors between two tags.
pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option<Html> {

View File

@ -8,11 +8,12 @@ use pulldown_cmark as md;
use serde::{Deserialize, Serialize};
use typed_arena::Arena;
use typst::diag::{FileResult, StrResult};
use typst::eval::{Bytes, Datetime, Library, Tracer};
use typst::font::{Font, FontBook};
use typst::geom::{Abs, Point, Size};
use typst::eval::Tracer;
use typst::foundations::{Bytes, Datetime};
use typst::layout::{Abs, Point, Size};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::World;
use typst::text::{Font, FontBook};
use typst::{Library, World};
use unscanny::Scanner;
use yaml_front_matter::YamlFrontMatter;
@ -360,13 +361,13 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
buf.push_str("</pre>");
return Html::new(buf);
} else if !matches!(lang, "example" | "typ" | "preview") {
let set = &*typst_library::text::SYNTAXES;
let set = &*typst::text::RAW_SYNTAXES;
let buf = syntect::html::highlighted_html_for_string(
&display,
set,
set.find_syntax_by_token(lang)
.unwrap_or_else(|| panic!("unsupported highlighting language: {lang}")),
&typst_library::text::THEME,
&typst::text::RAW_THEME,
)
.expect("failed to highlight code");
return Html::new(buf);

View File

@ -5,9 +5,9 @@ mod html;
mod link;
mod model;
pub use contribs::{contributors, Author, Commit};
pub use html::Html;
pub use model::*;
pub use self::contribs::*;
pub use self::html::*;
pub use self::model::*;
use std::path::Path;
@ -20,30 +20,48 @@ use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_yaml as yaml;
use typst::diag::{bail, StrResult};
use typst::doc::Frame;
use typst::eval::{
CastInfo, Func, Library, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
use typst::foundations::{
CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
FOUNDATIONS,
};
use typst::font::{Font, FontBook};
use typst::geom::Abs;
use typst_library::layout::{Margin, PageElem};
use typst::introspection::INTROSPECTION;
use typst::layout::{Abs, Frame, Margin, PageElem, LAYOUT};
use typst::loading::DATA_LOADING;
use typst::math::MATH;
use typst::model::MODEL;
use typst::symbols::SYMBOLS;
use typst::text::{Font, FontBook, TEXT};
use typst::visualize::VISUALIZE;
use typst::Library;
static DOCS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../docs");
static FILE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/files");
static FONT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../assets/fonts");
static CATEGORIES: Lazy<yaml::Mapping> = Lazy::new(|| yaml("reference/categories.yml"));
static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| yaml("reference/groups.yml"));
static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| {
let mut groups: Vec<GroupData> = yaml("reference/groups.yml");
for group in &mut groups {
if group.filter.is_empty() {
group.filter = group
.module()
.scope()
.iter()
.filter(|(_, v)| matches!(v, Value::Func(_)))
.map(|(k, _)| k.clone())
.collect();
}
}
groups
});
static LIBRARY: Lazy<Prehashed<Library>> = Lazy::new(|| {
let mut lib = typst_library::build();
let mut lib = Library::build();
lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));
lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
Abs::pt(15.0).into(),
)))));
typst::eval::set_lang_items(lib.items.clone());
Prehashed::new(lib)
});
@ -128,14 +146,15 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel {
.with_part("Language"),
markdown_page(resolver, "/docs/reference/", "reference/styling.md"),
markdown_page(resolver, "/docs/reference/", "reference/scripting.md"),
category_page(resolver, "foundations").with_part("Library"),
category_page(resolver, "text"),
category_page(resolver, "math"),
category_page(resolver, "layout"),
category_page(resolver, "visualize"),
category_page(resolver, "meta"),
category_page(resolver, "symbols"),
category_page(resolver, "data-loading"),
category_page(resolver, FOUNDATIONS).with_part("Library"),
category_page(resolver, MODEL),
category_page(resolver, TEXT),
category_page(resolver, MATH),
category_page(resolver, SYMBOLS),
category_page(resolver, LAYOUT),
category_page(resolver, VISUALIZE),
category_page(resolver, INTROSPECTION),
category_page(resolver, DATA_LOADING),
];
page
}
@ -152,50 +171,73 @@ fn guide_pages(resolver: &dyn Resolver) -> PageModel {
/// Build the packages section.
fn packages_page(resolver: &dyn Resolver) -> PageModel {
let md = DOCS_DIR
.get_file("reference/packages.md")
.unwrap()
.contents_utf8()
.unwrap();
PageModel {
route: "/docs/packages/".into(),
title: "Packages".into(),
description: "Packages for Typst.".into(),
part: None,
outline: vec![],
body: BodyModel::Packages(Html::markdown(
resolver,
category_details("packages"),
Some(1),
)),
body: BodyModel::Packages(Html::markdown(resolver, md, Some(1))),
children: vec![],
}
}
/// Create a page for a category.
#[track_caller]
fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
let route = eco_format!("/docs/reference/{category}/");
fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
let route = eco_format!("/docs/reference/{}/", category.name());
let mut children = vec![];
let mut items = vec![];
let mut shorthands = None;
let mut markup = vec![];
let mut math = vec![];
let (module, path): (&Module, &[&str]) = match category {
"math" => (&LIBRARY.math, &["math"]),
_ => (&LIBRARY.global, &[]),
let (module, path): (&Module, &[&str]) = if category == MATH {
(&LIBRARY.math, &["math"])
} else {
(&LIBRARY.global, &[])
};
// Add groups.
for mut group in GROUPS.iter().filter(|g| g.category == category).cloned() {
let mut focus = module;
if matches!(group.name.as_str(), "calc" | "sys") {
focus = get_module(focus, &group.name).unwrap();
group.functions = focus
.scope()
.iter()
.filter(|(_, v)| matches!(v, Value::Func(_)))
.map(|(k, _)| k.clone())
.collect();
for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() {
if matches!(group.name.as_str(), "sym" | "emoji") {
let subpage = symbols_page(resolver, &route, &group);
let BodyModel::Symbols(model) = &subpage.body else { continue };
let list = &model.list;
markup.extend(
list.iter()
.filter(|symbol| symbol.markup_shorthand.is_some())
.cloned(),
);
math.extend(
list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(),
);
items.push(CategoryItem {
name: group.name.clone(),
route: subpage.route.clone(),
oneliner: oneliner(category.docs()).into(),
code: true,
});
children.push(subpage);
continue;
}
let (child, item) = group_page(resolver, &route, &group, focus.scope());
let (child, item) = group_page(resolver, &route, &group);
children.push(child);
items.push(item);
}
// Add symbol pages. These are ordered manually.
if category == SYMBOLS {
shorthands = Some(ShorthandsModel { markup, math });
}
// Add functions.
let scope = module.scope();
for (name, value) in scope.iter() {
@ -203,9 +245,9 @@ fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
continue;
}
if category == "math" {
if category == MATH {
// Skip grouped functions.
if GROUPS.iter().flat_map(|group| &group.functions).any(|f| f == name) {
if GROUPS.iter().flat_map(|group| &group.filter).any(|f| f == name) {
continue;
}
@ -242,54 +284,35 @@ fn category_page(resolver: &dyn Resolver, category: &str) -> PageModel {
}
}
children.sort_by_cached_key(|child| child.title.clone());
items.sort_by_cached_key(|item| item.name.clone());
// Add symbol pages. These are ordered manually.
let mut shorthands = None;
if category == "symbols" {
let mut markup = vec![];
let mut math = vec![];
for module in ["sym", "emoji"] {
let subpage = symbols_page(resolver, &route, module);
let BodyModel::Symbols(model) = &subpage.body else { continue };
let list = &model.list;
markup.extend(
list.iter()
.filter(|symbol| symbol.markup_shorthand.is_some())
.cloned(),
);
math.extend(
list.iter().filter(|symbol| symbol.math_shorthand.is_some()).cloned(),
);
items.push(CategoryItem {
name: module.into(),
route: subpage.route.clone(),
oneliner: oneliner(category_details(module)).into(),
code: true,
});
children.push(subpage);
}
shorthands = Some(ShorthandsModel { markup, math });
if category != SYMBOLS {
children.sort_by_cached_key(|child| child.title.clone());
items.sort_by_cached_key(|item| item.name.clone());
}
let name: EcoString = category.to_title_case().into();
let details = Html::markdown(resolver, category_details(category), Some(1));
let name = category.title();
let details = Html::markdown(resolver, category.docs(), Some(1));
let mut outline = vec![OutlineItem::from_name("Summary")];
outline.extend(details.outline());
outline.push(OutlineItem::from_name("Definitions"));
if shorthands.is_some() {
outline.push(OutlineItem::from_name("Shorthands"));
}
PageModel {
route,
title: name.clone(),
title: name.into(),
description: eco_format!(
"Documentation for functions related to {name} in Typst."
),
part: None,
outline,
body: BodyModel::Category(CategoryModel { name, details, items, shorthands }),
body: BodyModel::Category(CategoryModel {
name: category.name(),
title: category.title(),
details,
items,
shorthands,
}),
children,
}
}
@ -498,18 +521,17 @@ fn group_page(
resolver: &dyn Resolver,
parent: &str,
group: &GroupData,
scope: &Scope,
) -> (PageModel, CategoryItem) {
let mut functions = vec![];
let mut outline = vec![OutlineItem::from_name("Summary")];
let path: Vec<_> = group.path.iter().map(|s| s.as_str()).collect();
let details = Html::markdown(resolver, &group.description, Some(1));
let details = Html::markdown(resolver, &group.details, Some(1));
outline.extend(details.outline());
let mut outline_items = vec![];
for name in &group.functions {
let value = scope.get(name).unwrap();
for name in &group.filter {
let value = group.module().scope().get(name).unwrap();
let Value::Func(func) = value else { panic!("not a function") };
let func = func_model(resolver, func, &path, true);
let id_base = urlify(&eco_format!("functions-{}", func.name));
@ -530,13 +552,13 @@ fn group_page(
let model = PageModel {
route: eco_format!("{parent}{}", group.name),
title: group.display.clone(),
title: group.title.clone(),
description: eco_format!("Documentation for the {} functions.", group.name),
part: None,
outline,
body: BodyModel::Group(GroupModel {
name: group.name.clone(),
title: group.display.clone(),
title: group.title.clone(),
details,
functions,
}),
@ -546,7 +568,7 @@ fn group_page(
let item = CategoryItem {
name: group.name.clone(),
route: model.route.clone(),
oneliner: oneliner(&group.description).into(),
oneliner: oneliner(&group.details).into(),
code: false,
};
@ -601,19 +623,12 @@ fn type_outline(model: &TypeModel) -> Vec<OutlineItem> {
}
/// Create a page for symbols.
fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel {
let module = get_module(&LIBRARY.global, name).unwrap();
let title = match name {
"sym" => "General",
"emoji" => "Emoji",
_ => unreachable!(),
};
let model = symbols_model(resolver, name, title, module.scope());
fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> PageModel {
let model = symbols_model(resolver, group);
PageModel {
route: eco_format!("{parent}{name}/"),
title: title.into(),
description: eco_format!("Documentation for the `{name}` module."),
route: eco_format!("{parent}{}/", group.name),
title: group.title.clone(),
description: eco_format!("Documentation for the `{}` module.", group.name),
part: None,
outline: vec![],
body: BodyModel::Symbols(model),
@ -622,14 +637,9 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, name: &str) -> PageModel
}
/// Produce a symbol list's model.
fn symbols_model(
resolver: &dyn Resolver,
name: &str,
title: &'static str,
scope: &Scope,
) -> SymbolsModel {
fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
let mut list = vec![];
for (name, value) in scope.iter() {
for (name, value) in group.module().scope().iter() {
let Value::Symbol(symbol) = value else { continue };
let complete = |variant: &str| {
if variant.is_empty() {
@ -649,7 +659,7 @@ fn symbols_model(
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::MARKUP_LIST),
math_shorthand: shorthand(typst::syntax::ast::Shorthand::MATH_LIST),
codepoint: c as u32,
accent: typst::eval::Symbol::combining_accent(c).is_some(),
accent: typst::symbols::Symbol::combining_accent(c).is_some(),
unicode_name: unicode_names2::name(c)
.map(|s| s.to_string().to_title_case().into()),
alternates: symbol
@ -662,8 +672,9 @@ fn symbols_model(
}
SymbolsModel {
name: title,
details: Html::markdown(resolver, category_details(name), Some(1)),
name: group.name.clone(),
title: group.title.clone(),
details: Html::markdown(resolver, &group.details, Some(1)),
list,
}
}
@ -684,15 +695,6 @@ fn yaml<T: DeserializeOwned>(path: &str) -> T {
yaml::from_slice(file.contents()).unwrap()
}
/// Load details for an identifying key.
#[track_caller]
fn category_details(key: &str) -> &str {
CATEGORIES
.get(&yaml::Value::String(key.into()))
.and_then(|value| value.as_str())
.unwrap_or_else(|| panic!("missing details for {key}"))
}
/// Turn a title into an URL fragment.
pub fn urlify(title: &str) -> EcoString {
title
@ -752,13 +754,23 @@ const TYPE_ORDER: &[&str] = &[
#[derive(Debug, Clone, Deserialize)]
struct GroupData {
name: EcoString,
title: EcoString,
category: EcoString,
display: EcoString,
#[serde(default)]
path: Vec<EcoString>,
#[serde(default)]
functions: Vec<EcoString>,
description: EcoString,
filter: Vec<EcoString>,
details: EcoString,
}
impl GroupData {
fn module(&self) -> &'static Module {
let mut focus = &LIBRARY.global;
for path in &self.path {
focus = get_module(focus, path).unwrap();
}
focus
}
}
#[cfg(test)]

View File

@ -1,5 +1,5 @@
use typst::diag::{bail, StrResult};
use typst::eval::Func;
use typst::foundations::Func;
use crate::{get_module, GROUPS, LIBRARY};
@ -55,6 +55,15 @@ fn resolve_known(head: &str) -> Option<&'static str> {
fn resolve_definition(head: &str) -> StrResult<String> {
let mut parts = head.trim_start_matches('$').split('.').peekable();
let mut focus = &LIBRARY.global;
let Some(name) = parts.peek() else {
bail!("missing first link component");
};
let Some(category) = focus.scope().get_category(name) else {
bail!("{name} has no category");
};
while let Some(m) = parts.peek().and_then(|&name| get_module(focus, name).ok()) {
focus = m;
parts.next();
@ -62,18 +71,15 @@ fn resolve_definition(head: &str) -> StrResult<String> {
let name = parts.next().ok_or("link is missing first part")?;
let value = focus.field(name)?;
let Some(category) = focus.scope().get_category(name) else {
bail!("{name} has no category");
};
// Handle grouped functions.
if let Some(group) = GROUPS
.iter()
.filter(|_| category == "math")
.find(|group| group.functions.iter().any(|func| func == name))
{
let mut route =
format!("/docs/reference/math/{}/#functions-{}", group.name, name);
if let Some(group) = GROUPS.iter().find(|group| {
group.category == category.name() && group.filter.iter().any(|func| func == name)
}) {
let mut route = format!(
"/docs/reference/{}/{}/#functions-{}",
group.category, group.name, name
);
if let Some(param) = parts.next() {
route.push('-');
route.push_str(param);
@ -81,7 +87,7 @@ fn resolve_definition(head: &str) -> StrResult<String> {
return Ok(route);
}
let mut route = format!("/docs/reference/{category}/{name}/");
let mut route = format!("/docs/reference/{}/{name}/", category.name());
if let Some(next) = parts.next() {
if value.field(next).is_ok() {
route.push_str("#definitions-");

View File

@ -62,7 +62,8 @@ pub enum BodyModel {
/// Details about a category.
#[derive(Debug, Serialize)]
pub struct CategoryModel {
pub name: EcoString,
pub name: &'static str,
pub title: &'static str,
pub details: Html,
pub items: Vec<CategoryItem>,
pub shorthands: Option<ShorthandsModel>,
@ -144,7 +145,8 @@ pub struct TypeModel {
/// A collection of symbols.
#[derive(Debug, Serialize)]
pub struct SymbolsModel {
pub name: &'static str,
pub name: EcoString,
pub title: EcoString,
pub details: Html,
pub list: Vec<SymbolModel>,
}

View File

@ -1,8 +1,11 @@
use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec};
use typst::doc::Frame;
use typst::eval::{Route, Scopes, Tracer, Value, Vm};
use typst::model::{DelayedErrors, Introspector, Label, Locator, Vt};
use typst::diag::DelayedErrors;
use typst::eval::{Route, Tracer, Vm};
use typst::foundations::{Label, Scopes, Value};
use typst::introspection::{Introspector, Locator};
use typst::layout::{Frame, Vt};
use typst::model::BibliographyElem;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use typst::World;
@ -75,13 +78,9 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
/// - All labels and descriptions for them, if available
/// - A split offset: All labels before this offset belong to nodes, all after
/// belong to a bibliography.
pub fn analyze_labels(
world: &dyn World,
frames: &[Frame],
) -> (Vec<(Label, Option<EcoString>)>, usize) {
pub fn analyze_labels(frames: &[Frame]) -> (Vec<(Label, Option<EcoString>)>, usize) {
let mut output = vec![];
let introspector = Introspector::new(frames);
let items = &world.library().items;
// Labels in the document.
for elem in introspector.all() {
@ -102,7 +101,7 @@ pub fn analyze_labels(
let split = output.len();
// Bibliography keys.
for (key, detail) in (items.bibliography_keys)(introspector.track()) {
for (key, detail) in BibliographyElem::keys(introspector.track()) {
output.push((Label::new(&key), detail));
}

View File

@ -4,21 +4,21 @@ use std::collections::{BTreeSet, HashSet};
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use serde::{Deserialize, Serialize};
use typst::doc::Frame;
use typst::eval::{
format_str, repr, AutoValue, CastInfo, Func, Library, NoneValue, Repr, Scope, Type,
Value,
use typst::foundations::{
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label,
NoneValue, Repr, Scope, Type, Value,
};
use typst::geom::Color;
use typst::model::Label;
use typst::layout::Frame;
use typst::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind,
};
use typst::text::RawElem;
use typst::visualize::Color;
use typst::World;
use unscanny::Scanner;
use crate::analyze::analyze_labels;
use crate::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family};
use crate::analyze::{analyze_expr, analyze_import, analyze_labels};
use crate::{plain_docs_sentence, summarize_font_family};
/// Autocomplete a cursor position in a source file.
///
@ -367,7 +367,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}
}
for &(method, args) in typst::eval::mutable_methods_on(value.ty()) {
for &(method, args) in mutable_methods_on(value.ty()) {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: method.into(),
@ -380,7 +380,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
})
}
for &field in typst::eval::fields_on(value.ty()) {
for &field in fields_on(value.ty()) {
// Complete the field name along with its value. Notes:
// 1. No parentheses since function fields cannot currently be called
// with method syntax;
@ -967,7 +967,6 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) {
struct CompletionContext<'a> {
world: &'a (dyn World + 'a),
frames: &'a [Frame],
library: &'a Library,
global: &'a Scope,
math: &'a Scope,
text: &'a str,
@ -996,7 +995,6 @@ impl<'a> CompletionContext<'a> {
Some(Self {
world,
frames,
library,
global: library.global.scope(),
math: library.math.scope(),
text,
@ -1074,7 +1072,7 @@ impl<'a> CompletionContext<'a> {
/// Add completions for raw block tags.
fn raw_completions(&mut self) {
for (name, mut tags) in (self.library.items.raw_languages)() {
for (name, mut tags) in RawElem::languages() {
let lower = name.to_lowercase();
if !tags.contains(&lower.as_str()) {
tags.push(lower.as_str());
@ -1096,7 +1094,7 @@ impl<'a> CompletionContext<'a> {
/// Add completions for labels and references.
fn label_completions(&mut self) {
let (labels, split) = analyze_labels(self.world, self.frames);
let (labels, split) = analyze_labels(self.frames);
let head = &self.text[..self.from];
let at = head.ends_with('@');

View File

@ -1,10 +1,11 @@
use std::num::NonZeroUsize;
use ecow::EcoString;
use typst::doc::{Destination, Frame, FrameItem, Meta, Position};
use typst::geom::{Geometry, Point, Size};
use typst::model::Introspector;
use typst::introspection::{Introspector, Meta};
use typst::layout::{Frame, FrameItem, Point, Position, Size};
use typst::model::Destination;
use typst::syntax::{FileId, LinkedNode, Source, Span, SyntaxKind};
use typst::visualize::Geometry;
use typst::World;
/// Where to [jump](jump_from_click) to.

View File

@ -13,9 +13,7 @@ pub use self::tooltip::{tooltip, Tooltip};
use std::fmt::Write;
use ecow::{eco_format, EcoString};
use typst::font::{FontInfo, FontStyle};
use self::analyze::*;
use typst::text::{FontInfo, FontStyle};
/// Extract the first sentence of plain text of a piece of documentation.
///

View File

@ -2,14 +2,15 @@ use std::fmt::Write;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use typst::doc::Frame;
use typst::eval::{repr, CapturesVisitor, CastInfo, Repr, Tracer, Value};
use typst::geom::{round_2, Length, Numeric};
use typst::eval::{CapturesVisitor, Tracer};
use typst::foundations::{repr, CastInfo, Repr, Value};
use typst::layout::{Frame, Length};
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use typst::util::{round_2, Numeric};
use typst::World;
use crate::analyze::analyze_labels;
use crate::{analyze_expr, plain_docs_sentence, summarize_font_family};
use crate::analyze::{analyze_expr, analyze_labels};
use crate::{plain_docs_sentence, summarize_font_family};
/// Describe the item under the cursor.
pub fn tooltip(
@ -25,7 +26,7 @@ pub fn tooltip(
named_param_tooltip(world, &leaf)
.or_else(|| font_tooltip(world, &leaf))
.or_else(|| label_tooltip(world, frames, &leaf))
.or_else(|| label_tooltip(frames, &leaf))
.or_else(|| expr_tooltip(world, &leaf))
.or_else(|| closure_tooltip(&leaf))
}
@ -144,18 +145,14 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
}
/// Tooltip for a hovered reference or label.
fn label_tooltip(
world: &dyn World,
frames: &[Frame],
leaf: &LinkedNode,
) -> Option<Tooltip> {
fn label_tooltip(frames: &[Frame], leaf: &LinkedNode) -> Option<Tooltip> {
let target = match leaf.kind() {
SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'),
SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'),
_ => return None,
};
for (label, detail) in analyze_labels(world, frames).0 {
for (label, detail) in analyze_labels(frames).0 {
if label.as_str() == target {
return Some(Tooltip::Text(detail?));
}

View File

@ -1,53 +0,0 @@
[package]
name = "typst-library"
description = "The standard library for Typst."
version.workspace = true
rust-version.workspace = true
authors.workspace = true
edition.workspace = true
homepage.workspace = true
repository.workspace = true
license.workspace = true
categories.workspace = true
keywords.workspace = true
[lib]
test = false
doctest = false
bench = false
[dependencies]
typst = { workspace = true }
az = { workspace = true }
chinese-number = { workspace = true }
ciborium = { workspace = true }
comemo = { workspace = true }
csv = { workspace = true }
ecow = { workspace = true }
hayagriva = { workspace = true }
hypher = { workspace = true }
icu_properties = { workspace = true }
icu_provider = { workspace = true }
icu_provider_adapters = { workspace = true }
icu_provider_blob = { workspace = true }
icu_segmenter = { workspace = true }
indexmap = { workspace = true }
kurbo = { workspace = true }
lipsum = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
roxmltree = { workspace = true }
rustybuzz = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
smallvec = { workspace = true }
syntect = { workspace = true }
time = { workspace = true }
toml = { workspace = true, features = ["display"] }
tracing = { workspace = true }
ttf-parser = { workspace = true }
typed-arena = { workspace = true }
unicode-bidi = { workspace = true }
unicode-math-class = { workspace = true }
unicode-script = { workspace = true }
unicode-segmentation = { workspace = true }

View File

@ -1,609 +0,0 @@
use typst::diag::{format_xml_like_error, FileError};
use typst::eval::Bytes;
use typst::syntax::is_newline;
use crate::prelude::*;
/// Hook up all data loading definitions.
pub(super) fn define(global: &mut Scope) {
global.category("data-loading");
global.define_func::<read>();
global.define_func::<csv>();
global.define_func::<json>();
global.define_func::<toml>();
global.define_func::<yaml>();
global.define_func::<cbor>();
global.define_func::<xml>();
}
/// Reads plain text or data from a file.
///
/// By default, the file will be read as UTF-8 and returned as a [string]($str).
///
/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead.
///
/// # Example
/// ```example
/// An example for a HTML file: \
/// #let text = read("data.html")
/// #raw(text, lang: "html")
///
/// Raw bytes:
/// #read("tiger.jpg", encoding: none)
/// ```
#[func]
pub fn read(
/// The virtual machine.
vm: &mut Vm,
/// Path to a file.
path: Spanned<EcoString>,
/// The encoding to read the file with.
///
/// If set to `{none}`, this function returns raw bytes.
#[named]
#[default(Some(Encoding::Utf8))]
encoding: Option<Encoding>,
) -> SourceResult<Readable> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
Ok(match encoding {
None => Readable::Bytes(data),
Some(Encoding::Utf8) => Readable::Str(
std::str::from_utf8(&data)
.map_err(|_| "file is not valid utf-8")
.at(span)?
.into(),
),
})
}
/// An encoding of a file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Encoding {
/// The Unicode UTF-8 encoding.
Utf8,
}
/// A value that can be read from a file.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Readable {
/// A decoded string.
Str(Str),
/// Raw bytes.
Bytes(Bytes),
}
impl Readable {
fn as_slice(&self) -> &[u8] {
match self {
Readable::Bytes(v) => v,
Readable::Str(v) => v.as_bytes(),
}
}
}
cast! {
Readable,
self => match self {
Self::Str(v) => v.into_value(),
Self::Bytes(v) => v.into_value(),
},
v: Str => Self::Str(v),
v: Bytes => Self::Bytes(v),
}
impl From<Readable> for Bytes {
fn from(value: Readable) -> Self {
match value {
Readable::Bytes(v) => v,
Readable::Str(v) => v.as_bytes().into(),
}
}
}
/// Reads structured data from a CSV file.
///
/// The CSV file will be read and parsed into a 2-dimensional array of strings:
/// Each row in the CSV file will be represented as an array of strings, and all
/// rows will be collected into a single array. Header rows will not be
/// stripped.
///
/// # Example
/// ```example
/// #let results = csv("data.csv")
///
/// #table(
/// columns: 2,
/// [*Condition*], [*Result*],
/// ..results.flatten(),
/// )
/// ```
#[func(scope, title = "CSV")]
pub fn csv(
/// The virtual machine.
vm: &mut Vm,
/// Path to a CSV file.
path: Spanned<EcoString>,
/// The delimiter that separates columns in the CSV file.
/// Must be a single ASCII character.
#[named]
#[default]
delimiter: Delimiter,
) -> SourceResult<Array> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter)
}
#[scope]
impl csv {
/// Reads structured data from a CSV string/bytes.
#[func(title = "Decode CSV")]
pub fn decode(
/// CSV data.
data: Spanned<Readable>,
/// The delimiter that separates columns in the CSV file.
/// Must be a single ASCII character.
#[named]
#[default]
delimiter: Delimiter,
) -> SourceResult<Array> {
let Spanned { v: data, span } = data;
let mut builder = ::csv::ReaderBuilder::new();
builder.has_headers(false);
builder.delimiter(delimiter.0 as u8);
let mut reader = builder.from_reader(data.as_slice());
let mut array = Array::new();
for (line, result) in reader.records().enumerate() {
// Original solution use line from error, but that is incorrect with
// `has_headers` set to `false`. See issue:
// https://github.com/BurntSushi/rust-csv/issues/184
let line = line + 1; // Counting lines from 1
let row = result.map_err(|err| format_csv_error(err, line)).at(span)?;
let sub = row.into_iter().map(|field| field.into_value()).collect();
array.push(Value::Array(sub))
}
Ok(array)
}
}
/// The delimiter to use when parsing CSV files.
pub struct Delimiter(char);
impl Default for Delimiter {
fn default() -> Self {
Self(',')
}
}
cast! {
Delimiter,
self => self.0.into_value(),
v: EcoString => {
let mut chars = v.chars();
let first = chars.next().ok_or("delimiter must not be empty")?;
if chars.next().is_some() {
bail!("delimiter must be a single character");
}
if !first.is_ascii() {
bail!("delimiter must be an ASCII character");
}
Self(first)
},
}
/// Format the user-facing CSV error message.
fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString {
match err.kind() {
::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
eco_format!(
"failed to parse CSV (found {len} instead of \
{expected_len} fields in line {line})"
)
}
_ => eco_format!("failed to parse CSV ({err})"),
}
}
/// Reads structured data from a JSON file.
///
/// The file must contain a valid JSON object or array. JSON objects will be
/// converted into Typst dictionaries, and JSON arrays will be converted into
/// Typst arrays. Strings and booleans will be converted into the Typst
/// equivalents, `null` will be converted into `{none}`, and numbers will be
/// converted to floats or integers depending on whether they are whole numbers.
///
/// The function returns a dictionary or an array, depending on the JSON file.
///
/// The JSON files in the example contain objects with the keys `temperature`,
/// `unit`, and `weather`.
///
/// # Example
/// ```example
/// #let forecast(day) = block[
/// #box(square(
/// width: 2cm,
/// inset: 8pt,
/// fill: if day.weather == "sunny" {
/// yellow
/// } else {
/// aqua
/// },
/// align(
/// bottom + right,
/// strong(day.weather),
/// ),
/// ))
/// #h(6pt)
/// #set text(22pt, baseline: -8pt)
/// #day.temperature °#day.unit
/// ]
///
/// #forecast(json("monday.json"))
/// #forecast(json("tuesday.json"))
/// ```
#[func(scope, title = "JSON")]
pub fn json(
/// The virtual machine.
vm: &mut Vm,
/// Path to a JSON file.
path: Spanned<EcoString>,
) -> SourceResult<Value> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
json::decode(Spanned::new(Readable::Bytes(data), span))
}
#[scope]
impl json {
/// Reads structured data from a JSON string/bytes.
#[func(title = "Decode JSON")]
pub fn decode(
/// JSON data.
data: Spanned<Readable>,
) -> SourceResult<Value> {
let Spanned { v: data, span } = data;
serde_json::from_slice(data.as_slice())
.map_err(|err| eco_format!("failed to parse JSON ({err})"))
.at(span)
}
/// Encodes structured data into a JSON string.
#[func(title = "Encode JSON")]
pub fn encode(
/// Value to be encoded.
value: Spanned<Value>,
/// Whether to pretty print the JSON with newlines and indentation.
#[named]
#[default(true)]
pretty: bool,
) -> SourceResult<Str> {
let Spanned { v: value, span } = value;
if pretty {
serde_json::to_string_pretty(&value)
} else {
serde_json::to_string(&value)
}
.map(|v| v.into())
.map_err(|err| eco_format!("failed to encode value as JSON ({err})"))
.at(span)
}
}
/// Reads structured data from a TOML file.
///
/// The file must contain a valid TOML table. TOML tables will be converted into
/// Typst dictionaries, and TOML arrays will be converted into Typst arrays.
/// Strings, booleans and datetimes will be converted into the Typst equivalents
/// and numbers will be converted to floats or integers depending on whether
/// they are whole numbers.
///
/// The TOML file in the example consists of a table with the keys `title`,
/// `version`, and `authors`.
///
/// # Example
/// ```example
/// #let details = toml("details.toml")
///
/// Title: #details.title \
/// Version: #details.version \
/// Authors: #(details.authors
/// .join(", ", last: " and "))
/// ```
#[func(scope, title = "TOML")]
pub fn toml(
/// The virtual machine.
vm: &mut Vm,
/// Path to a TOML file.
path: Spanned<EcoString>,
) -> SourceResult<Value> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
toml::decode(Spanned::new(Readable::Bytes(data), span))
}
#[scope]
impl toml {
/// Reads structured data from a TOML string/bytes.
#[func(title = "Decode TOML")]
pub fn decode(
/// TOML data.
data: Spanned<Readable>,
) -> SourceResult<Value> {
let Spanned { v: data, span } = data;
let raw = std::str::from_utf8(data.as_slice())
.map_err(|_| "file is not valid utf-8")
.at(span)?;
::toml::from_str(raw)
.map_err(|err| format_toml_error(err, raw))
.at(span)
}
/// Encodes structured data into a TOML string.
#[func(title = "Encode TOML")]
pub fn encode(
/// Value to be encoded.
value: Spanned<Value>,
/// Whether to pretty-print the resulting TOML.
#[named]
#[default(true)]
pretty: bool,
) -> SourceResult<Str> {
let Spanned { v: value, span } = value;
if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) }
.map(|v| v.into())
.map_err(|err| eco_format!("failed to encode value as TOML ({err})"))
.at(span)
}
}
/// Format the user-facing TOML error message.
fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString {
if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) {
let line = head.lines().count();
let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count();
eco_format!(
"failed to parse TOML ({} at line {line} column {column})",
error.message(),
)
} else {
eco_format!("failed to parse TOML ({})", error.message())
}
}
/// Reads structured data from a YAML file.
///
/// The file must contain a valid YAML object or array. YAML mappings will be
/// converted into Typst dictionaries, and YAML sequences will be converted into
/// Typst arrays. Strings and booleans will be converted into the Typst
/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
/// `{none}`, and numbers will be converted to floats or integers depending on
/// whether they are whole numbers. Custom YAML tags are ignored, though the
/// loaded value will still be present.
///
/// The YAML files in the example contain objects with authors as keys,
/// each with a sequence of their own submapping with the keys
/// "title" and "published"
///
/// # Example
/// ```example
/// #let bookshelf(contents) = {
/// for (author, works) in contents {
/// author
/// for work in works [
/// - #work.title (#work.published)
/// ]
/// }
/// }
///
/// #bookshelf(
/// yaml("scifi-authors.yaml")
/// )
/// ```
#[func(scope, title = "YAML")]
pub fn yaml(
/// The virtual machine.
vm: &mut Vm,
/// Path to a YAML file.
path: Spanned<EcoString>,
) -> SourceResult<Value> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
yaml::decode(Spanned::new(Readable::Bytes(data), span))
}
#[scope]
impl yaml {
/// Reads structured data from a YAML string/bytes.
#[func(title = "Decode YAML")]
pub fn decode(
/// YAML data.
data: Spanned<Readable>,
) -> SourceResult<Value> {
let Spanned { v: data, span } = data;
serde_yaml::from_slice(data.as_slice())
.map_err(|err| eco_format!("failed to parse YAML ({err})"))
.at(span)
}
/// Encode structured data into a YAML string.
#[func(title = "Encode YAML")]
pub fn encode(
/// Value to be encoded.
value: Spanned<Value>,
) -> SourceResult<Str> {
let Spanned { v: value, span } = value;
serde_yaml::to_string(&value)
.map(|v| v.into())
.map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
.at(span)
}
}
/// Reads structured data from a CBOR file.
///
/// The file must contain a valid cbor serialization. Mappings will be
/// converted into Typst dictionaries, and sequences will be converted into
/// Typst arrays. Strings and booleans will be converted into the Typst
/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
/// `{none}`, and numbers will be converted to floats or integers depending on
/// whether they are whole numbers.
#[func(scope, title = "CBOR")]
pub fn cbor(
/// The virtual machine.
vm: &mut Vm,
/// Path to a CBOR file.
path: Spanned<EcoString>,
) -> SourceResult<Value> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
cbor::decode(Spanned::new(data, span))
}
#[scope]
impl cbor {
/// Reads structured data from CBOR bytes.
#[func(title = "Decode CBOR")]
pub fn decode(
/// cbor data.
data: Spanned<Bytes>,
) -> SourceResult<Value> {
let Spanned { v: data, span } = data;
ciborium::from_reader(data.as_slice())
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
.at(span)
}
/// Encode structured data into CBOR bytes.
#[func(title = "Encode CBOR")]
pub fn encode(
/// Value to be encoded.
value: Spanned<Value>,
) -> SourceResult<Bytes> {
let Spanned { v: value, span } = value;
let mut res = Vec::new();
ciborium::into_writer(&value, &mut res)
.map(|_| res.into())
.map_err(|err| eco_format!("failed to encode value as CBOR ({err})"))
.at(span)
}
}
/// Reads structured data from an XML file.
///
/// The XML file is parsed into an array of dictionaries and strings. XML nodes
/// can be elements or strings. Elements are represented as dictionaries with
/// the the following keys:
///
/// - `tag`: The name of the element as a string.
/// - `attrs`: A dictionary of the element's attributes as strings.
/// - `children`: An array of the element's child nodes.
///
/// The XML file in the example contains a root `news` tag with multiple
/// `article` tags. Each article has a `title`, `author`, and `content` tag. The
/// `content` tag contains one or more paragraphs, which are represented as `p`
/// tags.
///
/// # Example
/// ```example
/// #let find-child(elem, tag) = {
/// elem.children
/// .find(e => "tag" in e and e.tag == tag)
/// }
///
/// #let article(elem) = {
/// let title = find-child(elem, "title")
/// let author = find-child(elem, "author")
/// let pars = find-child(elem, "content")
///
/// heading(title.children.first())
/// text(10pt, weight: "medium")[
/// Published by
/// #author.children.first()
/// ]
///
/// for p in pars.children {
/// if (type(p) == "dictionary") {
/// parbreak()
/// p.children.first()
/// }
/// }
/// }
///
/// #let data = xml("example.xml")
/// #for elem in data.first().children {
/// if (type(elem) == "dictionary") {
/// article(elem)
/// }
/// }
/// ```
#[func(scope, title = "XML")]
pub fn xml(
/// The virtual machine.
vm: &mut Vm,
/// Path to an XML file.
path: Spanned<EcoString>,
) -> SourceResult<Value> {
let Spanned { v: path, span } = path;
let id = vm.resolve_path(&path).at(span)?;
let data = vm.world().file(id).at(span)?;
xml::decode(Spanned::new(Readable::Bytes(data), span))
}
#[scope]
impl xml {
/// Reads structured data from an XML string/bytes.
#[func(title = "Decode XML")]
pub fn decode(
/// XML data.
data: Spanned<Readable>,
) -> SourceResult<Value> {
let Spanned { v: data, span } = data;
let text = std::str::from_utf8(data.as_slice())
.map_err(FileError::from)
.at(span)?;
let document =
roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
Ok(convert_xml(document.root()))
}
}
/// Convert an XML node to a Typst value.
fn convert_xml(node: roxmltree::Node) -> Value {
if node.is_text() {
return node.text().unwrap_or_default().into_value();
}
let children: Array = node.children().map(convert_xml).collect();
if node.is_root() {
return Value::Array(children);
}
let tag: Str = node.tag_name().name().into();
let attrs: Dict = node
.attributes()
.map(|attr| (attr.name().into(), attr.value().into_value()))
.collect();
Value::Dict(dict! {
"tag" => tag,
"attrs" => attrs,
"children" => children,
})
}
/// Format the user-facing XML error message.
fn format_xml_error(error: roxmltree::Error) -> EcoString {
format_xml_like_error("XML", error)
}

View File

@ -1,20 +0,0 @@
//! Computational functions.
pub mod calc;
pub mod sys;
mod data;
mod foundations;
pub use self::data::*;
pub use self::foundations::*;
use crate::prelude::*;
/// Hook up all compute definitions.
pub(super) fn define(global: &mut Scope) {
self::foundations::define(global);
self::data::define(global);
self::calc::define(global);
self::sys::define(global);
}

View File

@ -1,46 +0,0 @@
use crate::prelude::*;
/// Aligns content horizontally and vertically.
///
/// # Example
/// ```example
/// #set align(center)
///
/// Centered text, a sight to see \
/// In perfect balance, visually \
/// Not left nor right, it stands alone \
/// A work of art, a visual throne
/// ```
#[elem(Show)]
pub struct AlignElem {
/// The [alignment]($alignment) along both axes.
///
/// ```example
/// #set page(height: 6cm)
/// #set text(lang: "ar")
///
/// مثال
/// #align(
/// end + horizon,
/// rect(inset: 12pt)[ركن]
/// )
/// ```
#[positional]
#[fold]
#[default]
pub alignment: Align,
/// The content to align.
#[required]
pub body: Content,
}
impl Show for AlignElem {
#[tracing::instrument(name = "AlignElem::show", skip_all)]
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self
.body()
.clone()
.styled(Self::set_alignment(self.alignment(styles))))
}
}

View File

@ -1,170 +0,0 @@
//! Typst's standard library.
#![allow(clippy::wildcard_in_or_patterns)]
#![allow(clippy::manual_range_contains)]
#![allow(clippy::comparison_chain)]
pub mod compute;
pub mod layout;
pub mod math;
pub mod meta;
pub mod prelude;
pub mod shared;
pub mod symbols;
pub mod text;
pub mod visualize;
use typst::eval::{Array, LangItems, Library, Module, Scope, Smart};
use typst::geom::{Align, Color, Dir};
use typst::model::{NativeElement, Styles};
use self::layout::LayoutRoot;
/// Construct the standard library.
pub fn build() -> Library {
let math = math::module();
let global = global(math.clone());
Library { global, math, styles: styles(), items: items() }
}
/// Construct the module with global definitions.
#[tracing::instrument(skip_all)]
fn global(math: Module) -> Module {
let mut global = Scope::deduplicating();
text::define(&mut global);
global.define_module(math);
layout::define(&mut global);
visualize::define(&mut global);
meta::define(&mut global);
symbols::define(&mut global);
compute::define(&mut global);
prelude(&mut global);
Module::new("global", global)
}
/// Defines scoped values that are globally available, too.
fn prelude(global: &mut Scope) {
global.reset_category();
global.define("black", Color::BLACK);
global.define("gray", Color::GRAY);
global.define("silver", Color::SILVER);
global.define("white", Color::WHITE);
global.define("navy", Color::NAVY);
global.define("blue", Color::BLUE);
global.define("aqua", Color::AQUA);
global.define("teal", Color::TEAL);
global.define("eastern", Color::EASTERN);
global.define("purple", Color::PURPLE);
global.define("fuchsia", Color::FUCHSIA);
global.define("maroon", Color::MAROON);
global.define("red", Color::RED);
global.define("orange", Color::ORANGE);
global.define("yellow", Color::YELLOW);
global.define("olive", Color::OLIVE);
global.define("green", Color::GREEN);
global.define("lime", Color::LIME);
global.define("luma", Color::luma_data());
global.define("oklab", Color::oklab_data());
global.define("oklch", Color::oklch_data());
global.define("rgb", Color::rgb_data());
global.define("cmyk", Color::cmyk_data());
global.define("range", Array::range_data());
global.define("ltr", Dir::LTR);
global.define("rtl", Dir::RTL);
global.define("ttb", Dir::TTB);
global.define("btt", Dir::BTT);
global.define("start", Align::START);
global.define("left", Align::LEFT);
global.define("center", Align::CENTER);
global.define("right", Align::RIGHT);
global.define("end", Align::END);
global.define("top", Align::TOP);
global.define("horizon", Align::HORIZON);
global.define("bottom", Align::BOTTOM);
}
/// Construct the standard style map.
fn styles() -> Styles {
Styles::new()
}
/// Construct the standard lang item mapping.
fn items() -> LangItems {
LangItems {
layout: |world, content, styles| content.layout_root(world, styles),
em: text::TextElem::size_in,
dir: text::TextElem::dir_in,
space: || text::SpaceElem::new().pack(),
linebreak: || text::LinebreakElem::new().pack(),
text: |text| text::TextElem::new(text).pack(),
text_elem: text::TextElem::elem(),
text_str: |content| Some(content.to::<text::TextElem>()?.text()),
smart_quote: |double| text::SmartquoteElem::new().with_double(double).pack(),
parbreak: || layout::ParbreakElem::new().pack(),
strong: |body| text::StrongElem::new(body).pack(),
emph: |body| text::EmphElem::new(body).pack(),
raw: |text, lang, block| {
let mut elem = text::RawElem::new(text).with_block(block);
if let Some(lang) = lang {
elem.push_lang(Some(lang));
}
elem.pack()
},
raw_languages: text::RawElem::languages,
link: |url| meta::LinkElem::from_url(url).pack(),
reference: |target, supplement| {
let mut elem = meta::RefElem::new(target);
if let Some(supplement) = supplement {
elem.push_supplement(Smart::Custom(Some(meta::Supplement::Content(
supplement,
))));
}
elem.pack()
},
bibliography_keys: meta::BibliographyElem::keys,
heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
heading_elem: meta::HeadingElem::elem(),
list_item: |body| layout::ListItem::new(body).pack(),
enum_item: |number, body| {
let mut elem = layout::EnumItem::new(body);
if let Some(number) = number {
elem.push_number(Some(number));
}
elem.pack()
},
term_item: |term, description| layout::TermItem::new(term, description).pack(),
equation: |body, block| math::EquationElem::new(body).with_block(block).pack(),
math_align_point: || math::AlignPointElem::new().pack(),
math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(),
math_attach: |base, t, b, tl, bl, tr, br| {
let mut elem = math::AttachElem::new(base);
if let Some(t) = t {
elem.push_t(Some(t));
}
if let Some(b) = b {
elem.push_b(Some(b));
}
if let Some(tl) = tl {
elem.push_tl(Some(tl));
}
if let Some(bl) = bl {
elem.push_bl(Some(bl));
}
if let Some(tr) = tr {
elem.push_tr(Some(tr));
}
if let Some(br) = br {
elem.push_br(Some(br));
}
elem.pack()
},
math_primes: |count| math::PrimesElem::new(count).pack(),
math_accent: |base, accent| {
math::AccentElem::new(base, math::Accent::new(accent)).pack()
},
math_frac: |num, denom| math::FracElem::new(num, denom).pack(),
math_root: |index, radicand| {
math::RootElem::new(radicand).with_index(index).pack()
},
}
}

View File

@ -1,76 +0,0 @@
//! Interaction between document parts.
mod bibliography;
mod cite;
mod context;
mod counter;
mod document;
mod figure;
mod footnote;
mod heading;
mod link;
mod metadata;
#[path = "numbering.rs"]
mod numbering_;
mod outline;
#[path = "query.rs"]
mod query_;
mod reference;
mod state;
pub use self::bibliography::*;
pub use self::cite::*;
pub use self::context::*;
pub use self::counter::*;
pub use self::document::*;
pub use self::figure::*;
pub use self::footnote::*;
pub use self::heading::*;
pub use self::link::*;
pub use self::metadata::*;
pub use self::numbering_::*;
pub use self::outline::*;
pub use self::query_::*;
pub use self::reference::*;
pub use self::state::*;
use crate::prelude::*;
use crate::text::TextElem;
/// Hook up all meta definitions.
pub(super) fn define(global: &mut Scope) {
global.category("meta");
global.define_type::<Label>();
global.define_type::<Selector>();
global.define_type::<Location>();
global.define_type::<Counter>();
global.define_type::<State>();
global.define_elem::<DocumentElem>();
global.define_elem::<RefElem>();
global.define_elem::<LinkElem>();
global.define_elem::<OutlineElem>();
global.define_elem::<HeadingElem>();
global.define_elem::<FigureElem>();
global.define_elem::<FootnoteElem>();
global.define_elem::<CiteElem>();
global.define_elem::<BibliographyElem>();
global.define_elem::<MetadataElem>();
global.define_func::<locate>();
global.define_func::<style>();
global.define_func::<layout>();
global.define_func::<numbering>();
global.define_func::<query>();
}
/// An element that has a local name.
pub trait LocalNameIn: LocalName {
/// Gets the local name from the style chain.
fn local_name_in(styles: StyleChain) -> &'static str
where
Self: Sized,
{
Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles))
}
}
impl<T: LocalName> LocalNameIn for T {}

View File

@ -1,42 +0,0 @@
//! Helpful imports for creating library functionality.
#[doc(no_inline)]
pub use std::fmt::{self, Debug, Formatter};
#[doc(no_inline)]
pub use std::num::NonZeroUsize;
#[doc(no_inline)]
pub use comemo::{Track, Tracked, TrackedMut};
#[doc(no_inline)]
pub use ecow::{eco_format, EcoString};
#[doc(no_inline)]
pub use smallvec::{smallvec, SmallVec};
#[doc(no_inline)]
pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult};
#[doc(no_inline)]
pub use typst::doc::*;
#[doc(no_inline)]
pub use typst::eval::{
array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict,
FromValue, Func, IntoValue, Repr, Scope, Smart, Str, Symbol, Type, Value, Vm,
};
#[doc(no_inline)]
pub use typst::geom::*;
#[doc(no_inline)]
pub use typst::model::{
elem, select_where, Behave, Behaviour, Construct, Content, Element, ElementFields,
Finalize, Fold, Introspector, Label, LocalName, Locatable, LocatableSelector,
Location, Locator, MetaElem, NativeElement, PlainText, Resolve, Selector, Set, Show,
StyleChain, StyleVec, Styles, Synthesize, Unlabellable, Vt,
};
#[doc(no_inline)]
pub use typst::syntax::{FileId, Span, Spanned};
#[doc(no_inline)]
pub use typst::util::NonZeroExt;
#[doc(no_inline)]
pub use typst::World;
#[doc(no_inline)]
pub use crate::layout::{Fragment, Layout, Regions};
#[doc(no_inline)]
pub use crate::shared::{ContentExt, StylesExt};

View File

@ -1,92 +0,0 @@
//! Extension traits.
use crate::layout::{AlignElem, MoveElem, PadElem};
use crate::prelude::*;
use crate::text::{EmphElem, FontFamily, FontList, StrongElem, TextElem, UnderlineElem};
/// Additional methods on content.
pub trait ContentExt {
/// Make this content strong.
fn strong(self) -> Self;
/// Make this content emphasized.
fn emph(self) -> Self;
/// Underline this content.
fn underlined(self) -> Self;
/// Link the content somewhere.
fn linked(self, dest: Destination) -> Self;
/// Make the content linkable by `.linked(Destination::Location(loc))`.
///
/// Should be used in combination with [`Location::variant`].
fn backlinked(self, loc: Location) -> Self;
/// Set alignments for this content.
fn aligned(self, align: Align) -> Self;
/// Pad this content at the sides.
fn padded(self, padding: Sides<Rel<Length>>) -> Self;
/// Transform this content's contents without affecting layout.
fn moved(self, delta: Axes<Rel<Length>>) -> Self;
}
impl ContentExt for Content {
fn strong(self) -> Self {
StrongElem::new(self).pack()
}
fn emph(self) -> Self {
EmphElem::new(self).pack()
}
fn underlined(self) -> Self {
UnderlineElem::new(self).pack()
}
fn linked(self, dest: Destination) -> Self {
self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)]))
}
fn backlinked(self, loc: Location) -> Self {
let mut backlink = Content::empty();
backlink.set_location(loc);
self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)]))
}
fn aligned(self, align: Align) -> Self {
self.styled(AlignElem::set_alignment(align))
}
fn padded(self, padding: Sides<Rel<Length>>) -> Self {
PadElem::new(self)
.with_left(padding.left)
.with_top(padding.top)
.with_right(padding.right)
.with_bottom(padding.bottom)
.pack()
}
fn moved(self, delta: Axes<Rel<Length>>) -> Self {
MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack()
}
}
/// Additional methods for style lists.
pub trait StylesExt {
/// Set a font family composed of a preferred family and existing families
/// from a style chain.
fn set_family(&mut self, preferred: FontFamily, existing: StyleChain);
}
impl StylesExt for Styles {
fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
self.set(TextElem::set_font(FontList(
std::iter::once(preferred)
.chain(TextElem::font_in(existing).into_iter().cloned())
.collect(),
)));
}
}

View File

@ -1,7 +0,0 @@
//! Shared definitions for the standard library.
mod behave;
mod ext;
pub use behave::*;
pub use ext::*;

View File

@ -1,17 +0,0 @@
//! Modifiable symbols.
mod emoji;
mod sym;
pub use emoji::*;
pub use sym::*;
use crate::prelude::*;
/// Hook up all symbol definitions.
pub(super) fn define(global: &mut Scope) {
global.category("symbols");
global.define_type::<Symbol>();
global.define_module(sym());
global.define_module(emoji());
}

View File

@ -1,315 +0,0 @@
use crate::prelude::*;
use crate::text::TextElem;
/// A text space.
#[elem(Behave, Unlabellable, PlainText, Repr)]
pub struct SpaceElem {}
impl Repr for SpaceElem {
fn repr(&self) -> EcoString {
EcoString::inline("[ ]")
}
}
impl Behave for SpaceElem {
fn behaviour(&self) -> Behaviour {
Behaviour::Weak(2)
}
}
impl Unlabellable for SpaceElem {}
impl PlainText for SpaceElem {
fn plain_text(&self, text: &mut EcoString) {
text.push(' ');
}
}
/// Inserts a line break.
///
/// Advances the paragraph to the next line. A single trailing line break at the
/// end of a paragraph is ignored, but more than one creates additional empty
/// lines.
///
/// # Example
/// ```example
/// *Date:* 26.12.2022 \
/// *Topic:* Infrastructure Test \
/// *Severity:* High \
/// ```
///
/// # Syntax
/// This function also has dedicated syntax: To insert a line break, simply write
/// a backslash followed by whitespace. This always creates an unjustified
/// break.
#[elem(title = "Line Break", Behave)]
pub struct LinebreakElem {
/// Whether to justify the line before the break.
///
/// This is useful if you found a better line break opportunity in your
/// justified text than Typst did.
///
/// ```example
/// #set par(justify: true)
/// #let jb = linebreak(justify: true)
///
/// I have manually tuned the #jb
/// line breaks in this paragraph #jb
/// for an _interesting_ result. #jb
/// ```
#[default(false)]
pub justify: bool,
}
impl Behave for LinebreakElem {
fn behaviour(&self) -> Behaviour {
Behaviour::Destructive
}
}
/// Strongly emphasizes content by increasing the font weight.
///
/// Increases the current font weight by a given `delta`.
///
/// # Example
/// ```example
/// This is *strong.* \
/// This is #strong[too.] \
///
/// #show strong: set text(red)
/// And this is *evermore.*
/// ```
///
/// # Syntax
/// This function also has dedicated syntax: To strongly emphasize content,
/// simply enclose it in stars/asterisks (`*`). Note that this only works at
/// word boundaries. To strongly emphasize part of a word, you have to use the
/// function.
#[elem(title = "Strong Emphasis", Show)]
pub struct StrongElem {
/// The delta to apply on the font weight.
///
/// ```example
/// #set strong(delta: 0)
/// No *effect!*
/// ```
#[default(300)]
pub delta: i64,
/// The content to strongly emphasize.
#[required]
pub body: Content,
}
impl Show for StrongElem {
#[tracing::instrument(name = "StrongElem::show", skip_all)]
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self
.body()
.clone()
.styled(TextElem::set_delta(Delta(self.delta(styles)))))
}
}
/// A delta that is summed up when folded.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Delta(pub i64);
cast! {
Delta,
self => self.0.into_value(),
v: i64 => Self(v),
}
impl Fold for Delta {
type Output = i64;
fn fold(self, outer: Self::Output) -> Self::Output {
outer + self.0
}
}
/// Emphasizes content by setting it in italics.
///
/// - If the current [text style]($text.style) is `{"normal"}`, this turns it
/// into `{"italic"}`.
/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to
/// `{"normal"}`.
///
/// # Example
/// ```example
/// This is _emphasized._ \
/// This is #emph[too.]
///
/// #show emph: it => {
/// text(blue, it.body)
/// }
///
/// This is _emphasized_ differently.
/// ```
///
/// # Syntax
/// This function also has dedicated syntax: To emphasize content, simply
/// enclose it in underscores (`_`). Note that this only works at word
/// boundaries. To emphasize part of a word, you have to use the function.
#[elem(title = "Emphasis", Show)]
pub struct EmphElem {
/// The content to emphasize.
#[required]
pub body: Content,
}
impl Show for EmphElem {
#[tracing::instrument(name = "EmphElem::show", skip(self))]
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
Ok(self.body().clone().styled(TextElem::set_emph(Toggle)))
}
}
/// A toggle that turns on and off alternatingly if folded.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Toggle;
cast! {
Toggle,
self => Value::None,
_: Value => Self,
}
impl Fold for Toggle {
type Output = bool;
fn fold(self, outer: Self::Output) -> Self::Output {
!outer
}
}
/// Converts text or content to lowercase.
///
/// # Example
/// ```example
/// #lower("ABC") \
/// #lower[*My Text*] \
/// #lower[already low]
/// ```
#[func(title = "Lowercase")]
pub fn lower(
/// The text to convert to lowercase.
text: Caseable,
) -> Caseable {
case(text, Case::Lower)
}
/// Converts text or content to uppercase.
///
/// # Example
/// ```example
/// #upper("abc") \
/// #upper[*my text*] \
/// #upper[ALREADY HIGH]
/// ```
#[func(title = "Uppercase")]
pub fn upper(
/// The text to convert to uppercase.
text: Caseable,
) -> Caseable {
case(text, Case::Upper)
}
/// Change the case of text.
fn case(text: Caseable, case: Case) -> Caseable {
match text {
Caseable::Str(v) => Caseable::Str(case.apply(&v).into()),
Caseable::Content(v) => {
Caseable::Content(v.styled(TextElem::set_case(Some(case))))
}
}
}
/// A value whose case can be changed.
pub enum Caseable {
Str(Str),
Content(Content),
}
cast! {
Caseable,
self => match self {
Self::Str(v) => v.into_value(),
Self::Content(v) => v.into_value(),
},
v: Str => Self::Str(v),
v: Content => Self::Content(v),
}
/// A case transformation on text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Case {
/// Everything is lowercased.
Lower,
/// Everything is uppercased.
Upper,
}
impl Case {
/// Apply the case to a string.
pub fn apply(self, text: &str) -> String {
match self {
Self::Lower => text.to_lowercase(),
Self::Upper => text.to_uppercase(),
}
}
}
/// Displays text in small capitals.
///
/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
/// support this feature. Sometimes smallcaps are part of a dedicated font and
/// sometimes they are not available at all. In the future, this function will
/// support selecting a dedicated smallcaps font as well as synthesizing
/// smallcaps from normal letters, but this is not yet implemented.
///
/// # Example
/// ```example
/// #set par(justify: true)
/// #set heading(numbering: "I.")
///
/// #show heading: it => {
/// set block(below: 10pt)
/// set text(weight: "regular")
/// align(center, smallcaps(it))
/// }
///
/// = Introduction
/// #lorem(40)
/// ```
#[func(title = "Small Capitals")]
pub fn smallcaps(
/// The text to display to small capitals.
body: Content,
) -> Content {
body.styled(TextElem::set_smallcaps(true))
}
/// Creates blind text.
///
/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given
/// number of words. The sequence of words generated by the function is always
/// the same but randomly chosen. As usual for blind texts, it does not make any
/// sense. Use it as a placeholder to try layouts.
///
/// # Example
/// ```example
/// = Blind Text
/// #lorem(30)
///
/// = More Blind Text
/// #lorem(15)
/// ```
#[func(keywords = ["Blind Text"])]
pub fn lorem(
/// The length of the blind text in words.
words: usize,
) -> Str {
lipsum::lipsum(words).replace("--", "").into()
}

View File

@ -1,547 +0,0 @@
use std::f64::consts::SQRT_2;
use crate::prelude::*;
/// A rectangle with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #rect(width: 35%, height: 30pt)
///
/// // With content.
/// #rect[
/// Automatically sized \
/// to fit the content.
/// ]
/// ```
#[elem(title = "Rectangle", Layout)]
pub struct RectElem {
/// The rectangle's width, relative to its parent container.
pub width: Smart<Rel<Length>>,
/// The rectangle's height, relative to its parent container.
pub height: Smart<Rel<Length>>,
/// How to fill the rectangle.
///
/// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both.
///
/// ```example
/// #rect(fill: blue)
/// ```
pub fill: Option<Paint>,
/// How to stroke the rectangle. This can be:
///
/// - `{none}` to disable stroking
/// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
/// given.
/// - Any kind of [stroke]($stroke)
/// - A dictionary describing the stroke for each side inidvidually. The
/// dictionary can contain the following keys in order of precedence:
/// - `top`: The top stroke.
/// - `right`: The right stroke.
/// - `bottom`: The bottom stroke.
/// - `left`: The left stroke.
/// - `x`: The horizontal stroke.
/// - `y`: The vertical stroke.
/// - `rest`: The stroke on all sides except those for which the
/// dictionary explicitly sets a size.
///
/// ```example
/// #stack(
/// dir: ltr,
/// spacing: 1fr,
/// rect(stroke: red),
/// rect(stroke: 2pt),
/// rect(stroke: 2pt + red),
/// )
/// ```
#[resolve]
#[fold]
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
/// How much to round the rectangle's corners, relative to the minimum of
/// the width and height divided by two. This can be:
///
/// - A relative length for a uniform corner radius.
/// - A dictionary: With a dictionary, the stroke for each side can be set
/// individually. The dictionary can contain the following keys in order
/// of precedence:
/// - `top-left`: The top-left corner radius.
/// - `top-right`: The top-right corner radius.
/// - `bottom-right`: The bottom-right corner radius.
/// - `bottom-left`: The bottom-left corner radius.
/// - `left`: The top-left and bottom-left corner radii.
/// - `top`: The top-left and top-right corner radii.
/// - `right`: The top-right and bottom-right corner radii.
/// - `bottom`: The bottom-left and bottom-right corner radii.
/// - `rest`: The radii for all corners except those for which the
/// dictionary explicitly sets a size.
///
/// ```example
/// #set rect(stroke: 4pt)
/// #rect(
/// radius: (
/// left: 5pt,
/// top-right: 20pt,
/// bottom-right: 10pt,
/// ),
/// stroke: (
/// left: red,
/// top: yellow,
/// right: green,
/// bottom: blue,
/// ),
/// )
/// ```
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the rectangle's content.
/// See the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Abs::pt(5.0).into()))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the rectangle's size without affecting the layout.
/// See the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the rectangle.
///
/// When this is omitted, the rectangle takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
pub body: Option<Content>,
}
impl Layout for RectElem {
#[tracing::instrument(name = "RectElem::layout", skip_all)]
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
layout(
vt,
styles,
regions,
ShapeKind::Rect,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles),
self.inset(styles),
self.outset(styles),
self.radius(styles),
self.span(),
)
}
}
/// A square with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #square(size: 40pt)
///
/// // With content.
/// #square[
/// Automatically \
/// sized to fit.
/// ]
/// ```
#[elem(Layout)]
pub struct SquareElem {
/// The square's side length. This is mutually exclusive with `width` and
/// `height`.
#[external]
pub size: Smart<Length>,
/// The square's width. This is mutually exclusive with `size` and `height`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// width.
#[parse(
let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
match size {
None => args.named("width")?,
size => size,
}
)]
pub width: Smart<Rel<Length>>,
/// The square's height. This is mutually exclusive with `size` and `width`.
///
/// In contrast to `size`, this can be relative to the parent container's
/// height.
#[parse(match size {
None => args.named("height")?,
size => size,
})]
pub height: Smart<Rel<Length>>,
/// How to fill the square. See the [rectangle's documentation]($rect.fill)
/// for more details.
pub fill: Option<Paint>,
/// How to stroke the square. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
/// How much to round the square's corners. See the
/// [rectangle's documentation]($rect.radius) for more details.
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the square's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Abs::pt(5.0).into()))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the square's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the square. The square expands to fit this
/// content, keeping the 1-1 aspect ratio.
///
/// When this is omitted, the square takes on a default size of at most
/// `{30pt}`.
#[positional]
pub body: Option<Content>,
}
impl Layout for SquareElem {
#[tracing::instrument(name = "SquareElem::layout", skip_all)]
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
layout(
vt,
styles,
regions,
ShapeKind::Square,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles),
self.inset(styles),
self.outset(styles),
self.radius(styles),
self.span(),
)
}
}
/// An ellipse with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #ellipse(width: 35%, height: 30pt)
///
/// // With content.
/// #ellipse[
/// #set align(center)
/// Automatically sized \
/// to fit the content.
/// ]
/// ```
#[elem(Layout)]
pub struct EllipseElem {
/// The ellipse's width, relative to its parent container.
pub width: Smart<Rel<Length>>,
/// The ellipse's height, relative to its parent container.
pub height: Smart<Rel<Length>>,
/// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
/// for more details.
pub fill: Option<Paint>,
/// How to stroke the ellipse. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
pub stroke: Smart<Option<Stroke>>,
/// How much to pad the ellipse's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Abs::pt(5.0).into()))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the ellipse's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the ellipse.
///
/// When this is omitted, the ellipse takes on a default size of at most
/// `{45pt}` by `{30pt}`.
#[positional]
pub body: Option<Content>,
}
impl Layout for EllipseElem {
#[tracing::instrument(name = "EllipseElem::layout", skip_all)]
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
layout(
vt,
styles,
regions,
ShapeKind::Ellipse,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles).map(Sides::splat),
self.inset(styles),
self.outset(styles),
Corners::splat(Rel::zero()),
self.span(),
)
}
}
/// A circle with optional content.
///
/// # Example
/// ```example
/// // Without content.
/// #circle(radius: 25pt)
///
/// // With content.
/// #circle[
/// #set align(center + horizon)
/// Automatically \
/// sized to fit.
/// ]
/// ```
#[elem(Layout)]
pub struct CircleElem {
/// The circle's radius. This is mutually exclusive with `width` and
/// `height`.
#[external]
pub radius: Length,
/// The circle's width. This is mutually exclusive with `radius` and
/// `height`.
///
/// In contrast to `radius`, this can be relative to the parent container's
/// width.
#[parse(
let size = args
.named::<Smart<Length>>("radius")?
.map(|s| s.map(|r| 2.0 * Rel::from(r)));
match size {
None => args.named("width")?,
size => size,
}
)]
pub width: Smart<Rel<Length>>,
/// The circle's height. This is mutually exclusive with `radius` and
/// `width`.
///
/// In contrast to `radius`, this can be relative to the parent container's
/// height.
#[parse(match size {
None => args.named("height")?,
size => size,
})]
pub height: Smart<Rel<Length>>,
/// How to fill the circle. See the [rectangle's documentation]($rect.fill)
/// for more details.
pub fill: Option<Paint>,
/// How to stroke the circle. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
#[default(Smart::Auto)]
pub stroke: Smart<Option<Stroke>>,
/// How much to pad the circle's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
#[default(Sides::splat(Abs::pt(5.0).into()))]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the circle's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
/// The content to place into the circle. The circle expands to fit this
/// content, keeping the 1-1 aspect ratio.
#[positional]
pub body: Option<Content>,
}
impl Layout for CircleElem {
#[tracing::instrument(name = "CircleElem::layout", skip_all)]
fn layout(
&self,
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
layout(
vt,
styles,
regions,
ShapeKind::Circle,
&self.body(styles),
Axes::new(self.width(styles), self.height(styles)),
self.fill(styles),
self.stroke(styles).map(Sides::splat),
self.inset(styles),
self.outset(styles),
Corners::splat(Rel::zero()),
self.span(),
)
}
}
/// Layout a shape.
#[tracing::instrument(name = "shape::layout", skip_all)]
#[allow(clippy::too_many_arguments)]
fn layout(
vt: &mut Vt,
styles: StyleChain,
regions: Regions,
kind: ShapeKind,
body: &Option<Content>,
sizing: Axes<Smart<Rel<Length>>>,
fill: Option<Paint>,
stroke: Smart<Sides<Option<Stroke<Abs>>>>,
mut inset: Sides<Rel<Abs>>,
outset: Sides<Rel<Abs>>,
radius: Corners<Rel<Abs>>,
span: Span,
) -> SourceResult<Fragment> {
let resolved = sizing
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
let mut frame;
if let Some(child) = body {
let region = resolved.unwrap_or(regions.base());
if kind.is_round() {
inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
}
// Pad the child.
let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
let expand = sizing.as_ref().map(Smart::is_custom);
let pod = Regions::one(region, expand);
frame = child.layout(vt, styles, pod)?.into_frame();
// Enforce correct size.
*frame.size_mut() = expand.select(region, frame.size());
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
if kind.is_quadratic() {
frame.set_size(Size::splat(frame.size().max_by_side()));
let length = frame.size().max_by_side().min(region.min_by_side());
let pod = Regions::one(Size::splat(length), Axes::splat(true));
frame = child.layout(vt, styles, pod)?.into_frame();
}
// Enforce correct size again.
*frame.size_mut() = expand.select(region, frame.size());
if kind.is_quadratic() {
frame.set_size(Size::splat(frame.size().max_by_side()));
}
} else {
// The default size that a shape takes on if it has no child and
// enough space.
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
let mut size = resolved.unwrap_or(default.min(regions.base()));
if kind.is_quadratic() {
size = Size::splat(size.min_by_side());
}
frame = Frame::soft(size);
}
// Prepare stroke.
let stroke = match stroke {
Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())),
Smart::Auto => Sides::splat(None),
Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)),
};
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
if kind.is_round() {
let outset = outset.relative_to(frame.size());
let size = frame.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top);
let shape = ellipse(size, fill, stroke.left);
frame.prepend(pos, FrameItem::Shape(shape, span));
} else {
frame.fill_and_stroke(fill, stroke, outset, radius, span);
}
}
// Apply metadata.
frame.meta(styles, false);
Ok(Fragment::frame(frame))
}
/// A category of shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ShapeKind {
/// A rectangle with equal side lengths.
Square,
/// A quadrilateral with four right angles.
Rect,
/// An ellipse with coinciding foci.
Circle,
/// A curve around two focal points.
Ellipse,
}
impl ShapeKind {
/// Whether this shape kind is curvy.
fn is_round(self) -> bool {
matches!(self, Self::Circle | Self::Ellipse)
}
/// Whether this shape kind has equal side length.
fn is_quadratic(self) -> bool {
matches!(self, Self::Square | Self::Circle)
}
}

View File

@ -1,4 +1,11 @@
use super::*;
use heck::ToKebabCase;
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{DeriveInput, Ident, Result, Token};
use crate::util::{documentation, foundations};
/// Expand the `#[derive(Cast)]` macro.
pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> {
@ -43,9 +50,9 @@ pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> {
});
Ok(quote! {
::typst::eval::cast! {
#foundations::cast! {
#ty,
self => ::typst::eval::IntoValue::into_value(match self {
self => #foundations::IntoValue::into_value(match self {
#(#variants_to_strs),*
}),
#(#strs_to_variants),*
@ -62,8 +69,6 @@ struct Variant {
/// Expand the `cast!` macro.
pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let eval = quote! { ::typst::eval };
let input: CastInput = syn::parse2(stream)?;
let ty = &input.ty;
let castable_body = create_castable_body(&input);
@ -74,16 +79,16 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! {
impl #eval::Reflect for #ty {
fn input() -> #eval::CastInfo {
impl #foundations::Reflect for #ty {
fn input() -> #foundations::CastInfo {
#input_body
}
fn output() -> #eval::CastInfo {
fn output() -> #foundations::CastInfo {
#output_body
}
fn castable(value: &#eval::Value) -> bool {
fn castable(value: &#foundations::Value) -> bool {
#castable_body
}
}
@ -92,8 +97,8 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let into_value = (input.into_value.is_some() || input.dynamic).then(|| {
quote! {
impl #eval::IntoValue for #ty {
fn into_value(self) -> #eval::Value {
impl #foundations::IntoValue for #ty {
fn into_value(self) -> #foundations::Value {
#into_value_body
}
}
@ -102,8 +107,8 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! {
impl #eval::FromValue for #ty {
fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> {
impl #foundations::FromValue for #ty {
fn from_value(value: #foundations::Value) -> ::typst::diag::StrResult<Self> {
#from_value_body
}
}
@ -196,7 +201,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
}
Pattern::Ty(_, ty) => {
casts.push(quote! {
if <#ty as ::typst::eval::Reflect>::castable(value) {
if <#ty as #foundations::Reflect>::castable(value) {
return true;
}
});
@ -206,7 +211,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
let dynamic_check = input.dynamic.then(|| {
quote! {
if let ::typst::eval::Value::Dyn(dynamic) = &value {
if let #foundations::Value::Dyn(dynamic) = &value {
if dynamic.is::<Self>() {
return true;
}
@ -216,7 +221,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
let str_check = (!strings.is_empty()).then(|| {
quote! {
if let ::typst::eval::Value::Str(string) = &value {
if let #foundations::Value::Str(string) = &value {
match string.as_str() {
#(#strings,)*
_ => {}
@ -241,21 +246,21 @@ fn create_input_body(input: &CastInput) -> TokenStream {
infos.push(match &cast.pattern {
Pattern::Str(lit) => {
quote! {
::typst::eval::CastInfo::Value(
::typst::eval::IntoValue::into_value(#lit),
#foundations::CastInfo::Value(
#foundations::IntoValue::into_value(#lit),
#docs,
)
}
}
Pattern::Ty(_, ty) => {
quote! { <#ty as ::typst::eval::Reflect>::input() }
quote! { <#ty as #foundations::Reflect>::input() }
}
});
}
if input.dynamic {
infos.push(quote! {
::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>())
#foundations::CastInfo::Type(#foundations::Type::of::<Self>())
});
}
@ -266,7 +271,7 @@ fn create_input_body(input: &CastInput) -> TokenStream {
fn create_output_body(input: &CastInput) -> TokenStream {
if input.dynamic {
quote! { ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>()) }
quote! { #foundations::CastInfo::Type(#foundations::Type::of::<Self>()) }
} else {
quote! { Self::input() }
}
@ -276,7 +281,7 @@ fn create_into_value_body(input: &CastInput) -> TokenStream {
if let Some(expr) = &input.into_value {
quote! { #expr }
} else {
quote! { ::typst::eval::Value::dynamic(self) }
quote! { #foundations::Value::dynamic(self) }
}
}
@ -292,8 +297,8 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
}
Pattern::Ty(binding, ty) => {
cast_checks.push(quote! {
if <#ty as ::typst::eval::Reflect>::castable(&value) {
let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?;
if <#ty as #foundations::Reflect>::castable(&value) {
let #binding = <#ty as #foundations::FromValue>::from_value(value)?;
return Ok(#expr);
}
});
@ -303,7 +308,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
let dynamic_check = input.dynamic.then(|| {
quote! {
if let ::typst::eval::Value::Dyn(dynamic) = &value {
if let #foundations::Value::Dyn(dynamic) = &value {
if let Some(concrete) = dynamic.downcast::<Self>() {
return Ok(concrete.clone());
}
@ -313,7 +318,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
let str_check = (!string_arms.is_empty()).then(|| {
quote! {
if let ::typst::eval::Value::Str(string) = &value {
if let #foundations::Value::Str(string) = &value {
match string.as_str() {
#(#string_arms,)*
_ => {}
@ -326,6 +331,6 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
#dynamic_check
#str_check
#(#cast_checks)*
Err(<Self as ::typst::eval::Reflect>::error(&value))
Err(<Self as #foundations::Reflect>::error(&value))
}
}

View File

@ -0,0 +1,57 @@
use heck::{ToKebabCase, ToTitleCase};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Attribute, Ident, Result, Token, Type, Visibility};
use crate::util::{documentation, foundations};
/// Expand the `#[category]` macro.
pub fn category(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
let syn::Item::Verbatim(stream) = item else {
bail!(item, "expected bare static");
};
let BareStatic { attrs, vis, ident, ty, .. } = syn::parse2(stream)?;
let name = ident.to_string().to_kebab_case();
let title = name.to_title_case();
let docs = documentation(&attrs);
Ok(quote! {
#(#attrs)*
#vis static #ident: #ty = {
static DATA: #foundations::CategoryData = #foundations::CategoryData {
name: #name,
title: #title,
docs: #docs,
};
#foundations::Category::from_data(&DATA)
};
})
}
/// Parse a bare `pub static CATEGORY: Category;` item.
pub struct BareStatic {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub static_token: Token![static],
pub ident: Ident,
pub colon_token: Token![:],
pub ty: Type,
pub semi_token: Token![;],
}
impl Parse for BareStatic {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
attrs: input.call(Attribute::parse_outer)?,
vis: input.parse()?,
static_token: input.parse()?,
ident: input.parse()?,
colon_token: input.parse()?,
ty: input.parse()?,
semi_token: input.parse()?,
})
}
}

View File

@ -1,4 +1,15 @@
use super::*;
use heck::{ToKebabCase, ToShoutySnakeCase, ToUpperCamelCase};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{parse_quote, Ident, Result, Token};
use crate::util::{
determine_name_and_title, documentation, foundations, has_attr, kw, parse_attr,
parse_flag, parse_string, parse_string_array, quote_option, validate_attrs,
BlockWithReturn,
};
/// Expand the `#[elem]` macro.
pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
@ -136,7 +147,6 @@ struct Field {
synthesized: bool,
borrowed: bool,
ghost: bool,
forced_variant: Option<usize>,
parse: Option<BlockWithReturn>,
default: Option<syn::Expr>,
}
@ -226,10 +236,6 @@ fn parse_field(field: &syn::Field) -> Result<Field> {
docs: documentation(&attrs),
internal: has_attr(&mut attrs, "internal"),
external: has_attr(&mut attrs, "external"),
forced_variant: parse_attr::<syn::LitInt>(&mut attrs, "variant")?
.flatten()
.map(|lit| lit.base10_parse())
.transpose()?,
positional,
required,
variadic,
@ -262,12 +268,12 @@ fn parse_field(field: &syn::Field) -> Result<Field> {
if field.resolve {
let output = &field.output;
field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output };
field.output = parse_quote! { <#output as #foundations::Resolve>::Output };
}
if field.fold {
let output = &field.output;
field.output = parse_quote! { <#output as ::typst::model::Fold>::Output };
field.output = parse_quote! { <#output as #foundations::Fold>::Output };
}
validate_attrs(&attrs)?;
@ -317,8 +323,8 @@ fn create(element: &Elem) -> Result<TokenStream> {
let label_and_location = element.unless_capability("Unlabellable", || {
quote! {
location: Option<::typst::model::Location>,
label: Option<::typst::model::Label>,
location: Option<::typst::introspection::Location>,
label: Option<#foundations::Label>,
prepared: bool,
}
});
@ -330,7 +336,7 @@ fn create(element: &Elem) -> Result<TokenStream> {
#vis struct #ident {
span: ::typst::syntax::Span,
#label_and_location
guards: ::std::vec::Vec<::typst::model::Guard>,
guards: ::std::vec::Vec<#foundations::Guard>,
#(#fields,)*
}
@ -353,9 +359,9 @@ fn create(element: &Elem) -> Result<TokenStream> {
#partial_eq_impl
#repr_impl
impl ::typst::eval::IntoValue for #ident {
fn into_value(self) -> ::typst::eval::Value {
::typst::eval::Value::Content(::typst::model::Content::new(self))
impl #foundations::IntoValue for #ident {
fn into_value(self) -> #foundations::Value {
#foundations::Value::Content(#foundations::Content::new(self))
}
}
})
@ -382,12 +388,9 @@ fn create_field(field: &Field) -> TokenStream {
/// Creates the element's enum for field identifiers.
fn create_fields_enum(element: &Elem) -> TokenStream {
let model = quote! { ::typst::model };
let Elem { ident, enum_ident, .. } = element;
let mut fields = element.real_fields().collect::<Vec<_>>();
fields.sort_by_key(|field| field.forced_variant.unwrap_or(usize::MAX));
let fields = element.real_fields().collect::<Vec<_>>();
let field_names = fields.iter().map(|Field { name, .. }| name).collect::<Vec<_>>();
let field_consts = fields
.iter()
@ -399,20 +402,14 @@ fn create_fields_enum(element: &Elem) -> TokenStream {
.map(|Field { enum_ident, .. }| enum_ident)
.collect::<Vec<_>>();
let definitions =
fields.iter().map(|Field { forced_variant, enum_ident, .. }| {
if let Some(variant) = forced_variant {
let variant = proc_macro2::Literal::u8_unsuffixed(*variant as _);
quote! { #enum_ident = #variant }
} else {
quote! { #enum_ident }
}
});
let definitions = fields.iter().map(|Field { enum_ident, .. }| {
quote! { #enum_ident }
});
quote! {
// To hide the private type
const _: () = {
impl #model::ElementFields for #ident {
impl #foundations::ElementFields for #ident {
type Fields = #enum_ident;
}
@ -578,16 +575,15 @@ fn create_push_field_method(field: &Field) -> TokenStream {
/// Create a setter method for a field.
fn create_set_field_method(element: &Elem, field: &Field) -> TokenStream {
let model = quote! { ::typst::model };
let elem = &element.ident;
let Field { vis, ident, set_ident, enum_ident, ty, name, .. } = field;
let doc = format!("Create a style property for the `{}` field.", name);
quote! {
#[doc = #doc]
#vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
::typst::model::Style::Property(::typst::model::Property::new(
<Self as ::typst::model::NativeElement>::elem(),
<#elem as #model::ElementFields>::Fields::#enum_ident as u8,
#vis fn #set_ident(#ident: #ty) -> #foundations::Style {
#foundations::Style::Property(#foundations::Property::new(
<Self as #foundations::NativeElement>::elem(),
<#elem as #foundations::ElementFields>::Fields::#enum_ident as u8,
#ident,
))
}
@ -608,7 +604,7 @@ fn create_field_in_method(element: &Elem, field: &Field) -> TokenStream {
quote! {
#[doc = #doc]
#vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output {
#vis fn #ident_in(styles: #foundations::StyleChain) -> #output {
#access
}
}
@ -646,7 +642,7 @@ fn create_field_method(element: &Elem, field: &Field) -> TokenStream {
quote! {
#[doc = #docs]
#vis fn #ident<'a>(&'a self, styles: ::typst::model::StyleChain<'a>) -> &'a #output {
#vis fn #ident<'a>(&'a self, styles: #foundations::StyleChain<'a>) -> &'a #output {
#access
}
}
@ -655,7 +651,7 @@ fn create_field_method(element: &Elem, field: &Field) -> TokenStream {
quote! {
#[doc = #docs]
#vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output {
#vis fn #ident(&self, styles: #foundations::StyleChain) -> #output {
#access
}
}
@ -668,7 +664,6 @@ fn create_style_chain_access(
field: &Field,
inherent: TokenStream,
) -> TokenStream {
let model = quote! { ::typst::model };
let elem = &element.ident;
let Field { ty, default, enum_ident, .. } = field;
@ -693,8 +688,8 @@ fn create_style_chain_access(
quote! {
#init
styles.#getter::<#ty>(
<Self as ::typst::model::NativeElement>::elem(),
<#elem as #model::ElementFields>::Fields::#enum_ident as u8,
<Self as #foundations::NativeElement>::elem(),
<#elem as #foundations::ElementFields>::Fields::#enum_ident as u8,
#inherent,
#default,
)
@ -703,9 +698,6 @@ fn create_style_chain_access(
/// Creates the element's `Pack` implementation.
fn create_native_elem_impl(element: &Elem) -> TokenStream {
let eval = quote! { ::typst::eval };
let model = quote! { ::typst::model };
let Elem { name, ident, title, scope, keywords, docs, .. } = element;
let vtable_func = create_vtable_func(element);
@ -716,9 +708,9 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
.map(create_param_info);
let scope = if *scope {
quote! { <#ident as #eval::NativeScope>::scope() }
quote! { <#ident as #foundations::NativeScope>::scope() }
} else {
quote! { #eval::Scope::new() }
quote! { #foundations::Scope::new() }
};
// Fields that can be accessed using the `field` method.
@ -729,37 +721,39 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
if field.ghost {
quote! {
<#elem as #model::ElementFields>::Fields::#name => None,
<#elem as #foundations::ElementFields>::Fields::#name => None,
}
} else if field.inherent() {
quote! {
<#elem as #model::ElementFields>::Fields::#name => Some(
::typst::eval::IntoValue::into_value(self.#field_ident.clone())
<#elem as #foundations::ElementFields>::Fields::#name => Some(
#foundations::IntoValue::into_value(self.#field_ident.clone())
),
}
} else {
quote! {
<#elem as #model::ElementFields>::Fields::#name => {
self.#field_ident.clone().map(::typst::eval::IntoValue::into_value)
<#elem as #foundations::ElementFields>::Fields::#name => {
self.#field_ident.clone().map(#foundations::IntoValue::into_value)
}
}
}
});
// Fields that can be set using the `set_field` method.
let field_set_matches = element.visible_fields()
.filter(|field| field.settable() && !field.synthesized && !field.ghost).map(|field| {
let elem = &element.ident;
let name = &field.enum_ident;
let field_ident = &field.ident;
let field_set_matches = element
.visible_fields()
.filter(|field| field.settable() && !field.synthesized && !field.ghost)
.map(|field| {
let elem = &element.ident;
let name = &field.enum_ident;
let field_ident = &field.ident;
quote! {
<#elem as #model::ElementFields>::Fields::#name => {
self.#field_ident = Some(::typst::eval::FromValue::from_value(value)?);
return Ok(());
quote! {
<#elem as #foundations::ElementFields>::Fields::#name => {
self.#field_ident = Some(#foundations::FromValue::from_value(value)?);
return Ok(());
}
}
}
});
});
// Fields that are inherent.
let field_inherent_matches = element
@ -771,8 +765,8 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let field_ident = &field.ident;
quote! {
<#elem as #model::ElementFields>::Fields::#name => {
self.#field_ident = ::typst::eval::FromValue::from_value(value)?;
<#elem as #foundations::ElementFields>::Fields::#name => {
self.#field_ident = #foundations::FromValue::from_value(value)?;
return Ok(());
}
}
@ -790,13 +784,13 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
// Internal fields create an error that they are unknown.
let unknown_field = format!("unknown field `{field_name}` on `{name}`");
quote! {
<#elem as #model::ElementFields>::Fields::#ident => ::typst::diag::bail!(#unknown_field),
<#elem as #foundations::ElementFields>::Fields::#ident => ::typst::diag::bail!(#unknown_field),
}
} else {
// Fields that cannot be set create an error that they are not settable.
let not_settable = format!("cannot set `{field_name}` on `{name}`");
quote! {
<#elem as #model::ElementFields>::Fields::#ident => ::typst::diag::bail!(#not_settable),
<#elem as #foundations::ElementFields>::Fields::#ident => ::typst::diag::bail!(#not_settable),
}
}
});
@ -824,15 +818,15 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let field_ident = &field.ident;
let field_call = if name.len() > 15 {
quote! { EcoString::from(#name).into() }
quote! { ::ecow::EcoString::from(#name).into() }
} else {
quote! { EcoString::inline(#name).into() }
quote! { ::ecow::EcoString::inline(#name).into() }
};
quote! {
fields.insert(
#field_call,
::typst::eval::IntoValue::into_value(self.#field_ident.clone())
#foundations::IntoValue::into_value(self.#field_ident.clone())
);
}
});
@ -847,16 +841,16 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let field_ident = &field.ident;
let field_call = if name.len() > 15 {
quote! { EcoString::from(#name).into() }
quote! { ::ecow::EcoString::from(#name).into() }
} else {
quote! { EcoString::inline(#name).into() }
quote! { ::ecow::EcoString::inline(#name).into() }
};
quote! {
if let Some(value) = &self.#field_ident {
fields.insert(
#field_call,
::typst::eval::IntoValue::into_value(value.clone())
#foundations::IntoValue::into_value(value.clone())
);
}
}
@ -889,7 +883,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let label_field = element
.unless_capability("Unlabellable", || {
quote! {
self.label().map(::typst::eval::Value::Label)
self.label().map(#foundations::Value::Label)
}
})
.unwrap_or_else(|| quote! { None });
@ -905,51 +899,51 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
let local_name = element
.if_capability(
"LocalName",
|| quote! { Some(<#ident as ::typst::model::LocalName>::local_name) },
|| quote! { Some(<#ident as ::typst::text::LocalName>::local_name) },
)
.unwrap_or_else(|| quote! { None });
let unknown_field = format!("unknown field {{}} on {}", name);
let label_error = format!("cannot set label on {}", name);
let data = quote! {
#model::NativeElementData {
#foundations::NativeElementData {
name: #name,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
construct: <#ident as #model::Construct>::construct,
set: <#ident as #model::Set>::set,
construct: <#ident as #foundations::Construct>::construct,
set: <#ident as #foundations::Set>::set,
vtable: #vtable_func,
field_id: |name|
<
<#ident as #model::ElementFields>::Fields as ::std::str::FromStr
<#ident as #foundations::ElementFields>::Fields as ::std::str::FromStr
>::from_str(name).ok().map(|id| id as u8),
field_name: |id|
<
<#ident as #model::ElementFields>::Fields as ::std::convert::TryFrom<u8>
>::try_from(id).ok().map(<#ident as #model::ElementFields>::Fields::to_str),
<#ident as #foundations::ElementFields>::Fields as ::std::convert::TryFrom<u8>
>::try_from(id).ok().map(<#ident as #foundations::ElementFields>::Fields::to_str),
local_name: #local_name,
scope: #eval::Lazy::new(|| #scope),
params: #eval::Lazy::new(|| ::std::vec![#(#params),*])
scope: #foundations::Lazy::new(|| #scope),
params: #foundations::Lazy::new(|| ::std::vec![#(#params),*])
}
};
quote! {
impl #model::NativeElement for #ident {
fn data() -> &'static #model::NativeElementData {
static DATA: #model::NativeElementData = #data;
impl #foundations::NativeElement for #ident {
fn data() -> &'static #foundations::NativeElementData {
static DATA: #foundations::NativeElementData = #data;
&DATA
}
fn dyn_elem(&self) -> #model::Element {
#model::Element::of::<Self>()
fn dyn_elem(&self) -> #foundations::Element {
#foundations::Element::of::<Self>()
}
fn dyn_hash(&self, mut hasher: &mut dyn ::std::hash::Hasher) {
<Self as ::std::hash::Hash>::hash(self, &mut hasher);
}
fn dyn_eq(&self, other: &#model::Content) -> bool {
fn dyn_eq(&self, other: &#foundations::Content) -> bool {
if let Some(other) = other.to::<Self>() {
<Self as ::std::cmp::PartialEq>::eq(self, other)
} else {
@ -957,7 +951,7 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
}
}
fn dyn_clone(&self) -> ::std::sync::Arc<dyn #model::NativeElement> {
fn dyn_clone(&self) -> ::std::sync::Arc<dyn #foundations::NativeElement> {
::std::sync::Arc::new(Clone::clone(self))
}
@ -983,27 +977,27 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
}
}
fn label(&self) -> Option<#model::Label> {
fn label(&self) -> Option<#foundations::Label> {
#label
}
fn set_label(&mut self, label: #model::Label) {
fn set_label(&mut self, label: #foundations::Label) {
#set_label
}
fn location(&self) -> Option<#model::Location> {
fn location(&self) -> Option<::typst::introspection::Location> {
#location
}
fn set_location(&mut self, location: #model::Location) {
fn set_location(&mut self, location: ::typst::introspection::Location) {
#set_location
}
fn push_guard(&mut self, guard: #model::Guard) {
fn push_guard(&mut self, guard: #foundations::Guard) {
self.guards.push(guard);
}
fn is_guarded(&self, guard: #model::Guard) -> bool {
fn is_guarded(&self, guard: #foundations::Guard) -> bool {
self.guards.contains(&guard)
}
@ -1023,30 +1017,30 @@ fn create_native_elem_impl(element: &Elem) -> TokenStream {
#prepared
}
fn field(&self, id: u8) -> Option<::typst::eval::Value> {
let id = <#ident as #model::ElementFields>::Fields::try_from(id).ok()?;
fn field(&self, id: u8) -> Option<#foundations::Value> {
let id = <#ident as #foundations::ElementFields>::Fields::try_from(id).ok()?;
match id {
<#ident as #model::ElementFields>::Fields::Label => #label_field,
<#ident as #foundations::ElementFields>::Fields::Label => #label_field,
#(#field_matches)*
_ => None,
}
}
fn fields(&self) -> Dict {
let mut fields = Dict::new();
fn fields(&self) -> #foundations::Dict {
let mut fields = #foundations::Dict::new();
#(#field_dict)*
#(#field_opt_dict)*
fields
}
fn set_field(&mut self, id: u8, value: Value) -> ::typst::diag::StrResult<()> {
let id = <#ident as #model::ElementFields>::Fields::try_from(id)
fn set_field(&mut self, id: u8, value: #foundations::Value) -> ::typst::diag::StrResult<()> {
let id = <#ident as #foundations::ElementFields>::Fields::try_from(id)
.map_err(|_| ::ecow::eco_format!(#unknown_field, id))?;
match id {
#(#field_set_matches)*
#(#field_inherent_matches)*
#(#field_not_set_matches)*
<#ident as #model::ElementFields>::Fields::Label => {
<#ident as #foundations::ElementFields>::Fields::Label => {
::typst::diag::bail!(#label_error);
}
}
@ -1087,18 +1081,18 @@ fn create_construct_impl(element: &Elem) -> TokenStream {
.map(|field| &field.ident);
quote! {
impl ::typst::model::Construct for #ident {
impl #foundations::Construct for #ident {
fn construct(
vm: &mut ::typst::eval::Vm,
args: &mut ::typst::eval::Args,
) -> ::typst::diag::SourceResult<::typst::model::Content> {
args: &mut #foundations::Args,
) -> ::typst::diag::SourceResult<#foundations::Content> {
#(#pre)*
let mut element = Self::new(#(#defaults),*);
#(#handlers)*
Ok(::typst::model::Content::new(element))
Ok(#foundations::Content::new(element))
}
}
}
@ -1119,12 +1113,12 @@ fn create_set_impl(element: &Elem) -> TokenStream {
});
quote! {
impl ::typst::model::Set for #ident {
impl #foundations::Set for #ident {
fn set(
vm: &mut Vm,
args: &mut ::typst::eval::Args,
) -> ::typst::diag::SourceResult<::typst::model::Styles> {
let mut styles = ::typst::model::Styles::new();
vm: &mut ::typst::eval::Vm,
args: &mut #foundations::Args,
) -> ::typst::diag::SourceResult<#foundations::Styles> {
let mut styles = #foundations::Styles::new();
#(#handlers)*
Ok(styles)
}
@ -1135,7 +1129,7 @@ fn create_set_impl(element: &Elem) -> TokenStream {
/// Creates the element's `Locatable` implementation.
fn create_locatable_impl(element: &Elem) -> TokenStream {
let ident = &element.ident;
quote! { impl ::typst::model::Locatable for #ident {} }
quote! { impl ::typst::introspection::Locatable for #ident {} }
}
/// Creates the element's `PartialEq` implementation.
@ -1159,12 +1153,12 @@ fn create_repr_impl(element: &Elem) -> TokenStream {
let ident = &element.ident;
let repr_format = format!("{}{{}}", element.name);
quote! {
impl ::typst::eval::Repr for #ident {
impl #foundations::Repr for #ident {
fn repr(&self) -> ::ecow::EcoString {
let fields = self.fields().into_iter()
.map(|(name, value)| eco_format!("{}: {}", name, value.repr()))
let fields = #foundations::NativeElement::fields(self).into_iter()
.map(|(name, value)| ::ecow::eco_format!("{}: {}", name, value.repr()))
.collect::<Vec<_>>();
::ecow::eco_format!(#repr_format, ::typst::eval::repr::pretty_array_like(&fields, false))
::ecow::eco_format!(#repr_format, #foundations::repr::pretty_array_like(&fields, false))
}
}
}
@ -1185,7 +1179,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream {
if id == ::std::any::TypeId::of::<dyn #capability>() {
let vtable = unsafe {
let dangling = ::std::ptr::NonNull::<#ident>::dangling().as_ptr() as *const dyn #capability;
::typst::model::fat::vtable(dangling)
::typst::util::fat::vtable(dangling)
};
return Some(vtable);
}
@ -1224,20 +1218,20 @@ fn create_param_info(field: &Field) -> TokenStream {
quote! {
|| {
let typed: #default_ty = #default;
::typst::eval::IntoValue::into_value(typed)
#foundations::IntoValue::into_value(typed)
}
}
}));
let ty = if *variadic {
quote! { <#ty as ::typst::eval::Container>::Inner }
quote! { <#ty as #foundations::Container>::Inner }
} else {
quote! { #ty }
};
quote! {
::typst::eval::ParamInfo {
#foundations::ParamInfo {
name: #name,
docs: #docs,
input: <#ty as ::typst::eval::Reflect>::input(),
input: <#ty as #foundations::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,

View File

@ -1,4 +1,14 @@
use super::*;
use heck::ToKebabCase;
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_quote, Ident, Result};
use crate::util::{
determine_name_and_title, documentation, foundations, has_attr, kw, parse_attr,
parse_flag, parse_key_value, parse_string, parse_string_array, quote_option,
validate_attrs,
};
/// Expand the `#[func]` macro.
pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> {
@ -191,8 +201,6 @@ fn parse_param(
/// Produce the function's definition.
fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
let eval = quote! { ::typst::eval };
let Func { docs, vis, ident, .. } = func;
let item = rewrite_fn_item(item);
let ty = create_func_ty(func);
@ -200,9 +208,9 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
let creator = if ty.is_some() {
quote! {
impl #eval::NativeFunc for #ident {
fn data() -> &'static #eval::NativeFuncData {
static DATA: #eval::NativeFuncData = #data;
impl #foundations::NativeFunc for #ident {
fn data() -> &'static #foundations::NativeFuncData {
static DATA: #foundations::NativeFuncData = #data;
&DATA
}
}
@ -211,8 +219,8 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
let ident_data = quote::format_ident!("{ident}_data");
quote! {
#[doc(hidden)]
#vis fn #ident_data() -> &'static #eval::NativeFuncData {
static DATA: #eval::NativeFuncData = #data;
#vis fn #ident_data() -> &'static #foundations::NativeFuncData {
static DATA: #foundations::NativeFuncData = #data;
&DATA
}
}
@ -231,8 +239,6 @@ fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
/// Create native function data for the function.
fn create_func_data(func: &Func) -> TokenStream {
let eval = quote! { ::typst::eval };
let Func {
ident,
name,
@ -247,30 +253,30 @@ fn create_func_data(func: &Func) -> TokenStream {
} = func;
let scope = if *scope {
quote! { <#ident as #eval::NativeScope>::scope() }
quote! { <#ident as #foundations::NativeScope>::scope() }
} else {
quote! { #eval::Scope::new() }
quote! { #foundations::Scope::new() }
};
let closure = create_wrapper_closure(func);
let params = func.special.self_.iter().chain(&func.params).map(create_param_info);
let name = if *constructor {
quote! { <#parent as #eval::NativeType>::NAME }
quote! { <#parent as #foundations::NativeType>::NAME }
} else {
quote! { #name }
};
quote! {
#eval::NativeFuncData {
#foundations::NativeFuncData {
function: #closure,
name: #name,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
scope: #eval::Lazy::new(|| #scope),
params: #eval::Lazy::new(|| ::std::vec![#(#params),*]),
returns: #eval::Lazy::new(|| <#returns as #eval::Reflect>::output()),
scope: #foundations::Lazy::new(|| #scope),
params: #foundations::Lazy::new(|| ::std::vec![#(#params),*]),
returns: #foundations::Lazy::new(|| <#returns as #foundations::Reflect>::output()),
}
}
}
@ -335,7 +341,7 @@ fn create_wrapper_closure(func: &Func) -> TokenStream {
#handlers
#finish
let output = #call;
::typst::eval::IntoResult::into_result(output, args.span)
#foundations::IntoResult::into_result(output, args.span)
}
}
}
@ -346,7 +352,7 @@ fn create_param_info(param: &Param) -> TokenStream {
let positional = !named;
let required = !named && default.is_none();
let ty = if *variadic || (*named && default.is_none()) {
quote! { <#ty as ::typst::eval::Container>::Inner }
quote! { <#ty as #foundations::Container>::Inner }
} else {
quote! { #ty }
};
@ -354,15 +360,15 @@ fn create_param_info(param: &Param) -> TokenStream {
quote! {
|| {
let typed: #ty = #default;
::typst::eval::IntoValue::into_value(typed)
#foundations::IntoValue::into_value(typed)
}
}
}));
quote! {
::typst::eval::ParamInfo {
#foundations::ParamInfo {
name: #name,
docs: #docs,
input: <#ty as ::typst::eval::Reflect>::input(),
input: <#ty as #foundations::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,

View File

@ -5,22 +5,15 @@ extern crate proc_macro;
#[macro_use]
mod util;
mod cast;
mod category;
mod elem;
mod func;
mod scope;
mod symbols;
mod ty;
use heck::*;
use proc_macro::TokenStream as BoundaryStream;
use proc_macro2::TokenStream;
use quote::quote;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::{parse_quote, DeriveInput, Ident, Result, Token};
use self::util::*;
use syn::DeriveInput;
/// Makes a native Rust function usable as a Typst function.
///
@ -190,9 +183,6 @@ pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
/// - `#[synthesized]`: The field cannot be specified in a constructor or set
/// rule. Instead, it is added to an element before its show rule runs
/// through the `Synthesize` trait.
/// - `#[variant]`: Allows setting the ID of a field's variant. This is used
/// for fields that are accessed in `typst` and not `typst-library`. It gives
/// the field a stable ID that can be used to access it.
/// - `#[ghost]`: Allows creating fields that are only present in the style chain,
/// this means that they *cannot* be accessed by the user, they cannot be set
/// on an individual instantiated element, and must be set via the style chain.
@ -256,6 +246,15 @@ pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
.into()
}
/// Defines a category of definitions.
#[proc_macro_attribute]
pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::Item);
category::category(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type.
///
/// - `Reflect` makes Typst's runtime aware of the type's characteristics.

View File

@ -1,6 +1,9 @@
use heck::ToKebabCase;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_quote, Result};
use super::*;
use crate::util::{foundations, BareType};
/// Expand the `#[scope]` macro.
pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
@ -8,7 +11,6 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
bail!(item, "expected module or impl item");
};
let eval = quote! { ::typst::eval };
let self_ty = &item.self_ty;
let mut definitions = vec![];
@ -43,13 +45,13 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
Ok(quote! {
#base
impl #eval::NativeScope for #self_ty {
fn constructor() -> ::std::option::Option<&'static #eval::NativeFuncData> {
impl #foundations::NativeScope for #self_ty {
fn constructor() -> ::std::option::Option<&'static #foundations::NativeFuncData> {
#constructor
}
fn scope() -> #eval::Scope {
let mut scope = #eval::Scope::deduplicating();
fn scope() -> #foundations::Scope {
let mut scope = #foundations::Scope::deduplicating();
#(#definitions;)*
scope
}
@ -92,7 +94,7 @@ fn handle_fn(self_ty: &syn::Type, item: &mut syn::ImplItemFn) -> Result<FnKind>
}
syn::Meta::List(list) => {
let tokens = &list.tokens;
let meta: super::func::Meta = syn::parse2(tokens.clone())?;
let meta: crate::func::Meta = syn::parse2(tokens.clone())?;
list.tokens = quote! { #tokens, parent = #self_ty };
if meta.constructor {
return Ok(FnKind::Constructor(quote! { Some(#self_ty::#ident_data()) }));
@ -135,7 +137,7 @@ fn rewrite_primitive_base(item: &syn::ItemImpl, ident: &syn::Ident) -> TokenStre
let ident_data = quote::format_ident!("{}_data", sig.ident);
sigs.push(quote! { #sig; });
sigs.push(quote! {
fn #ident_data() -> &'static ::typst::eval::NativeFuncData;
fn #ident_data() -> &'static #foundations::NativeFuncData;
});
}

View File

@ -1,4 +1,9 @@
use super::*;
use proc_macro2::TokenStream;
use quote::quote;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::{Ident, Result, Token};
/// Expand the `symbols!` macro.
pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
@ -7,7 +12,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
let pairs = list.iter().map(|symbol| {
let name = symbol.name.to_string();
let kind = match &symbol.kind {
Kind::Single(c) => quote! { typst::eval::Symbol::single(#c), },
Kind::Single(c) => quote! { ::typst::symbols::Symbol::single(#c), },
Kind::Multiple(variants) => {
let variants = variants.iter().map(|variant| {
let name = &variant.name;
@ -15,7 +20,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
quote! { (#name, #c) }
});
quote! {
typst::eval::Symbol::list(&[#(#variants),*])
::typst::symbols::Symbol::list(&[#(#variants),*])
}
}
};

View File

@ -1,6 +1,12 @@
use syn::Attribute;
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Attribute, Ident, Result};
use super::*;
use crate::util::{
determine_name_and_title, documentation, foundations, kw, parse_flag, parse_string,
parse_string_array, BareType,
};
/// Expand the `#[ty]` macro.
pub fn ty(stream: TokenStream, item: syn::Item) -> Result<TokenStream> {
@ -68,44 +74,42 @@ fn parse(meta: Meta, ident: Ident, attrs: &[Attribute]) -> Result<Type> {
/// Produce the output of the macro.
fn create(ty: &Type, item: Option<&syn::Item>) -> TokenStream {
let eval = quote! { ::typst::eval };
let Type {
ident, name, long, title, docs, keywords, scope, ..
} = ty;
let constructor = if *scope {
quote! { <#ident as #eval::NativeScope>::constructor() }
quote! { <#ident as #foundations::NativeScope>::constructor() }
} else {
quote! { None }
};
let scope = if *scope {
quote! { <#ident as #eval::NativeScope>::scope() }
quote! { <#ident as #foundations::NativeScope>::scope() }
} else {
quote! { #eval::Scope::new() }
quote! { #foundations::Scope::new() }
};
let data = quote! {
#eval::NativeTypeData {
#foundations::NativeTypeData {
name: #name,
long_name: #long,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
constructor: #eval::Lazy::new(|| #constructor),
scope: #eval::Lazy::new(|| #scope),
constructor: #foundations::Lazy::new(|| #constructor),
scope: #foundations::Lazy::new(|| #scope),
}
};
quote! {
#item
impl #eval::NativeType for #ident {
impl #foundations::NativeType for #ident {
const NAME: &'static str = #name;
fn data() -> &'static #eval::NativeTypeData {
static DATA: #eval::NativeTypeData = #data;
fn data() -> &'static #foundations::NativeTypeData {
static DATA: #foundations::NativeTypeData = #data;
&DATA
}
}

View File

@ -1,8 +1,9 @@
use quote::ToTokens;
use heck::{ToKebabCase, ToTitleCase};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::token::Token;
use syn::Attribute;
use super::*;
use syn::{Attribute, Ident, Result, Token};
/// Return an error at the given item.
macro_rules! bail {
@ -199,6 +200,16 @@ impl<T: Parse> Parse for Array<T> {
}
}
/// Shorthand for `::typst::foundations`.
#[allow(non_camel_case_types)]
pub struct foundations;
impl quote::ToTokens for foundations {
fn to_tokens(&self, tokens: &mut TokenStream) {
quote! { ::typst::foundations }.to_tokens(tokens);
}
}
/// For parsing attributes of the form:
/// #[attr(
/// statement;
@ -220,15 +231,6 @@ impl Parse for BlockWithReturn {
}
}
pub mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(title);
syn::custom_keyword!(scope);
syn::custom_keyword!(constructor);
syn::custom_keyword!(keywords);
syn::custom_keyword!(parent);
}
/// Parse a bare `type Name;` item.
pub struct BareType {
pub attrs: Vec<Attribute>,
@ -239,7 +241,7 @@ pub struct BareType {
impl Parse for BareType {
fn parse(input: ParseStream) -> Result<Self> {
Ok(BareType {
Ok(Self {
attrs: input.call(Attribute::parse_outer)?,
type_token: input.parse()?,
ident: input.parse()?,
@ -247,3 +249,12 @@ impl Parse for BareType {
})
}
}
pub mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(title);
syn::custom_keyword!(scope);
syn::custom_keyword!(constructor);
syn::custom_keyword!(keywords);
syn::custom_keyword!(parent);
}

View File

@ -1,7 +1,7 @@
use once_cell::sync::Lazy;
use pdf_writer::types::DeviceNSubtype;
use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref};
use typst::geom::{Color, ColorSpace, Paint};
use typst::visualize::{Color, ColorSpace, Paint};
use crate::deflate;
use crate::page::{PageContext, Transforms};

View File

@ -5,7 +5,7 @@ use ecow::{eco_format, EcoString};
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap};
use pdf_writer::{Filter, Finish, Name, Rect, Str};
use ttf_parser::{name_id, GlyphId, Tag};
use typst::font::Font;
use typst::text::Font;
use typst::util::SliceExt;
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};

View File

@ -5,9 +5,10 @@ use ecow::{eco_format, EcoString};
use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType};
use pdf_writer::writers::StreamShadingType;
use pdf_writer::{Filter, Finish, Name, Ref};
use typst::geom::{
Abs, Angle, Color, ColorSpace, ConicGradient, Gradient, Numeric, Point, Quadrant,
Ratio, Relative, Transform, WeightedColor,
use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
use typst::util::Numeric;
use typst::visualize::{
Color, ColorSpace, ConicGradient, Gradient, GradientRelative, WeightedColor,
};
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
@ -301,8 +302,8 @@ fn register_gradient(
transforms.size.y = Abs::pt(1.0);
}
let size = match gradient.unwrap_relative(on_text) {
Relative::Self_ => transforms.size,
Relative::Parent => transforms.container_size,
GradientRelative::Self_ => transforms.size,
GradientRelative::Parent => transforms.container_size,
};
let (offset_x, offset_y) = match gradient {
@ -316,8 +317,8 @@ fn register_gradient(
let rotation = gradient.angle().unwrap_or_else(Angle::zero);
let transform = match gradient.unwrap_relative(on_text) {
Relative::Self_ => transforms.transform,
Relative::Parent => transforms.container_transform,
GradientRelative::Self_ => transforms.transform,
GradientRelative::Parent => transforms.container_transform,
};
let scale_offset = match gradient {

View File

@ -3,9 +3,10 @@ use std::io::Cursor;
use image::{DynamicImage, GenericImageView, Rgba};
use pdf_writer::{Chunk, Filter, Finish, Ref};
use typst::geom::ColorSpace;
use typst::image::{Image, ImageKind, RasterFormat, RasterImage, SvgImage};
use typst::util::Deferred;
use typst::visualize::{
ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
};
use crate::{deflate, PdfContext};

View File

@ -16,13 +16,13 @@ use base64::Engine;
use ecow::{eco_format, EcoString};
use pdf_writer::types::Direction;
use pdf_writer::{Finish, Name, Pdf, Ref, TextStr};
use typst::doc::{Document, Lang};
use typst::eval::Datetime;
use typst::font::Font;
use typst::geom::{Abs, Dir, Em};
use typst::image::Image;
use typst::model::Introspector;
use typst::foundations::Datetime;
use typst::introspection::Introspector;
use typst::layout::{Abs, Dir, Em};
use typst::model::Document;
use typst::text::{Font, Lang};
use typst::util::Deferred;
use typst::visualize::Image;
use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter};
use crate::color::ColorSpaces;

View File

@ -1,9 +1,9 @@
use std::num::NonZeroUsize;
use pdf_writer::{Finish, Ref, TextStr};
use typst::eval::{item, Smart};
use typst::geom::Abs;
use typst::model::Content;
use typst::foundations::{Content, NativeElement, Smart};
use typst::layout::Abs;
use typst::model::HeadingElem;
use crate::{AbsExt, PdfContext};
@ -18,7 +18,7 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
// Therefore, its next descendant must be added at its level, which is
// enforced in the manner shown below.
let mut last_skipped_level = None;
for heading in ctx.introspector.query(&item!(heading_elem).select()).iter() {
for heading in ctx.introspector.query(&HeadingElem::elem().select()).iter() {
let leaf = HeadingNode::leaf((**heading).clone());
if leaf.bookmarked {

View File

@ -8,16 +8,17 @@ use pdf_writer::types::{
};
use pdf_writer::writers::PageLabel;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr};
use typst::doc::{
Destination, Frame, FrameItem, GroupItem, Meta, PdfPageLabel, PdfPageLabelStyle,
TextItem,
use typst::introspection::Meta;
use typst::layout::{
Abs, Em, Frame, FrameItem, GroupItem, PdfPageLabel, PdfPageLabelStyle, Point, Ratio,
Size, Transform,
};
use typst::font::Font;
use typst::geom::{
self, Abs, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint, Point,
Ratio, Shape, Size, Transform,
use typst::model::Destination;
use typst::text::{Font, TextItem};
use typst::util::Numeric;
use typst::visualize::{
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
};
use typst::image::Image;
use crate::color::PaintEncode;
use crate::extg::ExtGState;
@ -581,7 +582,7 @@ fn write_text(ctx: &mut PageContext, pos: Point, text: &TextItem) {
adjustment = Em::zero();
}
let cid = super::font::glyph_cid(&text.font, glyph.id);
let cid = crate::font::glyph_cid(&text.font, glyph.id);
encoded.push((cid >> 8) as u8);
encoded.push((cid & 0xff) as u8);
@ -656,16 +657,16 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
}
/// Encode a bezier path into the content stream.
fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) {
fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &Path) {
for elem in &path.0 {
match elem {
geom::PathItem::MoveTo(p) => {
PathItem::MoveTo(p) => {
ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
}
geom::PathItem::LineTo(p) => {
PathItem::LineTo(p) => {
ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
}
geom::PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
x + p1.x.to_f32(),
y + p1.y.to_f32(),
x + p2.x.to_f32(),
@ -673,7 +674,7 @@ fn write_path(ctx: &mut PageContext, x: f32, y: f32, path: &geom::Path) {
x + p3.x.to_f32(),
y + p3.y.to_f32(),
),
geom::PathItem::ClosePath => ctx.content.close_path(),
PathItem::ClosePath => ctx.content.close_path(),
};
}
}

View File

@ -9,13 +9,15 @@ use pixglyph::Bitmap;
use resvg::tiny_skia::IntRect;
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
use typst::doc::{Frame, FrameItem, FrameKind, GroupItem, Meta, TextItem};
use typst::font::Font;
use typst::geom::{
self, Abs, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
PathItem, Point, Ratio, Relative, Shape, Size, Transform,
use typst::introspection::Meta;
use typst::layout::{
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
};
use typst::text::{Font, TextItem};
use typst::visualize::{
Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageKind, LineCap,
LineJoin, Paint, Path, PathItem, RasterFormat, Shape,
};
use typst::image::{Image, ImageKind, RasterFormat};
use usvg::{NodeExt, TreeParsing};
/// Export a frame into a raster image.
@ -634,7 +636,7 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
}
/// Convert a Typst path into a tiny-skia path.
fn convert_path(path: &geom::Path) -> Option<sk::Path> {
fn convert_path(path: &Path) -> Option<sk::Path> {
let mut builder = sk::PathBuilder::new();
for elem in &path.0 {
match elem {
@ -773,13 +775,13 @@ impl<'a> GradientSampler<'a> {
) -> Self {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
Relative::Self_ => item_size,
Relative::Parent => state.size,
GradientRelative::Self_ => item_size,
GradientRelative::Parent => state.size,
};
let fill_transform = match relative {
Relative::Self_ => sk::Transform::identity(),
Relative::Parent => state.container_transform.invert().unwrap(),
GradientRelative::Self_ => sk::Transform::identity(),
GradientRelative::Parent => state.container_transform.invert().unwrap(),
};
Self {
@ -857,13 +859,13 @@ fn to_sk_paint<'a>(
Paint::Gradient(gradient) => {
let relative = gradient.unwrap_relative(on_text);
let container_size = match relative {
Relative::Self_ => item_size,
Relative::Parent => state.size,
GradientRelative::Self_ => item_size,
GradientRelative::Parent => state.size,
};
let fill_transform = match relative {
Relative::Self_ => fill_transform.unwrap_or_default(),
Relative::Parent => state
GradientRelative::Self_ => fill_transform.unwrap_or_default(),
GradientRelative::Parent => state
.container_transform
.post_concat(state.transform.invert().unwrap()),
};

View File

@ -6,16 +6,18 @@ use std::io::Read;
use base64::Engine;
use ecow::{eco_format, EcoString};
use ttf_parser::{GlyphId, OutlineBuilder};
use typst::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
use typst::eval::Repr;
use typst::font::Font;
use typst::geom::{
self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin,
Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size,
Transform,
use typst::foundations::Repr;
use typst::layout::{
Abs, Angle, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Quadrant, Ratio,
Size, Transform,
};
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use typst::text::{Font, TextItem};
use typst::util::hash128;
use typst::visualize::{
Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat,
LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape,
VectorFormat,
};
use xmlwriter::XmlWriter;
/// The number of segments in a conic gradient.
@ -432,8 +434,8 @@ impl SVGRenderer {
};
match gradient.unwrap_relative(true) {
Relative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
Relative::Parent => Transform::scale(
GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()),
GradientRelative::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
@ -488,11 +490,11 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
Relative::Self_ => Transform::scale(
GradientRelative::Self_ => Transform::scale(
Ratio::new(shape_size.x.to_pt()),
Ratio::new(shape_size.y.to_pt()),
),
Relative::Parent => Transform::scale(
GradientRelative::Parent => Transform::scale(
Ratio::new(state.size.x.to_pt()),
Ratio::new(state.size.y.to_pt()),
)
@ -517,8 +519,8 @@ impl SVGRenderer {
if let Paint::Gradient(gradient) = paint {
match gradient.unwrap_relative(false) {
Relative::Self_ => shape_size,
Relative::Parent => state.size,
GradientRelative::Self_ => shape_size,
GradientRelative::Parent => state.size,
}
} else {
shape_size
@ -1047,7 +1049,7 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
builder.0
}
fn convert_path(path: &geom::Path) -> EcoString {
fn convert_path(path: &Path) -> EcoString {
let mut builder = SvgPathBuilder::default();
for item in &path.0 {
match item {

View File

@ -11,7 +11,7 @@ use crate::{
/// ultimately reparsed.
///
/// The high-level API for this function is
/// [`Source::edit`](super::Source::edit).
/// [`Source::edit`](crate::Source::edit).
pub fn reparse(
root: &mut SyntaxNode,
text: &str,

View File

@ -7,7 +7,7 @@ use crate::FileId;
/// A unique identifier for a syntax node.
///
/// This is used throughout the compiler to track which source section an error
/// or element stems from. Can be [mapped back](super::Source::range) to a byte
/// or element stems from. Can be [mapped back](crate::Source::range) to a byte
/// range for user facing display.
///
/// During editing, the span values stay mostly stable, even for nodes behind an
@ -76,7 +76,7 @@ impl Span {
Some(FileId::from_raw(bits))
}
/// The unique number of the span within its [`Source`](super::Source).
/// The unique number of the span within its [`Source`](crate::Source).
pub const fn number(self) -> u64 {
self.0.get() & ((1 << Self::BITS) - 1)
}

View File

@ -18,14 +18,26 @@ bench = false
[dependencies]
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
az = { workspace = true }
bitflags = { workspace = true }
chinese-number = { workspace = true }
ciborium = { workspace = true }
comemo = { workspace = true }
csv = { workspace = true }
ecow = { workspace = true}
fontdb = { workspace = true }
hayagriva = { workspace = true }
hypher = { workspace = true }
icu_properties = { workspace = true }
icu_provider = { workspace = true }
icu_provider_adapters = { workspace = true }
icu_provider_blob = { workspace = true }
icu_segmenter = { workspace = true }
image = { workspace = true }
indexmap = { workspace = true }
kurbo = { workspace = true }
lasso = { workspace = true }
lipsum = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
palette = { workspace = true }
@ -34,13 +46,19 @@ regex = { workspace = true }
roxmltree = { workspace = true }
rustybuzz = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
siphasher = { workspace = true }
smallvec = { workspace = true }
syntect = { workspace = true }
time = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
ttf-parser = { workspace = true }
typed-arena = { workspace = true }
unicode-bidi = { workspace = true }
unicode-math-class = { workspace = true }
unicode-script = { workspace = true }
unicode-segmentation = { workspace = true }
usvg = { workspace = true }
wasmi = { workspace = true }

View File

@ -42,10 +42,16 @@ macro_rules! __bail {
}
#[doc(inline)]
pub use crate::{__bail as bail, __error as error, __warning as warning};
pub use crate::__bail as bail;
#[doc(inline)]
pub use crate::__error as error;
#[doc(inline)]
pub use crate::__warning as warning;
#[doc(hidden)]
pub use ecow::{eco_format, EcoString};
pub use ecow::eco_format;
#[doc(hidden)]
pub use ecow::EcoString;
/// Construct an [`EcoString`] or [`SourceDiagnostic`] with severity `Error`.
#[macro_export]
@ -159,6 +165,25 @@ impl From<SyntaxError> for SourceDiagnostic {
}
}
/// Holds delayed errors.
#[derive(Default, Clone)]
pub struct DelayedErrors(pub EcoVec<SourceDiagnostic>);
impl DelayedErrors {
/// Create an empty list of delayed errors.
pub fn new() -> Self {
Self::default()
}
}
#[comemo::track]
impl DelayedErrors {
/// Push a delayed error.
pub fn push(&mut self, error: SourceDiagnostic) {
self.0.push(error);
}
}
/// A part of a diagnostic's [trace](SourceDiagnostic::trace).
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Tracepoint {

View File

@ -0,0 +1,99 @@
use ecow::eco_format;
use crate::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
use crate::eval::{Eval, Vm};
use crate::foundations::{call_method_access, is_accessor_method, Dict, Value};
use crate::syntax::ast::{self, AstNode};
/// Access an expression mutably.
pub(crate) trait Access {
/// Access the value.
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value>;
}
impl Access for ast::Expr<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
match self {
Self::Ident(v) => v.access(vm),
Self::Parenthesized(v) => v.access(vm),
Self::FieldAccess(v) => v.access(vm),
Self::FuncCall(v) => v.access(vm),
_ => {
let _ = self.eval(vm)?;
bail!(self.span(), "cannot mutate a temporary value");
}
}
}
}
impl Access for ast::Ident<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
let span = self.span();
let value = vm.scopes.get_mut(&self).at(span)?;
if vm.inspected == Some(span) {
vm.vt.tracer.value(value.clone());
}
Ok(value)
}
}
impl Access for ast::Parenthesized<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
self.expr().access(vm)
}
}
impl Access for ast::FieldAccess<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
access_dict(vm, self)?.at_mut(self.field().get()).at(self.span())
}
}
impl Access for ast::FuncCall<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
if let ast::Expr::FieldAccess(access) = self.callee() {
let method = access.field();
if is_accessor_method(&method) {
let span = self.span();
let world = vm.world();
let args = self.args().eval(vm)?;
let value = access.target().access(vm)?;
let result = call_method_access(value, &method, args, span);
let point = || Tracepoint::Call(Some(method.get().clone()));
return result.trace(world, point, span);
}
}
let _ = self.eval(vm)?;
bail!(self.span(), "cannot mutate a temporary value");
}
}
pub(crate) fn access_dict<'a>(
vm: &'a mut Vm,
access: ast::FieldAccess,
) -> SourceResult<&'a mut Dict> {
match access.target().access(vm)? {
Value::Dict(dict) => Ok(dict),
value => {
let ty = value.ty();
let span = access.target().span();
if matches!(
value, // those types have their own field getters
Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_)
) {
bail!(span, "cannot mutate fields on {ty}");
} else if crate::foundations::fields_on(ty).is_empty() {
bail!(span, "{ty} does not have accessible fields");
} else {
// type supports static fields, which don't yet have
// setters
Err(eco_format!("fields on {ty} are not yet mutable"))
.hint(eco_format!(
"try creating a new {ty} with the updated field value instead"
))
.at(span)
}
}
}
}

View File

@ -0,0 +1,179 @@
use std::collections::HashSet;
use crate::diag::{bail, At, SourceResult};
use crate::eval::{Access, Eval, Vm};
use crate::foundations::{Array, Dict, Value};
use crate::syntax::ast::{self, AstNode};
impl Eval for ast::LetBinding<'_> {
type Output = Value;
#[tracing::instrument(name = "LetBinding::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = match self.init() {
Some(expr) => expr.eval(vm)?,
None => Value::None,
};
if vm.flow.is_some() {
return Ok(Value::None);
}
match self.kind() {
ast::LetBindingKind::Normal(pattern) => destructure(vm, pattern, value)?,
ast::LetBindingKind::Closure(ident) => vm.define(ident, value),
}
Ok(Value::None)
}
}
impl Eval for ast::DestructAssignment<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.value().eval(vm)?;
destructure_impl(vm, self.pattern(), value, |vm, expr, value| {
let location = expr.access(vm)?;
*location = value;
Ok(())
})?;
Ok(Value::None)
}
}
/// Destructures a value into a pattern.
pub(crate) fn destructure(
vm: &mut Vm,
pattern: ast::Pattern,
value: Value,
) -> SourceResult<()> {
destructure_impl(vm, pattern, value, |vm, expr, value| match expr {
ast::Expr::Ident(ident) => {
vm.define(ident, value);
Ok(())
}
_ => bail!(expr.span(), "nested patterns are currently not supported"),
})
}
/// Destruct the given value into the pattern and apply the function to each binding.
#[tracing::instrument(skip_all)]
fn destructure_impl<T>(
vm: &mut Vm,
pattern: ast::Pattern,
value: Value,
f: T,
) -> SourceResult<()>
where
T: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
{
match pattern {
ast::Pattern::Normal(expr) => {
f(vm, expr, value)?;
}
ast::Pattern::Placeholder(_) => {}
ast::Pattern::Destructuring(destruct) => match value {
Value::Array(value) => destructure_array(vm, pattern, value, f, destruct)?,
Value::Dict(value) => destructure_dict(vm, value, f, destruct)?,
_ => bail!(pattern.span(), "cannot destructure {}", value.ty()),
},
}
Ok(())
}
fn destructure_array<F>(
vm: &mut Vm,
pattern: ast::Pattern,
value: Array,
f: F,
destruct: ast::Destructuring,
) -> SourceResult<()>
where
F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
{
let mut i = 0;
let len = value.as_slice().len();
for p in destruct.bindings() {
match p {
ast::DestructuringKind::Normal(expr) => {
let Ok(v) = value.at(i as i64, None) else {
bail!(expr.span(), "not enough elements to destructure");
};
f(vm, expr, v)?;
i += 1;
}
ast::DestructuringKind::Sink(spread) => {
let sink_size = (1 + len).checked_sub(destruct.bindings().count());
let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s));
if let (Some(sink_size), Some(sink)) = (sink_size, sink) {
if let Some(expr) = spread.expr() {
f(vm, expr, Value::Array(sink.into()))?;
}
i += sink_size;
} else {
bail!(pattern.span(), "not enough elements to destructure")
}
}
ast::DestructuringKind::Named(named) => {
bail!(named.span(), "cannot destructure named elements from an array")
}
ast::DestructuringKind::Placeholder(underscore) => {
if i < len {
i += 1
} else {
bail!(underscore.span(), "not enough elements to destructure")
}
}
}
}
if i < len {
bail!(pattern.span(), "too many elements to destructure");
}
Ok(())
}
fn destructure_dict<F>(
vm: &mut Vm,
dict: Dict,
f: F,
destruct: ast::Destructuring,
) -> SourceResult<()>
where
F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>,
{
let mut sink = None;
let mut used = HashSet::new();
for p in destruct.bindings() {
match p {
ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => {
let v = dict.get(&ident).at(ident.span())?;
f(vm, ast::Expr::Ident(ident), v.clone())?;
used.insert(ident.as_str());
}
ast::DestructuringKind::Sink(spread) => sink = spread.expr(),
ast::DestructuringKind::Named(named) => {
let name = named.name();
let v = dict.get(&name).at(name.span())?;
f(vm, named.expr(), v.clone())?;
used.insert(name.as_str());
}
ast::DestructuringKind::Placeholder(_) => {}
ast::DestructuringKind::Normal(expr) => {
bail!(expr.span(), "expected key, found expression");
}
}
}
if let Some(expr) = sink {
let mut sink = Dict::new();
for (key, value) in dict {
if !used.contains(key.as_str()) {
sink.insert(key, value);
}
}
f(vm, expr, Value::Dict(sink))?;
}
Ok(())
}

View File

@ -0,0 +1,587 @@
use comemo::{Prehashed, Tracked, TrackedMut};
use ecow::EcoVec;
use crate::diag::{
bail, error, At, DelayedErrors, HintedStrResult, SourceResult, Trace, Tracepoint,
};
use crate::eval::{Access, Eval, FlowEvent, Route, Tracer, Vm};
use crate::foundations::{
call_method_mut, is_mutating_method, Arg, Args, Bytes, Closure, Content, Func,
IntoValue, NativeElement, Scope, Scopes, Value,
};
use crate::introspection::{Introspector, Locator};
use crate::layout::Vt;
use crate::math::{Accent, AccentElem, LrElem};
use crate::symbols::Symbol;
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{Spanned, SyntaxNode};
use crate::text::TextElem;
use crate::World;
/// The maxmium function call depth.
const MAX_CALL_DEPTH: usize = 64;
impl Eval for ast::FuncCall<'_> {
type Output = Value;
#[tracing::instrument(name = "FuncCall::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.span();
if vm.depth >= MAX_CALL_DEPTH {
bail!(span, "maximum function call depth exceeded");
}
let callee = self.callee();
let in_math = in_math(callee);
let callee_span = callee.span();
let args = self.args();
// Try to evaluate as a call to an associated function or field.
let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee {
let target = access.target();
let target_span = target.span();
let field = access.field();
let field_span = field.span();
let target = if is_mutating_method(&field) {
let mut args = args.eval(vm)?;
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)?;
// 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()
);
if let Value::Dict(dict) = target {
if matches!(dict.get(&field), Ok(Value::Func(_))) {
error.hint(
"to call the function stored in the dictionary, \
surround the field access with parentheses",
);
}
}
bail!(error);
}
} else {
(callee.eval(vm)?, args.eval(vm)?)
};
// Handle math special cases for non-functions:
// Combining accent symbols apply themselves while everything else
// simply displays the arguments verbatim.
if in_math && !matches!(callee, Value::Func(_)) {
if let Value::Symbol(sym) = &callee {
let c = sym.get();
if let Some(accent) = Symbol::combining_accent(c) {
let base = args.expect("base")?;
args.finish()?;
return Ok(Value::Content(
AccentElem::new(base, Accent::new(accent)).pack(),
));
}
}
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
body += TextElem::packed(',');
}
body += arg;
}
return Ok(Value::Content(
callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')'))
.pack(),
));
}
let callee = callee.cast::<Func>().at(callee_span)?;
let point = || Tracepoint::Call(callee.name().map(Into::into));
let f = || callee.call_vm(vm, args).trace(vm.world(), point, span);
// Stacker is broken on WASM.
#[cfg(target_arch = "wasm32")]
return f();
#[cfg(not(target_arch = "wasm32"))]
stacker::maybe_grow(32 * 1024, 2 * 1024 * 1024, f)
}
}
impl Eval for ast::Args<'_> {
type Output = Args;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let mut items = EcoVec::with_capacity(self.items().count());
for arg in self.items() {
let span = arg.span();
match arg {
ast::Arg::Pos(expr) => {
items.push(Arg {
span,
name: None,
value: Spanned::new(expr.eval(vm)?, expr.span()),
});
}
ast::Arg::Named(named) => {
items.push(Arg {
span,
name: Some(named.name().get().clone().into()),
value: Spanned::new(named.expr().eval(vm)?, named.expr().span()),
});
}
ast::Arg::Spread(expr) => match expr.eval(vm)? {
Value::None => {}
Value::Array(array) => {
items.extend(array.into_iter().map(|value| Arg {
span,
name: None,
value: Spanned::new(value, span),
}));
}
Value::Dict(dict) => {
items.extend(dict.into_iter().map(|(key, value)| Arg {
span,
name: Some(key),
value: Spanned::new(value, span),
}));
}
Value::Args(args) => items.extend(args.items),
v => bail!(expr.span(), "cannot spread {}", v.ty()),
},
}
}
Ok(Args { span: self.span(), items })
}
}
impl Eval for ast::Closure<'_> {
type Output = Value;
#[tracing::instrument(name = "Closure::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
// Evaluate default values of named parameters.
let mut defaults = Vec::new();
for param in self.params().children() {
if let ast::Param::Named(named) = param {
defaults.push(named.expr().eval(vm)?);
}
}
// Collect captured variables.
let captured = {
let mut visitor = CapturesVisitor::new(Some(&vm.scopes));
visitor.visit(self.to_untyped());
visitor.finish()
};
// Define the closure.
let closure = Closure {
node: self.to_untyped().clone(),
file: vm.file,
defaults,
captured,
};
Ok(Value::Func(Func::from(closure).spanned(self.params().span())))
}
}
/// Call the function in the context with the arguments.
#[comemo::memoize]
#[tracing::instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn call_closure(
func: &Func,
closure: &Prehashed<Closure>,
world: Tracked<dyn World + '_>,
route: Tracked<Route>,
introspector: Tracked<Introspector>,
locator: Tracked<Locator>,
delayed: TrackedMut<DelayedErrors>,
tracer: TrackedMut<Tracer>,
depth: usize,
mut args: Args,
) -> SourceResult<Value> {
let node = closure.node.cast::<ast::Closure>().unwrap();
// Don't leak the scopes from the call site. Instead, we use the scope
// of captured variables we collected earlier.
let mut scopes = Scopes::new(None);
scopes.top = closure.captured.clone();
// Prepare VT.
let mut locator = Locator::chained(locator);
let vt = Vt {
world,
introspector,
locator: &mut locator,
delayed,
tracer,
};
// Prepare VM.
let mut vm = Vm::new(vt, route, closure.file, scopes);
vm.depth = depth;
// Provide the closure itself for recursive calls.
if let Some(name) = node.name() {
vm.define(name, Value::Func(func.clone()));
}
// Parse the arguments according to the parameter list.
let num_pos_params = node
.params()
.children()
.filter(|p| matches!(p, ast::Param::Pos(_)))
.count();
let num_pos_args = args.to_pos().len();
let sink_size = num_pos_args.checked_sub(num_pos_params);
let mut sink = None;
let mut sink_pos_values = None;
let mut defaults = closure.defaults.iter();
for p in node.params().children() {
match p {
ast::Param::Pos(pattern) => match pattern {
ast::Pattern::Normal(ast::Expr::Ident(ident)) => {
vm.define(ident, args.expect::<Value>(&ident)?)
}
ast::Pattern::Normal(_) => unreachable!(),
pattern => {
crate::eval::destructure(
&mut vm,
pattern,
args.expect::<Value>("pattern parameter")?,
)?;
}
},
ast::Param::Sink(ident) => {
sink = ident.name();
if let Some(sink_size) = sink_size {
sink_pos_values = Some(args.consume(sink_size)?);
}
}
ast::Param::Named(named) => {
let name = named.name();
let default = defaults.next().unwrap();
let value =
args.named::<Value>(&name)?.unwrap_or_else(|| default.clone());
vm.define(name, value);
}
}
}
if let Some(sink) = sink {
let mut remaining_args = args.take();
if let Some(sink_pos_values) = sink_pos_values {
remaining_args.items.extend(sink_pos_values);
}
vm.define(sink, remaining_args);
}
// Ensure all arguments have been used.
args.finish()?;
// Handle control flow.
let output = node.body().eval(&mut vm)?;
match vm.flow {
Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit),
Some(FlowEvent::Return(_, None)) => {}
Some(flow) => bail!(flow.forbidden()),
None => {}
}
Ok(output)
}
fn in_math(expr: ast::Expr) -> bool {
match expr {
ast::Expr::MathIdent(_) => true,
ast::Expr::FieldAccess(access) => in_math(access.target()),
_ => false,
}
}
/// A visitor that determines which variables to capture for a closure.
pub struct CapturesVisitor<'a> {
external: Option<&'a Scopes<'a>>,
internal: Scopes<'a>,
captures: Scope,
}
impl<'a> CapturesVisitor<'a> {
/// Create a new visitor for the given external scopes.
pub fn new(external: Option<&'a Scopes<'a>>) -> Self {
Self {
external,
internal: Scopes::new(None),
captures: Scope::new(),
}
}
/// Return the scope of captured variables.
pub fn finish(self) -> Scope {
self.captures
}
/// Visit any node and collect all captured variables.
#[tracing::instrument(skip_all)]
pub fn visit(&mut self, node: &SyntaxNode) {
match node.cast() {
// Every identifier is a potential variable that we need to capture.
// Identifiers that shouldn't count as captures because they
// actually bind a new name are handled below (individually through
// the expressions that contain them).
Some(ast::Expr::Ident(ident)) => self.capture(&ident, Scopes::get),
Some(ast::Expr::MathIdent(ident)) => {
self.capture(&ident, Scopes::get_in_math)
}
// Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
self.internal.enter();
for child in node.children() {
self.visit(child);
}
self.internal.exit();
}
// Don't capture the field of a field access.
Some(ast::Expr::FieldAccess(access)) => {
self.visit(access.target().to_untyped());
}
// A closure contains parameter bindings, which are bound before the
// body is evaluated. Care must be taken so that the default values
// of named parameters cannot access previous parameter bindings.
Some(ast::Expr::Closure(expr)) => {
for param in expr.params().children() {
if let ast::Param::Named(named) = param {
self.visit(named.expr().to_untyped());
}
}
self.internal.enter();
if let Some(name) = expr.name() {
self.bind(name);
}
for param in expr.params().children() {
match param {
ast::Param::Pos(pattern) => {
for ident in pattern.idents() {
self.bind(ident);
}
}
ast::Param::Named(named) => self.bind(named.name()),
ast::Param::Sink(spread) => {
self.bind(spread.name().unwrap_or_default())
}
}
}
self.visit(expr.body().to_untyped());
self.internal.exit();
}
// A let expression contains a binding, but that binding is only
// active after the body is evaluated.
Some(ast::Expr::Let(expr)) => {
if let Some(init) = expr.init() {
self.visit(init.to_untyped());
}
for ident in expr.kind().idents() {
self.bind(ident);
}
}
// A for loop contains one or two bindings in its pattern. These are
// active after the iterable is evaluated but before the body is
// evaluated.
Some(ast::Expr::For(expr)) => {
self.visit(expr.iter().to_untyped());
self.internal.enter();
let pattern = expr.pattern();
for ident in pattern.idents() {
self.bind(ident);
}
self.visit(expr.body().to_untyped());
self.internal.exit();
}
// An import contains items, but these are active only after the
// path is evaluated.
Some(ast::Expr::Import(expr)) => {
self.visit(expr.source().to_untyped());
if let Some(ast::Imports::Items(items)) = expr.imports() {
for item in items.iter() {
self.bind(item.bound_name());
}
}
}
_ => {
// Never capture the name part of a named pair.
if let Some(named) = node.cast::<ast::Named>() {
self.visit(named.expr().to_untyped());
return;
}
// Everything else is traversed from left to right.
for child in node.children() {
self.visit(child);
}
}
}
}
/// Bind a new internal variable.
fn bind(&mut self, ident: ast::Ident) {
self.internal.top.define(ident.get().clone(), Value::None);
}
/// Capture a variable if it isn't internal.
#[inline]
fn capture(
&mut self,
ident: &str,
getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>,
) {
if self.internal.get(ident).is_err() {
let Some(value) = self
.external
.map(|external| getter(external, ident).ok())
.unwrap_or(Some(&Value::None))
else {
return;
};
self.captures.define_captured(ident, value.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::parse;
#[track_caller]
fn test(text: &str, result: &[&str]) {
let mut scopes = Scopes::new(None);
scopes.top.define("f", 0);
scopes.top.define("x", 0);
scopes.top.define("y", 0);
scopes.top.define("z", 0);
let mut visitor = CapturesVisitor::new(Some(&scopes));
let root = parse(text);
visitor.visit(&root);
let captures = visitor.finish();
let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect();
names.sort();
assert_eq!(names, result);
}
#[test]
fn test_captures() {
// Let binding and function definition.
test("#let x = x", &["x"]);
test("#let x; #(x + y)", &["y"]);
test("#let f(x, y) = x + y", &[]);
test("#let f(x, y) = f", &[]);
test("#let f = (x, y) => f", &["f"]);
// Closure with different kinds of params.
test("#((x, y) => x + z)", &["z"]);
test("#((x: y, z) => x + z)", &["y"]);
test("#((..x) => x + y)", &["y"]);
test("#((x, y: x + z) => x + y)", &["x", "z"]);
test("#{x => x; x}", &["x"]);
// Show rule.
test("#show y: x => x", &["y"]);
test("#show y: x => x + z", &["y", "z"]);
test("#show x: x => x", &["x"]);
// For loop.
test("#for x in y { x + z }", &["y", "z"]);
test("#for (x, y) in y { x + y }", &["y"]);
test("#for x in y {} #x", &["x", "y"]);
// Import.
test("#import z: x, y", &["z"]);
test("#import x + y: x, y, z", &["x", "y"]);
// Blocks.
test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
test("#[#let x = 1]#x", &["x"]);
// Field access.
test("#foo(body: 1)", &[]);
test("#(body: 1)", &[]);
test("#(body = 1)", &[]);
test("#(body += y)", &["y"]);
test("#{ (body, a) = (y, 1) }", &["y"]);
test("#(x.at(y) = 5)", &["x", "y"])
}
}

View File

@ -0,0 +1,317 @@
use ecow::{eco_vec, EcoVec};
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::eval::{ops, Eval, Vm};
use crate::foundations::{Array, Content, Dict, Str, Value};
use crate::syntax::ast::{self, AstNode};
impl Eval for ast::Code<'_> {
type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
eval_code(vm, &mut self.exprs())
}
}
/// Evaluate a stream of expressions.
fn eval_code<'a>(
vm: &mut Vm,
exprs: &mut impl Iterator<Item = ast::Expr<'a>>,
) -> SourceResult<Value> {
let flow = vm.flow.take();
let mut output = Value::None;
while let Some(expr) = exprs.next() {
let span = expr.span();
let value = match expr {
ast::Expr::Set(set) => {
let styles = set.eval(vm)?;
if vm.flow.is_some() {
break;
}
let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_map(styles))
}
ast::Expr::Show(show) => {
let recipe = show.eval(vm)?;
if vm.flow.is_some() {
break;
}
let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_recipe(vm, recipe)?)
}
_ => expr.eval(vm)?,
};
output = ops::join(output, value).at(span)?;
if vm.flow.is_some() {
break;
}
}
if flow.is_some() {
vm.flow = flow;
}
Ok(output)
}
impl Eval for ast::Expr<'_> {
type Output = Value;
#[tracing::instrument(name = "Expr::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.span();
let forbidden = |name| {
error!(span, "{} is only allowed directly in code and content blocks", name)
};
let v = match self {
Self::Text(v) => v.eval(vm).map(Value::Content),
Self::Space(v) => v.eval(vm).map(Value::Content),
Self::Linebreak(v) => v.eval(vm).map(Value::Content),
Self::Parbreak(v) => v.eval(vm).map(Value::Content),
Self::Escape(v) => v.eval(vm),
Self::Shorthand(v) => v.eval(vm),
Self::SmartQuote(v) => v.eval(vm).map(Value::Content),
Self::Strong(v) => v.eval(vm).map(Value::Content),
Self::Emph(v) => v.eval(vm).map(Value::Content),
Self::Raw(v) => v.eval(vm).map(Value::Content),
Self::Link(v) => v.eval(vm).map(Value::Content),
Self::Label(v) => v.eval(vm),
Self::Ref(v) => v.eval(vm).map(Value::Content),
Self::Heading(v) => v.eval(vm).map(Value::Content),
Self::List(v) => v.eval(vm).map(Value::Content),
Self::Enum(v) => v.eval(vm).map(Value::Content),
Self::Term(v) => v.eval(vm).map(Value::Content),
Self::Equation(v) => v.eval(vm).map(Value::Content),
Self::Math(v) => v.eval(vm).map(Value::Content),
Self::MathIdent(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
Self::MathDelimited(v) => v.eval(vm).map(Value::Content),
Self::MathAttach(v) => v.eval(vm).map(Value::Content),
Self::MathPrimes(v) => v.eval(vm).map(Value::Content),
Self::MathFrac(v) => v.eval(vm).map(Value::Content),
Self::MathRoot(v) => v.eval(vm).map(Value::Content),
Self::Ident(v) => v.eval(vm),
Self::None(v) => v.eval(vm),
Self::Auto(v) => v.eval(vm),
Self::Bool(v) => v.eval(vm),
Self::Int(v) => v.eval(vm),
Self::Float(v) => v.eval(vm),
Self::Numeric(v) => v.eval(vm),
Self::Str(v) => v.eval(vm),
Self::Code(v) => v.eval(vm),
Self::Content(v) => v.eval(vm).map(Value::Content),
Self::Array(v) => v.eval(vm).map(Value::Array),
Self::Dict(v) => v.eval(vm).map(Value::Dict),
Self::Parenthesized(v) => v.eval(vm),
Self::FieldAccess(v) => v.eval(vm),
Self::FuncCall(v) => v.eval(vm),
Self::Closure(v) => v.eval(vm),
Self::Unary(v) => v.eval(vm),
Self::Binary(v) => v.eval(vm),
Self::Let(v) => v.eval(vm),
Self::DestructAssign(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")),
Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm),
Self::For(v) => v.eval(vm),
Self::Import(v) => v.eval(vm),
Self::Include(v) => v.eval(vm).map(Value::Content),
Self::Break(v) => v.eval(vm),
Self::Continue(v) => v.eval(vm),
Self::Return(v) => v.eval(vm),
}?
.spanned(span);
if vm.inspected == Some(span) {
vm.vt.tracer.value(v.clone());
}
Ok(v)
}
}
impl Eval for ast::Ident<'_> {
type Output = Value;
#[tracing::instrument(name = "Ident::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.get(&self).cloned().at(self.span())
}
}
impl Eval for ast::None<'_> {
type Output = Value;
#[tracing::instrument(name = "None::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::None)
}
}
impl Eval for ast::Auto<'_> {
type Output = Value;
#[tracing::instrument(name = "Auto::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Auto)
}
}
impl Eval for ast::Bool<'_> {
type Output = Value;
#[tracing::instrument(name = "Bool::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Bool(self.get()))
}
}
impl Eval for ast::Int<'_> {
type Output = Value;
#[tracing::instrument(name = "Int::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Int(self.get()))
}
}
impl Eval for ast::Float<'_> {
type Output = Value;
#[tracing::instrument(name = "Float::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Float(self.get()))
}
}
impl Eval for ast::Numeric<'_> {
type Output = Value;
#[tracing::instrument(name = "Numeric::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::numeric(self.get()))
}
}
impl Eval for ast::Str<'_> {
type Output = Value;
#[tracing::instrument(name = "Str::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Str(self.get().into()))
}
}
impl Eval for ast::Array<'_> {
type Output = Array;
#[tracing::instrument(skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let items = self.items();
let mut vec = EcoVec::with_capacity(items.size_hint().0);
for item in items {
match item {
ast::ArrayItem::Pos(expr) => vec.push(expr.eval(vm)?),
ast::ArrayItem::Spread(expr) => match expr.eval(vm)? {
Value::None => {}
Value::Array(array) => vec.extend(array.into_iter()),
v => bail!(expr.span(), "cannot spread {} into array", v.ty()),
},
}
}
Ok(vec.into())
}
}
impl Eval for ast::Dict<'_> {
type Output = Dict;
#[tracing::instrument(skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let mut map = indexmap::IndexMap::new();
let mut invalid_keys = eco_vec![];
for item in self.items() {
match item {
ast::DictItem::Named(named) => {
map.insert(named.name().get().clone().into(), named.expr().eval(vm)?);
}
ast::DictItem::Keyed(keyed) => {
let raw_key = keyed.key();
let key = raw_key.eval(vm)?;
let key = key.cast::<Str>().unwrap_or_else(|error| {
let error = SourceDiagnostic::error(raw_key.span(), error);
invalid_keys.push(error);
Str::default()
});
map.insert(key, keyed.expr().eval(vm)?);
}
ast::DictItem::Spread(expr) => match expr.eval(vm)? {
Value::None => {}
Value::Dict(dict) => map.extend(dict.into_iter()),
v => bail!(expr.span(), "cannot spread {} into dictionary", v.ty()),
},
}
}
if !invalid_keys.is_empty() {
return Err(invalid_keys);
}
Ok(map.into())
}
}
impl Eval for ast::CodeBlock<'_> {
type Output = Value;
#[tracing::instrument(name = "CodeBlock::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.enter();
let output = self.body().eval(vm)?;
vm.scopes.exit();
Ok(output)
}
}
impl Eval for ast::ContentBlock<'_> {
type Output = Content;
#[tracing::instrument(name = "ContentBlock::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.enter();
let content = self.body().eval(vm)?;
vm.scopes.exit();
Ok(content)
}
}
impl Eval for ast::Parenthesized<'_> {
type Output = Value;
#[tracing::instrument(name = "Parenthesized::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
self.expr().eval(vm)
}
}
impl Eval for ast::FieldAccess<'_> {
type Output = Value;
#[tracing::instrument(name = "FieldAccess::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.target().eval(vm)?;
let field = self.field();
value.field(&field).at(field.span())
}
}

View File

@ -0,0 +1,227 @@
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
use crate::eval::{destructure, ops, Eval, Vm};
use crate::foundations::{IntoValue, Value};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{Span, SyntaxKind, SyntaxNode};
/// The maximum number of loop iterations.
const MAX_ITERATIONS: usize = 10_000;
/// A control flow event that occurred during evaluation.
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum FlowEvent {
/// Stop iteration in a loop.
Break(Span),
/// Skip the remainder of the current iteration in a loop.
Continue(Span),
/// Stop execution of a function early, optionally returning an explicit
/// value.
Return(Span, Option<Value>),
}
impl FlowEvent {
/// Return an error stating that this control flow is forbidden.
pub fn forbidden(&self) -> SourceDiagnostic {
match *self {
Self::Break(span) => {
error!(span, "cannot break outside of loop")
}
Self::Continue(span) => {
error!(span, "cannot continue outside of loop")
}
Self::Return(span, _) => {
error!(span, "cannot return outside of function")
}
}
}
}
impl Eval for ast::Conditional<'_> {
type Output = Value;
#[tracing::instrument(name = "Conditional::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let condition = self.condition();
if condition.eval(vm)?.cast::<bool>().at(condition.span())? {
self.if_body().eval(vm)
} else if let Some(else_body) = self.else_body() {
else_body.eval(vm)
} else {
Ok(Value::None)
}
}
}
impl Eval for ast::WhileLoop<'_> {
type Output = Value;
#[tracing::instrument(name = "WhileLoop::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let flow = vm.flow.take();
let mut output = Value::None;
let mut i = 0;
let condition = self.condition();
let body = self.body();
while condition.eval(vm)?.cast::<bool>().at(condition.span())? {
if i == 0
&& is_invariant(condition.to_untyped())
&& !can_diverge(body.to_untyped())
{
bail!(condition.span(), "condition is always true");
} else if i >= MAX_ITERATIONS {
bail!(self.span(), "loop seems to be infinite");
}
let value = body.eval(vm)?;
output = ops::join(output, value).at(body.span())?;
match vm.flow {
Some(FlowEvent::Break(_)) => {
vm.flow = None;
break;
}
Some(FlowEvent::Continue(_)) => vm.flow = None,
Some(FlowEvent::Return(..)) => break,
None => {}
}
i += 1;
}
if flow.is_some() {
vm.flow = flow;
}
Ok(output)
}
}
impl Eval for ast::ForLoop<'_> {
type Output = Value;
#[tracing::instrument(name = "ForLoop::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let flow = vm.flow.take();
let mut output = Value::None;
macro_rules! iter {
(for $pat:ident in $iter:expr) => {{
vm.scopes.enter();
#[allow(unused_parens)]
for value in $iter {
destructure(vm, $pat, value.into_value())?;
let body = self.body();
let value = body.eval(vm)?;
output = ops::join(output, value).at(body.span())?;
match vm.flow {
Some(FlowEvent::Break(_)) => {
vm.flow = None;
break;
}
Some(FlowEvent::Continue(_)) => vm.flow = None,
Some(FlowEvent::Return(..)) => break,
None => {}
}
}
vm.scopes.exit();
}};
}
let iter = self.iter().eval(vm)?;
let pattern = self.pattern();
match (&pattern, iter.clone()) {
(ast::Pattern::Normal(_), Value::Str(string)) => {
// Iterate over graphemes of string.
iter!(for pattern in string.as_str().graphemes(true));
}
(_, Value::Dict(dict)) => {
// Iterate over pairs of dict.
iter!(for pattern in dict.pairs());
}
(_, Value::Array(array)) => {
// Iterate over values of array.
iter!(for pattern in array);
}
(ast::Pattern::Normal(_), _) => {
bail!(self.iter().span(), "cannot loop over {}", iter.ty());
}
(_, _) => {
bail!(pattern.span(), "cannot destructure values of {}", iter.ty())
}
}
if flow.is_some() {
vm.flow = flow;
}
Ok(output)
}
}
impl Eval for ast::LoopBreak<'_> {
type Output = Value;
#[tracing::instrument(name = "LoopBreak::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
if vm.flow.is_none() {
vm.flow = Some(FlowEvent::Break(self.span()));
}
Ok(Value::None)
}
}
impl Eval for ast::LoopContinue<'_> {
type Output = Value;
#[tracing::instrument(name = "LoopContinue::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
if vm.flow.is_none() {
vm.flow = Some(FlowEvent::Continue(self.span()));
}
Ok(Value::None)
}
}
impl Eval for ast::FuncReturn<'_> {
type Output = Value;
#[tracing::instrument(name = "FuncReturn::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.body().map(|body| body.eval(vm)).transpose()?;
if vm.flow.is_none() {
vm.flow = Some(FlowEvent::Return(self.span(), value));
}
Ok(Value::None)
}
}
/// Whether the expression always evaluates to the same value.
fn is_invariant(expr: &SyntaxNode) -> bool {
match expr.cast() {
Some(ast::Expr::Ident(_)) => false,
Some(ast::Expr::MathIdent(_)) => false,
Some(ast::Expr::FieldAccess(access)) => {
is_invariant(access.target().to_untyped())
}
Some(ast::Expr::FuncCall(call)) => {
is_invariant(call.callee().to_untyped())
&& is_invariant(call.args().to_untyped())
}
_ => expr.children().all(is_invariant),
}
}
/// Whether the expression contains a break or return.
fn can_diverge(expr: &SyntaxNode) -> bool {
matches!(expr.kind(), SyntaxKind::Break | SyntaxKind::Return)
|| expr.children().any(can_diverge)
}

View File

@ -0,0 +1,227 @@
use comemo::TrackedMut;
use ecow::{eco_format, eco_vec, EcoString};
use serde::{Deserialize, Serialize};
use crate::diag::{
bail, error, warning, At, FileError, SourceResult, StrResult, Trace, Tracepoint,
};
use crate::eval::{eval, Eval, Vm};
use crate::foundations::{Content, Module, Value};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{FileId, PackageSpec, PackageVersion, Span, VirtualPath};
use crate::World;
impl Eval for ast::ModuleImport<'_> {
type Output = Value;
#[tracing::instrument(name = "ModuleImport::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let source = self.source();
let source_span = source.span();
let mut source = source.eval(vm)?;
let new_name = self.new_name();
let imports = self.imports();
match &source {
Value::Func(func) => {
if func.scope().is_none() {
bail!(source_span, "cannot import from user-defined functions");
}
}
Value::Type(_) => {}
other => {
source = Value::Module(import(vm, other.clone(), source_span, true)?);
}
}
if let Some(new_name) = &new_name {
if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() {
// Warn on `import x as x`
vm.vt.tracer.warn(warning!(
new_name.span(),
"unnecessary import rename to same name",
));
}
}
// Define renamed module on the scope.
vm.scopes.top.define(new_name.as_str(), source.clone());
}
let scope = source.scope().unwrap();
match imports {
None => {
// Only import here if there is no rename.
if new_name.is_none() {
let name: EcoString = source.name().unwrap().into();
vm.scopes.top.define(name, source);
}
}
Some(ast::Imports::Wildcard) => {
for (var, value) in scope.iter() {
vm.scopes.top.define(var.clone(), value.clone());
}
}
Some(ast::Imports::Items(items)) => {
let mut errors = eco_vec![];
for item in items.iter() {
let original_ident = item.original_name();
if let Some(value) = scope.get(&original_ident) {
// Warn on `import ...: x as x`
if let ast::ImportItem::Renamed(renamed_item) = &item {
if renamed_item.original_name().as_str()
== renamed_item.new_name().as_str()
{
vm.vt.tracer.warn(warning!(
renamed_item.new_name().span(),
"unnecessary import rename to same name",
));
}
}
vm.define(item.bound_name(), value.clone());
} else {
errors.push(error!(original_ident.span(), "unresolved import"));
}
}
if !errors.is_empty() {
return Err(errors);
}
}
}
Ok(Value::None)
}
}
impl Eval for ast::ModuleInclude<'_> {
type Output = Content;
#[tracing::instrument(name = "ModuleInclude::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let span = self.source().span();
let source = self.source().eval(vm)?;
let module = import(vm, source, span, false)?;
Ok(module.content())
}
}
/// Process an import of a module relative to the current location.
pub fn import(
vm: &mut Vm,
source: Value,
span: Span,
allow_scopes: bool,
) -> SourceResult<Module> {
let path = match source {
Value::Str(path) => path,
Value::Module(module) => return Ok(module),
v if allow_scopes => {
bail!(span, "expected path, module, function, or type, found {}", v.ty())
}
v => bail!(span, "expected path or module, found {}", v.ty()),
};
// Handle package and file imports.
let path = path.as_str();
if path.starts_with('@') {
let spec = path.parse::<PackageSpec>().at(span)?;
import_package(vm, spec, span)
} else {
import_file(vm, path, span)
}
}
/// Import an external package.
fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> {
// Evaluate the manifest.
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
let bytes = vm.world().file(manifest_id).at(span)?;
let manifest = PackageManifest::parse(&bytes).at(span)?;
manifest.validate(&spec).at(span)?;
// Evaluate the entry point.
let entrypoint_id = manifest_id.join(&manifest.package.entrypoint);
let source = vm.world().source(entrypoint_id).at(span)?;
let point = || Tracepoint::Import;
Ok(eval(vm.world(), vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source)
.trace(vm.world(), point, span)?
.with_name(manifest.package.name))
}
/// Import a file from a path.
fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
// Load the source file.
let world = vm.world();
let id = vm.resolve_path(path).at(span)?;
let source = world.source(id).at(span)?;
// Prevent cyclic importing.
if vm.route.contains(source.id()) {
bail!(span, "cyclic import");
}
// Evaluate the file.
let point = || Tracepoint::Import;
eval(world, vm.route, TrackedMut::reborrow_mut(&mut vm.vt.tracer), &source)
.trace(world, point, span)
}
/// A parsed package manifest.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
struct PackageManifest {
/// Details about the package itself.
package: PackageInfo,
}
/// The `package` key in the manifest.
///
/// More fields are specified, but they are not relevant to the compiler.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
struct PackageInfo {
/// The name of the package within its namespace.
name: EcoString,
/// The package's version.
version: PackageVersion,
/// The path of the entrypoint into the package.
entrypoint: EcoString,
/// The minimum required compiler version for the package.
compiler: Option<PackageVersion>,
}
impl PackageManifest {
/// Parse the manifest from raw bytes.
fn parse(bytes: &[u8]) -> StrResult<Self> {
let string = std::str::from_utf8(bytes).map_err(FileError::from)?;
toml::from_str(string).map_err(|err| {
eco_format!("package manifest is malformed: {}", err.message())
})
}
/// Ensure that this manifest is indeed for the specified package.
fn validate(&self, spec: &PackageSpec) -> StrResult<()> {
if self.package.name != spec.name {
bail!("package manifest contains mismatched name `{}`", self.package.name);
}
if self.package.version != spec.version {
bail!(
"package manifest contains mismatched version {}",
self.package.version
);
}
if let Some(compiler) = self.package.compiler {
let current = PackageVersion::compiler();
if current < compiler {
bail!(
"package requires typst {compiler} or newer \
(current version is {current})"
);
}
}
Ok(())
}
}

View File

@ -1,179 +0,0 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use comemo::Tracked;
use ecow::EcoString;
use std::sync::OnceLock;
use crate::diag::SourceResult;
use crate::doc::Document;
use crate::eval::Module;
use crate::geom::{Abs, Dir};
use crate::model::{Content, Element, Introspector, Label, StyleChain, Styles, Vt};
use crate::util::hash128;
/// Definition of Typst's standard library.
#[derive(Debug, Clone, Hash)]
pub struct Library {
/// The scope containing definitions that are available everywhere.
pub global: Module,
/// The scope containing definitions available in math mode.
pub math: Module,
/// The default properties for page size, font selection and so on.
pub styles: Styles,
/// Defines which standard library items fulfill which syntactical roles.
pub items: LangItems,
}
/// Definition of library items the language is aware of.
#[derive(Clone)]
pub struct LangItems {
/// The root layout function.
pub layout:
fn(vt: &mut Vt, content: &Content, styles: StyleChain) -> SourceResult<Document>,
/// Access the em size.
pub em: fn(StyleChain) -> Abs,
/// Access the text direction.
pub dir: fn(StyleChain) -> Dir,
/// Whitespace.
pub space: fn() -> Content,
/// A forced line break: `\`.
pub linebreak: fn() -> Content,
/// Plain text without markup.
pub text: fn(text: EcoString) -> Content,
/// The text element.
pub text_elem: Element,
/// Get the string if this is a text element.
pub text_str: fn(&Content) -> Option<&EcoString>,
/// A smart quote: `'` or `"`.
pub smart_quote: fn(double: bool) -> Content,
/// A paragraph break.
pub parbreak: fn() -> Content,
/// Strong content: `*Strong*`.
pub strong: fn(body: Content) -> Content,
/// Emphasized content: `_Emphasized_`.
pub emph: fn(body: Content) -> Content,
/// Raw text with optional syntax highlighting: `` `...` ``.
pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content,
/// The language names and tags supported by raw text.
pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>,
/// A hyperlink: `https://typst.org`.
pub link: fn(url: EcoString) -> Content,
/// A reference: `@target`, `@target[..]`.
pub reference: fn(target: Label, supplement: Option<Content>) -> Content,
/// The keys contained in the bibliography and short descriptions of them.
#[allow(clippy::type_complexity)]
pub bibliography_keys:
fn(introspector: Tracked<Introspector>) -> Vec<(EcoString, Option<EcoString>)>,
/// A section heading: `= Introduction`.
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
/// The heading element.
pub heading_elem: Element,
/// An item in a bullet list: `- ...`.
pub list_item: fn(body: Content) -> Content,
/// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
pub enum_item: fn(number: Option<usize>, body: Content) -> Content,
/// An item in a term list: `/ Term: Details`.
pub term_item: fn(term: Content, description: Content) -> Content,
/// A mathematical equation: `$x$`, `$ x^2 $`.
pub equation: fn(body: Content, block: bool) -> Content,
/// An alignment point in math: `&`.
pub math_align_point: fn() -> Content,
/// Matched delimiters in math: `[x + y]`.
pub math_delimited: fn(open: Content, body: Content, close: Content) -> Content,
/// A base with optional attachments in math: `a_1^2`.
#[allow(clippy::type_complexity)]
pub math_attach: fn(
base: Content,
// Positioned smartly.
t: Option<Content>,
b: Option<Content>,
// Fixed positions.
tl: Option<Content>,
bl: Option<Content>,
tr: Option<Content>,
br: Option<Content>,
) -> Content,
/// Grouped primes: `a'''`.
pub math_primes: fn(count: usize) -> Content,
/// A base with an accent: `arrow(x)`.
pub math_accent: fn(base: Content, accent: char) -> Content,
/// A fraction in math: `x/2`.
pub math_frac: fn(num: Content, denom: Content) -> Content,
/// A root in math: `√x`, `∛x` or `∜x`.
pub math_root: fn(index: Option<Content>, radicand: Content) -> Content,
}
impl Debug for LangItems {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("LangItems { .. }")
}
}
impl Hash for LangItems {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.layout as usize).hash(state);
(self.em as usize).hash(state);
(self.dir as usize).hash(state);
self.space.hash(state);
self.linebreak.hash(state);
self.text.hash(state);
self.text_elem.hash(state);
(self.text_str as usize).hash(state);
self.smart_quote.hash(state);
self.parbreak.hash(state);
self.strong.hash(state);
self.emph.hash(state);
self.raw.hash(state);
self.raw_languages.hash(state);
self.link.hash(state);
self.reference.hash(state);
(self.bibliography_keys as usize).hash(state);
self.heading.hash(state);
self.heading_elem.hash(state);
self.list_item.hash(state);
self.enum_item.hash(state);
self.term_item.hash(state);
self.equation.hash(state);
self.math_align_point.hash(state);
self.math_delimited.hash(state);
self.math_attach.hash(state);
self.math_accent.hash(state);
self.math_frac.hash(state);
self.math_root.hash(state);
}
}
/// Global storage for lang items.
#[doc(hidden)]
pub static LANG_ITEMS: OnceLock<LangItems> = OnceLock::new();
/// Set the lang items.
///
/// This is a hack :(
///
/// Passing the lang items everywhere they are needed (especially text related
/// things) is very painful. By storing them globally, in theory, we break
/// incremental, but only when different sets of lang items are used in the same
/// program. For this reason, if this function is called multiple times, the
/// items must be the same (and this is enforced).
pub fn set_lang_items(items: LangItems) {
if let Err(items) = LANG_ITEMS.set(items) {
let first = hash128(LANG_ITEMS.get().unwrap());
let second = hash128(&items);
assert_eq!(first, second, "set differing lang items");
}
}
/// Access a lang item.
#[macro_export]
#[doc(hidden)]
macro_rules! __item {
($name:ident) => {
$crate::eval::LANG_ITEMS.get().unwrap().$name
};
}
#[doc(inline)]
pub use crate::__item as item;

View File

@ -0,0 +1,272 @@
use crate::diag::{warning, SourceResult};
use crate::eval::{Eval, Vm};
use crate::foundations::{Content, Label, NativeElement, Smart, Unlabellable, Value};
use crate::math::EquationElem;
use crate::model::{
EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem,
StrongElem, Supplement, TermItem,
};
use crate::symbols::Symbol;
use crate::syntax::ast::{self, AstNode};
use crate::text::{LinebreakElem, RawElem, SmartQuoteElem, SpaceElem, TextElem};
impl Eval for ast::Markup<'_> {
type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
eval_markup(vm, &mut self.exprs())
}
}
/// Evaluate a stream of markup.
fn eval_markup<'a>(
vm: &mut Vm,
exprs: &mut impl Iterator<Item = ast::Expr<'a>>,
) -> SourceResult<Content> {
let flow = vm.flow.take();
let mut seq = Vec::with_capacity(exprs.size_hint().1.unwrap_or_default());
while let Some(expr) = exprs.next() {
match expr {
ast::Expr::Set(set) => {
let styles = set.eval(vm)?;
if vm.flow.is_some() {
break;
}
seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
}
ast::Expr::Show(show) => {
let recipe = show.eval(vm)?;
if vm.flow.is_some() {
break;
}
let tail = eval_markup(vm, exprs)?;
seq.push(tail.styled_with_recipe(vm, recipe)?)
}
expr => match expr.eval(vm)? {
Value::Label(label) => {
if let Some(elem) =
seq.iter_mut().rev().find(|node| !node.can::<dyn Unlabellable>())
{
*elem = std::mem::take(elem).labelled(label);
}
}
value => seq.push(value.display().spanned(expr.span())),
},
}
if vm.flow.is_some() {
break;
}
}
if flow.is_some() {
vm.flow = flow;
}
Ok(Content::sequence(seq))
}
impl Eval for ast::Text<'_> {
type Output = Content;
#[tracing::instrument(name = "Text::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(TextElem::packed(self.get().clone()))
}
}
impl Eval for ast::Space<'_> {
type Output = Content;
#[tracing::instrument(name = "Space::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(SpaceElem::new().pack())
}
}
impl Eval for ast::Linebreak<'_> {
type Output = Content;
#[tracing::instrument(name = "Linebreak::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(LinebreakElem::new().pack())
}
}
impl Eval for ast::Parbreak<'_> {
type Output = Content;
#[tracing::instrument(name = "Parbreak::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(ParbreakElem::new().pack())
}
}
impl Eval for ast::Escape<'_> {
type Output = Value;
#[tracing::instrument(name = "Escape::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get())))
}
}
impl Eval for ast::Shorthand<'_> {
type Output = Value;
#[tracing::instrument(name = "Shorthand::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get())))
}
}
impl Eval for ast::SmartQuote<'_> {
type Output = Content;
#[tracing::instrument(name = "SmartQuote::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(SmartQuoteElem::new().with_double(self.double()).pack())
}
}
impl Eval for ast::Strong<'_> {
type Output = Content;
#[tracing::instrument(name = "Strong::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let body = self.body();
if body.exprs().next().is_none() {
vm.vt
.tracer
.warn(warning!(self.span(), "no text within stars").with_hint(
"using multiple consecutive stars (e.g. **) has no additional effect",
));
}
Ok(StrongElem::new(body.eval(vm)?).pack())
}
}
impl Eval for ast::Emph<'_> {
type Output = Content;
#[tracing::instrument(name = "Emph::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let body = self.body();
if body.exprs().next().is_none() {
vm.vt
.tracer
.warn(warning!(self.span(), "no text within underscores").with_hint(
"using multiple consecutive underscores (e.g. __) has no additional effect"
));
}
Ok(EmphElem::new(body.eval(vm)?).pack())
}
}
impl Eval for ast::Raw<'_> {
type Output = Content;
#[tracing::instrument(name = "Raw::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
let mut elem = RawElem::new(self.text()).with_block(self.block());
if let Some(lang) = self.lang() {
elem.push_lang(Some(lang.into()));
}
Ok(elem.pack())
}
}
impl Eval for ast::Link<'_> {
type Output = Content;
#[tracing::instrument(name = "Link::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(LinkElem::from_url(self.get().clone()).pack())
}
}
impl Eval for ast::Label<'_> {
type Output = Value;
#[tracing::instrument(name = "Label::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Label(Label::new(self.get())))
}
}
impl Eval for ast::Ref<'_> {
type Output = Content;
#[tracing::instrument(name = "Ref::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let target = Label::new(self.target());
let mut elem = RefElem::new(target);
if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content(
supplement.eval(vm)?,
))));
}
Ok(elem.pack())
}
}
impl Eval for ast::Heading<'_> {
type Output = Content;
#[tracing::instrument(name = "Heading::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let level = self.level();
let body = self.body().eval(vm)?;
Ok(HeadingElem::new(body).with_level(level).pack())
}
}
impl Eval for ast::ListItem<'_> {
type Output = Content;
#[tracing::instrument(name = "ListItem::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(ListItem::new(self.body().eval(vm)?).pack())
}
}
impl Eval for ast::EnumItem<'_> {
type Output = Content;
#[tracing::instrument(name = "EnumItem::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let body = self.body().eval(vm)?;
let mut elem = EnumItem::new(body);
if let Some(number) = self.number() {
elem.push_number(Some(number));
}
Ok(elem.pack())
}
}
impl Eval for ast::TermItem<'_> {
type Output = Content;
#[tracing::instrument(name = "TermItem::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let term = self.term().eval(vm)?;
let description = self.description().eval(vm)?;
Ok(TermItem::new(term, description).pack())
}
}
impl Eval for ast::Equation<'_> {
type Output = Content;
#[tracing::instrument(name = "Equation::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let body = self.body().eval(vm)?;
let block = self.block();
Ok(EquationElem::new(body).with_block(block).pack())
}
}

View File

@ -0,0 +1,113 @@
use ecow::eco_format;
use crate::diag::{At, SourceResult};
use crate::eval::{Eval, Vm};
use crate::foundations::{Content, NativeElement, Value};
use crate::math::{AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem};
use crate::syntax::ast::{self, AstNode};
use crate::text::TextElem;
impl Eval for ast::Math<'_> {
type Output = Content;
#[tracing::instrument(name = "Math::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(Content::sequence(
self.exprs()
.map(|expr| expr.eval_display(vm))
.collect::<SourceResult<Vec<_>>>()?,
))
}
}
impl Eval for ast::MathIdent<'_> {
type Output = Value;
#[tracing::instrument(name = "MathIdent::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.get_in_math(&self).cloned().at(self.span())
}
}
impl Eval for ast::MathAlignPoint<'_> {
type Output = Content;
#[tracing::instrument(name = "MathAlignPoint::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(AlignPointElem::new().pack())
}
}
impl Eval for ast::MathDelimited<'_> {
type Output = Content;
#[tracing::instrument(name = "MathDelimited::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let open = self.open().eval_display(vm)?;
let body = self.body().eval(vm)?;
let close = self.close().eval_display(vm)?;
Ok(LrElem::new(open + body + close).pack())
}
}
impl Eval for ast::MathAttach<'_> {
type Output = Content;
#[tracing::instrument(name = "MathAttach::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let base = self.base().eval_display(vm)?;
let mut elem = AttachElem::new(base);
if let Some(expr) = self.top() {
elem.push_t(Some(expr.eval_display(vm)?));
} else if let Some(primes) = self.primes() {
elem.push_t(Some(primes.eval(vm)?));
}
if let Some(expr) = self.bottom() {
elem.push_b(Some(expr.eval_display(vm)?));
}
Ok(elem.pack())
}
}
impl Eval for ast::MathPrimes<'_> {
type Output = Content;
#[tracing::instrument(name = "MathPrimes::eval", skip_all)]
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(PrimesElem::new(self.count()).pack())
}
}
impl Eval for ast::MathFrac<'_> {
type Output = Content;
#[tracing::instrument(name = "MathFrac::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let num = self.num().eval_display(vm)?;
let denom = self.denom().eval_display(vm)?;
Ok(FracElem::new(num, denom).pack())
}
}
impl Eval for ast::MathRoot<'_> {
type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
let radicand = self.radicand().eval_display(vm)?;
Ok(RootElem::new(radicand).with_index(index).pack())
}
}
trait ExprExt {
fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content>;
}
impl ExprExt for ast::Expr<'_> {
fn eval_display(&self, vm: &mut Vm) -> SourceResult<Content> {
Ok(self.eval(vm)?.display().spanned(self.span()))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,102 @@ use std::cmp::Ordering;
use ecow::eco_format;
use crate::diag::{bail, StrResult};
use crate::eval::{format_str, item, IntoValue, Regex, Repr, Smart, Value};
use crate::geom::{Align, Length, Numeric, Rel, Stroke};
use Value::*;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::eval::{access_dict, Access, Eval, Vm};
use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Smart, Value};
use crate::layout::{Align, Length, Rel};
use crate::syntax::ast::{self, AstNode};
use crate::text::TextElem;
use crate::util::Numeric;
use crate::visualize::Stroke;
impl Eval for ast::Unary<'_> {
type Output = Value;
#[tracing::instrument(name = "Unary::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.expr().eval(vm)?;
let result = match self.op() {
ast::UnOp::Pos => pos(value),
ast::UnOp::Neg => neg(value),
ast::UnOp::Not => not(value),
};
result.at(self.span())
}
}
impl Eval for ast::Binary<'_> {
type Output = Value;
#[tracing::instrument(name = "Binary::eval", skip_all)]
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
match self.op() {
ast::BinOp::Add => apply_binary(self, vm, add),
ast::BinOp::Sub => apply_binary(self, vm, sub),
ast::BinOp::Mul => apply_binary(self, vm, mul),
ast::BinOp::Div => apply_binary(self, vm, div),
ast::BinOp::And => apply_binary(self, vm, and),
ast::BinOp::Or => apply_binary(self, vm, or),
ast::BinOp::Eq => apply_binary(self, vm, eq),
ast::BinOp::Neq => apply_binary(self, vm, neq),
ast::BinOp::Lt => apply_binary(self, vm, lt),
ast::BinOp::Leq => apply_binary(self, vm, leq),
ast::BinOp::Gt => apply_binary(self, vm, gt),
ast::BinOp::Geq => apply_binary(self, vm, geq),
ast::BinOp::In => apply_binary(self, vm, in_),
ast::BinOp::NotIn => apply_binary(self, vm, not_in),
ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)),
ast::BinOp::AddAssign => apply_assignment(self, vm, add),
ast::BinOp::SubAssign => apply_assignment(self, vm, sub),
ast::BinOp::MulAssign => apply_assignment(self, vm, mul),
ast::BinOp::DivAssign => apply_assignment(self, vm, div),
}
}
}
/// Apply a basic binary operation.
fn apply_binary(
binary: ast::Binary,
vm: &mut Vm,
op: fn(Value, Value) -> StrResult<Value>,
) -> SourceResult<Value> {
let lhs = binary.lhs().eval(vm)?;
// Short-circuit boolean operations.
if (binary.op() == ast::BinOp::And && lhs == false.into_value())
|| (binary.op() == ast::BinOp::Or && lhs == true.into_value())
{
return Ok(lhs);
}
let rhs = binary.rhs().eval(vm)?;
op(lhs, rhs).at(binary.span())
}
/// Apply an assignment operation.
fn apply_assignment(
binary: ast::Binary,
vm: &mut Vm,
op: fn(Value, Value) -> StrResult<Value>,
) -> SourceResult<Value> {
let rhs = binary.rhs().eval(vm)?;
let lhs = binary.lhs();
// An assignment to a dictionary field is different from a normal access
// since it can create the field instead of just modifying it.
if binary.op() == ast::BinOp::Assign {
if let ast::Expr::FieldAccess(access) = lhs {
let dict = access_dict(vm, access)?;
dict.insert(access.field().get().clone().into(), rhs);
return Ok(Value::None);
}
}
let location = binary.lhs().access(vm)?;
let lhs = std::mem::take(&mut *location);
*location = op(lhs, rhs).at(binary.span())?;
Ok(Value::None)
}
/// Bail with a type mismatch error.
macro_rules! mismatch {
@ -18,6 +110,7 @@ macro_rules! mismatch {
/// Join a value with another value.
pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
use Value::*;
Ok(match (lhs, rhs) {
(a, None) => a,
(None, b) => b,
@ -27,10 +120,10 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())),
(Content(a), Str(b)) => Content(a + item!(text)(b.into())),
(Str(a), Content(b)) => Content(item!(text)(a.into()) + b),
(Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
@ -44,6 +137,7 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Apply the unary plus operator to a value.
pub fn pos(value: Value) -> StrResult<Value> {
use Value::*;
Ok(match value {
Int(v) => Int(v),
Float(v) => Float(v),
@ -68,6 +162,7 @@ pub fn pos(value: Value) -> StrResult<Value> {
/// Compute the negation of a value.
pub fn neg(value: Value) -> StrResult<Value> {
use Value::*;
Ok(match value {
Int(v) => Int(v.checked_neg().ok_or_else(too_large)?),
Float(v) => Float(-v),
@ -84,6 +179,7 @@ pub fn neg(value: Value) -> StrResult<Value> {
/// Compute the sum of two values.
pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
use Value::*;
Ok(match (lhs, rhs) {
(a, None) => a,
(None, b) => b,
@ -115,10 +211,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + item!(text)(b.get().into())),
(Content(a), Str(b)) => Content(a + item!(text)(b.into())),
(Str(a), Content(b)) => Content(item!(text)(a.into()) + b),
(Symbol(a), Content(b)) => Content(item!(text)(a.get().into()) + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b),
@ -161,6 +257,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Compute the difference of two values.
pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> {
use Value::*;
Ok(match (lhs, rhs) {
(Int(a), Int(b)) => Int(a.checked_sub(b).ok_or_else(too_large)?),
(Int(a), Float(b)) => Float(a as f64 - b),
@ -193,6 +290,7 @@ pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Compute the product of two values.
pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
use Value::*;
Ok(match (lhs, rhs) {
(Int(a), Int(b)) => Int(a.checked_mul(b).ok_or_else(too_large)?),
(Int(a), Float(b)) => Float(a as f64 * b),
@ -251,6 +349,7 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Compute the quotient of two values.
pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> {
use Value::*;
if is_zero(&rhs) {
bail!("cannot divide by zero");
}
@ -295,6 +394,7 @@ pub fn div(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Whether a value is a numeric zero.
fn is_zero(v: &Value) -> bool {
use Value::*;
match *v {
Int(v) => v == 0,
Float(v) => v == 0.0,
@ -322,7 +422,7 @@ fn try_div_relative(a: Rel<Length>, b: Rel<Length>) -> StrResult<f64> {
/// Compute the logical "not" of a value.
pub fn not(value: Value) -> StrResult<Value> {
match value {
Bool(b) => Ok(Bool(!b)),
Value::Bool(b) => Ok(Value::Bool(!b)),
v => mismatch!("cannot apply 'not' to {}", v),
}
}
@ -330,7 +430,7 @@ pub fn not(value: Value) -> StrResult<Value> {
/// Compute the logical "and" of two values.
pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) {
(Bool(a), Bool(b)) => Ok(Bool(a && b)),
(Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a && b)),
(a, b) => mismatch!("cannot apply 'and' to {} and {}", a, b),
}
}
@ -338,19 +438,19 @@ pub fn and(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Compute the logical "or" of two values.
pub fn or(lhs: Value, rhs: Value) -> StrResult<Value> {
match (lhs, rhs) {
(Bool(a), Bool(b)) => Ok(Bool(a || b)),
(Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a || b)),
(a, b) => mismatch!("cannot apply 'or' to {} and {}", a, b),
}
}
/// Compute whether two values are equal.
pub fn eq(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(Bool(equal(&lhs, &rhs)))
Ok(Value::Bool(equal(&lhs, &rhs)))
}
/// Compute whether two values are unequal.
pub fn neq(lhs: Value, rhs: Value) -> StrResult<Value> {
Ok(Bool(!equal(&lhs, &rhs)))
Ok(Value::Bool(!equal(&lhs, &rhs)))
}
macro_rules! comparison {
@ -358,7 +458,7 @@ macro_rules! comparison {
/// Compute how a value compares with another value.
pub fn $name(lhs: Value, rhs: Value) -> StrResult<Value> {
let ordering = compare(&lhs, &rhs)?;
Ok(Bool(matches!(ordering, $($pat)*)))
Ok(Value::Bool(matches!(ordering, $($pat)*)))
}
};
}
@ -370,6 +470,7 @@ comparison!(geq, ">=", Ordering::Greater | Ordering::Equal);
/// Determine whether two values are equal.
pub fn equal(lhs: &Value, rhs: &Value) -> bool {
use Value::*;
match (lhs, rhs) {
// Compare reflexively.
(None, None) => true,
@ -418,6 +519,7 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
/// Compare two values.
pub fn compare(lhs: &Value, rhs: &Value) -> StrResult<Ordering> {
use Value::*;
Ok(match (lhs, rhs) {
(Bool(a), Bool(b)) => a.cmp(b),
(Int(a), Int(b)) => a.cmp(b),
@ -452,7 +554,7 @@ fn try_cmp_values<T: PartialOrd + Repr>(a: &T, b: &T) -> StrResult<Ordering> {
}
/// Try to compare two datetimes.
fn try_cmp_datetimes(a: &super::Datetime, b: &super::Datetime) -> StrResult<Ordering> {
fn try_cmp_datetimes(a: &Datetime, b: &Datetime) -> StrResult<Ordering> {
a.partial_cmp(b)
.ok_or_else(|| eco_format!("cannot compare {} and {}", a.kind(), b.kind()))
}
@ -460,7 +562,7 @@ fn try_cmp_datetimes(a: &super::Datetime, b: &super::Datetime) -> StrResult<Orde
/// Test whether one value is "in" another one.
pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
Ok(Bool(b))
Ok(Value::Bool(b))
} else {
mismatch!("cannot apply 'in' to {} and {}", lhs, rhs)
}
@ -469,7 +571,7 @@ pub fn in_(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Test whether one value is "not in" another one.
pub fn not_in(lhs: Value, rhs: Value) -> StrResult<Value> {
if let Some(b) = contains(&lhs, &rhs) {
Ok(Bool(!b))
Ok(Value::Bool(!b))
} else {
mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs)
}
@ -477,6 +579,7 @@ pub fn not_in(lhs: Value, rhs: Value) -> StrResult<Value> {
/// Test for containment.
pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
use Value::*;
match (lhs, rhs) {
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),

View File

@ -0,0 +1,51 @@
use crate::diag::{At, SourceResult};
use crate::eval::{Eval, Vm};
use crate::foundations::{Func, Recipe, ShowableSelector, Styles, Transformation};
use crate::syntax::ast::{self, AstNode};
impl Eval for ast::SetRule<'_> {
type Output = Styles;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
if let Some(condition) = self.condition() {
if !condition.eval(vm)?.cast::<bool>().at(condition.span())? {
return Ok(Styles::new());
}
}
let target = self.target();
let target = target
.eval(vm)?
.cast::<Func>()
.and_then(|func| {
func.element().ok_or_else(|| {
"only element functions can be used in set rules".into()
})
})
.at(target.span())?;
let args = self.args().eval(vm)?;
Ok(target.set(vm, args)?.spanned(self.span()))
}
}
impl Eval for ast::ShowRule<'_> {
type Output = Recipe;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let selector = self
.selector()
.map(|sel| sel.eval(vm)?.cast::<ShowableSelector>().at(sel.span()))
.transpose()?
.map(|selector| selector.0);
let transform = self.transform();
let span = transform.span();
let transform = match transform {
ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?),
expr => expr.eval(vm)?.cast::<Transformation>().at(span)?,
};
Ok(Recipe { span, selector, transform })
}
}

View File

@ -3,7 +3,7 @@ use std::collections::HashSet;
use ecow::EcoVec;
use crate::diag::SourceDiagnostic;
use crate::eval::Value;
use crate::foundations::Value;
use crate::syntax::{FileId, Span};
use crate::util::hash128;
@ -44,7 +44,7 @@ impl Tracer {
#[comemo::track]
impl Tracer {
/// The inspeted span if it is part of the given source file.
/// The inspected span if it is part of the given source file.
pub fn inspected(&self, id: FileId) -> Option<Span> {
if self.inspected.and_then(Span::id) == Some(id) {
self.inspected

127
crates/typst/src/eval/vm.rs Normal file
View File

@ -0,0 +1,127 @@
use comemo::{Track, Tracked, Validate};
use crate::diag::{bail, StrResult};
use crate::eval::FlowEvent;
use crate::foundations::{IntoValue, Scopes};
use crate::layout::Vt;
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{FileId, Span};
use crate::World;
/// A virtual machine.
///
/// Holds the state needed to [evaluate](crate::eval::eval()) Typst sources. A new
/// virtual machine is created for each module evaluation and function call.
pub struct Vm<'a> {
/// The underlying virtual typesetter.
pub(crate) vt: Vt<'a>,
/// The route of source ids the VM took to reach its current location.
pub(crate) route: Tracked<'a, Route<'a>>,
/// The id of the currently evaluated file.
pub(crate) file: Option<FileId>,
/// A control flow event that is currently happening.
pub(crate) flow: Option<FlowEvent>,
/// The stack of scopes.
pub(crate) scopes: Scopes<'a>,
/// The current call depth.
pub(crate) depth: usize,
/// A span that is currently under inspection.
pub(crate) inspected: Option<Span>,
}
impl<'a> Vm<'a> {
/// Create a new virtual machine.
pub fn new(
vt: Vt<'a>,
route: Tracked<'a, Route>,
file: Option<FileId>,
scopes: Scopes<'a>,
) -> Self {
let inspected = file.and_then(|id| vt.tracer.inspected(id));
Self {
vt,
route,
file,
flow: None,
scopes,
depth: 0,
inspected,
}
}
/// Access the underlying world.
pub fn world(&self) -> Tracked<'a, dyn World + 'a> {
self.vt.world
}
/// The id of the currently evaluated file.
///
/// Returns `None` if the VM is in a detached context, e.g. when evaluating
/// a user-provided string.
pub fn file(&self) -> Option<FileId> {
self.file
}
/// Resolve a path relative to the currently evaluated file.
pub fn resolve_path(&self, path: &str) -> StrResult<FileId> {
let Some(file) = self.file else {
bail!("cannot access file system from here");
};
Ok(file.join(path))
}
/// Define a variable in the current scope.
#[tracing::instrument(skip_all)]
pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) {
let value = value.into_value();
if self.inspected == Some(var.span()) {
self.vt.tracer.value(value.clone());
}
self.scopes.top.define(var.get().clone(), value);
}
}
/// A route of source ids.
#[derive(Default)]
pub struct Route<'a> {
// We need to override the constraint's lifetime here so that `Tracked` is
// covariant over the constraint. If it becomes invariant, we're in for a
// world of lifetime pain.
outer: Option<Tracked<'a, Self, <Route<'static> as Validate>::Constraint>>,
id: Option<FileId>,
}
impl<'a> Route<'a> {
/// Create a new route with just one entry.
pub fn new(id: Option<FileId>) -> Self {
Self { id, outer: None }
}
/// Insert a new id into the route.
///
/// You must guarantee that `outer` lives longer than the resulting
/// route is ever used.
pub fn insert(outer: Tracked<'a, Self>, id: FileId) -> Self {
Route { outer: Some(outer), id: Some(id) }
}
/// Start tracking this locator.
///
/// In comparison to [`Track::track`], this method skips this chain link
/// if it does not contribute anything.
pub fn track(&self) -> Tracked<'_, Self> {
match self.outer {
Some(outer) if self.id.is_none() => outer,
_ => Track::track(self),
}
}
}
#[comemo::track]
impl<'a> Route<'a> {
/// Whether the given id is part of the route.
pub fn contains(&self, id: FileId) -> bool {
self.id == Some(id) || self.outer.map_or(false, |outer| outer.contains(id))
}
}

View File

@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use crate::diag::{bail, At, SourceDiagnostic, SourceResult};
use crate::eval::{
use crate::foundations::{
func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value,
};
use crate::syntax::{Span, Spanned};

View File

@ -8,9 +8,10 @@ use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::diag::{At, SourceResult, StrResult};
use crate::eval::{
cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue,
Reflect, Repr, Value, Version, Vm,
use crate::eval::{ops, Vm};
use crate::foundations::{
cast, func, repr, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue,
Reflect, Repr, Value, Version,
};
use crate::syntax::Span;
@ -19,15 +20,15 @@ use crate::syntax::Span;
#[doc(hidden)]
macro_rules! __array {
($value:expr; $count:expr) => {
$crate::eval::Array::from($crate::eval::eco_vec![
$crate::eval::IntoValue::into_value($value);
$crate::foundations::Array::from($crate::foundations::eco_vec![
$crate::foundations::IntoValue::into_value($value);
$count
])
};
($($value:expr),* $(,)?) => {
$crate::eval::Array::from($crate::eval::eco_vec![$(
$crate::eval::IntoValue::into_value($value)
$crate::foundations::Array::from($crate::foundations::eco_vec![$(
$crate::foundations::IntoValue::into_value($value)
),*])
};
}
@ -35,9 +36,6 @@ macro_rules! __array {
#[doc(inline)]
pub use crate::__array as array;
#[doc(hidden)]
pub use ecow::eco_vec;
/// A sequence of values.
///
/// You can construct an array by enclosing a comma-separated sequence of values
@ -736,7 +734,7 @@ impl Array {
vec.make_mut().sort_by(|a, b| {
// Until we get `try` blocks :)
match (key_of(a.clone()), key_of(b.clone())) {
(Ok(a), Ok(b)) => super::ops::compare(&a, &b).unwrap_or_else(|err| {
(Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| {
if result.is_ok() {
result = Err(err).at(span);
}
@ -790,7 +788,7 @@ impl Array {
}
for second in out.iter() {
if super::ops::equal(&key, &key_of(second.clone())?) {
if ops::equal(&key, &key_of(second.clone())?) {
continue 'outer;
}
}

View File

@ -2,8 +2,10 @@ use ecow::EcoString;
use std::fmt::{self, Debug, Formatter};
use crate::diag::StrResult;
use crate::eval::{ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value};
use crate::model::{Fold, Resolve, StyleChain};
use crate::foundations::{
ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type,
Value,
};
/// A value that indicates a smart default.
///

View File

@ -1,6 +1,6 @@
use ecow::EcoString;
use super::{ty, Repr};
use crate::foundations::{ty, Repr};
/// A type with two states.
///

View File

@ -8,7 +8,7 @@ use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use crate::diag::{bail, StrResult};
use crate::eval::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
/// A sequence of bytes.
///

View File

@ -4,20 +4,15 @@ use std::cmp;
use std::cmp::Ordering;
use std::ops::{Div, Rem};
use typst::eval::{Module, Scope};
use crate::prelude::*;
/// Hook up all calculation definitions.
pub(super) fn define(global: &mut Scope) {
global.category("calculate");
global.define_module(module());
}
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::eval::ops;
use crate::foundations::{cast, func, IntoValue, Module, Scope, Value};
use crate::layout::{Angle, Fr, Length, Ratio};
use crate::syntax::{Span, Spanned};
/// A module with calculation definitions.
fn module() -> Module {
pub fn module() -> Module {
let mut scope = Scope::new();
scope.category("calculate");
scope.define_func::<abs>();
scope.define_func::<pow>();
scope.define_func::<exp>();
@ -747,7 +742,7 @@ fn minmax(
};
for Spanned { v, span } in iter {
let ordering = typst::eval::ops::compare(&v, &extremum).at(span)?;
let ordering = ops::compare(&v, &extremum).at(span)?;
if ordering == goal {
extremum = v;
}

View File

@ -1,5 +1,3 @@
pub use typst_macros::{cast, Cast};
use std::borrow::Cow;
use std::fmt::Write;
use std::hash::Hash;
@ -11,9 +9,12 @@ use smallvec::SmallVec;
use unicode_math_class::MathClass;
use crate::diag::{At, SourceResult, StrResult};
use crate::eval::{repr, Repr, Type, Value};
use crate::foundations::{repr, Repr, Type, Value};
use crate::syntax::{Span, Spanned};
#[doc(inline)]
pub use typst_macros::{cast, Cast};
/// Determine details of a type.
///
/// Type casting works as follows:

View File

@ -7,19 +7,20 @@ use std::sync::Arc;
use comemo::Prehashed;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use smallvec::SmallVec;
use typst_macros::elem;
use smallvec::smallvec;
use crate::diag::{SourceResult, StrResult};
use crate::doc::Meta;
use crate::eval::{
func, repr, scope, ty, Dict, FromValue, IntoValue, Repr, Str, Value, Vm,
};
use crate::model::{
Behave, Behaviour, Element, Guard, Label, Location, NativeElement, Recipe, Selector,
Style, Styles,
use crate::eval::Vm;
use crate::foundations::{
elem, func, scope, ty, Dict, Element, FromValue, Guard, IntoValue, Label,
NativeElement, Recipe, Repr, Selector, Str, Style, Styles, Value,
};
use crate::introspection::{Location, Meta, MetaElem};
use crate::layout::{Align, AlignElem, Axes, Length, MoveElem, PadElem, Rel, Sides};
use crate::model::{Destination, EmphElem, StrongElem};
use crate::syntax::Span;
use crate::text::UnderlineElem;
use crate::util::fat;
/// A piece of document content.
///
@ -457,6 +458,57 @@ impl Content {
}
}
impl Content {
/// Strongly emphasize this content.
pub fn strong(self) -> Self {
StrongElem::new(self).pack()
}
/// Emphasize this content.
pub fn emph(self) -> Self {
EmphElem::new(self).pack()
}
/// Underline this content.
pub fn underlined(self) -> Self {
UnderlineElem::new(self).pack()
}
/// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self {
self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)]))
}
/// Make the content linkable by `.linked(Destination::Location(loc))`.
///
/// Should be used in combination with [`Location::variant`].
pub fn backlinked(self, loc: Location) -> Self {
let mut backlink = Content::empty();
backlink.set_location(loc);
self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)]))
}
/// Set alignments for this content.
pub fn aligned(self, align: Align) -> Self {
self.styled(AlignElem::set_alignment(align))
}
/// Pad this content at the sides.
pub fn padded(self, padding: Sides<Rel<Length>>) -> Self {
PadElem::new(self)
.with_left(padding.left)
.with_top(padding.top)
.with_right(padding.right)
.with_bottom(padding.bottom)
.pack()
}
/// Transform this content's contents without affecting layout.
pub fn moved(self, delta: Axes<Rel<Length>>) -> Self {
MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack()
}
}
#[scope]
impl Content {
/// The content's element function. This function can be used to create the element
@ -690,7 +742,7 @@ impl Repr for SequenceElem {
} else {
eco_format!(
"[{}]",
repr::pretty_array_like(
crate::foundations::repr::pretty_array_like(
&self.children.iter().map(|c| c.0.repr()).collect::<Vec<_>>(),
false
)
@ -720,21 +772,6 @@ impl Repr for StyledElem {
}
}
/// Hosts metadata and ensures metadata is produced even for empty elements.
#[elem(Behave)]
pub struct MetaElem {
/// Metadata that should be attached to all elements affected by this style
/// property.
#[fold]
pub data: SmallVec<[Meta; 1]>,
}
impl Behave for MetaElem {
fn behaviour(&self) -> Behaviour {
Behaviour::Invisible
}
}
/// Tries to extract the plain-text representation of the element.
pub trait PlainText {
/// Write this element's plain text into the given buffer.
@ -743,7 +780,7 @@ pub trait PlainText {
/// The missing field access error message.
#[cold]
pub fn missing_field(field: &str) -> EcoString {
fn missing_field(field: &str) -> EcoString {
eco_format!("content does not contain field {}", field.repr())
}
@ -756,66 +793,3 @@ fn missing_field_no_default(field: &str) -> EcoString {
field.repr()
)
}
/// Fat pointer handling.
///
/// This assumes the memory representation of fat pointers. Although it is not
/// guaranteed by Rust, it's improbable that it will change. Still, when the
/// pointer metadata APIs are stable, we should definitely move to them:
/// <https://github.com/rust-lang/rust/issues/81513>
pub mod fat {
use std::alloc::Layout;
use std::mem;
/// Create a fat pointer from a data address and a vtable address.
///
/// # Safety
/// Must only be called when `T` is a `dyn Trait`. The data address must point
/// to a value whose type implements the trait of `T` and the `vtable` must have
/// been extracted with [`vtable`].
#[track_caller]
pub unsafe fn from_raw_parts<T: ?Sized>(
data: *const (),
vtable: *const (),
) -> *const T {
let fat = FatPointer { data, vtable };
debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>());
mem::transmute_copy::<FatPointer, *const T>(&fat)
}
/// Create a mutable fat pointer from a data address and a vtable address.
///
/// # Safety
/// Must only be called when `T` is a `dyn Trait`. The data address must point
/// to a value whose type implements the trait of `T` and the `vtable` must have
/// been extracted with [`vtable`].
#[track_caller]
pub unsafe fn from_raw_parts_mut<T: ?Sized>(
data: *mut (),
vtable: *const (),
) -> *mut T {
let fat = FatPointer { data, vtable };
debug_assert_eq!(Layout::new::<*mut T>(), Layout::new::<FatPointer>());
mem::transmute_copy::<FatPointer, *mut T>(&fat)
}
/// Extract the address to a trait object's vtable.
///
/// # Safety
/// Must only be called when `T` is a `dyn Trait`.
#[track_caller]
pub unsafe fn vtable<T: ?Sized>(ptr: *const T) -> *const () {
debug_assert_eq!(Layout::new::<*const T>(), Layout::new::<FatPointer>());
mem::transmute_copy::<*const T, FatPointer>(&ptr).vtable
}
/// The memory representation of a trait object pointer.
///
/// Although this is not guaranteed by Rust, it's improbable that it will
/// change.
#[repr(C)]
struct FatPointer {
data: *const (),
vtable: *const (),
}
}

View File

@ -1,6 +1,4 @@
use std::cmp::Ordering;
use std::fmt::Debug;
use std::hash::Hash;
use std::ops::{Add, Sub};
@ -10,8 +8,9 @@ use time::macros::format_description;
use time::{format_description, Month, PrimitiveDateTime};
use crate::diag::{bail, StrResult};
use crate::eval::{
cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value, Vm,
use crate::eval::Vm;
use crate::foundations::{
cast, func, repr, scope, ty, Dict, Duration, Repr, Smart, Str, Value,
};
use crate::World;
@ -238,26 +237,26 @@ impl Datetime {
pub fn construct(
/// The year of the datetime.
#[named]
year: Option<YearComponent>,
year: Option<i32>,
/// The month of the datetime.
#[named]
month: Option<MonthComponent>,
month: Option<Month>,
/// The day of the datetime.
#[named]
day: Option<DayComponent>,
day: Option<u8>,
/// The hour of the datetime.
#[named]
hour: Option<HourComponent>,
hour: Option<u8>,
/// The minute of the datetime.
#[named]
minute: Option<MinuteComponent>,
minute: Option<u8>,
/// The second of the datetime.
#[named]
second: Option<SecondComponent>,
second: Option<u8>,
) -> StrResult<Datetime> {
let time = match (hour, minute, second) {
(Some(hour), Some(minute), Some(second)) => {
match time::Time::from_hms(hour.0, minute.0, second.0) {
match time::Time::from_hms(hour, minute, second) {
Ok(time) => Some(time),
Err(_) => bail!("time is invalid"),
}
@ -268,7 +267,7 @@ impl Datetime {
let date = match (year, month, day) {
(Some(year), Some(month), Some(day)) => {
match time::Date::from_calendar_date(year.0, month.0, day.0) {
match time::Date::from_calendar_date(year, month, day) {
Ok(date) => Some(date),
Err(_) => bail!("date is invalid"),
}
@ -493,43 +492,6 @@ impl Sub for Datetime {
}
}
pub struct YearComponent(i32);
pub struct MonthComponent(Month);
pub struct DayComponent(u8);
pub struct HourComponent(u8);
pub struct MinuteComponent(u8);
pub struct SecondComponent(u8);
cast! {
YearComponent,
v: i32 => Self(v),
}
cast! {
MonthComponent,
v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?)
}
cast! {
DayComponent,
v: u8 => Self(v),
}
cast! {
HourComponent,
v: u8 => Self(v),
}
cast! {
MinuteComponent,
v: u8 => Self(v),
}
cast! {
SecondComponent,
v: u8 => Self(v),
}
/// A format in which a datetime can be displayed.
pub struct DisplayPattern(Str, format_description::OwnedFormatItem);
@ -543,6 +505,11 @@ cast! {
}
}
cast! {
Month,
v: u8 => Self::try_from(v).map_err(|_| "month is invalid")?
}
/// Format the `Format` error of the time crate in an appropriate way.
fn format_time_format_error(error: Format) -> EcoString {
match error {

View File

@ -8,7 +8,7 @@ use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::diag::StrResult;
use crate::eval::{array, func, repr, scope, ty, Array, Repr, Str, Value};
use crate::foundations::{array, func, repr, scope, ty, Array, Repr, Str, Value};
use crate::syntax::is_ident;
use crate::util::ArcExt;
@ -18,9 +18,9 @@ use crate::util::ArcExt;
macro_rules! __dict {
($($key:expr => $value:expr),* $(,)?) => {{
#[allow(unused_mut)]
let mut map = $crate::eval::IndexMap::new();
$(map.insert($key.into(), $crate::eval::IntoValue::into_value($value));)*
$crate::eval::Dict::from(map)
let mut map = $crate::foundations::IndexMap::new();
$(map.insert($key.into(), $crate::foundations::IntoValue::into_value($value));)*
$crate::foundations::Dict::from(map)
}};
}

View File

@ -1,10 +1,10 @@
use ecow::{eco_format, EcoString};
use std::fmt::{self, Debug, Formatter};
use std::ops::{Add, Div, Mul, Neg, Sub};
use ecow::{eco_format, EcoString};
use time::ext::NumericalDuration;
use crate::eval::{func, repr, scope, ty, Repr};
use crate::foundations::{func, repr, scope, ty, Repr};
/// Represents a positive or negative span of time.
#[ty(scope)]

View File

@ -1,4 +1,5 @@
use std::any::{Any, TypeId};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::Hasher;
@ -8,14 +9,21 @@ use ecow::EcoString;
use once_cell::sync::Lazy;
use smallvec::SmallVec;
use super::{Content, Selector, Styles};
use crate::diag::{SourceResult, StrResult};
use crate::doc::{Lang, Region};
use crate::eval::{cast, Args, Dict, Func, ParamInfo, Repr, Scope, Value, Vm};
use crate::model::{Guard, Label, Location};
use crate::eval::Vm;
use crate::foundations::{
cast, Args, Content, Dict, Func, Label, ParamInfo, Repr, Scope, Selector, StyleChain,
Styles, Value,
};
use crate::introspection::Location;
use crate::layout::Vt;
use crate::syntax::Span;
use crate::text::{Lang, Region};
use crate::util::Static;
#[doc(inline)]
pub use typst_macros::elem;
/// A document element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Element(Static<NativeElementData>);
@ -93,8 +101,8 @@ impl Element {
Selector::Elem(self, None)
}
/// Create a selector for this element, filtering for those
/// that [fields](super::Content::field) match the given argument.
/// Create a selector for this element, filtering for those that
/// [fields](crate::foundations::Content::field) match the given argument.
pub fn where_(self, fields: SmallVec<[(u8, Value); 1]>) -> Selector {
Selector::Elem(self, Some(fields))
}
@ -308,8 +316,67 @@ cast! {
self => Element::from(self).into_value(),
}
/// The named with which an element is referenced.
pub trait LocalName {
/// Get the name in the given language and (optionally) region.
fn local_name(lang: Lang, region: Option<Region>) -> &'static str;
/// Synthesize fields on an element. This happens before execution of any show
/// rule.
pub trait Synthesize {
/// Prepare the element for show rule application.
fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()>;
}
/// The base recipe for an element.
pub trait Show {
/// Execute the base recipe for this element.
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>;
}
/// Post-process an element after it was realized.
pub trait Finalize {
/// Finalize the fully realized form of the element. Use this for effects
/// that should work even in the face of a user-defined show rule.
fn finalize(&self, realized: Content, styles: StyleChain) -> Content;
}
/// How the element interacts with other elements.
pub trait Behave {
/// The element's interaction behaviour.
fn behaviour(&self) -> Behaviour;
/// Whether this weak element is larger than a previous one and thus picked
/// as the maximum when the levels are the same.
#[allow(unused_variables)]
fn larger(
&self,
prev: &(Cow<Content>, Behaviour, StyleChain),
styles: StyleChain,
) -> bool {
false
}
}
/// How an element interacts with other elements in a stream.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Behaviour {
/// A weak element which only survives when a supportive element is before
/// and after it. Furthermore, per consecutive run of weak elements, only
/// one survives: The one with the lowest weakness level (or the larger one
/// if there is a tie).
Weak(usize),
/// An element that enables adjacent weak elements to exist. The default.
Supportive,
/// An element that destroys adjacent weak elements.
Destructive,
/// An element that does not interact at all with other elements, having the
/// same effect as if it didn't exist, but has a visual representation.
Ignorant,
/// An element that does not have a visual representation.
Invisible,
}
/// Guards content against being affected by the same show rule multiple times.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Guard {
/// The nth recipe from the top of the chain.
Nth(usize),
/// The [base recipe](Show) for a kind of element.
Base(Element),
}

View File

@ -1,8 +1,11 @@
//! Fields on values.
use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::eval::{IntoValue, Type, Value, Version};
use crate::geom::{Align, Length, Rel, Stroke};
use crate::foundations::{IntoValue, Type, Value, Version};
use crate::layout::{Align, Length, Rel};
use crate::visualize::Stroke;
/// Try to access a field on a value.
///

View File

@ -2,8 +2,8 @@ use std::num::ParseFloatError;
use ecow::{eco_format, EcoString};
use crate::eval::{cast, func, repr, scope, ty, Repr, Str};
use crate::geom::Ratio;
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str};
use crate::layout::Ratio;
/// A floating-point number.
///

View File

@ -1,22 +1,20 @@
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use comemo::{Prehashed, Tracked, TrackedMut};
use comemo::{Prehashed, TrackedMut};
use ecow::{eco_format, EcoString};
use once_cell::sync::Lazy;
use crate::diag::{bail, HintedStrResult, SourceResult, StrResult};
use crate::eval::{
cast, scope, ty, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes,
Tracer, Type, Value, Vm,
use crate::diag::{bail, SourceResult, StrResult};
use crate::eval::{Route, Vm};
use crate::foundations::{
cast, repr, scope, ty, Args, CastInfo, Content, Element, IntoValue, Scope, Scopes,
Selector, Type, Value,
};
use crate::model::{
Content, DelayedErrors, Element, Introspector, Locator, Selector, Vt,
};
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{FileId, Span, SyntaxNode};
use crate::introspection::Locator;
use crate::layout::Vt;
use crate::syntax::{ast, FileId, Span, SyntaxNode};
use crate::util::Static;
use crate::World;
#[doc(inline)]
pub use typst_macros::func;
@ -276,9 +274,9 @@ impl Func {
// Determine the route inside the closure.
let fresh = Route::new(closure.file);
let route = if vm.file.is_none() { fresh.track() } else { vm.route };
Closure::call(
crate::eval::call_closure(
self,
closure,
vm.world(),
route,
vm.vt.introspector,
@ -396,7 +394,7 @@ impl Debug for Func {
}
}
impl super::Repr for Func {
impl repr::Repr for Func {
fn repr(&self) -> EcoString {
match self.name() {
Some(name) => name.into(),
@ -496,7 +494,7 @@ pub struct ParamInfo {
/// A user-defined closure.
#[derive(Debug, Hash)]
pub(super) struct Closure {
pub struct Closure {
/// The closure's syntax node. Must be castable to `ast::Closure`.
pub node: SyntaxNode,
/// The source file where the closure was defined.
@ -516,116 +514,6 @@ impl Closure {
.name()
.map(|ident| ident.as_str())
}
/// Call the function in the context with the arguments.
#[comemo::memoize]
#[tracing::instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
fn call(
func: &Func,
world: Tracked<dyn World + '_>,
route: Tracked<Route>,
introspector: Tracked<Introspector>,
locator: Tracked<Locator>,
delayed: TrackedMut<DelayedErrors>,
tracer: TrackedMut<Tracer>,
depth: usize,
mut args: Args,
) -> SourceResult<Value> {
let Repr::Closure(this) = &func.repr else {
panic!("`this` must be a closure");
};
let closure = this.node.cast::<ast::Closure>().unwrap();
// Don't leak the scopes from the call site. Instead, we use the scope
// of captured variables we collected earlier.
let mut scopes = Scopes::new(None);
scopes.top = this.captured.clone();
// Prepare VT.
let mut locator = Locator::chained(locator);
let vt = Vt {
world,
introspector,
locator: &mut locator,
delayed,
tracer,
};
// Prepare VM.
let mut vm = Vm::new(vt, route, this.file, scopes);
vm.depth = depth;
// Provide the closure itself for recursive calls.
if let Some(name) = closure.name() {
vm.define(name, Value::Func(func.clone()));
}
// Parse the arguments according to the parameter list.
let num_pos_params = closure
.params()
.children()
.filter(|p| matches!(p, ast::Param::Pos(_)))
.count();
let num_pos_args = args.to_pos().len();
let sink_size = num_pos_args.checked_sub(num_pos_params);
let mut sink = None;
let mut sink_pos_values = None;
let mut defaults = this.defaults.iter();
for p in closure.params().children() {
match p {
ast::Param::Pos(pattern) => match pattern {
ast::Pattern::Normal(ast::Expr::Ident(ident)) => {
vm.define(ident, args.expect::<Value>(&ident)?)
}
ast::Pattern::Normal(_) => unreachable!(),
pattern => {
super::define_pattern(
&mut vm,
pattern,
args.expect::<Value>("pattern parameter")?,
)?;
}
},
ast::Param::Sink(ident) => {
sink = ident.name();
if let Some(sink_size) = sink_size {
sink_pos_values = Some(args.consume(sink_size)?);
}
}
ast::Param::Named(named) => {
let name = named.name();
let default = defaults.next().unwrap();
let value =
args.named::<Value>(&name)?.unwrap_or_else(|| default.clone());
vm.define(name, value);
}
}
}
if let Some(sink) = sink {
let mut remaining_args = args.take();
if let Some(sink_pos_values) = sink_pos_values {
remaining_args.items.extend(sink_pos_values);
}
vm.define(sink, remaining_args);
}
// Ensure all arguments have been used.
args.finish()?;
// Handle control flow.
let output = closure.body().eval(&mut vm)?;
match vm.flow {
Some(FlowEvent::Return(_, Some(explicit))) => return Ok(explicit),
Some(FlowEvent::Return(_, None)) => {}
Some(flow) => bail!(flow.forbidden()),
None => {}
}
Ok(output)
}
}
impl From<Closure> for Func {
@ -638,233 +526,3 @@ cast! {
Closure,
self => Value::Func(self.into()),
}
/// A visitor that determines which variables to capture for a closure.
pub struct CapturesVisitor<'a> {
external: Option<&'a Scopes<'a>>,
internal: Scopes<'a>,
captures: Scope,
}
impl<'a> CapturesVisitor<'a> {
/// Create a new visitor for the given external scopes.
pub fn new(external: Option<&'a Scopes<'a>>) -> Self {
Self {
external,
internal: Scopes::new(None),
captures: Scope::new(),
}
}
/// Return the scope of captured variables.
pub fn finish(self) -> Scope {
self.captures
}
/// Visit any node and collect all captured variables.
#[tracing::instrument(skip_all)]
pub fn visit(&mut self, node: &SyntaxNode) {
match node.cast() {
// Every identifier is a potential variable that we need to capture.
// Identifiers that shouldn't count as captures because they
// actually bind a new name are handled below (individually through
// the expressions that contain them).
Some(ast::Expr::Ident(ident)) => self.capture(&ident, Scopes::get),
Some(ast::Expr::MathIdent(ident)) => {
self.capture(&ident, Scopes::get_in_math)
}
// Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => {
self.internal.enter();
for child in node.children() {
self.visit(child);
}
self.internal.exit();
}
// Don't capture the field of a field access.
Some(ast::Expr::FieldAccess(access)) => {
self.visit(access.target().to_untyped());
}
// A closure contains parameter bindings, which are bound before the
// body is evaluated. Care must be taken so that the default values
// of named parameters cannot access previous parameter bindings.
Some(ast::Expr::Closure(expr)) => {
for param in expr.params().children() {
if let ast::Param::Named(named) = param {
self.visit(named.expr().to_untyped());
}
}
self.internal.enter();
if let Some(name) = expr.name() {
self.bind(name);
}
for param in expr.params().children() {
match param {
ast::Param::Pos(pattern) => {
for ident in pattern.idents() {
self.bind(ident);
}
}
ast::Param::Named(named) => self.bind(named.name()),
ast::Param::Sink(spread) => {
self.bind(spread.name().unwrap_or_default())
}
}
}
self.visit(expr.body().to_untyped());
self.internal.exit();
}
// A let expression contains a binding, but that binding is only
// active after the body is evaluated.
Some(ast::Expr::Let(expr)) => {
if let Some(init) = expr.init() {
self.visit(init.to_untyped());
}
for ident in expr.kind().idents() {
self.bind(ident);
}
}
// A for loop contains one or two bindings in its pattern. These are
// active after the iterable is evaluated but before the body is
// evaluated.
Some(ast::Expr::For(expr)) => {
self.visit(expr.iter().to_untyped());
self.internal.enter();
let pattern = expr.pattern();
for ident in pattern.idents() {
self.bind(ident);
}
self.visit(expr.body().to_untyped());
self.internal.exit();
}
// An import contains items, but these are active only after the
// path is evaluated.
Some(ast::Expr::Import(expr)) => {
self.visit(expr.source().to_untyped());
if let Some(ast::Imports::Items(items)) = expr.imports() {
for item in items.iter() {
self.bind(item.bound_name());
}
}
}
_ => {
// Never capture the name part of a named pair.
if let Some(named) = node.cast::<ast::Named>() {
self.visit(named.expr().to_untyped());
return;
}
// Everything else is traversed from left to right.
for child in node.children() {
self.visit(child);
}
}
}
}
/// Bind a new internal variable.
fn bind(&mut self, ident: ast::Ident) {
self.internal.top.define(ident.get().clone(), Value::None);
}
/// Capture a variable if it isn't internal.
#[inline]
fn capture(
&mut self,
ident: &str,
getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>,
) {
if self.internal.get(ident).is_err() {
let Some(value) = self
.external
.map(|external| getter(external, ident).ok())
.unwrap_or(Some(&Value::None))
else {
return;
};
self.captures.define_captured(ident, value.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::parse;
#[track_caller]
fn test(text: &str, result: &[&str]) {
let mut scopes = Scopes::new(None);
scopes.top.define("f", 0);
scopes.top.define("x", 0);
scopes.top.define("y", 0);
scopes.top.define("z", 0);
let mut visitor = CapturesVisitor::new(Some(&scopes));
let root = parse(text);
visitor.visit(&root);
let captures = visitor.finish();
let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect();
names.sort();
assert_eq!(names, result);
}
#[test]
fn test_captures() {
// Let binding and function definition.
test("#let x = x", &["x"]);
test("#let x; #(x + y)", &["y"]);
test("#let f(x, y) = x + y", &[]);
test("#let f(x, y) = f", &[]);
test("#let f = (x, y) => f", &["f"]);
// Closure with different kinds of params.
test("#((x, y) => x + z)", &["z"]);
test("#((x: y, z) => x + z)", &["y"]);
test("#((..x) => x + y)", &["y"]);
test("#((x, y: x + z) => x + y)", &["x", "z"]);
test("#{x => x; x}", &["x"]);
// Show rule.
test("#show y: x => x", &["y"]);
test("#show y: x => x + z", &["y", "z"]);
test("#show x: x => x", &["x"]);
// For loop.
test("#for x in y { x + z }", &["y", "z"]);
test("#for (x, y) in y { x + y }", &["y"]);
test("#for x in y {} #x", &["x", "y"]);
// Import.
test("#import z: x, y", &["z"]);
test("#import x + y: x, y, z", &["x", "y"]);
// Blocks.
test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
test("#[#let x = 1]#x", &["x"]);
// Field access.
test("#foo(body: 1)", &[]);
test("#(body: 1)", &[]);
test("#(body = 1)", &[]);
test("#(body += y)", &["y"]);
test("#{ (body, a) = (y, 1) }", &["y"]);
test("#(x.at(y) = 5)", &["x", "y"])
}
}

View File

@ -2,7 +2,7 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError
use ecow::{eco_format, EcoString};
use crate::eval::{cast, func, repr, scope, ty, Repr, Str, Value};
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value};
/// A whole number.
///

View File

@ -1,8 +1,6 @@
use std::fmt::Debug;
use ecow::{eco_format, EcoString};
use crate::eval::{func, scope, ty, Repr};
use crate::foundations::{func, scope, ty, Repr};
use crate::util::PicoStr;
/// A label for an element.

View File

@ -1,19 +1,9 @@
//! Handles special built-in methods on values.
use crate::diag::{At, SourceResult};
use crate::eval::{Args, Array, Dict, Str, Type, Value};
use crate::foundations::{Args, Array, Dict, Str, Type, Value};
use crate::syntax::Span;
/// Whether a specific method is mutating.
pub fn is_mutating(method: &str) -> bool {
matches!(method, "push" | "pop" | "insert" | "remove")
}
/// Whether a specific method is an accessor.
pub fn is_accessor(method: &str) -> bool {
matches!(method, "first" | "last" | "at")
}
/// List the available methods for a type and whether they take arguments.
pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] {
if ty == Type::of::<Array>() {
@ -33,8 +23,18 @@ pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] {
}
}
/// Whether a specific method is mutating.
pub(crate) fn is_mutating_method(method: &str) -> bool {
matches!(method, "push" | "pop" | "insert" | "remove")
}
/// Whether a specific method is an accessor.
pub(crate) fn is_accessor_method(method: &str) -> bool {
matches!(method, "first" | "last" | "at")
}
/// Call a mutating method on a value.
pub fn call_mut(
pub(crate) fn call_method_mut(
value: &mut Value,
method: &str,
mut args: Args,
@ -76,7 +76,7 @@ pub fn call_mut(
}
/// Call an accessor method on a value.
pub fn call_access<'a>(
pub(crate) fn call_method_access<'a>(
value: &'a mut Value,
method: &str,
mut args: Args,

View File

@ -1,16 +1,93 @@
use typst::eval::{
Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, Repr, Version,
//! Foundational types and functions.
pub mod calc;
pub mod repr;
pub mod sys;
mod args;
mod array;
mod auto;
mod bool;
mod bytes;
mod cast;
mod content;
mod datetime;
mod dict;
mod duration;
mod element;
mod fields;
mod float;
mod func;
mod int;
mod label;
mod methods;
mod module;
mod none;
mod plugin;
mod scope;
mod selector;
mod str;
mod styles;
mod ty;
mod value;
mod version;
pub use self::args::*;
pub use self::array::*;
pub use self::auto::*;
pub use self::bytes::*;
pub use self::cast::*;
pub use self::content::*;
pub use self::datetime::*;
pub use self::dict::*;
pub use self::duration::*;
pub use self::element::*;
pub use self::fields::*;
pub use self::float::*;
pub use self::func::*;
pub use self::int::*;
pub use self::label::*;
pub use self::methods::*;
pub use self::module::*;
pub use self::none::*;
pub use self::plugin::*;
pub use self::repr::Repr;
pub use self::scope::*;
pub use self::selector::*;
pub use self::str::*;
pub use self::styles::*;
pub use self::ty::*;
pub use self::value::*;
pub use self::version::*;
#[doc(hidden)]
pub use {
ecow::{eco_format, eco_vec},
indexmap::IndexMap,
once_cell::sync::Lazy,
};
use crate::prelude::*;
use ecow::EcoString;
/// Hook up all foundational definitions.
use crate::diag::{bail, SourceResult, StrResult};
use crate::eval::{EvalMode, Vm};
use crate::syntax::Spanned;
/// Foundational types and functions.
///
/// Here, you'll find documentation for basic data types like [integers]($int)
/// and [strings]($str) as well as details about core computational functions.
#[category]
pub static FOUNDATIONS: Category;
/// Hook up all `foundations` definitions.
pub(super) fn define(global: &mut Scope) {
global.category("foundations");
global.category(FOUNDATIONS);
global.define_type::<bool>();
global.define_type::<i64>();
global.define_type::<f64>();
global.define_type::<Str>();
global.define_type::<Label>();
global.define_type::<Bytes>();
global.define_type::<Content>();
global.define_type::<Array>();
@ -20,38 +97,18 @@ pub(super) fn define(global: &mut Scope) {
global.define_type::<Type>();
global.define_type::<Module>();
global.define_type::<Regex>();
global.define_type::<Selector>();
global.define_type::<Datetime>();
global.define_type::<Duration>();
global.define_type::<Version>();
global.define_type::<Plugin>();
global.define_func::<repr>();
global.define_func::<repr::repr>();
global.define_func::<panic>();
global.define_func::<assert>();
global.define_func::<eval>();
}
/// Returns the string representation of a value.
///
/// When inserted into content, most values are displayed as this representation
/// in monospace with syntax-highlighting. The exceptions are `{none}`,
/// integers, floats, strings, content, and functions.
///
/// **Note:** This function is for debugging purposes. Its output should not be
/// considered stable and may change at any time!
///
/// # Example
/// ```example
/// #none vs #repr(none) \
/// #"hello" vs #repr("hello") \
/// #(1, 2) vs #repr((1, 2)) \
/// #[*Hi*] vs #repr([*Hi*])
/// ```
#[func(title = "Representation")]
pub fn repr(
/// The value whose string representation to produce.
value: Value,
) -> Str {
value.repr().into()
global.define_func::<style>();
global.define_module(calc::module());
global.define_module(sys::module());
}
/// Fails with an error.
@ -232,5 +289,5 @@ pub fn eval(
for (key, value) in dict {
scope.define(key, value);
}
typst::eval::eval_string(vm.world(), &text, span, mode, scope)
crate::eval::eval_string(vm.world(), &text, span, mode, scope)
}

View File

@ -4,7 +4,7 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::eval::{ty, Content, Scope, Value};
use crate::foundations::{repr, ty, Content, Scope, Value};
/// An evaluated module, either built-in or resulting from a file.
///
@ -110,7 +110,7 @@ impl Debug for Module {
}
}
impl super::Repr for Module {
impl repr::Repr for Module {
fn repr(&self) -> EcoString {
eco_format!("<module {}>", self.name())
}

View File

@ -1,10 +1,12 @@
use ecow::EcoString;
use std::fmt::{self, Debug, Formatter};
use ecow::EcoString;
use serde::{Serialize, Serializer};
use crate::diag::StrResult;
use crate::eval::{cast, ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value};
use crate::foundations::{
cast, ty, CastInfo, FromValue, IntoValue, Reflect, Repr, Type, Value,
};
/// A value that indicates the absence of any other value.
///

View File

@ -1,12 +1,13 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
use ecow::{eco_format, EcoString};
use std::sync::{Arc, Mutex};
use wasmi::{AsContext, AsContextMut, Caller, Engine, Linker, Module};
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::eval::{func, scope, ty, Bytes, Vm};
use crate::eval::Vm;
use crate::foundations::{func, repr, scope, ty, Bytes};
use crate::syntax::Spanned;
use crate::World;
@ -303,7 +304,7 @@ impl Debug for Plugin {
}
}
impl super::Repr for Plugin {
impl repr::Repr for Plugin {
fn repr(&self) -> EcoString {
"plugin(..)".into()
}

View File

@ -1,7 +1,36 @@
//! Debug representation of values.
use ecow::{eco_format, EcoString};
use crate::foundations::{func, Str, Value};
/// The Unicode minus sign.
pub const MINUS_SIGN: &str = "\u{2212}";
/// Returns the string representation of a value.
///
/// When inserted into content, most values are displayed as this representation
/// in monospace with syntax-highlighting. The exceptions are `{none}`,
/// integers, floats, strings, content, and functions.
///
/// **Note:** This function is for debugging purposes. Its output should not be
/// considered stable and may change at any time!
///
/// # Example
/// ```example
/// #none vs #repr(none) \
/// #"hello" vs #repr("hello") \
/// #(1, 2) vs #repr((1, 2)) \
/// #[*Hi*] vs #repr([*Hi*])
/// ```
#[func(title = "Representation")]
pub fn repr(
/// The value whose string representation to produce.
value: Value,
) -> Str {
value.repr().into()
}
/// A trait that defines the `repr` of a Typst value.
pub trait Repr {
/// Return the debug representation of the value.

View File

@ -5,10 +5,15 @@ use ecow::{eco_format, EcoString};
use indexmap::IndexMap;
use crate::diag::{bail, HintedStrResult, HintedString, StrResult};
use crate::eval::{
Func, IntoValue, Library, Module, NativeFunc, NativeFuncData, NativeType, Type, Value,
use crate::foundations::{
Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData,
NativeType, Type, Value,
};
use crate::model::{Element, NativeElement};
use crate::util::Static;
use crate::Library;
#[doc(inline)]
pub use typst_macros::category;
/// A stack of scopes.
#[derive(Debug, Default, Clone)]
@ -97,7 +102,7 @@ fn unknown_variable(var: &str) -> HintedString {
pub struct Scope {
map: IndexMap<EcoString, Slot>,
deduplicate: bool,
category: Option<&'static str>,
category: Option<Category>,
}
impl Scope {
@ -112,8 +117,8 @@ impl Scope {
}
/// Enter a new category.
pub fn category(&mut self, name: &'static str) {
self.category = Some(name);
pub fn category(&mut self, category: Category) {
self.category = Some(category);
}
/// Reset the category.
@ -185,7 +190,7 @@ impl Scope {
}
/// Get the category of a definition.
pub fn get_category(&self, var: &str) -> Option<&'static str> {
pub fn get_category(&self, var: &str) -> Option<Category> {
self.map.get(var)?.category
}
@ -215,6 +220,15 @@ impl Hash for Scope {
}
}
/// Defines the associated scope of a Rust type.
pub trait NativeScope {
/// The constructor function for the type, if any.
fn constructor() -> Option<&'static NativeFuncData>;
/// Get the associated scope for the type.
fn scope() -> Scope;
}
/// A slot where a value is stored.
#[derive(Clone, Hash)]
struct Slot {
@ -223,7 +237,7 @@ struct Slot {
/// The kind of slot, determines how the value can be accessed.
kind: Kind,
/// The category of the slot.
category: Option<&'static str>,
category: Option<Category>,
}
/// The different kinds of slots.
@ -237,7 +251,7 @@ enum Kind {
impl Slot {
/// Create a new slot.
fn new(value: Value, kind: Kind, category: Option<&'static str>) -> Self {
fn new(value: Value, kind: Kind, category: Option<Category>) -> Self {
Self { value, kind, category }
}
@ -260,11 +274,42 @@ impl Slot {
}
}
/// Defines the associated scope of a Rust type.
pub trait NativeScope {
/// The constructor function for the type, if any.
fn constructor() -> Option<&'static NativeFuncData>;
/// A group of related definitions.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Category(Static<CategoryData>);
/// Get the associated scope for the type.
fn scope() -> Scope;
impl Category {
/// Create a new category from raw data.
pub const fn from_data(data: &'static CategoryData) -> Self {
Self(Static(data))
}
/// The category's name.
pub fn name(&self) -> &'static str {
self.0.name
}
/// The type's title case name, for use in documentation (e.g. `String`).
pub fn title(&self) -> &'static str {
self.0.title
}
/// Documentation for the category.
pub fn docs(&self) -> &'static str {
self.0.docs
}
}
impl Debug for Category {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Category({})", self.name())
}
}
/// Defines a category.
#[derive(Debug)]
pub struct CategoryData {
pub name: &'static str,
pub title: &'static str,
pub docs: &'static str,
}

View File

@ -1,16 +1,17 @@
use std::any::{Any, TypeId};
use std::fmt::Debug;
use std::sync::Arc;
use ecow::{eco_format, EcoString, EcoVec};
use smallvec::SmallVec;
use crate::diag::{bail, StrResult};
use crate::eval::{
cast, func, item, repr, scope, ty, CastInfo, Dict, FromValue, Func, Reflect, Regex,
Repr, Str, Symbol, Type, Value,
use crate::foundations::{
cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func,
Label, Reflect, Regex, Repr, Str, Type, Value,
};
use crate::model::{Content, Element, Label, Locatable, Location};
use crate::introspection::{Locatable, Location};
use crate::symbols::Symbol;
use crate::text::TextElem;
/// A helper macro to create a field selector used in [`Selector::Elem`]
///
@ -25,12 +26,12 @@ macro_rules! __select_where {
let mut fields = ::smallvec::SmallVec::new();
$(
fields.push((
<$ty as $crate::model::ElementFields>::Fields::$field as u8,
$crate::eval::IntoValue::into_value($value),
<$ty as $crate::foundations::ElementFields>::Fields::$field as u8,
$crate::foundations::IntoValue::into_value($value),
));
)*
$crate::model::Selector::Elem(
<$ty as $crate::model::NativeElement>::elem(),
$crate::foundations::Selector::Elem(
<$ty as $crate::foundations::NativeElement>::elem(),
Some(fields),
)
}};
@ -138,10 +139,9 @@ impl Selector {
.all(|(id, value)| target.get(*id).as_ref() == Some(value))
}
Self::Label(label) => target.label() == Some(*label),
Self::Regex(regex) => {
target.func() == item!(text_elem)
&& item!(text_str)(target).map_or(false, |text| regex.is_match(text))
}
Self::Regex(regex) => target
.to::<TextElem>()
.map_or(false, |elem| regex.is_match(elem.text())),
Self::Can(cap) => target.func().can_type_id(*cap),
Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)),
Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)),

View File

@ -8,12 +8,12 @@ use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::eval::{
cast, dict, func, repr, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Repr,
Type, Value, Version, Vm,
use crate::eval::Vm;
use crate::foundations::{
cast, dict, func, repr, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Label,
Repr, Type, Value, Version,
};
use crate::geom::Align;
use crate::model::Label;
use crate::layout::Align;
use crate::syntax::{Span, Spanned};
/// Create a new [`Str`] from a format string.
@ -21,7 +21,7 @@ use crate::syntax::{Span, Spanned};
#[doc(hidden)]
macro_rules! __format_str {
($($tts:tt)*) => {{
$crate::eval::Str::from($crate::eval::eco_format!($($tts)*))
$crate::foundations::Str::from($crate::foundations::eco_format!($($tts)*))
}};
}

View File

@ -2,9 +2,7 @@ use std::any::Any;
use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::iter;
use std::mem;
use std::ptr;
use std::{iter, mem, ptr};
use comemo::Prehashed;
use ecow::{eco_vec, EcoString, EcoVec};
@ -12,9 +10,58 @@ use once_cell::sync::Lazy;
use smallvec::SmallVec;
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::eval::{cast, ty, Args, Func, Repr, Value, Vm};
use crate::model::{Content, Element, NativeElement, Selector, Vt};
use crate::eval::Vm;
use crate::foundations::{
cast, elem, func, ty, Args, Content, Element, Func, NativeElement, Repr, Selector,
Show, Value,
};
use crate::layout::Vt;
use crate::syntax::Span;
use crate::text::{FontFamily, FontList, TextElem};
/// Provides access to active styles.
///
/// The styles are currently opaque and only useful in combination with the
/// [`measure`]($measure) function. See its documentation for more details. In
/// the future, the provided styles might also be directly accessed to look up
/// styles defined by [set rules]($styling/#set-rules).
///
/// ```example
/// #let thing(body) = style(styles => {
/// let size = measure(body, styles)
/// [Width of "#body" is #size.width]
/// })
///
/// #thing[Hey] \
/// #thing[Welcome]
/// ```
#[func]
pub fn style(
/// A function to call with the styles. Its return value is displayed
/// in the document.
///
/// This function is called once for each time the content returned by
/// `style` appears in the document. That makes it possible to generate
/// content that depends on the style context it appears in.
func: Func,
) -> Content {
StyleElem::new(func).pack()
}
/// Executes a style access.
#[elem(Show)]
struct StyleElem {
/// The function to call with the styles.
#[required]
func: Func,
}
impl Show for StyleElem {
#[tracing::instrument(name = "StyleElem::show", skip_all)]
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
Ok(self.func().call_vt(vt, [styles.to_map()])?.display())
}
}
/// A list of style properties.
#[ty]
@ -83,6 +130,16 @@ impl Styles {
Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)),
})
}
/// Set a font family composed of a preferred family and existing families
/// from a style chain.
pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
self.set(TextElem::set_font(FontList(
std::iter::once(preferred)
.chain(TextElem::font_in(existing).into_iter().cloned())
.collect(),
)));
}
}
impl From<Style> for Styles {
@ -279,7 +336,7 @@ pub struct Recipe {
/// Determines whether the recipe applies to an element.
pub selector: Option<Selector>,
/// The transformation to perform on the match.
pub transform: Transform,
pub transform: Transformation,
}
impl Recipe {
@ -301,8 +358,8 @@ impl Recipe {
/// Apply the recipe to the given content.
pub fn apply_vm(&self, vm: &mut Vm, content: Content) -> SourceResult<Content> {
match &self.transform {
Transform::Content(content) => Ok(content.clone()),
Transform::Func(func) => {
Transformation::Content(content) => Ok(content.clone()),
Transformation::Func(func) => {
let args = Args::new(self.span, [Value::Content(content.clone())]);
let mut result = func.call_vm(vm, args);
// For selector-less show rules, a tracepoint makes no sense.
@ -312,15 +369,15 @@ impl Recipe {
}
Ok(result?.display())
}
Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())),
Transformation::Style(styles) => Ok(content.styled_with_map(styles.clone())),
}
}
/// Apply the recipe to the given content.
pub fn apply_vt(&self, vt: &mut Vt, content: Content) -> SourceResult<Content> {
match &self.transform {
Transform::Content(content) => Ok(content.clone()),
Transform::Func(func) => {
Transformation::Content(content) => Ok(content.clone()),
Transformation::Func(func) => {
let mut result = func.call_vt(vt, [Value::Content(content.clone())]);
if self.selector.is_some() {
let point = || Tracepoint::Show(content.func().name().into());
@ -328,7 +385,7 @@ impl Recipe {
}
Ok(result?.display())
}
Transform::Style(styles) => Ok(content.styled_with_map(styles.clone())),
Transformation::Style(styles) => Ok(content.styled_with_map(styles.clone())),
}
}
}
@ -346,7 +403,7 @@ impl Debug for Recipe {
/// A show rule transformation that can be applied to a match.
#[derive(Clone, PartialEq, Hash)]
pub enum Transform {
pub enum Transformation {
/// Replacement content.
Content(Content),
/// A function to apply to the match.
@ -355,7 +412,7 @@ pub enum Transform {
Style(Styles),
}
impl Debug for Transform {
impl Debug for Transformation {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Content(content) => content.fmt(f),
@ -366,7 +423,7 @@ impl Debug for Transform {
}
cast! {
Transform,
Transformation,
content: Content => Self::Content(content),
func: Func => Self::Func(func),
}

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