summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2022-05-27 16:39:06 +0200
committerGitHub <noreply@github.com>2022-05-27 16:39:06 +0200
commit73086b5a7c1b0f9f638165803c237901499adb64 (patch)
treec120a2449aaf325cb675ea3363ee69758a734d86 /src
parent99cb655832161d4ebec73273a15453a8f6acc1b7 (diff)
parent8ba11b0722599892499337b3272cec38945d11de (diff)
Merge pull request #71 from typst/pins
Diffstat (limited to 'src')
-rw-r--r--src/eval/args.rs9
-rw-r--r--src/eval/capture.rs16
-rw-r--r--src/eval/func.rs4
-rw-r--r--src/eval/methods.rs35
-rw-r--r--src/eval/mod.rs102
-rw-r--r--src/eval/ops.rs10
-rw-r--r--src/eval/scope.rs138
-rw-r--r--src/eval/value.rs14
-rw-r--r--src/export/pdf.rs11
-rw-r--r--src/export/render.rs1
-rw-r--r--src/frame.rs34
-rw-r--r--src/lib.rs13
-rw-r--r--src/library/layout/grid.rs4
-rw-r--r--src/library/layout/locate.rs14
-rw-r--r--src/library/layout/mod.rs2
-rw-r--r--src/library/mod.rs62
-rw-r--r--src/library/prelude.rs4
-rw-r--r--src/library/text/link.rs21
-rw-r--r--src/library/text/par.rs47
-rw-r--r--src/memo.rs20
-rw-r--r--src/model/content.rs48
-rw-r--r--src/model/layout.rs15
-rw-r--r--src/model/locate.rs342
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/recipe.rs20
25 files changed, 734 insertions, 254 deletions
diff --git a/src/eval/args.rs b/src/eval/args.rs
index 69e6aaee..4d280ff7 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<T>(&mut self) -> TypResult<Option<T>>
where
diff --git a/src/eval/capture.rs b/src/eval/capture.rs
index 24fc7abc..1192eaa7 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 f15549dc..12dbfb2e 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::<Value>(param)?,
Some(default) => {
args.named::<Value>(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/methods.rs b/src/eval/methods.rs
index f6de614f..e8296d23 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,32 @@ pub fn call(
_ => missing()?,
},
- Value::Dyn(dynamic) => {
- if let Some(regex) = dynamic.downcast::<Regex>() {
- match method {
- "matches" => {
- Value::Bool(regex.matches(&args.expect::<EcoString>("text")?))
- }
- _ => missing()?,
+ Value::Dyn(dynamic) => match method {
+ "matches" => {
+ if let Some(regex) = dynamic.downcast::<Regex>() {
+ Value::Bool(regex.matches(&args.expect::<EcoString>("text")?))
+ } else {
+ missing()?
}
- } else {
- missing()?
}
- }
+ "entry" => {
+ if let Some(group) = dynamic.downcast::<Group>() {
+ Value::Content(Content::Locate(
+ group.entry(args.expect("recipe")?, args.named("value")?),
+ ))
+ } else {
+ missing()?
+ }
+ }
+ "all" => {
+ if let Some(group) = dynamic.downcast::<Group>() {
+ Value::Content(Content::Locate(group.all(args.expect("recipe")?)))
+ } else {
+ missing()?
+ }
+ }
+ _ => missing()?,
+ },
_ => missing()?,
};
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index e54dfce4..db7595f9 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<Self::Output> {
- 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<Value>,
) -> TypResult<Value> {
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)
}
}
@@ -797,7 +792,7 @@ impl Eval for ShowExpr {
body,
});
- Ok(Recipe { pattern, func, span })
+ Ok(Recipe { pattern, func: Spanned::new(func, span) })
}
}
@@ -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<Location<'a>>;
+ 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<Location<'a>> {
+ 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<Location<'a>> {
- 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<Location<'a>> {
- 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<Location<'a>> {
+ 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<F>(location: Location, f: F) -> TypResult<Location>
-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/ops.rs b/src/eval/ops.rs
index b3f2f3b4..f88f3cee 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<Value> {
(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<Value> {
(a.downcast::<RawAlign>(), b.downcast::<RawAlign>())
{
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/scope.rs b/src/eval/scope.rs
index 8a0b8165..29778a90 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<RwLock<Value>>;
-
/// A stack of scopes.
#[derive(Debug, Default, Clone)]
pub struct Scopes<'a> {
@@ -42,46 +35,43 @@ 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")?)
}
-}
-/// A map from variable names to variable slots.
-#[derive(Default, Clone)]
-pub struct Scope {
- /// The mapping from names to slots.
- values: BTreeMap<EcoString, Slot>,
+ /// 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 binding names to values.
+#[derive(Default, Clone, Hash)]
+pub struct Scope(BTreeMap<EcoString, Slot>);
+
impl Scope {
/// Create a new empty scope.
pub fn new() -> Self {
Self::default()
}
- /// Define a constant variable with a value.
- pub fn def_const(&mut self, var: impl Into<EcoString>, value: impl Into<Value>) {
- 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<EcoString>, value: impl Into<Value>) {
- 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<EcoString>, slot: Slot) {
- self.values.insert(var.into(), slot);
+ /// Bind a value to a name.
+ pub fn define(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) {
+ 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<Value>,
) {
- 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<T: Node>(&mut self, name: &'static str) {
- self.def_const(name, Func::from_node::<T>(name));
+ self.define(name, Func::from_node::<T>(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<EcoString>,
+ value: impl Into<Value>,
+ ) {
+ self.0.insert(var.into(), Slot::new(value.into(), Kind::Captured));
}
- /// Iterate over all definitions.
- pub fn iter(&self) -> impl Iterator<Item = (&str, &Slot)> {
- self.values.iter().map(|(k, v)| (k.as_str(), v))
+ /// Try to access a variable immutably.
+ pub fn get(&self, var: &str) -> Option<&Value> {
+ self.0.get(var).map(Slot::read)
}
-}
-impl Hash for Scope {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.values.len().hash(state);
- for (name, value) in self.values.iter() {
- name.hash(state);
- value.read().hash(state);
- }
+ /// Try to access a variable mutably.
+ pub fn get_mut(&mut self, var: &str) -> Option<StrResult<&mut Value>> {
+ self.0.get_mut(var).map(Slot::write)
+ }
+
+ /// Iterate over all definitions.
+ pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
+ 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/eval/value.rs b/src/eval/value.rs
index b47d1e91..9b36812a 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<T>(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/export/pdf.rs b/src/export/pdf.rs
index aeb5c47e..c050bfc5 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);
}
}
}
@@ -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..2a16b6f2 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,
@@ -218,6 +219,9 @@ pub enum Element {
Image(ImageId, Size),
/// A link to an external resource and its trigger region.
Link(Destination, Size),
+ /// 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),
}
impl Debug for Element {
@@ -227,7 +231,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})"),
}
}
}
@@ -310,21 +315,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/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/layout/grid.rs b/src/library/layout/grid.rs
index 5b621732..4cad9de6 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<Vec<Arc<Frame>>> {
+ 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/layout/locate.rs b/src/library/layout/locate.rs
new file mode 100644
index 00000000..74480b91
--- /dev/null
+++ b/src/library/layout/locate.rs
@@ -0,0 +1,14 @@
+use crate::library::prelude::*;
+use crate::model::{Group, LocateNode};
+
+/// Format content with access to its location on the page.
+pub fn locate(_: &mut Machine, args: &mut Args) -> TypResult<Value> {
+ let node = LocateNode::single(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<Value> {
+ 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 588b15aa..6cf5b550 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 ac0cbb92..27658189 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -54,6 +54,8 @@ pub fn new() -> Scope {
std.def_node::<layout::ColumnsNode>("columns");
std.def_node::<layout::ColbreakNode>("colbreak");
std.def_node::<layout::PlaceNode>("place");
+ std.def_fn("locate", layout::locate);
+ std.def_fn("group", layout::group);
// Graphics.
std.def_node::<graphics::ImageNode>("image");
@@ -94,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/src/library/prelude.rs b/src/library/prelude.rs
index 371d6776..a61157a7 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/link.rs b/src/library/text/link.rs
index 2ce7a469..12cbaf59 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>("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/library/text/par.rs b/src/library/text/par.rs
index 1269ffed..709dc756 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})"),
}
}
}
@@ -191,10 +194,11 @@ impl LinebreakNode {
/// Range of a substring of text.
type Range = std::ops::Range<usize>;
-// 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.
@@ -275,6 +279,8 @@ enum Segment<'a> {
Spacing(Spacing),
/// An arbitrary inline-level layout node.
Node(&'a LayoutNode),
+ /// A pin identified by index.
+ Pin(usize),
}
impl Segment<'_> {
@@ -284,6 +290,7 @@ impl Segment<'_> {
Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
Self::Node(_) => NODE_REPLACE.len_utf8(),
+ Self::Pin(_) => PIN_REPLACE.len_utf8(),
}
}
}
@@ -301,6 +308,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> {
@@ -318,16 +327,17 @@ impl<'a> Item<'a> {
Self::Text(shaped) => shaped.text.len(),
Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(),
+ Self::Pin(_) => PIN_REPLACE.len_utf8(),
}
}
/// 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 +457,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);
@@ -458,22 +468,27 @@ fn collect<'a>(
ParChild::Quote { .. } => Some('"'),
ParChild::Spacing(_) => Some(SPACING_REPLACE),
ParChild::Node(_) => Some(NODE_REPLACE),
+ ParChild::Pin(_) => Some(PIN_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(PIN_REPLACE);
+ Segment::Pin(idx)
+ }
};
if let Some(last) = full.chars().last() {
@@ -540,6 +555,7 @@ fn prepare<'a>(
items.push(Item::Frame(frame));
}
}
+ Segment::Pin(idx) => items.push(Item::Pin(idx)),
}
cursor = end;
@@ -1171,6 +1187,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/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..21bf8369 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,33 @@ 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);
+
+ // Quit if we're done or if we've had five passes.
+ let unresolved = ctx.pins.unresolved(&prev);
+ if unresolved == 0 || 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 +114,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 +302,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 +420,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 +469,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 +680,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 +702,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..92d73977 100644
--- a/src/model/layout.rs
+++ b/src/model/layout.rs
@@ -221,13 +221,20 @@ impl Layout for LayoutNode {
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> {
- crate::memo::memoized(
- (self, ctx, regions, styles),
+ 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()));
- node.0.layout(ctx, regions, entry.chain(&styles))
+ let result = node.0.layout(ctx, regions, entry.chain(&styles));
+ (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.replay(at, pins);
+ result
}
fn pack(self) -> LayoutNode {
diff --git a/src/model/locate.rs b/src/model/locate.rs
new file mode 100644
index 00000000..97c14034
--- /dev/null
+++ b/src/model/locate.rs
@@ -0,0 +1,342 @@
+use std::fmt::{self, Debug, Formatter};
+use std::sync::Arc;
+
+use super::Content;
+use crate::diag::TypResult;
+use crate::eval::{Args, Array, Dict, Func, Value};
+use crate::frame::{Element, Frame, Location};
+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<Func>, value: Option<Value>) -> LocateNode {
+ LocateNode::entry(self.clone(), recipe, value)
+ }
+
+ /// Do something with all entries of a group.
+ pub fn all(&self, recipe: Spanned<Func>) -> LocateNode {
+ LocateNode::all(self.clone(), recipe)
+ }
+}
+
+impl Debug for Group {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "group({:?})", self.0)
+ }
+}
+
+/// A node that can be realized with pinned document locations.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub struct LocateNode(Arc<Repr>);
+
+impl LocateNode {
+ /// Create a new locatable single node.
+ pub fn single(recipe: Spanned<Func>) -> Self {
+ Self(Arc::new(Repr::Single(SingleNode(recipe))))
+ }
+
+ /// Create a new locatable group entry node.
+ pub fn entry(group: Group, recipe: Spanned<Func>, value: Option<Value>) -> Self {
+ Self(Arc::new(Repr::Entry(EntryNode { group, recipe, value })))
+ }
+
+ /// Create a new node with access to a group's members.
+ pub fn all(group: Group, recipe: Spanned<Func>) -> Self {
+ Self(Arc::new(Repr::All(AllNode { group, recipe })))
+ }
+
+ /// Realize the node.
+ pub fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
+ 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),
+}
+
+/// An ungrouped locatable node.
+#[derive(Debug, Clone, PartialEq, Hash)]
+struct SingleNode(Spanned<Func>);
+
+impl SingleNode {
+ fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
+ let idx = ctx.pins.cursor();
+ 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())
+ }
+}
+
+/// A locatable grouped node which can interact with its peers' details.
+#[derive(Debug, Clone, PartialEq, Hash)]
+struct EntryNode {
+ /// Which group the node belongs to.
+ group: Group,
+ /// The recipe to execute.
+ recipe: Spanned<Func>,
+ /// An arbitrary attached value.
+ value: Option<Value>,
+}
+
+impl EntryNode {
+ fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
+ let idx = ctx.pins.cursor();
+ let pin = ctx.pins.get_or_create(Some(self.group.clone()), self.value.clone());
+
+ // Determine the index among the peers.
+ let index = ctx
+ .pins
+ .iter()
+ .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.
+ let dict = pin.encode(Some(index));
+ let mut args = Args::new(self.recipe.span, [Value::Dict(dict)]);
+
+ // 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))
+ }
+
+ Ok(Content::Pin(idx) + self.recipe.v.call_detached(ctx, args)?.display())
+ }
+}
+
+/// A node with access to a group's members.
+#[derive(Debug, Clone, PartialEq, Hash)]
+struct AllNode {
+ /// Which group the node has access to.
+ group: Group,
+ /// The recipe to execute.
+ recipe: Spanned<Func>,
+}
+
+impl AllNode {
+ fn realize(&self, ctx: &mut Context) -> TypResult<Content> {
+ 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 document pins.
+#[derive(Debug, Clone, Hash)]
+pub struct PinBoard {
+ /// All currently active pins.
+ list: Vec<Pin>,
+ /// The index of the next pin, in order.
+ cursor: usize,
+ /// 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 { list: vec![], cursor: 0, frozen: 0 }
+ }
+
+ /// The current cursor.
+ pub fn cursor(&self) -> usize {
+ self.cursor
+ }
+
+ /// All pins from `prev` to the current cursor.
+ pub fn from(&self, prev: usize) -> Vec<Pin> {
+ self.list[prev .. self.cursor].to_vec()
+ }
+
+ /// Add the given pins at the given location and set the cursor behind them.
+ pub fn replay(&mut self, at: usize, pins: Vec<Pin>) {
+ if !self.frozen() {
+ self.cursor = at + pins.len();
+ let end = self.cursor.min(self.list.len());
+ self.list.splice(at .. end, pins);
+ }
+ }
+
+ /// 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;
+ }
+
+ /// 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);
+ self.cursor = 0;
+ }
+
+ /// Locate all pins in the frames.
+ pub fn locate(&mut self, frames: &[Arc<Frame>]) {
+ let mut flow = 0;
+ for (i, frame) in frames.iter().enumerate() {
+ locate_in_frame(
+ &mut self.list,
+ &mut flow,
+ 1 + i,
+ frame,
+ Transform::identity(),
+ );
+ }
+ }
+
+ /// 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 or create the next pin.
+ fn get_or_create(&mut self, group: Option<Group>, value: Option<Value>) -> Pin {
+ if self.frozen() {
+ return Pin::default();
+ }
+
+ let cursor = self.cursor;
+ 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;
+ pin.value = value;
+ pin.clone()
+ }
+
+ /// Iterate over all pins on the board.
+ fn iter(&self) -> std::slice::Iter<Pin> {
+ self.list.iter()
+ }
+
+ /// Encode a group into a user-facing array.
+ fn encode_group(&self, group: &Group) -> Array {
+ let mut all: Vec<_> = self.iter().filter(|pin| pin.is_in(group)).collect();
+ all.sort_by_key(|pin| pin.flow);
+ all.iter()
+ .enumerate()
+ .map(|(index, member)| Value::Dict(member.encode(Some(index))))
+ .collect()
+ }
+}
+
+/// Locate all pins in a frame.
+fn locate_in_frame(
+ 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_in_frame(pins, flow, page, &group.frame, ts);
+ }
+
+ Element::Pin(idx) => {
+ let pin = &mut pins[*idx];
+ pin.loc.page = page;
+ pin.loc.pos = pos.transform(ts);
+ pin.flow = *flow;
+ *flow += 1;
+ }
+
+ _ => {}
+ }
+ }
+}
+
+/// A document pin.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub 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<Group>,
+ /// An arbitrary attached value.
+ value: Option<Value>,
+}
+
+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<usize>) -> Dict {
+ let mut dict = self.loc.encode();
+
+ 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
+ }
+}
+
+impl Default for Pin {
+ fn default() -> Self {
+ Self {
+ loc: Location { page: 0, pos: Point::zero() },
+ flow: 0,
+ group: None,
+ value: None,
+ }
+ }
+}
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
+ )
}
}