Conversation
|
Some thoughts:
|
I discussed this in my post above, indeed every heading counts up to its own level. The only other issue I see so far is how to change the default show rule for headings then. You would have to recreate the "count up to my own level". Maybe one could introduce a sub element of
You are absolutely right! In fact, I am the creator of the only package that offers dependent counters (see here) where I have basically implemented this, just with a different API. However, implementing it with
Yes this is just using
No, that's the great point! This is (essentially) not less efficient than counters. If you look at the implementation of counters, you will in fact find that it uses the counting that I'm doing (though only on one level) as a sub step when it computes the counter. Let's think about counting all figures. It works like this: In the document there are the counter update elements, which in 99% of the cases will just do a +1. Imagine it as an array like this: Then Typst looks at all those update elements, and produces a sequence of states after every update. Now when you call Now
No, if I'm not mistaken, |
|
Alright, that's good to hear. In that case I have no further objections. The issue with insanely deep headings is probably best ignored for now. I don't think it's very likely to come up and if it does, we can think about a solution at that time. |
|
I need a bit of time to gather my thoughts before leaving a comment here. |
|
I think it's a very valid observation that "Stepping and getting a counter happen through the same object." is a flaw when viewed through the lens of dependant counters. The main issue I see with the proposed design is that it disconnects different usages of the same counter: There is no way anymore to affect all usages of a specific counter. If I say I'm also a bit concerned by the user-facing complexity problem with deeper headings (conceptually I think your approach makes sense) and the API design questions of the What I'm not sure about yet, is whether it's really not possible to extend the existing |
|
Yeah I can understand your points. I think there is a fundamental dilemma:
However, the "separate state for every counting combination" approach also has their annoyances. As you said, you somehow need to maintain a global list of all counter combinations that the user uses (which grows exponentially), have a state for each of them, and every e.g. I think your problem regarding "reset headings once to zero at it should affect all counters that depend on the heading counter" can be solved. For example there could be a "headings reset here" element/metadata, which would automatically be considered by the But there are still the other little annoyances I mentioned above which would need a nice solution. Ultimately, I just thought that we should go with the idiomatic approach that I sketched, because I always feel like using the non-idiomatic approach will backfire at some point. But yeah, it will need some ideas regarding a good UX design and necessarily everyone's brain will need to adapt to the new approach. |
I think keeping the |
|
It seems like now you want to mix the two approaches.
Is that correct? Wouldn't it be better if there was a unified approach? |
|
I'm not so much thinking in terms of something having an associated state and more just of expanding which counter If we have
I'm not yet sure how the implementation specifically would look (ad-hoc vs precomputed array). We'd have to see which makes more sense. But the above seems implementable to me. |
|
Yeah, updates to counters with multiple keys are forbidden. That's exactly the point I'm trying to make:
That's simply the nature of hierarchical counting. These two things should be separated, and that's what I'm "fighting" for. Now for the "querying" part, I feel like we're on the same page. Let's assume you don't use a precomputed array but do it ad-hoc. At that point, what's the value of the object The other thing I'm suggesting, and I feel like this is what you're concerned about, is how we handle the "step/update/place" part. I feel like you still want something like So I guess we could:
|
I agree with your mental model of counters, I think where we disagree is just the concrete API. As for the API, I see value in keeping the
I mean, we already do that on main. A heading element does not produce a counter update by default. Rather, it is directly considered when resolving |
|
Setting and getting in one place might be good for beginners but I think it could become confusing when you make complicated things, because that is simply not how multi-level counters work "by nature". But more importantly: How exactly would you make the API? Because it seems like you will always need 2 objects and there is (almost) no way around that. For example, take the case where you want have theorems numbered depending on the first heading. Then you will definitely create The only solution I see to this is to allow stepping on multi-key counters, and it will always step the last key. I guess that could work, and fits your API preferences? |
Yeah, that would work I think. |
|
What I still wonder is how all of this would be put into practice when I want to use top-level section numbers in my equations (#2652) without rebuilding equation numbering from scratch. |
|
I don't know how equation numbering works on the inside, but how I would expect/suggest that it works is:
Or what is the problem that I don't see yet? |
|
By the way, just a note for the implementation. When we pass
And another side note. We can't really do |
I disagree. In a call like |
I'd kinda like to avoid adding a |
|
How about letting the |
|
What if I don't want to prepend though? And also, it would be good to have control over the separators. |
You can support both dictionary and pattern string. You need to put an argument somewhere, right; at some point the user needs to transfer the information of how they want it to appear. How do you imagine the API?
Come on, isn't it just |
What I meant was "what if I want to append or do some other thing" rather than preprend vs do nothing. I wasn't entirely clear.
Yeah of course. I just think from a user PoV something like
Perhaps, but I think it's needlessly restrictive. We can manage to come up with a more general design. |
|
I'm optimistic that we'll find a good design for dependent counters, but I'm not really convinced that the proposed |
This adds a new hierarchical
count()function which will both enable dependent counters and simplify the Rust code for counters at the same time.I propose that this mechanism will replace the
counterelement/type, with the reasons and mechanism explained below.This would be a breaking change and proceed in ~3 phases:
countfunction. (this PR)heading,figure, etc. withcountand deprecatecounter. (future big PR)counter.OPEN ME: Motivation and thoughts behind it.
The main issue with current counters is that one cannot define e.g. a theorem counter that inherits the first level of the heading counter.
I thought a lot about how a good counter design should look like, while also being idiomatic/nice/efficient to implement.
I came to the conclusion that the
countertype has the following flaw which hinders it from easily supporting dependent counters: Stepping and getting a counter happen through the same object.A typical math document consists of multiple levels of headings and a counted theorem environment, which however only inherits the first level of the heading counter. So counting-wise it's like a tree: The basis are the headings of level 1, and based on that count both headings of level two (and higher) and theorems. Now if you would want to model this with something like
counter, you would have something likecounter(heading.where(level: 1), heading.where(level: 2)andcounter(heading: where(level: 1), "mytheorem"). This models the "get" side very well, this (rough) syntax describes exactly what the user wants. But the problem is that stepping is tied to a counter, i.e., tied to the "getting". In this case, eachheadingwould need to step both of the counters.The resolution.
Instead, it is naturally the case that stepping and getting happen on different objects.
What you step are the little units, like headings of level 1, or "Theorem", or "Lemma". When you step this, you don't even care in what context it will be counted at some point, you only want to make clear that you insert a new element of something.
Conversely, when you "get"/count, then you are combining a few different of those little units and want to have them counted in a hierarchical way.
So to reflect this in Typst, we should get rid of one combining
counterobject, and split it into one "marking" and "counting". Luckily, we don't even need a special element for "marking", because we already have elements/labels/metadata, which are pretty much exactly that.Then we only need a way to count hierarchically, which is what this PR provides with the
count()function.OPEN ME: The `count` function.
The API of
count()is very simple. You just pass a list of targets (selectors), and it counts them hierarchically.That is, it will first count all the elements of the first selector. Then it will count all the elements of the second selector, but only those which appear after the last element of the first selector. And so on.
So what is currently
counter(heading).get()would then be something like:Now if you want to count your theorems as explained before, just tag them with some label (or in the future they will be elements themselves), and do:
The function returns an array of integers, which can then be displayed using
numbering(...).OPEN ME: But how do I do X and Y then?!
Now this also throws away the
updatefunction. But this is not a problem! I have seenupdateused in only two ways:With
count()you can achieve the same thing, in a semantically much better way. Just put a metadata/label somewhere to indicate that the appendix begins (e.g.metadata(()) <appendix_start>), and then usecount(after: <appendix_start>, ...).state.Another question is maybe what the default show rule of
headingwill be, because it somehow has infinitely many levels butcount()can only take finitely many arguments. The answer is, the show rule for a heading of levelkwill roughly incorporatecount(heading.where(level: 1), ..., heading.where(level: k)). There is still the issue with ignoring headings that don't want to be numbered, that's discussed below.Now to the issues / points of discussion / todos.
headings/figures/etc. which havenumbering != none? You would want a selectorcount(figure.where(numbering !: none). I guess one could introduce this syntax, but that would be pain on adapting parsing (I think right now it's just parsed as a dict). Or use a syntax likefigure.wherenot(numbering: none).Another solution would be to introduce a new boolean field
countedwhich is automatically computed from whethernumberingis something ornone.For example, if you want to make an appendix as explained above, you don't want to manually change the show rule of heading with code that lists the selectors up until the level of the heading. One option would be to introduce a new field on heading called
count-afterwhich is passed ascount(after: this.count-after, ...)in the default show rule. But there are also other solutions, I hope we can find a better one.get()vsat()forcounter) By default until the current location, and have an optional parameter forat/before?before/afterinclusive/exclusive?at/beforeargument)Let me know what you think! I know the issues seem annoying, and indeed they are, but thinking about this for so long made it clear to me that there definitely needs to be a separation of objects between stepping and counting!