Split crates

This commit is contained in:
Laurenz 2022-11-03 11:44:53 +01:00
parent 56342bd972
commit 37a7afddfa
93 changed files with 1542 additions and 1357 deletions

61
Cargo.lock generated
View File

@ -1110,49 +1110,78 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytemuck", "bytemuck",
"chrono",
"codespan-reporting",
"comemo", "comemo",
"csv",
"dirs",
"elsa", "elsa",
"flate2", "flate2",
"hypher",
"iai", "iai",
"image", "image",
"kurbo",
"lipsum",
"memmap2",
"miniz_oxide", "miniz_oxide",
"notify",
"once_cell", "once_cell",
"pdf-writer", "pdf-writer",
"pico-args",
"pixglyph", "pixglyph",
"regex", "regex",
"resvg", "resvg",
"rex", "rex",
"roxmltree", "roxmltree",
"rustybuzz", "rustybuzz",
"same-file",
"serde", "serde",
"serde_json",
"siphasher", "siphasher",
"subsetter", "subsetter",
"svg2pdf", "svg2pdf",
"syntect", "syntect",
"tiny-skia", "tiny-skia",
"ttf-parser 0.17.1", "ttf-parser 0.17.1",
"typed-arena", "typst-library",
"typst-macros", "typst-macros",
"unicode-bidi",
"unicode-math",
"unicode-script",
"unicode-segmentation", "unicode-segmentation",
"unicode-xid", "unicode-xid",
"unscanny", "unscanny",
"usvg", "usvg",
"walkdir", "walkdir",
]
[[package]]
name = "typst-cli"
version = "0.1.0"
dependencies = [
"chrono",
"codespan-reporting",
"comemo",
"dirs",
"elsa",
"memmap2",
"notify",
"once_cell",
"pico-args",
"same-file",
"siphasher",
"typst",
"typst-library",
"walkdir",
]
[[package]]
name = "typst-library"
version = "0.1.0"
dependencies = [
"comemo",
"csv",
"hypher",
"kurbo",
"lipsum",
"once_cell",
"rex",
"roxmltree",
"rustybuzz",
"serde_json",
"syntect",
"ttf-parser 0.17.1",
"typed-arena",
"typst",
"unicode-bidi",
"unicode-math",
"unicode-script",
"unscanny",
"xi-unicode", "xi-unicode",
] ]

View File

@ -5,97 +5,47 @@ authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
[workspace] [workspace]
members = ["macros"] members = ["cli", "library", "macros"]
[dependencies] [dependencies]
# Workspace typst-macros = { path = "macros" }
typst-macros = { path = "./macros" }
# Utilities
bitflags = "1" bitflags = "1"
bytemuck = "1" bytemuck = "1"
comemo = "0.1" comemo = "0.1"
flate2 = "1"
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
miniz_oxide = "0.5"
once_cell = "1" once_cell = "1"
pdf-writer = "0.6"
pixglyph = { git = "https://github.com/typst/pixglyph" }
regex = "1" regex = "1"
resvg = { version = "0.22", default-features = false }
rex = { git = "https://github.com/laurmaedje/ReX" }
roxmltree = "0.14"
rustybuzz = "0.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
siphasher = "0.3" siphasher = "0.3"
typed-arena = "2"
unscanny = "0.1"
# Text and font handling
hypher = "0.1"
kurbo = "0.8"
rustybuzz = "0.5"
ttf-parser = "0.17"
unicode-bidi = "0.3.5"
unicode-script = "0.5"
unicode-segmentation = "1"
unicode-xid = "0.2"
xi-unicode = "0.3"
# Raster and vector graphics handling
image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] }
usvg = { version = "0.22", default-features = false }
# External implementation of user-facing features
csv = "1"
lipsum = { git = "https://github.com/reknih/lipsum" }
rex = { git = "https://github.com/laurmaedje/ReX" }
serde_json = "1"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
unicode-math = { git = "https://github.com/s3bk/unicode-math/" }
# PDF export
miniz_oxide = "0.5"
pdf-writer = "0.6"
subsetter = "0.1" subsetter = "0.1"
svg2pdf = "0.4" svg2pdf = "0.4"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
# Rendering
flate2 = "1"
pixglyph = { git = "https://github.com/typst/pixglyph" }
resvg = { version = "0.22", default-features = false }
roxmltree = "0.14"
tiny-skia = "0.6.2" tiny-skia = "0.6.2"
ttf-parser = "0.17"
# Command line interface unicode-segmentation = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true } unicode-xid = "0.2"
codespan-reporting = { version = "0.11", optional = true } unscanny = "0.1"
dirs = { version = "4", optional = true } usvg = { version = "0.22", default-features = false }
elsa = { version = "1.7", optional = true }
memmap2 = { version = "0.5", optional = true }
notify = { version = "5", optional = true }
pico-args = { version = "0.4", optional = true }
same-file = { version = "1", optional = true }
walkdir = { version = "2", optional = true }
[dev-dependencies] [dev-dependencies]
typst-library = { path = "library" }
iai = { git = "https://github.com/reknih/iai" } iai = { git = "https://github.com/reknih/iai" }
elsa = "1.7" elsa = "1.7"
walkdir = "2" walkdir = "2"
[features]
cli = [
"chrono",
"codespan-reporting",
"dirs",
"elsa",
"memmap2",
"notify",
"pico-args",
"same-file",
"walkdir",
]
[profile.dev] [profile.dev]
debug = 0 # Faster compilation debug = 0
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 2 # Faster test execution opt-level = 2
[[bin]]
name = "typst"
required-features = ["cli"]
[[test]] [[test]]
name = "typeset" name = "typeset"

View File

