From 66d8f4569a9f13270c5f477e0730f127a22333e2 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 May 2022 11:59:53 +0200 Subject: [PATCH 01/10] Locate me! --- src/eval/mod.rs | 2 +- src/export/pdf.rs | 1 + src/export/render.rs | 1 + src/frame.rs | 5 +- src/lib.rs | 13 ++-- src/library/mod.rs | 1 + src/library/text/par.rs | 43 +++++++--- src/library/utility/locate.rs | 8 ++ src/library/utility/mod.rs | 2 + src/memo.rs | 20 ++--- src/model/content.rs | 50 +++++++++++- src/model/layout.rs | 14 +++- src/model/locate.rs | 125 ++++++++++++++++++++++++++++++ src/model/mod.rs | 2 + src/model/recipe.rs | 20 ++--- tests/ref/layout/locate-break.png | Bin 0 -> 122 bytes tests/ref/layout/locate.png | Bin 0 -> 2795 bytes tests/typ/layout/locate-break.typ | 5 ++ tests/typ/layout/locate.typ | 22 ++++++ 19 files changed, 289 insertions(+), 45 deletions(-) create mode 100644 src/library/utility/locate.rs create mode 100644 src/model/locate.rs create mode 100644 tests/ref/layout/locate-break.png create mode 100644 tests/ref/layout/locate.png create mode 100644 tests/typ/layout/locate-break.typ create mode 100644 tests/typ/layout/locate.typ diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e54dfce42..d76257fa9 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -797,7 +797,7 @@ impl Eval for ShowExpr { body, }); - Ok(Recipe { pattern, func, span }) + Ok(Recipe { pattern, func: Spanned::new(func, span) }) } } diff --git a/src/export/pdf.rs b/src/export/pdf.rs index aeb5c47eb..5e8896f75 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -490,6 +490,7 @@ impl<'a> PageExporter<'a> { Element::Shape(ref shape) => self.write_shape(x, y, shape), Element::Image(id, size) => self.write_image(x, y, id, size), Element::Link(ref dest, size) => self.write_link(pos, dest, size), + Element::Pin(_) => {} } } } diff --git a/src/export/render.rs b/src/export/render.rs index aa60e67e6..163707ebc 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -62,6 +62,7 @@ fn render_frame( render_image(canvas, ts, mask, ctx.images.get(id), size); } Element::Link(_, _) => {} + Element::Pin(_) => {} } } } diff --git a/src/frame.rs b/src/frame.rs index 6475f92a2..289de6dad 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -218,6 +218,8 @@ pub enum Element { Image(ImageId, Size), /// A link to an external resource and its trigger region. Link(Destination, Size), + /// A pin identified by index. + Pin(usize), } impl Debug for Element { @@ -227,7 +229,8 @@ impl Debug for Element { Self::Text(text) => write!(f, "{text:?}"), Self::Shape(shape) => write!(f, "{shape:?}"), Self::Image(image, _) => write!(f, "{image:?}"), - Self::Link(target, _) => write!(f, "Link({target:?})"), + Self::Link(dest, _) => write!(f, "Link({dest:?})"), + Self::Pin(idx) => write!(f, "Pin({idx})"), } } } diff --git a/src/lib.rs b/src/lib.rs index 17225b328..34e87c5e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ use crate::font::FontStore; use crate::frame::Frame; use crate::image::ImageStore; use crate::loading::Loader; -use crate::model::StyleMap; +use crate::model::{PinBoard, StyleMap}; use crate::source::{SourceId, SourceStore}; /// Typeset a source file into a collection of layouted frames. @@ -76,27 +76,30 @@ pub fn typeset(ctx: &mut Context, id: SourceId) -> TypResult>> { /// The core context which holds the configuration and stores. pub struct Context { - /// The context's configuration. - pub config: Config, /// Stores loaded source files. pub sources: SourceStore, /// Stores parsed font faces. pub fonts: FontStore, /// Stores decoded images. pub images: ImageStore, + /// The context's configuration. + config: Config, /// Stores evaluated modules. - pub modules: HashMap, + modules: HashMap, + /// Stores document pins. + pins: PinBoard, } impl Context { /// Create a new context. pub fn new(loader: Arc, config: Config) -> Self { Self { - config, sources: SourceStore::new(Arc::clone(&loader)), fonts: FontStore::new(Arc::clone(&loader)), images: ImageStore::new(loader), + config, modules: HashMap::new(), + pins: PinBoard::new(), } } } diff --git a/src/library/mod.rs b/src/library/mod.rs index ac0cbb92b..3321a36b5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -92,6 +92,7 @@ pub fn new() -> Scope { std.def_fn("roman", utility::roman); std.def_fn("symbol", utility::symbol); std.def_fn("lorem", utility::lorem); + std.def_fn("locate", utility::locate); // Predefined colors. std.def_const("black", Color::BLACK); diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 1269ffed5..65098b617 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -26,6 +26,8 @@ pub enum ParChild { Spacing(Spacing), /// An arbitrary inline-level node. Node(LayoutNode), + /// A pin identified by index. + Pin(usize), } #[node] @@ -100,6 +102,7 @@ impl Debug for ParChild { Self::Quote { double } => write!(f, "Quote({double})"), Self::Spacing(kind) => write!(f, "{:?}", kind), Self::Node(node) => node.fmt(f), + Self::Pin(idx) => write!(f, "Pin({idx})"), } } } @@ -275,6 +278,8 @@ enum Segment<'a> { Spacing(Spacing), /// An arbitrary inline-level layout node. Node(&'a LayoutNode), + /// A pin identified by index. + Pin(usize), } impl Segment<'_> { @@ -282,7 +287,7 @@ impl Segment<'_> { fn len(&self) -> usize { match *self { Self::Text(len) => len, - Self::Spacing(_) => SPACING_REPLACE.len_utf8(), + Self::Spacing(_) | Self::Pin(_) => SPACING_REPLACE.len_utf8(), Self::Node(_) => NODE_REPLACE.len_utf8(), } } @@ -301,6 +306,8 @@ enum Item<'a> { Frame(Arc), /// A repeating node. Repeat(&'a RepeatNode, StyleChain<'a>), + /// A pin identified by index. + Pin(usize), } impl<'a> Item<'a> { @@ -316,7 +323,9 @@ impl<'a> Item<'a> { fn len(&self) -> usize { match self { Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), + Self::Absolute(_) | Self::Fractional(_) | Self::Pin(_) => { + SPACING_REPLACE.len_utf8() + } Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), } } @@ -324,10 +333,10 @@ impl<'a> Item<'a> { /// The natural width of the item. fn width(&self) -> Length { match self { - Item::Text(shaped) => shaped.width, - Item::Absolute(v) => *v, - Item::Fractional(_) | Self::Repeat(_, _) => Length::zero(), - Item::Frame(frame) => frame.size.x, + Self::Text(shaped) => shaped.width, + Self::Absolute(v) => *v, + Self::Frame(frame) => frame.size.x, + Self::Fractional(_) | Self::Repeat(_, _) | Self::Pin(_) => Length::zero(), } } } @@ -447,7 +456,7 @@ fn collect<'a>( } Segment::Text(full.len() - prev) } - ParChild::Quote { double } => { + &ParChild::Quote { double } => { let prev = full.len(); if styles.get(TextNode::SMART_QUOTES) { let lang = styles.get(TextNode::LANG); @@ -456,24 +465,28 @@ fn collect<'a>( let peeked = iter.peek().and_then(|(child, _)| match child { ParChild::Text(text) => text.chars().next(), ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) => Some(SPACING_REPLACE), + ParChild::Spacing(_) | ParChild::Pin(_) => Some(SPACING_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE), }); - full.push_str(quoter.quote("es, *double, peeked)); + full.push_str(quoter.quote("es, double, peeked)); } else { - full.push(if *double { '"' } else { '\'' }); + full.push(if double { '"' } else { '\'' }); } Segment::Text(full.len() - prev) } - ParChild::Spacing(spacing) => { + &ParChild::Spacing(spacing) => { full.push(SPACING_REPLACE); - Segment::Spacing(*spacing) + Segment::Spacing(spacing) } ParChild::Node(node) => { full.push(NODE_REPLACE); Segment::Node(node) } + &ParChild::Pin(idx) => { + full.push(SPACING_REPLACE); + Segment::Pin(idx) + } }; if let Some(last) = full.chars().last() { @@ -540,6 +553,7 @@ fn prepare<'a>( items.push(Item::Frame(frame)); } } + Segment::Pin(idx) => items.push(Item::Pin(idx)), } cursor = end; @@ -1171,6 +1185,11 @@ fn commit( } offset = before + width; } + Item::Pin(idx) => { + let mut frame = Frame::new(Size::zero()); + frame.push(Point::zero(), Element::Pin(*idx)); + push(&mut offset, MaybeShared::Owned(frame)); + } } } diff --git a/src/library/utility/locate.rs b/src/library/utility/locate.rs new file mode 100644 index 000000000..0352199ff --- /dev/null +++ b/src/library/utility/locate.rs @@ -0,0 +1,8 @@ +use crate::library::prelude::*; +use crate::model::LocateNode; + +/// Format content with access to its location on the page. +pub fn locate(_: &mut Machine, args: &mut Args) -> TypResult { + let node = LocateNode::new(args.expect("recipe")?); + Ok(Value::Content(Content::Locate(node))) +} diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 10aa7c7a1..328156074 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -1,10 +1,12 @@ //! Computational utility functions. mod color; +mod locate; mod math; mod string; pub use color::*; +pub use locate::*; pub use math::*; pub use string::*; diff --git a/src/memo.rs b/src/memo.rs index 6545ebcc8..2eee071cc 100644 --- a/src/memo.rs +++ b/src/memo.rs @@ -4,7 +4,7 @@ use std::any::Any; use std::cell::RefCell; use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; -use std::hash::Hash; +use std::hash::{Hash, Hasher}; thread_local! { /// The thread-local cache. @@ -60,7 +60,7 @@ where O: 'static, G: Fn(&O) -> R, { - let hash = fxhash::hash64(&input); + let hash = fxhash::hash64(&(f, &input)); let result = with(|cache| { let entry = cache.get_mut(&hash)?; entry.age = 0; @@ -111,13 +111,13 @@ impl Display for Eviction { } // These impls are temporary and incorrect. -macro_rules! skip { - ($ty:ty) => { - impl Hash for $ty { - fn hash(&self, _: &mut H) {} - } - }; + +impl Hash for crate::font::FontStore { + fn hash(&self, _: &mut H) {} } -skip!(crate::font::FontStore); -skip!(crate::Context); +impl Hash for crate::Context { + fn hash(&self, state: &mut H) { + self.pins.hash(state); + } +} diff --git a/src/model/content.rs b/src/model/content.rs index c09979d52..effe84ae8 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -7,8 +7,8 @@ use std::ops::{Add, AddAssign}; use typed_arena::Arena; use super::{ - Barrier, CollapsingBuilder, Interruption, Key, Layout, LayoutNode, Property, Show, - ShowNode, StyleEntry, StyleMap, StyleVecBuilder, Target, + Barrier, CollapsingBuilder, Interruption, Key, Layout, LayoutNode, LocateNode, + Property, Show, ShowNode, StyleEntry, StyleMap, StyleVecBuilder, Target, }; use crate::diag::StrResult; use crate::library::layout::{FlowChild, FlowNode, PageNode, PlaceNode, Spacing}; @@ -20,7 +20,35 @@ use crate::library::text::{ use crate::util::EcoString; /// Layout content into a collection of pages. +/// +/// Relayouts until all pinned locations are converged. pub fn layout(ctx: &mut Context, content: &Content) -> TypResult>> { + let mut pass = 0; + let mut frames; + + loop { + let prev = ctx.pins.clone(); + let result = layout_once(ctx, content); + ctx.pins.reset(); + frames = result?; + pass += 1; + + ctx.pins.locate(&frames); + + let count = ctx.pins.len(); + let resolved = ctx.pins.resolved(&prev); + + // Quit if we're done or if we've had five passes. + if resolved == count || pass >= 5 { + break; + } + } + + Ok(frames) +} + +/// Layout content into a collection of pages once. +fn layout_once(ctx: &mut Context, content: &Content) -> TypResult>> { let copy = ctx.config.styles.clone(); let styles = StyleChain::with_root(©); let scratch = Scratch::default(); @@ -88,6 +116,10 @@ pub enum Content { /// A node that can be realized with styles, optionally with attached /// properties. Show(ShowNode, Option), + /// A node that can be realized with its location on the page. + Locate(LocateNode), + /// A pin identified by index. + Pin(usize), /// Content with attached styles. Styled(Arc<(Self, StyleMap)>), /// A sequence of multiple nodes. @@ -272,6 +304,8 @@ impl Debug for Content { Self::Pagebreak { weak } => write!(f, "Pagebreak({weak})"), Self::Page(page) => page.fmt(f), Self::Show(node, _) => node.fmt(f), + Self::Locate(node) => node.fmt(f), + Self::Pin(idx) => write!(f, "Pin({idx})"), Self::Styled(styled) => { let (sub, map) = styled.as_ref(); map.fmt(f)?; @@ -388,6 +422,7 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { } Content::Show(node, _) => return self.show(node, styles), + Content::Locate(node) => return self.locate(node, styles), Content::Styled(styled) => return self.styled(styled, styles), Content::Sequence(seq) => return self.sequence(seq, styles), @@ -436,6 +471,12 @@ impl<'a, 'ctx> Builder<'a, 'ctx> { Ok(()) } + fn locate(&mut self, node: &LocateNode, styles: StyleChain<'a>) -> TypResult<()> { + let realized = node.realize(self.ctx)?; + let stored = self.scratch.templates.alloc(realized); + self.accept(stored, styles) + } + fn styled( &mut self, (content, map): &'a (Content, StyleMap), @@ -641,6 +682,9 @@ impl<'a> ParBuilder<'a> { Content::Inline(node) => { self.0.supportive(ParChild::Node(node.clone()), styles); } + &Content::Pin(idx) => { + self.0.ignorant(ParChild::Pin(idx), styles); + } _ => return false, } @@ -660,7 +704,7 @@ impl<'a> ParBuilder<'a> { && children .items() .find_map(|child| match child { - ParChild::Spacing(_) => None, + ParChild::Spacing(_) | ParChild::Pin(_) => None, ParChild::Text(_) | ParChild::Quote { .. } => Some(true), ParChild::Node(_) => Some(false), }) diff --git a/src/model/layout.rs b/src/model/layout.rs index 6dfbcb90e..49720be4d 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -221,13 +221,19 @@ impl Layout for LayoutNode { regions: &Regions, styles: StyleChain, ) -> TypResult>> { - crate::memo::memoized( - (self, ctx, regions, styles), + let (result, cursor) = crate::memo::memoized( + (self, &mut *ctx, regions, styles), |(node, ctx, regions, styles)| { let entry = StyleEntry::Barrier(Barrier::new(node.id())); - node.0.layout(ctx, regions, entry.chain(&styles)) + let result = node.0.layout(ctx, regions, entry.chain(&styles)); + (result, ctx.pins.cursor()) }, - ) + ); + + // Replay the side effect in case of caching. This should currently be + // more or less the only relevant side effect on the context. + ctx.pins.jump(cursor); + result } fn pack(self) -> LayoutNode { diff --git a/src/model/locate.rs b/src/model/locate.rs new file mode 100644 index 000000000..9b0d13e73 --- /dev/null +++ b/src/model/locate.rs @@ -0,0 +1,125 @@ +use std::sync::Arc; + +use super::Content; +use crate::diag::TypResult; +use crate::eval::{Args, Func, Value}; +use crate::frame::{Element, Frame}; +use crate::geom::{Point, Transform}; +use crate::syntax::Spanned; +use crate::Context; + +/// A node that can realize itself with its own location. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct LocateNode(Spanned); + +impl LocateNode { + /// Create a new locate node. + pub fn new(recipe: Spanned) -> Self { + Self(recipe) + } + + /// Realize the node. + pub fn realize(&self, ctx: &mut Context) -> TypResult { + let idx = ctx.pins.cursor(); + let location = ctx.pins.next(); + let dict = dict! { + "page" => Value::Int(location.page as i64), + "x" => Value::Length(location.pos.x.into()), + "y" => Value::Length(location.pos.y.into()), + }; + + let args = Args::new(self.0.span, [Value::Dict(dict)]); + Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display()) + } +} + +/// Manages ordered pins. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct PinBoard { + /// All currently pinned locations. + pins: Vec, + /// The index of the next pin in order. + cursor: usize, +} + +impl PinBoard { + /// Create an empty pin board. + pub fn new() -> Self { + Self { pins: vec![], cursor: 0 } + } + + /// The number of pins on the board. + pub fn len(&self) -> usize { + self.pins.len() + } + + /// How many pins are resolved in comparison to an earlier snapshot. + pub fn resolved(&self, prev: &Self) -> usize { + self.pins.iter().zip(&prev.pins).filter(|(a, b)| a == b).count() + } + + /// Access the next pin location. + pub fn next(&mut self) -> Location { + let cursor = self.cursor; + self.jump(self.cursor + 1); + self.pins[cursor] + } + + /// The current cursor. + pub fn cursor(&self) -> usize { + self.cursor + } + + /// Set the current cursor. + pub fn jump(&mut self, cursor: usize) { + if cursor >= self.pins.len() { + let loc = self.pins.last().copied().unwrap_or_default(); + self.pins.resize(cursor + 1, loc); + } + self.cursor = cursor; + } + + /// Reset the cursor and remove all unused pins. + pub fn reset(&mut self) { + self.pins.truncate(self.cursor); + self.cursor = 0; + } + + /// Locate all pins in the frames. + pub fn locate(&mut self, frames: &[Arc]) { + for (i, frame) in frames.iter().enumerate() { + self.locate_impl(1 + i, frame, Transform::identity()); + } + } + + /// Locate all pins in a frame. + fn locate_impl(&mut self, page: usize, frame: &Frame, ts: Transform) { + for &(pos, ref element) in &frame.elements { + match element { + Element::Group(group) => { + let ts = ts + .pre_concat(Transform::translate(pos.x, pos.y)) + .pre_concat(group.transform); + self.locate_impl(page, &group.frame, ts); + } + + Element::Pin(idx) => { + let pin = &mut self.pins[*idx]; + pin.page = page; + pin.pos = pos.transform(ts); + } + + _ => {} + } + } + } +} + +/// A physical location in a document. +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] +pub struct Location { + /// The page, starting at 1. + pub page: usize, + /// The exact coordinates on the page (from the top left, as usual). + pub pos: Point, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 5c8b82c0b..379b633f6 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -5,6 +5,7 @@ mod styles; mod collapse; mod content; mod layout; +mod locate; mod property; mod recipe; mod show; @@ -12,6 +13,7 @@ mod show; pub use collapse::*; pub use content::*; pub use layout::*; +pub use locate::*; pub use property::*; pub use recipe::*; pub use show::*; diff --git a/src/model/recipe.rs b/src/model/recipe.rs index e4417adfe..6261e7045 100644 --- a/src/model/recipe.rs +++ b/src/model/recipe.rs @@ -4,7 +4,7 @@ use super::{Content, Interruption, NodeId, Show, ShowNode, StyleChain, StyleEntr use crate::diag::TypResult; use crate::eval::{Args, Func, Regex, Value}; use crate::library::structure::{EnumNode, ListNode}; -use crate::syntax::Span; +use crate::syntax::Spanned; use crate::Context; /// A show rule recipe. @@ -13,9 +13,7 @@ pub struct Recipe { /// The patterns to customize. pub pattern: Pattern, /// The function that defines the recipe. - pub func: Func, - /// The span to report all erros with. - pub span: Span, + pub func: Spanned, } impl Recipe { @@ -81,13 +79,13 @@ impl Recipe { where F: FnOnce() -> Value, { - let args = if self.func.argc() == Some(0) { - Args::new(self.span, []) + let args = if self.func.v.argc() == Some(0) { + Args::new(self.func.span, []) } else { - Args::new(self.span, [arg()]) + Args::new(self.func.span, [arg()]) }; - Ok(self.func.call_detached(ctx, args)?.display()) + Ok(self.func.v.call_detached(ctx, args)?.display()) } /// What kind of structure the property interrupts. @@ -104,7 +102,11 @@ impl Recipe { impl Debug for Recipe { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Recipe matching {:?} from {:?}", self.pattern, self.span) + write!( + f, + "Recipe matching {:?} from {:?}", + self.pattern, self.func.span + ) } } diff --git a/tests/ref/layout/locate-break.png b/tests/ref/layout/locate-break.png new file mode 100644 index 0000000000000000000000000000000000000000..25abb4a094b634121f41868d8f66cfe6946a8b52 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZe=!0HhWt4!RY1zo)5S5Q;?~>C8+jQFI1U)t zJubD&wogcMI5cy!mDA^^ueI8fk1BqXebZgb{yXXZ!F@N%H|Qg{|K8f2VPEH;yicbU OWT>aBpUXO@geCw$buLH% literal 0 HcmV?d00001 diff --git a/tests/ref/layout/locate.png b/tests/ref/layout/locate.png new file mode 100644 index 0000000000000000000000000000000000000000..015c5a6dde1fb21ee0351dd131dc87bc32862f1b GIT binary patch literal 2795 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 R`q@hW(7kgPR-tJh@ehF~E!_YB literal 0 HcmV?d00001 diff --git a/tests/typ/layout/locate-break.typ b/tests/typ/layout/locate-break.typ new file mode 100644 index 000000000..28631cfa7 --- /dev/null +++ b/tests/typ/layout/locate-break.typ @@ -0,0 +1,5 @@ +// Test locate with crazy pagebreaks. + +--- +#set page(height: 10pt) +{3 * locate(me => me.page * pagebreak())} diff --git a/tests/typ/layout/locate.typ b/tests/typ/layout/locate.typ new file mode 100644 index 000000000..ec2262c50 --- /dev/null +++ b/tests/typ/layout/locate.typ @@ -0,0 +1,22 @@ +// Test locate me. + +--- +#set page(height: 60pt) +#let pin = locate(me => box({ + let c(length) = str(int(length / 1pt ) ) + square(size: 1.5pt, fill: blue) + h(0.15em) + text(0.5em)[{me.page}, #c(me.x), #c(me.y)] +})) + +#place(rotate(origin: top + left, 25deg, move(dx: 40pt, pin))) + +#pin +#h(10pt) +#box(pin) \ +#pin + +#place(bottom + right, pin) + +#pagebreak() +#align(center + horizon, pin + [\ ] + pin) From a9869c212f7c1bc77a52e301ad014641b014e834 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 May 2022 13:49:44 +0200 Subject: [PATCH 02/10] Locatable groups --- src/eval/args.rs | 9 + src/eval/methods.rs | 26 +- src/eval/ops.rs | 10 +- src/eval/value.rs | 14 +- src/library/layout/grid.rs | 4 + src/library/{utility => layout}/locate.rs | 8 +- src/library/layout/mod.rs | 2 + src/library/mod.rs | 3 +- src/library/prelude.rs | 4 +- src/library/text/par.rs | 22 +- src/library/utility/mod.rs | 2 - src/model/content.rs | 2 + src/model/locate.rs | 280 +++++++++++++++------- 13 files changed, 273 insertions(+), 113 deletions(-) rename src/library/{utility => layout}/locate.rs (53%) diff --git a/src/eval/args.rs b/src/eval/args.rs index 69e6aaee5..4d280ff7f 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -39,6 +39,15 @@ impl Args { Self { span, items } } + /// Push a positional argument. + pub fn push(&mut self, span: Span, value: Value) { + self.items.push(Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }) + } + /// Consume and cast the first positional argument if there is one. pub fn eat(&mut self) -> TypResult> where diff --git a/src/eval/methods.rs b/src/eval/methods.rs index f6de614f2..6ccd98e6f 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -2,6 +2,7 @@ use super::{Args, Machine, Regex, StrExt, Value}; use crate::diag::{At, TypResult}; +use crate::model::{Content, Group}; use crate::syntax::Span; use crate::util::EcoString; @@ -66,18 +67,23 @@ pub fn call( _ => missing()?, }, - Value::Dyn(dynamic) => { - if let Some(regex) = dynamic.downcast::() { - match method { - "matches" => { - Value::Bool(regex.matches(&args.expect::("text")?)) - } - _ => missing()?, + Value::Dyn(dynamic) => match method { + "matches" => { + if let Some(regex) = dynamic.downcast::() { + Value::Bool(regex.matches(&args.expect::("text")?)) + } else { + missing()? } - } else { - missing()? } - } + "entry" => { + if let Some(group) = dynamic.downcast::() { + Value::Content(Content::Locate(group.entry(args.expect("recipe")?))) + } else { + missing()? + } + } + _ => missing()?, + }, _ => missing()?, }; diff --git a/src/eval/ops.rs b/src/eval/ops.rs index b3f2f3b47..f88f3ceec 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; -use super::{Dynamic, RawAlign, RawLength, RawStroke, Smart, StrExt, Value}; +use super::{RawAlign, RawLength, RawStroke, Smart, StrExt, Value}; use crate::diag::StrResult; use crate::geom::{Numeric, Relative, Spec, SpecAxis}; use crate::model; @@ -94,10 +94,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (Dict(a), Dict(b)) => Dict(a + b), (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { - Dyn(Dynamic::new(RawStroke { + Value::dynamic(RawStroke { paint: Smart::Custom(color.into()), thickness: Smart::Custom(thickness), - })) + }) } (Dyn(a), Dyn(b)) => { @@ -106,10 +106,10 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { (a.downcast::(), b.downcast::()) { if a.axis() != b.axis() { - Dyn(Dynamic::new(match a.axis() { + Value::dynamic(match a.axis() { SpecAxis::Horizontal => Spec { x: a, y: b }, SpecAxis::Vertical => Spec { x: b, y: a }, - })) + }) } else { return Err(format!("cannot add two {:?} alignments", a.axis())); } diff --git a/src/eval/value.rs b/src/eval/value.rs index b47d1e910..9b36812ac 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -11,7 +11,7 @@ use crate::geom::{ Angle, Color, Dir, Em, Fraction, Length, Paint, Ratio, Relative, RgbaColor, Sides, }; use crate::library::text::RawNode; -use crate::model::{Content, Layout, LayoutNode, Pattern}; +use crate::model::{Content, Group, Layout, LayoutNode, Pattern}; use crate::syntax::Spanned; use crate::util::EcoString; @@ -73,6 +73,14 @@ impl Value { Self::Content(Content::block(node)) } + /// Create a new dynamic value. + pub fn dynamic(any: T) -> Self + where + T: Type + Debug + PartialEq + Hash + Sync + Send + 'static, + { + Self::Dyn(Dynamic::new(any)) + } + /// The name of the stored value's type. pub fn type_name(&self) -> &'static str { match self { @@ -653,6 +661,10 @@ dynamic! { Regex: "regular expression", } +dynamic! { + Group: "group", +} + castable! { usize, Expected: "non-negative integer", diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs index 5b6217327..4cad9de68 100644 --- a/src/library/layout/grid.rs +++ b/src/library/layout/grid.rs @@ -204,7 +204,9 @@ impl<'a> GridLayouter<'a> { /// Determines the columns sizes and then layouts the grid row-by-row. pub fn layout(mut self) -> TypResult>> { + self.ctx.pins.freeze(); self.measure_columns()?; + self.ctx.pins.unfreeze(); for y in 0 .. self.rows.len() { // Skip to next region if current one is full, but only for content @@ -370,10 +372,12 @@ impl<'a> GridLayouter<'a> { pod.base.x = self.regions.base.x; } + self.ctx.pins.freeze(); let mut sizes = node .layout(self.ctx, &pod, self.styles)? .into_iter() .map(|frame| frame.size.y); + self.ctx.pins.unfreeze(); // For each region, we want to know the maximum height any // column requires. diff --git a/src/library/utility/locate.rs b/src/library/layout/locate.rs similarity index 53% rename from src/library/utility/locate.rs rename to src/library/layout/locate.rs index 0352199ff..e94a48bac 100644 --- a/src/library/utility/locate.rs +++ b/src/library/layout/locate.rs @@ -1,8 +1,14 @@ use crate::library::prelude::*; -use crate::model::LocateNode; +use crate::model::{Group, LocateNode}; /// Format content with access to its location on the page. pub fn locate(_: &mut Machine, args: &mut Args) -> TypResult { let node = LocateNode::new(args.expect("recipe")?); Ok(Value::Content(Content::Locate(node))) } + +/// Create a new group of locatable elements. +pub fn group(_: &mut Machine, args: &mut Args) -> TypResult { + let key = args.expect("key")?; + Ok(Value::dynamic(Group::new(key))) +} diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs index 588b15aa1..6cf5b550c 100644 --- a/src/library/layout/mod.rs +++ b/src/library/layout/mod.rs @@ -5,6 +5,7 @@ mod columns; mod container; mod flow; mod grid; +mod locate; mod pad; mod page; mod place; @@ -16,6 +17,7 @@ pub use columns::*; pub use container::*; pub use flow::*; pub use grid::*; +pub use locate::*; pub use pad::*; pub use page::*; pub use place::*; diff --git a/src/library/mod.rs b/src/library/mod.rs index 3321a36b5..cf5d6e64f 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -54,6 +54,8 @@ pub fn new() -> Scope { std.def_node::("columns"); std.def_node::("colbreak"); std.def_node::("place"); + std.def_fn("locate", layout::locate); + std.def_fn("group", layout::group); // Graphics. std.def_node::("image"); @@ -92,7 +94,6 @@ pub fn new() -> Scope { std.def_fn("roman", utility::roman); std.def_fn("symbol", utility::symbol); std.def_fn("lorem", utility::lorem); - std.def_fn("locate", utility::locate); // Predefined colors. std.def_const("black", Color::BLACK); diff --git a/src/library/prelude.rs b/src/library/prelude.rs index 371d67761..a61157a78 100644 --- a/src/library/prelude.rs +++ b/src/library/prelude.rs @@ -9,8 +9,8 @@ pub use typst_macros::node; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::eval::{ - Arg, Args, Array, Cast, Dict, Func, Machine, Node, RawAlign, RawLength, RawStroke, - Scope, Smart, Value, + Arg, Args, Array, Cast, Dict, Dynamic, Func, Machine, Node, RawAlign, RawLength, + RawStroke, Scope, Smart, Value, }; pub use crate::frame::*; pub use crate::geom::*; diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 65098b617..709dc756c 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -194,10 +194,11 @@ impl LinebreakNode { /// Range of a substring of text. type Range = std::ops::Range; -// The characters by which spacing and nodes are replaced in the paragraph's -// full text. -const SPACING_REPLACE: char = ' '; -const NODE_REPLACE: char = '\u{FFFC}'; +// The characters by which spacing, nodes and pins are replaced in the +// paragraph's full text. +const SPACING_REPLACE: char = ' '; // Space +const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character +const PIN_REPLACE: char = '\u{200D}'; // Zero Width Joiner /// A paragraph representation in which children are already layouted and text /// is already preshaped. @@ -287,8 +288,9 @@ impl Segment<'_> { fn len(&self) -> usize { match *self { Self::Text(len) => len, - Self::Spacing(_) | Self::Pin(_) => SPACING_REPLACE.len_utf8(), + Self::Spacing(_) => SPACING_REPLACE.len_utf8(), Self::Node(_) => NODE_REPLACE.len_utf8(), + Self::Pin(_) => PIN_REPLACE.len_utf8(), } } } @@ -323,10 +325,9 @@ impl<'a> Item<'a> { fn len(&self) -> usize { match self { Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_) | Self::Pin(_) => { - SPACING_REPLACE.len_utf8() - } + Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(), + Self::Pin(_) => PIN_REPLACE.len_utf8(), } } @@ -465,8 +466,9 @@ fn collect<'a>( let peeked = iter.peek().and_then(|(child, _)| match child { ParChild::Text(text) => text.chars().next(), ParChild::Quote { .. } => Some('"'), - ParChild::Spacing(_) | ParChild::Pin(_) => Some(SPACING_REPLACE), + ParChild::Spacing(_) => Some(SPACING_REPLACE), ParChild::Node(_) => Some(NODE_REPLACE), + ParChild::Pin(_) => Some(PIN_REPLACE), }); full.push_str(quoter.quote("es, double, peeked)); @@ -484,7 +486,7 @@ fn collect<'a>( Segment::Node(node) } &ParChild::Pin(idx) => { - full.push(SPACING_REPLACE); + full.push(PIN_REPLACE); Segment::Pin(idx) } }; diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs index 328156074..10aa7c7a1 100644 --- a/src/library/utility/mod.rs +++ b/src/library/utility/mod.rs @@ -1,12 +1,10 @@ //! Computational utility functions. mod color; -mod locate; mod math; mod string; pub use color::*; -pub use locate::*; pub use math::*; pub use string::*; diff --git a/src/model/content.rs b/src/model/content.rs index effe84ae8..dad212c8a 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -44,6 +44,8 @@ pub fn layout(ctx: &mut Context, content: &Content) -> TypResult> } } + // println!("Took {pass} passes"); + Ok(frames) } diff --git a/src/model/locate.rs b/src/model/locate.rs index 9b0d13e73..fd48e5ad4 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -1,117 +1,122 @@ +use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use super::Content; use crate::diag::TypResult; -use crate::eval::{Args, Func, Value}; +use crate::eval::{Args, Dict, Func, Value}; use crate::frame::{Element, Frame}; use crate::geom::{Point, Transform}; use crate::syntax::Spanned; +use crate::util::EcoString; use crate::Context; +/// A group of locatable elements. +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct Group(EcoString); + +impl Group { + /// Create a group of elements that is identified by a string key. + pub fn new(key: EcoString) -> Self { + Self(key) + } + + /// Add an entry to the group. + pub fn entry(&self, recipe: Spanned) -> LocateNode { + LocateNode { recipe, group: Some(self.clone()) } + } +} + +impl Debug for Group { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "group({:?})", self.0) + } +} + /// A node that can realize itself with its own location. #[derive(Debug, Clone, PartialEq, Hash)] -pub struct LocateNode(Spanned); +pub struct LocateNode { + recipe: Spanned, + group: Option, +} impl LocateNode { /// Create a new locate node. pub fn new(recipe: Spanned) -> Self { - Self(recipe) + Self { recipe, group: None } } /// Realize the node. pub fn realize(&self, ctx: &mut Context) -> TypResult { let idx = ctx.pins.cursor(); - let location = ctx.pins.next(); - let dict = dict! { - "page" => Value::Int(location.page as i64), - "x" => Value::Length(location.pos.x.into()), - "y" => Value::Length(location.pos.y.into()), - }; + let pin = ctx.pins.next(self.group.clone()); - let args = Args::new(self.0.span, [Value::Dict(dict)]); - Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display()) + // Determine the index among the peers. + let index = self.group.as_ref().map(|_| { + ctx.pins + .iter() + .filter(|other| { + other.group == self.group && other.loc.flow < pin.loc.flow + }) + .count() + }); + + let dict = pin.encode(index); + let mut args = Args::new(self.recipe.span, [Value::Dict(dict)]); + + // Collect all members if requested. + if self.group.is_some() && self.recipe.v.argc() == Some(2) { + let mut all: Vec<_> = + ctx.pins.iter().filter(|other| other.group == self.group).collect(); + + all.sort_by_key(|pin| pin.loc.flow); + + let array = all + .iter() + .enumerate() + .map(|(index, member)| Value::Dict(member.encode(Some(index)))) + .collect(); + + args.push(self.recipe.span, Value::Array(array)) + } + + Ok(Content::Pin(idx) + self.recipe.v.call_detached(ctx, args)?.display()) } } -/// Manages ordered pins. -#[derive(Debug, Clone, PartialEq, Hash)] +/// Manages pins. +#[derive(Debug, Clone, Hash)] pub struct PinBoard { - /// All currently pinned locations. - pins: Vec, + /// All currently active pins. + pins: Vec, /// The index of the next pin in order. cursor: usize, + /// If larger than zero, the board is frozen. + frozen: usize, } -impl PinBoard { - /// Create an empty pin board. - pub fn new() -> Self { - Self { pins: vec![], cursor: 0 } - } +/// A document pin. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Pin { + /// The physical location of the pin in the document. + loc: Location, + /// The group the pin belongs to, if any. + group: Option, +} - /// The number of pins on the board. - pub fn len(&self) -> usize { - self.pins.len() - } +impl Pin { + /// Encode into a user-facing dictionary. + fn encode(&self, index: Option) -> Dict { + let mut dict = dict! { + "page" => Value::Int(self.loc.page as i64), + "x" => Value::Length(self.loc.pos.x.into()), + "y" => Value::Length(self.loc.pos.y.into()), + }; - /// How many pins are resolved in comparison to an earlier snapshot. - pub fn resolved(&self, prev: &Self) -> usize { - self.pins.iter().zip(&prev.pins).filter(|(a, b)| a == b).count() - } - - /// Access the next pin location. - pub fn next(&mut self) -> Location { - let cursor = self.cursor; - self.jump(self.cursor + 1); - self.pins[cursor] - } - - /// The current cursor. - pub fn cursor(&self) -> usize { - self.cursor - } - - /// Set the current cursor. - pub fn jump(&mut self, cursor: usize) { - if cursor >= self.pins.len() { - let loc = self.pins.last().copied().unwrap_or_default(); - self.pins.resize(cursor + 1, loc); + if let Some(index) = index { + dict.insert("index".into(), Value::Int(index as i64)); } - self.cursor = cursor; - } - /// Reset the cursor and remove all unused pins. - pub fn reset(&mut self) { - self.pins.truncate(self.cursor); - self.cursor = 0; - } - - /// Locate all pins in the frames. - pub fn locate(&mut self, frames: &[Arc]) { - for (i, frame) in frames.iter().enumerate() { - self.locate_impl(1 + i, frame, Transform::identity()); - } - } - - /// Locate all pins in a frame. - fn locate_impl(&mut self, page: usize, frame: &Frame, ts: Transform) { - for &(pos, ref element) in &frame.elements { - match element { - Element::Group(group) => { - let ts = ts - .pre_concat(Transform::translate(pos.x, pos.y)) - .pre_concat(group.transform); - self.locate_impl(page, &group.frame, ts); - } - - Element::Pin(idx) => { - let pin = &mut self.pins[*idx]; - pin.page = page; - pin.pos = pos.transform(ts); - } - - _ => {} - } - } + dict } } @@ -122,4 +127,117 @@ pub struct Location { pub page: usize, /// The exact coordinates on the page (from the top left, as usual). pub pos: Point, + /// The flow index. + pub flow: usize, +} + +impl PinBoard { + /// Create an empty pin board. + pub fn new() -> Self { + Self { pins: vec![], cursor: 0, frozen: 0 } + } + + /// The number of pins on the board. + pub fn len(&self) -> usize { + self.pins.len() + } + + /// Iterate over all pins on the board. + pub fn iter(&self) -> std::slice::Iter { + self.pins.iter() + } + + /// Freeze the board to prevent modifications. + pub fn freeze(&mut self) { + self.frozen += 1; + } + + /// Freeze the board to prevent modifications. + pub fn unfreeze(&mut self) { + self.frozen -= 1; + } + + /// Access the next pin. + pub fn next(&mut self, group: Option) -> Pin { + if self.frozen > 0 { + return Pin::default(); + } + + let cursor = self.cursor; + self.jump(self.cursor + 1); + self.pins[cursor].group = group; + self.pins[cursor].clone() + } + + /// The current cursor. + pub fn cursor(&self) -> usize { + self.cursor + } + + /// Set the current cursor. + pub fn jump(&mut self, cursor: usize) { + if self.frozen > 0 { + return; + } + + self.cursor = cursor; + if cursor >= self.pins.len() { + self.pins.resize(cursor, Pin::default()); + } + } + + /// Reset the cursor and remove all unused pins. + pub fn reset(&mut self) { + self.pins.truncate(self.cursor); + self.cursor = 0; + } + + /// Locate all pins in the frames. + pub fn locate(&mut self, frames: &[Arc]) { + let mut flow = 0; + for (i, frame) in frames.iter().enumerate() { + locate_impl( + &mut self.pins, + &mut flow, + 1 + i, + frame, + Transform::identity(), + ); + } + } + + /// How many pins are resolved in comparison to an earlier snapshot. + pub fn resolved(&self, prev: &Self) -> usize { + self.pins.iter().zip(&prev.pins).filter(|(a, b)| a == b).count() + } +} + +/// Locate all pins in a frame. +fn locate_impl( + pins: &mut [Pin], + flow: &mut usize, + page: usize, + frame: &Frame, + ts: Transform, +) { + for &(pos, ref element) in &frame.elements { + match element { + Element::Group(group) => { + let ts = ts + .pre_concat(Transform::translate(pos.x, pos.y)) + .pre_concat(group.transform); + locate_impl(pins, flow, page, &group.frame, ts); + } + + Element::Pin(idx) => { + let loc = &mut pins[*idx].loc; + loc.page = page; + loc.pos = pos.transform(ts); + loc.flow = *flow; + *flow += 1; + } + + _ => {} + } + } } From 3e052e6e017eaf51b101fa87629e392c16c32ac2 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 May 2022 14:29:10 +0200 Subject: [PATCH 03/10] All methods for groups --- src/eval/methods.rs | 11 +- src/library/layout/locate.rs | 2 +- src/model/locate.rs | 234 ++++++++++++++++++++++++----------- 3 files changed, 171 insertions(+), 76 deletions(-) diff --git a/src/eval/methods.rs b/src/eval/methods.rs index 6ccd98e6f..e8296d236 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -77,7 +77,16 @@ pub fn call( } "entry" => { if let Some(group) = dynamic.downcast::() { - Value::Content(Content::Locate(group.entry(args.expect("recipe")?))) + Value::Content(Content::Locate( + group.entry(args.expect("recipe")?, args.named("value")?), + )) + } else { + missing()? + } + } + "all" => { + if let Some(group) = dynamic.downcast::() { + Value::Content(Content::Locate(group.all(args.expect("recipe")?))) } else { missing()? } diff --git a/src/library/layout/locate.rs b/src/library/layout/locate.rs index e94a48bac..74480b915 100644 --- a/src/library/layout/locate.rs +++ b/src/library/layout/locate.rs @@ -3,7 +3,7 @@ use crate::model::{Group, LocateNode}; /// Format content with access to its location on the page. pub fn locate(_: &mut Machine, args: &mut Args) -> TypResult { - let node = LocateNode::new(args.expect("recipe")?); + let node = LocateNode::single(args.expect("recipe")?); Ok(Value::Content(Content::Locate(node))) } diff --git a/src/model/locate.rs b/src/model/locate.rs index fd48e5ad4..10de70e15 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::Content; use crate::diag::TypResult; -use crate::eval::{Args, Dict, Func, Value}; +use crate::eval::{Args, Array, Dict, Func, Value}; use crate::frame::{Element, Frame}; use crate::geom::{Point, Transform}; use crate::syntax::Spanned; @@ -21,8 +21,13 @@ impl Group { } /// Add an entry to the group. - pub fn entry(&self, recipe: Spanned) -> LocateNode { - LocateNode { recipe, group: Some(self.clone()) } + pub fn entry(&self, recipe: Spanned, value: Option) -> LocateNode { + LocateNode::entry(self.clone(), recipe, value) + } + + /// Do something with all entries of a group. + pub fn all(&self, recipe: Spanned) -> LocateNode { + LocateNode::all(self.clone(), recipe) } } @@ -32,57 +37,114 @@ impl Debug for Group { } } -/// A node that can realize itself with its own location. +/// A node that can be realized with pinned document locations. #[derive(Debug, Clone, PartialEq, Hash)] -pub struct LocateNode { - recipe: Spanned, - group: Option, -} +pub struct LocateNode(Arc); impl LocateNode { - /// Create a new locate node. - pub fn new(recipe: Spanned) -> Self { - Self { recipe, group: None } + /// Create a new locatable single node. + pub fn single(recipe: Spanned) -> Self { + Self(Arc::new(Repr::Single(SingleNode(recipe)))) + } + + /// Create a new locatable group entry node. + pub fn entry(group: Group, recipe: Spanned, value: Option) -> Self { + Self(Arc::new(Repr::Entry(EntryNode { group, recipe, value }))) + } + + /// Create a new all node with access to a group's members. + pub fn all(group: Group, recipe: Spanned) -> Self { + Self(Arc::new(Repr::All(AllNode { group, recipe }))) } /// Realize the node. pub fn realize(&self, ctx: &mut Context) -> TypResult { + match self.0.as_ref() { + Repr::Single(single) => single.realize(ctx), + Repr::Entry(entry) => entry.realize(ctx), + Repr::All(all) => all.realize(ctx), + } + } +} + +/// The different kinds of locate nodes. +#[derive(Debug, Clone, PartialEq, Hash)] +enum Repr { + /// A single `locate(me => ...)`. + Single(SingleNode), + /// A locatable group entry. + Entry(EntryNode), + /// A recipe for all entries of a group. + All(AllNode), +} + +/// A solo locatable node. +#[derive(Debug, Clone, PartialEq, Hash)] +struct SingleNode(Spanned); + +impl SingleNode { + fn realize(&self, ctx: &mut Context) -> TypResult { let idx = ctx.pins.cursor(); - let pin = ctx.pins.next(self.group.clone()); + let pin = ctx.pins.next(None, None); + let dict = pin.encode(None); + let args = Args::new(self.0.span, [Value::Dict(dict)]); + Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display()) + } +} + +/// A group node which can interact with its peer's details. +#[derive(Debug, Clone, PartialEq, Hash)] +struct EntryNode { + /// Which group the node belongs to, if any. + group: Group, + /// The recipe to execute. + recipe: Spanned, + /// An arbitrary attached value. + value: Option, +} + +impl EntryNode { + fn realize(&self, ctx: &mut Context) -> TypResult { + let idx = ctx.pins.cursor(); + let pin = ctx.pins.next(Some(self.group.clone()), self.value.clone()); // Determine the index among the peers. - let index = self.group.as_ref().map(|_| { - ctx.pins - .iter() - .filter(|other| { - other.group == self.group && other.loc.flow < pin.loc.flow - }) - .count() - }); + let index = ctx + .pins + .iter() + .filter(|other| other.is_in(&self.group) && other.loc.flow < pin.loc.flow) + .count(); - let dict = pin.encode(index); + let dict = pin.encode(Some(index)); let mut args = Args::new(self.recipe.span, [Value::Dict(dict)]); // Collect all members if requested. - if self.group.is_some() && self.recipe.v.argc() == Some(2) { - let mut all: Vec<_> = - ctx.pins.iter().filter(|other| other.group == self.group).collect(); - - all.sort_by_key(|pin| pin.loc.flow); - - let array = all - .iter() - .enumerate() - .map(|(index, member)| Value::Dict(member.encode(Some(index)))) - .collect(); - - args.push(self.recipe.span, Value::Array(array)) + if self.recipe.v.argc() == Some(2) { + let all = ctx.pins.encode_group(&self.group); + args.push(self.recipe.span, Value::Array(all)) } Ok(Content::Pin(idx) + self.recipe.v.call_detached(ctx, args)?.display()) } } +/// A node with access to the group's members without being one itself. +#[derive(Debug, Clone, PartialEq, Hash)] +struct AllNode { + /// Which group. + group: Group, + /// The recipe to execute. + recipe: Spanned, +} + +impl AllNode { + fn realize(&self, ctx: &mut Context) -> TypResult { + let all = ctx.pins.encode_group(&self.group); + let args = Args::new(self.recipe.span, [Value::Array(all)]); + Ok(self.recipe.v.call_detached(ctx, args)?.display()) + } +} + /// Manages pins. #[derive(Debug, Clone, Hash)] pub struct PinBoard { @@ -94,43 +156,6 @@ pub struct PinBoard { frozen: usize, } -/// A document pin. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct Pin { - /// The physical location of the pin in the document. - loc: Location, - /// The group the pin belongs to, if any. - group: Option, -} - -impl Pin { - /// Encode into a user-facing dictionary. - fn encode(&self, index: Option) -> Dict { - let mut dict = dict! { - "page" => Value::Int(self.loc.page as i64), - "x" => Value::Length(self.loc.pos.x.into()), - "y" => Value::Length(self.loc.pos.y.into()), - }; - - if let Some(index) = index { - dict.insert("index".into(), Value::Int(index as i64)); - } - - dict - } -} - -/// A physical location in a document. -#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] -pub struct Location { - /// The page, starting at 1. - pub page: usize, - /// The exact coordinates on the page (from the top left, as usual). - pub pos: Point, - /// The flow index. - pub flow: usize, -} - impl PinBoard { /// Create an empty pin board. pub fn new() -> Self { @@ -158,15 +183,18 @@ impl PinBoard { } /// Access the next pin. - pub fn next(&mut self, group: Option) -> Pin { + pub fn next(&mut self, group: Option, value: Option) -> Pin { if self.frozen > 0 { return Pin::default(); } let cursor = self.cursor; self.jump(self.cursor + 1); - self.pins[cursor].group = group; - self.pins[cursor].clone() + + let pin = &mut self.pins[cursor]; + pin.group = group; + pin.value = value; + pin.clone() } /// The current cursor. @@ -210,6 +238,16 @@ impl PinBoard { pub fn resolved(&self, prev: &Self) -> usize { self.pins.iter().zip(&prev.pins).filter(|(a, b)| a == b).count() } + + /// Encode a group into a user-facing array. + pub fn encode_group(&self, group: &Group) -> Array { + let mut all: Vec<_> = self.iter().filter(|other| other.is_in(group)).collect(); + all.sort_by_key(|pin| pin.loc.flow); + all.iter() + .enumerate() + .map(|(index, member)| Value::Dict(member.encode(Some(index)))) + .collect() + } } /// Locate all pins in a frame. @@ -241,3 +279,51 @@ fn locate_impl( } } } + +/// A document pin. +#[derive(Debug, Default, Clone, PartialEq, Hash)] +pub struct Pin { + /// The physical location of the pin in the document. + loc: Location, + /// The group the pin belongs to, if any. + group: Option, + /// An arbitrary attached value. + value: Option, +} + +impl Pin { + /// Whether the pin is part of the given group. + fn is_in(&self, group: &Group) -> bool { + self.group.as_ref() == Some(group) + } + + /// Encode into a user-facing dictionary. + fn encode(&self, index: Option) -> Dict { + let mut dict = dict! { + "page" => Value::Int(self.loc.page as i64), + "x" => Value::Length(self.loc.pos.x.into()), + "y" => Value::Length(self.loc.pos.y.into()), + }; + + if let Some(value) = &self.value { + dict.insert("value".into(), value.clone()); + } + + if let Some(index) = index { + dict.insert("index".into(), Value::Int(index as i64)); + } + + dict + } +} + +/// A physical location in a document. +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] +pub struct Location { + /// The page, starting at 1. + pub page: usize, + /// The exact coordinates on the page (from the top left, as usual). + pub pos: Point, + /// The flow index. + pub flow: usize, +} From 22214a1e0a79666caefd486e41828f015878ecb0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 May 2022 15:59:11 +0200 Subject: [PATCH 04/10] Test groups --- src/model/content.rs | 8 +- src/model/locate.rs | 154 +++++++++++++++--------------- tests/ref/layout/locate-group.png | Bin 0 -> 3881 bytes tests/typ/layout/locate-group.typ | 43 +++++++++ 4 files changed, 124 insertions(+), 81 deletions(-) create mode 100644 tests/ref/layout/locate-group.png create mode 100644 tests/typ/layout/locate-group.typ diff --git a/src/model/content.rs b/src/model/content.rs index dad212c8a..21bf83695 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -35,17 +35,13 @@ pub fn layout(ctx: &mut Context, content: &Content) -> TypResult> ctx.pins.locate(&frames); - let count = ctx.pins.len(); - let resolved = ctx.pins.resolved(&prev); - // Quit if we're done or if we've had five passes. - if resolved == count || pass >= 5 { + let unresolved = ctx.pins.unresolved(&prev); + if unresolved == 0 || pass >= 5 { break; } } - // println!("Took {pass} passes"); - Ok(frames) } diff --git a/src/model/locate.rs b/src/model/locate.rs index 10de70e15..a4b25d1de 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -52,7 +52,7 @@ impl LocateNode { Self(Arc::new(Repr::Entry(EntryNode { group, recipe, value }))) } - /// Create a new all node with access to a group's members. + /// Create a new node with access to a group's members. pub fn all(group: Group, recipe: Spanned) -> Self { Self(Arc::new(Repr::All(AllNode { group, recipe }))) } @@ -78,7 +78,7 @@ enum Repr { All(AllNode), } -/// A solo locatable node. +/// An ungrouped locatable node. #[derive(Debug, Clone, PartialEq, Hash)] struct SingleNode(Spanned); @@ -92,10 +92,10 @@ impl SingleNode { } } -/// A group node which can interact with its peer's details. +/// A locatable grouped node which can interact with its peers' details. #[derive(Debug, Clone, PartialEq, Hash)] struct EntryNode { - /// Which group the node belongs to, if any. + /// Which group the node belongs to. group: Group, /// The recipe to execute. recipe: Spanned, @@ -112,13 +112,14 @@ impl EntryNode { let index = ctx .pins .iter() - .filter(|other| other.is_in(&self.group) && other.loc.flow < pin.loc.flow) + .filter(|other| other.is_in(&self.group) && other.flow < pin.flow) .count(); + // Prepare first argument. let dict = pin.encode(Some(index)); let mut args = Args::new(self.recipe.span, [Value::Dict(dict)]); - // Collect all members if requested. + // Collect all group members if second argument is requested. if self.recipe.v.argc() == Some(2) { let all = ctx.pins.encode_group(&self.group); args.push(self.recipe.span, Value::Array(all)) @@ -128,10 +129,10 @@ impl EntryNode { } } -/// A node with access to the group's members without being one itself. +/// A node with access to a group's members. #[derive(Debug, Clone, PartialEq, Hash)] struct AllNode { - /// Which group. + /// Which group the node has access to. group: Group, /// The recipe to execute. recipe: Spanned, @@ -145,56 +146,22 @@ impl AllNode { } } -/// Manages pins. +/// Manages document pins. #[derive(Debug, Clone, Hash)] pub struct PinBoard { /// All currently active pins. - pins: Vec, - /// The index of the next pin in order. + list: Vec, + /// The index of the next pin, in order. cursor: usize, - /// If larger than zero, the board is frozen. + /// If larger than zero, the board is frozen and the cursor will not be + /// advanced. This is used to disable pinning during measure-only layouting. frozen: usize, } impl PinBoard { /// Create an empty pin board. pub fn new() -> Self { - Self { pins: vec![], cursor: 0, frozen: 0 } - } - - /// The number of pins on the board. - pub fn len(&self) -> usize { - self.pins.len() - } - - /// Iterate over all pins on the board. - pub fn iter(&self) -> std::slice::Iter { - self.pins.iter() - } - - /// Freeze the board to prevent modifications. - pub fn freeze(&mut self) { - self.frozen += 1; - } - - /// Freeze the board to prevent modifications. - pub fn unfreeze(&mut self) { - self.frozen -= 1; - } - - /// Access the next pin. - pub fn next(&mut self, group: Option, value: Option) -> Pin { - if self.frozen > 0 { - return Pin::default(); - } - - let cursor = self.cursor; - self.jump(self.cursor + 1); - - let pin = &mut self.pins[cursor]; - pin.group = group; - pin.value = value; - pin.clone() + Self { list: vec![], cursor: 0, frozen: 0 } } /// The current cursor. @@ -209,14 +176,24 @@ impl PinBoard { } self.cursor = cursor; - if cursor >= self.pins.len() { - self.pins.resize(cursor, Pin::default()); + if cursor >= self.list.len() { + self.list.resize(cursor, Pin::default()); } } + /// Freeze the board to prevent modifications. + pub fn freeze(&mut self) { + self.frozen += 1; + } + + /// Freeze the board to prevent modifications. + pub fn unfreeze(&mut self) { + self.frozen -= 1; + } + /// Reset the cursor and remove all unused pins. pub fn reset(&mut self) { - self.pins.truncate(self.cursor); + self.list.truncate(self.cursor); self.cursor = 0; } @@ -224,8 +201,8 @@ impl PinBoard { pub fn locate(&mut self, frames: &[Arc]) { let mut flow = 0; for (i, frame) in frames.iter().enumerate() { - locate_impl( - &mut self.pins, + locate_in_frame( + &mut self.list, &mut flow, 1 + i, frame, @@ -234,15 +211,35 @@ impl PinBoard { } } - /// How many pins are resolved in comparison to an earlier snapshot. - pub fn resolved(&self, prev: &Self) -> usize { - self.pins.iter().zip(&prev.pins).filter(|(a, b)| a == b).count() + /// How many pins are unresolved in comparison to an earlier snapshot. + pub fn unresolved(&self, prev: &Self) -> usize { + self.list.len() - self.list.iter().zip(&prev.list).filter(|(a, b)| a == b).count() + } + + /// Access the next pin. + fn next(&mut self, group: Option, value: Option) -> Pin { + if self.frozen > 0 { + return Pin::default(); + } + + let cursor = self.cursor; + self.jump(self.cursor + 1); + + let pin = &mut self.list[cursor]; + pin.group = group; + pin.value = value; + pin.clone() + } + + /// Iterate over all pins on the board. + fn iter(&self) -> std::slice::Iter { + self.list.iter() } /// Encode a group into a user-facing array. - pub fn encode_group(&self, group: &Group) -> Array { + fn encode_group(&self, group: &Group) -> Array { let mut all: Vec<_> = self.iter().filter(|other| other.is_in(group)).collect(); - all.sort_by_key(|pin| pin.loc.flow); + all.sort_by_key(|pin| pin.flow); all.iter() .enumerate() .map(|(index, member)| Value::Dict(member.encode(Some(index)))) @@ -251,7 +248,7 @@ impl PinBoard { } /// Locate all pins in a frame. -fn locate_impl( +fn locate_in_frame( pins: &mut [Pin], flow: &mut usize, page: usize, @@ -264,14 +261,14 @@ fn locate_impl( let ts = ts .pre_concat(Transform::translate(pos.x, pos.y)) .pre_concat(group.transform); - locate_impl(pins, flow, page, &group.frame, ts); + locate_in_frame(pins, flow, page, &group.frame, ts); } Element::Pin(idx) => { - let loc = &mut pins[*idx].loc; - loc.page = page; - loc.pos = pos.transform(ts); - loc.flow = *flow; + let pin = &mut pins[*idx]; + pin.loc.page = page; + pin.loc.pos = pos.transform(ts); + pin.flow = *flow; *flow += 1; } @@ -282,9 +279,11 @@ fn locate_impl( /// A document pin. #[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct Pin { +struct Pin { /// The physical location of the pin in the document. loc: Location, + /// The flow index. + flow: usize, /// The group the pin belongs to, if any. group: Option, /// An arbitrary attached value. @@ -299,11 +298,7 @@ impl Pin { /// Encode into a user-facing dictionary. fn encode(&self, index: Option) -> Dict { - let mut dict = dict! { - "page" => Value::Int(self.loc.page as i64), - "x" => Value::Length(self.loc.pos.x.into()), - "y" => Value::Length(self.loc.pos.y.into()), - }; + let mut dict = self.loc.encode(); if let Some(value) = &self.value { dict.insert("value".into(), value.clone()); @@ -319,11 +314,20 @@ impl Pin { /// A physical location in a document. #[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] -pub struct Location { +struct Location { /// The page, starting at 1. - pub page: usize, + page: usize, /// The exact coordinates on the page (from the top left, as usual). - pub pos: Point, - /// The flow index. - pub flow: usize, + pos: Point, +} + +impl Location { + /// Encode into a user-facing dictionary. + fn encode(&self) -> Dict { + dict! { + "page" => Value::Int(self.page as i64), + "x" => Value::Length(self.pos.x.into()), + "y" => Value::Length(self.pos.y.into()), + } + } } diff --git a/tests/ref/layout/locate-group.png b/tests/ref/layout/locate-group.png new file mode 100644 index 0000000000000000000000000000000000000000..e38a4a32a00304b5b9cb4f5fcd1faba64bf84339 GIT binary patch literal 3881 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 [{1 + me.index} / {all.len()}] +) + +#counter \ +#box(counter) \ +#counter \ + +--- +// Test minimal citation engine with references before the document. +#let cited = group("citations") +#let num(cited, key) = { + let index = 0 + for item in cited { + if item.value == key { + index = item.index + break + } + } + [\[{index + 1}\]] +} + +#let cite(key) = cited.entry(value: key, (_, all) => num(all, key)) +{cited.all(all => grid( + columns: (auto, 1fr), + gutter: 5pt, + ..{ + let seen = () + for item in all { + if item.value not in seen { + seen.push(item.value) + (num(all, item.value), item.value) + } + } + } +))} + +As shown in #cite("abc") and #cite("def") and #cite("abc") ... From 806d9f0d9ab381500318f3e106b9c20c5eabccb7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 May 2022 17:14:44 +0200 Subject: [PATCH 05/10] Pure functions! --- Cargo.lock | 83 --------------------- Cargo.toml | 1 - src/eval/capture.rs | 16 ++-- src/eval/func.rs | 4 +- src/eval/mod.rs | 100 ++++++++----------------- src/eval/scope.rs | 140 +++++++++++++++++++++-------------- src/library/mod.rs | 60 +++++++-------- tests/typ/code/closure.typ | 13 ++-- tests/typ/code/return.typ | 24 +++--- tests/typ/code/spread.typ | 10 +-- tests/typ/layout/stack-1.typ | 11 +-- tests/typeset.rs | 6 +- 12 files changed, 187 insertions(+), 281 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f54d5f78..2f35c121b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,16 +433,6 @@ dependencies = [ "rand_chacha 0.3.1", ] -[[package]] -name = "lock_api" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.17" @@ -568,29 +558,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" -[[package]] -name = "parking_lot" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys", -] - [[package]] name = "pathfinder_color" version = "0.5.0" @@ -943,12 +910,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "semver" version = "0.11.0" @@ -1177,7 +1138,6 @@ dependencies = [ "memmap2", "miniz_oxide 0.4.4", "once_cell", - "parking_lot", "pdf-writer", "pico-args", "pixglyph", @@ -1420,49 +1380,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - [[package]] name = "xi-unicode" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 585e85ee0..494a912f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ lipsum = { git = "https://github.com/reknih/lipsum", default-features = false } once_cell = "1" serde = { version = "1", features = ["derive"] } typed-arena = "2" -parking_lot = "0.12" unscanny = { git = "https://github.com/typst/unscanny" } regex = "1" diff --git a/src/eval/capture.rs b/src/eval/capture.rs index 24fc7abcd..1192eaa78 100644 --- a/src/eval/capture.rs +++ b/src/eval/capture.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use super::{Scope, Scopes, Value}; use crate::syntax::ast::{ClosureParam, Expr, Ident, Imports, TypedNode}; use crate::syntax::RedRef; @@ -28,14 +26,14 @@ impl<'a> CapturesVisitor<'a> { /// Bind a new internal variable. pub fn bind(&mut self, ident: Ident) { - self.internal.top.def_mut(ident.take(), Value::None); + self.internal.top.define(ident.take(), Value::None); } /// Capture a variable if it isn't internal. pub fn capture(&mut self, ident: Ident) { - if self.internal.get(&ident).is_none() { - if let Some(slot) = self.external.get(&ident) { - self.captures.def_slot(ident.take(), Arc::clone(slot)); + if self.internal.get(&ident).is_err() { + if let Ok(value) = self.external.get(&ident) { + self.captures.define_captured(ident.take(), value.clone()); } } } @@ -145,9 +143,9 @@ mod tests { let red = RedNode::from_root(green, SourceId::from_raw(0)); let mut scopes = Scopes::new(None); - scopes.top.def_const("x", 0); - scopes.top.def_const("y", 0); - scopes.top.def_const("z", 0); + scopes.top.define("x", 0); + scopes.top.define("y", 0); + scopes.top.define("z", 0); let mut visitor = CapturesVisitor::new(&scopes); visitor.visit(red.as_ref()); diff --git a/src/eval/func.rs b/src/eval/func.rs index f15549dce..12dbfb2ee 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -206,7 +206,7 @@ impl Closure { // Parse the arguments according to the parameter list. for (param, default) in &self.params { - scopes.top.def_mut(param, match default { + scopes.top.define(param, match default { None => args.expect::(param)?, Some(default) => { args.named::(param)?.unwrap_or_else(|| default.clone()) @@ -216,7 +216,7 @@ impl Closure { // Put the remaining arguments into the sink. if let Some(sink) = &self.sink { - scopes.top.def_mut(sink, args.take()); + scopes.top.define(sink, args.take()); } // Determine the route inside the closure. diff --git a/src/eval/mod.rs b/src/eval/mod.rs index d76257fa9..db7595f9c 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -31,7 +31,6 @@ pub use value::*; use std::collections::BTreeMap; -use parking_lot::{MappedRwLockWriteGuard, RwLockWriteGuard}; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{At, StrResult, Trace, Tracepoint, TypResult}; @@ -165,7 +164,7 @@ fn eval_markup( } MarkupNode::Expr(Expr::Wrap(wrap)) => { let tail = eval_markup(vm, nodes)?; - vm.scopes.top.def_mut(wrap.binding().take(), tail); + vm.scopes.top.define(wrap.binding().take(), tail); wrap.body().eval(vm)?.display() } @@ -354,10 +353,7 @@ impl Eval for Ident { type Output = Value; fn eval(&self, vm: &mut Machine) -> TypResult { - match vm.scopes.get(self) { - Some(slot) => Ok(slot.read().clone()), - None => bail!(self.span(), "unknown variable"), - } + vm.scopes.get(self).cloned().at(self.span()) } } @@ -404,7 +400,7 @@ fn eval_code( } Expr::Wrap(wrap) => { let tail = eval_code(vm, exprs)?; - vm.scopes.top.def_mut(wrap.binding().take(), tail); + vm.scopes.top.define(wrap.binding().take(), tail); wrap.body().eval(vm)? } @@ -565,8 +561,7 @@ impl BinaryExpr { op: fn(Value, Value) -> StrResult, ) -> TypResult { let rhs = self.rhs().eval(vm)?; - let lhs = self.lhs(); - let mut location = lhs.access(vm)?; + let location = self.lhs().access(vm)?; let lhs = std::mem::take(&mut *location); *location = op(lhs, rhs).at(self.span())?; Ok(Value::None) @@ -748,7 +743,7 @@ impl Eval for LetExpr { Some(expr) => expr.eval(vm)?, None => Value::None, }; - vm.scopes.top.def_mut(self.binding().take(), value); + vm.scopes.top.define(self.binding().take(), value); Ok(Value::None) } } @@ -860,7 +855,7 @@ impl Eval for ForExpr { (for ($($binding:ident => $value:ident),*) in $iter:expr) => {{ #[allow(unused_parens)] for ($($value),*) in $iter { - $(vm.scopes.top.def_mut(&$binding, $value);)* + $(vm.scopes.top.define(&$binding, $value);)* let body = self.body(); let value = body.eval(vm)?; @@ -937,14 +932,14 @@ impl Eval for ImportExpr { match self.imports() { Imports::Wildcard => { - for (var, slot) in module.scope.iter() { - vm.scopes.top.def_mut(var, slot.read().clone()); + for (var, value) in module.scope.iter() { + vm.scopes.top.define(var, value.clone()); } } Imports::Items(idents) => { for ident in idents { - if let Some(slot) = module.scope.get(&ident) { - vm.scopes.top.def_mut(ident.take(), slot.read().clone()); + if let Some(value) = module.scope.get(&ident) { + vm.scopes.top.define(ident.take(), value.clone()); } else { bail!(ident.span(), "unresolved import"); } @@ -1028,11 +1023,11 @@ impl Eval for ReturnExpr { /// Access an expression mutably. pub trait Access { /// Access the value. - fn access<'a>(&self, vm: &'a mut Machine) -> TypResult>; + fn access<'a>(&self, vm: &'a mut Machine) -> TypResult<&'a mut Value>; } impl Access for Expr { - fn access<'a>(&self, vm: &'a mut Machine) -> TypResult> { + fn access<'a>(&self, vm: &'a mut Machine) -> TypResult<&'a mut Value> { match self { Expr::Ident(v) => v.access(vm), Expr::FieldAccess(v) => v.access(vm), @@ -1043,68 +1038,35 @@ impl Access for Expr { } impl Access for Ident { - fn access<'a>(&self, vm: &'a mut Machine) -> TypResult> { - match vm.scopes.get(self) { - Some(slot) => match slot.try_write() { - Some(guard) => Ok(RwLockWriteGuard::map(guard, |v| v)), - None => bail!(self.span(), "cannot mutate a constant"), - }, - None => bail!(self.span(), "unknown variable"), - } + fn access<'a>(&self, vm: &'a mut Machine) -> TypResult<&'a mut Value> { + vm.scopes.get_mut(self).at(self.span()) } } impl Access for FieldAccess { - fn access<'a>(&self, vm: &'a mut Machine) -> TypResult> { - let guard = self.object().access(vm)?; - try_map(guard, |value| { - Ok(match value { - Value::Dict(dict) => dict.get_mut(self.field().take()), - v => bail!( - self.object().span(), - "expected dictionary, found {}", - v.type_name(), - ), - }) + fn access<'a>(&self, vm: &'a mut Machine) -> TypResult<&'a mut Value> { + Ok(match self.object().access(vm)? { + Value::Dict(dict) => dict.get_mut(self.field().take()), + v => bail!( + self.object().span(), + "expected dictionary, found {}", + v.type_name(), + ), }) } } impl Access for FuncCall { - fn access<'a>(&self, vm: &'a mut Machine) -> TypResult> { + fn access<'a>(&self, vm: &'a mut Machine) -> TypResult<&'a mut Value> { let args = self.args().eval(vm)?; - let guard = self.callee().access(vm)?; - try_map(guard, |value| { - Ok(match value { - Value::Array(array) => { - array.get_mut(args.into_index()?).at(self.span())? - } - Value::Dict(dict) => dict.get_mut(args.into_key()?), - v => bail!( - self.callee().span(), - "expected collection, found {}", - v.type_name(), - ), - }) + Ok(match self.callee().access(vm)? { + Value::Array(array) => array.get_mut(args.into_index()?).at(self.span())?, + Value::Dict(dict) => dict.get_mut(args.into_key()?), + v => bail!( + self.callee().span(), + "expected collection, found {}", + v.type_name(), + ), }) } } - -/// A mutable location. -type Location<'a> = MappedRwLockWriteGuard<'a, Value>; - -/// Map a reader-writer lock with a function. -fn try_map(location: Location, f: F) -> TypResult -where - F: FnOnce(&mut Value) -> TypResult<&mut Value>, -{ - let mut error = None; - MappedRwLockWriteGuard::try_map(location, |value| match f(value) { - Ok(value) => Some(value), - Err(err) => { - error = Some(err); - None - } - }) - .map_err(|_| error.unwrap()) -} diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 8a0b81659..29778a903 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -1,18 +1,11 @@ use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::iter; -use std::sync::Arc; - -use parking_lot::RwLock; +use std::hash::Hash; use super::{Args, Func, Machine, Node, Value}; -use crate::diag::TypResult; +use crate::diag::{StrResult, TypResult}; use crate::util::EcoString; -/// A slot where a variable is stored. -pub type Slot = Arc>; - /// A stack of scopes. #[derive(Debug, Default, Clone)] pub struct Scopes<'a> { @@ -42,21 +35,33 @@ impl<'a> Scopes<'a> { self.top = self.scopes.pop().expect("no pushed scope"); } - /// Look up the slot of a variable. - pub fn get(&self, var: &str) -> Option<&Slot> { - iter::once(&self.top) + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> StrResult<&Value> { + Ok(std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .chain(self.base.into_iter()) .find_map(|scope| scope.get(var)) + .ok_or("unknown variable")?) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> StrResult<&mut Value> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + if self.base.map_or(false, |base| base.get(var).is_some()) { + "cannot mutate a constant" + } else { + "unknown variable" + } + })? } } -/// A map from variable names to variable slots. -#[derive(Default, Clone)] -pub struct Scope { - /// The mapping from names to slots. - values: BTreeMap, -} +/// A map from binding names to values. +#[derive(Default, Clone, Hash)] +pub struct Scope(BTreeMap); impl Scope { /// Create a new empty scope. @@ -64,24 +69,9 @@ impl Scope { Self::default() } - /// Define a constant variable with a value. - pub fn def_const(&mut self, var: impl Into, value: impl Into) { - let cell = RwLock::new(value.into()); - - // Make it impossible to write to this value again. - std::mem::forget(cell.read()); - - self.values.insert(var.into(), Arc::new(cell)); - } - - /// Define a mutable variable with a value. - pub fn def_mut(&mut self, var: impl Into, value: impl Into) { - self.values.insert(var.into(), Arc::new(RwLock::new(value.into()))); - } - - /// Define a variable with a slot. - pub fn def_slot(&mut self, var: impl Into, slot: Slot) { - self.values.insert(var.into(), slot); + /// Bind a value to a name. + pub fn define(&mut self, name: impl Into, value: impl Into) { + self.0.insert(name.into(), Slot::new(value.into(), Kind::Normal)); } /// Define a function through a native rust function. @@ -90,32 +80,36 @@ impl Scope { name: &'static str, func: fn(&mut Machine, &mut Args) -> TypResult, ) { - self.def_const(name, Func::from_fn(name, func)); + self.define(name, Func::from_fn(name, func)); } /// Define a function through a native rust node. pub fn def_node(&mut self, name: &'static str) { - self.def_const(name, Func::from_node::(name)); + self.define(name, Func::from_node::(name)); } - /// Look up the value of a variable. - pub fn get(&self, var: &str) -> Option<&Slot> { - self.values.get(var) + /// Define a captured, immutable binding. + pub fn define_captured( + &mut self, + var: impl Into, + value: impl Into, + ) { + self.0.insert(var.into(), Slot::new(value.into(), Kind::Captured)); + } + + /// Try to access a variable immutably. + pub fn get(&self, var: &str) -> Option<&Value> { + self.0.get(var).map(Slot::read) + } + + /// Try to access a variable mutably. + pub fn get_mut(&mut self, var: &str) -> Option> { + self.0.get_mut(var).map(Slot::write) } /// Iterate over all definitions. - pub fn iter(&self) -> impl Iterator { - self.values.iter().map(|(k, v)| (k.as_str(), v)) - } -} - -impl Hash for Scope { - fn hash(&self, state: &mut H) { - self.values.len().hash(state); - for (name, value) in self.values.iter() { - name.hash(state); - value.read().hash(state); - } + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (k.as_str(), v.read())) } } @@ -123,7 +117,45 @@ impl Debug for Scope { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str("Scope ")?; f.debug_map() - .entries(self.values.iter().map(|(k, v)| (k, v.read()))) + .entries(self.0.iter().map(|(k, v)| (k, v.read()))) .finish() } } + +/// A slot where a variable is stored. +#[derive(Clone, Hash)] +struct Slot { + /// The stored value. + value: Value, + /// The kind of slot, determines how the value can be accessed. + kind: Kind, +} + +/// The different kinds of slots. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum Kind { + /// A normal, mutable binding. + Normal, + /// A captured copy of another variable. + Captured, +} + +impl Slot { + /// Create a new constant slot. + fn new(value: Value, kind: Kind) -> Self { + Self { value, kind } + } + + /// Read the variable. + fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the variable. + fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + Kind::Normal => Ok(&mut self.value), + Kind::Captured => Err("cannot mutate a captured variable")?, + } + } +} diff --git a/src/library/mod.rs b/src/library/mod.rs index cf5d6e64f..27658189b 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -96,38 +96,38 @@ pub fn new() -> Scope { std.def_fn("lorem", utility::lorem); // Predefined colors. - std.def_const("black", Color::BLACK); - std.def_const("gray", Color::GRAY); - std.def_const("silver", Color::SILVER); - std.def_const("white", Color::WHITE); - std.def_const("navy", Color::NAVY); - std.def_const("blue", Color::BLUE); - std.def_const("aqua", Color::AQUA); - std.def_const("teal", Color::TEAL); - std.def_const("eastern", Color::EASTERN); - std.def_const("purple", Color::PURPLE); - std.def_const("fuchsia", Color::FUCHSIA); - std.def_const("maroon", Color::MAROON); - std.def_const("red", Color::RED); - std.def_const("orange", Color::ORANGE); - std.def_const("yellow", Color::YELLOW); - std.def_const("olive", Color::OLIVE); - std.def_const("green", Color::GREEN); - std.def_const("lime", Color::LIME); + std.define("black", Color::BLACK); + std.define("gray", Color::GRAY); + std.define("silver", Color::SILVER); + std.define("white", Color::WHITE); + std.define("navy", Color::NAVY); + std.define("blue", Color::BLUE); + std.define("aqua", Color::AQUA); + std.define("teal", Color::TEAL); + std.define("eastern", Color::EASTERN); + std.define("purple", Color::PURPLE); + std.define("fuchsia", Color::FUCHSIA); + std.define("maroon", Color::MAROON); + std.define("red", Color::RED); + std.define("orange", Color::ORANGE); + std.define("yellow", Color::YELLOW); + std.define("olive", Color::OLIVE); + std.define("green", Color::GREEN); + std.define("lime", Color::LIME); // Other constants. - std.def_const("ltr", Dir::LTR); - std.def_const("rtl", Dir::RTL); - std.def_const("ttb", Dir::TTB); - std.def_const("btt", Dir::BTT); - std.def_const("start", RawAlign::Start); - std.def_const("end", RawAlign::End); - std.def_const("left", RawAlign::Specific(Align::Left)); - std.def_const("center", RawAlign::Specific(Align::Center)); - std.def_const("right", RawAlign::Specific(Align::Right)); - std.def_const("top", RawAlign::Specific(Align::Top)); - std.def_const("horizon", RawAlign::Specific(Align::Horizon)); - std.def_const("bottom", RawAlign::Specific(Align::Bottom)); + std.define("ltr", Dir::LTR); + std.define("rtl", Dir::RTL); + std.define("ttb", Dir::TTB); + std.define("btt", Dir::BTT); + std.define("start", RawAlign::Start); + std.define("end", RawAlign::End); + std.define("left", RawAlign::Specific(Align::Left)); + std.define("center", RawAlign::Specific(Align::Center)); + std.define("right", RawAlign::Specific(Align::Right)); + std.define("top", RawAlign::Specific(Align::Top)); + std.define("horizon", RawAlign::Specific(Align::Horizon)); + std.define("bottom", RawAlign::Specific(Align::Bottom)); std } diff --git a/tests/typ/code/closure.typ b/tests/typ/code/closure.typ index aa7bc5b92..e9389e131 100644 --- a/tests/typ/code/closure.typ +++ b/tests/typ/code/closure.typ @@ -31,7 +31,7 @@ --- // Capture environment. { - let mark = "?" + let mark = "!" let greet = { let hi = "Hi" name => { @@ -39,9 +39,10 @@ } } - test(greet("Typst"), "Hi, Typst?") + test(greet("Typst"), "Hi, Typst!") - mark = "!" + // Changing the captured variable after the closure definition has no effect. + mark = "?" test(greet("Typst"), "Hi, Typst!") } @@ -71,12 +72,12 @@ // For loop bindings. { let v = (1, 2, 3) - let s = 0 let f() = { + let s = 0 for v in v { s += v } + s } - f() - test(s, 6) + test(f(), 6) } --- diff --git a/tests/typ/code/return.typ b/tests/typ/code/return.typ index 8db99a813..0eea394e2 100644 --- a/tests/typ/code/return.typ +++ b/tests/typ/code/return.typ @@ -55,30 +55,28 @@ --- // Test that the expression is evaluated to the end. -#let y = 1 -#let identity(x, ..rest) = x -#let f(x) = { - identity( - ..return, - x + 1, - y = 2, - ) +#let sum(..args) = { + let s = 0 + for v in args.positional() { + s += v + } + s +} + +#let f() = { + sum(..return, 1, 2, 3) "nope" } -#test(f(1), 2) -#test(y, 2) +#test(f(), 6) --- // Test value return from content. #let x = 3 #let f() = [ Hello 😀 - { x = 1 } #return "nope" - { x = 2 } World ] #test(f(), "nope") -#test(x, 1) diff --git a/tests/typ/code/spread.typ b/tests/typ/code/spread.typ index 86dbfd989..ff661eadb 100644 --- a/tests/typ/code/spread.typ +++ b/tests/typ/code/spread.typ @@ -23,16 +23,14 @@ } --- -// Test storing arguments in a variable. +// Test doing things with arguments. { - let args - let save(..sink) = { - args = sink + let save(..args) = { + test(type(args), "arguments") + test(repr(args), "(1, 2, three: true)") } save(1, 2, three: true) - test(type(args), "arguments") - test(repr(args), "(1, 2, three: true)") } --- diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ index 19a00de59..0ecbe246d 100644 --- a/tests/typ/layout/stack-1.typ +++ b/tests/typ/layout/stack-1.typ @@ -7,13 +7,14 @@ 30pt, 50%, 20pt, 100%, ) -#let shaded = { - let v = 0% - let next() = { v += 10%; rgb(v, v, v) } - w => rect(width: w, height: 10pt, fill: next()) +#let shaded(i, w) = { + let v = (i + 1) * 10% + rect(width: w, height: 10pt, fill: rgb(v, v, v)) } -#let items = for w in widths { (align(right, shaded(w)),) } +#let items = for i, w in widths { + (align(right, shaded(i, w)),) +} #set page(width: 50pt, margins: 0pt) #stack(dir: btt, ..items) diff --git a/tests/typeset.rs b/tests/typeset.rs index b334ae9a0..72bdb4313 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -70,10 +70,10 @@ fn main() { ); styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into())); - // Hook up an assert function into the global scope. + // Hook up two more colors and an assert function into the global scope. let mut std = typst::library::new(); - std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); - std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); + std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); + std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); std.def_fn("test", move |_, args| { let lhs = args.expect::("left-hand side")?; let rhs = args.expect::("right-hand side")?; From 9950a69d23a6415cdbea08bc770f4019344318a0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 May 2022 09:36:51 +0200 Subject: [PATCH 06/10] Replay group and value side effects --- src/model/layout.rs | 7 ++++--- src/model/locate.rs | 32 +++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/model/layout.rs b/src/model/layout.rs index 49720be4d..92d73977b 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -221,18 +221,19 @@ impl Layout for LayoutNode { regions: &Regions, styles: StyleChain, ) -> TypResult>> { - let (result, cursor) = crate::memo::memoized( + let (result, at, pins) = crate::memo::memoized( (self, &mut *ctx, regions, styles), |(node, ctx, regions, styles)| { + let at = ctx.pins.cursor(); let entry = StyleEntry::Barrier(Barrier::new(node.id())); let result = node.0.layout(ctx, regions, entry.chain(&styles)); - (result, ctx.pins.cursor()) + (result, at, ctx.pins.from(at)) }, ); // Replay the side effect in case of caching. This should currently be // more or less the only relevant side effect on the context. - ctx.pins.jump(cursor); + ctx.pins.replay(at, pins); result } diff --git a/src/model/locate.rs b/src/model/locate.rs index a4b25d1de..c73c03399 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -169,15 +169,17 @@ impl PinBoard { self.cursor } - /// Set the current cursor. - pub fn jump(&mut self, cursor: usize) { - if self.frozen > 0 { - return; - } + /// All pins from `prev` to the current cursor. + pub fn from(&self, prev: usize) -> Vec { + self.list[prev .. self.cursor].to_vec() + } - self.cursor = cursor; - if cursor >= self.list.len() { - self.list.resize(cursor, Pin::default()); + /// Add the given pins at the given location and set the cursor behind them. + pub fn replay(&mut self, at: usize, pins: Vec) { + if !self.frozen() { + self.cursor = at + pins.len(); + let end = self.cursor.min(self.list.len()); + self.list.splice(at .. end, pins); } } @@ -191,6 +193,11 @@ impl PinBoard { self.frozen -= 1; } + /// Whether the board is currently frozen. + pub fn frozen(&self) -> bool { + self.frozen > 0 + } + /// Reset the cursor and remove all unused pins. pub fn reset(&mut self) { self.list.truncate(self.cursor); @@ -218,12 +225,15 @@ impl PinBoard { /// Access the next pin. fn next(&mut self, group: Option, value: Option) -> Pin { - if self.frozen > 0 { + if self.frozen() { return Pin::default(); } let cursor = self.cursor; - self.jump(self.cursor + 1); + self.cursor += 1; + if self.cursor >= self.list.len() { + self.list.resize(self.cursor, Pin::default()); + } let pin = &mut self.list[cursor]; pin.group = group; @@ -279,7 +289,7 @@ fn locate_in_frame( /// A document pin. #[derive(Debug, Default, Clone, PartialEq, Hash)] -struct Pin { +pub struct Pin { /// The physical location of the pin in the document. loc: Location, /// The flow index. From ae68a15a46c3a9fce4db7bfbf129ee103dd4c30d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 May 2022 14:19:58 +0200 Subject: [PATCH 07/10] Print function for debugging --- tests/typeset.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/typeset.rs b/tests/typeset.rs index 72bdb4313..0d6031259 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -70,7 +70,7 @@ fn main() { ); styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into())); - // Hook up two more colors and an assert function into the global scope. + // Hook up helpers into the global scope. let mut std = typst::library::new(); std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF)); @@ -82,6 +82,17 @@ fn main() { } Ok(Value::None) }); + std.def_fn("print", move |_, args| { + print!("> "); + for (i, value) in args.all::()?.into_iter().enumerate() { + if i > 0 { + print!(", ") + } + print!("{value:?}"); + } + println!(); + Ok(Value::None) + }); // Create loader and context. let loader = FsLoader::new().with_path(FONT_DIR); From 736289b064c53c215249aad70b3a6a96e79b6021 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 May 2022 14:20:05 +0200 Subject: [PATCH 08/10] Ensure me is always contained in all --- src/model/locate.rs | 12 ++++++++++-- tests/typ/layout/locate-group.typ | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/model/locate.rs b/src/model/locate.rs index c73c03399..05204d2ec 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -112,7 +112,15 @@ impl EntryNode { let index = ctx .pins .iter() - .filter(|other| other.is_in(&self.group) && other.flow < pin.flow) + .enumerate() + .filter(|&(k, other)| { + other.is_in(&self.group) + && if k < idx { + other.flow <= pin.flow + } else { + other.flow < pin.flow + } + }) .count(); // Prepare first argument. @@ -248,7 +256,7 @@ impl PinBoard { /// Encode a group into a user-facing array. fn encode_group(&self, group: &Group) -> Array { - let mut all: Vec<_> = self.iter().filter(|other| other.is_in(group)).collect(); + let mut all: Vec<_> = self.iter().filter(|pin| pin.is_in(group)).collect(); all.sort_by_key(|pin| pin.flow); all.iter() .enumerate() diff --git a/tests/typ/layout/locate-group.typ b/tests/typ/layout/locate-group.typ index 49d4e3356..5c022f5ac 100644 --- a/tests/typ/layout/locate-group.typ +++ b/tests/typ/layout/locate-group.typ @@ -41,3 +41,22 @@ ))} As shown in #cite("abc") and #cite("def") and #cite("abc") ... + +--- +// Test that `all` contains `me`. +// Ref: false +#show it: heading as group("headings").entry( + (me, all) => { + let last + for prev in all { + last = prev + if prev.index == me.index { + break + } + } + assert(last == me) + } +) + += A +== B From f8009b5b59660b8252bd3ee2cedfd234a1c30cb1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 May 2022 14:33:00 +0200 Subject: [PATCH 09/10] Reuse location type --- src/export/pdf.rs | 10 +++++----- src/frame.rs | 28 +++++++++++++++++++--------- src/library/text/link.rs | 21 ++++++++------------- src/model/locate.rs | 27 +++++++++------------------ 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 5e8896f75..c050bfc5d 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -333,14 +333,14 @@ impl<'a> PdfExporter<'a> { .action_type(ActionType::Uri) .uri(Str(uri.as_str().as_bytes())); } - Destination::Internal(page, point) => { - let page = page - 1; - let height = page_heights[page]; + Destination::Internal(loc) => { + let index = loc.page - 1; + let height = page_heights[index]; link.action() .action_type(ActionType::GoTo) .destination_direct() - .page(page_refs[page]) - .xyz(point.x.to_f32(), height - point.y.to_f32(), None); + .page(page_refs[index]) + .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None); } } } diff --git a/src/frame.rs b/src/frame.rs index 289de6dad..2ece51476 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -3,6 +3,7 @@ use std::fmt::{self, Debug, Formatter, Write}; use std::sync::Arc; +use crate::eval::{Dict, Value}; use crate::font::FaceId; use crate::geom::{ Align, Em, Length, Numeric, Paint, Point, Shape, Size, Spec, Transform, @@ -313,21 +314,30 @@ pub struct Glyph { } /// A link destination. -#[derive(Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { /// A link to a point on a page. - Internal(usize, Point), + Internal(Location), /// A link to a URL. Url(EcoString), } -impl Debug for Destination { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Internal(page, point) => { - write!(f, "Internal(Page {}, {:?})", page, point) - } - Self::Url(url) => write!(f, "Url({})", url), +/// A physical location in a document. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Location { + /// The page, starting at 1. + pub page: usize, + /// The exact coordinates on the page (from the top left, as usual). + pub pos: Point, +} + +impl Location { + /// Encode into a user-facing dictionary. + pub fn encode(&self) -> Dict { + dict! { + "page" => Value::Int(self.page as i64), + "x" => Value::Length(self.pos.x.into()), + "y" => Value::Length(self.pos.y.into()), } } } diff --git a/src/library/text/link.rs b/src/library/text/link.rs index 2ce7a4695..12cbaf590 100644 --- a/src/library/text/link.rs +++ b/src/library/text/link.rs @@ -1,6 +1,5 @@ use super::TextNode; use crate::library::prelude::*; -use crate::util::EcoString; /// Link text and other elements to an URL. #[derive(Debug, Hash)] @@ -24,7 +23,7 @@ impl LinkNode { let dest = args.expect::("destination")?; let body = match dest { Destination::Url(_) => args.eat()?, - Destination::Internal(_, _) => Some(args.expect("body")?), + Destination::Internal(_) => Some(args.expect("body")?), }; Self { dest, body } })) @@ -36,10 +35,10 @@ castable! { Expected: "string or dictionary with `page`, `x`, and `y` keys", Value::Str(string) => Self::Url(string), Value::Dict(dict) => { - let page: i64 = dict.get(&EcoString::from_str("page"))?.clone().cast()?; - let x: RawLength = dict.get(&EcoString::from_str("x"))?.clone().cast()?; - let y: RawLength = dict.get(&EcoString::from_str("y"))?.clone().cast()?; - Self::Internal(page as usize, Point::new(x.length, y.length)) + let page: i64 = dict.get(&"page".into())?.clone().cast()?; + let x: RawLength = dict.get(&"x".into())?.clone().cast()?; + let y: RawLength = dict.get(&"y".into())?.clone().cast()?; + Self::Internal(Location { page: page as usize, pos: Point::new(x.length, y.length) }) }, } @@ -56,11 +55,7 @@ impl Show for LinkNode { dict! { "url" => match &self.dest { Destination::Url(url) => Value::Str(url.clone()), - Destination::Internal(page, point) => Value::Dict(dict!{ - "page" => Value::Int(*page as i64), - "x" => Value::Length(point.x.into()), - "y" => Value::Length(point.y.into()), - }), + Destination::Internal(loc) => Value::Dict(loc.encode()), }, "body" => match &self.body { Some(body) => Value::Content(body.clone()), @@ -79,7 +74,7 @@ impl Show for LinkNode { let shorter = text.len() < url.len(); Content::Text(if shorter { text.into() } else { url.clone() }) } - Destination::Internal(_, _) => panic!("missing body"), + Destination::Internal(_) => Content::Empty, })) } @@ -99,7 +94,7 @@ impl Show for LinkNode { if match styles.get(Self::UNDERLINE) { Smart::Auto => match &self.dest { Destination::Url(_) => true, - Destination::Internal(_, _) => false, + Destination::Internal(_) => false, }, Smart::Custom(underline) => underline, } { diff --git a/src/model/locate.rs b/src/model/locate.rs index 05204d2ec..c61facc55 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use super::Content; use crate::diag::TypResult; use crate::eval::{Args, Array, Dict, Func, Value}; -use crate::frame::{Element, Frame}; +use crate::frame::{Element, Frame, Location}; use crate::geom::{Point, Transform}; use crate::syntax::Spanned; use crate::util::EcoString; @@ -296,7 +296,7 @@ fn locate_in_frame( } /// A document pin. -#[derive(Debug, Default, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq, Hash)] pub struct Pin { /// The physical location of the pin in the document. loc: Location, @@ -330,22 +330,13 @@ impl Pin { } } -/// A physical location in a document. -#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] -struct Location { - /// The page, starting at 1. - page: usize, - /// The exact coordinates on the page (from the top left, as usual). - pos: Point, -} - -impl Location { - /// Encode into a user-facing dictionary. - fn encode(&self) -> Dict { - dict! { - "page" => Value::Int(self.page as i64), - "x" => Value::Length(self.pos.x.into()), - "y" => Value::Length(self.pos.y.into()), +impl Default for Pin { + fn default() -> Self { + Self { + loc: Location { page: 0, pos: Point::zero() }, + flow: 0, + group: None, + value: None, } } } From 8ba11b0722599892499337b3272cec38945d11de Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 27 May 2022 16:37:03 +0200 Subject: [PATCH 10/10] Reword --- src/frame.rs | 3 ++- src/model/locate.rs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index 2ece51476..2a16b6f21 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -219,7 +219,8 @@ pub enum Element { Image(ImageId, Size), /// A link to an external resource and its trigger region. Link(Destination, Size), - /// A pin identified by index. + /// A pin identified by index. This is used to find elements on the pages + /// and use their location in formatting. Exporters can just ignore it. Pin(usize), } diff --git a/src/model/locate.rs b/src/model/locate.rs index c61facc55..97c140349 100644 --- a/src/model/locate.rs +++ b/src/model/locate.rs @@ -85,7 +85,7 @@ struct SingleNode(Spanned); impl SingleNode { fn realize(&self, ctx: &mut Context) -> TypResult { let idx = ctx.pins.cursor(); - let pin = ctx.pins.next(None, None); + let pin = ctx.pins.get_or_create(None, None); let dict = pin.encode(None); let args = Args::new(self.0.span, [Value::Dict(dict)]); Ok(Content::Pin(idx) + self.0.v.call_detached(ctx, args)?.display()) @@ -106,7 +106,7 @@ struct EntryNode { impl EntryNode { fn realize(&self, ctx: &mut Context) -> TypResult { let idx = ctx.pins.cursor(); - let pin = ctx.pins.next(Some(self.group.clone()), self.value.clone()); + let pin = ctx.pins.get_or_create(Some(self.group.clone()), self.value.clone()); // Determine the index among the peers. let index = ctx @@ -231,8 +231,8 @@ impl PinBoard { self.list.len() - self.list.iter().zip(&prev.list).filter(|(a, b)| a == b).count() } - /// Access the next pin. - fn next(&mut self, group: Option, value: Option) -> Pin { + /// Access or create the next pin. + fn get_or_create(&mut self, group: Option, value: Option) -> Pin { if self.frozen() { return Pin::default(); }