summaryrefslogtreecommitdiff
path: root/src/model/realize.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/model/realize.rs')
-rw-r--r--src/model/realize.rs145
1 files changed, 145 insertions, 0 deletions
diff --git a/src/model/realize.rs b/src/model/realize.rs
new file mode 100644
index 00000000..d63c1aac
--- /dev/null
+++ b/src/model/realize.rs
@@ -0,0 +1,145 @@
+use comemo::Tracked;
+
+use super::{capability, Content, NodeId, Recipe, Selector, StyleChain};
+use crate::diag::SourceResult;
+use crate::World;
+
+/// Whether the target is affected by show rules in the given style chain.
+pub fn applicable(target: &Content, styles: StyleChain) -> bool {
+ // Find out how many recipes there are.
+ let mut n = styles.recipes().count();
+
+ // Find out whether any recipe matches and is unguarded.
+ for recipe in styles.recipes() {
+ if recipe.applicable(target) && !target.is_guarded(Guard::Nth(n)) {
+ return true;
+ }
+ n -= 1;
+ }
+
+ false
+}
+
+/// Apply the show rules in the given style chain to a target.
+pub fn realize(
+ world: Tracked<dyn World>,
+ target: &Content,
+ styles: StyleChain,
+) -> SourceResult<Option<Content>> {
+ // Find out how many recipes there are.
+ let mut n = styles.recipes().count();
+
+ // Find an applicable recipe.
+ let mut realized = None;
+ for recipe in styles.recipes() {
+ let guard = Guard::Nth(n);
+ if recipe.applicable(target) && !target.is_guarded(guard) {
+ if let Some(content) = try_apply(world, &target, recipe, guard)? {
+ realized = Some(content);
+ break;
+ }
+ }
+ n -= 1;
+ }
+
+ // Realize if there was no matching recipe.
+ if let Some(showable) = target.with::<dyn Show>() {
+ let guard = Guard::Base(target.id());
+ if realized.is_none() && !target.is_guarded(guard) {
+ realized = Some(showable.show(world, styles));
+ }
+ }
+
+ // Finalize only if this is the first application for this node.
+ if let Some(node) = target.with::<dyn Finalize>() {
+ if target.is_pristine() {
+ if let Some(already) = realized {
+ realized = Some(node.finalize(already));
+ }
+ }
+ }
+
+ Ok(realized)
+}
+
+/// Try to apply a recipe to the target.
+fn try_apply(
+ world: Tracked<dyn World>,
+ target: &Content,
+ recipe: &Recipe,
+ guard: Guard,
+) -> SourceResult<Option<Content>> {
+ match &recipe.selector {
+ Some(Selector::Node(id, _)) => {
+ if target.id() != *id {
+ return Ok(None);
+ }
+
+ recipe.apply(world, target.clone().guarded(guard)).map(Some)
+ }
+
+ Some(Selector::Regex(regex)) => {
+ let Some(text) = item!(text_str)(&target) else {
+ return Ok(None);
+ };
+
+ let make = |s| {
+ let mut content = item!(text)(s);
+ content.copy_meta(&target);
+ content
+ };
+
+ let mut result = vec![];
+ let mut cursor = 0;
+
+ for m in regex.find_iter(text) {
+ let start = m.start();
+ if cursor < start {
+ result.push(make(text[cursor..start].into()));
+ }
+
+ let piece = make(m.as_str().into()).guarded(guard);
+ let transformed = recipe.apply(world, piece)?;
+ result.push(transformed);
+ cursor = m.end();
+ }
+
+ if result.is_empty() {
+ return Ok(None);
+ }
+
+ if cursor < text.len() {
+ result.push(make(text[cursor..].into()));
+ }
+
+ Ok(Some(Content::sequence(result)))
+ }
+
+ None => Ok(None),
+ }
+}
+
+/// The base recipe for a node.
+#[capability]
+pub trait Show {
+ /// Execute the base recipe for this node.
+ fn show(&self, world: Tracked<dyn World>, styles: StyleChain) -> Content;
+}
+
+/// Post-process a node after it was realized.
+#[capability]
+pub trait Finalize {
+ /// Finalize the fully realized form of the node. Use this for effects that
+ /// should work even in the face of a user-defined show rule, for example
+ /// the linking behaviour of a link node.
+ fn finalize(&self, realized: Content) -> Content;
+}
+
+/// Guards content against being affected by the same show rule multiple times.
+#[derive(Debug, Copy, Clone, PartialEq, Hash)]
+pub enum Guard {
+ /// The nth recipe from the top of the chain.
+ Nth(usize),
+ /// The [base recipe](Show) for a kind of node.
+ Base(NodeId),
+}