Thoughts on auto referencing of math terms, abbreviations, etc

When working on a longer document with a considerable number of math definitions (variables, custom operators, functions) as well as acronyms, I thought it would be wonderful if typst, ofc optionally (feature, package or setting), could automatically, that is, without having to use extra code for every instance, insert (possibly invisible) cross-references to the place of the first (or most recent, if duplicated) definition.

//Page 1
Let $def(x) in bb(R)^2$ be vector in real space (#def[VIR]).

#pagebreak()

//Page 2
$
x^2 &= x^2 + 0 h(1em) forall x in bb(R)^2
$<eq:>

The heavy use of acronyms like VIR in this document
is no problem because I can always click on them
when having forgotten what they are about.

So in this example, on page 2, both [VIR] and $x$ would be clickable, though not necessarily formatted in any special way, allowing the reader to click and jump back to page 1 when looking for the (most recent applicable) definition.

Without having the skills to contribute such feature neither as a package nor within the language, I wondered whether the Typst model would even allow for such behaviour (particularly allowing certain content / formula objects to get special behaviour without explicitly wrapped into some sort of function every time, which I believe would hardly be practicable). I feel this goes into some macro / meta programming direction, so my bet is that such thing is impossible without a core Typst implementation, but I hope I’m wrong and will wait for one day this kind of feature coming into life :slight_smile:

So once you opt in, all defined variables will be linked for the rest of the document even if they are defined many times in isolation? Because x or i is so common that it can be an independent variable many times. The “most recent” kind of auto-fixes this. But what if you need to reference a prior x and not the most recent?

Implicit behavior in Typst is a slippery slope. See Clarify the (intended) use of `show "word": "substitution"` vs. `let word = "supstitution"; word` · Issue #5209 · typst/typst · GitHub. The thing is that WYSIWYG software are based on editing implicit stuff. The WYSIWYM software like Typst is nice because you control everything, and this has a lot of advantages. Implicit references without strictly defined rules is not great, as you don’t know what is liking to where, unlike if you use def(x, "x1") ref("x1").

Revoking rules is cool, and it could be some sort of an escape hatch, but now instead of manually referring to things you will be removing all the previous things. Might be cleaner math blocks, but you will have to keep track of what revokes what and where link will be created and where wouldn’t.

1 Like

Is it possible? Yes, on the general level it is possible in typst because

  • You can apply style to a whole document (like a template does) and this includes things like highlighting words by regex
  • Typst’s state system allows you to collect definitions in a list and yet recall the final state (final as it is at the end of the document) when you are in the template function, i.e your state information can travel back in document time to the start. :joy_cat:

I would probably still start by looking at existing packages that implement an index, not that they have this specific feature but an index seems like the natural way to start with this.

1 Like

Just for fun, I tried seeing if I could implement this and came up with

#let definitions = state("definitions", ())

#let def(it) = {
  definitions.update(def => (..def, it))
  [#it#label(it.text)]
}

#show: body => context definitions.final().fold(body, (body, def) => {
  show def.text: it => link(label(def.text))[#it]
  body
})

It doesn’t work if there are multiple of the same defintion in the same document, but maybe it’s a good place to start

P.S: you can use wide instead of #h(1em) inside math mode :)

2 Likes

I’m impressed as always about the typst community power, so cool to see a working prototype of this, @aarnent, thanks a lot!

Thanks for your thoughts @bluss and @Andrew. I feel like I generally agree with you, Andrew, but yet there are some widely accepted exceptions to this, aren’t there? Like page, heading, etc. numbering that will always depend on what happened before. But I like the idea of revoking rules/commands, and for the valid point on trying to reference not the most recent but a previous variable of the same name (although this would be against my experience in math/stats docs that the reader would have to deal with competing definitions of the same symbol while multiple of them still may apply) I believe an according optional argument could fix this somehow.

@aarnent, would you mind giving me a hint on how to extend the rule in a way that it works with math, too? Currently, it throws an error about math not having a .text field.

I had a go at this too and came up with basically the same approach as @aarnent, though I went a bit further with it:

#let definitions = state("definitions", ())

#let def(body) = context {
  assert(
    // Ensure that definitions are unique.
    definitions.get().all(def => def.element != body),
    message: repr(body) + " has already been defined"
  )
  
  let location = here()
  definitions.update(defs => (..defs, (
    element: [#body],
    location: location
  )))
  
  body
}

#show: it => context {
  definitions.final().fold(it, (body, def) => {
    let elem = def.element
    let selector = if elem.func() == text {
      // Handle text separately as it's likely merged with
      // adjacent text nodes. Note that this also matches
      // if the definition is inside another word, e.g.
      // "cos" matches inside "cosine".
      elem.text
    } else {
      // Probably not as robust as one would hope, but works
      // mostly for simple stuff.
      elem.func().where(..elem.fields())
    }
    
    show selector: it => context {
      if def.location.position() == here().position() {
        // We don't need to link the definition to itself.
        return it
      }
      link(def.location, it)
    }
    
    body
  })
}

There are of course still cases where this won’t work, but for simple cases (like the one in the original post) it’s good enough.

2 Likes

Very cool @Eric, works for math too.

What doesnt currently work yet is something similar to the following:

// Works
$def(overline(x)) defeq sum_(n=1)^N x_n$

#pagebreak()
$overline(x) = 0$
// Doesn't work
#let overlinedx = $overline(x)$

$def(overlinedx) defeq sum_(n=1)^N x_n$

#pagebreak()
$overlinedx = 0$