Here is a recursion blocking function - can we extend it to block by element identity?

Here is a recursion blocking function, that could be useful when you want to block recursion in for example show rules that create new elements.

This is based on elembic’s main device - which was explained in a different thread (Elembic 1.0.0: Custom elements and types! )

See the code below. It works! At least I hope so - tell me if you see any problems. What it needs to do its job is a unique label name that nobody else uses for the same thing.

Here is the question though: Can we extend this mechanism so that we allow some recursion but not all? We want to be able to recurse and apply the same rule to nested tables. But we want to block recursion when trying to style or change a table or paragraph we have already modified.

The Code & Example
/// - how (str): is one of `"marker"` and `"identity"`
#let recursion-block(markerlabel, func, how: "marker") = body => {
  let sentinel = if how == "marker" {
    [#str(markerlabel)]
  } else {
    panic("unknown 'how':" + repr(how))
  }
  context {
    let orig-title = bibliography.title
    [#context {
      let ctx-title = bibliography.title
      set bibliography(title: orig-title)
      if ctx-title != sentinel {
        show markerlabel: set bibliography(title: sentinel)
        func(body)
      } else {
        body
      }
    }#markerlabel]
  }
}


// I can add text to paragraphs..
#show par: recursion-block(<__par_recursion>, it => {
  par(it.body + text(red)[ (cheekily modified this paragraph)])
  let a = context { [#bibliography.title] }
  let b = [#context { [#bibliography.title] }<__par_recursion>]
  //[(for debugging: #a and #b)]
})


#lorem(10)

#lorem(20)

// I can make all table strokes red..
#show table: recursion-block(<__table_munge>, tab => {
  let fields = tab.fields()
  let ch = fields.remove("children")
  table(..fields, ..ch, stroke: 1pt + red)
})

Note how the nested tables are not modified:

#table[A][B]

#table(columns: 2,
  table[A][B],
  table[A][B],
)

I don’t think the label and contexts are required (but maybe I’m missing something). Here’s a simplified version that seems to work and allows disabling only the first recursion:

#let recursion-block(name, func, it) = {
  let blocked = (:)
  let title = bibliography.title
  if type(title) == content and title.func() == metadata {
    blocked = title.value
  }
  if blocked.at(name, default: false) {
    blocked.insert(name, false)
    return {
      // Re-enable recursion for deeper elements
      set bibliography(title: metadata(blocked))
      it
    }
  }
  blocked.insert(name, true)
  set bibliography(title: metadata(blocked))
  func(it)
}

#show par: recursion-block.with("par-recursion", it =>
  par(it.body + text(red)[ (cheekily modified this paragraph)])
)


#lorem(10)

#lorem(20)

#show table: recursion-block.with("table-recursion", tab => {
  let fields = tab.fields()
  let ch = fields.remove("children")
  table(..fields, ..ch, stroke: 1pt + red)
})

#table[A][B]

#table(columns: 2,
  table[A][B],
  table[A][B],
)

Edit: I just realized I don’t bother restoring the original bibliography.title… I’m not sure it’s required in practice though?

1 Like

Thank you for that, that seems to solve it!

I think this idea is working:

// Re-enable recursion for deeper elements
      set bibliography(title: metadata(blocked))

and in the function I posted the equivalent is this:

} else {
  show markerlabel: set bibliography(title: orig-title)
  body
}

I appreciate the simplification but I think the part you removed - smuggling the bibliography.title setting by setting it only for a specific context - the label - is necessary to make this fully interoperable with other code and other packages. As this was inspired by elembic - they don’t just borrow bibliography.title, they do it in such a way that it’s not noticeable.

Multiple cheeky abuses of the same bibliography.title can happen interleaved with each other if we smuggle this correctly, that’s what it looks like.

Simpler explanation (speaking to everyone): we can use the regular style chain to store information this way, in this case in bibliography.title keyed under a unique label, and the label means that only those that bother looking under that label will find the information.


Edited: Here’s an updated version that incorporates that, and also a newly learned thing:

The show rule produces func(body) - a styled element or a new element. We can’t store and check that. When we first revisit an element we receive just body and we can store that because it comes directly from the document.