@ -1,4 +1,4 @@
use std::path::Path; use std::path::{Path, PathBuf};
use comemo::{Prehashed, Track, Tracked}; use comemo::{Prehashed, Track, Tracked};
use iai::{black_box, main, Iai}; use iai::{black_box, main, Iai};
@ -20,7 +20,7 @@ main!(
bench_parse, bench_parse,
bench_edit, bench_edit,
bench_eval, bench_eval,
bench_layout, bench_typeset,
bench_highlight, bench_highlight,
bench_render, bench_render,
); );
@ -80,12 +80,10 @@ fn bench_eval(iai: &mut Iai) {
iai.run(|| typst::model::eval(world.track(), route.track(), id).unwrap()); iai.run(|| typst::model::eval(world.track(), route.track(), id).unwrap());
} }
fn bench_layout(iai: &mut Iai) { fn bench_typeset(iai: &mut Iai) {
let world = BenchWorld::new(); let world = BenchWorld::new();
let id = world.source.id(); let id = world.source.id();
let route = typst::model::Route::default(); iai.run(|| typst::typeset(&world, id));
let module = typst::model::eval(world.track(), route.track(), id).unwrap();
iai.run(|| typst::library::layout::Layout::layout(&module.content, world.track()));
} }
fn bench_render(iai: &mut Iai) { fn bench_render(iai: &mut Iai) {
@ -104,7 +102,13 @@ struct BenchWorld {
impl BenchWorld { impl BenchWorld {
fn new() -> Self { fn new() -> Self {
let config = Config::default(); let config = Config {
root: PathBuf::new(),
scope: typst_library::scope(),
styles: typst_library::styles(),
items: typst_library::items(),
};
let font = Font::new(FONT.into(), 0).unwrap(); let font = Font::new(FONT.into(), 0).unwrap();
let book = FontBook::from_fonts([&font]); let book = FontBook::from_fonts([&font]);
let id = SourceId::from_u16(0); let id = SourceId::from_u16(0);

26
cli/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "typst-cli"
version = "0.1.0"
authors = ["The Typst Project Developers"]
edition = "2021"
[[bin]]
name = "typst"
path = "src/main.rs"
doc = false
[dependencies]
typst = { path = ".." }
typst-library = { path = "../library" }
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
codespan-reporting = "0.11"
comemo = "0.1"
dirs = "4"
elsa = "1.7"
memmap2 = "0.5"
notify = "5"
once_cell = "1"
pico-args = "0.4"
same-file = "1"
siphasher = "0.3"
walkdir = "2"

View File

@ -174,13 +174,20 @@ fn dispatch(command: Command) -> StrResult<()> {
/// Execute a typesetting command. /// Execute a typesetting command.
fn typeset(command: TypesetCommand) -> StrResult<()> { fn typeset(command: TypesetCommand) -> StrResult<()> {
let mut config = Config::default(); let root = if let Some(root) = &command.root {
if let Some(root) = &command.root { root.clone()
config.root = root.clone();
} else if let Some(dir) = command.input.parent() { } else if let Some(dir) = command.input.parent() {
config.root = dir.into(); dir.into()
} } else {
PathBuf::new()
};
let config = Config {
root,
scope: typst_library::scope(),
styles: typst_library::styles(),
items: typst_library::items(),
};
// Create the world that serves sources, fonts and files. // Create the world that serves sources, fonts and files.
let mut world = SystemWorld::new(config); let mut world = SystemWorld::new(config);

26
library/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "typst-library"
version = "0.1.0"
authors = ["The Typst Project Developers"]
edition = "2021"
[dependencies]
typst = { path = ".." }
comemo = "0.1"
csv = "1"
hypher = "0.1"
kurbo = "0.8"
lipsum = { git = "https://github.com/reknih/lipsum" }
once_cell = "1"
rex = { git = "https://github.com/laurmaedje/ReX" }
roxmltree = "0.14"
rustybuzz = "0.5"
serde_json = "1"
syntect = { version = "5", default-features = false, features = ["default-syntaxes", "regex-fancy"] }
ttf-parser = "0.17"
typed-arena = "2"
unicode-bidi = "0.3.5"
unicode-math = { git = "https://github.com/s3bk/unicode-math/" }
unicode-script = "0.5"
unscanny = "0.1"
xi-unicode = "0.3"

View File

@ -1,5 +1,5 @@
use super::*; use super::*;
use crate::library::prelude::*; use crate::prelude::*;
/// Additional methods on content. /// Additional methods on content.
pub trait ContentExt { pub trait ContentExt {
@ -19,7 +19,7 @@ pub trait ContentExt {
fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self; fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self;
/// Set alignments for this content. /// Set alignments for this content.
fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self; fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
/// Pad this content at the sides. /// Pad this content at the sides.
fn padded(self, padding: Sides<Rel<Length>>) -> Self; fn padded(self, padding: Sides<Rel<Length>>) -> Self;
@ -83,7 +83,7 @@ impl ContentExt for Content {
layout::BoxNode { sizing, child: self }.pack() layout::BoxNode { sizing, child: self }.pack()
} }
fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self { fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
layout::AlignNode { aligns, child: self }.pack() layout::AlignNode { aligns, child: self }.pack()
} }
@ -115,9 +115,11 @@ impl StyleMapExt for StyleMap {
fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) { fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) {
self.set( self.set(
text::TextNode::FAMILY, text::TextNode::FAMILY,
FallbackList(
std::iter::once(preferred) std::iter::once(preferred)
.chain(existing.get(text::TextNode::FAMILY).iter().cloned()) .chain(existing.get(text::TextNode::FAMILY).0.iter().cloned())
.collect(), .collect(),
),
); );
} }
} }

View File

@ -1,4 +1,4 @@
use crate::library::prelude::*; use crate::prelude::*;
/// Hide content without affecting layout. /// Hide content without affecting layout.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,8 +1,9 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat}; use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::library::prelude::*;
use crate::library::text::TextNode; use crate::prelude::*;
use crate::text::TextNode;
/// Show a raster or vector graphic. /// Show a raster or vector graphic.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,4 +1,4 @@
use crate::library::prelude::*; use crate::prelude::*;
/// Display a line without affecting the layout. /// Display a line without affecting the layout.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -13,7 +13,7 @@ pub struct LineNode {
impl LineNode { impl LineNode {
/// How to stroke the line. /// How to stroke the line.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const STROKE: RawStroke = RawStroke::default(); pub const STROKE: PartialStroke = PartialStroke::default();
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let origin = args.named("origin")?.unwrap_or_default(); let origin = args.named("origin")?.unwrap_or_default();
@ -66,15 +66,3 @@ impl LayoutInline for LineNode {
Ok(vec![frame]) Ok(vec![frame])
} }
} }
castable! {
Axes<Rel<Length>>,
Expected: "array of two relative lengths",
Value::Array(array) => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => Err("point array must contain exactly two entries")?,
}
},
}

View File

@ -1,7 +1,7 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::TextNode; use crate::text::TextNode;
/// A sizable and fillable shape with optional content. /// A sizable and fillable shape with optional content.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -25,7 +25,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
pub const FILL: Option<Paint> = None; pub const FILL: Option<Paint> = None;
/// How to stroke the shape. /// How to stroke the shape.
#[property(skip, resolve, fold)] #[property(skip, resolve, fold)]
pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto; pub const STROKE: Smart<Sides<Option<PartialStroke>>> = Smart::Auto;
/// How much to pad the shape's content. /// How much to pad the shape's content.
#[property(resolve, fold)] #[property(resolve, fold)]
@ -62,7 +62,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
if is_round(S) { if is_round(S) {
styles.set_opt( styles.set_opt(
Self::STROKE, Self::STROKE,
args.named::<Smart<Option<RawStroke>>>("stroke")? args.named::<Smart<Option<PartialStroke>>>("stroke")?
.map(|some| some.map(Sides::splat)), .map(|some| some.map(Sides::splat)),
); );
} else { } else {
@ -140,7 +140,7 @@ impl<const S: ShapeKind> LayoutInline for ShapeNode<S> {
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())), Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
Smart::Auto => Sides::splat(None), Smart::Auto => Sides::splat(None),
Smart::Custom(strokes) => { Smart::Custom(strokes) => {
strokes.map(|s| s.map(RawStroke::unwrap_or_default)) strokes.map(|s| s.map(PartialStroke::unwrap_or_default))
} }
}; };

View File

@ -1,11 +1,11 @@
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::{HorizontalAlign, ParNode}; use crate::text::{HorizontalAlign, ParNode};
/// Align content along the layouting axes. /// Align content along the layouting axes.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct AlignNode { pub struct AlignNode {
/// How to align the content horizontally and vertically. /// How to align the content horizontally and vertically.
pub aligns: Axes<Option<RawAlign>>, pub aligns: Axes<Option<GenAlign>>,
/// The content to be aligned. /// The content to be aligned.
pub child: Content, pub child: Content,
} }
@ -13,7 +13,7 @@ pub struct AlignNode {
#[node(LayoutBlock)] #[node(LayoutBlock)]
impl AlignNode { impl AlignNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let aligns: Axes<Option<RawAlign>> = args.find()?.unwrap_or_default(); let aligns: Axes<Option<GenAlign>> = args.find()?.unwrap_or_default();
let body: Content = args.expect("body")?; let body: Content = args.expect("body")?;
if let Axes { x: Some(x), y: None } = aligns { if let Axes { x: Some(x), y: None } = aligns {

View File

@ -1,5 +1,5 @@
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::TextNode; use crate::text::TextNode;
/// Separate a region into multiple equally sized columns. /// Separate a region into multiple equally sized columns.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,4 +1,4 @@
use crate::library::prelude::*; use crate::prelude::*;
/// An inline-level container that sizes content. /// An inline-level container that sizes content.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]

View File

@ -1,8 +1,8 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use super::{AlignNode, PlaceNode, Spacing}; use super::{AlignNode, PlaceNode, Spacing};
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::ParNode; use crate::text::ParNode;
/// Arrange spacing, paragraphs and block-level nodes into a flow. /// Arrange spacing, paragraphs and block-level nodes into a flow.
/// ///
@ -256,7 +256,7 @@ impl FlowLayouter {
/// Finish layouting and return the resulting frames. /// Finish layouting and return the resulting frames.
pub fn finish(mut self) -> Vec<Frame> { pub fn finish(mut self) -> Vec<Frame> {
if self.expand.y { if self.expand.y {
while self.regions.backlog.len() > 0 { while !self.regions.backlog.is_empty() {
self.finish_region(); self.finish_region();
} }
} }

View File

@ -1,4 +1,4 @@
use crate::library::prelude::*; use crate::prelude::*;
/// Arrange content in a grid. /// Arrange content in a grid.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -14,11 +14,11 @@ pub struct GridNode {
#[node(LayoutBlock)] #[node(LayoutBlock)]
impl GridNode { impl GridNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let columns = args.named("columns")?.unwrap_or_default(); let TrackSizings(columns) = args.named("columns")?.unwrap_or_default();
let rows = args.named("rows")?.unwrap_or_default(); let TrackSizings(rows) = args.named("rows")?.unwrap_or_default();
let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default(); let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default();
let column_gutter = args.named("column-gutter")?; let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v);
let row_gutter = args.named("row-gutter")?; let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v);
Ok(Self { Ok(Self {
tracks: Axes::new(columns, rows), tracks: Axes::new(columns, rows),
gutter: Axes::new( gutter: Axes::new(
@ -66,19 +66,26 @@ pub enum TrackSizing {
Fractional(Fr), Fractional(Fr),
} }
/// Track sizing definitions.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TrackSizings(pub Vec<TrackSizing>);
castable! { castable! {
Vec<TrackSizing>, TrackSizings,
Expected: "integer, auto, relative length, fraction, or array of the latter three", Expected: "integer, auto, relative length, fraction, or array of the latter three",
Value::Auto => vec![TrackSizing::Auto], Value::Auto => Self(vec![TrackSizing::Auto]),
Value::Length(v) => vec![TrackSizing::Relative(v.into())], Value::Length(v) => Self(vec![TrackSizing::Relative(v.into())]),
Value::Ratio(v) => vec![TrackSizing::Relative(v.into())], Value::Ratio(v) => Self(vec![TrackSizing::Relative(v.into())]),
Value::Relative(v) => vec![TrackSizing::Relative(v)], Value::Relative(v) => Self(vec![TrackSizing::Relative(v)]),
Value::Fraction(v) => vec![TrackSizing::Fractional(v)], Value::Fraction(v) => Self(vec![TrackSizing::Fractional(v)]),
Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::<NonZeroUsize>()?.get()], Value::Int(v) => Self(vec![
Value::Array(values) => values TrackSizing::Auto;
Value::Int(v).cast::<NonZeroUsize>()?.get()
]),
Value::Array(values) => Self(values
.into_iter() .into_iter()
.filter_map(|v| v.cast().ok()) .filter_map(|v| v.cast().ok())
.collect(), .collect()),
} }
castable! { castable! {

View File

@ -28,19 +28,21 @@ use std::mem;
use comemo::Tracked; use comemo::Tracked;
use typed_arena::Arena; use typed_arena::Arena;
use typst::diag::SourceResult;
use typst::frame::Frame;
use typst::geom::*;
use typst::model::{
capability, Barrier, Content, Node, SequenceNode, Show, StyleChain, StyleEntry,
StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target,
};
use typst::World;
use crate::diag::SourceResult; use crate::structure::{
use crate::frame::Frame; DescNode, DocNode, EnumNode, ListItem, ListNode, DESC, ENUM, LIST,
use crate::geom::*; };
use crate::library::structure::{DocNode, ListItem, ListNode, DESC, ENUM, LIST}; use crate::text::{
use crate::library::text::{
LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode, LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
}; };
use crate::model::{
capability, Barrier, Content, Interruption, Node, SequenceNode, Show, StyleChain,
StyleEntry, StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target,
};
use crate::World;
/// The root-level layout. /// The root-level layout.
#[capability] #[capability]
@ -204,7 +206,7 @@ impl Regions {
/// ///
/// If this is true, calling `next()` will have no effect. /// If this is true, calling `next()` will have no effect.
pub fn in_last(&self) -> bool { pub fn in_last(&self) -> bool {
self.backlog.len() == 0 && self.last.map_or(true, |height| self.first.y == height) self.backlog.is_empty() && self.last.map_or(true, |height| self.first.y == height)
} }
/// Advance to the next region if there is any. /// Advance to the next region if there is any.
@ -255,6 +257,17 @@ struct Scratch<'a> {
templates: Arena<Content>, templates: Arena<Content>,
} }
/// Determines whether a style could interrupt some composable structure.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Interruption {
/// The style forces a list break.
List,
/// The style forces a paragraph break.
Par,
/// The style forces a page break.
Page,
}
impl<'a> Builder<'a> { impl<'a> Builder<'a> {
pub fn new( pub fn new(
world: Tracked<'a, dyn World>, world: Tracked<'a, dyn World>,
@ -303,11 +316,9 @@ impl<'a> Builder<'a> {
return self.styled(styled, styles); return self.styled(styled, styles);
} else if let Some(seq) = content.downcast::<SequenceNode>() { } else if let Some(seq) = content.downcast::<SequenceNode>() {
return self.sequence(seq, styles); return self.sequence(seq, styles);
} else if content.has::<dyn Show>() { } else if content.has::<dyn Show>() && self.show(content, styles)? {
if self.show(&content, styles)? {
return Ok(()); return Ok(());
} }
}
if self.list.accept(content, styles) { if self.list.accept(content, styles) {
return Ok(()); return Ok(());
@ -371,7 +382,19 @@ impl<'a> Builder<'a> {
) -> SourceResult<()> { ) -> SourceResult<()> {
let stored = self.scratch.styles.alloc(styles); let stored = self.scratch.styles.alloc(styles);
let styles = styled.map.chain(stored); let styles = styled.map.chain(stored);
let intr = styled.map.interruption();
let intr = if styled.map.interrupts::<PageNode>() {
Some(Interruption::Page)
} else if styled.map.interrupts::<ParNode>() {
Some(Interruption::Par)
} else if styled.map.interrupts::<ListNode>()
|| styled.map.interrupts::<EnumNode>()
|| styled.map.interrupts::<DescNode>()
{
Some(Interruption::List)
} else {
None
};
if let Some(intr) = intr { if let Some(intr) = intr {
self.interrupt(intr, styles, false)?; self.interrupt(intr, styles, false)?;
@ -396,11 +419,9 @@ impl<'a> Builder<'a> {
mem::take(&mut self.list).finish(self)?; mem::take(&mut self.list).finish(self)?;
} }
if intr >= Interruption::Par { if intr >= Interruption::Par && !self.par.is_empty() {
if !self.par.is_empty() {
mem::take(&mut self.par).finish(self); mem::take(&mut self.par).finish(self);
} }
}
if intr >= Interruption::Page { if intr >= Interruption::Page {
if let Some(doc) = &mut self.doc { if let Some(doc) = &mut self.doc {
@ -471,7 +492,7 @@ impl<'a> FlowBuilder<'a> {
// 4 | generated weak fractional spacing // 4 | generated weak fractional spacing
// 5 | par spacing // 5 | par spacing
if let Some(_) = content.downcast::<ParbreakNode>() { if content.is::<ParbreakNode>() {
/* Nothing to do */ /* Nothing to do */
} else if let Some(colbreak) = content.downcast::<ColbreakNode>() { } else if let Some(colbreak) = content.downcast::<ColbreakNode>() {
if colbreak.weak { if colbreak.weak {

View File

@ -1,4 +1,4 @@
use crate::library::prelude::*; use crate::prelude::*;
/// Pad content at the sides. /// Pad content at the sides.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,7 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use super::ColumnsNode; use super::ColumnsNode;
use crate::library::prelude::*; use crate::prelude::*;
/// Layouts its child onto one or multiple pages. /// Layouts its child onto one or multiple pages.
#[derive(PartialEq, Clone, Hash)] #[derive(PartialEq, Clone, Hash)]

View File

@ -1,5 +1,5 @@
use super::AlignNode; use super::AlignNode;
use crate::library::prelude::*; use crate::prelude::*;
/// Place content at an absolute position. /// Place content at an absolute position.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -8,7 +8,7 @@ pub struct PlaceNode(pub Content);
#[node(LayoutBlock)] #[node(LayoutBlock)]
impl PlaceNode { impl PlaceNode {
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let aligns = args.find()?.unwrap_or(Axes::with_x(Some(RawAlign::Start))); let aligns = args.find()?.unwrap_or(Axes::with_x(Some(GenAlign::Start)));
let dx = args.named("dx")?.unwrap_or_default(); let dx = args.named("dx")?.unwrap_or_default();
let dy = args.named("dy")?.unwrap_or_default(); let dy = args.named("dy")?.unwrap_or_default();
let body = args.expect::<Content>("body")?; let body = args.expect::<Content>("body")?;

View File

@ -1,7 +1,7 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::ParNode; use crate::text::ParNode;
/// Horizontal spacing. /// Horizontal spacing.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]

View File

@ -1,7 +1,8 @@
use typst::model::StyledNode;
use super::{AlignNode, Spacing}; use super::{AlignNode, Spacing};
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::ParNode; use crate::text::ParNode;
use crate::model::StyledNode;
/// Arrange content and spacing along an axis. /// Arrange content and spacing along an axis.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,5 +1,6 @@
use crate::geom::Transform; use typst::geom::Transform;
use crate::library::prelude::*;
use crate::prelude::*;
/// Move content without affecting layout. /// Move content without affecting layout.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -61,7 +62,7 @@ pub type ScaleNode = TransformNode<SCALE>;
impl<const T: TransformKind> TransformNode<T> { impl<const T: TransformKind> TransformNode<T> {
/// The origin of the transformation. /// The origin of the transformation.
#[property(resolve)] #[property(resolve)]
pub const ORIGIN: Axes<Option<RawAlign>> = Axes::default(); pub const ORIGIN: Axes<Option<GenAlign>> = Axes::default();
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let transform = match T { let transform = match T {

View File

@ -1,7 +1,4 @@
//! The standard library. //! Typst's standard library.
//!
//! Call [`scope`] to obtain a [`Scope`] containing all standard library
//! definitions.
pub mod graphics; pub mod graphics;
pub mod layout; pub mod layout;
@ -12,15 +9,13 @@ pub mod text;
pub mod utility; pub mod utility;
mod ext; mod ext;
mod raw;
pub use raw::*; use typst::geom::{Align, Color, Dir, GenAlign};
use typst::model::{LangItems, Node, Scope, StyleMap};
use crate::geom::{Align, Color, Dir}; use self::layout::Layout;
use crate::model::{Node, Scope};
use crate::LangItems;
/// Construct a scope containing all standard library definitions. /// Construct the standard library scope.
pub fn scope() -> Scope { pub fn scope() -> Scope {
let mut std = Scope::new(); let mut std = Scope::new();
@ -140,21 +135,29 @@ pub fn scope() -> Scope {
std.define("rtl", Dir::RTL); std.define("rtl", Dir::RTL);
std.define("ttb", Dir::TTB); std.define("ttb", Dir::TTB);
std.define("btt", Dir::BTT); std.define("btt", Dir::BTT);
std.define("start", RawAlign::Start); std.define("start", GenAlign::Start);
std.define("end", RawAlign::End); std.define("end", GenAlign::End);
std.define("left", RawAlign::Specific(Align::Left)); std.define("left", GenAlign::Specific(Align::Left));
std.define("center", RawAlign::Specific(Align::Center)); std.define("center", GenAlign::Specific(Align::Center));
std.define("right", RawAlign::Specific(Align::Right)); std.define("right", GenAlign::Specific(Align::Right));
std.define("top", RawAlign::Specific(Align::Top)); std.define("top", GenAlign::Specific(Align::Top));
std.define("horizon", RawAlign::Specific(Align::Horizon)); std.define("horizon", GenAlign::Specific(Align::Horizon));
std.define("bottom", RawAlign::Specific(Align::Bottom)); std.define("bottom", GenAlign::Specific(Align::Bottom));
std std
} }
/// Construct the language map. /// Construct the standard style map.
pub fn styles() -> StyleMap {
StyleMap::new()
}
/// Construct the standard lang item mapping.
pub fn items() -> LangItems { pub fn items() -> LangItems {
LangItems { LangItems {
root: |world, content| content.layout(world),
em: |styles| styles.get(text::TextNode::SIZE),
dir: |styles| styles.get(text::TextNode::DIR),
space: || text::SpaceNode.pack(), space: || text::SpaceNode.pack(),
linebreak: |justify| text::LinebreakNode { justify }.pack(), linebreak: |justify| text::LinebreakNode { justify }.pack(),
text: |text| text::TextNode(text).pack(), text: |text| text::TextNode(text).pack(),
@ -179,5 +182,10 @@ pub fn items() -> LangItems {
desc_item: |term, body| { desc_item: |term, body| {
structure::ListItem::Desc(Box::new(structure::DescItem { term, body })).pack() structure::ListItem::Desc(Box::new(structure::DescItem { term, body })).pack()
}, },
math: |children, display| math::MathNode { children, display }.pack(),
math_atom: |atom| math::AtomNode(atom).pack(),
math_script: |base, sub, sup| math::ScriptNode { base, sub, sup }.pack(),
math_frac: |num, denom| math::FracNode { num, denom }.pack(),
math_align: |count| math::AlignNode(count).pack(),
} }
} }

188
library/src/math/mod.rs Normal file
View File

@ -0,0 +1,188 @@
//! Mathematical formulas.
mod tex;
use std::fmt::Write;
use self::tex::{layout_tex, Texify};
use crate::layout::BlockSpacing;
use crate::prelude::*;
use crate::text::FontFamily;
/// A piece of a mathematical formula.
#[derive(Debug, Clone, Hash)]
pub struct MathNode {
/// The pieces of the formula.
pub children: Vec<Content>,
/// Whether the formula is display-level.
pub display: bool,
}
#[node(Show, LayoutInline, Texify)]
impl MathNode {
/// The math font family.
#[property(referenced)]
pub const FAMILY: FontFamily = FontFamily::new("NewComputerModernMath");
/// The spacing above display math.
#[property(resolve, shorthand(around))]
pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
/// The spacing below display math.
#[property(resolve, shorthand(around))]
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
}
impl Show for MathNode {
fn unguard_parts(&self, _: Selector) -> Content {
self.clone().pack()
}
fn field(&self, _: &str) -> Option<Value> {
None
}
fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
Ok(if self.display {
self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into())))
} else {
self.clone().pack()
})
}
fn finalize(
&self,
_: Tracked<dyn World>,
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
Ok(if self.display {
realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))
} else {
realized
})
}
}
impl LayoutInline for MathNode {
fn layout_inline(
&self,
world: Tracked<dyn World>,
_: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
Ok(vec![layout_tex(
&self.texify(),
self.display,
world,
styles,
)?])
}
}
impl Texify for MathNode {
fn texify(&self) -> EcoString {
self.children.iter().map(Texify::texify).collect()
}
}
/// An atom in a math formula: `x`, `+`, `12`.
#[derive(Debug, Hash)]
pub struct AtomNode(pub EcoString);
#[node(Texify)]
impl AtomNode {}
impl Texify for AtomNode {
fn texify(&self) -> EcoString {
self.0.chars().map(escape_char).collect()
}
}
/// A fraction in a mathematical formula.
#[derive(Debug, Hash)]
pub struct FracNode {
/// The numerator.
pub num: Content,
/// The denominator.
pub denom: Content,
}
#[node(Texify)]
impl FracNode {}
impl Texify for FracNode {
fn texify(&self) -> EcoString {
format_eco!(
"\\frac{{{}}}{{{}}}",
unparen(self.num.texify()),
unparen(self.denom.texify())
)
}
}
/// A sub- and/or superscript in a mathematical formula.
#[derive(Debug, Hash)]
pub struct ScriptNode {
/// The base.
pub base: Content,
/// The subscript.
pub sub: Option<Content>,
/// The superscript.
pub sup: Option<Content>,
}
#[node(Texify)]
impl ScriptNode {}
impl Texify for ScriptNode {
fn texify(&self) -> EcoString {
let mut tex = self.base.texify();
if let Some(sub) = &self.sub {
write!(tex, "_{{{}}}", unparen(sub.texify())).unwrap();
}
if let Some(sup) = &self.sup {
write!(tex, "^{{{}}}", unparen(sup.texify())).unwrap();
}
tex
}
}
/// A math alignment indicator: `&`, `&&`.
#[derive(Debug, Hash)]
pub struct AlignNode(pub usize);
#[node(Texify)]
impl AlignNode {}
impl Texify for AlignNode {
fn texify(&self) -> EcoString {
EcoString::new()
}
}
/// Escape a char for TeX usage.
#[rustfmt::skip]
fn escape_char(c: char) -> EcoString {
match c {
'{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "),
'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' |
'*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' |
':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(),
c => unicode_math::SYMBOLS
.iter()
.find(|sym| sym.codepoint == c)
.map(|sym| format_eco!("\\{} ", sym.name))
.unwrap_or_default(),
}
}
/// Trim grouping parenthesis≤.
fn unparen(s: EcoString) -> EcoString {
if s.starts_with('(') && s.ends_with(')') {
s[1 .. s.len() - 1].into()
} else {
s
}
}

164
library/src/math/tex.rs Normal file
View File

@ -0,0 +1,164 @@
use rex::error::{Error, LayoutError};
use rex::font::FontContext;
use rex::layout::{LayoutSettings, Style};
use rex::parser::color::RGBA;
use rex::render::{Backend, Cursor, Renderer};
use typst::font::Font;
use super::*;
use crate::prelude::*;
use crate::text::{variant, LinebreakNode, SpaceNode, TextNode};
/// Turn a math node into TeX math code.
#[capability]
pub trait Texify: 'static + Sync + Send {
/// Perform the conversion.
fn texify(&self) -> EcoString;
}
impl Texify for Content {
fn texify(&self) -> EcoString {
if self.is::<SpaceNode>() {
return EcoString::new();
}
if self.is::<LinebreakNode>() {
return r"\\".into();
}
if let Some(node) = self.to::<dyn Texify>() {
return node.texify();
}
panic!("{self:?} is not math");
}
}
/// Layout a TeX formula into a frame.
pub fn layout_tex(
tex: &str,
display: bool,
world: Tracked<dyn World>,
styles: StyleChain,
) -> SourceResult<Frame> {
// Load the font.
let font = world
.book()
.select(styles.get(MathNode::FAMILY).as_str(), variant(styles))
.and_then(|id| world.font(id))
.expect("failed to find math font");
// Prepare the font context.
let ctx = font
.math()
.map(|math| FontContext::new(font.ttf(), math))
.expect("font is not suitable for math");
// Layout the formula.
let em = styles.get(TextNode::SIZE);
let style = if display { Style::Display } else { Style::Text };
let settings = LayoutSettings::new(&ctx, em.to_pt(), style);
let renderer = Renderer::new();
let layout = renderer
.layout(&tex, settings)
.map_err(|err| match err {
Error::Parse(err) => err.to_string(),
Error::Layout(LayoutError::Font(err)) => err.to_string(),
})
.expect("failed to layout with rex");
// Determine the metrics.
let (x0, y0, x1, y1) = renderer.size(&layout);
let width = Abs::pt(x1 - x0);
let mut top = Abs::pt(y1);
let mut bottom = Abs::pt(-y0);
if style != Style::Display {
let metrics = font.metrics();
top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics);
bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics);
};
// Prepare a frame rendering backend.
let size = Size::new(width, top + bottom);
let mut backend = FrameBackend {
frame: {
let mut frame = Frame::new(size);
frame.set_baseline(top);
frame.apply_role(Role::Formula);
frame
},
baseline: top,
font: font.clone(),
fill: styles.get(TextNode::FILL),
lang: styles.get(TextNode::LANG),
colors: vec![],
};
// Render into the frame.
renderer.render(&layout, &mut backend);
Ok(backend.frame)
}
/// A ReX rendering backend that renders into a frame.
struct FrameBackend {
frame: Frame,
baseline: Abs,
font: Font,
fill: Paint,
lang: Lang,
colors: Vec<RGBA>,
}
impl FrameBackend {
/// The currently active fill paint.
fn fill(&self) -> Paint {
self.colors
.last()
.map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into())
.unwrap_or(self.fill)
}
/// Convert a cursor to a point.
fn transform(&self, cursor: Cursor) -> Point {
Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y))
}
}
impl Backend for FrameBackend {
fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) {
self.frame.push(
self.transform(pos),
Element::Text(Text {
font: self.font.clone(),
size: Abs::pt(scale),
fill: self.fill(),
lang: self.lang,
glyphs: vec![Glyph {
id: gid,
x_advance: Em::new(0.0),
x_offset: Em::new(0.0),
c: ' ',
}],
}),
);
}
fn rule(&mut self, pos: Cursor, width: f64, height: f64) {
self.frame.push(
self.transform(pos),
Element::Shape(Shape {
geometry: Geometry::Rect(Size::new(Abs::pt(width), Abs::pt(height))),
fill: Some(self.fill()),
stroke: None,
}),
);
}
fn begin_color(&mut self, color: RGBA) {
self.colors.push(color);
}
fn end_color(&mut self) {
self.colors.pop();
}
}

27
library/src/prelude.rs Normal file
View File

@ -0,0 +1,27 @@
//! Helpful imports for creating library functionality.
pub use std::fmt::{self, Debug, Formatter};
pub use std::hash::Hash;
pub use std::io;
pub use std::num::NonZeroUsize;
pub use std::sync::Arc;
pub use comemo::Tracked;
pub use typst::diag::{
bail, error, with_alternative, At, FileError, FileResult, SourceError, SourceResult,
StrResult,
};
pub use typst::frame::*;
pub use typst::geom::*;
pub use typst::model::{
array, capability, castable, dict, dynamic, format_str, node, Args, Array,
Capability, Cast, Content, Dict, Dynamic, Fold, Func, Key, LangItems, Node, Resolve,
Scope, Selector, Show, Smart, Str, StyleChain, StyleMap, StyleVec, Value, Vm,
};
pub use typst::syntax::{Span, Spanned};
pub use typst::util::{format_eco, EcoString};
pub use typst::World;
pub use super::ext::{ContentExt, StyleMapExt};
pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions};
pub use super::text::{FallbackList, TextNode};

View File

@ -1,5 +1,5 @@
use crate::library::layout::PageNode; use crate::layout::PageNode;
use crate::library::prelude::*; use crate::prelude::*;
/// A sequence of page runs. /// A sequence of page runs.
#[derive(Hash)] #[derive(Hash)]

View File

@ -1,6 +1,6 @@
use crate::library::layout::{BlockNode, BlockSpacing}; use crate::layout::{BlockNode, BlockSpacing};
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::{FontFamily, TextNode, TextSize}; use crate::text::{FontFamily, TextNode, TextSize};
/// A section heading. /// A section heading.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,9 +1,9 @@
use unscanny::Scanner; use unscanny::Scanner;
use crate::library::layout::{BlockSpacing, GridNode, HNode, TrackSizing}; use crate::layout::{BlockSpacing, GridNode, HNode, TrackSizing};
use crate::library::prelude::*; use crate::prelude::*;
use crate::library::text::{ParNode, SpaceNode}; use crate::text::{ParNode, SpaceNode};
use crate::library::utility::Numbering; use crate::utility::Numbering;
/// An unordered (bulleted) or ordered (numbered) list. /// An unordered (bulleted) or ordered (numbered) list.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,4 +1,4 @@
use crate::library::prelude::*; use crate::prelude::*;
/// A reference to a label. /// A reference to a label.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]

View File

@ -1,5 +1,5 @@
use crate::library::layout::{BlockSpacing, GridNode, TrackSizing}; use crate::layout::{BlockSpacing, GridNode, TrackSizing, TrackSizings};
use crate::library::prelude::*; use crate::prelude::*;
/// A table of items. /// A table of items.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -19,7 +19,7 @@ impl TableNode {
pub const FILL: Celled<Option<Paint>> = Celled::Value(None); pub const FILL: Celled<Option<Paint>> = Celled::Value(None);
/// How to stroke the cells. /// How to stroke the cells.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const STROKE: Option<RawStroke> = Some(RawStroke::default()); pub const STROKE: Option<PartialStroke> = Some(PartialStroke::default());
/// How much to pad the cells's content. /// How much to pad the cells's content.
pub const PADDING: Rel<Length> = Abs::pt(5.0).into(); pub const PADDING: Rel<Length> = Abs::pt(5.0).into();
@ -31,11 +31,11 @@ impl TableNode {
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into()); pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
let columns = args.named("columns")?.unwrap_or_default(); let TrackSizings(columns) = args.named("columns")?.unwrap_or_default();
let rows = args.named("rows")?.unwrap_or_default(); let TrackSizings(rows) = args.named("rows")?.unwrap_or_default();
let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default(); let TrackSizings(base_gutter) = args.named("gutter")?.unwrap_or_default();
let column_gutter = args.named("column-gutter")?; let column_gutter = args.named("column-gutter")?.map(|TrackSizings(v)| v);
let row_gutter = args.named("row-gutter")?; let row_gutter = args.named("row-gutter")?.map(|TrackSizings(v)| v);
Ok(Self { Ok(Self {
tracks: Axes::new(columns, rows), tracks: Axes::new(columns, rows),
gutter: Axes::new( gutter: Axes::new(
@ -73,7 +73,7 @@ impl Show for TableNode {
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let fill = styles.get(Self::FILL); let fill = styles.get(Self::FILL);
let stroke = styles.get(Self::STROKE).map(RawStroke::unwrap_or_default); let stroke = styles.get(Self::STROKE).map(PartialStroke::unwrap_or_default);
let padding = styles.get(Self::PADDING); let padding = styles.get(Self::PADDING);
let cols = self.tracks.x.len().max(1); let cols = self.tracks.x.len().max(1);

View File

@ -2,7 +2,7 @@ use kurbo::{BezPath, Line, ParamCurve};
use ttf_parser::{GlyphId, OutlineBuilder}; use ttf_parser::{GlyphId, OutlineBuilder};
use super::TextNode; use super::TextNode;
use crate::library::prelude::*; use crate::prelude::*;
/// Typeset underline, stricken-through or overlined text. /// Typeset underline, stricken-through or overlined text.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -22,7 +22,7 @@ impl<const L: DecoLine> DecoNode<L> {
/// How to stroke the line. The text color and thickness are read from the /// How to stroke the line. The text color and thickness are read from the
/// font tables if `auto`. /// font tables if `auto`.
#[property(shorthand, resolve, fold)] #[property(shorthand, resolve, fold)]
pub const STROKE: Smart<RawStroke> = Smart::Auto; pub const STROKE: Smart<PartialStroke> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `auto`. /// if `auto`.
#[property(resolve)] #[property(resolve)]
@ -72,7 +72,7 @@ impl<const L: DecoLine> Show for DecoNode<L> {
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Decoration { pub struct Decoration {
pub line: DecoLine, pub line: DecoLine,
pub stroke: RawStroke<Abs>, pub stroke: PartialStroke<Abs>,
pub offset: Smart<Abs>, pub offset: Smart<Abs>,
pub extent: Abs, pub extent: Abs,
pub evade: bool, pub evade: bool,
@ -157,7 +157,6 @@ pub fn decorate(
if bbox.map_or(false, |bbox| { if bbox.map_or(false, |bbox| {
let y_min = -text.font.to_em(bbox.y_max).at(text.size); let y_min = -text.font.to_em(bbox.y_max).at(text.size);
let y_max = -text.font.to_em(bbox.y_min).at(text.size); let y_max = -text.font.to_em(bbox.y_min).at(text.size);
offset >= y_min && offset <= y_max offset >= y_min && offset <= y_max
}) { }) {
// Find all intersections of segments with the line. // Find all intersections of segments with the line.

View File

@ -1,5 +1,5 @@
use super::TextNode; use super::TextNode;
use crate::library::prelude::*; use crate::prelude::*;
/// Link text and other elements to a destination. /// Link text and other elements to a destination.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -35,18 +35,6 @@ impl LinkNode {
} }
} }
castable! {
Destination,
Expected: "string or dictionary with `page`, `x`, and `y` keys",
Value::Str(string) => Self::Url(string.into()),
Value::Dict(dict) => {
let page = dict.get("page")?.clone().cast()?;
let x: Length = dict.get("x")?.clone().cast()?;
let y: Length = dict.get("y")?.clone().cast()?;
Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) })
},
}
impl Show for LinkNode { impl Show for LinkNode {
fn unguard_parts(&self, sel: Selector) -> Content { fn unguard_parts(&self, sel: Selector) -> Content {
Self { Self {

View File

@ -19,10 +19,10 @@ pub use shift::*;
use std::borrow::Cow; use std::borrow::Cow;
use rustybuzz::Tag; use rustybuzz::Tag;
use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
use typst::util::EcoString;
use crate::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; use crate::prelude::*;
use crate::library::prelude::*;
use crate::util::EcoString;
/// A single run of text with the same style. /// A single run of text with the same style.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
@ -32,7 +32,7 @@ pub struct TextNode(pub EcoString);
impl TextNode { impl TextNode {
/// A prioritized sequence of font families. /// A prioritized sequence of font families.
#[property(skip, referenced)] #[property(skip, referenced)]
pub const FAMILY: Vec<FontFamily> = vec![FontFamily::new("IBM Plex Sans")]; pub const FAMILY: FallbackList = FallbackList(vec![FontFamily::new("IBM Plex Sans")]);
/// Whether to allow font fallback when the primary font list contains no /// Whether to allow font fallback when the primary font list contains no
/// match. /// match.
pub const FALLBACK: bool = true; pub const FALLBACK: bool = true;
@ -73,11 +73,11 @@ impl TextNode {
/// The direction for text and inline objects. When `auto`, the direction is /// The direction for text and inline objects. When `auto`, the direction is
/// automatically inferred from the language. /// automatically inferred from the language.
#[property(resolve)] #[property(resolve)]
pub const DIR: Smart<HorizontalDir> = Smart::Auto; pub const DIR: HorizontalDir = HorizontalDir(Smart::Auto);
/// Whether to hyphenate text to improve line breaking. When `auto`, words /// Whether to hyphenate text to improve line breaking. When `auto`, words
/// will will be hyphenated if and only if justification is enabled. /// will will be hyphenated if and only if justification is enabled.
#[property(resolve)] #[property(resolve)]
pub const HYPHENATE: Smart<Hyphenate> = Smart::Auto; pub const HYPHENATE: Hyphenate = Hyphenate(Smart::Auto);
/// Whether to apply smart quotes. /// Whether to apply smart quotes.
pub const SMART_QUOTES: bool = true; pub const SMART_QUOTES: bool = true;
@ -103,7 +103,7 @@ impl TextNode {
pub const FRACTIONS: bool = false; pub const FRACTIONS: bool = false;
/// Raw OpenType features to apply. /// Raw OpenType features to apply.
#[property(fold)] #[property(fold)]
pub const FEATURES: Vec<(Tag, u32)> = vec![]; pub const FEATURES: FontFeatures = FontFeatures(vec![]);
/// Whether the font weight should be increased by 300. /// Whether the font weight should be increased by 300.
#[property(skip, fold)] #[property(skip, fold)]
@ -156,7 +156,7 @@ impl TextNode {
list.push(args.find()?.unwrap()); list.push(args.find()?.unwrap());
} }
styles.set(Self::FAMILY, list); styles.set(Self::FAMILY, FallbackList(list));
} }
} }
} }
@ -190,53 +190,19 @@ castable! {
Value::Str(string) => Self::new(&string), Value::Str(string) => Self::new(&string),
} }
/// Font family fallback list.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct FallbackList(pub Vec<FontFamily>);
castable! { castable! {
Vec<FontFamily>, FallbackList,
Expected: "string or array of strings", Expected: "string or array of strings",
Value::Str(string) => vec![FontFamily::new(&string)], Value::Str(string) => Self(vec![FontFamily::new(&string)]),
Value::Array(values) => values Value::Array(values) => Self(values
.into_iter() .into_iter()
.filter_map(|v| v.cast().ok()) .filter_map(|v| v.cast().ok())
.map(|string: EcoString| FontFamily::new(&string)) .map(|string: EcoString| FontFamily::new(&string))
.collect(), .collect()),
}
castable! {
FontStyle,
Expected: "string",
Value::Str(string) => match string.as_str() {
"normal" => Self::Normal,
"italic" => Self::Italic,
"oblique" => Self::Oblique,
_ => Err(r#"expected "normal", "italic" or "oblique""#)?,
},
}
castable! {
FontWeight,
Expected: "integer or string",
Value::Int(v) => Value::Int(v)
.cast::<usize>()?
.try_into()
.map_or(Self::BLACK, Self::from_number),
Value::Str(string) => match string.as_str() {
"thin" => Self::THIN,
"extralight" => Self::EXTRALIGHT,
"light" => Self::LIGHT,
"regular" => Self::REGULAR,
"medium" => Self::MEDIUM,
"semibold" => Self::SEMIBOLD,
"bold" => Self::BOLD,
"extrabold" => Self::EXTRABOLD,
"black" => Self::BLACK,
_ => Err("unknown font weight")?,
},
}
castable! {
FontStretch,
Expected: "ratio",
Value::Ratio(v) => Self::from_ratio(v.get() as f32),
} }
/// The size of text. /// The size of text.
@ -286,57 +252,49 @@ castable! {
}), }),
} }
castable! {
Lang,
Expected: "string",
Value::Str(string) => Self::from_str(&string)
.ok_or("expected two or three letter language code (ISO 639-1/2/3)")?,
}
castable! {
Region,
Expected: "string",
Value::Str(string) => Self::from_str(&string)
.ok_or("expected two letter region code (ISO 3166-1 alpha-2)")?,
}
/// The direction of text and inline objects in their line. /// The direction of text and inline objects in their line.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalDir(pub Dir); pub struct HorizontalDir(pub Smart<Dir>);
castable! { castable! {
HorizontalDir, HorizontalDir,
Expected: "direction", Expected: "direction or auto",
Value::Auto => Self(Smart::Auto),
@dir: Dir => match dir.axis() { @dir: Dir => match dir.axis() {
Axis::X => Self(*dir), Axis::X => Self(Smart::Custom(*dir)),
Axis::Y => Err("must be horizontal")?, Axis::Y => Err("must be horizontal")?,
}, },
} }
impl Resolve for Smart<HorizontalDir> { impl Resolve for HorizontalDir {
type Output = Dir; type Output = Dir;
fn resolve(self, styles: StyleChain) -> Self::Output { fn resolve(self, styles: StyleChain) -> Self::Output {
match self { match self.0 {
Smart::Auto => styles.get(TextNode::LANG).dir(), Smart::Auto => styles.get(TextNode::LANG).dir(),
Smart::Custom(dir) => dir.0, Smart::Custom(dir) => dir,
} }
} }
} }
/// Whether to hyphenate text. /// Whether to hyphenate text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub bool); pub struct Hyphenate(pub Smart<bool>);
castable!(Hyphenate: bool); castable! {
Hyphenate,
Expected: "boolean or auto",
Value::Auto => Self(Smart::Auto),
Value::Bool(v) => Self(Smart::Custom(v)),
}
impl Resolve for Smart<Hyphenate> { impl Resolve for Hyphenate {
type Output = bool; type Output = bool;
fn resolve(self, styles: StyleChain) -> Self::Output { fn resolve(self, styles: StyleChain) -> Self::Output {
match self { match self.0 {
Smart::Auto => styles.get(ParNode::JUSTIFY), Smart::Auto => styles.get(ParNode::JUSTIFY),
Smart::Custom(v) => v.0, Smart::Custom(v) => v,
} }
} }
} }
@ -404,29 +362,33 @@ castable! {
}, },
} }
/// OpenType font features settings.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct FontFeatures(pub Vec<(Tag, u32)>);
castable! { castable! {
Vec<(Tag, u32)>, FontFeatures,
Expected: "array of strings or dictionary mapping tags to integers", Expected: "array of strings or dictionary mapping tags to integers",
Value::Array(values) => values Value::Array(values) => Self(values
.into_iter() .into_iter()
.filter_map(|v| v.cast().ok()) .filter_map(|v| v.cast().ok())
.map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1)) .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
.collect(), .collect()),
Value::Dict(values) => values Value::Dict(values) => Self(values
.into_iter() .into_iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| {
let tag = Tag::from_bytes_lossy(k.as_bytes()); let tag = Tag::from_bytes_lossy(k.as_bytes());
let num = v.cast::<i64>().ok()?.try_into().ok()?; let num = v.cast::<i64>().ok()?.try_into().ok()?;
Some((tag, num)) Some((tag, num))
}) })
.collect(), .collect()),
} }
impl Fold for Vec<(Tag, u32)> { impl Fold for FontFeatures {
type Output = Self; type Output = Self;
fn fold(mut self, outer: Self::Output) -> Self::Output { fn fold(mut self, outer: Self::Output) -> Self::Output {
self.extend(outer); self.0.extend(outer.0);
self self
} }
} }
@ -515,7 +477,7 @@ pub fn smallcaps(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true))) Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
} }
/// Strong text, rendered in boldface by default. /// Strong content, rendered in boldface by default.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct StrongNode(pub Content); pub struct StrongNode(pub Content);
@ -543,7 +505,7 @@ impl Show for StrongNode {
} }
} }
/// Emphasized text, rendered with an italic font by default. /// Emphasized content, rendered with an italic font by default.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
pub struct EmphNode(pub Content); pub struct EmphNode(pub Content);

View File

@ -1,13 +1,13 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use typst::util::EcoString;
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use xi_unicode::LineBreakIterator; use xi_unicode::LineBreakIterator;
use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode}; use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode};
use crate::library::layout::Spacing; use crate::layout::Spacing;
use crate::library::prelude::*; use crate::prelude::*;
use crate::util::EcoString;
/// Arrange text, spacing and inline-level nodes into a paragraph. /// Arrange text, spacing and inline-level nodes into a paragraph.
#[derive(Hash)] #[derive(Hash)]
@ -42,11 +42,10 @@ impl ParNode {
/// How to align text and inline objects in their line. /// How to align text and inline objects in their line.
#[property(resolve)] #[property(resolve)]
pub const ALIGN: HorizontalAlign = HorizontalAlign(RawAlign::Start); pub const ALIGN: HorizontalAlign = HorizontalAlign(GenAlign::Start);
/// Whether to justify text in its line. /// Whether to justify text in its line.
pub const JUSTIFY: bool = false; pub const JUSTIFY: bool = false;
/// How to determine line breaks. /// How to determine line breaks.
#[property(resolve)]
pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto; pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto;
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
@ -113,12 +112,12 @@ impl PartialOrd for ParChild {
/// A horizontal alignment. /// A horizontal alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub RawAlign); pub struct HorizontalAlign(pub GenAlign);
castable! { castable! {
HorizontalAlign, HorizontalAlign,
Expected: "alignment", Expected: "alignment",
@align: RawAlign => match align.axis() { @align: GenAlign => match align.axis() {
Axis::X => Self(*align), Axis::X => Self(*align),
Axis::Y => Err("must be horizontal")?, Axis::Y => Err("must be horizontal")?,
}, },
@ -151,20 +150,6 @@ castable! {
}, },
} }
impl Resolve for Smart<Linebreaks> {
type Output = Linebreaks;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.unwrap_or_else(|| {
if styles.get(ParNode::JUSTIFY) {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
})
}
}
/// A paragraph break. /// A paragraph break.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct ParbreakNode; pub struct ParbreakNode;
@ -446,7 +431,7 @@ fn collect<'a>(
let mut iter = par.0.iter().peekable(); let mut iter = par.0.iter().peekable();
while let Some((child, map)) = iter.next() { while let Some((child, map)) = iter.next() {
let styles = map.chain(&styles); let styles = map.chain(styles);
let segment = match child { let segment = match child {
ParChild::Text(text) => { ParChild::Text(text) => {
let prev = full.len(); let prev = full.len();
@ -515,7 +500,7 @@ fn prepare<'a>(
regions: &Regions, regions: &Regions,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) { let bidi = BidiInfo::new(text, match styles.get(TextNode::DIR) {
Dir::LTR => Some(BidiLevel::ltr()), Dir::LTR => Some(BidiLevel::ltr()),
Dir::RTL => Some(BidiLevel::rtl()), Dir::RTL => Some(BidiLevel::rtl()),
_ => None, _ => None,
@ -642,7 +627,15 @@ fn linebreak<'a>(
world: Tracked<dyn World>, world: Tracked<dyn World>,
width: Abs, width: Abs,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
match p.styles.get(ParNode::LINEBREAKS) { let linebreaks = p.styles.get(ParNode::LINEBREAKS).unwrap_or_else(|| {
if p.styles.get(ParNode::JUSTIFY) {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
match linebreaks {
Linebreaks::Simple => linebreak_simple(p, world, width), Linebreaks::Simple => linebreak_simple(p, world, width),
Linebreaks::Optimized => linebreak_optimized(p, world, width), Linebreaks::Optimized => linebreak_optimized(p, world, width),
} }

View File

@ -1,5 +1,6 @@
use typst::syntax::is_newline;
use super::{Lang, Region}; use super::{Lang, Region};
use crate::syntax::is_newline;
/// State machine for smart quote subtitution. /// State machine for smart quote subtitution.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -4,10 +4,11 @@ use syntect::highlighting::{
Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings, Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings,
}; };
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use typst::syntax;
use super::{FontFamily, Hyphenate, LinebreakNode, TextNode}; use super::{FontFamily, Hyphenate, LinebreakNode, TextNode};
use crate::library::layout::{BlockNode, BlockSpacing}; use crate::layout::{BlockNode, BlockSpacing};
use crate::library::prelude::*; use crate::prelude::*;
/// Monospaced text with optional syntax highlighting. /// Monospaced text with optional syntax highlighting.
#[derive(Debug, Hash)] #[derive(Debug, Hash)]
@ -70,12 +71,12 @@ impl Show for RawNode {
let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
let root = match lang.as_deref() { let root = match lang.as_deref() {
Some("typc") => crate::syntax::parse_code(&self.text), Some("typc") => syntax::parse_code(&self.text),
_ => crate::syntax::parse(&self.text), _ => syntax::parse(&self.text),
}; };
let mut seq = vec![]; let mut seq = vec![];
crate::syntax::highlight::highlight_themed(&root, &THEME, |range, style| { syntax::highlight::highlight_themed(&root, &THEME, |range, style| {
seq.push(styled(&self.text[range], foreground, style)); seq.push(styled(&self.text[range], foreground, style));
}); });
@ -108,7 +109,7 @@ impl Show for RawNode {
let mut map = StyleMap::new(); let mut map = StyleMap::new();
map.set(TextNode::OVERHANG, false); map.set(TextNode::OVERHANG, false);
map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false))); map.set(TextNode::HYPHENATE, Hyphenate(Smart::Custom(false)));
map.set(TextNode::SMART_QUOTES, false); map.set(TextNode::SMART_QUOTES, false);
Ok(realized.styled_with_map(map)) Ok(realized.styled_with_map(map))

View File

@ -2,11 +2,11 @@ use std::ops::Range;
use std::str::FromStr; use std::str::FromStr;
use rustybuzz::{Feature, Tag, UnicodeBuffer}; use rustybuzz::{Feature, Tag, UnicodeBuffer};
use typst::font::{Font, FontVariant};
use typst::util::SliceExt;
use super::*; use super::*;
use crate::font::{Font, FontVariant}; use crate::prelude::*;
use crate::library::prelude::*;
use crate::util::SliceExt;
/// The result of shaping text. /// The result of shaping text.
/// ///
@ -128,7 +128,7 @@ impl<'a> ShapedText<'a> {
// Apply line decorations. // Apply line decorations.
for deco in &decos { for deco in &decos {
decorate(&mut frame, &deco, &text, shift, pos, width); decorate(&mut frame, deco, &text, shift, pos, width);
} }
frame.insert(text_layer, pos, Element::Text(text)); frame.insert(text_layer, pos, Element::Text(text));
@ -339,7 +339,7 @@ pub fn shape<'a>(
}; };
if !text.is_empty() { if !text.is_empty() {
shape_segment(&mut ctx, 0, &text, families(styles)); shape_segment(&mut ctx, 0, text, families(styles));
} }
track_and_space(&mut ctx); track_and_space(&mut ctx);
@ -570,6 +570,7 @@ fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] }; let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] };
styles styles
.get(TextNode::FAMILY) .get(TextNode::FAMILY)
.0
.iter() .iter()
.map(|family| family.as_str()) .map(|family| family.as_str())
.chain(tail.iter().copied()) .chain(tail.iter().copied())
@ -635,7 +636,7 @@ fn tags(styles: StyleChain) -> Vec<Feature> {
feat(b"frac", 1); feat(b"frac", 1);
} }
for (tag, value) in styles.get(TextNode::FEATURES) { for (tag, value) in styles.get(TextNode::FEATURES).0 {
tags.push(Feature::new(tag, value, ..)) tags.push(Feature::new(tag, value, ..))
} }

View File

@ -1,7 +1,8 @@
use typst::model::SequenceNode;
use typst::util::EcoString;
use super::{variant, SpaceNode, TextNode, TextSize}; use super::{variant, SpaceNode, TextNode, TextSize};
use crate::library::prelude::*; use crate::prelude::*;
use crate::model::SequenceNode;
use crate::util::EcoString;
/// Sub or superscript text. /// Sub or superscript text.
/// ///
@ -98,7 +99,7 @@ fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> {
/// Checks whether the first retrievable family contains all code points of the /// Checks whether the first retrievable family contains all code points of the
/// given string. /// given string.
fn is_shapable(world: Tracked<dyn World>, text: &str, styles: StyleChain) -> bool { fn is_shapable(world: Tracked<dyn World>, text: &str, styles: StyleChain) -> bool {
for family in styles.get(TextNode::FAMILY).iter() { for family in styles.get(TextNode::FAMILY).0.iter() {
if let Some(font) = world if let Some(font) = world
.book() .book()
.select(family.as_str(), variant(styles)) .select(family.as_str(), variant(styles))

View File

@ -1,6 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use crate::library::prelude::*; use crate::prelude::*;
/// Create a grayscale color. /// Create a grayscale color.
pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult<Value> { pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {

View File

@ -1,7 +1,8 @@
use std::fmt::Write; use std::fmt::Write;
use crate::diag::format_xml_like_error; use typst::diag::format_xml_like_error;
use crate::library::prelude::*;
use crate::prelude::*;
/// Read structured data from a CSV file. /// Read structured data from a CSV file.
pub fn csv(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> { pub fn csv(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {

View File

@ -1,6 +1,6 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::library::prelude::*; use crate::prelude::*;
/// Convert a value to an integer. /// Convert a value to an integer.
pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult<Value> { pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {

View File

@ -11,10 +11,10 @@ pub use math::*;
pub use string::*; pub use string::*;
use comemo::Track; use comemo::Track;
use typst::model::{Eval, Route, Scopes, Vm};
use typst::syntax::Source;
use crate::library::prelude::*; use crate::prelude::*;
use crate::model::{Eval, Route, Scopes, Vm};
use crate::syntax::Source;
/// The name of a value's type. /// The name of a value's type.
pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult<Value> { pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
@ -39,7 +39,7 @@ pub fn eval(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
let ast = source.ast()?; let ast = source.ast()?;
// Evaluate the source. // Evaluate the source.
let std = &vm.world.config().std; let std = &vm.world.config().scope;
let scopes = Scopes::new(Some(std)); let scopes = Scopes::new(Some(std));
let route = Route::default(); let route = Route::default();
let mut sub = Vm::new(vm.world, route.track(), None, scopes); let mut sub = Vm::new(vm.world, route.track(), None, scopes);

View File

@ -1,5 +1,6 @@
use crate::library::prelude::*; use typst::model::Regex;
use crate::model::Regex;
use crate::prelude::*;
/// The string representation of a value. /// The string representation of a value.
pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult<Value> { pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {

View File

@ -15,7 +15,7 @@ pub fn capability(_: TokenStream, item: TokenStream) -> TokenStream {
let name = &item_trait.ident; let name = &item_trait.ident;
quote! { quote! {
#item_trait #item_trait
impl crate::model::Capability for dyn #name {} impl ::typst::model::Capability for dyn #name {}
}.into() }.into()
} }
@ -70,7 +70,7 @@ fn expand_node(
fn construct( fn construct(
_: &mut model::Vm, _: &mut model::Vm,
_: &mut model::Args, _: &mut model::Args,
) -> crate::diag::SourceResult<model::Content> { ) -> typst::diag::SourceResult<model::Content> {
unimplemented!() unimplemented!()
} }
} }
@ -84,7 +84,7 @@ fn expand_node(
let checks = items.iter().map(|cap| { let checks = items.iter().map(|cap| {
quote! { quote! {
if id == TypeId::of::<dyn #cap>() { if id == TypeId::of::<dyn #cap>() {
return Some(unsafe { crate::util::fat::vtable(self as &dyn #cap) }); return Some(unsafe { typst::util::fat::vtable(self as &dyn #cap) });
} }
} }
}); });
@ -101,10 +101,10 @@ fn expand_node(
Ok(quote! { Ok(quote! {
#[allow(non_snake_case)] #[allow(non_snake_case)]
mod #module { mod #module {
use std::any::TypeId; use ::std::any::TypeId;
use std::marker::PhantomData; use ::std::marker::PhantomData;
use once_cell::sync::Lazy; use ::once_cell::sync::Lazy;
use crate::model; use ::typst::model;
use super::*; use super::*;
#impl_block #impl_block
@ -370,7 +370,7 @@ fn generate_set(
) -> syn::ImplItemMethod { ) -> syn::ImplItemMethod {
let user = user.map(|method| { let user = user.map(|method| {
let block = &method.block; let block = &method.block;
quote! { (|| -> crate::diag::SourceResult<()> { #block; Ok(()) } )()?; } quote! { (|| -> typst::diag::SourceResult<()> { #block; Ok(()) } )()?; }
}); });
let mut shorthands = vec![]; let mut shorthands = vec![];
@ -379,7 +379,7 @@ fn generate_set(
.filter(|p| !p.skip) .filter(|p| !p.skip)
.map(|property| { .map(|property| {
let name = &property.name; let name = &property.name;
let string = name.to_string().replace("_", "-").to_lowercase(); let string = name.to_string().replace('_', "-").to_lowercase();
let value = if let Some(short) = &property.shorthand { let value = if let Some(short) = &property.shorthand {
match short { match short {
@ -409,7 +409,7 @@ fn generate_set(
fn set( fn set(
args: &mut model::Args, args: &mut model::Args,
constructor: bool, constructor: bool,
) -> crate::diag::SourceResult<model::StyleMap> { ) -> typst::diag::SourceResult<model::StyleMap> {
let mut styles = model::StyleMap::new(); let mut styles = model::StyleMap::new();
#user #user
#(#bindings)* #(#bindings)*

View File

@ -10,33 +10,41 @@ use std::string::FromUtf8Error;
use comemo::Tracked; use comemo::Tracked;
use crate::syntax::{ErrorPos, Span, Spanned}; use crate::syntax::{ErrorPos, Span, Spanned};
use crate::util::EcoString; use crate::util::{format_eco, EcoString};
use crate::World; use crate::World;
/// Early-return with a [`SourceError`]. /// Early-return with a [`SourceError`].
#[macro_export] #[macro_export]
macro_rules! bail { #[doc(hidden)]
macro_rules! __bail {
($error:expr) => { ($error:expr) => {
return Err(Box::new(vec![$error])) return Err(Box::new(vec![$error]))
}; };
($($tts:tt)*) => { ($($tts:tt)*) => {
$crate::bail!($crate::error!($($tts)*)) $crate::diag::bail!($crate::diag::error!($($tts)*))
}; };
} }
#[doc(inline)]
pub use crate::__bail as bail;
/// Construct a [`SourceError`]. /// Construct a [`SourceError`].
#[macro_export] #[macro_export]
macro_rules! error { #[doc(hidden)]
macro_rules! __error {
($span:expr, $message:expr $(,)?) => { ($span:expr, $message:expr $(,)?) => {
$crate::diag::SourceError::new($span, $message) $crate::diag::SourceError::new($span, $message)
}; };
($span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { ($span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => {
$crate::error!($span, format!($fmt, $($arg),+)) $crate::diag::error!($span, format!($fmt, $($arg),+))
}; };
} }
#[doc(inline)]
pub use crate::__error as error;
/// A result that can carry multiple source errors. /// A result that can carry multiple source errors.
pub type SourceResult<T> = Result<T, Box<Vec<SourceError>>>; pub type SourceResult<T> = Result<T, Box<Vec<SourceError>>>;

View File

@ -5,7 +5,7 @@ use pdf_writer::{Filter, Finish, Name, Rect, Str};
use ttf_parser::{name_id, GlyphId, Tag}; use ttf_parser::{name_id, GlyphId, Tag};
use super::{deflate, EmExt, PdfContext, RefExt}; use super::{deflate, EmExt, PdfContext, RefExt};
use crate::util::SliceExt; use crate::util::{format_eco, SliceExt};
/// Embed all used fonts into the PDF. /// Embed all used fonts into the PDF.
pub fn write_fonts(ctx: &mut PdfContext) { pub fn write_fonts(ctx: &mut PdfContext) {

View File

@ -71,7 +71,7 @@ pub fn write_outline_item(
let current_child = Ref::new(id.get() + 1); let current_child = Ref::new(id.get() + 1);
outline.first(current_child); outline.first(current_child);
outline.last(Ref::new(next_ref.get() - 1)); outline.last(Ref::new(next_ref.get() - 1));
outline.count(-1 * node.children.len() as i32); outline.count(-(node.children.len() as i32));
} }
outline.title(TextStr(&node.heading.content)); outline.title(TextStr(&node.heading.content));

View File

@ -12,6 +12,7 @@ use crate::geom::{
Transform, Transform,
}; };
use crate::image::Image; use crate::image::Image;
use crate::util::format_eco;
/// Construct page objects. /// Construct page objects.
pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {

View File

@ -312,8 +312,10 @@ fn render_shape(
if let Some(Stroke { paint, thickness }) = shape.stroke { if let Some(Stroke { paint, thickness }) = shape.stroke {
let paint = paint.into(); let paint = paint.into();
let mut stroke = sk::Stroke::default(); let stroke = sk::Stroke {
stroke.width = thickness.to_f32(); width: thickness.to_f32(),
..Default::default()
};
canvas.stroke_path(&path, &paint, &stroke, ts, mask); canvas.stroke_path(&path, &paint, &stroke, ts, mask);
} }
@ -364,14 +366,16 @@ fn render_image(
let scale_x = view_width / pixmap.width() as f32; let scale_x = view_width / pixmap.width() as f32;
let scale_y = view_height / pixmap.height() as f32; let scale_y = view_height / pixmap.height() as f32;
let mut paint = sk::Paint::default(); let paint = sk::Paint {
paint.shader = sk::Pattern::new( shader: sk::Pattern::new(
pixmap.as_ref(), pixmap.as_ref(),
sk::SpreadMode::Pad, sk::SpreadMode::Pad,
sk::FilterQuality::Nearest, sk::FilterQuality::Nearest,
1.0, 1.0,
sk::Transform::from_scale(scale_x, scale_y), sk::Transform::from_scale(scale_x, scale_y),
); ),
..Default::default()
};
let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?; let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height)?;
canvas.fill_rect(rect, &paint, ts, mask); canvas.fill_rect(rect, &paint, ts, mask);

View File

@ -170,7 +170,7 @@ bitflags::bitflags! {
impl FontInfo { impl FontInfo {
/// Compute metadata for all fonts in the given data. /// Compute metadata for all fonts in the given data.
pub fn from_data<'a>(data: &'a [u8]) -> impl Iterator<Item = FontInfo> + 'a { pub fn from_data(data: &[u8]) -> impl Iterator<Item = FontInfo> + '_ {
let count = ttf_parser::fonts_in_collection(data).unwrap_or(1); let count = ttf_parser::fonts_in_collection(data).unwrap_or(1);
(0 .. count).filter_map(move |index| { (0 .. count).filter_map(move |index| {
let ttf = ttf_parser::Face::parse(data, index).ok()?; let ttf = ttf_parser::Face::parse(data, index).ok()?;

View File

@ -111,7 +111,7 @@ impl FontWeight {
/// The absolute number distance between this and another font weight. /// The absolute number distance between this and another font weight.
pub fn distance(self, other: Self) -> u16 { pub fn distance(self, other: Self) -> u16 {
(self.0 as i16 - other.0 as i16).abs() as u16 (self.0 as i16 - other.0 as i16).unsigned_abs()
} }
} }

View File

@ -2,6 +2,7 @@
use std::fmt::{self, Debug, Formatter, Write}; use std::fmt::{self, Debug, Formatter, Write};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use crate::font::Font; use crate::font::Font;
@ -9,7 +10,7 @@ use crate::geom::{
Abs, Align, Axes, Dir, Em, Numeric, Paint, Point, Shape, Size, Transform, Abs, Align, Axes, Dir, Em, Numeric, Paint, Point, Shape, Size, Transform,
}; };
use crate::image::Image; use crate::image::Image;
use crate::model::{Dict, Value}; use crate::model::{dict, Dict, Value};
use crate::util::EcoString; use crate::util::EcoString;
/// A finished layout with elements at fixed positions. /// A finished layout with elements at fixed positions.
@ -396,7 +397,7 @@ pub struct Glyph {
pub c: char, pub c: char,
} }
/// A code for a natural language. /// An identifier for a natural language.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Lang([u8; 3], u8); pub struct Lang([u8; 3], u8);
@ -404,19 +405,6 @@ impl Lang {
/// The code for the english language. /// The code for the english language.
pub const ENGLISH: Self = Self(*b"en ", 2); pub const ENGLISH: Self = Self(*b"en ", 2);
/// Construct a language from a two- or three-byte ISO 639-1/2/3 code.
pub fn from_str(iso: &str) -> Option<Self> {
let len = iso.len();
if matches!(len, 2 ..= 3) && iso.is_ascii() {
let mut bytes = [b' '; 3];
bytes[.. len].copy_from_slice(iso.as_bytes());
bytes.make_ascii_lowercase();
Some(Self(bytes, len as u8))
} else {
None
}
}
/// Return the language code as an all lowercase string slice. /// Return the language code as an all lowercase string slice.
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0[.. usize::from(self.1)]).unwrap_or_default() std::str::from_utf8(&self.0[.. usize::from(self.1)]).unwrap_or_default()
@ -432,28 +420,49 @@ impl Lang {
} }
} }
/// A code for a region somewhere in the world. impl FromStr for Lang {
type Err = &'static str;
/// Construct a language from a two- or three-byte ISO 639-1/2/3 code.
fn from_str(iso: &str) -> Result<Self, Self::Err> {
let len = iso.len();
if matches!(len, 2 ..= 3) && iso.is_ascii() {
let mut bytes = [b' '; 3];
bytes[.. len].copy_from_slice(iso.as_bytes());
bytes.make_ascii_lowercase();
Ok(Self(bytes, len as u8))
} else {
Err("expected two or three letter language code (ISO 639-1/2/3)")
}
}
}
/// An identifier for a region somewhere in the world.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Region([u8; 2]); pub struct Region([u8; 2]);
impl Region { impl Region {
/// Construct a region from its two-byte ISO 3166-1 alpha-2 code.
pub fn from_str(iso: &str) -> Option<Self> {
if iso.is_ascii() {
let mut bytes: [u8; 2] = iso.as_bytes().try_into().ok()?;
bytes.make_ascii_uppercase();
Some(Self(bytes))
} else {
None
}
}
/// Return the region code as an all uppercase string slice. /// Return the region code as an all uppercase string slice.
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap_or_default() std::str::from_utf8(&self.0).unwrap_or_default()
} }
} }
impl FromStr for Region {
type Err = &'static str;
/// Construct a region from its two-byte ISO 3166-1 alpha-2 code.
fn from_str(iso: &str) -> Result<Self, Self::Err> {
if iso.len() == 2 && iso.is_ascii() {
let mut bytes: [u8; 2] = iso.as_bytes().try_into().unwrap();
bytes.make_ascii_uppercase();
Ok(Self(bytes))
} else {
Err("expected two letter region code (ISO 3166-1 alpha-2)")
}
}
}
/// A link destination. /// A link destination.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination { pub enum Destination {
@ -529,9 +538,9 @@ impl Role {
pub fn is_weak(self) -> bool { pub fn is_weak(self) -> bool {
// In Typst, all text is in a paragraph, so paragraph isn't very // In Typst, all text is in a paragraph, so paragraph isn't very
// descriptive. // descriptive.
match self { matches!(
Self::Paragraph | Self::GenericBlock | Self::GenericInline => true, self,
_ => false, Self::Paragraph | Self::GenericBlock | Self::GenericInline
} )
} }
} }

View File

@ -78,3 +78,40 @@ impl Debug for Align {
}) })
} }
} }
/// The generic alignment representation.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum GenAlign {
/// Align at the start side of the text direction.
Start,
/// Align at the end side of the text direction.
End,
/// Align at a specific alignment.
Specific(Align),
}
impl GenAlign {
/// The axis this alignment belongs to.
pub const fn axis(self) -> Axis {
match self {
Self::Start | Self::End => Axis::X,
Self::Specific(align) => align.axis(),
}
}
}
impl From<Align> for GenAlign {
fn from(align: Align) -> Self {
Self::Specific(align)
}
}
impl Debug for GenAlign {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Start => f.pad("start"),
Self::End => f.pad("end"),
Self::Specific(align) => align.fmt(f),
}
}
}

View File

@ -178,6 +178,6 @@ mod tests {
#[test] #[test]
fn test_angle_unit_conversion() { fn test_angle_unit_conversion() {
assert!((Angle::rad(2.0 * PI).to_deg() - 360.0) < 1e-4); assert!((Angle::rad(2.0 * PI).to_deg() - 360.0) < 1e-4);
assert!((Angle::deg(45.0).to_rad() - 0.7854) < 1e-4); assert!((Angle::deg(45.0).to_rad() - std::f64::consts::FRAC_PI_4) < 1e-4);
} }
} }

View File

@ -21,6 +21,7 @@ mod rounded;
mod scalar; mod scalar;
mod sides; mod sides;
mod size; mod size;
mod stroke;
mod transform; mod transform;
pub use abs::*; pub use abs::*;
@ -42,6 +43,7 @@ pub use rounded::*;
pub use scalar::*; pub use scalar::*;
pub use sides::*; pub use sides::*;
pub use size::*; pub use size::*;
pub use stroke::*;
pub use transform::*; pub use transform::*;
use std::cmp::Ordering; use std::cmp::Ordering;

View File

@ -388,24 +388,6 @@ impl From<CmykColor> for Color {
} }
} }
/// A stroke of a geometric shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Stroke {
/// The stroke's paint.
pub paint: Paint,
/// The stroke's thickness.
pub thickness: Abs,
}
impl Default for Stroke {
fn default() -> Self {
Self {
paint: Paint::Solid(Color::BLACK.into()),
thickness: Abs::pt(1.0),
}
}
}
/// Convert to the closest u8. /// Convert to the closest u8.
fn round_u8(value: f64) -> u8 { fn round_u8(value: f64) -> u8 {
value.round() as u8 value.round() as u8

61
src/geom/stroke.rs Normal file
View File

@ -0,0 +1,61 @@
use super::*;
use crate::model::Smart;
/// A stroke of a geometric shape.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Stroke {
/// The stroke's paint.
pub paint: Paint,
/// The stroke's thickness.
pub thickness: Abs,
}
impl Default for Stroke {
fn default() -> Self {
Self {
paint: Paint::Solid(Color::BLACK),
thickness: Abs::pt(1.0),
}
}
}
/// A partial stroke representation.
///
/// In this representation, both fields are optional so that you can pass either
/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
/// this is expected.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct PartialStroke<T = Length> {
/// The stroke's paint.
pub paint: Smart<Paint>,
/// The stroke's thickness.
pub thickness: Smart<T>,
}
impl PartialStroke<Abs> {
/// Unpack the stroke, filling missing fields from the `default`.
pub fn unwrap_or(self, default: Stroke) -> Stroke {
Stroke {
paint: self.paint.unwrap_or(default.paint),
thickness: self.thickness.unwrap_or(default.thickness),
}
}
/// Unpack the stroke, filling missing fields with the default values.
pub fn unwrap_or_default(self) -> Stroke {
self.unwrap_or(Stroke::default())
}
}
impl<T: Debug> Debug for PartialStroke<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match (self.paint, &self.thickness) {
(Smart::Custom(paint), Smart::Custom(thickness)) => {
write!(f, "{thickness:?} + {paint:?}")
}
(Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
(Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
(Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
}
}
}

View File

@ -26,9 +26,7 @@
//! [content]: model::Content //! [content]: model::Content
//! [PDF]: export::pdf //! [PDF]: export::pdf
#![allow(clippy::len_without_is_empty)] extern crate self as typst;
#![allow(clippy::or_fun_call)]
#![allow(clippy::try_err)]
#[macro_use] #[macro_use]
pub mod util; pub mod util;
@ -42,10 +40,8 @@ pub mod export;
pub mod font; pub mod font;
pub mod frame; pub mod frame;
pub mod image; pub mod image;
pub mod library;
pub mod syntax; pub mod syntax;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use comemo::{Prehashed, Track}; use comemo::{Prehashed, Track};
@ -53,9 +49,9 @@ use comemo::{Prehashed, Track};
use crate::diag::{FileResult, SourceResult}; use crate::diag::{FileResult, SourceResult};
use crate::font::{Font, FontBook}; use crate::font::{Font, FontBook};
use crate::frame::Frame; use crate::frame::Frame;
use crate::model::{Content, Route, Scope, StyleMap}; use crate::model::{LangItems, Route, Scope, StyleMap};
use crate::syntax::{Source, SourceId}; use crate::syntax::{Source, SourceId};
use crate::util::{Buffer, EcoString}; use crate::util::Buffer;
/// Typeset a source file into a collection of layouted frames. /// Typeset a source file into a collection of layouted frames.
/// ///
@ -66,9 +62,10 @@ pub fn typeset(
world: &(dyn World + 'static), world: &(dyn World + 'static),
main: SourceId, main: SourceId,
) -> SourceResult<Vec<Frame>> { ) -> SourceResult<Vec<Frame>> {
crate::model::set_lang_items(world.config().items);
let route = Route::default(); let route = Route::default();
let module = model::eval(world.track(), route.track(), main)?; let module = model::eval(world.track(), route.track(), main)?;
library::layout::Layout::layout(&module.content, world.track()) item!(root)(world.track(), &module.content)
} }
/// The environment in which typesetting occurs. /// The environment in which typesetting occurs.
@ -97,49 +94,11 @@ pub trait World {
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub struct Config { pub struct Config {
/// The compilation root, relative to which absolute paths are. /// The compilation root, relative to which absolute paths are.
///
/// Default: Empty path.
pub root: PathBuf, pub root: PathBuf,
/// The scope containing definitions that are available everywhere. /// The scope containing definitions that are available everywhere.
/// pub scope: Scope,
/// Default: Typst's standard library.
pub std: Scope,
/// Defines which standard library items fulfill which syntactical roles.
///
/// Default: Typst's standard library's language map.
pub items: LangItems,
/// The default properties for page size, font selection and so on. /// The default properties for page size, font selection and so on.
///
/// Default: Empty style map.
pub styles: StyleMap, pub styles: StyleMap,
} /// Defines which standard library items fulfill which syntactical roles.
pub items: LangItems,
impl Default for Config {
fn default() -> Self {
Self {
root: PathBuf::new(),
std: library::scope(),
items: library::items(),
styles: StyleMap::new(),
}
}
}
/// Definition of certain standard library items the language is aware of.
#[derive(Debug, Clone, Hash)]
pub struct LangItems {
pub space: fn() -> Content,
pub linebreak: fn(justify: bool) -> Content,
pub text: fn(text: EcoString) -> Content,
pub smart_quote: fn(double: bool) -> Content,
pub parbreak: fn() -> Content,
pub strong: fn(body: Content) -> Content,
pub emph: fn(body: Content) -> Content,
pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content,
pub link: fn(label: EcoString) -> Content,
pub ref_: fn(target: EcoString) -> Content,
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
pub list_item: fn(body: Content) -> Content,
pub enum_item: fn(number: Option<usize>, body: Content) -> Content,
pub desc_item: fn(term: Content, body: Content) -> Content,
} }

View File

@ -1,17 +0,0 @@
use super::*;
use crate::library::prelude::*;
/// A fraction in a mathematical formula.
#[derive(Debug, Hash)]
pub struct FracNode {
/// The numerator.
pub num: MathNode,
/// The denominator.
pub denom: MathNode,
}
impl Texify for FracNode {
fn texify(&self) -> EcoString {
format_eco!("\\frac{{{}}}{{{}}}", self.num.texify(), self.denom.texify())
}
}

View File

@ -1,295 +0,0 @@
//! Mathematical formulas.
mod frac;
mod script;
pub use frac::*;
pub use script::*;
use rex::error::{Error, LayoutError};
use rex::font::FontContext;
use rex::layout::{LayoutSettings, Style};
use rex::parser::color::RGBA;
use rex::render::{Backend, Cursor, Renderer};
use crate::font::Font;
use crate::library::layout::BlockSpacing;
use crate::library::prelude::*;
use crate::library::text::{variant, FontFamily, TextNode};
/// A piece of a mathematical formula.
#[derive(Debug, Clone, Hash)]
pub enum MathNode {
/// Whitespace.
Space,
/// A forced line break.
Linebreak,
/// An atom in a math formula: `x`, `+`, `12`.
Atom(EcoString),
/// A base with optional sub and superscripts: `a_1^2`.
Script(Arc<ScriptNode>),
/// A fraction: `x/2`.
Frac(Arc<FracNode>),
/// A numbered math alignment indicator: `&`, `&&`.
Align(usize),
/// A row of mathematical material.
Row(Arc<Vec<MathNode>>, Span),
}
#[node(Show, LayoutInline)]
impl MathNode {
/// The math font family.
#[property(referenced)]
pub const FAMILY: FontFamily = FontFamily::new("NewComputerModernMath");
/// The spacing above display math.
#[property(resolve, shorthand(around))]
pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
/// The spacing below display math.
#[property(resolve, shorthand(around))]
pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
todo!()
}
}
impl MathNode {
/// Strip parentheses from the node.
pub fn unparen(self) -> Self {
if let Self::Row(row, span) = &self {
if let [MathNode::Atom(l), .., MathNode::Atom(r)] = row.as_slice() {
if l == "(" && r == ")" {
let inner = row[1 .. row.len() - 1].to_vec();
return Self::Row(Arc::new(inner), *span);
}
}
}
self
}
/// Whether the formula is display level.
pub fn display(&self) -> bool {
if let Self::Row(row, _) = self {
matches!(row.as_slice(), [MathNode::Space, .., MathNode::Space])
} else {
false
}
}
}
impl Show for MathNode {
fn unguard_parts(&self, _: Selector) -> Content {
self.clone().pack()
}
fn field(&self, _: &str) -> Option<Value> {
None
}
fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
Ok(if self.display() {
self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into())))
} else {
self.clone().pack()
})
}
fn finalize(
&self,
_: Tracked<dyn World>,
styles: StyleChain,
realized: Content,
) -> SourceResult<Content> {
Ok(if self.display() {
realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))
} else {
realized
})
}
}
impl LayoutInline for MathNode {
fn layout_inline(
&self,
world: Tracked<dyn World>,
_: &Regions,
styles: StyleChain,
) -> SourceResult<Vec<Frame>> {
let style = if self.display() { Style::Display } else { Style::Text };
let span = match self {
&Self::Row(_, span) => span,
_ => Span::detached(),
};
Ok(vec![layout_tex(world, self, span, style, styles)?])
}
}
/// Layout a TeX formula into a frame.
fn layout_tex(
world: Tracked<dyn World>,
node: &dyn Texify,
span: Span,
style: Style,
styles: StyleChain,
) -> SourceResult<Frame> {
let tex = node.texify();
// Load the font.
let font = world
.book()
.select(styles.get(MathNode::FAMILY).as_str(), variant(styles))
.and_then(|id| world.font(id))
.ok_or("failed to find math font")
.at(span)?;
// Prepare the font context.
let ctx = font
.math()
.map(|math| FontContext::new(font.ttf(), math))
.ok_or("font is not suitable for math")
.at(span)?;
// Layout the formula.
let em = styles.get(TextNode::SIZE);
let settings = LayoutSettings::new(&ctx, em.to_pt(), style);
let renderer = Renderer::new();
let layout = renderer
.layout(&tex, settings)
.map_err(|err| match err {
Error::Parse(err) => err.to_string(),
Error::Layout(LayoutError::Font(err)) => err.to_string(),
})
.at(span)?;
// Determine the metrics.
let (x0, y0, x1, y1) = renderer.size(&layout);
let width = Abs::pt(x1 - x0);
let mut top = Abs::pt(y1);
let mut bottom = Abs::pt(-y0);
if style != Style::Display {
let metrics = font.metrics();
top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics);
bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics);
};
// Prepare a frame rendering backend.
let size = Size::new(width, top + bottom);
let mut backend = FrameBackend {
frame: {
let mut frame = Frame::new(size);
frame.set_baseline(top);
frame.apply_role(Role::Formula);
frame
},
baseline: top,
font: font.clone(),
fill: styles.get(TextNode::FILL),
lang: styles.get(TextNode::LANG),
colors: vec![],
};
// Render into the frame.
renderer.render(&layout, &mut backend);
Ok(backend.frame)
}
/// A ReX rendering backend that renders into a frame.
struct FrameBackend {
frame: Frame,
baseline: Abs,
font: Font,
fill: Paint,
lang: Lang,
colors: Vec<RGBA>,
}
impl FrameBackend {
/// The currently active fill paint.
fn fill(&self) -> Paint {
self.colors
.last()
.map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into())
.unwrap_or(self.fill)
}
/// Convert a cursor to a point.
fn transform(&self, cursor: Cursor) -> Point {
Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y))
}
}
impl Backend for FrameBackend {
fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) {
self.frame.push(
self.transform(pos),
Element::Text(Text {
font: self.font.clone(),
size: Abs::pt(scale),
fill: self.fill(),
lang: self.lang,
glyphs: vec![Glyph {
id: gid,
x_advance: Em::new(0.0),
x_offset: Em::new(0.0),
c: ' ',
}],
}),
);
}
fn rule(&mut self, pos: Cursor, width: f64, height: f64) {
self.frame.push(
self.transform(pos),
Element::Shape(Shape {
geometry: Geometry::Rect(Size::new(Abs::pt(width), Abs::pt(height))),
fill: Some(self.fill()),
stroke: None,
}),
);
}
fn begin_color(&mut self, color: RGBA) {
self.colors.push(color);
}
fn end_color(&mut self) {
self.colors.pop();
}
}
/// Turn a math node into TeX math code.
trait Texify {
/// Perform the conversion.
fn texify(&self) -> EcoString;
}
impl Texify for MathNode {
fn texify(&self) -> EcoString {
match self {
Self::Space => "".into(),
Self::Linebreak => r"\\".into(),
Self::Atom(atom) => atom.chars().map(escape_char).collect(),
Self::Script(script) => script.texify(),
Self::Frac(frac) => frac.texify(),
Self::Align(_) => "".into(),
Self::Row(row, _) => row.iter().map(Texify::texify).collect(),
}
}
}
#[rustfmt::skip]
fn escape_char(c: char) -> EcoString {
match c {
'{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "),
'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' |
'*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' |
':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(),
c => unicode_math::SYMBOLS
.iter()
.find(|sym| sym.codepoint == c)
.map(|sym| format_eco!("\\{} ", sym.name))
.unwrap_or_default(),
}
}

View File

@ -1,31 +0,0 @@
use std::fmt::Write;
use super::*;
use crate::library::prelude::*;
/// A sub- and/or superscript in a mathematical formula.
#[derive(Debug, Hash)]
pub struct ScriptNode {
/// The base.
pub base: MathNode,
/// The subscript.
pub sub: Option<MathNode>,
/// The superscript.
pub sup: Option<MathNode>,
}
impl Texify for ScriptNode {
fn texify(&self) -> EcoString {
let mut tex = self.base.texify();
if let Some(sub) = &self.sub {
write!(tex, "_{{{}}}", sub.texify()).unwrap();
}
if let Some(sup) = &self.sup {
write!(tex, "^{{{}}}", sup.texify()).unwrap();
}
tex
}
}

View File

@ -1,27 +0,0 @@
//! Helpful imports for creating library functionality.
pub use std::fmt::{self, Debug, Formatter};
pub use std::hash::Hash;
pub use std::io;
pub use std::num::NonZeroUsize;
pub use std::sync::Arc;
pub use comemo::Tracked;
pub use super::ext::{ContentExt, StyleMapExt};
pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions};
pub use super::text::TextNode;
pub use super::{RawAlign, RawStroke};
pub use crate::diag::{
with_alternative, At, FileError, FileResult, SourceError, SourceResult, StrResult,
};
pub use crate::frame::*;
pub use crate::geom::*;
pub use crate::model::{
capability, node, Arg, Args, Array, Capability, Cast, Content, Dict, Dynamic, Fold,
Func, Key, Node, Resolve, Scope, Selector, Show, Smart, Str, StyleChain, StyleMap,
StyleVec, Value, Vm,
};
pub use crate::syntax::{Span, Spanned};
pub use crate::util::EcoString;
pub use crate::{LangItems, World};

View File

@ -1,149 +0,0 @@
use std::fmt::{self, Debug, Formatter};
use crate::geom::{Abs, Align, Axes, Axis, Get, Length, Paint, Stroke};
use crate::library::text::TextNode;
use crate::model::{Fold, Resolve, Smart, StyleChain, Value};
/// The unresolved alignment representation.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum RawAlign {
/// Align at the start side of the text direction.
Start,
/// Align at the end side of the text direction.
End,
/// Align at a specific alignment.
Specific(Align),
}
impl Resolve for RawAlign {
type Output = Align;
fn resolve(self, styles: StyleChain) -> Self::Output {
let dir = styles.get(TextNode::DIR);
match self {
Self::Start => dir.start().into(),
Self::End => dir.end().into(),
Self::Specific(align) => align,
}
}
}
impl RawAlign {
/// The axis this alignment belongs to.
pub const fn axis(self) -> Axis {
match self {
Self::Start | Self::End => Axis::X,
Self::Specific(align) => align.axis(),
}
}
}
impl From<Align> for RawAlign {
fn from(align: Align) -> Self {
Self::Specific(align)
}
}
impl Debug for RawAlign {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Start => f.pad("start"),
Self::End => f.pad("end"),
Self::Specific(align) => align.fmt(f),
}
}
}
dynamic! {
RawAlign: "alignment",
}
dynamic! {
Axes<RawAlign>: "2d alignment",
}
castable! {
Axes<Option<RawAlign>>,
Expected: "1d or 2d alignment",
@align: RawAlign => {
let mut aligns = Axes::default();
aligns.set(align.axis(), Some(*align));
aligns
},
@aligns: Axes<RawAlign> => aligns.map(Some),
}
/// The unresolved stroke representation.
///
/// In this representation, both fields are optional so that you can pass either
/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
/// this is expected.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct RawStroke<T = Length> {
/// The stroke's paint.
pub paint: Smart<Paint>,
/// The stroke's thickness.
pub thickness: Smart<T>,
}
impl RawStroke<Abs> {
/// Unpack the stroke, filling missing fields from the `default`.
pub fn unwrap_or(self, default: Stroke) -> Stroke {
Stroke {
paint: self.paint.unwrap_or(default.paint),
thickness: self.thickness.unwrap_or(default.thickness),
}
}
/// Unpack the stroke, filling missing fields with the default values.
pub fn unwrap_or_default(self) -> Stroke {
self.unwrap_or(Stroke::default())
}
}
impl Resolve for RawStroke {
type Output = RawStroke<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
RawStroke {
paint: self.paint,
thickness: self.thickness.resolve(styles),
}
}
}
impl Fold for RawStroke<Abs> {
type Output = Self;
fn fold(self, outer: Self::Output) -> Self::Output {
Self {
paint: self.paint.or(outer.paint),
thickness: self.thickness.or(outer.thickness),
}
}
}
impl<T: Debug> Debug for RawStroke<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match (self.paint, &self.thickness) {
(Smart::Custom(paint), Smart::Custom(thickness)) => {
write!(f, "{thickness:?} + {paint:?}")
}
(Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
(Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
(Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
}
}
}
dynamic! {
RawStroke: "stroke",
Value::Length(thickness) => Self {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
},
Value::Color(color) => Self {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
},
}

View File

@ -1,7 +1,7 @@
use std::fmt::{self, Debug, Formatter, Write}; use std::fmt::{self, Debug, Formatter, Write};
use super::{Array, Cast, Dict, Str, Value}; use super::{Array, Cast, Dict, Str, Value};
use crate::diag::{At, SourceResult}; use crate::diag::{bail, At, SourceResult};
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
/// Evaluated arguments to a function. /// Evaluated arguments to a function.

View File

@ -9,8 +9,9 @@ use crate::syntax::Spanned;
use crate::util::ArcExt; use crate::util::ArcExt;
/// Create a new [`Array`] from values. /// Create a new [`Array`] from values.
#[allow(unused_macros)] #[macro_export]
macro_rules! array { #[doc(hidden)]
macro_rules! __array {
($value:expr; $count:expr) => { ($value:expr; $count:expr) => {
$crate::model::Array::from_vec(vec![$value.into(); $count]) $crate::model::Array::from_vec(vec![$value.into(); $count])
}; };
@ -20,6 +21,9 @@ macro_rules! array {
}; };
} }
#[doc(inline)]
pub use crate::__array as array;
/// A reference counted array with value semantics. /// A reference counted array with value semantics.
#[derive(Default, Clone, PartialEq, Hash)] #[derive(Default, Clone, PartialEq, Hash)]
pub struct Array(Arc<Vec<Value>>); pub struct Array(Arc<Vec<Value>>);
@ -97,7 +101,7 @@ impl Array {
.ok_or_else(|| out_of_bounds(index, len))?; .ok_or_else(|| out_of_bounds(index, len))?;
Arc::make_mut(&mut self.0).remove(i); Arc::make_mut(&mut self.0).remove(i);
return Ok(()); Ok(())
} }
/// Extract a contigous subregion of the array. /// Extract a contigous subregion of the array.

View File

@ -1,8 +1,13 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::str::FromStr;
use super::{Pattern, Regex, Value}; use super::{Pattern, Regex, Value};
use crate::diag::{with_alternative, StrResult}; use crate::diag::{with_alternative, StrResult};
use crate::geom::{Corners, Dir, Paint, Sides}; use crate::font::{FontStretch, FontStyle, FontWeight};
use crate::frame::{Destination, Lang, Location, Region};
use crate::geom::{
Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides,
};
use crate::syntax::Spanned; use crate::syntax::Spanned;
use crate::util::EcoString; use crate::util::EcoString;
@ -16,7 +21,9 @@ pub trait Cast<V = Value>: Sized {
} }
/// Implement traits for dynamic types. /// Implement traits for dynamic types.
macro_rules! dynamic { #[macro_export]
#[doc(hidden)]
macro_rules! __dynamic {
($type:ty: $name:literal, $($tts:tt)*) => { ($type:ty: $name:literal, $($tts:tt)*) => {
impl $crate::model::Type for $type { impl $crate::model::Type for $type {
const TYPE_NAME: &'static str = $name; const TYPE_NAME: &'static str = $name;
@ -37,8 +44,13 @@ macro_rules! dynamic {
}; };
} }
#[doc(inline)]
pub use crate::__dynamic as dynamic;
/// Make a type castable from a value. /// Make a type castable from a value.
macro_rules! castable { #[macro_export]
#[doc(hidden)]
macro_rules! __castable {
($type:ty: $inner:ty) => { ($type:ty: $inner:ty) => {
impl $crate::model::Cast<$crate::model::Value> for $type { impl $crate::model::Cast<$crate::model::Value> for $type {
fn is(value: &$crate::model::Value) -> bool { fn is(value: &$crate::model::Value) -> bool {
@ -88,6 +100,9 @@ macro_rules! castable {
}; };
} }
#[doc(inline)]
pub use crate::__castable as castable;
impl Cast for Value { impl Cast for Value {
fn is(_: &Value) -> bool { fn is(_: &Value) -> bool {
true true
@ -119,14 +134,6 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
} }
} }
dynamic! {
Dir: "direction",
}
dynamic! {
Regex: "regular expression",
}
castable! { castable! {
usize, usize,
Expected: "non-negative integer", Expected: "non-negative integer",
@ -170,6 +177,10 @@ castable! {
Value::Str(string) => string.into(), Value::Str(string) => string.into(),
} }
dynamic! {
Regex: "regular expression",
}
castable! { castable! {
Pattern, Pattern,
Expected: "function, string or regular expression", Expected: "function, string or regular expression",
@ -178,6 +189,115 @@ castable! {
@regex: Regex => Self::Regex(regex.clone()), @regex: Regex => Self::Regex(regex.clone()),
} }
dynamic! {
Dir: "direction",
}
dynamic! {
GenAlign: "alignment",
}
dynamic! {
Axes<GenAlign>: "2d alignment",
}
castable! {
Axes<Option<GenAlign>>,
Expected: "1d or 2d alignment",
@align: GenAlign => {
let mut aligns = Axes::default();
aligns.set(align.axis(), Some(*align));
aligns
},
@aligns: Axes<GenAlign> => aligns.map(Some),
}
dynamic! {
PartialStroke: "stroke",
Value::Length(thickness) => Self {
paint: Smart::Auto,
thickness: Smart::Custom(thickness),
},
Value::Color(color) => Self {
paint: Smart::Custom(color.into()),
thickness: Smart::Auto,
},
}
castable! {
Axes<Rel<Length>>,
Expected: "array of two relative lengths",
Value::Array(array) => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => Err("point array must contain exactly two entries")?,
}
},
}
castable! {
Destination,
Expected: "string or dictionary with `page`, `x`, and `y` keys",
Value::Str(string) => Self::Url(string.into()),
Value::Dict(dict) => {
let page = dict.get("page")?.clone().cast()?;
let x: Length = dict.get("x")?.clone().cast()?;
let y: Length = dict.get("y")?.clone().cast()?;
Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) })
},
}
castable! {
FontStyle,
Expected: "string",
Value::Str(string) => match string.as_str() {
"normal" => Self::Normal,
"italic" => Self::Italic,
"oblique" => Self::Oblique,
_ => Err(r#"expected "normal", "italic" or "oblique""#)?,
},
}
castable! {
FontWeight,
Expected: "integer or string",
Value::Int(v) => Value::Int(v)
.cast::<usize>()?
.try_into()
.map_or(Self::BLACK, Self::from_number),
Value::Str(string) => match string.as_str() {
"thin" => Self::THIN,
"extralight" => Self::EXTRALIGHT,
"light" => Self::LIGHT,
"regular" => Self::REGULAR,
"medium" => Self::MEDIUM,
"semibold" => Self::SEMIBOLD,
"bold" => Self::BOLD,
"extrabold" => Self::EXTRABOLD,
"black" => Self::BLACK,
_ => Err("unknown font weight")?,
},
}
castable! {
FontStretch,
Expected: "ratio",
Value::Ratio(v) => Self::from_ratio(v.get() as f32),
}
castable! {
Lang,
Expected: "string",
Value::Str(string) => Self::from_str(&string)?,
}
castable! {
Region,
Expected: "string",
Value::Str(string) => Self::from_str(&string)?,
}
impl<T: Cast> Cast for Option<T> { impl<T: Cast> Cast for Option<T> {
fn is(value: &Value) -> bool { fn is(value: &Value) -> bool {
matches!(value, Value::None) || T::is(value) matches!(value, Value::None) || T::is(value)

View File

@ -9,8 +9,9 @@ use siphasher::sip128::{Hasher128, SipHasher};
use typst_macros::node; use typst_macros::node;
use super::{Args, Key, Property, Selector, StyleEntry, StyleMap, Vm}; use super::{Args, Key, Property, Selector, StyleEntry, StyleMap, Vm};
use crate as typst;
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::util::ReadableTypeId; use crate::util::{EcoString, ReadableTypeId};
/// Composable representation of styled content. /// Composable representation of styled content.
/// ///
@ -26,6 +27,11 @@ impl Content {
SequenceNode(vec![]).pack() SequenceNode(vec![]).pack()
} }
/// Create content from a string of text.
pub fn text(text: impl Into<EcoString>) -> Self {
item!(text)(text.into())
}
/// Create a new sequence node from multiples nodes. /// Create a new sequence node from multiples nodes.
pub fn sequence(seq: Vec<Self>) -> Self { pub fn sequence(seq: Vec<Self>) -> Self {
match seq.as_slice() { match seq.as_slice() {
@ -146,7 +152,7 @@ impl Add for Content {
let mut lhs = self; let mut lhs = self;
if let Some(lhs_mut) = lhs.try_downcast_mut::<SequenceNode>() { if let Some(lhs_mut) = lhs.try_downcast_mut::<SequenceNode>() {
if let Some(rhs_mut) = rhs.try_downcast_mut::<SequenceNode>() { if let Some(rhs_mut) = rhs.try_downcast_mut::<SequenceNode>() {
lhs_mut.0.extend(rhs_mut.0.drain(..)); lhs_mut.0.append(&mut rhs_mut.0);
} else if let Some(rhs) = rhs.downcast::<SequenceNode>() { } else if let Some(rhs) = rhs.downcast::<SequenceNode>() {
lhs_mut.0.extend(rhs.0.iter().cloned()); lhs_mut.0.extend(rhs.0.iter().cloned());
} else { } else {

View File

@ -10,8 +10,9 @@ use crate::syntax::Spanned;
use crate::util::ArcExt; use crate::util::ArcExt;
/// Create a new [`Dict`] from key-value pairs. /// Create a new [`Dict`] from key-value pairs.
#[allow(unused_macros)] #[macro_export]
macro_rules! dict { #[doc(hidden)]
macro_rules! __dict {
($($key:expr => $value:expr),* $(,)?) => {{ ($($key:expr => $value:expr),* $(,)?) => {{
#[allow(unused_mut)] #[allow(unused_mut)]
let mut map = std::collections::BTreeMap::new(); let mut map = std::collections::BTreeMap::new();
@ -20,6 +21,9 @@ macro_rules! dict {
}}; }};
} }
#[doc(inline)]
pub use crate::__dict as dict;
/// A reference-counted dictionary with value semantics. /// A reference-counted dictionary with value semantics.
#[derive(Default, Clone, PartialEq, Hash)] #[derive(Default, Clone, PartialEq, Hash)]
pub struct Dict(Arc<BTreeMap<Str, Value>>); pub struct Dict(Arc<BTreeMap<Str, Value>>);

View File

@ -1,22 +1,19 @@
//! Evaluation of markup into modules. //! Evaluation of markup into modules.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc;
use comemo::{Track, Tracked}; use comemo::{Track, Tracked};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{ use super::{
methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func, methods, ops, Arg, Args, Array, CapturesVisitor, Closure, Content, Dict, Flow, Func,
Node, Pattern, Recipe, Scope, Scopes, Show, StyleEntry, StyleMap, Value, Vm, Pattern, Recipe, Scope, Scopes, Show, StyleEntry, StyleMap, Value, Vm,
}; };
use crate::diag::{At, SourceResult, StrResult, Trace, Tracepoint}; use crate::diag::{bail, error, At, SourceResult, StrResult, Trace, Tracepoint};
use crate::geom::{Abs, Angle, Em, Fr, Ratio}; use crate::geom::{Abs, Angle, Em, Fr, Ratio};
use crate::library::math;
use crate::library::text::TextNode;
use crate::syntax::ast::TypedNode; use crate::syntax::ast::TypedNode;
use crate::syntax::{ast, SourceId, Span, Spanned, Unit}; use crate::syntax::{ast, SourceId, Span, Spanned, Unit};
use crate::util::EcoString; use crate::util::{format_eco, EcoString};
use crate::World; use crate::World;
/// Evaluate a source file and return the resulting module. /// Evaluate a source file and return the resulting module.
@ -39,7 +36,7 @@ pub fn eval(
// Evaluate the module. // Evaluate the module.
let route = unsafe { Route::insert(route, id) }; let route = unsafe { Route::insert(route, id) };
let ast = world.source(id).ast()?; let ast = world.source(id).ast()?;
let std = &world.config().std; let std = &world.config().scope;
let scopes = Scopes::new(Some(std)); let scopes = Scopes::new(Some(std));
let mut vm = Vm::new(world, route.track(), Some(id), scopes); let mut vm = Vm::new(world, route.track(), Some(id), scopes);
let result = ast.eval(&mut vm); let result = ast.eval(&mut vm);
@ -136,8 +133,7 @@ fn eval_markup(
break; break;
} }
eval_markup(vm, nodes)? eval_markup(vm, nodes)?.styled_with_entry(StyleEntry::Recipe(recipe))
.styled_with_entry(StyleEntry::Recipe(recipe).into())
} }
ast::MarkupNode::Expr(ast::Expr::Wrap(wrap)) => { ast::MarkupNode::Expr(ast::Expr::Wrap(wrap)) => {
let tail = eval_markup(vm, nodes)?; let tail = eval_markup(vm, nodes)?;
@ -165,10 +161,13 @@ impl Eval for ast::MarkupNode {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
match self { match self {
Self::Space(v) => v.eval(vm), Self::Space(v) => Ok(match v.newlines() {
0 ..= 1 => (vm.items.space)(),
_ => (vm.items.parbreak)(),
}),
Self::Linebreak(v) => v.eval(vm), Self::Linebreak(v) => v.eval(vm),
Self::Text(v) => v.eval(vm), Self::Text(v) => v.eval(vm),
Self::Escape(v) => v.eval(vm), Self::Escape(v) => Ok((vm.items.text)(v.get().into())),
Self::Shorthand(v) => v.eval(vm), Self::Shorthand(v) => v.eval(vm),
Self::SmartQuote(v) => v.eval(vm), Self::SmartQuote(v) => v.eval(vm),
Self::Strong(v) => v.eval(vm), Self::Strong(v) => v.eval(vm),
@ -187,23 +186,11 @@ impl Eval for ast::MarkupNode {
} }
} }
impl Eval for ast::Space {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(if self.newlines() < 2 {
(vm.items().space)()
} else {
(vm.items().parbreak)()
})
}
}
impl Eval for ast::Linebreak { impl Eval for ast::Linebreak {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().linebreak)(false)) Ok((vm.items.linebreak)(false))
} }
} }
@ -211,15 +198,7 @@ impl Eval for ast::Text {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(vm.text(self.get().clone())) Ok((vm.items.text)(self.get().clone()))
}
}
impl Eval for ast::Escape {
type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(vm.text(self.get()))
} }
} }
@ -227,7 +206,7 @@ impl Eval for ast::Shorthand {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(vm.text(self.get())) Ok((vm.items.text)(self.get().into()))
} }
} }
@ -235,7 +214,7 @@ impl Eval for ast::SmartQuote {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().smart_quote)(self.double())) Ok((vm.items.smart_quote)(self.double()))
} }
} }
@ -243,7 +222,7 @@ impl Eval for ast::Strong {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().strong)(self.body().eval(vm)?)) Ok((vm.items.strong)(self.body().eval(vm)?))
} }
} }
@ -251,7 +230,7 @@ impl Eval for ast::Emph {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().emph)(self.body().eval(vm)?)) Ok((vm.items.emph)(self.body().eval(vm)?))
} }
} }
@ -262,7 +241,7 @@ impl Eval for ast::Raw {
let text = self.text().clone(); let text = self.text().clone();
let lang = self.lang().cloned(); let lang = self.lang().cloned();
let block = self.block(); let block = self.block();
Ok((vm.items().raw)(text, lang, block)) Ok((vm.items.raw)(text, lang, block))
} }
} }
@ -270,7 +249,7 @@ impl Eval for ast::Link {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().link)(self.url().clone())) Ok((vm.items.link)(self.url().clone()))
} }
} }
@ -286,7 +265,7 @@ impl Eval for ast::Ref {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().ref_)(self.get().clone())) Ok((vm.items.ref_)(self.get().clone()))
} }
} }
@ -296,7 +275,7 @@ impl Eval for ast::Heading {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let level = self.level(); let level = self.level();
let body = self.body().eval(vm)?; let body = self.body().eval(vm)?;
Ok((vm.items().heading)(level, body)) Ok((vm.items.heading)(level, body))
} }
} }
@ -304,7 +283,7 @@ impl Eval for ast::ListItem {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok((vm.items().list_item)(self.body().eval(vm)?)) Ok((vm.items.list_item)(self.body().eval(vm)?))
} }
} }
@ -314,7 +293,7 @@ impl Eval for ast::EnumItem {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let number = self.number(); let number = self.number();
let body = self.body().eval(vm)?; let body = self.body().eval(vm)?;
Ok((vm.items().enum_item)(number, body)) Ok((vm.items.enum_item)(number, body))
} }
} }
@ -324,7 +303,7 @@ impl Eval for ast::DescItem {
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let term = self.term().eval(vm)?; let term = self.term().eval(vm)?;
let body = self.body().eval(vm)?; let body = self.body().eval(vm)?;
Ok((vm.items().desc_item)(term, body)) Ok((vm.items.desc_item)(term, body))
} }
} }
@ -332,82 +311,76 @@ impl Eval for ast::Math {
type Output = Content; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
let nodes = self Ok((vm.items.math)(
.children() self.children()
.map(|node| node.eval(vm)) .map(|node| node.eval(vm))
.collect::<SourceResult<_>>()?; .collect::<SourceResult<_>>()?,
Ok(math::MathNode::Row(Arc::new(nodes), self.span()).pack()) self.display(),
))
} }
} }
impl Eval for ast::MathNode { impl Eval for ast::MathNode {
type Output = math::MathNode; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(match self { Ok(match self {
Self::Space(_) => math::MathNode::Space, Self::Space(_) => (vm.items.space)(),
Self::Linebreak(_) => math::MathNode::Linebreak, Self::Linebreak(v) => v.eval(vm)?,
Self::Escape(c) => math::MathNode::Atom(c.get().into()), Self::Escape(v) => (vm.items.math_atom)(v.get().into()),
Self::Atom(atom) => math::MathNode::Atom(atom.get().clone()), Self::Atom(v) => v.eval(vm)?,
Self::Script(node) => node.eval(vm)?, Self::Script(v) => v.eval(vm)?,
Self::Frac(node) => node.eval(vm)?, Self::Frac(v) => v.eval(vm)?,
Self::Align(node) => node.eval(vm)?, Self::Align(v) => v.eval(vm)?,
Self::Group(node) => math::MathNode::Row( Self::Group(v) => v.eval(vm)?,
Arc::new( Self::Expr(v) => match v.eval(vm)? {
node.children() Value::None => Content::empty(),
.map(|node| node.eval(vm)) Value::Int(v) => (vm.items.math_atom)(format_eco!("{}", v)),
.collect::<SourceResult<_>>()?, Value::Float(v) => (vm.items.math_atom)(format_eco!("{}", v)),
), Value::Str(v) => (vm.items.math_atom)(v.into()),
node.span(), Value::Content(v) => v,
), _ => bail!(v.span(), "unexpected garbage"),
Self::Expr(expr) => { },
let content = expr.eval(vm)?.display(vm.world);
if let Some(node) = content.downcast::<TextNode>() {
math::MathNode::Atom(node.0.clone())
} else {
bail!(expr.span(), "expected text")
}
}
}) })
} }
} }
impl Eval for ast::Script { impl Eval for ast::Atom {
type Output = math::MathNode; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(math::MathNode::Script(Arc::new(math::ScriptNode { Ok((vm.items.math_atom)(self.get().clone()))
base: self.base().eval(vm)?, }
sub: self }
.sub()
.map(|node| node.eval(vm)) impl Eval for ast::Script {
.transpose()? type Output = Content;
.map(|node| node.unparen()),
sup: self fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
.sup() Ok((vm.items.math_script)(
.map(|node| node.eval(vm)) self.base().eval(vm)?,
.transpose()? self.sub().map(|node| node.eval(vm)).transpose()?,
.map(|node| node.unparen()), self.sup().map(|node| node.eval(vm)).transpose()?,
}))) ))
} }
} }
impl Eval for ast::Frac { impl Eval for ast::Frac {
type Output = math::MathNode; type Output = Content;
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(math::MathNode::Frac(Arc::new(math::FracNode { Ok((vm.items.math_frac)(
num: self.num().eval(vm)?.unparen(), self.num().eval(vm)?,
denom: self.denom().eval(vm)?.unparen(), self.denom().eval(vm)?,
}))) ))
} }
} }
impl Eval for ast::Align { impl Eval for ast::Align {
type Output = math::MathNode; type Output = Content;
fn eval(&self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
Ok(math::MathNode::Align(self.count())) Ok((vm.items.math_align)(self.count()))
} }
} }
@ -515,7 +488,7 @@ fn eval_code(
} }
ast::Expr::Show(show) => { ast::Expr::Show(show) => {
let recipe = show.eval(vm)?; let recipe = show.eval(vm)?;
let entry = StyleEntry::Recipe(recipe).into(); let entry = StyleEntry::Recipe(recipe);
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;
} }
@ -627,7 +600,7 @@ impl Eval for ast::Unary {
ast::UnOp::Neg => ops::neg(value), ast::UnOp::Neg => ops::neg(value),
ast::UnOp::Not => ops::not(value), ast::UnOp::Not => ops::not(value),
}; };
Ok(result.at(self.span())?) result.at(self.span())
} }
} }
@ -676,7 +649,7 @@ impl ast::Binary {
} }
let rhs = self.rhs().eval(vm)?; let rhs = self.rhs().eval(vm)?;
Ok(op(lhs, rhs).at(self.span())?) op(lhs, rhs).at(self.span())
} }
/// Apply an assignment operation. /// Apply an assignment operation.
@ -708,8 +681,7 @@ impl Eval for ast::FieldAccess {
.to::<dyn Show>() .to::<dyn Show>()
.and_then(|node| node.field(&field)) .and_then(|node| node.field(&field))
.ok_or_else(|| format!("unknown field {field:?}")) .ok_or_else(|| format!("unknown field {field:?}"))
.at(span)? .at(span)?,
.clone(),
v => bail!( v => bail!(
self.target().span(), self.target().span(),
@ -754,9 +726,8 @@ impl Eval for ast::MethodCall {
Ok(if methods::is_mutating(&method) { Ok(if methods::is_mutating(&method) {
let args = self.args().eval(vm)?; let args = self.args().eval(vm)?;
let mut value = self.target().access(vm)?; let value = self.target().access(vm)?;
methods::call_mut(&mut value, &method, args, span) methods::call_mut(value, &method, args, span).trace(vm.world, point, span)?;
.trace(vm.world, point, span)?;
Value::None Value::None
} else { } else {
let value = self.target().eval(vm)?; let value = self.target().eval(vm)?;
@ -882,7 +853,7 @@ impl Eval for ast::SetRule {
let target = self.target(); let target = self.target();
let target = target.eval(vm)?.cast::<Func>().at(target.span())?; let target = target.eval(vm)?.cast::<Func>().at(target.span())?;
let args = self.args().eval(vm)?; let args = self.args().eval(vm)?;
Ok(target.set(args)?) target.set(args)
} }
} }
@ -1085,14 +1056,14 @@ impl Eval for ast::ModuleInclude {
let span = self.path().span(); let span = self.path().span();
let path = self.path().eval(vm)?.cast::<EcoString>().at(span)?; let path = self.path().eval(vm)?.cast::<EcoString>().at(span)?;
let module = import(vm, &path, span)?; let module = import(vm, &path, span)?;
Ok(module.content.clone()) Ok(module.content)
} }
} }
/// Process an import of a module relative to the current location. /// Process an import of a module relative to the current location.
fn import(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> { fn import(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
// Load the source file. // Load the source file.
let full = vm.locate(&path).at(span)?; let full = vm.locate(path).at(span)?;
let id = vm.world.resolve(&full).at(span)?; let id = vm.world.resolve(&full).at(span)?;
// Prevent cyclic importing. // Prevent cyclic importing.

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use comemo::{Track, Tracked}; use comemo::{Track, Tracked};
use super::{Args, Eval, Flow, Node, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm}; use super::{Args, Eval, Flow, Node, NodeId, Route, Scope, Scopes, StyleMap, Value, Vm};
use crate::diag::{SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::syntax::ast::{self, Expr, TypedNode}; use crate::syntax::ast::{self, Expr, TypedNode};
use crate::syntax::{SourceId, SyntaxNode}; use crate::syntax::{SourceId, SyntaxNode};
use crate::util::EcoString; use crate::util::EcoString;
@ -229,7 +229,7 @@ impl Closure {
} }
/// A visitor that determines which variables to capture for a closure. /// A visitor that determines which variables to capture for a closure.
pub struct CapturesVisitor<'a> { pub(super) struct CapturesVisitor<'a> {
external: &'a Scopes<'a>, external: &'a Scopes<'a>,
internal: Scopes<'a>, internal: Scopes<'a>,
captures: Scope, captures: Scope,

123
src/model/items.rs Normal file
View File

@ -0,0 +1,123 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use comemo::Tracked;
use once_cell::sync::OnceCell;
use super::{Content, StyleChain};
use crate::diag::SourceResult;
use crate::frame::Frame;
use crate::geom::{Abs, Dir};
use crate::util::{hash128, EcoString};
use crate::World;
/// Global storage for lang items.
#[doc(hidden)]
pub static LANG_ITEMS: OnceCell<LangItems> = OnceCell::new();
/// Set the lang items. This is a hack :(
///
/// Passing the lang items everywhere they are needed (especially the text node
/// 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.
pub fn set_lang_items(items: LangItems) {
if LANG_ITEMS.set(items).is_err() {
let first = hash128(LANG_ITEMS.get().unwrap());
let second = hash128(&items);
assert_eq!(first, second, "set differing lang items");
}
}
/// Access a lang item.
macro_rules! item {
($name:ident) => {
$crate::model::LANG_ITEMS.get().unwrap().$name
};
}
/// Definition of certain standard library items the language is aware of.
#[derive(Copy, Clone)]
pub struct LangItems {
/// The root layout function.
pub root:
fn(world: Tracked<dyn World>, document: &Content) -> SourceResult<Vec<Frame>>,
/// Access the em size.
pub em: fn(StyleChain) -> Abs,
/// Access the text direction.
pub dir: fn(StyleChain) -> Dir,
/// A space.
pub space: fn() -> Content,
/// A forced line break.
pub linebreak: fn(justify: bool) -> Content,
/// Plain text.
pub text: fn(text: EcoString) -> Content,
/// 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,
/// A raw block with optional syntax highlighting: `` `...` ``.
pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content,
/// A hyperlink: `https://typst.org`.
pub link: fn(url: EcoString) -> Content,
/// A reference: `@target`.
pub ref_: fn(target: EcoString) -> Content,
/// A section heading: `= Introduction`.
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
/// An item in an unordered list: `- ...`.
pub list_item: fn(body: Content) -> Content,
/// An item in an enumeration (ordered list): `1. ...`.
pub enum_item: fn(number: Option<usize>, body: Content) -> Content,
/// An item in a description list: `/ Term: Details`.
pub desc_item: fn(term: Content, body: Content) -> Content,
/// A math formula: `$x$`, `$ x^2 $`.
pub math: fn(children: Vec<Content>, display: bool) -> Content,
/// A atom in a formula: `x`, `+`, `12`.
pub math_atom: fn(atom: EcoString) -> Content,
/// A base with an optional sub- and superscript in a formula: `a_1^2`.
pub math_script:
fn(base: Content, sub: Option<Content>, sup: Option<Content>) -> Content,
/// A fraction in a formula: `x/2`
pub math_frac: fn(num: Content, denom: Content) -> Content,
/// An alignment indicator in a formula: `&`, `&&`.
pub math_align: fn(count: usize) -> 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.root 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.smart_quote.hash(state);
self.parbreak.hash(state);
self.strong.hash(state);
self.emph.hash(state);
self.raw.hash(state);
self.link.hash(state);
self.ref_.hash(state);
self.heading.hash(state);
self.list_item.hash(state);
self.enum_item.hash(state);
self.desc_item.hash(state);
self.math.hash(state);
self.math_atom.hash(state);
self.math_script.hash(state);
self.math_frac.hash(state);
self.math_align.hash(state);
}
}

View File

@ -97,7 +97,7 @@ pub fn call(
}, },
Value::Func(func) => match method { Value::Func(func) => match method {
"with" => Value::Func(func.clone().with(args.take())), "with" => Value::Func(func.with(args.take())),
_ => return missing(), _ => return missing(),
}, },

View File

@ -1,5 +1,7 @@
//! Layout and computation model. //! Layout and computation model.
#[macro_use]
mod items;
#[macro_use] #[macro_use]
mod cast; mod cast;
#[macro_use] #[macro_use]
@ -16,12 +18,11 @@ mod args;
mod content; mod content;
mod eval; mod eval;
mod func; mod func;
mod methods;
mod ops;
mod scope; mod scope;
mod vm; mod vm;
pub mod methods;
pub mod ops;
pub use self::str::*; pub use self::str::*;
pub use args::*; pub use args::*;
pub use array::*; pub use array::*;
@ -30,6 +31,7 @@ pub use content::*;
pub use dict::*; pub use dict::*;
pub use eval::*; pub use eval::*;
pub use func::*; pub use func::*;
pub use items::*;
pub use scope::*; pub use scope::*;
pub use styles::*; pub use styles::*;
pub use value::*; pub use value::*;

View File

@ -1,12 +1,9 @@
//! Operations on values. //! Operations on values.
use std::cmp::Ordering; use super::{Regex, Smart, Value};
use super::{Node, Regex, Smart, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Axes, Axis, Length, Numeric, Rel}; use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel};
use crate::library::text::TextNode; use std::cmp::Ordering;
use crate::library::{RawAlign, RawStroke};
use Value::*; use Value::*;
/// Bail with a type mismatch error. /// Bail with a type mismatch error.
@ -22,8 +19,8 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(a, None) => a, (a, None) => a,
(None, b) => b, (None, b) => b,
(Str(a), Str(b)) => Str(a + b), (Str(a), Str(b)) => Str(a + b),
(Str(a), Content(b)) => Content(TextNode(a.into()).pack() + b), (Str(a), Content(b)) => Content(super::Content::text(a) + b),
(Content(a), Str(b)) => Content(a + TextNode(b.into()).pack()), (Content(a), Str(b)) => Content(a + super::Content::text(b)),
(Content(a), Content(b)) => Content(a + b), (Content(a), Content(b)) => Content(a + b),
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
@ -88,14 +85,14 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Str(a), Str(b)) => Str(a + b), (Str(a), Str(b)) => Str(a + b),
(Content(a), Content(b)) => Content(a + b), (Content(a), Content(b)) => Content(a + b),
(Content(a), Str(b)) => Content(a + TextNode(b.into()).pack()), (Content(a), Str(b)) => Content(a + super::Content::text(b)),
(Str(a), Content(b)) => Content(TextNode(a.into()).pack() + b), (Str(a), Content(b)) => Content(super::Content::text(a) + b),
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
Value::dynamic(RawStroke { Value::dynamic(PartialStroke {
paint: Smart::Custom(color.into()), paint: Smart::Custom(color.into()),
thickness: Smart::Custom(thickness), thickness: Smart::Custom(thickness),
}) })
@ -104,7 +101,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
(Dyn(a), Dyn(b)) => { (Dyn(a), Dyn(b)) => {
// 1D alignments can be summed into 2D alignments. // 1D alignments can be summed into 2D alignments.
if let (Some(&a), Some(&b)) = if let (Some(&a), Some(&b)) =
(a.downcast::<RawAlign>(), b.downcast::<RawAlign>()) (a.downcast::<GenAlign>(), b.downcast::<GenAlign>())
{ {
if a.axis() != b.axis() { if a.axis() != b.axis() {
Value::dynamic(match a.axis() { Value::dynamic(match a.axis() {

View File

@ -5,19 +5,23 @@ use std::ops::{Add, AddAssign, Deref};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{Array, Dict, Value}; use super::{castable, dict, Array, Dict, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::library::RawAlign; use crate::geom::GenAlign;
use crate::util::EcoString; use crate::util::EcoString;
/// Create a new [`Str`] from a format string. /// Create a new [`Str`] from a format string.
#[allow(unused_macros)] #[macro_export]
macro_rules! format_str { #[doc(hidden)]
macro_rules! __format_str {
($($tts:tt)*) => {{ ($($tts:tt)*) => {{
$crate::model::Str::from(format_eco!($($tts)*)) $crate::model::Str::from(format_eco!($($tts)*))
}}; }};
} }
#[doc(inline)]
pub use crate::__format_str as format_str;
/// An immutable reference counted string. /// An immutable reference counted string.
#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Str(EcoString); pub struct Str(EcoString);
@ -463,9 +467,9 @@ pub enum StrSide {
castable! { castable! {
StrSide, StrSide,
Expected: "start or end", Expected: "start or end",
@align: RawAlign => match align { @align: GenAlign => match align {
RawAlign::Start => Self::Start, GenAlign::Start => Self::Start,
RawAlign::End => Self::End, GenAlign::End => Self::End,
_ => Err("expected either `start` or `end`")?, _ => Err("expected either `start` or `end`")?,
}, },
} }

View File

@ -7,12 +7,11 @@ use std::sync::Arc;
use comemo::{Prehashed, Tracked}; use comemo::{Prehashed, Tracked};
use super::{capability, Args, Content, Func, Node, NodeId, Regex, Smart, Value}; use super::{capability, Args, Content, Func, NodeId, Regex, Smart, Value};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::geom::{Abs, Axes, Corners, Em, Length, Numeric, Rel, Sides}; use crate::geom::{
use crate::library::layout::PageNode; Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides,
use crate::library::structure::{DescNode, EnumNode, ListNode}; };
use crate::library::text::{ParNode, TextNode};
use crate::syntax::Spanned; use crate::syntax::Spanned;
use crate::util::ReadableTypeId; use crate::util::ReadableTypeId;
use crate::World; use crate::World;
@ -111,9 +110,9 @@ impl StyleMap {
self self
} }
/// The highest-level kind of of structure the map interrupts. /// Whether this map contains styles for the given `node.`
pub fn interruption(&self) -> Option<Interruption> { pub fn interrupts<T: 'static>(&self) -> bool {
self.0.iter().filter_map(|entry| entry.interruption()).max() self.0.iter().any(|entry| entry.is_of(NodeId::of::<T>()))
} }
} }
@ -132,17 +131,6 @@ impl Debug for StyleMap {
} }
} }
/// Determines whether a style could interrupt some composable structure.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Interruption {
/// The style forces a list break.
List,
/// The style forces a paragraph break.
Par,
/// The style forces a page break.
Page,
}
/// An entry for a single style property, recipe or barrier. /// An entry for a single style property, recipe or barrier.
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
pub enum StyleEntry { pub enum StyleEntry {
@ -193,12 +181,12 @@ impl StyleEntry {
} }
} }
/// The highest-level kind of structure the entry interrupts. /// Whether this entry contains styles for the given `node.`
pub fn interruption(&self) -> Option<Interruption> { pub fn is_of(&self, node: NodeId) -> bool {
match self { match self {
Self::Property(property) => property.interruption(), Self::Property(property) => property.is_of(node),
Self::Recipe(recipe) => recipe.interruption(), Self::Recipe(recipe) => recipe.is_of(node),
_ => None, _ => false,
} }
} }
} }
@ -397,7 +385,7 @@ impl<'a, K: Key<'a>> Iterator for Values<'a, K> {
type Item = &'a K::Value; type Item = &'a K::Value;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
while let Some(entry) = self.entries.next() { for entry in &mut self.entries {
match entry { match entry {
StyleEntry::Property(property) => { StyleEntry::Property(property) => {
if let Some(value) = property.downcast::<K>() { if let Some(value) = property.downcast::<K>() {
@ -662,9 +650,9 @@ impl Property {
self.key == KeyId::of::<K>() self.key == KeyId::of::<K>()
} }
/// Whether this property belongs to the node `T`. /// Whether this property belongs to the node with the given id.
pub fn is_of<T: 'static>(&self) -> bool { pub fn is_of(&self, node: NodeId) -> bool {
self.node == NodeId::of::<T>() self.node == node
} }
/// Access the property's value if it is of the given key. /// Access the property's value if it is of the given key.
@ -690,22 +678,6 @@ impl Property {
pub fn make_scoped(&mut self) { pub fn make_scoped(&mut self) {
self.scoped = true; self.scoped = true;
} }
/// What kind of structure the property interrupts.
pub fn interruption(&self) -> Option<Interruption> {
if self.is_of::<PageNode>() {
Some(Interruption::Page)
} else if self.is_of::<ParNode>() {
Some(Interruption::Par)
} else if self.is_of::<ListNode>()
|| self.is_of::<EnumNode>()
|| self.is_of::<DescNode>()
{
Some(Interruption::List)
} else {
None
}
}
} }
impl Debug for Property { impl Debug for Property {
@ -826,7 +798,7 @@ impl Resolve for Em {
if self.is_zero() { if self.is_zero() {
Abs::zero() Abs::zero()
} else { } else {
self.at(styles.get(TextNode::SIZE)) self.at(item!(em)(styles))
} }
} }
} }
@ -891,6 +863,30 @@ where
} }
} }
impl Resolve for GenAlign {
type Output = Align;
fn resolve(self, styles: StyleChain) -> Self::Output {
let dir = item!(dir)(styles);
match self {
Self::Start => dir.start().into(),
Self::End => dir.end().into(),
Self::Specific(align) => align,
}
}
}
impl Resolve for PartialStroke {
type Output = PartialStroke<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
PartialStroke {
paint: self.paint,
thickness: self.thickness.resolve(styles),
}
}
}
/// A property that is folded to determine its final value. /// A property that is folded to determine its final value.
pub trait Fold { pub trait Fold {
/// The type of the folded output. /// The type of the folded output.
@ -970,6 +966,17 @@ impl Fold for Corners<Option<Rel<Abs>>> {
} }
} }
impl Fold for PartialStroke<Abs> {
type Output = Self;
fn fold(self, outer: Self::Output) -> Self::Output {
Self {
paint: self.paint.or(outer.paint),
thickness: self.thickness.or(outer.thickness),
}
}
}
/// A show rule recipe. /// A show rule recipe.
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
pub struct Recipe { pub struct Recipe {
@ -1003,13 +1010,14 @@ impl Recipe {
} }
(Target::Text(text), Pattern::Regex(regex)) => { (Target::Text(text), Pattern::Regex(regex)) => {
let make = world.config().items.text;
let mut result = vec![]; let mut result = vec![];
let mut cursor = 0; let mut cursor = 0;
for mat in regex.find_iter(text) { for mat in regex.find_iter(text) {
let start = mat.start(); let start = mat.start();
if cursor < start { if cursor < start {
result.push(TextNode(text[cursor .. start].into()).pack()); result.push(make(text[cursor .. start].into()));
} }
result.push(self.call(world, || Value::Str(mat.as_str().into()))?); result.push(self.call(world, || Value::Str(mat.as_str().into()))?);
@ -1021,7 +1029,7 @@ impl Recipe {
} }
if cursor < text.len() { if cursor < text.len() {
result.push(TextNode(text[cursor ..].into()).pack()); result.push(make(text[cursor ..].into()));
} }
Content::sequence(result) Content::sequence(result)
@ -1047,19 +1055,13 @@ impl Recipe {
Ok(self.func.v.call_detached(world, args)?.display(world)) Ok(self.func.v.call_detached(world, args)?.display(world))
} }
/// What kind of structure the property interrupts. /// Whether this recipe is for the given node.
pub fn interruption(&self) -> Option<Interruption> { pub fn is_of(&self, node: NodeId) -> bool {
if let Pattern::Node(id) = self.pattern { match self.pattern {
if id == NodeId::of::<ListNode>() Pattern::Node(id) => id == node,
|| id == NodeId::of::<EnumNode>() _ => false,
|| id == NodeId::of::<DescNode>()
{
return Some(Interruption::List);
} }
} }
None
}
} }
impl Debug for Recipe { impl Debug for Recipe {

View File

@ -7,11 +7,10 @@ use std::sync::Arc;
use comemo::Tracked; use comemo::Tracked;
use siphasher::sip128::{Hasher128, SipHasher}; use siphasher::sip128::{Hasher128, SipHasher};
use super::{ops, Args, Array, Cast, Content, Dict, Func, Node, Str}; use super::{format_str, ops, Args, Array, Cast, Content, Dict, Func, Str};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor};
use crate::library::text::TextNode; use crate::util::{format_eco, EcoString};
use crate::util::EcoString;
use crate::World; use crate::World;
/// A computational value. /// A computational value.
@ -385,7 +384,7 @@ primitive! { Str: "string", Str }
primitive! { Content: "content", primitive! { Content: "content",
Content, Content,
None => Content::empty(), None => Content::empty(),
Str(text) => TextNode(text.into()).pack() Str(text) => Content::text(text)
} }
primitive! { Array: "array", Array } primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict } primitive! { Dict: "dictionary", Dict }
@ -395,6 +394,7 @@ primitive! { Args: "arguments", Args }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::model::{array, dict};
#[track_caller] #[track_caller]
fn test(value: impl Into<Value>, exp: &str) { fn test(value: impl Into<Value>, exp: &str) {

View File

@ -2,11 +2,11 @@ use std::path::PathBuf;
use comemo::Tracked; use comemo::Tracked;
use super::{Content, Route, Scopes, Value}; use super::{LangItems, Route, Scopes, Value};
use crate::diag::{SourceError, StrResult}; use crate::diag::{error, SourceError, StrResult};
use crate::syntax::{SourceId, Span}; use crate::syntax::{SourceId, Span};
use crate::util::{EcoString, PathExt}; use crate::util::PathExt;
use crate::{LangItems, World}; use crate::World;
/// A virtual machine. /// A virtual machine.
pub struct Vm<'a> { pub struct Vm<'a> {
@ -20,6 +20,8 @@ pub struct Vm<'a> {
pub scopes: Scopes<'a>, pub scopes: Scopes<'a>,
/// A control flow event that is currently happening. /// A control flow event that is currently happening.
pub flow: Option<Flow>, pub flow: Option<Flow>,
/// The language items.
pub items: LangItems,
} }
impl<'a> Vm<'a> { impl<'a> Vm<'a> {
@ -36,6 +38,7 @@ impl<'a> Vm<'a> {
location, location,
scopes, scopes,
flow: None, flow: None,
items: world.config().items,
} }
} }
@ -54,18 +57,6 @@ impl<'a> Vm<'a> {
Err("cannot access file system from here".into()) Err("cannot access file system from here".into())
} }
/// The language items.
pub fn items(&self) -> &LangItems {
&self.world.config().items
}
/// Create text content.
///
/// This is a shorthand for `(vm.items().text)(..)`.
pub fn text(&self, text: impl Into<EcoString>) -> Content {
(self.items().text)(text.into())
}
} }
/// A control flow event that occurred during evaluation. /// A control flow event that occurred during evaluation.

