summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/mod.rs2
-rw-r--r--src/export/pdf.rs1
-rw-r--r--src/export/render.rs1
-rw-r--r--src/frame.rs5
-rw-r--r--src/lib.rs13
-rw-r--r--src/library/mod.rs1
-rw-r--r--src/library/text/par.rs43
-rw-r--r--src/library/utility/locate.rs8
-rw-r--r--src/library/utility/mod.rs2
-rw-r--r--src/memo.rs20
-rw-r--r--src/model/content.rs50
-rw-r--r--src/model/layout.rs14
-rw-r--r--src/model/locate.rs125
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/recipe.rs20
15 files changed, 262 insertions, 45 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index e54dfce4..d76257fa 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 aeb5c47e..5e8896f7 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 aa60e67e..163707eb 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 6475f92a..289de6da 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 17225b32..34e87c5e 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<Vec<Arc<Frame>>> {
/// 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<SourceId, Module>,
+ modules: HashMap<SourceId, Module>,
+ /// Stores document pins.
+ pins: PinBoard,
}
impl Context {
/// Create a new context.
pub fn new(loader: Arc<dyn Loader>, 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 ac0cbb92..3321a36b 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 1269ffed..65098b61 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<Frame>),
/// 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(&quotes, *double, peeked));
+ full.push_str(quoter.quote(&quotes, 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 00000000..0352199f
--- /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<Value> {
+ 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 10aa7c7a..32815607 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 6545ebcc..2eee071c 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<H: std::hash::Hasher>(&self, _: &mut H) {}
- }
- };
+
+impl Hash for crate::font::FontStore {
+ fn hash<H: Hasher>(&self, _: &mut H) {}
}
-skip!(crate::font::FontStore);
-skip!(crate::Context);
+impl Hash for crate::Context {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.pins.hash(state);
+ }
+}
diff --git a/src/model/content.rs b/src/model/content.rs
index c09979d5..effe84ae 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<Vec<Arc<Frame>>> {
+ 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<Vec<Arc<Frame>>> {
let copy = ctx.config.styles.clone();
let styles = StyleChain::with_root(&copy);
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<Dict>),
+ /// 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 6dfbcb90..49720be4 100644
--- a/src/model/layout.rs
+++ b/src/model/layout.rs
@@ -221,13 +221,19 @@ impl Layout for LayoutNode {
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> {
- 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 00000000..9b0d13e7
--- /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<Func>);
+
+impl LocateNode {
+ /// Create a new locate node.
+ pub fn new(recipe: Spanned<Func>) -> Self {
+ Self(recipe)
+ }
+
+ /// Realize the node.
+ pub fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
+ 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<Location>,
+ /// 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<Frame>]) {
+ 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 5c8b82c0..379b633f 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 e4417adf..6261e704 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<Func>,
}
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
+ )
}
}