So we combine the two approaches - a simple sentinel string to detect first recursion, and an array of already seen elements.

  • We already solved the test case in the first post
  • This also solves the question in the title (“by element identity”). This guards against global reentrancy, let’s say you apply the same style rule twice for some reason, which can happen when multiple packages do the same thing…
Updated Code

/// Take an element and replace it. The field `_positional` will be positional, and it can be a single thing or an array; _map is applied to each element of this array.
#let replace-elt(element, ..args, _positional: "body", _map: x => x) = {
  let fields = element.fields()
  let body = fields.remove(_positional)
  if type(body) != array { body = (body, )}
  (element.func())(..fields, ..args, ..body.map(_map))
}

#let append-to-marker(mark, body) = {
  let arr = ()
  if type(mark) == content and mark.func() == metadata {
    arr = mark.value
  }
  arr.push(body)
  metadata(arr)
}

#let contained-in-marker(mark, body) = {
  let arr = ()
  if type(mark) == content and mark.func() == metadata {
    arr = mark.value
  }
  body in arr
}

/// Apply a style function, like for a show rule, but block recursive application of it
///
/// - markerlabel (label): unique label for this style function
/// - func (function): the style function; signature function(content) -> content
/// - body (content): the style function argument
/// - allow-nesting (bool): allow nesting of the same show rule, after having
///   blocked repeated calls on the same element once. If you say false here,
///   then no recursion at all is allowed (only outermost table of nested
///   tables are affected by a table rule for example). If you say true here,
///   nested tables are affected in this example.
#let recursion-block(markerlabel, func, body, allow-nesting: false) = {
  let sentinel = [#str(markerlabel)]
  context {
    let orig-title = bibliography.title
    [#context {
      let ctx-title = bibliography.title
      set bibliography(title: orig-title)
      if ctx-title != sentinel and (not allow-nesting or not contained-in-marker(ctx-title, body)) {
        // set simple recursion marker, just the sentinel
        show markerlabel: set bibliography(title: sentinel)
        func(body)
      } else {
        // we arrived back at something we have seen before
        // store the whole element in the marker this time
        // this guards against global reentrancy (if we have multiple instances of the same rule)
        // note: we can store `body` because it comes directly from the document.
        // storing `func(body)` from above would match nothing we see again - the document element that
        // results from `func(body)` is not exactly equal to that.
        let marker = append-to-marker(ctx-title, body)
        show markerlabel: set bibliography(title: marker) if allow-nesting
        body
      }
    }#markerlabel]
  }
}

// I can add text to paragraphs..
#show par: recursion-block.with(<__par_recursion>, it => {
  replace-elt(it, _map: body => body + text(red)[ (cheekily modified this paragraph)])
  let a = context { [#bibliography.title] }
  let b = [#context { [#bibliography.title] }<__par_recursion>]
  block[(debugging: inside par, bibliography.title=#a and #repr(<__par_recursion>)bibliography.title=#b )]
})


Paragraph one

Paragraph two



// I can make all table strokes red..
#show table: recursion-block.with(<__table_recursion>, allow-nesting: true, tab => {
  replace-elt(tab, stroke: 1pt + red, _positional: "children")
  
  let a = context { [#bibliography.title] }
  let b = [#context { [#bibliography.title] }<__table_recursion>]
  //block[(debugging: inside table, bibliography.title=#a and #repr(<__table_recursion>)bibliography.title=#b )]
})


== Tables

Note how the nested tables *are now included, thanks sijo!*

#table[A][B]

#table(columns: 2,
  table[C][D],
  table[E][#table[F][Nested paragraph#parbreak()]],
)



== Grids


// I can make all grid strokes blue..
#let blue-grids = recursion-block.with(<__grid_recursion>, allow-nesting: true, tab => {
  replace-elt(tab, stroke: blue + 2pt, _positional: "children")
  
  let a = context { [#bibliography.title] }
  let b = [#context { [#bibliography.title] }<__grid_recursion>]
  //block[(debugging: inside grid, bibliography.title=#a and #repr(<__grid_recursion>)bibliography.title=#b )]
})

// just for the example, apply the rule twice..
#show grid: blue-grids
#show grid: blue-grids


#grid(inset: 1em, fill: yellow)[A][B][
  #grid(inset: 1em)[#grid(inset: 0.5em)[M][N]][#grid(inset: 0.5em)[P][Q]]
]
1 Like