mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Set Rules Episode II: Attack of the properties
This commit is contained in:
parent
26bdc1f0f6
commit
40b87d4066
@ -19,17 +19,18 @@ debug = 0
|
||||
opt-level = 2
|
||||
|
||||
[dependencies]
|
||||
fxhash = "0.2.1"
|
||||
fxhash = "0.2"
|
||||
image = { version = "0.23", default-features = false, features = ["png", "jpeg"] }
|
||||
itertools = "0.10"
|
||||
miniz_oxide = "0.4"
|
||||
once_cell = "1"
|
||||
pdf-writer = "0.4"
|
||||
rustybuzz = "0.4"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
svg2pdf = { version = "0.1", default-features = false, features = ["text", "png", "jpeg"] }
|
||||
ttf-parser = "0.12"
|
||||
unicode-bidi = "0.3.5"
|
||||
unicode-segmentation = "1.8"
|
||||
unicode-segmentation = "1"
|
||||
unicode-xid = "0.2"
|
||||
usvg = { version = "0.19", default-features = false, features = ["text"] }
|
||||
xi-unicode = "0.3"
|
||||
|
@ -6,6 +6,8 @@ mod array;
|
||||
mod dict;
|
||||
#[macro_use]
|
||||
mod value;
|
||||
#[macro_use]
|
||||
mod styles;
|
||||
mod capture;
|
||||
mod function;
|
||||
mod node;
|
||||
@ -18,6 +20,7 @@ pub use dict::*;
|
||||
pub use function::*;
|
||||
pub use node::*;
|
||||
pub use scope::*;
|
||||
pub use styles::*;
|
||||
pub use value::*;
|
||||
|
||||
use std::cell::RefMut;
|
||||
@ -31,13 +34,12 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use crate::diag::{At, Error, StrResult, Trace, Tracepoint, TypResult};
|
||||
use crate::geom::{Angle, Fractional, Length, Relative, Spec};
|
||||
use crate::image::ImageStore;
|
||||
use crate::library::{GridNode, TrackSizing};
|
||||
use crate::library::{GridNode, TextNode, TrackSizing};
|
||||
use crate::loading::Loader;
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
use crate::style::Style;
|
||||
use crate::syntax::ast::*;
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::util::{BoolExt, EcoString, RefMutExt};
|
||||
use crate::util::{EcoString, RefMutExt};
|
||||
use crate::Context;
|
||||
|
||||
/// Evaluate a parsed source file into a module.
|
||||
@ -70,8 +72,8 @@ pub struct EvalContext<'a> {
|
||||
pub modules: HashMap<SourceId, Module>,
|
||||
/// The active scopes.
|
||||
pub scopes: Scopes<'a>,
|
||||
/// The active style.
|
||||
pub style: Style,
|
||||
/// The active styles.
|
||||
pub styles: Styles,
|
||||
}
|
||||
|
||||
impl<'a> EvalContext<'a> {
|
||||
@ -84,7 +86,7 @@ impl<'a> EvalContext<'a> {
|
||||
route: vec![source],
|
||||
modules: HashMap::new(),
|
||||
scopes: Scopes::new(Some(&ctx.std)),
|
||||
style: ctx.style.clone(),
|
||||
styles: Styles::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,14 +160,10 @@ impl Eval for Markup {
|
||||
type Output = Node;
|
||||
|
||||
fn eval(&self, ctx: &mut EvalContext) -> TypResult<Self::Output> {
|
||||
let snapshot = ctx.style.clone();
|
||||
|
||||
let mut result = Node::new();
|
||||
for piece in self.nodes() {
|
||||
result += piece.eval(ctx)?;
|
||||
}
|
||||
|
||||
ctx.style = snapshot;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@ -179,11 +177,11 @@ impl Eval for MarkupNode {
|
||||
Self::Linebreak => Node::Linebreak,
|
||||
Self::Parbreak => Node::Parbreak,
|
||||
Self::Strong => {
|
||||
ctx.style.text_mut().strong.flip();
|
||||
ctx.styles.set(TextNode::STRONG, !ctx.styles.get(TextNode::STRONG));
|
||||
Node::new()
|
||||
}
|
||||
Self::Emph => {
|
||||
ctx.style.text_mut().emph.flip();
|
||||
ctx.styles.set(TextNode::EMPH, !ctx.styles.get(TextNode::EMPH));
|
||||
Node::new()
|
||||
}
|
||||
Self::Text(text) => Node::Text(text.clone()),
|
||||
|
@ -9,6 +9,7 @@ use crate::geom::SpecAxis;
|
||||
use crate::layout::{Layout, PackedNode};
|
||||
use crate::library::{
|
||||
Decoration, DocumentNode, FlowChild, FlowNode, PageNode, ParChild, ParNode, Spacing,
|
||||
TextNode,
|
||||
};
|
||||
use crate::util::EcoString;
|
||||
|
||||
@ -158,10 +159,10 @@ impl NodePacker {
|
||||
fn walk(&mut self, node: Node) {
|
||||
match node {
|
||||
Node::Space => {
|
||||
self.push_inline(ParChild::Text(' '.into()));
|
||||
self.push_inline(ParChild::Text(TextNode(' '.into())));
|
||||
}
|
||||
Node::Linebreak => {
|
||||
self.push_inline(ParChild::Text('\n'.into()));
|
||||
self.push_inline(ParChild::Text(TextNode('\n'.into())));
|
||||
}
|
||||
Node::Parbreak => {
|
||||
self.parbreak();
|
||||
@ -170,7 +171,7 @@ impl NodePacker {
|
||||
self.pagebreak();
|
||||
}
|
||||
Node::Text(text) => {
|
||||
self.push_inline(ParChild::Text(text));
|
||||
self.push_inline(ParChild::Text(TextNode(text)));
|
||||
}
|
||||
Node::Spacing(axis, amount) => match axis {
|
||||
SpecAxis::Horizontal => self.push_inline(ParChild::Spacing(amount)),
|
||||
|
124
src/eval/styles.rs
Normal file
124
src/eval/styles.rs
Normal file
@ -0,0 +1,124 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::rc::Rc;
|
||||
|
||||
// Possible optimizations:
|
||||
// - Ref-count map for cheaper cloning and smaller footprint
|
||||
// - Store map in `Option` to make empty maps non-allocating
|
||||
// - Store small properties inline
|
||||
|
||||
/// A map of style properties.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Styles {
|
||||
map: HashMap<TypeId, Rc<dyn Any>>,
|
||||
}
|
||||
|
||||
impl Styles {
|
||||
/// Create a new, empty style map.
|
||||
pub fn new() -> Self {
|
||||
Self { map: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Set the value for a style property.
|
||||
pub fn set<P: Property>(&mut self, _: P, value: P::Value) {
|
||||
self.map.insert(TypeId::of::<P>(), Rc::new(value));
|
||||
}
|
||||
|
||||
/// Get the value of a copyable style property.
|
||||
///
|
||||
/// Returns the property's default value if the map does not contain an
|
||||
/// entry for it.
|
||||
pub fn get<P: Property>(&self, key: P) -> P::Value
|
||||
where
|
||||
P::Value: Copy,
|
||||
{
|
||||
self.get_inner(key).copied().unwrap_or_else(P::default)
|
||||
}
|
||||
|
||||
/// Get a reference to a style property.
|
||||
///
|
||||
/// Returns a reference to the property's default value if the map does not
|
||||
/// contain an entry for it.
|
||||
pub fn get_ref<P: Property>(&self, key: P) -> &P::Value {
|
||||
self.get_inner(key).unwrap_or_else(|| P::default_ref())
|
||||
}
|
||||
|
||||
/// Get a reference to a style directly in this map.
|
||||
fn get_inner<P: Property>(&self, _: P) -> Option<&P::Value> {
|
||||
self.map
|
||||
.get(&TypeId::of::<P>())
|
||||
.and_then(|boxed| boxed.downcast_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Styles {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
// TODO(set): Better debug printing possible?
|
||||
f.pad("Styles(..)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Stylistic property keys.
|
||||
pub trait Property: 'static {
|
||||
/// The type of this property, for example, this could be
|
||||
/// [`Length`](crate::geom::Length) for a `WIDTH` property.
|
||||
type Value;
|
||||
|
||||
/// The default value of the property.
|
||||
fn default() -> Self::Value;
|
||||
|
||||
/// 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
|
||||
/// recreated all the time.
|
||||
fn default_ref() -> &'static Self::Value;
|
||||
}
|
||||
|
||||
macro_rules! set {
|
||||
($ctx:expr, $target:expr => $source:expr) => {
|
||||
if let Some(v) = $source {
|
||||
$ctx.styles.set($target, v);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! properties {
|
||||
($node:ty, $(
|
||||
$(#[$attr:meta])*
|
||||
$name:ident: $type:ty = $default:expr
|
||||
),* $(,)?) => {
|
||||
// TODO(set): Fix possible name clash.
|
||||
mod properties {
|
||||
use std::marker::PhantomData;
|
||||
use super::*;
|
||||
|
||||
$(#[allow(non_snake_case)] mod $name {
|
||||
use $crate::eval::Property;
|
||||
use once_cell::sync::Lazy;
|
||||
use super::*;
|
||||
|
||||
pub struct Key<T>(pub PhantomData<T>);
|
||||
|
||||
impl Property for Key<$type> {
|
||||
type Value = $type;
|
||||
|
||||
fn default() -> Self::Value {
|
||||
$default
|
||||
}
|
||||
|
||||
fn default_ref() -> &'static Self::Value {
|
||||
static LAZY: Lazy<$type> = Lazy::new(|| $default);
|
||||
&*LAZY
|
||||
}
|
||||
}
|
||||
})*
|
||||
|
||||
impl $node {
|
||||
$($(#[$attr])* pub const $name: $name::Key<$type>
|
||||
= $name::Key(PhantomData);)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -171,14 +171,19 @@ pub struct Text {
|
||||
pub face_id: FaceId,
|
||||
/// The font size.
|
||||
pub size: Length,
|
||||
/// The width of the text run.
|
||||
pub width: Length,
|
||||
/// Glyph color.
|
||||
pub fill: Paint,
|
||||
/// The glyphs.
|
||||
pub glyphs: Vec<Glyph>,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
/// The width of the text run.
|
||||
pub fn width(&self) -> Length {
|
||||
self.glyphs.iter().map(|g| g.x_advance.to_length(self.size)).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// A glyph in a run of shaped text.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Glyph {
|
||||
|
26
src/lib.rs
26
src/lib.rs
@ -44,13 +44,12 @@ pub mod library;
|
||||
pub mod loading;
|
||||
pub mod parse;
|
||||
pub mod source;
|
||||
pub mod style;
|
||||
pub mod syntax;
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::diag::TypResult;
|
||||
use crate::eval::{Module, Scope};
|
||||
use crate::eval::{Module, Scope, Styles};
|
||||
use crate::font::FontStore;
|
||||
use crate::frame::Frame;
|
||||
use crate::image::ImageStore;
|
||||
@ -59,7 +58,6 @@ use crate::layout::{EvictionPolicy, LayoutCache};
|
||||
use crate::library::DocumentNode;
|
||||
use crate::loading::Loader;
|
||||
use crate::source::{SourceId, SourceStore};
|
||||
use crate::style::Style;
|
||||
|
||||
/// The core context which holds the loader, configuration and cached artifacts.
|
||||
pub struct Context {
|
||||
@ -76,8 +74,8 @@ pub struct Context {
|
||||
pub layouts: LayoutCache,
|
||||
/// The standard library scope.
|
||||
std: Scope,
|
||||
/// The default style.
|
||||
style: Style,
|
||||
/// The default styles.
|
||||
styles: Styles,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@ -96,9 +94,9 @@ impl Context {
|
||||
&self.std
|
||||
}
|
||||
|
||||
/// A read-only reference to the style.
|
||||
pub fn style(&self) -> &Style {
|
||||
&self.style
|
||||
/// A read-only reference to the styles.
|
||||
pub fn styles(&self) -> &Styles {
|
||||
&self.styles
|
||||
}
|
||||
|
||||
/// Evaluate a source file and return the resulting module.
|
||||
@ -136,7 +134,7 @@ impl Context {
|
||||
/// This struct is created by [`Context::builder`].
|
||||
pub struct ContextBuilder {
|
||||
std: Option<Scope>,
|
||||
style: Option<Style>,
|
||||
styles: Option<Styles>,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
policy: EvictionPolicy,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
@ -151,9 +149,9 @@ impl ContextBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// The initial properties for page size, font selection and so on.
|
||||
pub fn style(mut self, style: Style) -> Self {
|
||||
self.style = Some(style);
|
||||
/// The default properties for page size, font selection and so on.
|
||||
pub fn styles(mut self, styles: Styles) -> Self {
|
||||
self.styles = Some(styles);
|
||||
self
|
||||
}
|
||||
|
||||
@ -185,7 +183,7 @@ impl ContextBuilder {
|
||||
#[cfg(feature = "layout-cache")]
|
||||
layouts: LayoutCache::new(self.policy, self.max_size),
|
||||
std: self.std.unwrap_or_else(library::new),
|
||||
style: self.style.unwrap_or_default(),
|
||||
styles: self.styles.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,7 +192,7 @@ impl Default for ContextBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
std: None,
|
||||
style: None,
|
||||
styles: None,
|
||||
#[cfg(feature = "layout-cache")]
|
||||
policy: EvictionPolicy::default(),
|
||||
#[cfg(feature = "layout-cache")]
|
||||
|
@ -2,18 +2,6 @@ use super::prelude::*;
|
||||
|
||||
/// `align`: Configure the alignment along the layouting axes.
|
||||
pub fn align(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
castable! {
|
||||
Spec<Option<Align>>,
|
||||
Expected: "1d or 2d alignment",
|
||||
@align: Align => {
|
||||
let mut aligns = Spec::default();
|
||||
aligns.set(align.axis(), Some(*align));
|
||||
aligns
|
||||
},
|
||||
@aligns: Spec<Align> => aligns.map(Some),
|
||||
|
||||
}
|
||||
|
||||
let aligns = args.expect::<Spec<_>>("alignment")?;
|
||||
let body = args.expect::<Node>("body")?;
|
||||
|
||||
@ -62,3 +50,19 @@ impl Layout for AlignNode {
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
Align: "alignment",
|
||||
}
|
||||
|
||||
castable! {
|
||||
Spec<Option<Align>>,
|
||||
Expected: "1d or 2d alignment",
|
||||
@align: Align => {
|
||||
let mut aligns = Spec::default();
|
||||
aligns.set(align.axis(), Some(*align));
|
||||
aligns
|
||||
},
|
||||
@aligns: Spec<Align> => aligns.map(Some),
|
||||
|
||||
}
|
||||
|
@ -31,7 +31,11 @@ fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> {
|
||||
pub fn link(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
let url = args.expect::<EcoString>("url")?;
|
||||
let body = args.find().unwrap_or_else(|| {
|
||||
Node::Text(url.trim_start_matches("mailto:").trim_start_matches("tel:").into())
|
||||
let mut text = url.as_str();
|
||||
for prefix in ["mailto:", "tel:"] {
|
||||
text = text.trim_start_matches(prefix);
|
||||
}
|
||||
Node::Text(text.into())
|
||||
});
|
||||
Ok(Value::Node(body.decorate(Decoration::Link(url))))
|
||||
}
|
||||
@ -120,7 +124,7 @@ impl LineDecoration {
|
||||
let extent = self.extent.resolve(text.size);
|
||||
|
||||
let subpos = Point::new(pos.x - extent, pos.y + offset);
|
||||
let target = Point::new(text.width + 2.0 * extent, Length::zero());
|
||||
let target = Point::new(text.width() + 2.0 * extent, Length::zero());
|
||||
let shape = Shape::stroked(Geometry::Line(target), stroke);
|
||||
frame.push(subpos, Element::Shape(shape));
|
||||
}
|
||||
|
@ -39,8 +39,11 @@ pub fn grid(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
row_gutter.unwrap_or(base_gutter),
|
||||
);
|
||||
|
||||
let children = args.all().map(Node::into_block).collect();
|
||||
Ok(Value::block(GridNode { tracks, gutter, children }))
|
||||
Ok(Value::block(GridNode {
|
||||
tracks,
|
||||
gutter,
|
||||
children: args.all().map(Node::into_block).collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// A node that arranges its children in a grid.
|
||||
|
@ -26,7 +26,7 @@ mod prelude {
|
||||
pub use std::rc::Rc;
|
||||
|
||||
pub use crate::diag::{At, TypResult};
|
||||
pub use crate::eval::{Args, EvalContext, Node, Smart, Value};
|
||||
pub use crate::eval::{Args, EvalContext, Node, Property, Smart, Styles, Value};
|
||||
pub use crate::frame::*;
|
||||
pub use crate::geom::*;
|
||||
pub use crate::layout::*;
|
||||
@ -54,7 +54,6 @@ pub use utility::*;
|
||||
|
||||
use crate::eval::{Scope, Value};
|
||||
use crate::geom::*;
|
||||
use crate::style::FontFamily;
|
||||
|
||||
/// Construct a scope containing all standard library definitions.
|
||||
pub fn new() -> Scope {
|
||||
@ -139,15 +138,6 @@ dynamic! {
|
||||
Dir: "direction",
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
Align: "alignment",
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
FontFamily: "font family",
|
||||
Value::Str(string) => Self::Named(string.to_lowercase()),
|
||||
}
|
||||
|
||||
castable! {
|
||||
Paint,
|
||||
Expected: "color",
|
||||
|
@ -1,70 +1,43 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::prelude::*;
|
||||
use super::PadNode;
|
||||
use crate::style::{Paper, PaperClass};
|
||||
|
||||
/// `page`: Configure pages.
|
||||
pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
castable! {
|
||||
Paper,
|
||||
Expected: "string",
|
||||
Value::Str(string) => Paper::from_name(&string).ok_or("unknown paper")?,
|
||||
Value::Str(string) => Paper::from_str(&string).map_err(|e| e.to_string())?,
|
||||
}
|
||||
|
||||
if let Some(paper) = args.named::<Paper>("paper")?.or_else(|| args.find()) {
|
||||
ctx.styles.set(PageNode::CLASS, paper.class());
|
||||
ctx.styles.set(PageNode::WIDTH, Smart::Custom(paper.width()));
|
||||
ctx.styles.set(PageNode::HEIGHT, Smart::Custom(paper.height()));
|
||||
}
|
||||
|
||||
if let Some(width) = args.named("width")? {
|
||||
ctx.styles.set(PageNode::CLASS, PaperClass::Custom);
|
||||
ctx.styles.set(PageNode::WIDTH, width);
|
||||
}
|
||||
|
||||
if let Some(height) = args.named("height")? {
|
||||
ctx.styles.set(PageNode::CLASS, PaperClass::Custom);
|
||||
ctx.styles.set(PageNode::HEIGHT, height);
|
||||
}
|
||||
|
||||
let paper = args.named::<Paper>("paper")?.or_else(|| args.find());
|
||||
let width = args.named::<Smart<_>>("width")?;
|
||||
let height = args.named::<Smart<_>>("height")?;
|
||||
let flip = args.named("flip")?;
|
||||
let margins = args.named("margins")?;
|
||||
let left = args.named("left")?;
|
||||
let top = args.named("top")?;
|
||||
let right = args.named("right")?;
|
||||
let bottom = args.named("bottom")?;
|
||||
let fill = args.named("fill")?;
|
||||
|
||||
let page = ctx.style.page_mut();
|
||||
|
||||
if let Some(paper) = paper {
|
||||
page.class = paper.class();
|
||||
page.size = paper.size();
|
||||
}
|
||||
|
||||
if let Some(width) = width {
|
||||
page.class = PaperClass::Custom;
|
||||
page.size.x = width.unwrap_or(Length::inf());
|
||||
}
|
||||
|
||||
if let Some(height) = height {
|
||||
page.class = PaperClass::Custom;
|
||||
page.size.y = height.unwrap_or(Length::inf());
|
||||
}
|
||||
|
||||
if flip.unwrap_or(false) {
|
||||
std::mem::swap(&mut page.size.x, &mut page.size.y);
|
||||
}
|
||||
|
||||
if let Some(margins) = margins {
|
||||
page.margins = Sides::splat(margins);
|
||||
}
|
||||
|
||||
if let Some(left) = left {
|
||||
page.margins.left = left;
|
||||
}
|
||||
|
||||
if let Some(top) = top {
|
||||
page.margins.top = top;
|
||||
}
|
||||
|
||||
if let Some(right) = right {
|
||||
page.margins.right = right;
|
||||
}
|
||||
|
||||
if let Some(bottom) = bottom {
|
||||
page.margins.bottom = bottom;
|
||||
}
|
||||
|
||||
if let Some(fill) = fill {
|
||||
page.fill = fill;
|
||||
}
|
||||
set!(ctx, PageNode::FLIPPED => args.named("flipped")?);
|
||||
set!(ctx, PageNode::LEFT => args.named("left")?.or(margins));
|
||||
set!(ctx, PageNode::TOP => args.named("top")?.or(margins));
|
||||
set!(ctx, PageNode::RIGHT => args.named("right")?.or(margins));
|
||||
set!(ctx, PageNode::BOTTOM => args.named("bottom")?.or(margins));
|
||||
set!(ctx, PageNode::FILL => args.named("fill")?);
|
||||
|
||||
Ok(Value::None)
|
||||
}
|
||||
@ -74,29 +47,69 @@ pub fn pagebreak(_: &mut EvalContext, _: &mut Args) -> TypResult<Value> {
|
||||
Ok(Value::Node(Node::Pagebreak))
|
||||
}
|
||||
|
||||
/// Layouts its children onto one or multiple pages.
|
||||
/// Layouts its child onto one or multiple pages.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct PageNode(pub PackedNode);
|
||||
|
||||
properties! {
|
||||
PageNode,
|
||||
|
||||
/// The unflipped width of the page.
|
||||
WIDTH: Smart<Length> = Smart::Custom(Paper::default().width()),
|
||||
/// The unflipped height of the page.
|
||||
HEIGHT: Smart<Length> = Smart::Custom(Paper::default().height()),
|
||||
/// The class of paper. Defines the default margins.
|
||||
CLASS: PaperClass = Paper::default().class(),
|
||||
/// Whether the page is flipped into landscape orientation.
|
||||
FLIPPED: bool = false,
|
||||
/// The left margin.
|
||||
LEFT: Smart<Linear> = Smart::Auto,
|
||||
/// The right margin.
|
||||
RIGHT: Smart<Linear> = Smart::Auto,
|
||||
/// The top margin.
|
||||
TOP: Smart<Linear> = Smart::Auto,
|
||||
/// The bottom margin.
|
||||
BOTTOM: Smart<Linear> = Smart::Auto,
|
||||
/// The page's background color.
|
||||
FILL: Option<Paint> = None,
|
||||
}
|
||||
|
||||
impl PageNode {
|
||||
/// Layout the page run into a sequence of frames, one per page.
|
||||
pub fn layout(&self, ctx: &mut LayoutContext) -> Vec<Rc<Frame>> {
|
||||
// TODO(set): Get style from styles.
|
||||
let style = crate::style::PageStyle::default();
|
||||
// TODO(set): Take styles as parameter.
|
||||
let styles = Styles::new();
|
||||
|
||||
// When one of the lengths is infinite the page fits its content along
|
||||
// that axis.
|
||||
let expand = style.size.map(Length::is_finite);
|
||||
let regions = Regions::repeat(style.size, style.size, expand);
|
||||
let width = styles.get(Self::WIDTH).unwrap_or(Length::inf());
|
||||
let height = styles.get(Self::HEIGHT).unwrap_or(Length::inf());
|
||||
let mut size = Size::new(width, height);
|
||||
if styles.get(Self::FLIPPED) {
|
||||
std::mem::swap(&mut size.x, &mut size.y);
|
||||
}
|
||||
|
||||
// Determine the margins.
|
||||
let class = styles.get(Self::CLASS);
|
||||
let default = class.default_margins();
|
||||
let padding = Sides {
|
||||
left: styles.get(Self::LEFT).unwrap_or(default.left),
|
||||
right: styles.get(Self::RIGHT).unwrap_or(default.right),
|
||||
top: styles.get(Self::TOP).unwrap_or(default.top),
|
||||
bottom: styles.get(Self::BOTTOM).unwrap_or(default.bottom),
|
||||
};
|
||||
|
||||
// Pad the child.
|
||||
let padded = PadNode { child: self.0.clone(), padding }.pack();
|
||||
|
||||
// Layout the child.
|
||||
let padding = style.margins();
|
||||
let padded = PadNode { child: self.0.clone(), padding }.pack();
|
||||
let expand = size.map(Length::is_finite);
|
||||
let regions = Regions::repeat(size, size, expand);
|
||||
let mut frames: Vec<_> =
|
||||
padded.layout(ctx, ®ions).into_iter().map(|c| c.item).collect();
|
||||
|
||||
// Add background fill if requested.
|
||||
if let Some(fill) = style.fill {
|
||||
if let Some(fill) = styles.get(Self::FILL) {
|
||||
for frame in &mut frames {
|
||||
let shape = Shape::filled(Geometry::Rect(frame.size), fill);
|
||||
Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape));
|
||||
@ -106,3 +119,256 @@ impl PageNode {
|
||||
frames
|
||||
}
|
||||
}
|
||||
|
||||
/// Specification of a paper.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Paper {
|
||||
/// The broad class this paper belongs to.
|
||||
class: PaperClass,
|
||||
/// The width of the paper in millimeters.
|
||||
width: f64,
|
||||
/// The height of the paper in millimeters.
|
||||
height: f64,
|
||||
}
|
||||
|
||||
impl Paper {
|
||||
/// The class of the paper.
|
||||
pub fn class(self) -> PaperClass {
|
||||
self.class
|
||||
}
|
||||
|
||||
/// The width of the paper.
|
||||
pub fn width(self) -> Length {
|
||||
Length::mm(self.width)
|
||||
}
|
||||
|
||||
/// The height of the paper.
|
||||
pub fn height(self) -> Length {
|
||||
Length::mm(self.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Paper {
|
||||
fn default() -> Self {
|
||||
Paper::A4
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines default margins for a class of related papers.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum PaperClass {
|
||||
Custom,
|
||||
Base,
|
||||
US,
|
||||
Newspaper,
|
||||
Book,
|
||||
}
|
||||
|
||||
impl PaperClass {
|
||||
/// The default margins for this page class.
|
||||
fn default_margins(self) -> Sides<Linear> {
|
||||
let f = |r| Relative::new(r).into();
|
||||
let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
|
||||
match self {
|
||||
Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
|
||||
Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
|
||||
Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
|
||||
Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
|
||||
Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines paper constants and a paper parsing implementation.
|
||||
macro_rules! papers {
|
||||
($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
|
||||
/// Predefined papers.
|
||||
///
|
||||
/// Each paper is parsable from its name in kebab-case.
|
||||
impl Paper {
|
||||
$(pub const $var: Self = Self {
|
||||
class: PaperClass::$class,
|
||||
width: $width,
|
||||
height: $height,
|
||||
};)*
|
||||
}
|
||||
|
||||
impl FromStr for Paper {
|
||||
type Err = ParsePaperError;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
match name.to_lowercase().as_str() {
|
||||
$($($pats)* => Ok(Self::$var),)*
|
||||
_ => Err(ParsePaperError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The error when parsing a [`Paper`] from a string fails.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct ParsePaperError;
|
||||
|
||||
impl Display for ParsePaperError {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad("invalid paper name")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParsePaperError {}
|
||||
};
|
||||
}
|
||||
|
||||
// All paper sizes in mm.
|
||||
//
|
||||
// Resources:
|
||||
// - https://papersizes.io/
|
||||
// - https://en.wikipedia.org/wiki/Paper_size
|
||||
// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
|
||||
// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
|
||||
papers! {
|
||||
// ---------------------------------------------------------------------- //
|
||||
// ISO 216 A Series
|
||||
(A0: Base, 841.0, 1189.0, "a0")
|
||||
(A1: Base, 594.0, 841.0, "a1")
|
||||
(A2: Base, 420.0, 594.0, "a2")
|
||||
(A3: Base, 297.0, 420.0, "a3")
|
||||
(A4: Base, 210.0, 297.0, "a4")
|
||||
(A5: Base, 148.0, 210.0, "a5")
|
||||
(A6: Book, 105.0, 148.0, "a6")
|
||||
(A7: Base, 74.0, 105.0, "a7")
|
||||
(A8: Base, 52.0, 74.0, "a8")
|
||||
(A9: Base, 37.0, 52.0, "a9")
|
||||
(A10: Base, 26.0, 37.0, "a10")
|
||||
(A11: Base, 18.0, 26.0, "a11")
|
||||
|
||||
// ISO 216 B Series
|
||||
(ISO_B1: Base, 707.0, 1000.0, "iso-b1")
|
||||
(ISO_B2: Base, 500.0, 707.0, "iso-b2")
|
||||
(ISO_B3: Base, 353.0, 500.0, "iso-b3")
|
||||
(ISO_B4: Base, 250.0, 353.0, "iso-b4")
|
||||
(ISO_B5: Book, 176.0, 250.0, "iso-b5")
|
||||
(ISO_B6: Book, 125.0, 176.0, "iso-b6")
|
||||
(ISO_B7: Base, 88.0, 125.0, "iso-b7")
|
||||
(ISO_B8: Base, 62.0, 88.0, "iso-b8")
|
||||
|
||||
// ISO 216 C Series
|
||||
(ISO_C3: Base, 324.0, 458.0, "iso-c3")
|
||||
(ISO_C4: Base, 229.0, 324.0, "iso-c4")
|
||||
(ISO_C5: Base, 162.0, 229.0, "iso-c5")
|
||||
(ISO_C6: Base, 114.0, 162.0, "iso-c6")
|
||||
(ISO_C7: Base, 81.0, 114.0, "iso-c7")
|
||||
(ISO_C8: Base, 57.0, 81.0, "iso-c8")
|
||||
|
||||
// DIN D Series (extension to ISO)
|
||||
(DIN_D3: Base, 272.0, 385.0, "din-d3")
|
||||
(DIN_D4: Base, 192.0, 272.0, "din-d4")
|
||||
(DIN_D5: Base, 136.0, 192.0, "din-d5")
|
||||
(DIN_D6: Base, 96.0, 136.0, "din-d6")
|
||||
(DIN_D7: Base, 68.0, 96.0, "din-d7")
|
||||
(DIN_D8: Base, 48.0, 68.0, "din-d8")
|
||||
|
||||
// SIS (used in academia)
|
||||
(SIS_G5: Base, 169.0, 239.0, "sis-g5")
|
||||
(SIS_E5: Base, 115.0, 220.0, "sis-e5")
|
||||
|
||||
// ANSI Extensions
|
||||
(ANSI_A: Base, 216.0, 279.0, "ansi-a")
|
||||
(ANSI_B: Base, 279.0, 432.0, "ansi-b")
|
||||
(ANSI_C: Base, 432.0, 559.0, "ansi-c")
|
||||
(ANSI_D: Base, 559.0, 864.0, "ansi-d")
|
||||
(ANSI_E: Base, 864.0, 1118.0, "ansi-e")
|
||||
|
||||
// ANSI Architectural Paper
|
||||
(ARCH_A: Base, 229.0, 305.0, "arch-a")
|
||||
(ARCH_B: Base, 305.0, 457.0, "arch-b")
|
||||
(ARCH_C: Base, 457.0, 610.0, "arch-c")
|
||||
(ARCH_D: Base, 610.0, 914.0, "arch-d")
|
||||
(ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
|
||||
(ARCH_E: Base, 914.0, 1219.0, "arch-e")
|
||||
|
||||
// JIS B Series
|
||||
(JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
|
||||
(JIS_B1: Base, 728.0, 1030.0, "jis-b1")
|
||||
(JIS_B2: Base, 515.0, 728.0, "jis-b2")
|
||||
(JIS_B3: Base, 364.0, 515.0, "jis-b3")
|
||||
(JIS_B4: Base, 257.0, 364.0, "jis-b4")
|
||||
(JIS_B5: Base, 182.0, 257.0, "jis-b5")
|
||||
(JIS_B6: Base, 128.0, 182.0, "jis-b6")
|
||||
(JIS_B7: Base, 91.0, 128.0, "jis-b7")
|
||||
(JIS_B8: Base, 64.0, 91.0, "jis-b8")
|
||||
(JIS_B9: Base, 45.0, 64.0, "jis-b9")
|
||||
(JIS_B10: Base, 32.0, 45.0, "jis-b10")
|
||||
(JIS_B11: Base, 22.0, 32.0, "jis-b11")
|
||||
|
||||
// SAC D Series
|
||||
(SAC_D0: Base, 764.0, 1064.0, "sac-d0")
|
||||
(SAC_D1: Base, 532.0, 760.0, "sac-d1")
|
||||
(SAC_D2: Base, 380.0, 528.0, "sac-d2")
|
||||
(SAC_D3: Base, 264.0, 376.0, "sac-d3")
|
||||
(SAC_D4: Base, 188.0, 260.0, "sac-d4")
|
||||
(SAC_D5: Base, 130.0, 184.0, "sac-d5")
|
||||
(SAC_D6: Base, 92.0, 126.0, "sac-d6")
|
||||
|
||||
// ISO 7810 ID
|
||||
(ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
|
||||
(ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
|
||||
(ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// Asia
|
||||
(ASIA_F4: Base, 210.0, 330.0, "asia-f4")
|
||||
|
||||
// Japan
|
||||
(JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
|
||||
(JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
|
||||
(JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
|
||||
(JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
|
||||
(JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
|
||||
(JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
|
||||
|
||||
// China
|
||||
(CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
|
||||
|
||||
// Europe
|
||||
(EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
|
||||
|
||||
// French Traditional (AFNOR)
|
||||
(FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
|
||||
(FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
|
||||
(FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
|
||||
(FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
|
||||
(FR_CARRE: Base, 450.0, 560.0, "fr-carré")
|
||||
(FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
|
||||
|
||||
// United Kingdom Imperial
|
||||
(UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
|
||||
(UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
|
||||
(UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
|
||||
(UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
|
||||
(UK_CROWN: Base, 508.0, 381.0, "uk-crown")
|
||||
(UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
|
||||
(UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
|
||||
|
||||
// Unites States
|
||||
(US_LETTER: US, 215.9, 279.4, "us-letter")
|
||||
(US_LEGAL: US, 215.9, 355.6, "us-legal")
|
||||
(US_TABLOID: US, 279.4, 431.8, "us-tabloid")
|
||||
(US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
|
||||
(US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
|
||||
(US_STATEMENT: US, 139.7, 215.9, "us-statement")
|
||||
(US_LEDGER: US, 431.8, 279.4, "us-ledger")
|
||||
(US_OFICIO: US, 215.9, 340.36, "us-oficio")
|
||||
(US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
|
||||
(US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
|
||||
(US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
|
||||
(US_DIGEST: Book, 139.7, 215.9, "us-digest")
|
||||
(US_TRADE: Book, 152.4, 228.6, "us-trade")
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// Other
|
||||
(NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
|
||||
(NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
|
||||
(NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
|
||||
(PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
|
||||
(PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
|
||||
}
|
||||
|
@ -6,8 +6,7 @@ use unicode_bidi::{BidiInfo, Level};
|
||||
use xi_unicode::LineBreakIterator;
|
||||
|
||||
use super::prelude::*;
|
||||
use super::{shape, Decoration, ShapedText, Spacing};
|
||||
use crate::style::TextStyle;
|
||||
use super::{shape, Decoration, ShapedText, Spacing, TextNode};
|
||||
use crate::util::{EcoString, RangeExt, RcExt, SliceExt};
|
||||
|
||||
/// `par`: Configure paragraphs.
|
||||
@ -38,24 +37,14 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
align = Some(v);
|
||||
}
|
||||
|
||||
let par = ctx.style.par_mut();
|
||||
|
||||
if let Some(dir) = dir {
|
||||
par.dir = dir;
|
||||
par.align = if dir == Dir::LTR { Align::Left } else { Align::Right };
|
||||
if let (Some(dir), None) = (dir, align) {
|
||||
align = Some(if dir == Dir::LTR { Align::Left } else { Align::Right });
|
||||
}
|
||||
|
||||
if let Some(align) = align {
|
||||
par.align = align;
|
||||
}
|
||||
|
||||
if let Some(leading) = leading {
|
||||
par.leading = leading;
|
||||
}
|
||||
|
||||
if let Some(spacing) = spacing {
|
||||
par.spacing = spacing;
|
||||
}
|
||||
set!(ctx, ParNode::DIR => dir);
|
||||
set!(ctx, ParNode::ALIGN => align);
|
||||
set!(ctx, ParNode::LEADING => leading);
|
||||
set!(ctx, ParNode::SPACING => spacing);
|
||||
|
||||
Ok(Value::None)
|
||||
}
|
||||
@ -64,24 +53,39 @@ pub fn par(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct ParNode(pub Vec<ParChild>);
|
||||
|
||||
properties! {
|
||||
ParNode,
|
||||
|
||||
/// The direction for text and inline objects.
|
||||
DIR: Dir = Dir::LTR,
|
||||
/// How to align text and inline objects in their line.
|
||||
ALIGN: Align = Align::Left,
|
||||
// TODO(set): Make relative to font size.
|
||||
/// The spacing between lines (dependent on scaled font size).
|
||||
LEADING: Length = Length::pt(6.5),
|
||||
/// The spacing between paragraphs (dependent on scaled font size).
|
||||
SPACING: Length = Length::pt(12.0),
|
||||
}
|
||||
|
||||
impl Layout for ParNode {
|
||||
fn layout(
|
||||
&self,
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
) -> Vec<Constrained<Rc<Frame>>> {
|
||||
// TODO(set): Take styles as parameter.
|
||||
let styles = Styles::new();
|
||||
|
||||
// Collect all text into one string used for BiDi analysis.
|
||||
let text = self.collect_text();
|
||||
|
||||
// Find out the BiDi embedding levels.
|
||||
// TODO(set): Get dir from styles.
|
||||
let bidi = BidiInfo::new(&text, Level::from_dir(Dir::LTR));
|
||||
let default_level = Level::from_dir(styles.get(Self::DIR));
|
||||
let bidi = BidiInfo::new(&text, default_level);
|
||||
|
||||
// Prepare paragraph layout by building a representation on which we can
|
||||
// do line breaking without layouting each and every line from scratch.
|
||||
// TODO(set): Get text style from styles.
|
||||
let style = crate::style::TextStyle::default();
|
||||
let layouter = ParLayouter::new(self, ctx, regions, bidi, &style);
|
||||
let layouter = ParLayouter::new(self, ctx, regions, bidi, &styles);
|
||||
|
||||
// Find suitable linebreaks.
|
||||
layouter.layout(ctx, regions.clone())
|
||||
@ -115,7 +119,7 @@ impl ParNode {
|
||||
fn strings(&self) -> impl Iterator<Item = &str> {
|
||||
self.0.iter().map(|child| match child {
|
||||
ParChild::Spacing(_) => " ",
|
||||
ParChild::Text(ref piece, ..) => piece,
|
||||
ParChild::Text(ref piece, ..) => &piece.0,
|
||||
ParChild::Node(..) => "\u{FFFC}",
|
||||
ParChild::Decorate(_) | ParChild::Undecorate => "",
|
||||
})
|
||||
@ -128,7 +132,8 @@ pub enum ParChild {
|
||||
/// Spacing between other nodes.
|
||||
Spacing(Spacing),
|
||||
/// A run of text and how to align it in its line.
|
||||
Text(EcoString),
|
||||
// TODO(set): A single text run may also have its own style.
|
||||
Text(TextNode),
|
||||
/// Any child node and how to align it in its line.
|
||||
Node(PackedNode),
|
||||
/// A decoration that applies until a matching `Undecorate`.
|
||||
@ -188,7 +193,7 @@ impl<'a> ParLayouter<'a> {
|
||||
ctx: &mut LayoutContext,
|
||||
regions: &Regions,
|
||||
bidi: BidiInfo<'a>,
|
||||
style: &'a TextStyle,
|
||||
styles: &'a Styles,
|
||||
) -> Self {
|
||||
let mut items = vec![];
|
||||
let mut ranges = vec![];
|
||||
@ -215,7 +220,7 @@ impl<'a> ParLayouter<'a> {
|
||||
cursor += group.len();
|
||||
let subrange = start .. cursor;
|
||||
let text = &bidi.text[subrange.clone()];
|
||||
let shaped = shape(ctx, text, style, level.dir());
|
||||
let shaped = shape(ctx, text, styles, level.dir());
|
||||
items.push(ParItem::Text(shaped));
|
||||
ranges.push(subrange);
|
||||
}
|
||||
@ -243,9 +248,8 @@ impl<'a> ParLayouter<'a> {
|
||||
}
|
||||
|
||||
Self {
|
||||
// TODO(set): Get alignment and leading from styles.
|
||||
align: Align::Left,
|
||||
leading: Length::pt(6.0),
|
||||
align: styles.get(ParNode::ALIGN),
|
||||
leading: styles.get(ParNode::LEADING),
|
||||
bidi,
|
||||
items,
|
||||
ranges,
|
||||
@ -540,7 +544,7 @@ impl<'a> LineLayout<'a> {
|
||||
// Compute the reordered ranges in visual order (left to right).
|
||||
self.par.bidi.visual_runs(para, self.line.clone())
|
||||
} else {
|
||||
<_>::default()
|
||||
(vec![], vec![])
|
||||
};
|
||||
|
||||
runs.into_iter()
|
||||
|
@ -7,24 +7,21 @@ pub fn place(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
let tx = args.named("dx")?.unwrap_or_default();
|
||||
let ty = args.named("dy")?.unwrap_or_default();
|
||||
let body: Node = args.expect("body")?;
|
||||
Ok(Value::block(PlacedNode {
|
||||
child: body.into_block().moved(Point::new(tx, ty)).aligned(aligns),
|
||||
}))
|
||||
Ok(Value::block(PlacedNode(
|
||||
body.into_block().moved(Point::new(tx, ty)).aligned(aligns),
|
||||
)))
|
||||
}
|
||||
|
||||
/// A node that places its child absolutely.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct PlacedNode {
|
||||
/// The node to be placed.
|
||||
pub child: PackedNode,
|
||||
}
|
||||
pub struct PlacedNode(pub PackedNode);
|
||||
|
||||
impl PlacedNode {
|
||||
/// Whether this node wants to be placed relative to its its parent's base
|
||||
/// origin. instead of relative to the parent's current flow/cursor
|
||||
/// position.
|
||||
pub fn out_of_flow(&self) -> bool {
|
||||
self.child
|
||||
self.0
|
||||
.downcast::<AlignNode>()
|
||||
.map_or(false, |node| node.aligns.y.is_some())
|
||||
}
|
||||
@ -46,7 +43,7 @@ impl Layout for PlacedNode {
|
||||
Regions::one(regions.base, regions.base, expand)
|
||||
};
|
||||
|
||||
let mut frames = self.child.layout(ctx, &pod);
|
||||
let mut frames = self.0.layout(ctx, &pod);
|
||||
let Constrained { item: frame, cts } = &mut frames[0];
|
||||
|
||||
// If expansion is off, zero all sizes so that we don't take up any
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::ops::Range;
|
||||
|
||||
use rustybuzz::{Feature, UnicodeBuffer};
|
||||
@ -11,18 +12,150 @@ use crate::font::{
|
||||
VerticalFontMetric,
|
||||
};
|
||||
use crate::geom::{Dir, Em, Length, Point, Size};
|
||||
use crate::style::{
|
||||
FontFamily, FontFeatures, NumberPosition, NumberType, NumberWidth, StylisticSet,
|
||||
TextStyle,
|
||||
};
|
||||
use crate::util::{EcoString, SliceExt};
|
||||
|
||||
/// `font`: Configure the font.
|
||||
pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
let list = args.named("family")?.or_else(|| {
|
||||
let families: Vec<_> = args.all().collect();
|
||||
(!families.is_empty()).then(|| families)
|
||||
});
|
||||
|
||||
set!(ctx, TextNode::FAMILY_LIST => list);
|
||||
set!(ctx, TextNode::SERIF_LIST => args.named("serif")?);
|
||||
set!(ctx, TextNode::SANS_SERIF_LIST => args.named("sans-serif")?);
|
||||
set!(ctx, TextNode::MONOSPACE_LIST => args.named("monospace")?);
|
||||
set!(ctx, TextNode::FALLBACK => args.named("fallback")?);
|
||||
set!(ctx, TextNode::STYLE => args.named("style")?);
|
||||
set!(ctx, TextNode::WEIGHT => args.named("weight")?);
|
||||
set!(ctx, TextNode::STRETCH => args.named("stretch")?);
|
||||
set!(ctx, TextNode::FILL => args.named("fill")?.or_else(|| args.find()));
|
||||
set!(ctx, TextNode::SIZE => args.named("size")?.or_else(|| args.find()));
|
||||
set!(ctx, TextNode::TRACKING => args.named("tracking")?.map(Em::new));
|
||||
set!(ctx, TextNode::TOP_EDGE => args.named("top-edge")?);
|
||||
set!(ctx, TextNode::BOTTOM_EDGE => args.named("bottom-edge")?);
|
||||
set!(ctx, TextNode::KERNING => args.named("kerning")?);
|
||||
set!(ctx, TextNode::SMALLCAPS => args.named("smallcaps")?);
|
||||
set!(ctx, TextNode::ALTERNATES => args.named("alternates")?);
|
||||
set!(ctx, TextNode::STYLISTIC_SET => args.named("stylistic-set")?);
|
||||
set!(ctx, TextNode::LIGATURES => args.named("ligatures")?);
|
||||
set!(ctx, TextNode::DISCRETIONARY_LIGATURES => args.named("discretionary-ligatures")?);
|
||||
set!(ctx, TextNode::HISTORICAL_LIGATURES => args.named("historical-ligatures")?);
|
||||
set!(ctx, TextNode::NUMBER_TYPE => args.named("number-type")?);
|
||||
set!(ctx, TextNode::NUMBER_WIDTH => args.named("number-width")?);
|
||||
set!(ctx, TextNode::NUMBER_POSITION => args.named("number-position")?);
|
||||
set!(ctx, TextNode::SLASHED_ZERO => args.named("slashed-zero")?);
|
||||
set!(ctx, TextNode::FRACTIONS => args.named("fractions")?);
|
||||
set!(ctx, TextNode::FEATURES => args.named("features")?);
|
||||
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
/// A single run of text with the same style.
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct TextNode(pub EcoString);
|
||||
|
||||
properties! {
|
||||
TextNode,
|
||||
|
||||
/// A prioritized sequence of font families.
|
||||
FAMILY_LIST: Vec<FontFamily> = vec![FontFamily::SansSerif],
|
||||
/// The serif font family/families.
|
||||
SERIF_LIST: Vec<String> = vec!["ibm plex serif".into()],
|
||||
/// The sans-serif font family/families.
|
||||
SANS_SERIF_LIST: Vec<String> = vec!["ibm plex sans".into()],
|
||||
/// The monospace font family/families.
|
||||
MONOSPACE_LIST: Vec<String> = vec!["ibm plex mono".into()],
|
||||
/// Whether to allow font fallback when the primary font list contains no
|
||||
/// match.
|
||||
FALLBACK: bool = true,
|
||||
|
||||
/// How the font is styled.
|
||||
STYLE: FontStyle = FontStyle::Normal,
|
||||
/// The boldness / thickness of the font's glyphs.
|
||||
WEIGHT: FontWeight = FontWeight::REGULAR,
|
||||
/// The width of the glyphs.
|
||||
STRETCH: FontStretch = FontStretch::NORMAL,
|
||||
/// Whether the font weight should be increased by 300.
|
||||
STRONG: bool = false,
|
||||
/// Whether the the font style should be inverted.
|
||||
EMPH: bool = false,
|
||||
/// Whether a monospace font should be preferred.
|
||||
MONOSPACE: bool = false,
|
||||
/// The glyph fill color.
|
||||
FILL: Paint = RgbaColor::BLACK.into(),
|
||||
|
||||
/// The size of the glyphs.
|
||||
// TODO(set): Resolve relative to outer font size.
|
||||
SIZE: Length = Length::pt(11.0),
|
||||
/// The amount of space that should be added between characters.
|
||||
TRACKING: Em = Em::zero(),
|
||||
/// The top end of the text bounding box.
|
||||
TOP_EDGE: VerticalFontMetric = VerticalFontMetric::CapHeight,
|
||||
/// The bottom end of the text bounding box.
|
||||
BOTTOM_EDGE: VerticalFontMetric = VerticalFontMetric::Baseline,
|
||||
|
||||
/// Whether to apply kerning ("kern").
|
||||
KERNING: bool = true,
|
||||
/// Whether small capital glyphs should be used. ("smcp")
|
||||
SMALLCAPS: bool = false,
|
||||
/// Whether to apply stylistic alternates. ("salt")
|
||||
ALTERNATES: bool = false,
|
||||
/// Which stylistic set to apply. ("ss01" - "ss20")
|
||||
STYLISTIC_SET: Option<StylisticSet> = None,
|
||||
/// Whether standard ligatures are active. ("liga", "clig")
|
||||
LIGATURES: bool = true,
|
||||
/// Whether ligatures that should be used sparingly are active. ("dlig")
|
||||
DISCRETIONARY_LIGATURES: bool = false,
|
||||
/// Whether historical ligatures are active. ("hlig")
|
||||
HISTORICAL_LIGATURES: bool = false,
|
||||
/// Which kind of numbers / figures to select.
|
||||
NUMBER_TYPE: Smart<NumberType> = Smart::Auto,
|
||||
/// The width of numbers / figures.
|
||||
NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto,
|
||||
/// How to position numbers.
|
||||
NUMBER_POSITION: NumberPosition = NumberPosition::Normal,
|
||||
/// Whether to have a slash through the zero glyph. ("zero")
|
||||
SLASHED_ZERO: bool = false,
|
||||
/// Whether to convert fractions. ("frac")
|
||||
FRACTIONS: bool = false,
|
||||
/// Raw OpenType features to apply.
|
||||
FEATURES: Vec<(Tag, u32)> = vec![],
|
||||
}
|
||||
|
||||
/// A generic or named font family.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub enum FontFamily {
|
||||
/// A family that has "serifs", small strokes attached to letters.
|
||||
Serif,
|
||||
/// A family in which glyphs do not have "serifs", small attached strokes.
|
||||
SansSerif,
|
||||
/// A family in which (almost) all glyphs are of equal width.
|
||||
Monospace,
|
||||
/// A specific family with a name.
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl Debug for FontFamily {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad(match self {
|
||||
Self::Serif => "serif",
|
||||
Self::SansSerif => "sans-serif",
|
||||
Self::Monospace => "monospace",
|
||||
Self::Named(s) => s,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dynamic! {
|
||||
FontFamily: "font family",
|
||||
Value::Str(string) => Self::Named(string.to_lowercase().into()),
|
||||
}
|
||||
|
||||
castable! {
|
||||
Vec<FontFamily>,
|
||||
Expected: "string, generic family or array thereof",
|
||||
Value::Str(string) => vec![FontFamily::Named(string.to_lowercase())],
|
||||
Value::Str(string) => vec![FontFamily::Named(string.to_lowercase().into())],
|
||||
Value::Array(values) => {
|
||||
values.into_iter().filter_map(|v| v.cast().ok()).collect()
|
||||
},
|
||||
@ -30,13 +163,13 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
}
|
||||
|
||||
castable! {
|
||||
Vec<EcoString>,
|
||||
Vec<String>,
|
||||
Expected: "string or array of strings",
|
||||
Value::Str(string) => vec![string.to_lowercase()],
|
||||
Value::Str(string) => vec![string.to_lowercase().into()],
|
||||
Value::Array(values) => values
|
||||
.into_iter()
|
||||
.filter_map(|v| v.cast().ok())
|
||||
.map(|string: EcoString| string.to_lowercase())
|
||||
.map(|string: EcoString| string.to_lowercase().into())
|
||||
.collect(),
|
||||
}
|
||||
|
||||
@ -91,6 +224,22 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
},
|
||||
}
|
||||
|
||||
/// A stylistic set in a font face.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct StylisticSet(u8);
|
||||
|
||||
impl StylisticSet {
|
||||
/// Creates a new set, clamping to 1-20.
|
||||
pub fn new(index: u8) -> Self {
|
||||
Self(index.clamp(1, 20))
|
||||
}
|
||||
|
||||
/// Get the value, guaranteed to be 1-20.
|
||||
pub fn get(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
castable! {
|
||||
StylisticSet,
|
||||
Expected: "integer",
|
||||
@ -100,6 +249,15 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
},
|
||||
}
|
||||
|
||||
/// Which kind of numbers / figures to select.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberType {
|
||||
/// Numbers that fit well with capital text. ("lnum")
|
||||
Lining,
|
||||
/// Numbers that fit well into flow of upper- and lowercase text. ("onum")
|
||||
OldStyle,
|
||||
}
|
||||
|
||||
castable! {
|
||||
NumberType,
|
||||
Expected: "string",
|
||||
@ -110,6 +268,15 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
},
|
||||
}
|
||||
|
||||
/// The width of numbers / figures.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberWidth {
|
||||
/// Number widths are glyph specific. ("pnum")
|
||||
Proportional,
|
||||
/// All numbers are of equal width / monospaced. ("tnum")
|
||||
Tabular,
|
||||
}
|
||||
|
||||
castable! {
|
||||
NumberWidth,
|
||||
Expected: "string",
|
||||
@ -120,6 +287,17 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
},
|
||||
}
|
||||
|
||||
/// How to position numbers.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberPosition {
|
||||
/// Numbers are positioned on the same baseline as text.
|
||||
Normal,
|
||||
/// Numbers are smaller and placed at the bottom. ("subs")
|
||||
Subscript,
|
||||
/// Numbers are smaller and placed at the top. ("sups")
|
||||
Superscript,
|
||||
}
|
||||
|
||||
castable! {
|
||||
NumberPosition,
|
||||
Expected: "string",
|
||||
@ -149,81 +327,11 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
||||
.collect(),
|
||||
}
|
||||
|
||||
let list = args.named("family")?.or_else(|| {
|
||||
let families: Vec<_> = args.all().collect();
|
||||
(!families.is_empty()).then(|| families)
|
||||
});
|
||||
|
||||
let serif = args.named("serif")?;
|
||||
let sans_serif = args.named("sans-serif")?;
|
||||
let monospace = args.named("monospace")?;
|
||||
let fallback = args.named("fallback")?;
|
||||
let style = args.named("style")?;
|
||||
let weight = args.named("weight")?;
|
||||
let stretch = args.named("stretch")?;
|
||||
let size = args.named::<Linear>("size")?.or_else(|| args.find());
|
||||
let tracking = args.named("tracking")?.map(Em::new);
|
||||
let top_edge = args.named("top-edge")?;
|
||||
let bottom_edge = args.named("bottom-edge")?;
|
||||
let fill = args.named("fill")?.or_else(|| args.find());
|
||||
let kerning = args.named("kerning")?;
|
||||
let smallcaps = args.named("smallcaps")?;
|
||||
let alternates = args.named("alternates")?;
|
||||
let stylistic_set = args.named("stylistic-set")?;
|
||||
let ligatures = args.named("ligatures")?;
|
||||
let discretionary_ligatures = args.named("discretionary-ligatures")?;
|
||||
let historical_ligatures = args.named("historical-ligatures")?;
|
||||
let number_type = args.named("number-type")?;
|
||||
let number_width = args.named("number-width")?;
|
||||
let number_position = args.named("number-position")?;
|
||||
let slashed_zero = args.named("slashed-zero")?;
|
||||
let fractions = args.named("fractions")?;
|
||||
let features = args.named("features")?;
|
||||
|
||||
macro_rules! set {
|
||||
($target:expr => $source:expr) => {
|
||||
if let Some(v) = $source {
|
||||
$target = v;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let text = ctx.style.text_mut();
|
||||
set!(text.families_mut().list => list.clone());
|
||||
set!(text.families_mut().serif => serif.clone());
|
||||
set!(text.families_mut().sans_serif => sans_serif.clone());
|
||||
set!(text.families_mut().monospace => monospace.clone());
|
||||
set!(text.fallback => fallback);
|
||||
set!(text.variant.style => style);
|
||||
set!(text.variant.weight => weight);
|
||||
set!(text.variant.stretch => stretch);
|
||||
set!(text.size => size.map(|v| v.resolve(text.size)));
|
||||
set!(text.tracking => tracking);
|
||||
set!(text.top_edge => top_edge);
|
||||
set!(text.bottom_edge => bottom_edge);
|
||||
set!(text.fill => fill);
|
||||
set!(text.features_mut().kerning => kerning);
|
||||
set!(text.features_mut().smallcaps => smallcaps);
|
||||
set!(text.features_mut().alternates => alternates);
|
||||
set!(text.features_mut().stylistic_set => stylistic_set);
|
||||
set!(text.features_mut().ligatures.standard => ligatures);
|
||||
set!(text.features_mut().ligatures.discretionary => discretionary_ligatures);
|
||||
set!(text.features_mut().ligatures.historical => historical_ligatures);
|
||||
set!(text.features_mut().numbers.type_ => number_type);
|
||||
set!(text.features_mut().numbers.width => number_width);
|
||||
set!(text.features_mut().numbers.position => number_position);
|
||||
set!(text.features_mut().numbers.slashed_zero => slashed_zero);
|
||||
set!(text.features_mut().numbers.fractions => fractions);
|
||||
set!(text.features_mut().raw => features.clone());
|
||||
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
/// Shape text into [`ShapedText`].
|
||||
pub fn shape<'a>(
|
||||
ctx: &mut LayoutContext,
|
||||
text: &'a str,
|
||||
style: &'a TextStyle,
|
||||
styles: &'a Styles,
|
||||
dir: Dir,
|
||||
) -> ShapedText<'a> {
|
||||
let mut glyphs = vec![];
|
||||
@ -233,188 +341,27 @@ pub fn shape<'a>(
|
||||
&mut glyphs,
|
||||
0,
|
||||
text,
|
||||
style.variant(),
|
||||
style.families(),
|
||||
variant(styles),
|
||||
families(styles),
|
||||
None,
|
||||
dir,
|
||||
&tags(&style.features),
|
||||
&tags(styles),
|
||||
);
|
||||
}
|
||||
|
||||
track(&mut glyphs, style.tracking);
|
||||
track(&mut glyphs, styles.get(TextNode::TRACKING));
|
||||
let (size, baseline) = measure(ctx, &glyphs, styles);
|
||||
|
||||
let (size, baseline) = measure(ctx, &glyphs, style);
|
||||
ShapedText {
|
||||
text,
|
||||
dir,
|
||||
style,
|
||||
styles,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Owned(glyphs),
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of shaping text.
|
||||
///
|
||||
/// This type contains owned or borrowed shaped text runs, which can be
|
||||
/// measured, used to reshape substrings more quickly and converted into a
|
||||
/// frame.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShapedText<'a> {
|
||||
/// The text that was shaped.
|
||||
pub text: &'a str,
|
||||
/// The text direction.
|
||||
pub dir: Dir,
|
||||
/// The properties used for font selection.
|
||||
pub style: &'a TextStyle,
|
||||
/// The font size.
|
||||
pub size: Size,
|
||||
/// The baseline from the top of the frame.
|
||||
pub baseline: Length,
|
||||
/// The shaped glyphs.
|
||||
pub glyphs: Cow<'a, [ShapedGlyph]>,
|
||||
}
|
||||
|
||||
/// A single glyph resulting from shaping.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ShapedGlyph {
|
||||
/// The font face the glyph is contained in.
|
||||
pub face_id: FaceId,
|
||||
/// The glyph's index in the face.
|
||||
pub glyph_id: u16,
|
||||
/// The advance width of the glyph.
|
||||
pub x_advance: Em,
|
||||
/// The horizontal offset of the glyph.
|
||||
pub x_offset: Em,
|
||||
/// The start index of the glyph in the source text.
|
||||
pub text_index: usize,
|
||||
/// Whether splitting the shaping result before this glyph would yield the
|
||||
/// same results as shaping the parts to both sides of `text_index`
|
||||
/// separately.
|
||||
pub safe_to_break: bool,
|
||||
}
|
||||
|
||||
impl<'a> ShapedText<'a> {
|
||||
/// Build the shaped text's frame.
|
||||
pub fn build(&self) -> Frame {
|
||||
let mut offset = Length::zero();
|
||||
let mut frame = Frame::new(self.size);
|
||||
frame.baseline = Some(self.baseline);
|
||||
|
||||
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
||||
let pos = Point::new(offset, self.baseline);
|
||||
|
||||
let mut text = Text {
|
||||
face_id,
|
||||
size: self.style.size,
|
||||
width: Length::zero(),
|
||||
fill: self.style.fill,
|
||||
glyphs: vec![],
|
||||
};
|
||||
|
||||
for glyph in group {
|
||||
text.glyphs.push(Glyph {
|
||||
id: glyph.glyph_id,
|
||||
x_advance: glyph.x_advance,
|
||||
x_offset: glyph.x_offset,
|
||||
});
|
||||
text.width += glyph.x_advance.to_length(text.size);
|
||||
}
|
||||
|
||||
offset += text.width;
|
||||
frame.push(pos, Element::Text(text));
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
/// Reshape a range of the shaped text, reusing information from this
|
||||
/// shaping process if possible.
|
||||
pub fn reshape(
|
||||
&'a self,
|
||||
ctx: &mut LayoutContext,
|
||||
text_range: Range<usize>,
|
||||
) -> ShapedText<'a> {
|
||||
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
||||
let (size, baseline) = measure(ctx, glyphs, self.style);
|
||||
Self {
|
||||
text: &self.text[text_range],
|
||||
dir: self.dir,
|
||||
style: self.style,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Borrowed(glyphs),
|
||||
}
|
||||
} else {
|
||||
shape(ctx, &self.text[text_range], self.style, self.dir)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the subslice of glyphs that represent the given text range if both
|
||||
/// sides are safe to break.
|
||||
fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
|
||||
let Range { mut start, mut end } = text_range;
|
||||
if !self.dir.is_positive() {
|
||||
std::mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
let left = self.find_safe_to_break(start, Side::Left)?;
|
||||
let right = self.find_safe_to_break(end, Side::Right)?;
|
||||
Some(&self.glyphs[left .. right])
|
||||
}
|
||||
|
||||
/// Find the glyph offset matching the text index that is most towards the
|
||||
/// given side and safe-to-break.
|
||||
fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
|
||||
let ltr = self.dir.is_positive();
|
||||
|
||||
// Handle edge cases.
|
||||
let len = self.glyphs.len();
|
||||
if text_index == 0 {
|
||||
return Some(if ltr { 0 } else { len });
|
||||
} else if text_index == self.text.len() {
|
||||
return Some(if ltr { len } else { 0 });
|
||||
}
|
||||
|
||||
// Find any glyph with the text index.
|
||||
let mut idx = self
|
||||
.glyphs
|
||||
.binary_search_by(|g| {
|
||||
let ordering = g.text_index.cmp(&text_index);
|
||||
if ltr { ordering } else { ordering.reverse() }
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let next = match towards {
|
||||
Side::Left => usize::checked_sub,
|
||||
Side::Right => usize::checked_add,
|
||||
};
|
||||
|
||||
// Search for the outermost glyph with the text index.
|
||||
while let Some(next) = next(idx, 1) {
|
||||
if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
|
||||
break;
|
||||
}
|
||||
idx = next;
|
||||
}
|
||||
|
||||
// RTL needs offset one because the left side of the range should be
|
||||
// exclusive and the right side inclusive, contrary to the normal
|
||||
// behaviour of ranges.
|
||||
if !ltr {
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
self.glyphs[idx].safe_to_break.then(|| idx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A visual side.
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Shape text with font fallback using the `families` iterator.
|
||||
fn shape_segment<'a>(
|
||||
fonts: &mut FontStore,
|
||||
@ -562,23 +509,27 @@ fn track(glyphs: &mut [ShapedGlyph], tracking: Em) {
|
||||
fn measure(
|
||||
ctx: &mut LayoutContext,
|
||||
glyphs: &[ShapedGlyph],
|
||||
style: &TextStyle,
|
||||
styles: &Styles,
|
||||
) -> (Size, Length) {
|
||||
let mut width = Length::zero();
|
||||
let mut top = Length::zero();
|
||||
let mut bottom = Length::zero();
|
||||
|
||||
let size = styles.get(TextNode::SIZE);
|
||||
let top_edge = styles.get(TextNode::TOP_EDGE);
|
||||
let bottom_edge = styles.get(TextNode::BOTTOM_EDGE);
|
||||
|
||||
// Expand top and bottom by reading the face's vertical metrics.
|
||||
let mut expand = |face: &Face| {
|
||||
top.set_max(face.vertical_metric(style.top_edge, style.size));
|
||||
bottom.set_max(-face.vertical_metric(style.bottom_edge, style.size));
|
||||
top.set_max(face.vertical_metric(top_edge, size));
|
||||
bottom.set_max(-face.vertical_metric(bottom_edge, size));
|
||||
};
|
||||
|
||||
if glyphs.is_empty() {
|
||||
// When there are no glyphs, we just use the vertical metrics of the
|
||||
// first available font.
|
||||
for family in style.families() {
|
||||
if let Some(face_id) = ctx.fonts.select(family, style.variant) {
|
||||
for family in families(styles) {
|
||||
if let Some(face_id) = ctx.fonts.select(family, variant(styles)) {
|
||||
expand(ctx.fonts.get(face_id));
|
||||
break;
|
||||
}
|
||||
@ -589,7 +540,7 @@ fn measure(
|
||||
expand(face);
|
||||
|
||||
for glyph in group {
|
||||
width += glyph.x_advance.to_length(style.size);
|
||||
width += glyph.x_advance.to_length(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -597,75 +548,286 @@ fn measure(
|
||||
(Size::new(width, top + bottom), top)
|
||||
}
|
||||
|
||||
/// Resolved the font variant with `STRONG` and `EMPH` factored in.
|
||||
fn variant(styles: &Styles) -> FontVariant {
|
||||
let mut variant = FontVariant::new(
|
||||
styles.get(TextNode::STYLE),
|
||||
styles.get(TextNode::WEIGHT),
|
||||
styles.get(TextNode::STRETCH),
|
||||
);
|
||||
|
||||
if styles.get(TextNode::STRONG) {
|
||||
variant.weight = variant.weight.thicken(300);
|
||||
}
|
||||
|
||||
if styles.get(TextNode::EMPH) {
|
||||
variant.style = match variant.style {
|
||||
FontStyle::Normal => FontStyle::Italic,
|
||||
FontStyle::Italic => FontStyle::Normal,
|
||||
FontStyle::Oblique => FontStyle::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
variant
|
||||
}
|
||||
|
||||
/// Resolve a prioritized iterator over the font families.
|
||||
fn families(styles: &Styles) -> impl Iterator<Item = &str> + Clone {
|
||||
let head = if styles.get(TextNode::MONOSPACE) {
|
||||
styles.get_ref(TextNode::MONOSPACE_LIST).as_slice()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
let core = styles.get_ref(TextNode::FAMILY_LIST).iter().flat_map(move |family| {
|
||||
match family {
|
||||
FontFamily::Named(name) => std::slice::from_ref(name),
|
||||
FontFamily::Serif => styles.get_ref(TextNode::SERIF_LIST),
|
||||
FontFamily::SansSerif => styles.get_ref(TextNode::SANS_SERIF_LIST),
|
||||
FontFamily::Monospace => styles.get_ref(TextNode::MONOSPACE_LIST),
|
||||
}
|
||||
});
|
||||
|
||||
let tail: &[&str] = if styles.get(TextNode::FALLBACK) {
|
||||
&["ibm plex sans", "latin modern math", "twitter color emoji"]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
head.iter()
|
||||
.chain(core)
|
||||
.map(String::as_str)
|
||||
.chain(tail.iter().copied())
|
||||
}
|
||||
|
||||
/// Collect the tags of the OpenType features to apply.
|
||||
fn tags(features: &FontFeatures) -> Vec<Feature> {
|
||||
fn tags(styles: &Styles) -> Vec<Feature> {
|
||||
let mut tags = vec![];
|
||||
let mut feat = |tag, value| {
|
||||
tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
|
||||
};
|
||||
|
||||
// Features that are on by default in Harfbuzz are only added if disabled.
|
||||
if !features.kerning {
|
||||
if !styles.get(TextNode::KERNING) {
|
||||
feat(b"kern", 0);
|
||||
}
|
||||
|
||||
// Features that are off by default in Harfbuzz are only added if enabled.
|
||||
if features.smallcaps {
|
||||
if styles.get(TextNode::SMALLCAPS) {
|
||||
feat(b"smcp", 1);
|
||||
}
|
||||
|
||||
if features.alternates {
|
||||
if styles.get(TextNode::ALTERNATES) {
|
||||
feat(b"salt", 1);
|
||||
}
|
||||
|
||||
let storage;
|
||||
if let Some(set) = features.stylistic_set {
|
||||
if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
|
||||
storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
|
||||
feat(&storage, 1);
|
||||
}
|
||||
|
||||
if !features.ligatures.standard {
|
||||
if !styles.get(TextNode::LIGATURES) {
|
||||
feat(b"liga", 0);
|
||||
feat(b"clig", 0);
|
||||
}
|
||||
|
||||
if features.ligatures.discretionary {
|
||||
if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
|
||||
feat(b"dlig", 1);
|
||||
}
|
||||
|
||||
if features.ligatures.historical {
|
||||
if styles.get(TextNode::HISTORICAL_LIGATURES) {
|
||||
feat(b"hilg", 1);
|
||||
}
|
||||
|
||||
match features.numbers.type_ {
|
||||
match styles.get(TextNode::NUMBER_TYPE) {
|
||||
Smart::Auto => {}
|
||||
Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
|
||||
Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
|
||||
}
|
||||
|
||||
match features.numbers.width {
|
||||
match styles.get(TextNode::NUMBER_WIDTH) {
|
||||
Smart::Auto => {}
|
||||
Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
|
||||
Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
|
||||
}
|
||||
|
||||
match features.numbers.position {
|
||||
match styles.get(TextNode::NUMBER_POSITION) {
|
||||
NumberPosition::Normal => {}
|
||||
NumberPosition::Subscript => feat(b"subs", 1),
|
||||
NumberPosition::Superscript => feat(b"sups", 1),
|
||||
}
|
||||
|
||||
if features.numbers.slashed_zero {
|
||||
if styles.get(TextNode::SLASHED_ZERO) {
|
||||
feat(b"zero", 1);
|
||||
}
|
||||
|
||||
if features.numbers.fractions {
|
||||
if styles.get(TextNode::FRACTIONS) {
|
||||
feat(b"frac", 1);
|
||||
}
|
||||
|
||||
for &(tag, value) in features.raw.iter() {
|
||||
for &(tag, value) in styles.get_ref(TextNode::FEATURES).iter() {
|
||||
tags.push(Feature::new(tag, value, ..))
|
||||
}
|
||||
|
||||
tags
|
||||
}
|
||||
|
||||
/// The result of shaping text.
|
||||
///
|
||||
/// This type contains owned or borrowed shaped text runs, which can be
|
||||
/// measured, used to reshape substrings more quickly and converted into a
|
||||
/// frame.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ShapedText<'a> {
|
||||
/// The text that was shaped.
|
||||
pub text: &'a str,
|
||||
/// The text direction.
|
||||
pub dir: Dir,
|
||||
/// The text's style properties.
|
||||
pub styles: &'a Styles,
|
||||
/// The font size.
|
||||
pub size: Size,
|
||||
/// The baseline from the top of the frame.
|
||||
pub baseline: Length,
|
||||
/// The shaped glyphs.
|
||||
pub glyphs: Cow<'a, [ShapedGlyph]>,
|
||||
}
|
||||
|
||||
/// A single glyph resulting from shaping.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ShapedGlyph {
|
||||
/// The font face the glyph is contained in.
|
||||
pub face_id: FaceId,
|
||||
/// The glyph's index in the face.
|
||||
pub glyph_id: u16,
|
||||
/// The advance width of the glyph.
|
||||
pub x_advance: Em,
|
||||
/// The horizontal offset of the glyph.
|
||||
pub x_offset: Em,
|
||||
/// The start index of the glyph in the source text.
|
||||
pub text_index: usize,
|
||||
/// Whether splitting the shaping result before this glyph would yield the
|
||||
/// same results as shaping the parts to both sides of `text_index`
|
||||
/// separately.
|
||||
pub safe_to_break: bool,
|
||||
}
|
||||
|
||||
impl<'a> ShapedText<'a> {
|
||||
/// Build the shaped text's frame.
|
||||
pub fn build(&self) -> Frame {
|
||||
let mut offset = Length::zero();
|
||||
let mut frame = Frame::new(self.size);
|
||||
frame.baseline = Some(self.baseline);
|
||||
|
||||
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
|
||||
let pos = Point::new(offset, self.baseline);
|
||||
|
||||
let mut text = Text {
|
||||
face_id,
|
||||
size: self.styles.get(TextNode::SIZE),
|
||||
fill: self.styles.get(TextNode::FILL),
|
||||
glyphs: vec![],
|
||||
};
|
||||
|
||||
for glyph in group {
|
||||
text.glyphs.push(Glyph {
|
||||
id: glyph.glyph_id,
|
||||
x_advance: glyph.x_advance,
|
||||
x_offset: glyph.x_offset,
|
||||
});
|
||||
}
|
||||
|
||||
offset += text.width();
|
||||
frame.push(pos, Element::Text(text));
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
/// Reshape a range of the shaped text, reusing information from this
|
||||
/// shaping process if possible.
|
||||
pub fn reshape(
|
||||
&'a self,
|
||||
ctx: &mut LayoutContext,
|
||||
text_range: Range<usize>,
|
||||
) -> ShapedText<'a> {
|
||||
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
|
||||
let (size, baseline) = measure(ctx, glyphs, self.styles);
|
||||
Self {
|
||||
text: &self.text[text_range],
|
||||
dir: self.dir,
|
||||
styles: self.styles,
|
||||
size,
|
||||
baseline,
|
||||
glyphs: Cow::Borrowed(glyphs),
|
||||
}
|
||||
} else {
|
||||
shape(ctx, &self.text[text_range], self.styles, self.dir)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the subslice of glyphs that represent the given text range if both
|
||||
/// sides are safe to break.
|
||||
fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
|
||||
let Range { mut start, mut end } = text_range;
|
||||
if !self.dir.is_positive() {
|
||||
std::mem::swap(&mut start, &mut end);
|
||||
}
|
||||
|
||||
let left = self.find_safe_to_break(start, Side::Left)?;
|
||||
let right = self.find_safe_to_break(end, Side::Right)?;
|
||||
Some(&self.glyphs[left .. right])
|
||||
}
|
||||
|
||||
/// Find the glyph offset matching the text index that is most towards the
|
||||
/// given side and safe-to-break.
|
||||
fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
|
||||
let ltr = self.dir.is_positive();
|
||||
|
||||
// Handle edge cases.
|
||||
let len = self.glyphs.len();
|
||||
if text_index == 0 {
|
||||
return Some(if ltr { 0 } else { len });
|
||||
} else if text_index == self.text.len() {
|
||||
return Some(if ltr { len } else { 0 });
|
||||
}
|
||||
|
||||
// Find any glyph with the text index.
|
||||
let mut idx = self
|
||||
.glyphs
|
||||
.binary_search_by(|g| {
|
||||
let ordering = g.text_index.cmp(&text_index);
|
||||
if ltr { ordering } else { ordering.reverse() }
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let next = match towards {
|
||||
Side::Left => usize::checked_sub,
|
||||
Side::Right => usize::checked_add,
|
||||
};
|
||||
|
||||
// Search for the outermost glyph with the text index.
|
||||
while let Some(next) = next(idx, 1) {
|
||||
if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
|
||||
break;
|
||||
}
|
||||
idx = next;
|
||||
}
|
||||
|
||||
// RTL needs offset one because the left side of the range should be
|
||||
// exclusive and the right side inclusive, contrary to the normal
|
||||
// behaviour of ranges.
|
||||
if !ltr {
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
self.glyphs[idx].safe_to_break.then(|| idx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A visual side.
|
||||
enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ fn transform_impl(args: &mut Args, transform: Transform) -> TypResult<Value> {
|
||||
.named("origin")?
|
||||
.unwrap_or(Spec::splat(None))
|
||||
.unwrap_or(Align::CENTER_HORIZON);
|
||||
|
||||
Ok(Value::inline(
|
||||
body.into_block().transformed(transform, origin),
|
||||
))
|
||||
|
419
src/style/mod.rs
419
src/style/mod.rs
@ -1,419 +0,0 @@
|
||||
//! Style properties.
|
||||
|
||||
mod paper;
|
||||
|
||||
pub use paper::*;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::rc::Rc;
|
||||
|
||||
use ttf_parser::Tag;
|
||||
|
||||
use crate::eval::Smart;
|
||||
use crate::font::*;
|
||||
use crate::geom::*;
|
||||
use crate::util::EcoString;
|
||||
|
||||
/// Defines a set of properties a template can be instantiated with.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Style {
|
||||
/// The page settings.
|
||||
pub page: Rc<PageStyle>,
|
||||
/// The paragraph settings.
|
||||
pub par: Rc<ParStyle>,
|
||||
/// The current text settings.
|
||||
pub text: Rc<TextStyle>,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Access the `page` style mutably.
|
||||
pub fn page_mut(&mut self) -> &mut PageStyle {
|
||||
Rc::make_mut(&mut self.page)
|
||||
}
|
||||
|
||||
/// Access the `par` style mutably.
|
||||
pub fn par_mut(&mut self) -> &mut ParStyle {
|
||||
Rc::make_mut(&mut self.par)
|
||||
}
|
||||
|
||||
/// Access the `text` style mutably.
|
||||
pub fn text_mut(&mut self) -> &mut TextStyle {
|
||||
Rc::make_mut(&mut self.text)
|
||||
}
|
||||
|
||||
/// The resolved line spacing.
|
||||
pub fn leading(&self) -> Length {
|
||||
self.par.leading.resolve(self.text.size)
|
||||
}
|
||||
|
||||
/// The resolved paragraph spacing.
|
||||
pub fn par_spacing(&self) -> Length {
|
||||
self.par.spacing.resolve(self.text.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
page: Rc::new(PageStyle::default()),
|
||||
par: Rc::new(ParStyle::default()),
|
||||
text: Rc::new(TextStyle::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines style properties of pages.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct PageStyle {
|
||||
/// The class of this page.
|
||||
pub class: PaperClass,
|
||||
/// The width and height of the page.
|
||||
pub size: Size,
|
||||
/// The amount of white space on each side of the page. If a side is set to
|
||||
/// `None`, the default for the paper class is used.
|
||||
pub margins: Sides<Smart<Linear>>,
|
||||
/// The background fill of the page.
|
||||
pub fill: Option<Paint>,
|
||||
}
|
||||
|
||||
impl PageStyle {
|
||||
/// The resolved margins.
|
||||
pub fn margins(&self) -> Sides<Linear> {
|
||||
let default = self.class.default_margins();
|
||||
Sides {
|
||||
left: self.margins.left.unwrap_or(default.left),
|
||||
top: self.margins.top.unwrap_or(default.top),
|
||||
right: self.margins.right.unwrap_or(default.right),
|
||||
bottom: self.margins.bottom.unwrap_or(default.bottom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PageStyle {
|
||||
fn default() -> Self {
|
||||
let paper = Paper::A4;
|
||||
Self {
|
||||
class: paper.class(),
|
||||
size: paper.size(),
|
||||
margins: Sides::splat(Smart::Auto),
|
||||
fill: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines style properties of paragraphs.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct ParStyle {
|
||||
/// The direction for text and inline objects.
|
||||
pub dir: Dir,
|
||||
/// How to align text and inline objects in their line.
|
||||
pub align: Align,
|
||||
/// The spacing between lines (dependent on scaled font size).
|
||||
pub leading: Linear,
|
||||
/// The spacing between paragraphs (dependent on scaled font size).
|
||||
pub spacing: Linear,
|
||||
}
|
||||
|
||||
impl Default for ParStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dir: Dir::LTR,
|
||||
align: Align::Left,
|
||||
leading: Relative::new(0.65).into(),
|
||||
spacing: Relative::new(1.2).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines style properties of text.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TextStyle {
|
||||
/// The font size.
|
||||
pub size: Length,
|
||||
/// The selected font variant (the final variant also depends on `strong`
|
||||
/// and `emph`).
|
||||
pub variant: FontVariant,
|
||||
/// The top end of the text bounding box.
|
||||
pub top_edge: VerticalFontMetric,
|
||||
/// The bottom end of the text bounding box.
|
||||
pub bottom_edge: VerticalFontMetric,
|
||||
/// Glyph color.
|
||||
pub fill: Paint,
|
||||
/// A list of font families with generic class definitions (the final
|
||||
/// family list also depends on `monospace`).
|
||||
pub families: Rc<FamilyStyle>,
|
||||
/// OpenType features.
|
||||
pub features: Rc<FontFeatures>,
|
||||
/// The amount of space that should be added between character.
|
||||
pub tracking: Em,
|
||||
/// Whether 300 extra font weight should be added to what is defined by the
|
||||
/// `variant`.
|
||||
pub strong: bool,
|
||||
/// Whether the the font style defined by the `variant` should be inverted.
|
||||
pub emph: bool,
|
||||
/// Whether a monospace font should be preferred.
|
||||
pub monospace: bool,
|
||||
/// Whether font fallback to a base list should occur.
|
||||
pub fallback: bool,
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
/// The resolved variant with `strong` and `emph` factored in.
|
||||
pub fn variant(&self) -> FontVariant {
|
||||
let mut variant = self.variant;
|
||||
|
||||
if self.strong {
|
||||
variant.weight = variant.weight.thicken(300);
|
||||
}
|
||||
|
||||
if self.emph {
|
||||
variant.style = match variant.style {
|
||||
FontStyle::Normal => FontStyle::Italic,
|
||||
FontStyle::Italic => FontStyle::Normal,
|
||||
FontStyle::Oblique => FontStyle::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
variant
|
||||
}
|
||||
|
||||
/// The resolved family iterator.
|
||||
pub fn families(&self) -> impl Iterator<Item = &str> + Clone {
|
||||
let head = if self.monospace {
|
||||
self.families.monospace.as_slice()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
let core = self.families.list.iter().flat_map(move |family| {
|
||||
match family {
|
||||
FontFamily::Named(name) => std::slice::from_ref(name),
|
||||
FontFamily::Serif => &self.families.serif,
|
||||
FontFamily::SansSerif => &self.families.sans_serif,
|
||||
FontFamily::Monospace => &self.families.monospace,
|
||||
}
|
||||
});
|
||||
|
||||
let tail = if self.fallback {
|
||||
self.families.base.as_slice()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
head.iter().chain(core).chain(tail).map(EcoString::as_str)
|
||||
}
|
||||
|
||||
/// Access the `families` style mutably.
|
||||
pub fn families_mut(&mut self) -> &mut FamilyStyle {
|
||||
Rc::make_mut(&mut self.families)
|
||||
}
|
||||
|
||||
/// Access the font `features` mutably.
|
||||
pub fn features_mut(&mut self) -> &mut FontFeatures {
|
||||
Rc::make_mut(&mut self.features)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
size: Length::pt(11.0),
|
||||
variant: FontVariant {
|
||||
style: FontStyle::Normal,
|
||||
weight: FontWeight::REGULAR,
|
||||
stretch: FontStretch::NORMAL,
|
||||
},
|
||||
top_edge: VerticalFontMetric::CapHeight,
|
||||
bottom_edge: VerticalFontMetric::Baseline,
|
||||
fill: RgbaColor::BLACK.into(),
|
||||
families: Rc::new(FamilyStyle::default()),
|
||||
features: Rc::new(FontFeatures::default()),
|
||||
tracking: Em::zero(),
|
||||
strong: false,
|
||||
emph: false,
|
||||
monospace: false,
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Font list with family definitions.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FamilyStyle {
|
||||
/// The user-defined list of font families.
|
||||
pub list: Vec<FontFamily>,
|
||||
/// Definition of serif font families.
|
||||
pub serif: Vec<EcoString>,
|
||||
/// Definition of sans-serif font families.
|
||||
pub sans_serif: Vec<EcoString>,
|
||||
/// Definition of monospace font families used for raw text.
|
||||
pub monospace: Vec<EcoString>,
|
||||
/// Base fonts that are tried as last resort.
|
||||
pub base: Vec<EcoString>,
|
||||
}
|
||||
|
||||
impl Default for FamilyStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
list: vec![FontFamily::SansSerif],
|
||||
serif: vec!["ibm plex serif".into()],
|
||||
sans_serif: vec!["ibm plex sans".into()],
|
||||
monospace: vec!["ibm plex mono".into()],
|
||||
base: vec![
|
||||
"ibm plex sans".into(),
|
||||
"latin modern math".into(),
|
||||
"twitter color emoji".into(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic or named font family.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub enum FontFamily {
|
||||
/// A family that has "serifs", small strokes attached to letters.
|
||||
Serif,
|
||||
/// A family in which glyphs do not have "serifs", small attached strokes.
|
||||
SansSerif,
|
||||
/// A family in which (almost) all glyphs are of equal width.
|
||||
Monospace,
|
||||
/// A specific family with a name.
|
||||
Named(EcoString),
|
||||
}
|
||||
|
||||
impl Debug for FontFamily {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.pad(match self {
|
||||
Self::Serif => "serif",
|
||||
Self::SansSerif => "sans-serif",
|
||||
Self::Monospace => "monospace",
|
||||
Self::Named(s) => s,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether various kinds of ligatures should appear.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontFeatures {
|
||||
/// Whether to apply kerning ("kern").
|
||||
pub kerning: bool,
|
||||
/// Whether the text should use small caps. ("smcp")
|
||||
pub smallcaps: bool,
|
||||
/// Whether to apply stylistic alternates. ("salt")
|
||||
pub alternates: bool,
|
||||
/// Which stylistic set to apply. ("ss01" - "ss20")
|
||||
pub stylistic_set: Option<StylisticSet>,
|
||||
/// Configuration of ligature features.
|
||||
pub ligatures: LigatureFeatures,
|
||||
/// Configuration of numbers features.
|
||||
pub numbers: NumberFeatures,
|
||||
/// Raw OpenType features to apply.
|
||||
pub raw: Vec<(Tag, u32)>,
|
||||
}
|
||||
|
||||
impl Default for FontFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kerning: true,
|
||||
smallcaps: false,
|
||||
alternates: false,
|
||||
stylistic_set: None,
|
||||
ligatures: LigatureFeatures::default(),
|
||||
numbers: NumberFeatures::default(),
|
||||
raw: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stylistic set in a font face.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct StylisticSet(u8);
|
||||
|
||||
impl StylisticSet {
|
||||
/// Creates a new set, clamping to 1-20.
|
||||
pub fn new(index: u8) -> Self {
|
||||
Self(index.clamp(1, 20))
|
||||
}
|
||||
|
||||
/// Get the value, guaranteed to be 1-20.
|
||||
pub fn get(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether various kinds of ligatures should appear.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct LigatureFeatures {
|
||||
/// Standard ligatures. ("liga", "clig")
|
||||
pub standard: bool,
|
||||
/// Ligatures that should be used sparringly. ("dlig")
|
||||
pub discretionary: bool,
|
||||
/// Historical ligatures. ("hlig")
|
||||
pub historical: bool,
|
||||
}
|
||||
|
||||
impl Default for LigatureFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
standard: true,
|
||||
discretionary: false,
|
||||
historical: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the style of numbers.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct NumberFeatures {
|
||||
/// Whether to use lining or old-style numbers.
|
||||
pub type_: Smart<NumberType>,
|
||||
/// Whether to use proportional or tabular numbers.
|
||||
pub width: Smart<NumberWidth>,
|
||||
/// How to position numbers vertically.
|
||||
pub position: NumberPosition,
|
||||
/// Whether to have a slash through the zero glyph. ("zero")
|
||||
pub slashed_zero: bool,
|
||||
/// Whether to convert fractions. ("frac")
|
||||
pub fractions: bool,
|
||||
}
|
||||
|
||||
impl Default for NumberFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
type_: Smart::Auto,
|
||||
width: Smart::Auto,
|
||||
position: NumberPosition::Normal,
|
||||
slashed_zero: false,
|
||||
fractions: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which kind of numbers / figures to select.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberType {
|
||||
/// Numbers that fit well with capital text. ("lnum")
|
||||
Lining,
|
||||
/// Numbers that fit well into flow of upper- and lowercase text. ("onum")
|
||||
OldStyle,
|
||||
}
|
||||
|
||||
/// The width of numbers / figures.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberWidth {
|
||||
/// Number widths are glyph specific. ("pnum")
|
||||
Proportional,
|
||||
/// All numbers are of equal width / monospaced. ("tnum")
|
||||
Tabular,
|
||||
}
|
||||
|
||||
/// How to position numbers.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum NumberPosition {
|
||||
/// Numbers are positioned on the same baseline as text.
|
||||
Normal,
|
||||
/// Numbers are smaller and placed at the bottom. ("subs")
|
||||
Subscript,
|
||||
/// Numbers are smaller and placed at the top. ("sups")
|
||||
Superscript,
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
use crate::geom::{Length, Linear, Relative, Sides, Size};
|
||||
|
||||
/// Specification of a paper.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Paper {
|
||||
/// The broad class this paper belongs to.
|
||||
class: PaperClass,
|
||||
/// The width of the paper in millimeters.
|
||||
width: f64,
|
||||
/// The height of the paper in millimeters.
|
||||
height: f64,
|
||||
}
|
||||
|
||||
/// Defines default margins for a class of related papers.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum PaperClass {
|
||||
Custom,
|
||||
Base,
|
||||
US,
|
||||
Newspaper,
|
||||
Book,
|
||||
}
|
||||
|
||||
impl PaperClass {
|
||||
/// The default margins for this page class.
|
||||
pub fn default_margins(self) -> Sides<Linear> {
|
||||
let f = |r| Relative::new(r).into();
|
||||
let s = |l, t, r, b| Sides::new(f(l), f(t), f(r), f(b));
|
||||
match self {
|
||||
Self::Custom => s(0.1190, 0.0842, 0.1190, 0.0842),
|
||||
Self::Base => s(0.1190, 0.0842, 0.1190, 0.0842),
|
||||
Self::US => s(0.1760, 0.1092, 0.1760, 0.0910),
|
||||
Self::Newspaper => s(0.0455, 0.0587, 0.0455, 0.0294),
|
||||
Self::Book => s(0.1200, 0.0852, 0.1500, 0.0965),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! papers {
|
||||
($(($var:ident: $class:ident, $width:expr, $height: expr, $($pats:tt)*))*) => {
|
||||
impl Paper {
|
||||
/// Parse a paper from its name.
|
||||
///
|
||||
/// Both lower and upper case are fine.
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name.to_lowercase().as_str() {
|
||||
$($($pats)* => Some(Self::$var),)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The class of the paper.
|
||||
pub fn class(self) -> PaperClass {
|
||||
self.class
|
||||
}
|
||||
|
||||
/// The size of the paper.
|
||||
pub fn size(self) -> Size {
|
||||
Size::new(Length::mm(self.width), Length::mm(self.height))
|
||||
}
|
||||
}
|
||||
|
||||
/// Predefined papers.
|
||||
///
|
||||
/// Each paper is parsable from its name in kebab-case.
|
||||
impl Paper {
|
||||
$(papers!(@$var, stringify!($($pats)*), $class, $width, $height);)*
|
||||
}
|
||||
};
|
||||
|
||||
(@$var:ident, $names:expr, $class:ident, $width:expr, $height:expr) => {
|
||||
pub const $var: Self = Self {
|
||||
class: PaperClass::$class,
|
||||
width: $width,
|
||||
height: $height,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// All paper sizes in mm.
|
||||
//
|
||||
// Resources:
|
||||
// - https://papersizes.io/
|
||||
// - https://en.wikipedia.org/wiki/Paper_size
|
||||
// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
|
||||
// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
|
||||
papers! {
|
||||
// ---------------------------------------------------------------------- //
|
||||
// ISO 216 A Series
|
||||
(A0: Base, 841.0, 1189.0, "a0")
|
||||
(A1: Base, 594.0, 841.0, "a1")
|
||||
(A2: Base, 420.0, 594.0, "a2")
|
||||
(A3: Base, 297.0, 420.0, "a3")
|
||||
(A4: Base, 210.0, 297.0, "a4")
|
||||
(A5: Base, 148.0, 210.0, "a5")
|
||||
(A6: Book, 105.0, 148.0, "a6")
|
||||
(A7: Base, 74.0, 105.0, "a7")
|
||||
(A8: Base, 52.0, 74.0, "a8")
|
||||
(A9: Base, 37.0, 52.0, "a9")
|
||||
(A10: Base, 26.0, 37.0, "a10")
|
||||
(A11: Base, 18.0, 26.0, "a11")
|
||||
|
||||
// ISO 216 B Series
|
||||
(ISO_B1: Base, 707.0, 1000.0, "iso-b1")
|
||||
(ISO_B2: Base, 500.0, 707.0, "iso-b2")
|
||||
(ISO_B3: Base, 353.0, 500.0, "iso-b3")
|
||||
(ISO_B4: Base, 250.0, 353.0, "iso-b4")
|
||||
(ISO_B5: Book, 176.0, 250.0, "iso-b5")
|
||||
(ISO_B6: Book, 125.0, 176.0, "iso-b6")
|
||||
(ISO_B7: Base, 88.0, 125.0, "iso-b7")
|
||||
(ISO_B8: Base, 62.0, 88.0, "iso-b8")
|
||||
|
||||
// ISO 216 C Series
|
||||
(ISO_C3: Base, 324.0, 458.0, "iso-c3")
|
||||
(ISO_C4: Base, 229.0, 324.0, "iso-c4")
|
||||
(ISO_C5: Base, 162.0, 229.0, "iso-c5")
|
||||
(ISO_C6: Base, 114.0, 162.0, "iso-c6")
|
||||
(ISO_C7: Base, 81.0, 114.0, "iso-c7")
|
||||
(ISO_C8: Base, 57.0, 81.0, "iso-c8")
|
||||
|
||||
// DIN D Series (extension to ISO)
|
||||
(DIN_D3: Base, 272.0, 385.0, "din-d3")
|
||||
(DIN_D4: Base, 192.0, 272.0, "din-d4")
|
||||
(DIN_D5: Base, 136.0, 192.0, "din-d5")
|
||||
(DIN_D6: Base, 96.0, 136.0, "din-d6")
|
||||
(DIN_D7: Base, 68.0, 96.0, "din-d7")
|
||||
(DIN_D8: Base, 48.0, 68.0, "din-d8")
|
||||
|
||||
// SIS (used in academia)
|
||||
(SIS_G5: Base, 169.0, 239.0, "sis-g5")
|
||||
(SIS_E5: Base, 115.0, 220.0, "sis-e5")
|
||||
|
||||
// ANSI Extensions
|
||||
(ANSI_A: Base, 216.0, 279.0, "ansi-a")
|
||||
(ANSI_B: Base, 279.0, 432.0, "ansi-b")
|
||||
(ANSI_C: Base, 432.0, 559.0, "ansi-c")
|
||||
(ANSI_D: Base, 559.0, 864.0, "ansi-d")
|
||||
(ANSI_E: Base, 864.0, 1118.0, "ansi-e")
|
||||
|
||||
// ANSI Architectural Paper
|
||||
(ARCH_A: Base, 229.0, 305.0, "arch-a")
|
||||
(ARCH_B: Base, 305.0, 457.0, "arch-b")
|
||||
(ARCH_C: Base, 457.0, 610.0, "arch-c")
|
||||
(ARCH_D: Base, 610.0, 914.0, "arch-d")
|
||||
(ARCH_E1: Base, 762.0, 1067.0, "arch-e1")
|
||||
(ARCH_E: Base, 914.0, 1219.0, "arch-e")
|
||||
|
||||
// JIS B Series
|
||||
(JIS_B0: Base, 1030.0, 1456.0, "jis-b0")
|
||||
(JIS_B1: Base, 728.0, 1030.0, "jis-b1")
|
||||
(JIS_B2: Base, 515.0, 728.0, "jis-b2")
|
||||
(JIS_B3: Base, 364.0, 515.0, "jis-b3")
|
||||
(JIS_B4: Base, 257.0, 364.0, "jis-b4")
|
||||
(JIS_B5: Base, 182.0, 257.0, "jis-b5")
|
||||
(JIS_B6: Base, 128.0, 182.0, "jis-b6")
|
||||
(JIS_B7: Base, 91.0, 128.0, "jis-b7")
|
||||
(JIS_B8: Base, 64.0, 91.0, "jis-b8")
|
||||
(JIS_B9: Base, 45.0, 64.0, "jis-b9")
|
||||
(JIS_B10: Base, 32.0, 45.0, "jis-b10")
|
||||
(JIS_B11: Base, 22.0, 32.0, "jis-b11")
|
||||
|
||||
// SAC D Series
|
||||
(SAC_D0: Base, 764.0, 1064.0, "sac-d0")
|
||||
(SAC_D1: Base, 532.0, 760.0, "sac-d1")
|
||||
(SAC_D2: Base, 380.0, 528.0, "sac-d2")
|
||||
(SAC_D3: Base, 264.0, 376.0, "sac-d3")
|
||||
(SAC_D4: Base, 188.0, 260.0, "sac-d4")
|
||||
(SAC_D5: Base, 130.0, 184.0, "sac-d5")
|
||||
(SAC_D6: Base, 92.0, 126.0, "sac-d6")
|
||||
|
||||
// ISO 7810 ID
|
||||
(ISO_ID_1: Base, 85.6, 53.98, "iso-id-1")
|
||||
(ISO_ID_2: Base, 74.0, 105.0, "iso-id-2")
|
||||
(ISO_ID_3: Base, 88.0, 125.0, "iso-id-3")
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// Asia
|
||||
(ASIA_F4: Base, 210.0, 330.0, "asia-f4")
|
||||
|
||||
// Japan
|
||||
(JP_SHIROKU_BAN_4: Base, 264.0, 379.0, "jp-shiroku-ban-4")
|
||||
(JP_SHIROKU_BAN_5: Base, 189.0, 262.0, "jp-shiroku-ban-5")
|
||||
(JP_SHIROKU_BAN_6: Base, 127.0, 188.0, "jp-shiroku-ban-6")
|
||||
(JP_KIKU_4: Base, 227.0, 306.0, "jp-kiku-4")
|
||||
(JP_KIKU_5: Base, 151.0, 227.0, "jp-kiku-5")
|
||||
(JP_BUSINESS_CARD: Base, 91.0, 55.0, "jp-business-card")
|
||||
|
||||
// China
|
||||
(CN_BUSINESS_CARD: Base, 90.0, 54.0, "cn-business-card")
|
||||
|
||||
// Europe
|
||||
(EU_BUSINESS_CARD: Base, 85.0, 55.0, "eu-business-card")
|
||||
|
||||
// French Traditional (AFNOR)
|
||||
(FR_TELLIERE: Base, 340.0, 440.0, "fr-tellière")
|
||||
(FR_COURONNE_ECRITURE: Base, 360.0, 460.0, "fr-couronne-écriture")
|
||||
(FR_COURONNE_EDITION: Base, 370.0, 470.0, "fr-couronne-édition")
|
||||
(FR_RAISIN: Base, 500.0, 650.0, "fr-raisin")
|
||||
(FR_CARRE: Base, 450.0, 560.0, "fr-carré")
|
||||
(FR_JESUS: Base, 560.0, 760.0, "fr-jésus")
|
||||
|
||||
// United Kingdom Imperial
|
||||
(UK_BRIEF: Base, 406.4, 342.9, "uk-brief")
|
||||
(UK_DRAFT: Base, 254.0, 406.4, "uk-draft")
|
||||
(UK_FOOLSCAP: Base, 203.2, 330.2, "uk-foolscap")
|
||||
(UK_QUARTO: Base, 203.2, 254.0, "uk-quarto")
|
||||
(UK_CROWN: Base, 508.0, 381.0, "uk-crown")
|
||||
(UK_BOOK_A: Book, 111.0, 178.0, "uk-book-a")
|
||||
(UK_BOOK_B: Book, 129.0, 198.0, "uk-book-b")
|
||||
|
||||
// Unites States
|
||||
(US_LETTER: US, 215.9, 279.4, "us-letter")
|
||||
(US_LEGAL: US, 215.9, 355.6, "us-legal")
|
||||
(US_TABLOID: US, 279.4, 431.8, "us-tabloid")
|
||||
(US_EXECUTIVE: US, 184.15, 266.7, "us-executive")
|
||||
(US_FOOLSCAP_FOLIO: US, 215.9, 342.9, "us-foolscap-folio")
|
||||
(US_STATEMENT: US, 139.7, 215.9, "us-statement")
|
||||
(US_LEDGER: US, 431.8, 279.4, "us-ledger")
|
||||
(US_OFICIO: US, 215.9, 340.36, "us-oficio")
|
||||
(US_GOV_LETTER: US, 203.2, 266.7, "us-gov-letter")
|
||||
(US_GOV_LEGAL: US, 215.9, 330.2, "us-gov-legal")
|
||||
(US_BUSINESS_CARD: Base, 88.9, 50.8, "us-business-card")
|
||||
(US_DIGEST: Book, 139.7, 215.9, "us-digest")
|
||||
(US_TRADE: Book, 152.4, 228.6, "us-trade")
|
||||
|
||||
// ---------------------------------------------------------------------- //
|
||||
// Other
|
||||
(NEWSPAPER_COMPACT: Newspaper, 280.0, 430.0, "newspaper-compact")
|
||||
(NEWSPAPER_BERLINER: Newspaper, 315.0, 470.0, "newspaper-berliner")
|
||||
(NEWSPAPER_BROADSHEET: Newspaper, 381.0, 578.0, "newspaper-broadsheet")
|
||||
(PRESENTATION_16_9: Base, 297.0, 167.0625, "presentation-16-9")
|
||||
(PRESENTATION_4_3: Base, 280.0, 210.0, "presentation-4-3")
|
||||
}
|
@ -13,18 +13,6 @@ use std::ops::Range;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Additional methods for booleans.
|
||||
pub trait BoolExt {
|
||||
/// Toggle the value of the bool in place.
|
||||
fn flip(&mut self);
|
||||
}
|
||||
|
||||
impl BoolExt for bool {
|
||||
fn flip(&mut self) {
|
||||
*self = !*self;
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional methods for options.
|
||||
pub trait OptionExt<T> {
|
||||
/// Sets `other` as the value if `self` is `None` or if it contains a value
|
||||
|
@ -12,20 +12,17 @@ use usvg::FitTo;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use typst::diag::Error;
|
||||
use typst::eval::{Smart, Value};
|
||||
use typst::eval::{Smart, Styles, Value};
|
||||
use typst::font::Face;
|
||||
use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text};
|
||||
use typst::geom::{
|
||||
self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size, Transform,
|
||||
};
|
||||
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;
|
||||
use typst::library::{DocumentNode, PageNode, TextNode};
|
||||
use typst::loading::FsLoader;
|
||||
use typst::parse::Scanner;
|
||||
use typst::source::SourceFile;
|
||||
use typst::style::Style;
|
||||
use typst::syntax::Span;
|
||||
use typst::Context;
|
||||
|
||||
@ -64,12 +61,17 @@ fn main() {
|
||||
println!("Running {} tests", len);
|
||||
}
|
||||
|
||||
// We want to have "unbounded" pages, so we allow them to be infinitely
|
||||
// large and fit them to match their content.
|
||||
let mut style = Style::default();
|
||||
style.page_mut().size = Size::new(Length::pt(120.0), Length::inf());
|
||||
style.page_mut().margins = Sides::splat(Smart::Custom(Length::pt(10.0).into()));
|
||||
style.text_mut().size = Length::pt(10.0);
|
||||
// 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
|
||||
// that it multiplies to nice round numbers.
|
||||
let mut styles = Styles::new();
|
||||
styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0)));
|
||||
styles.set(PageNode::HEIGHT, Smart::Auto);
|
||||
styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
|
||||
styles.set(TextNode::SIZE, Length::pt(10.0));
|
||||
|
||||
// Hook up an assert function into the global scope.
|
||||
let mut std = typst::library::new();
|
||||
@ -87,7 +89,7 @@ fn main() {
|
||||
|
||||
// Create loader and context.
|
||||
let loader = FsLoader::new().with_path(FONT_DIR).wrap();
|
||||
let mut ctx = Context::builder().std(std).style(style).build(loader);
|
||||
let mut ctx = Context::builder().std(std).styles(styles).build(loader);
|
||||
|
||||
// Run all the tests.
|
||||
let mut ok = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user