View File

@ -55,7 +55,7 @@ node! {
impl Markup { impl Markup {
/// The children. /// The children.
pub fn children(&self) -> impl Iterator<Item = MarkupNode> + '_ { pub fn children(&self) -> impl DoubleEndedIterator<Item = MarkupNode> + '_ {
self.0.children().filter_map(SyntaxNode::cast) self.0.children().filter_map(SyntaxNode::cast)
} }
} }
@ -166,7 +166,7 @@ impl Space {
} }
node! { node! {
/// A forced line break. /// A forced line break: `\`.
Linebreak Linebreak
} }
@ -414,9 +414,15 @@ node! {
impl Math { impl Math {
/// The children. /// The children.
pub fn children(&self) -> impl Iterator<Item = MathNode> + '_ { pub fn children(&self) -> impl DoubleEndedIterator<Item = MathNode> + '_ {
self.0.children().filter_map(SyntaxNode::cast) self.0.children().filter_map(SyntaxNode::cast)
} }
/// Whether this is a display-level math formula.
pub fn display(&self) -> bool {
matches!(self.children().next(), Some(MathNode::Space(_)))
&& matches!(self.children().last(), Some(MathNode::Space(_)))
}
} }
/// A single piece of a math formula. /// A single piece of a math formula.
@ -424,7 +430,7 @@ impl Math {
pub enum MathNode { pub enum MathNode {
/// Whitespace. /// Whitespace.
Space(Space), Space(Space),
/// A forced line break. /// A forced line break: `\`.
Linebreak(Linebreak), Linebreak(Linebreak),
/// An escape sequence: `\#`, `\u{1F5FA}`. /// An escape sequence: `\#`, `\u{1F5FA}`.
Escape(Escape), Escape(Escape),
@ -535,7 +541,7 @@ impl Frac {
} }
node! { node! {
/// A math alignment indicator: `&`, `&&`. /// An alignment indicator in a formula: `&`, `&&`.
Align Align
} }
@ -736,7 +742,7 @@ node! {
impl CodeBlock { impl CodeBlock {
/// The list of expressions contained in the block. /// The list of expressions contained in the block.
pub fn exprs(&self) -> impl Iterator<Item = Expr> + '_ { pub fn exprs(&self) -> impl DoubleEndedIterator<Item = Expr> + '_ {
self.0.children().filter_map(SyntaxNode::cast) self.0.children().filter_map(SyntaxNode::cast)
} }
} }
@ -774,7 +780,7 @@ node! {
impl Array { impl Array {
/// The array's items. /// The array's items.
pub fn items(&self) -> impl Iterator<Item = ArrayItem> + '_ { pub fn items(&self) -> impl DoubleEndedIterator<Item = ArrayItem> + '_ {
self.0.children().filter_map(SyntaxNode::cast) self.0.children().filter_map(SyntaxNode::cast)
} }
} }
@ -811,7 +817,7 @@ node! {
impl Dict { impl Dict {
/// The dictionary's items. /// The dictionary's items.
pub fn items(&self) -> impl Iterator<Item = DictItem> + '_ { pub fn items(&self) -> impl DoubleEndedIterator<Item = DictItem> + '_ {
self.0.children().filter_map(SyntaxNode::cast) self.0.children().filter_map(SyntaxNode::cast)
} }
} }
@ -1204,7 +1210,7 @@ node! {
impl Args { impl Args {
/// The positional and named arguments. /// The positional and named arguments.
pub fn items(&self) -> impl Iterator<Item = Arg> + '_ { pub fn items(&self) -> impl DoubleEndedIterator<Item = Arg> + '_ {
self.0.children().filter_map(SyntaxNode::cast) self.0.children().filter_map(SyntaxNode::cast)
} }
} }
@ -1252,7 +1258,7 @@ impl Closure {
} }
/// The parameter bindings. /// The parameter bindings.
pub fn params(&self) -> impl Iterator<Item = Param> + '_ { pub fn params(&self) -> impl DoubleEndedIterator<Item = Param> + '_ {
self.0 self.0
.children() .children()
.find(|x| x.kind() == &NodeKind::Params) .find(|x| x.kind() == &NodeKind::Params)

View File

@ -97,7 +97,7 @@ where
} }
} }
let highlighter = Highlighter::new(&theme); let highlighter = Highlighter::new(theme);
process(0, root, vec![], &highlighter, &mut f); process(0, root, vec![], &highlighter, &mut f);
} }

