Set Rules Episode IX: The Rise of Testing
@ -2,8 +2,6 @@ use std::path::Path;
|
||||
|
||||
use iai::{black_box, main, Iai};
|
||||
|
||||
use typst::eval::eval;
|
||||
use typst::layout::layout;
|
||||
use typst::loading::MemLoader;
|
||||
use typst::parse::{parse, Scanner, TokenMode, Tokens};
|
||||
use typst::source::SourceId;
|
||||
@ -53,20 +51,14 @@ fn bench_parse(iai: &mut Iai) {
|
||||
|
||||
fn bench_eval(iai: &mut Iai) {
|
||||
let (mut ctx, id) = context();
|
||||
let ast = ctx.sources.get(id).ast().unwrap();
|
||||
iai.run(|| eval(&mut ctx, id, &ast).unwrap());
|
||||
}
|
||||
|
||||
fn bench_to_tree(iai: &mut Iai) {
|
||||
let (mut ctx, id) = context();
|
||||
let module = ctx.evaluate(id).unwrap();
|
||||
iai.run(|| module.node.clone().into_document());
|
||||
iai.run(|| ctx.evaluate(id).unwrap());
|
||||
}
|
||||
|
||||
fn bench_layout(iai: &mut Iai) {
|
||||
let (mut ctx, id) = context();
|
||||
let tree = ctx.execute(id).unwrap();
|
||||
iai.run(|| layout(&mut ctx, &tree));
|
||||
let module = ctx.evaluate(id).unwrap();
|
||||
let tree = module.into_root();
|
||||
iai.run(|| tree.layout(&mut ctx));
|
||||
}
|
||||
|
||||
main!(
|
||||
@ -75,6 +67,5 @@ main!(
|
||||
bench_tokenize,
|
||||
bench_parse,
|
||||
bench_eval,
|
||||
bench_to_tree,
|
||||
bench_layout
|
||||
);
|
||||
|
@ -34,9 +34,10 @@ use std::path::PathBuf;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult};
|
||||
use crate::geom::{Angle, Fractional, Length, Relative, Spec};
|
||||
use crate::geom::{Angle, Fractional, Length, Relative};
|
||||
use crate::image::ImageStore;
|
||||
use crate::library::{GridNode, TextNode, TrackSizing};
|
||||
use crate::layout::RootNode;
|
||||
use crate::library::{self, TextNode};
|
||||
use crate::loading::Loader;
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
use crate::syntax::ast::*;
|
||||
@ -44,15 +45,9 @@ use crate::syntax::{Span, Spanned};
|
||||
use crate::util::{EcoString, RefMutExt};
|
||||
use crate::Context;
|
||||
|
||||
/// Evaluate a parsed source file into a module.
|
||||
pub fn eval(ctx: &mut Context, source: SourceId, markup: &Markup) -> TypResult<Module> {
|
||||
let mut ctx = EvalContext::new(ctx, source);
|
||||
let node = markup.eval(&mut ctx)?;
|
||||
Ok(Module { scope: ctx.scopes.top, node })
|
||||
}
|
||||
|
||||
/// An evaluated module, ready for importing or instantiation.
|
||||
#[derive(Debug, Clone)]
|
||||
/// An evaluated module, ready for importing or conversion to a root layout
|
||||
/// tree.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Module {
|
||||
/// The top-level definitions that were bound in this module.
|
||||
pub scope: Scope,
|
||||
@ -60,6 +55,22 @@ pub struct Module {
|
||||
pub node: Node,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
/// Convert this module's node into a layout tree.
|
||||
pub fn into_root(self) -> RootNode {
|
||||
self.node.into_root()
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate an expression.
|
||||
pub trait Eval {
|
||||
/// The output of evaluating the expression.
|
||||
type Output;
|
||||
|
||||
/// Evaluate the expression to the output value.
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>;
|
||||
}
|
||||
|
||||
/// The context for evaluation.
|
||||
pub struct EvalContext<'a> {
|
||||
/// The loader from which resources (files and images) are loaded.
|
||||
@ -124,7 +135,7 @@ impl<'a> EvalContext<'a> {
|
||||
self.route.push(id);
|
||||
|
||||
// Evaluate the module.
|
||||
let template = ast.eval(self).trace(|| Tracepoint::Import, span)?;
|
||||
let node = ast.eval(self).trace(|| Tracepoint::Import, span)?;
|
||||
|
||||
// Restore the old context.
|
||||
let new_scopes = mem::replace(&mut self.scopes, prev_scopes);
|
||||
@ -132,7 +143,7 @@ impl<'a> EvalContext<'a> {
|
||||
self.route.pop().unwrap();
|
||||
|
||||
// Save the evaluated module.
|
||||
let module = Module { scope: new_scopes.top, node: template };
|
||||
let module = Module { scope: new_scopes.top, node };
|
||||
self.modules.insert(id, module);
|
||||
|
||||
Ok(id)
|
||||
@ -151,15 +162,6 @@ impl<'a> EvalContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate an expression.
|
||||
pub trait Eval {
|
||||
/// The output of evaluating the expression.
|
||||
type Output;
|
||||
|
||||
/// Evaluate the expression to the output value.
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output>;
|
||||
}
|
||||
|
||||
impl Eval for Markup {
|
||||
type Output = Node;
|
||||
|
||||
@ -231,13 +233,10 @@ impl Eval for HeadingNode {
|
||||
type Output = Node;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
let upscale = (1.6 - 0.1 * self.level() as f64).max(0.75);
|
||||
let mut styles = Styles::new();
|
||||
styles.set(TextNode::STRONG, true);
|
||||
styles.set(TextNode::SIZE, Relative::new(upscale).into());
|
||||
Ok(Node::Block(
|
||||
self.body().eval(ctx)?.into_block().styled(styles),
|
||||
))
|
||||
Ok(Node::block(library::HeadingNode {
|
||||
child: self.body().eval(ctx)?.into_block(),
|
||||
level: self.level(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,8 +244,10 @@ impl Eval for ListNode {
|
||||
type Output = Node;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
let body = self.body().eval(ctx)?;
|
||||
labelled(ctx, '•'.into(), body)
|
||||
Ok(Node::block(library::ListNode {
|
||||
child: self.body().eval(ctx)?.into_block(),
|
||||
labelling: library::Unordered,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,24 +255,13 @@ impl Eval for EnumNode {
|
||||
type Output = Node;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
let body = self.body().eval(ctx)?;
|
||||
let label = format_eco!("{}.", self.number().unwrap_or(1));
|
||||
labelled(ctx, label, body)
|
||||
Ok(Node::block(library::ListNode {
|
||||
child: self.body().eval(ctx)?.into_block(),
|
||||
labelling: library::Ordered(self.number()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a labelled list / enum.
|
||||
fn labelled(_: &mut EvalContext, label: EcoString, body: Node) -> TypResult<Node> {
|
||||
// Create a grid containing the label, a bit of gutter space and then
|
||||
// the item's body.
|
||||
// TODO: Switch to em units for gutter once available.
|
||||
Ok(Node::block(GridNode {
|
||||
tracks: Spec::new(vec![TrackSizing::Auto; 2], vec![]),
|
||||
gutter: Spec::new(vec![TrackSizing::Linear(Length::pt(5.0).into())], vec![]),
|
||||
children: vec![Node::Text(label).into_block(), body.into_block()],
|
||||
}))
|
||||
}
|
||||
|
||||
impl Eval for Expr {
|
||||
type Output = Value;
|
||||
|
||||
|
@ -8,17 +8,18 @@ use std::ops::{Add, AddAssign};
|
||||
use super::Styles;
|
||||
use crate::diag::StrResult;
|
||||
use crate::geom::SpecAxis;
|
||||
use crate::layout::{Layout, PackedNode};
|
||||
use crate::layout::{Layout, PackedNode, RootNode};
|
||||
use crate::library::{
|
||||
DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode,
|
||||
SpacingKind, SpacingNode, TextNode,
|
||||
FlowChild, FlowNode, PageNode, ParChild, ParNode, PlacedNode, SpacingKind,
|
||||
SpacingNode, TextNode,
|
||||
};
|
||||
use crate::util::EcoString;
|
||||
|
||||
/// A partial representation of a layout node.
|
||||
///
|
||||
/// A node is a composable intermediate representation that can be converted
|
||||
/// into a proper layout node by lifting it to a block-level or document node.
|
||||
/// into a proper layout node by lifting it to a [block-level](PackedNode) or
|
||||
/// [root node](RootNode).
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
pub enum Node {
|
||||
/// A word space.
|
||||
@ -90,19 +91,19 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lift to a document node, the root of the layout tree.
|
||||
pub fn into_document(self) -> DocumentNode {
|
||||
/// Lift to a root layout tree node.
|
||||
pub fn into_root(self) -> RootNode {
|
||||
let mut packer = Packer::new(true);
|
||||
packer.walk(self, Styles::new());
|
||||
packer.into_document()
|
||||
packer.into_root()
|
||||
}
|
||||
|
||||
/// Repeat this template `n` times.
|
||||
/// Repeat this node `n` times.
|
||||
pub fn repeat(&self, n: i64) -> StrResult<Self> {
|
||||
let count = usize::try_from(n)
|
||||
.map_err(|_| format!("cannot repeat this template {} times", n))?;
|
||||
|
||||
// TODO(set): Make more efficient.
|
||||
// TODO(style): Make more efficient.
|
||||
Ok(Self::Sequence(vec![(self.clone(), Styles::new()); count]))
|
||||
}
|
||||
}
|
||||
@ -117,7 +118,7 @@ impl Add for Node {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
// TODO(set): Make more efficient.
|
||||
// TODO(style): Make more efficient.
|
||||
Self::Sequence(vec![(self, Styles::new()), (rhs, Styles::new())])
|
||||
}
|
||||
}
|
||||
@ -134,9 +135,9 @@ impl Sum for Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Packs a [`Node`] into a flow or whole document.
|
||||
/// Packs a [`Node`] into a flow or root node.
|
||||
struct Packer {
|
||||
/// Whether this packer produces the top-level document.
|
||||
/// Whether this packer produces a root node.
|
||||
top: bool,
|
||||
/// The accumulated page nodes.
|
||||
pages: Vec<PageNode>,
|
||||
@ -163,10 +164,10 @@ impl Packer {
|
||||
FlowNode(self.flow.children).pack()
|
||||
}
|
||||
|
||||
/// Finish up and return the resulting document.
|
||||
fn into_document(mut self) -> DocumentNode {
|
||||
/// Finish up and return the resulting root node.
|
||||
fn into_root(mut self) -> RootNode {
|
||||
self.pagebreak();
|
||||
DocumentNode(self.pages)
|
||||
RootNode(self.pages)
|
||||
}
|
||||
|
||||
/// Consider a node with the given styles.
|
||||
|
@ -20,6 +20,11 @@ impl Styles {
|
||||
Self { map: vec![] }
|
||||
}
|
||||
|
||||
/// Whether this map contains no styles.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
|
||||
/// Create a style map with a single property-value pair.
|
||||
pub fn one<P: Property>(key: P, value: P::Value) -> Self {
|
||||
let mut styles = Self::new();
|
||||
@ -27,11 +32,6 @@ impl Styles {
|
||||
styles
|
||||
}
|
||||
|
||||
/// Whether this map contains no styles.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
|
||||
/// Set the value for a style property.
|
||||
pub fn set<P: Property>(&mut self, key: P, value: P::Value) {
|
||||
let id = StyleId::of::<P>();
|
||||
@ -47,6 +47,13 @@ impl Styles {
|
||||
self.map.push((id, Entry::new(key, value)));
|
||||
}
|
||||
|
||||
/// Set a value for a style property if it is `Some(_)`.
|
||||
pub fn set_opt<P: Property>(&mut self, key: P, value: Option<P::Value>) {
|
||||
if let Some(value) = value {
|
||||
self.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a boolean style property.
|
||||
pub fn toggle<P: Property<Value = bool>>(&mut self, key: P) {
|
||||
let id = StyleId::of::<P>();
|
||||
@ -82,13 +89,22 @@ impl Styles {
|
||||
}
|
||||
|
||||
/// Get a reference to a style directly in this map (no default value).
|
||||
pub fn get_direct<P: Property>(&self, _: P) -> Option<&P::Value> {
|
||||
fn get_direct<P: Property>(&self, _: P) -> Option<&P::Value> {
|
||||
self.map
|
||||
.iter()
|
||||
.find(|pair| pair.0 == StyleId::of::<P>())
|
||||
.and_then(|pair| pair.1.downcast())
|
||||
}
|
||||
|
||||
/// Create new styles combining `self` with `outer`.
|
||||
///
|
||||
/// Properties from `self` take precedence over the ones from `outer`.
|
||||
pub fn chain(&self, outer: &Self) -> Self {
|
||||
let mut styles = self.clone();
|
||||
styles.apply(outer);
|
||||
styles
|
||||
}
|
||||
|
||||
/// Apply styles from `outer` in-place.
|
||||
///
|
||||
/// Properties from `self` take precedence over the ones from `outer`.
|
||||
@ -105,13 +121,9 @@ impl Styles {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new styles combining `self` with `outer`.
|
||||
///
|
||||
/// Properties from `self` take precedence over the ones from `outer`.
|
||||
pub fn chain(&self, outer: &Self) -> Self {
|
||||
let mut styles = self.clone();
|
||||
styles.apply(outer);
|
||||
styles
|
||||
/// Keep only those styles that are not also in `other`.
|
||||
pub fn erase(&mut self, other: &Self) {
|
||||
self.map.retain(|a| other.map.iter().all(|b| a != b));
|
||||
}
|
||||
|
||||
/// Keep only those styles that are also in `other`.
|
||||
@ -119,18 +131,13 @@ impl Styles {
|
||||
self.map.retain(|a| other.map.iter().any(|b| a == b));
|
||||
}
|
||||
|
||||
/// Keep only those styles that are not also in `other`.
|
||||
pub fn erase(&mut self, other: &Self) {
|
||||
self.map.retain(|a| other.map.iter().all(|b| a != b));
|
||||
}
|
||||
|
||||
/// Whether two style maps are equal when filtered down to the given
|
||||
/// properties.
|
||||
pub fn compatible<F>(&self, other: &Self, filter: F) -> bool
|
||||
where
|
||||
F: Fn(StyleId) -> bool,
|
||||
{
|
||||
// TODO(set): Filtered length + one direction equal should suffice.
|
||||
// TODO(style): Filtered length + one direction equal should suffice.
|
||||
let f = |e: &&(StyleId, Entry)| filter(e.0);
|
||||
self.map.iter().filter(f).all(|pair| other.map.contains(pair))
|
||||
&& other.map.iter().filter(f).all(|pair| self.map.contains(pair))
|
||||
@ -177,7 +184,7 @@ impl Entry {
|
||||
|
||||
impl Debug for Entry {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
self.0.dyn_fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,22 +202,23 @@ impl Hash for Entry {
|
||||
|
||||
trait Bounds: 'static {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result;
|
||||
fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result;
|
||||
fn dyn_eq(&self, other: &Entry) -> bool;
|
||||
fn hash64(&self) -> u64;
|
||||
fn combine(&self, outer: &Entry) -> Entry;
|
||||
}
|
||||
|
||||
impl<P> Bounds for (P, P::Value)
|
||||
where
|
||||
P: Property,
|
||||
P::Value: Debug + Hash + PartialEq + 'static,
|
||||
{
|
||||
// `P` is always zero-sized. We only implement the trait for a pair of key and
|
||||
// associated value so that `P` is a constrained type parameter that we can use
|
||||
// in `dyn_fmt` to access the property's name. This way, we can effectively
|
||||
// store the property's name in its vtable instead of having an actual runtime
|
||||
// string somewhere in `Entry`.
|
||||
impl<P: Property> Bounds for (P, P::Value) {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
&self.1
|
||||
}
|
||||
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn dyn_fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if f.alternate() {
|
||||
write!(f, "#[{} = {:?}]", P::NAME, self.1)
|
||||
} else {
|
||||
@ -242,11 +250,12 @@ where
|
||||
/// Style property keys.
|
||||
///
|
||||
/// This trait is not intended to be implemented manually, but rather through
|
||||
/// the `properties!` macro.
|
||||
/// the `#[properties]` proc-macro.
|
||||
pub trait Property: Copy + 'static {
|
||||
/// The type of this property, for example, this could be
|
||||
/// [`Length`](crate::geom::Length) for a `WIDTH` property.
|
||||
type Value: Debug + Clone + Hash + PartialEq + 'static;
|
||||
/// The type of value that is returned when getting this property from a
|
||||
/// style map. For example, this could be [`Length`](crate::geom::Length)
|
||||
/// for a `WIDTH` property.
|
||||
type Value: Debug + Clone + PartialEq + Hash + 'static;
|
||||
|
||||
/// The name of the property, used for debug printing.
|
||||
const NAME: &'static str;
|
||||
@ -257,12 +266,16 @@ pub trait Property: Copy + 'static {
|
||||
/// A static reference to the default value of the property.
|
||||
///
|
||||
/// This is automatically implemented through lazy-initialization in the
|
||||
/// `properties!` macro. This way, expensive defaults don't need to be
|
||||
/// `#[properties]` macro. This way, expensive defaults don't need to be
|
||||
/// recreated all the time.
|
||||
fn default_ref() -> &'static Self::Value;
|
||||
|
||||
/// Combine the property with an outer value.
|
||||
fn combine(inner: Self::Value, _: Self::Value) -> Self::Value {
|
||||
/// Fold the property with an outer value.
|
||||
///
|
||||
/// For example, this would combine a relative font size with an outer
|
||||
/// absolute font size.
|
||||
#[allow(unused_variables)]
|
||||
fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value {
|
||||
inner
|
||||
}
|
||||
}
|
||||
@ -277,12 +290,3 @@ impl StyleId {
|
||||
Self(TypeId::of::<P>())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a style property to a value if the value is `Some`.
|
||||
macro_rules! set {
|
||||
($styles:expr, $target:expr => $value:expr) => {
|
||||
if let Some(v) = $value {
|
||||
$styles.set($target, v);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -20,43 +20,25 @@ use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::geom::{Align, Linear, Point, Sides, Size, Spec, Transform};
|
||||
use crate::image::ImageStore;
|
||||
use crate::library::{AlignNode, DocumentNode, PadNode, SizedNode, TransformNode};
|
||||
use crate::library::{AlignNode, PadNode, PageNode, SizedNode, TransformNode};
|
||||
use crate::Context;
|
||||
|
||||
/// Layout a document node into a collection of frames.
|
||||
pub fn layout(ctx: &mut Context, node: &DocumentNode) -> Vec<Rc<Frame>> {
|
||||
let mut ctx = LayoutContext::new(ctx);
|
||||
node.layout(&mut ctx)
|
||||
/// The root layout node, a document consisting of top-level page runs.
|
||||
#[derive(Hash)]
|
||||
pub struct RootNode(pub Vec<PageNode>);
|
||||
|
||||
impl RootNode {
|
||||
/// Layout the document into a sequence of frames, one per page.
|
||||
pub fn layout(&self, ctx: &mut Context) -> Vec<Rc<Frame>> {
|
||||
let mut ctx = LayoutContext::new(ctx);
|
||||
self.0.iter().flat_map(|node| node.layout(&mut ctx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The context for layouting.
|
||||
pub struct LayoutContext<'a> {
|
||||
/// Stores parsed font faces.
|
||||
pub fonts: &'a mut FontStore,
|
||||
/// Stores decoded images.
|
||||
pub images: &'a mut ImageStore,
|
||||
/// Caches layouting artifacts.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub layouts: &'a mut LayoutCache,
|
||||
/// The inherited style properties.
|
||||
pub styles: Styles,
|
||||
/// How deeply nested the current layout tree position is.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
/// Create a new layout context.
|
||||
pub fn new(ctx: &'a mut Context) -> Self {
|
||||
Self {
|
||||
fonts: &mut ctx.fonts,
|
||||
images: &mut ctx.images,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
layouts: &mut ctx.layouts,
|
||||
styles: ctx.styles.clone(),
|
||||
#[cfg(feature = "layout-cache")]
|
||||
level: 0,
|
||||
}
|
||||
impl Debug for RootNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("Root ")?;
|
||||
f.debug_list().entries(&self.0).finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +68,57 @@ pub trait Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/// The context for layouting.
|
||||
pub struct LayoutContext<'a> {
|
||||
/// Stores parsed font faces.
|
||||
pub fonts: &'a mut FontStore,
|
||||
/// Stores decoded images.
|
||||
pub images: &'a mut ImageStore,
|
||||
/// Caches layouting artifacts.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub layouts: &'a mut LayoutCache,
|
||||
/// The inherited style properties.
|
||||
// TODO(style): This probably shouldn't be here.
|
||||
pub styles: Styles,
|
||||
/// How deeply nested the current layout tree position is.
|
||||
#[cfg(feature = "layout-cache")]
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
impl<'a> LayoutContext<'a> {
|
||||
/// Create a new layout context.
|
||||
pub fn new(ctx: &'a mut Context) -> Self {
|
||||
Self {
|
||||
fonts: &mut ctx.fonts,
|
||||
images: &mut ctx.images,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
layouts: &mut ctx.layouts,
|
||||
styles: ctx.styles.clone(),
|
||||
#[cfg(feature = "layout-cache")]
|
||||
level: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A layout node that produces an empty frame.
|
||||
///
|
||||
/// The packed version of this is returned by [`PackedNode::default`].
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct EmptyNode;
|
||||
|
||||
impl Layout for EmptyNode {
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let size = regions.expand.select(regions.current, Size::zero());
|
||||
let mut cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.filter(regions.expand);
|
||||
vec![Frame::new(size).constrain(cts)]
|
||||
}
|
||||
}
|
||||
|
||||
/// A packed layouting node with precomputed hash.
|
||||
#[derive(Clone)]
|
||||
pub struct PackedNode {
|
||||
@ -288,22 +321,3 @@ where
|
||||
state.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A layout node that produces an empty frame.
|
||||
///
|
||||
/// The packed version of this is returned by [`PackedNode::default`].
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct EmptyNode;
|
||||
|
||||
impl Layout for EmptyNode {
|
||||
fn layout(
|
||||
&self,
|
||||
_: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let size = regions.expand.select(regions.current, Size::zero());
|
||||
let mut cts = Constraints::new(regions.expand);
|
||||
cts.exact = regions.current.filter(regions.expand);
|
||||
vec![Frame::new(size).constrain(cts)]
|
||||
}
|
||||
}
|
||||
|
54
src/lib.rs
@ -2,30 +2,32 @@
|
||||
//!
|
||||
//! # Steps
|
||||
//! - **Parsing:** The parsing step first transforms a plain string into an
|
||||
//! [iterator of tokens][tokens]. This token stream is [parsed] into [markup].
|
||||
//! The syntactical structures describing markup and embedded code can be
|
||||
//! found in the [syntax] module.
|
||||
//! [iterator of tokens][tokens]. This token stream is [parsed] into a
|
||||
//! [green tree]. The green tree itself is untyped, but a typed layer over it
|
||||
//! is provided in the [AST] module.
|
||||
//! - **Evaluation:** The next step is to [evaluate] the markup. This produces a
|
||||
//! [module], consisting of a scope of values that were exported by the code
|
||||
//! and a template with the contents of the module. This template can be
|
||||
//! instantiated with a style to produce a layout tree, a high-level, fully
|
||||
//! styled representation, rooted in the [document node]. The nodes of this
|
||||
//! tree are self-contained and order-independent and thus much better suited
|
||||
//! for layouting than the raw markup.
|
||||
//! and a [node] with the contents of the module. This node can be converted
|
||||
//! into a [layout tree], a hierarchical, styled representation of the
|
||||
//! document. The nodes of this tree are well structured and order-independent
|
||||
//! and thus much better suited for layouting than the raw markup.
|
||||
//! - **Layouting:** Next, the tree is [layouted] into a portable version of the
|
||||
//! typeset document. The output of this is a collection of [`Frame`]s (one
|
||||
//! per page), ready for exporting.
|
||||
//! per page), ready for exporting. This step is supported by an incremental
|
||||
//! [cache] that enables reuse of intermediate layouting results.
|
||||
//! - **Exporting:** The finished layout can be exported into a supported
|
||||
//! format. Currently, the only supported output format is [PDF].
|
||||
//!
|
||||
//! [tokens]: parse::Tokens
|
||||
//! [parsed]: parse::parse
|
||||
//! [markup]: syntax::ast::Markup
|
||||
//! [evaluate]: eval::eval
|
||||
//! [green tree]: syntax::GreenNode
|
||||
//! [AST]: syntax::ast
|
||||
//! [evaluate]: Context::evaluate
|
||||
//! [module]: eval::Module
|
||||
//! [layout tree]: layout::LayoutTree
|
||||
//! [document node]: library::DocumentNode
|
||||
//! [layouted]: layout::layout
|
||||
//! [node]: eval::Node
|
||||
//! [layout tree]: layout::RootNode
|
||||
//! [layouted]: layout::RootNode::layout
|
||||
//! [cache]: layout::LayoutCache
|
||||
//! [PDF]: export::pdf
|
||||
|
||||
#[macro_use]
|
||||
@ -49,13 +51,12 @@ pub mod syntax;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::{Module, Scope, Styles};
|
||||
use crate::eval::{Eval, EvalContext, Module, Scope, Styles};
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::image::ImageStore;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
use crate::layout::{EvictionPolicy, LayoutCache};
|
||||
use crate::library::DocumentNode;
|
||||
use crate::loading::Loader;
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
|
||||
@ -100,15 +101,15 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Evaluate a source file and return the resulting module.
|
||||
///
|
||||
/// Returns either a module containing a scope with top-level bindings and a
|
||||
/// layoutable node or diagnostics in the form of a vector of error message
|
||||
/// with file and span information.
|
||||
pub fn evaluate(&mut self, id: SourceId) -> TypResult<Module> {
|
||||
let ast = self.sources.get(id).ast()?;
|
||||
eval::eval(self, id, &ast)
|
||||
}
|
||||
|
||||
/// Execute a source file and produce the resulting page nodes.
|
||||
pub fn execute(&mut self, id: SourceId) -> TypResult<DocumentNode> {
|
||||
let module = self.evaluate(id)?;
|
||||
Ok(module.node.into_document())
|
||||
let markup = self.sources.get(id).ast()?;
|
||||
let mut ctx = EvalContext::new(self, id);
|
||||
let node = markup.eval(&mut ctx)?;
|
||||
Ok(Module { scope: ctx.scopes.top, node })
|
||||
}
|
||||
|
||||
/// Typeset a source file into a collection of layouted frames.
|
||||
@ -117,8 +118,9 @@ impl Context {
|
||||
/// diagnostics in the form of a vector of error message with file and span
|
||||
/// information.
|
||||
pub fn typeset(&mut self, id: SourceId) -> TypResult<Vec<Rc<Frame>>> {
|
||||
let tree = self.execute(id)?;
|
||||
let frames = layout::layout(self, &tree);
|
||||
let module = self.evaluate(id)?;
|
||||
let tree = module.into_root();
|
||||
let frames = tree.layout(self);
|
||||
Ok(frames)
|
||||
}
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
use super::prelude::*;
|
||||
use super::PageNode;
|
||||
|
||||
/// The root layout node, a document consisting of top-level page runs.
|
||||
#[derive(Hash)]
|
||||
pub struct DocumentNode(pub Vec<PageNode>);
|
||||
|
||||
impl DocumentNode {
|
||||
/// Layout the document into a sequence of frames, one per page.
|
||||
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||
self.0.iter().flat_map(|node| node.layout(ctx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for DocumentNode {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("Document ")?;
|
||||
f.debug_list().entries(&self.0).finish()
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
Value::Relative(v) => vec![TrackSizing::Linear(v.into())],
|
||||
Value::Linear(v) => vec![TrackSizing::Linear(v)],
|
||||
Value::Fractional(v) => vec![TrackSizing::Fractional(v)],
|
||||
Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize],
|
||||
Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast()?],
|
||||
Value::Array(values) => values
|
||||
.into_iter()
|
||||
.filter_map(|v| v.cast().ok())
|
||||
|
63
src/library/heading.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use super::prelude::*;
|
||||
use super::{FontFamily, TextNode};
|
||||
|
||||
/// A section heading.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct HeadingNode {
|
||||
/// The node that produces the heading's contents.
|
||||
pub child: PackedNode,
|
||||
/// The logical nesting depth of the section, starting from one. In the
|
||||
/// default style, this controls the text size of the heading.
|
||||
pub level: usize,
|
||||
}
|
||||
|
||||
#[properties]
|
||||
impl HeadingNode {
|
||||
/// The heading's font family.
|
||||
pub const FAMILY: Smart<String> = Smart::Auto;
|
||||
/// The fill color of heading in the text. Just the surrounding text color
|
||||
/// if `auto`.
|
||||
pub const FILL: Smart<Paint> = Smart::Auto;
|
||||
}
|
||||
|
||||
impl Construct for HeadingNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
Ok(Node::block(Self {
|
||||
child: args.expect::<Node>("body")?.into_block(),
|
||||
level: args.named("level")?.unwrap_or(1),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Set for HeadingNode {
|
||||
fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> {
|
||||
styles.set_opt(Self::FAMILY, args.named("family")?);
|
||||
styles.set_opt(Self::FILL, args.named("fill")?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for HeadingNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let upscale = (1.6 - 0.1 * self.level as f64).max(0.75);
|
||||
ctx.styles.set(TextNode::STRONG, true);
|
||||
ctx.styles.set(TextNode::SIZE, Relative::new(upscale).into());
|
||||
|
||||
if let Smart::Custom(family) = ctx.styles.get_ref(Self::FAMILY) {
|
||||
let list: Vec<_> = std::iter::once(FontFamily::named(family))
|
||||
.chain(ctx.styles.get_ref(TextNode::FAMILY_LIST).iter().cloned())
|
||||
.collect();
|
||||
ctx.styles.set(TextNode::FAMILY_LIST, list);
|
||||
}
|
||||
|
||||
if let Smart::Custom(fill) = ctx.styles.get(Self::FILL) {
|
||||
ctx.styles.set(TextNode::FILL, fill);
|
||||
}
|
||||
|
||||
self.child.layout(ctx, regions)
|
||||
}
|
||||
}
|
102
src/library/list.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use super::prelude::*;
|
||||
use super::{GridNode, TextNode, TrackSizing};
|
||||
|
||||
/// An unordered or ordered list.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct ListNode<L> {
|
||||
/// The node that produces the item's body.
|
||||
pub child: PackedNode,
|
||||
/// The list labelling style -- unordered or ordered.
|
||||
pub labelling: L,
|
||||
}
|
||||
|
||||
#[properties]
|
||||
impl<L: Labelling> ListNode<L> {
|
||||
/// The indentation of each item's label.
|
||||
pub const LABEL_INDENT: Linear = Relative::new(0.0).into();
|
||||
/// The space between the label and the body of each item.
|
||||
pub const BODY_INDENT: Linear = Relative::new(0.5).into();
|
||||
}
|
||||
|
||||
impl<L: Labelling> Construct for ListNode<L> {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
Ok(args
|
||||
.all()
|
||||
.map(|node: Node| {
|
||||
Node::block(Self {
|
||||
child: node.into_block(),
|
||||
labelling: L::default(),
|
||||
})
|
||||
})
|
||||
.sum())
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: Labelling> Set for ListNode<L> {
|
||||
fn set(styles: &mut Styles, args: &mut Args) -> TypResult<()> {
|
||||
styles.set_opt(Self::LABEL_INDENT, args.named("label-indent")?);
|
||||
styles.set_opt(Self::BODY_INDENT, args.named("body-indent")?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<L: Labelling> Layout for ListNode<L> {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
let em = ctx.styles.get(TextNode::SIZE).abs;
|
||||
let label_indent = ctx.styles.get(Self::LABEL_INDENT).resolve(em);
|
||||
let body_indent = ctx.styles.get(Self::BODY_INDENT).resolve(em);
|
||||
|
||||
let columns = vec![
|
||||
TrackSizing::Linear(label_indent.into()),
|
||||
TrackSizing::Auto,
|
||||
TrackSizing::Linear(body_indent.into()),
|
||||
TrackSizing::Auto,
|
||||
];
|
||||
|
||||
let children = vec![
|
||||
PackedNode::default(),
|
||||
Node::Text(self.labelling.label()).into_block(),
|
||||
PackedNode::default(),
|
||||
self.child.clone(),
|
||||
];
|
||||
|
||||
GridNode {
|
||||
tracks: Spec::new(columns, vec![]),
|
||||
gutter: Spec::default(),
|
||||
children,
|
||||
}
|
||||
.layout(ctx, regions)
|
||||
}
|
||||
}
|
||||
|
||||
/// How to label a list.
|
||||
pub trait Labelling: Debug + Default + Hash + 'static {
|
||||
/// Return the item's label.
|
||||
fn label(&self) -> EcoString;
|
||||
}
|
||||
|
||||
/// Unordered list labelling style.
|
||||
#[derive(Debug, Default, Hash)]
|
||||
pub struct Unordered;
|
||||
|
||||
impl Labelling for Unordered {
|
||||
fn label(&self) -> EcoString {
|
||||
'•'.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordered list labelling style.
|
||||
#[derive(Debug, Default, Hash)]
|
||||
pub struct Ordered(pub Option<usize>);
|
||||
|
||||
impl Labelling for Ordered {
|
||||
fn label(&self) -> EcoString {
|
||||
format_eco!("{}.", self.0.unwrap_or(1))
|
||||
}
|
||||
}
|
@ -4,11 +4,12 @@
|
||||
//! definitions.
|
||||
|
||||
mod align;
|
||||
mod document;
|
||||
mod flow;
|
||||
mod grid;
|
||||
mod heading;
|
||||
mod image;
|
||||
mod link;
|
||||
mod list;
|
||||
mod pad;
|
||||
mod page;
|
||||
mod par;
|
||||
@ -41,10 +42,11 @@ mod prelude {
|
||||
|
||||
pub use self::image::*;
|
||||
pub use align::*;
|
||||
pub use document::*;
|
||||
pub use flow::*;
|
||||
pub use grid::*;
|
||||
pub use heading::*;
|
||||
pub use link::*;
|
||||
pub use list::*;
|
||||
pub use pad::*;
|
||||
pub use page::*;
|
||||
pub use par::*;
|
||||
@ -68,21 +70,29 @@ pub fn new() -> Scope {
|
||||
std.def_class::<PageNode>("page");
|
||||
std.def_class::<ParNode>("par");
|
||||
std.def_class::<TextNode>("text");
|
||||
std.def_class::<HeadingNode>("heading");
|
||||
std.def_class::<ListNode<Unordered>>("list");
|
||||
std.def_class::<ListNode<Ordered>>("enum");
|
||||
|
||||
// Text functions.
|
||||
// TODO(style): These should be classes, once that works for inline nodes.
|
||||
std.def_func("strike", strike);
|
||||
std.def_func("underline", underline);
|
||||
std.def_func("overline", overline);
|
||||
std.def_func("link", link);
|
||||
|
||||
// Layout functions.
|
||||
std.def_func("h", h);
|
||||
std.def_func("v", v);
|
||||
std.def_func("box", box_);
|
||||
std.def_func("block", block);
|
||||
// Break and spacing functions.
|
||||
std.def_func("pagebreak", pagebreak);
|
||||
std.def_func("parbreak", parbreak);
|
||||
std.def_func("linebreak", linebreak);
|
||||
std.def_func("h", h);
|
||||
std.def_func("v", v);
|
||||
|
||||
// Layout functions.
|
||||
// TODO(style): Decide which of these should be classes
|
||||
// (and which of their properties should be settable).
|
||||
std.def_func("box", box_);
|
||||
std.def_func("block", block);
|
||||
std.def_func("stack", stack);
|
||||
std.def_func("grid", grid);
|
||||
std.def_func("pad", pad);
|
||||
@ -91,8 +101,6 @@ pub fn new() -> Scope {
|
||||
std.def_func("move", move_);
|
||||
std.def_func("scale", scale);
|
||||
std.def_func("rotate", rotate);
|
||||
|
||||
// Element functions.
|
||||
std.def_func("image", image);
|
||||
std.def_func("rect", rect);
|
||||
std.def_func("square", square);
|
||||
@ -118,6 +126,7 @@ pub fn new() -> Scope {
|
||||
std.def_func("sorted", sorted);
|
||||
|
||||
// Predefined colors.
|
||||
// TODO: More colors.
|
||||
std.def_const("white", RgbaColor::WHITE);
|
||||
std.def_const("black", RgbaColor::BLACK);
|
||||
std.def_const("eastern", RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF));
|
||||
@ -151,3 +160,15 @@ castable! {
|
||||
Expected: "color",
|
||||
Value::Color(color) => Paint::Solid(color),
|
||||
}
|
||||
|
||||
castable! {
|
||||
usize,
|
||||
Expected: "non-negative integer",
|
||||
Value::Int(int) => int.try_into().map_err(|_| "must be at least zero")?,
|
||||
}
|
||||
|
||||
castable! {
|
||||
String,
|
||||
Expected: "string",
|
||||
Value::Str(string) => string.into(),
|
||||
}
|
||||
|
@ -44,8 +44,6 @@ impl PageNode {
|
||||
|
||||
impl Construct for PageNode {
|
||||
fn construct(_: &mut EvalContext, args: &mut Args) -> TypResult<Node> {
|
||||
// TODO(set): Make sure it's really a page so that it doesn't merge
|
||||
// with adjacent pages.
|
||||
Ok(Node::Page(args.expect::<Node>("body")?.into_block()))
|
||||
}
|
||||
}
|
||||
@ -69,13 +67,12 @@ impl Set for PageNode {
|
||||
}
|
||||
|
||||
let margins = args.named("margins")?;
|
||||
|
||||
set!(styles, Self::FLIPPED => args.named("flipped")?);
|
||||
set!(styles, Self::LEFT => args.named("left")?.or(margins));
|
||||
set!(styles, Self::TOP => args.named("top")?.or(margins));
|
||||
set!(styles, Self::RIGHT => args.named("right")?.or(margins));
|
||||
set!(styles, Self::BOTTOM => args.named("bottom")?.or(margins));
|
||||
set!(styles, Self::FILL => args.named("fill")?);
|
||||
styles.set_opt(Self::FLIPPED, args.named("flipped")?);
|
||||
styles.set_opt(Self::LEFT, args.named("left")?.or(margins));
|
||||
styles.set_opt(Self::TOP, args.named("top")?.or(margins));
|
||||
styles.set_opt(Self::RIGHT, args.named("right")?.or(margins));
|
||||
styles.set_opt(Self::BOTTOM, args.named("bottom")?.or(margins));
|
||||
styles.set_opt(Self::FILL, args.named("fill")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -74,10 +74,10 @@ impl Set for ParNode {
|
||||
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
|
||||
}
|
||||
|
||||
set!(styles, Self::DIR => dir);
|
||||
set!(styles, Self::ALIGN => align);
|
||||
set!(styles, Self::LEADING => leading);
|
||||
set!(styles, Self::SPACING => spacing);
|
||||
styles.set_opt(Self::DIR, dir);
|
||||
styles.set_opt(Self::ALIGN, align);
|
||||
styles.set_opt(Self::LEADING, leading);
|
||||
styles.set_opt(Self::SPACING, spacing);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -93,8 +93,7 @@ impl Layout for ParNode {
|
||||
let text = self.collect_text();
|
||||
|
||||
// Find out the BiDi embedding levels.
|
||||
let default_level = Level::from_dir(ctx.styles.get(Self::DIR));
|
||||
let bidi = BidiInfo::new(&text, default_level);
|
||||
let bidi = BidiInfo::new(&text, Level::from_dir(ctx.styles.get(Self::DIR)));
|
||||
|
||||
// Prepare paragraph layout by building a representation on which we can
|
||||
// do line breaking without layouting each and every line from scratch.
|
||||
|
@ -56,11 +56,11 @@ impl TextNode {
|
||||
/// A prioritized sequence of font families.
|
||||
pub const FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif];
|
||||
/// The serif font family/families.
|
||||
pub const SERIF_LIST: Vec<String> = vec!["ibm plex serif".into()];
|
||||
pub const SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Serif")];
|
||||
/// The sans-serif font family/families.
|
||||
pub const SANS_SERIF_LIST: Vec<String> = vec!["ibm plex sans".into()];
|
||||
pub const SANS_SERIF_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Sans")];
|
||||
/// The monospace font family/families.
|
||||
pub const MONOSPACE_LIST: Vec<String> = vec!["ibm plex mono".into()];
|
||||
pub const MONOSPACE_LIST: Vec<NamedFamily> = vec![NamedFamily::new("IBM Plex Mono")];
|
||||
/// Whether to allow font fallback when the primary font list contains no
|
||||
/// match.
|
||||
pub const FALLBACK: bool = true;
|
||||
@ -139,32 +139,38 @@ impl Set for TextNode {
|
||||
(!families.is_empty()).then(|| families)
|
||||
});
|
||||
|
||||
set!(styles, Self::FAMILY_LIST => list);
|
||||
set!(styles, Self::SERIF_LIST => args.named("serif")?);
|
||||
set!(styles, Self::SANS_SERIF_LIST => args.named("sans-serif")?);
|
||||
set!(styles, Self::MONOSPACE_LIST => args.named("monospace")?);
|
||||
set!(styles, Self::FALLBACK => args.named("fallback")?);
|
||||
set!(styles, Self::STYLE => args.named("style")?);
|
||||
set!(styles, Self::WEIGHT => args.named("weight")?);
|
||||
set!(styles, Self::STRETCH => args.named("stretch")?);
|
||||
set!(styles, Self::FILL => args.named("fill")?.or_else(|| args.find()));
|
||||
set!(styles, Self::SIZE => args.named("size")?.or_else(|| args.find()));
|
||||
set!(styles, Self::TRACKING => args.named("tracking")?.map(Em::new));
|
||||
set!(styles, Self::TOP_EDGE => args.named("top-edge")?);
|
||||
set!(styles, Self::BOTTOM_EDGE => args.named("bottom-edge")?);
|
||||
set!(styles, Self::KERNING => args.named("kerning")?);
|
||||
set!(styles, Self::SMALLCAPS => args.named("smallcaps")?);
|
||||
set!(styles, Self::ALTERNATES => args.named("alternates")?);
|
||||
set!(styles, Self::STYLISTIC_SET => args.named("stylistic-set")?);
|
||||
set!(styles, Self::LIGATURES => args.named("ligatures")?);
|
||||
set!(styles, Self::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?);
|
||||
set!(styles, Self::HISTORICAL_LIGATURES => args.named("historical-ligatures")?);
|
||||
set!(styles, Self::NUMBER_TYPE => args.named("number-type")?);
|
||||
set!(styles, Self::NUMBER_WIDTH => args.named("number-width")?);
|
||||
set!(styles, Self::NUMBER_POSITION => args.named("number-position")?);
|
||||
set!(styles, Self::SLASHED_ZERO => args.named("slashed-zero")?);
|
||||
set!(styles, Self::FRACTIONS => args.named("fractions")?);
|
||||
set!(styles, Self::FEATURES => args.named("features")?);
|
||||
styles.set_opt(Self::FAMILY_LIST, list);
|
||||
styles.set_opt(Self::SERIF_LIST, args.named("serif")?);
|
||||
styles.set_opt(Self::SANS_SERIF_LIST, args.named("sans-serif")?);
|
||||
styles.set_opt(Self::MONOSPACE_LIST, args.named("monospace")?);
|
||||
styles.set_opt(Self::FALLBACK, args.named("fallback")?);
|
||||
styles.set_opt(Self::STYLE, args.named("style")?);
|
||||
styles.set_opt(Self::WEIGHT, args.named("weight")?);
|
||||
styles.set_opt(Self::STRETCH, args.named("stretch")?);
|
||||
styles.set_opt(Self::FILL, args.named("fill")?.or_else(|| args.find()));
|
||||
styles.set_opt(Self::SIZE, args.named("size")?.or_else(|| args.find()));
|
||||
styles.set_opt(Self::TRACKING, args.named("tracking")?.map(Em::new));
|
||||
styles.set_opt(Self::TOP_EDGE, args.named("top-edge")?);
|
||||
styles.set_opt(Self::BOTTOM_EDGE, args.named("bottom-edge")?);
|
||||
styles.set_opt(Self::KERNING, args.named("kerning")?);
|
||||
styles.set_opt(Self::SMALLCAPS, args.named("smallcaps")?);
|
||||
styles.set_opt(Self::ALTERNATES, args.named("alternates")?);
|
||||
styles.set_opt(Self::STYLISTIC_SET, args.named("stylistic-set")?);
|
||||
styles.set_opt(Self::LIGATURES, args.named("ligatures")?);
|
||||
styles.set_opt(
|
||||
Self::DISCRETIONARY_LIGATURES,
|
||||
args.named("discretionary-ligatures")?,
|
||||
);
|
||||
styles.set_opt(
|
||||
Self::HISTORICAL_LIGATURES,
|
||||
args.named("historical-ligatures")?,
|
||||
);
|
||||
styles.set_opt(Self::NUMBER_TYPE, args.named("number-type")?);
|
||||
styles.set_opt(Self::NUMBER_WIDTH, args.named("number-width")?);
|
||||
styles.set_opt(Self::NUMBER_POSITION, args.named("number-position")?);
|
||||
styles.set_opt(Self::SLASHED_ZERO, args.named("slashed-zero")?);
|
||||
styles.set_opt(Self::FRACTIONS, args.named("fractions")?);
|
||||
styles.set_opt(Self::FEATURES, args.named("features")?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -188,8 +194,15 @@ pub enum FontFamily {
|
||||
SansSerif,
|
||||
/// A family in which (almost) all glyphs are of equal width.
|
||||
Monospace,
|
||||
/// A specific family with a name.
|
||||
Named(String),
|
||||
/// A specific font family like "Arial".
|
||||
Named(NamedFamily),
|
||||
}
|
||||
|
||||
impl FontFamily {
|
||||
/// Create a named font family variant, directly from a string.
|
||||
pub fn named(string: &str) -> Self {
|
||||
Self::Named(NamedFamily::new(string))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FontFamily {
|
||||
@ -203,15 +216,37 @@ impl Debug for FontFamily {
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific font family like "Arial".
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub struct NamedFamily(String);
|
||||
|
||||
impl NamedFamily {
|
||||
/// Create a named font family variant.
|
||||
pub fn new(string: &str) -> Self {
|
||||
Self(string.to_lowercase())
|
||||
}
|
||||
|
||||
/// The lowercased family name.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for NamedFamily {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
FontFamily: "font family",
|
||||
Value::Str(string) => Self::Named(string.to_lowercase().into()),
|
||||
Value::Str(string) => Self::named(&string),
|
||||
}
|
||||
|
||||
castable! {
|
||||
Vec<FontFamily>,
|
||||
Expected: "string, generic family or array thereof",
|
||||
Value::Str(string) => vec![FontFamily::Named(string.to_lowercase().into())],
|
||||
Value::Str(string) => vec![FontFamily::named(&string)],
|
||||
Value::Array(values) => {
|
||||
values.into_iter().filter_map(|v| v.cast().ok()).collect()
|
||||
},
|
||||
@ -219,13 +254,13 @@ castable! {
|
||||
}
|
||||
|
||||
castable! {
|
||||
Vec<String>,
|
||||
Vec<NamedFamily>,
|
||||
Expected: "string or array of strings",
|
||||
Value::Str(string) => vec![string.to_lowercase().into()],
|
||||
Value::Str(string) => vec![NamedFamily::new(&string)],
|
||||
Value::Array(values) => values
|
||||
.into_iter()
|
||||
.filter_map(|v| v.cast().ok())
|
||||
.map(|string: EcoString| string.to_lowercase().into())
|
||||
.map(|string: EcoString| NamedFamily::new(&string))
|
||||
.collect(),
|
||||
}
|
||||
|
||||
@ -243,7 +278,10 @@ castable! {
|
||||
castable! {
|
||||
FontWeight,
|
||||
Expected: "integer or string",
|
||||
Value::Int(v) => v.try_into().map_or(Self::BLACK, Self::from_number),
|
||||
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,
|
||||
@ -681,7 +719,7 @@ fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone {
|
||||
|
||||
head.iter()
|
||||
.chain(core)
|
||||
.map(String::as_str)
|
||||
.map(|named| named.as_str())
|
||||
.chain(tail.iter().copied())
|
||||
}
|
||||
|
||||
@ -770,7 +808,7 @@ pub struct ShapedText<'a> {
|
||||
/// The text direction.
|
||||
pub dir: Dir,
|
||||
/// The text's style properties.
|
||||
// TODO(set): Go back to reference.
|
||||
// TODO(style): Go back to reference.
|
||||
pub styles: Styles,
|
||||
/// The font size.
|
||||
pub size: Size,
|
||||
|
@ -149,12 +149,12 @@ impl SourceFile {
|
||||
Self::new(SourceId(0), Path::new(""), src.into())
|
||||
}
|
||||
|
||||
/// The root node of the untyped green tree.
|
||||
/// The root node of the file's untyped green tree.
|
||||
pub fn root(&self) -> &Rc<GreenNode> {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// The file's abstract syntax tree.
|
||||
/// The root node of the file's typed abstract syntax tree.
|
||||
pub fn ast(&self) -> TypResult<Markup> {
|
||||
let red = RedNode::from_root(self.root.clone(), self.id);
|
||||
let errors = red.errors();
|
||||
|
@ -1,4 +1,6 @@
|
||||
//! A typed layer over the red-green tree.
|
||||
//!
|
||||
//! The AST is rooted in the [`Markup`] node.
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
@ -283,6 +285,7 @@ impl Expr {
|
||||
Self::Ident(_)
|
||||
| Self::Call(_)
|
||||
| Self::Let(_)
|
||||
| Self::Set(_)
|
||||
| Self::If(_)
|
||||
| Self::While(_)
|
||||
| Self::For(_)
|
||||
|
@ -187,7 +187,7 @@ impl From<GreenData> for Green {
|
||||
|
||||
impl Debug for GreenData {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}: {}", self.kind, self.len())
|
||||
write!(f, "{:?}: {}", self.kind, self.len)
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 8.9 KiB |
BIN
tests/ref/style/set-block.png
Normal file
After Width: | Height: | Size: 812 B |
BIN
tests/ref/style/set-site.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
tests/ref/style/set-toggle.png
Normal file
After Width: | Height: | Size: 968 B |
BIN
tests/ref/text/em.png
Normal file
After Width: | Height: | Size: 878 B |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.6 KiB |
@ -18,7 +18,17 @@ Add #h(10pt) #h(10pt) up
|
||||
| #h(1fr) | #h(2fr) | #h(1fr) |
|
||||
|
||||
---
|
||||
// Test that spacing has style properties.
|
||||
// Test spacing collapsing with parbreaks.
|
||||
#v(0pt)
|
||||
A
|
||||
#v(0pt)
|
||||
B
|
||||
#v(0pt)
|
||||
|
||||
C #parbreak() D
|
||||
|
||||
---
|
||||
// Test that spacing can carry paragraph and page style properties.
|
||||
|
||||
A[#set par(align: right);#h(1cm)]B
|
||||
[#set page(height: 20pt);#v(1cm)]
|
||||
|
@ -39,3 +39,12 @@ is not.
|
||||
= A {
|
||||
"B"
|
||||
}
|
||||
|
||||
---
|
||||
// Test styling.
|
||||
= Heading
|
||||
|
||||
#set heading(family: "Roboto", fill: eastern)
|
||||
|
||||
===== Heading 🌍
|
||||
#heading(level: 5)[Heading]
|
||||
|
10
tests/typ/style/set-block.typ
Normal file
@ -0,0 +1,10 @@
|
||||
// Test set in code blocks.
|
||||
|
||||
---
|
||||
// Test that template in block is not affected by set
|
||||
// rule in block ...
|
||||
A{set text(fill: eastern); [B]}C
|
||||
|
||||
---
|
||||
// ... no matter the order.
|
||||
A{[B]; set text(fill: eastern)}C
|
30
tests/typ/style/set-site.typ
Normal file
@ -0,0 +1,30 @@
|
||||
// Test that set affects the instantiation site and not the
|
||||
// definition site of a template.
|
||||
|
||||
---
|
||||
// Test that text is affected by instantion-site bold.
|
||||
#let x = [World]
|
||||
Hello *{x}*
|
||||
|
||||
---
|
||||
// Test that lists are affected by correct indents.
|
||||
#set par(spacing: 4pt)
|
||||
#let fruit = [
|
||||
- Apple
|
||||
- Orange
|
||||
#set list(body-indent: 10pt)
|
||||
- Pear
|
||||
]
|
||||
|
||||
- Fruit
|
||||
[#set list(label-indent: 10pt)
|
||||
#fruit]
|
||||
- No more fruit
|
||||
|
||||
---
|
||||
// Test that that par spacing and text style are respected from
|
||||
// the outside, but the more specific fill is respected.
|
||||
#set par(spacing: 4pt)
|
||||
#set text(style: "italic", fill: eastern)
|
||||
#let x = [And the forest #parbreak() lay silent!]
|
||||
#text(fill: forest, x)
|
10
tests/typ/style/set-toggle.typ
Normal file
@ -0,0 +1,10 @@
|
||||
// Test set rules for toggleable booleans.
|
||||
|
||||
---
|
||||
// Test toggling and untoggling.
|
||||
*AB_C*DE
|
||||
*_*
|
||||
|
||||
---
|
||||
// Test toggling and nested templates.
|
||||
*A[B*[_C]]D*E
|
17
tests/typ/text/em.typ
Normal file
@ -0,0 +1,17 @@
|
||||
// Test font-relative sizing.
|
||||
|
||||
---
|
||||
#set text(size: 5pt)
|
||||
A // 5pt
|
||||
[
|
||||
#set text(size: 200%)
|
||||
B // 10pt
|
||||
[
|
||||
#set text(size: 150% + 1pt)
|
||||
C // 16pt
|
||||
#text(size: 200%)[D] // 32pt
|
||||
E // 16pt
|
||||
]
|
||||
F // 10pt
|
||||
]
|
||||
G // 5pt
|
@ -15,6 +15,16 @@ To the right! Where the sunlight peeks behind the mountain.
|
||||
|
||||
Third
|
||||
|
||||
---
|
||||
// Test that paragraph spacing uses correct set rule.
|
||||
Hello
|
||||
|
||||
#set par(spacing: 100pt)
|
||||
World
|
||||
#set par(spacing: 0pt)
|
||||
|
||||
You
|
||||
|
||||
---
|
||||
// Test that paragraph break due to incompatibility respects
|
||||
// spacing defined by the two adjacent paragraphs.
|
||||
|
@ -1,32 +1,21 @@
|
||||
// Test whitespace handling.
|
||||
|
||||
---
|
||||
// Spacing around let.
|
||||
// Spacing around code constructs.
|
||||
A#let x = 1;B #test(x, 1) \
|
||||
A #let x = 2;B #test(x, 2) \
|
||||
A#let x = 3; B #test(x, 3)
|
||||
C #let x = 2;D #test(x, 2) \
|
||||
E#if true [F]G \
|
||||
H #if true{"I"} J \
|
||||
K #if true [L] else []M \
|
||||
#let c = true; N#while c [{c = false}O] P \
|
||||
#let c = true; Q #while c { c = false; "R" } S \
|
||||
T#for _ in (none,) {"U"}V
|
||||
|
||||
---
|
||||
// Spacing around if-else.
|
||||
A#if true [B]C \
|
||||
A#if true [B] C \
|
||||
A #if true{"B"}C \
|
||||
A #if true{"B"} C \
|
||||
A#if false [] else [B]C \
|
||||
A#if true [B] else [] C
|
||||
|
||||
---
|
||||
// Spacing around while loop.
|
||||
#let c = true; A#while c [{c = false}B]C \
|
||||
#let c = true; A#while c [{c = false}B] C \
|
||||
#let c = true; A #while c { c = false; "B" }C \
|
||||
#let c = true; A #while c { c = false; "B" } C
|
||||
|
||||
---
|
||||
// Spacing around for loop.
|
||||
A#for _ in (none,) [B]C \
|
||||
A#for _ in (none,) [B] C \
|
||||
A #for _ in (none,) {"B"}C
|
||||
// Test spacing with comments.
|
||||
A/**/B/**/C \
|
||||
A /**/ B/**/C \
|
||||
A /**/B/**/ C
|
||||
|
||||
---
|
||||
// Test that a run consisting only of whitespace isn't trimmed.
|
||||
@ -37,7 +26,11 @@ A[#set text(serif); ]B
|
||||
Left [#set text(serif);Right].
|
||||
|
||||
---
|
||||
// Test that space at start of line is not trimmed.
|
||||
// Test that linebreak consumed surrounding spaces.
|
||||
#align(center)[A \ B \ C]
|
||||
|
||||
---
|
||||
// Test that space at start of non-backslash-linebreak line isn't trimmed.
|
||||
A{"\n"} B
|
||||
|
||||
---
|
||||
|
@ -17,9 +17,9 @@ use typst::font::Face;
|
||||
use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
|
||||
use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Size, Transform};
|
||||
use typst::image::{Image, RasterImage, Svg};
|
||||
use typst::layout::layout;
|
||||
#[cfg(feature = "layout-cache")]
|
||||
use typst::library::{DocumentNode, PageNode, TextNode};
|
||||
use typst::layout::RootNode;
|
||||
use typst::library::{PageNode, TextNode};
|
||||
use typst::loading::FsLoader;
|
||||
use typst::parse::Scanner;
|
||||
use typst::source::SourceFile;
|
||||
@ -254,16 +254,17 @@ fn test_part(
|
||||
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
|
||||
|
||||
let mut ok = true;
|
||||
let (frames, mut errors) = match ctx.execute(id) {
|
||||
Ok(document) => {
|
||||
let (frames, mut errors) = match ctx.evaluate(id) {
|
||||
Ok(module) => {
|
||||
let tree = module.into_root();
|
||||
if debug {
|
||||
println!("{:#?}", document);
|
||||
println!("{:#?}", tree);
|
||||
}
|
||||
|
||||
let mut frames = layout(ctx, &document);
|
||||
let mut frames = tree.layout(ctx);
|
||||
|
||||
#[cfg(feature = "layout-cache")]
|
||||
(ok &= test_incremental(ctx, i, &document, &frames));
|
||||
(ok &= test_incremental(ctx, i, &tree, &frames));
|
||||
|
||||
if !compare_ref {
|
||||
frames.clear();
|
||||
@ -311,7 +312,7 @@ fn test_part(
|
||||
fn test_incremental(
|
||||
ctx: &mut Context,
|
||||
i: usize,
|
||||
document: &DocumentNode,
|
||||
tree: &RootNode,
|
||||
frames: &[Rc<Frame>],
|
||||
) -> bool {
|
||||
let mut ok = true;
|
||||
@ -326,7 +327,7 @@ fn test_incremental(
|
||||
|
||||
ctx.layouts.turnaround();
|
||||
|
||||
let cached = silenced(|| layout(ctx, document));
|
||||
let cached = silenced(|| tree.layout(ctx));
|
||||
let misses = ctx
|
||||
.layouts
|
||||
.entries()
|
||||
|