summaryrefslogtreecommitdiff
path: root/src/exec
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-02-09 19:46:57 +0100
committerLaurenz <laurmaedje@gmail.com>2021-02-09 19:46:57 +0100
commit06ca740d01b428f12f6bd327257cd05dce737b03 (patch)
tree995bf8ff3a606aedecf296c9e805e11e9cd0ae8e /src/exec
parente35bbfffcb1f84b2fb0679759152ca0a5eabfad4 (diff)
Split evaluation and execution 🔪
Diffstat (limited to 'src/exec')
-rw-r--r--src/exec/context.rs316
-rw-r--r--src/exec/mod.rs161
-rw-r--r--src/exec/state.rs163
3 files changed, 640 insertions, 0 deletions
diff --git a/src/exec/context.rs b/src/exec/context.rs
new file mode 100644
index 00000000..a1998f29
--- /dev/null
+++ b/src/exec/context.rs
@@ -0,0 +1,316 @@
+use std::any::Any;
+use std::rc::Rc;
+
+use fontdock::FontStyle;
+
+use super::*;
+use crate::diag::Diag;
+use crate::diag::{Deco, Feedback, Pass};
+use crate::geom::{ChildAlign, Dir, Gen, LayoutDirs, Length, Linear, Sides, Size};
+use crate::layout::{
+ Expansion, Node, NodePad, NodePages, NodePar, NodeSpacing, NodeStack, NodeText, Tree,
+};
+
+/// The context for execution.
+#[derive(Debug)]
+pub struct ExecContext<'a> {
+ /// The environment from which resources are gathered.
+ pub env: &'a mut Env,
+ /// The active execution state.
+ pub state: State,
+ /// The accumulated feedback.
+ feedback: Feedback,
+ /// The finished page runs.
+ runs: Vec<NodePages>,
+ /// The stack of logical groups (paragraphs and such).
+ ///
+ /// Each entry contains metadata about the group and nodes that are at the
+ /// same level as the group, which will return to `inner` once the group is
+ /// finished.
+ groups: Vec<(Box<dyn Any>, Vec<Node>)>,
+ /// The nodes in the current innermost group
+ /// (whose metadata is in `groups.last()`).
+ inner: Vec<Node>,
+}
+
+impl<'a> ExecContext<'a> {
+ /// Create a new execution context with a base state.
+ pub fn new(env: &'a mut Env, state: State) -> Self {
+ Self {
+ env,
+ state,
+ groups: vec![],
+ inner: vec![],
+ runs: vec![],
+ feedback: Feedback::new(),
+ }
+ }
+
+ /// Finish execution and return the created layout tree.
+ pub fn finish(self) -> Pass<Tree> {
+ assert!(self.groups.is_empty(), "unfinished group");
+ Pass::new(Tree { runs: self.runs }, self.feedback)
+ }
+
+ /// Add a diagnostic to the feedback.
+ pub fn diag(&mut self, diag: Spanned<Diag>) {
+ self.feedback.diags.push(diag);
+ }
+
+ /// Add a decoration to the feedback.
+ pub fn deco(&mut self, deco: Spanned<Deco>) {
+ self.feedback.decos.push(deco);
+ }
+
+ /// Push a layout node to the active group.
+ ///
+ /// Spacing nodes will be handled according to their [`Softness`].
+ pub fn push(&mut self, node: impl Into<Node>) {
+ let node = node.into();
+
+ if let Node::Spacing(this) = node {
+ if this.softness == Softness::Soft && self.inner.is_empty() {
+ return;
+ }
+
+ if let Some(&Node::Spacing(other)) = self.inner.last() {
+ if this.softness > other.softness {
+ self.inner.pop();
+ } else if this.softness == Softness::Soft {
+ return;
+ }
+ }
+ }
+
+ self.inner.push(node);
+ }
+
+ /// Start a page group based on the active page state.
+ ///
+ /// The `softness` is a hint on whether empty pages should be kept in the
+ /// output.
+ ///
+ /// This also starts an inner paragraph.
+ pub fn start_page_group(&mut self, softness: Softness) {
+ self.start_group(PageGroup {
+ size: self.state.page.size,
+ expand: self.state.page.expand,
+ padding: self.state.page.margins(),
+ dirs: self.state.dirs,
+ align: self.state.align,
+ softness,
+ });
+ self.start_par_group();
+ }
+
+ /// End a page group, returning its [`Softness`].
+ ///
+ /// Whether the page is kept when it's empty is decided by `keep_empty`
+ /// based on its softness. If kept, the page is pushed to the finished page
+ /// runs.
+ ///
+ /// This also ends an inner paragraph.
+ pub fn end_page_group<F>(&mut self, keep_empty: F) -> Softness
+ where
+ F: FnOnce(Softness) -> bool,
+ {
+ self.end_par_group();
+ let (group, children) = self.end_group::<PageGroup>();
+ if !children.is_empty() || keep_empty(group.softness) {
+ self.runs.push(NodePages {
+ size: group.size,
+ child: NodePad {
+ padding: group.padding,
+ child: NodeStack {
+ dirs: group.dirs,
+ align: group.align,
+ expand: group.expand,
+ children,
+ }
+ .into(),
+ }
+ .into(),
+ })
+ }
+ group.softness
+ }
+
+ /// Start a content group.
+ ///
+ /// This also starts an inner paragraph.
+ pub fn start_content_group(&mut self) {
+ self.start_group(ContentGroup);
+ self.start_par_group();
+ }
+
+ /// End a content group and return the resulting nodes.
+ ///
+ /// This also ends an inner paragraph.
+ pub fn end_content_group(&mut self) -> Vec<Node> {
+ self.end_par_group();
+ self.end_group::<ContentGroup>().1
+ }
+
+ /// Start a paragraph group based on the active text state.
+ pub fn start_par_group(&mut self) {
+ let em = self.state.font.font_size();
+ self.start_group(ParGroup {
+ dirs: self.state.dirs,
+ align: self.state.align,
+ line_spacing: self.state.par.line_spacing.resolve(em),
+ });
+ }
+
+ /// End a paragraph group and push it to its parent group if it's not empty.
+ pub fn end_par_group(&mut self) {
+ let (group, children) = self.end_group::<ParGroup>();
+ if !children.is_empty() {
+ self.push(NodePar {
+ dirs: group.dirs,
+ align: group.align,
+ // FIXME: This is a hack and should be superseded by something
+ // better.
+ cross_expansion: if self.groups.len() <= 1 {
+ Expansion::Fill
+ } else {
+ Expansion::Fit
+ },
+ line_spacing: group.line_spacing,
+ children,
+ });
+ }
+ }
+
+ /// Start a layouting group.
+ ///
+ /// All further calls to [`push`](Self::push) will collect nodes for this group.
+ /// The given metadata will be returned alongside the collected nodes
+ /// in a matching call to [`end_group`](Self::end_group).
+ fn start_group<T: 'static>(&mut self, meta: T) {
+ self.groups.push((Box::new(meta), std::mem::take(&mut self.inner)));
+ }
+
+ /// End a layouting group started with [`start_group`](Self::start_group).
+ ///
+ /// This returns the stored metadata and the collected nodes.
+ #[track_caller]
+ fn end_group<T: 'static>(&mut self) -> (T, Vec<Node>) {
+ if let Some(&Node::Spacing(spacing)) = self.inner.last() {
+ if spacing.softness == Softness::Soft {
+ self.inner.pop();
+ }
+ }
+
+ let (any, outer) = self.groups.pop().expect("no pushed group");
+ let group = *any.downcast::<T>().expect("bad group type");
+ (group, std::mem::replace(&mut self.inner, outer))
+ }
+
+ /// Set the directions if they would apply to different axes, producing an
+ /// appropriate error otherwise.
+ pub fn set_dirs(&mut self, new: Gen<Option<Spanned<Dir>>>) {
+ let dirs = Gen::new(
+ new.main.map(|s| s.v).unwrap_or(self.state.dirs.main),
+ new.cross.map(|s| s.v).unwrap_or(self.state.dirs.cross),
+ );
+
+ if dirs.main.axis() != dirs.cross.axis() {
+ self.state.dirs = dirs;
+ } else {
+ for dir in new.main.iter().chain(new.cross.iter()) {
+ self.diag(error!(dir.span, "aligned axis"));
+ }
+ }
+ }
+
+ /// Push a normal space.
+ pub fn push_space(&mut self) {
+ let em = self.state.font.font_size();
+ self.push(NodeSpacing {
+ amount: self.state.par.word_spacing.resolve(em),
+ softness: Softness::Soft,
+ });
+ }
+
+ /// Push a text node.
+ pub fn push_text(&mut self, text: impl Into<String>) {
+ let node = self.make_text_node(text.into());
+ self.push(node);
+ }
+
+ /// Construct a text node from the given string based on the active text
+ /// state.
+ pub fn make_text_node(&self, text: String) -> NodeText {
+ let mut variant = self.state.font.variant;
+
+ if self.state.font.strong {
+ variant.weight = variant.weight.thicken(300);
+ }
+
+ if self.state.font.emph {
+ variant.style = match variant.style {
+ FontStyle::Normal => FontStyle::Italic,
+ FontStyle::Italic => FontStyle::Normal,
+ FontStyle::Oblique => FontStyle::Normal,
+ }
+ }
+
+ NodeText {
+ text,
+ align: self.state.align,
+ dir: self.state.dirs.cross,
+ font_size: self.state.font.font_size(),
+ families: Rc::clone(&self.state.font.families),
+ variant,
+ }
+ }
+
+ /// Apply a forced line break.
+ pub fn apply_linebreak(&mut self) {
+ self.end_par_group();
+ self.start_par_group();
+ }
+
+ /// Apply a forced paragraph break.
+ pub fn apply_parbreak(&mut self) {
+ self.end_par_group();
+ let em = self.state.font.font_size();
+ self.push(NodeSpacing {
+ amount: self.state.par.par_spacing.resolve(em),
+ softness: Softness::Soft,
+ });
+ self.start_par_group();
+ }
+}
+
+/// Defines how an item interacts with surrounding items.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum Softness {
+ /// A soft item can be skipped in some circumstances.
+ Soft,
+ /// A hard item is always retained.
+ Hard,
+}
+
+/// A group for a page run.
+#[derive(Debug)]
+struct PageGroup {
+ size: Size,
+ expand: Spec<Expansion>,
+ padding: Sides<Linear>,
+ dirs: LayoutDirs,
+ align: ChildAlign,
+ softness: Softness,
+}
+
+/// A group for generic content.
+#[derive(Debug)]
+struct ContentGroup;
+
+/// A group for a paragraph.
+#[derive(Debug)]
+struct ParGroup {
+ dirs: LayoutDirs,
+ align: ChildAlign,
+ line_spacing: Length,
+}
diff --git a/src/exec/mod.rs b/src/exec/mod.rs
new file mode 100644
index 00000000..25edcce3
--- /dev/null
+++ b/src/exec/mod.rs
@@ -0,0 +1,161 @@
+//! Execution of syntax trees.
+
+mod context;
+mod state;
+
+pub use context::*;
+pub use state::*;
+
+use std::rc::Rc;
+
+use crate::diag::Pass;
+use crate::env::Env;
+use crate::eval::{ExprMap, TemplateAny, TemplateNode, Value, ValueTemplate};
+use crate::geom::Spec;
+use crate::layout::{self, Expansion, NodeSpacing, NodeStack};
+use crate::pretty::pretty;
+use crate::syntax::*;
+
+/// Execute a syntax tree to produce a layout tree.
+///
+/// The `map` shall be an expression map computed for this tree with
+/// [`eval`](crate::eval::eval). Note that `tree` must be the _exact_ same tree
+/// as used for evaluation (no cloned version), because the expression map
+/// depends on the pointers being stable.
+///
+/// The `state` is the base state that may be updated over the course of
+/// execution.
+pub fn exec(
+ env: &mut Env,
+ tree: &Tree,
+ map: &ExprMap,
+ state: State,
+) -> Pass<layout::Tree> {
+ let mut ctx = ExecContext::new(env, state);
+ ctx.start_page_group(Softness::Hard);
+ tree.exec_with(&mut ctx, &map);
+ ctx.end_page_group(|s| s == Softness::Hard);
+ ctx.finish()
+}
+
+/// Execute a node.
+///
+/// This manipulates active styling and document state and produces layout
+/// nodes. Because syntax nodes and layout nodes do not correspond one-to-one,
+/// constructed layout nodes are pushed into the context instead of returned.
+/// The context takes care of reshaping the nodes into the correct tree
+/// structure.
+pub trait Exec {
+ /// Execute the node.
+ fn exec(&self, ctx: &mut ExecContext);
+}
+
+/// Execute a node with an expression map that applies to it.
+pub trait ExecWith {
+ /// Execute the node.
+ fn exec_with(&self, ctx: &mut ExecContext, map: &ExprMap);
+}
+
+impl ExecWith for Tree {
+ fn exec_with(&self, ctx: &mut ExecContext, map: &ExprMap) {
+ for node in self {
+ match node {
+ Node::Text(text) => ctx.push_text(text),
+ Node::Space => ctx.push_space(),
+ Node::Linebreak => ctx.apply_linebreak(),
+ Node::Parbreak => ctx.apply_parbreak(),
+ Node::Strong => ctx.state.font.strong ^= true,
+ Node::Emph => ctx.state.font.emph ^= true,
+ Node::Heading(heading) => heading.exec_with(ctx, map),
+ Node::Raw(raw) => raw.exec(ctx),
+ Node::Expr(expr) => map[&(expr as *const _)].exec(ctx),
+ }
+ }
+ }
+}
+
+impl ExecWith for NodeHeading {
+ fn exec_with(&self, ctx: &mut ExecContext, map: &ExprMap) {
+ let prev = ctx.state.clone();
+ let upscale = 1.5 - 0.1 * self.level as f64;
+ ctx.state.font.scale *= upscale;
+ ctx.state.font.strong = true;
+
+ self.contents.exec_with(ctx, map);
+ ctx.apply_parbreak();
+
+ ctx.state = prev;
+ }
+}
+
+impl Exec for NodeRaw {
+ fn exec(&self, ctx: &mut ExecContext) {
+ let prev = Rc::clone(&ctx.state.font.families);
+ let families = ctx.state.font.families_mut();
+ families.list.insert(0, "monospace".to_string());
+ families.flatten();
+
+ let em = ctx.state.font.font_size();
+ let line_spacing = ctx.state.par.line_spacing.resolve(em);
+
+ let mut children = vec![];
+ for line in &self.lines {
+ children.push(layout::Node::Text(ctx.make_text_node(line.clone())));
+ children.push(layout::Node::Spacing(NodeSpacing {
+ amount: line_spacing,
+ softness: Softness::Hard,
+ }));
+ }
+
+ if self.block {
+ ctx.apply_parbreak();
+ }
+
+ ctx.push(NodeStack {
+ dirs: ctx.state.dirs,
+ align: ctx.state.align,
+ expand: Spec::uniform(Expansion::Fit),
+ children,
+ });
+
+ if self.block {
+ ctx.apply_parbreak();
+ }
+
+ ctx.state.font.families = prev;
+ }
+}
+
+impl Exec for Value {
+ fn exec(&self, ctx: &mut ExecContext) {
+ match self {
+ Value::None => {}
+ Value::Str(s) => ctx.push_text(s),
+ Value::Template(template) => template.exec(ctx),
+ other => ctx.push_text(pretty(other)),
+ }
+ }
+}
+
+impl Exec for ValueTemplate {
+ fn exec(&self, ctx: &mut ExecContext) {
+ for part in self {
+ part.exec(ctx);
+ }
+ }
+}
+
+impl Exec for TemplateNode {
+ fn exec(&self, ctx: &mut ExecContext) {
+ match self {
+ Self::Tree { tree, map } => tree.exec_with(ctx, &map),
+ Self::Any(any) => any.exec(ctx),
+ }
+ }
+}
+
+impl Exec for TemplateAny {
+ fn exec(&self, ctx: &mut ExecContext) {
+ self(ctx);
+ }
+}
diff --git a/src/exec/state.rs b/src/exec/state.rs
new file mode 100644
index 00000000..21fb7fb6
--- /dev/null
+++ b/src/exec/state.rs
@@ -0,0 +1,163 @@
+use std::rc::Rc;
+
+use fontdock::{fallback, FallbackTree, FontStretch, FontStyle, FontVariant, FontWeight};
+
+use crate::geom::{
+ Align, ChildAlign, Dir, LayoutDirs, Length, Linear, Relative, Sides, Size, Spec,
+};
+use crate::layout::Expansion;
+use crate::paper::{Paper, PaperClass, PAPER_A4};
+
+/// The evaluation state.
+#[derive(Debug, Clone, PartialEq)]
+pub struct State {
+ /// The current page settings.
+ pub page: PageSettings,
+ /// The current paragraph settings.
+ pub par: ParSettings,
+ /// The current font settings.
+ pub font: FontSettings,
+ /// The current layouting directions.
+ pub dirs: LayoutDirs,
+ /// The current alignments of an item in its parent.
+ pub align: ChildAlign,
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self {
+ page: PageSettings::default(),
+ par: ParSettings::default(),
+ font: FontSettings::default(),
+ dirs: LayoutDirs::new(Dir::TTB, Dir::LTR),
+ align: ChildAlign::new(Align::Start, Align::Start),
+ }
+ }
+}
+
+/// Defines page properties.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct PageSettings {
+ /// The class of this page.
+ pub class: PaperClass,
+ /// The width and height of the page.
+ pub size: Size,
+ /// Whether the expand the pages to the `size` or to fit the content.
+ pub expand: Spec<Expansion>,
+ /// The amount of white space in the order [left, top, right, bottom]. If a
+ /// side is set to `None`, the default for the paper class is used.
+ pub margins: Sides<Option<Linear>>,
+}
+
+impl PageSettings {
+ /// The default page style for the given paper.
+ pub fn new(paper: Paper) -> Self {
+ Self {
+ class: paper.class,
+ size: paper.size(),
+ expand: Spec::uniform(Expansion::Fill),
+ margins: Sides::uniform(None),
+ }
+ }
+
+ /// The 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 PageSettings {
+ fn default() -> Self {
+ Self::new(PAPER_A4)
+ }
+}
+
+/// Defines paragraph properties.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct ParSettings {
+ /// The spacing between words (dependent on scaled font size).
+ pub word_spacing: Linear,
+ /// The spacing between lines (dependent on scaled font size).
+ pub line_spacing: Linear,
+ /// The spacing between paragraphs (dependent on scaled font size).
+ pub par_spacing: Linear,
+}
+
+impl Default for ParSettings {
+ fn default() -> Self {
+ Self {
+ word_spacing: Relative::new(0.25).into(),
+ line_spacing: Relative::new(0.2).into(),
+ par_spacing: Relative::new(0.5).into(),
+ }
+ }
+}
+
+/// Defines font properties.
+#[derive(Debug, Clone, PartialEq)]
+pub struct FontSettings {
+ /// A tree of font family names and generic class names.
+ pub families: Rc<FallbackTree>,
+ /// The selected font variant.
+ pub variant: FontVariant,
+ /// The font size.
+ pub size: Length,
+ /// The linear to apply on the base font size.
+ pub scale: Linear,
+ /// Whether the strong toggle is active or inactive. This determines
+ /// whether the next `*` adds or removes font weight.
+ pub strong: bool,
+ /// Whether the emphasis toggle is active or inactive. This determines
+ /// whether the next `_` makes italic or non-italic.
+ pub emph: bool,
+}
+
+impl FontSettings {
+ /// Access the `families` mutably.
+ pub fn families_mut(&mut self) -> &mut FallbackTree {
+ Rc::make_mut(&mut self.families)
+ }
+
+ /// The absolute font size.
+ pub fn font_size(&self) -> Length {
+ self.scale.resolve(self.size)
+ }
+}
+
+impl Default for FontSettings {
+ fn default() -> Self {
+ Self {
+ /// The default tree of font fallbacks.
+ families: Rc::new(fallback! {
+ list: ["sans-serif"],
+ classes: {
+ "serif" => ["source serif pro", "noto serif"],
+ "sans-serif" => ["source sans pro", "noto sans"],
+ "monospace" => ["source code pro", "noto sans mono"],
+ },
+ base: [
+ "source sans pro",
+ "noto sans",
+ "segoe ui emoji",
+ "noto emoji",
+ "latin modern math",
+ ],
+ }),
+ variant: FontVariant {
+ style: FontStyle::Normal,
+ weight: FontWeight::REGULAR,
+ stretch: FontStretch::Normal,
+ },
+ size: Length::pt(11.0),
+ scale: Linear::ONE,
+ strong: false,
+ emph: false,
+ }
+ }
+}