View File

@ -235,17 +235,17 @@ fn replace(
let (newborns, terminated, amount) = match mode { let (newborns, terminated, amount) = match mode {
ReparseMode::Code => reparse_code_block( ReparseMode::Code => reparse_code_block(
&prefix, prefix,
&change.text[newborn_span.start ..], &change.text[newborn_span.start ..],
newborn_span.len(), newborn_span.len(),
), ),
ReparseMode::Content => reparse_content_block( ReparseMode::Content => reparse_content_block(
&prefix, prefix,
&change.text[newborn_span.start ..], &change.text[newborn_span.start ..],
newborn_span.len(), newborn_span.len(),
), ),
ReparseMode::MarkupElements { at_start, min_indent } => reparse_markup_elements( ReparseMode::MarkupElements { at_start, min_indent } => reparse_markup_elements(
&prefix, prefix,
&change.text[newborn_span.start ..], &change.text[newborn_span.start ..],
newborn_span.len(), newborn_span.len(),
differential, differential,
@ -385,7 +385,8 @@ enum ReparseMode {
/// Whether changes _inside_ this node are safely encapsulated, so that only /// Whether changes _inside_ this node are safely encapsulated, so that only
/// this node must be reparsed. /// this node must be reparsed.
fn is_bounded(kind: &NodeKind) -> bool { fn is_bounded(kind: &NodeKind) -> bool {
match kind { matches!(
kind,
NodeKind::CodeBlock NodeKind::CodeBlock
| NodeKind::ContentBlock | NodeKind::ContentBlock
| NodeKind::Linebreak | NodeKind::Linebreak
@ -393,9 +394,8 @@ fn is_bounded(kind: &NodeKind) -> bool {
| NodeKind::BlockComment | NodeKind::BlockComment
| NodeKind::Space { .. } | NodeKind::Space { .. }
| NodeKind::Escape(_) | NodeKind::Escape(_)
| NodeKind::Shorthand(_) => true, | NodeKind::Shorthand(_)
_ => false, )
}
} }
/// Whether `at_start` would still be true after this node given the /// Whether `at_start` would still be true after this node given the

View File

@ -99,50 +99,6 @@ impl SyntaxNode {
self.children().rev().find_map(Self::cast) self.children().rev().find_map(Self::cast)
} }
/// Change the type of the node.
pub fn convert(&mut self, kind: NodeKind) {
match self {
Self::Inner(inner) => {
let node = Arc::make_mut(inner);
node.erroneous |= kind.is_error();
node.data.kind = kind;
}
Self::Leaf(leaf) => leaf.kind = kind,
}
}
/// Set a synthetic span for the node and all its descendants.
pub fn synthesize(&mut self, span: Span) {
match self {
Self::Inner(inner) => Arc::make_mut(inner).synthesize(span),
Self::Leaf(leaf) => leaf.synthesize(span),
}
}
/// Assign spans to each node.
pub fn numberize(&mut self, id: SourceId, within: Range<u64>) -> NumberingResult {
match self {
Self::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within),
Self::Leaf(leaf) => leaf.numberize(id, within),
}
}
/// The upper bound of assigned numbers in this subtree.
pub fn upper(&self) -> u64 {
match self {
Self::Inner(inner) => inner.upper(),
Self::Leaf(leaf) => leaf.span().number() + 1,
}
}
/// If the span points into this node, convert it to a byte range.
pub fn range(&self, span: Span, offset: usize) -> Option<Range<usize>> {
match self {
Self::Inner(inner) => inner.range(span, offset),
Self::Leaf(leaf) => leaf.range(span, offset),
}
}
/// Returns all leaf descendants of this node (may include itself). /// Returns all leaf descendants of this node (may include itself).
/// ///
/// This method is slow and only intended for testing. /// This method is slow and only intended for testing.
@ -156,6 +112,54 @@ impl SyntaxNode {
self.children().flat_map(Self::leafs).collect() self.children().flat_map(Self::leafs).collect()
} }
} }
/// Change the type of the node.
pub(super) fn convert(&mut self, kind: NodeKind) {
match self {
Self::Inner(inner) => {
let node = Arc::make_mut(inner);
node.erroneous |= kind.is_error();
node.data.kind = kind;
}
Self::Leaf(leaf) => leaf.kind = kind,
}
}
/// Set a synthetic span for the node and all its descendants.
pub(super) fn synthesize(&mut self, span: Span) {
match self {
Self::Inner(inner) => Arc::make_mut(inner).synthesize(span),
Self::Leaf(leaf) => leaf.synthesize(span),
}
}
/// Assign spans to each node.
pub(super) fn numberize(
&mut self,
id: SourceId,
within: Range<u64>,
) -> NumberingResult {
match self {
Self::Inner(inner) => Arc::make_mut(inner).numberize(id, None, within),
Self::Leaf(leaf) => leaf.numberize(id, within),
}
}
/// If the span points into this node, convert it to a byte range.
pub(super) fn range(&self, span: Span, offset: usize) -> Option<Range<usize>> {
match self {
Self::Inner(inner) => inner.range(span, offset),
Self::Leaf(leaf) => leaf.range(span, offset),
}
}
/// The upper bound of assigned numbers in this subtree.
fn upper(&self) -> u64 {
match self {
Self::Inner(inner) => inner.upper(),
Self::Leaf(leaf) => leaf.span().number() + 1,
}
}
} }
impl Default for SyntaxNode { impl Default for SyntaxNode {
@ -246,7 +250,7 @@ impl InnerNode {
} }
/// Set a synthetic span for the node and all its descendants. /// Set a synthetic span for the node and all its descendants.
pub fn synthesize(&mut self, span: Span) { fn synthesize(&mut self, span: Span) {
self.data.synthesize(span); self.data.synthesize(span);
for child in &mut self.children { for child in &mut self.children {
child.synthesize(span); child.synthesize(span);
@ -255,7 +259,7 @@ impl InnerNode {
/// Assign span numbers `within` an interval to this node's subtree or just /// Assign span numbers `within` an interval to this node's subtree or just
/// a `range` of its children. /// a `range` of its children.
pub fn numberize( fn numberize(
&mut self, &mut self,
id: SourceId, id: SourceId,
range: Option<Range<usize>>, range: Option<Range<usize>>,
@ -304,12 +308,12 @@ impl InnerNode {
} }
/// The upper bound of assigned numbers in this subtree. /// The upper bound of assigned numbers in this subtree.
pub fn upper(&self) -> u64 { fn upper(&self) -> u64 {
self.upper self.upper
} }
/// If the span points into this node, convert it to a byte range. /// If the span points into this node, convert it to a byte range.
pub fn range(&self, span: Span, mut offset: usize) -> Option<Range<usize>> { fn range(&self, span: Span, mut offset: usize) -> Option<Range<usize>> {
// Check whether we found it. // Check whether we found it.
if let Some(range) = self.data.range(span, offset) { if let Some(range) = self.data.range(span, offset) {
return Some(range); return Some(range);
@ -343,14 +347,14 @@ impl InnerNode {
} }
/// The node's children, mutably. /// The node's children, mutably.
pub(crate) fn children_mut(&mut self) -> &mut [SyntaxNode] { pub(super) fn children_mut(&mut self) -> &mut [SyntaxNode] {
&mut self.children &mut self.children
} }
/// Replaces a range of children with a replacement. /// Replaces a range of children with a replacement.
/// ///
/// May have mutated the children if it returns `Err(_)`. /// May have mutated the children if it returns `Err(_)`.
pub(crate) fn replace_children( pub(super) fn replace_children(
&mut self, &mut self,
mut range: Range<usize>, mut range: Range<usize>,
replacement: Vec<SyntaxNode>, replacement: Vec<SyntaxNode>,
@ -430,7 +434,7 @@ impl InnerNode {
} }
/// Update this node after changes were made to one of its children. /// Update this node after changes were made to one of its children.
pub(crate) fn update_parent( pub(super) fn update_parent(
&mut self, &mut self,
prev_len: usize, prev_len: usize,
new_len: usize, new_len: usize,
@ -509,12 +513,12 @@ impl NodeData {
} }
/// Set a synthetic span for the node. /// Set a synthetic span for the node.
pub fn synthesize(&mut self, span: Span) { fn synthesize(&mut self, span: Span) {
self.span = span; self.span = span;
} }
/// Assign a span to the node. /// Assign a span to the node.
pub fn numberize(&mut self, id: SourceId, within: Range<u64>) -> NumberingResult { fn numberize(&mut self, id: SourceId, within: Range<u64>) -> NumberingResult {
if within.start < within.end { if within.start < within.end {
self.span = Span::new(id, (within.start + within.end) / 2); self.span = Span::new(id, (within.start + within.end) / 2);
Ok(()) Ok(())
@ -524,7 +528,7 @@ impl NodeData {
} }
/// If the span points into this node, convert it to a byte range. /// If the span points into this node, convert it to a byte range.
pub fn range(&self, span: Span, offset: usize) -> Option<Range<usize>> { fn range(&self, span: Span, offset: usize) -> Option<Range<usize>> {
(self.span == span).then(|| offset .. offset + self.len()) (self.span == span).then(|| offset .. offset + self.len())
} }
} }

View File

@ -3,7 +3,7 @@ use std::mem;
use std::ops::Range; use std::ops::Range;
use super::{ErrorPos, InnerNode, NodeData, NodeKind, SyntaxNode, TokenMode, Tokens}; use super::{ErrorPos, InnerNode, NodeData, NodeKind, SyntaxNode, TokenMode, Tokens};
use crate::util::EcoString; use crate::util::{format_eco, EcoString};
/// A convenient token-based parser. /// A convenient token-based parser.
pub struct Parser<'s> { pub struct Parser<'s> {

View File

@ -110,11 +110,11 @@ const fn to_non_zero(v: u64) -> NonZeroU64 {
} }
/// Result of numbering a node within an interval. /// Result of numbering a node within an interval.
pub type NumberingResult = Result<(), Unnumberable>; pub(super) type NumberingResult = Result<(), Unnumberable>;
/// Indicates that a node cannot be numbered within a given interval. /// Indicates that a node cannot be numbered within a given interval.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Unnumberable; pub(super) struct Unnumberable;
impl Display for Unnumberable { impl Display for Unnumberable {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {

View File

@ -6,7 +6,7 @@ use unscanny::Scanner;
use super::resolve::{resolve_hex, resolve_raw, resolve_string}; use super::resolve::{resolve_hex, resolve_raw, resolve_string};
use super::{ErrorPos, NodeKind, RawKind, Unit}; use super::{ErrorPos, NodeKind, RawKind, Unit};
use crate::geom::{AbsUnit, AngleUnit}; use crate::geom::{AbsUnit, AngleUnit};
use crate::util::EcoString; use crate::util::{format_eco, EcoString};
/// An iterator over the tokens of a string of source code. /// An iterator over the tokens of a string of source code.
#[derive(Clone)] #[derive(Clone)]

View File

@ -8,7 +8,9 @@ use std::sync::Arc;
use super::ArcExt; use super::ArcExt;
/// Create a new [`EcoString`] from a format string. /// Create a new [`EcoString`] from a format string.
macro_rules! format_eco { #[macro_export]
#[doc(hidden)]
macro_rules! __format_eco {
($($tts:tt)*) => {{ ($($tts:tt)*) => {{
use std::fmt::Write; use std::fmt::Write;
let mut s = $crate::util::EcoString::new(); let mut s = $crate::util::EcoString::new();
@ -17,6 +19,9 @@ macro_rules! format_eco {
}}; }};
} }
#[doc(inline)]
pub use crate::__format_eco as format_eco;
/// An economical string with inline storage and clone-on-write semantics. /// An economical string with inline storage and clone-on-write semantics.
#[derive(Clone)] #[derive(Clone)]
pub struct EcoString(Repr); pub struct EcoString(Repr);
@ -55,7 +60,7 @@ impl EcoString {
} }
/// Create an instance from an existing string-like type. /// Create an instance from an existing string-like type.
pub fn from_str<S>(s: S) -> Self pub fn from_str_like<S>(s: S) -> Self
where where
S: AsRef<str> + Into<String>, S: AsRef<str> + Into<String>,
{ {
@ -324,13 +329,13 @@ impl From<char> for EcoString {
impl From<&str> for EcoString { impl From<&str> for EcoString {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Self::from_str(s) Self::from_str_like(s)
} }
} }
impl From<String> for EcoString { impl From<String> for EcoString {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Self::from_str(s) Self::from_str_like(s)
} }
} }

View File

@ -3,7 +3,7 @@
pub mod fat; pub mod fat;
pub use buffer::Buffer; pub use buffer::Buffer;
pub use eco::EcoString; pub use eco::{format_eco, EcoString};
#[macro_use] #[macro_use]
mod eco; mod eco;
@ -11,9 +11,12 @@ mod buffer;
use std::any::TypeId; use std::any::TypeId;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::Hash;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use siphasher::sip128::{Hasher128, SipHasher};
/// Turn a closure into a struct implementing [`Debug`]. /// Turn a closure into a struct implementing [`Debug`].
pub fn debug<F>(f: F) -> impl Debug pub fn debug<F>(f: F) -> impl Debug
where where
@ -33,6 +36,13 @@ where
Wrapper(f) Wrapper(f)
} }
/// Calculate a 128-bit siphash of a value.
pub fn hash128<T: Hash>(value: &T) -> u128 {
let mut state = SipHasher::new();
value.hash(&mut state);
state.finish128().as_u128()
}
/// Extra methods for [`str`]. /// Extra methods for [`str`].
pub trait StrExt { pub trait StrExt {
/// The number of code units this string would use if it was encoded in /// The number of code units this string would use if it was encoded in

View File

@ -15,12 +15,6 @@ $ sum_(k=0)^n k = (n(n+1))/2 $
// Test that blackboard style looks nice. // Test that blackboard style looks nice.
$ f: NN arrow RR $ $ f: NN arrow RR $
---
#set math(family: "IBM Plex Sans")
// Error: 1-4 font is not suitable for math
$a$
--- ---
// Error: 1:3 expected dollar sign // Error: 1:3 expected dollar sign
$a $a

View File

@ -14,16 +14,16 @@ use tiny_skia as sk;
use unscanny::Scanner; use unscanny::Scanner;
use walkdir::WalkDir; use walkdir::WalkDir;
use typst::diag::{FileError, FileResult}; use typst::diag::{bail, FileError, FileResult};
use typst::font::{Font, FontBook}; use typst::font::{Font, FontBook};
use typst::frame::{Element, Frame}; use typst::frame::{Element, Frame};
use typst::geom::{Abs, RgbaColor, Sides}; use typst::geom::{Abs, RgbaColor, Sides};
use typst::library::layout::PageNode; use typst::model::{Smart, Value};
use typst::library::text::{TextNode, TextSize};
use typst::model::{Smart, StyleMap, Value};
use typst::syntax::{Source, SourceId, SyntaxNode}; use typst::syntax::{Source, SourceId, SyntaxNode};
use typst::util::{Buffer, PathExt}; use typst::util::{Buffer, PathExt};
use typst::{bail, Config, World}; use typst::{Config, World};
use typst_library::layout::PageNode;
use typst_library::text::{TextNode, TextSize};
const TYP_DIR: &str = "./typ"; const TYP_DIR: &str = "./typ";
const REF_DIR: &str = "./ref"; const REF_DIR: &str = "./ref";
@ -149,7 +149,7 @@ fn config() -> Config {
// Set page width to 120pt with 10pt margins, so that the inner page is // Set page width to 120pt with 10pt margins, so that the inner page is
// exactly 100pt wide. Page height is unbounded and font size is 10pt so // exactly 100pt wide. Page height is unbounded and font size is 10pt so
// that it multiplies to nice round numbers. // that it multiplies to nice round numbers.
let mut styles = StyleMap::new(); let mut styles = typst_library::styles();
styles.set(PageNode::WIDTH, Smart::Custom(Abs::pt(120.0).into())); styles.set(PageNode::WIDTH, Smart::Custom(Abs::pt(120.0).into()));
styles.set(PageNode::HEIGHT, Smart::Auto); styles.set(PageNode::HEIGHT, Smart::Auto);
styles.set( styles.set(
@ -159,10 +159,10 @@ fn config() -> Config {
styles.set(TextNode::SIZE, TextSize(Abs::pt(10.0).into())); styles.set(TextNode::SIZE, TextSize(Abs::pt(10.0).into()));
// Hook up helpers into the global scope. // Hook up helpers into the global scope.
let mut std = typst::library::scope(); let mut scope = typst_library::scope();
std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); scope.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); scope.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
std.def_fn("test", move |_, args| { scope.def_fn("test", move |_, args| {
let lhs = args.expect::<Value>("left-hand side")?; let lhs = args.expect::<Value>("left-hand side")?;
let rhs = args.expect::<Value>("right-hand side")?; let rhs = args.expect::<Value>("right-hand side")?;
if lhs != rhs { if lhs != rhs {
@ -170,7 +170,7 @@ fn config() -> Config {
} }
Ok(Value::None) Ok(Value::None)
}); });
std.def_fn("print", move |_, args| { scope.def_fn("print", move |_, args| {
print!("> "); print!("> ");
for (i, value) in args.all::<Value>()?.into_iter().enumerate() { for (i, value) in args.all::<Value>()?.into_iter().enumerate() {
if i > 0 { if i > 0 {
@ -184,9 +184,9 @@ fn config() -> Config {
Config { Config {
root: PathBuf::new(), root: PathBuf::new(),
items: typst::library::items(), scope,
std,
styles, styles,
items: typst_library::items(),
} }
} }