Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-syntax] Make it easier to express ranges for <An+B> selectors #4140

Open
SebastianZ opened this issue Jul 22, 2019 · 27 comments
Open

[css-syntax] Make it easier to express ranges for <An+B> selectors #4140

SebastianZ opened this issue Jul 22, 2019 · 27 comments
Labels

Comments

@SebastianZ
Copy link
Contributor

To express the selection of the first or last n elements the An+B syntax is somewhat hard to understand. It requires the author to calculate the different resulting index values and requires them to know that negative indexes are ignored.

Therefore, I suggest to introduce a simpler syntax that covers those cases. My suggestions:

to n or n <= x

Positive side effect is that these syntaxes would allow to extend them to specify ranges, i.e. m to n or x <= n <= y. With that there wouldn't be the need anymore to define the ranges using both the :nth-child() and :nth-last-child() pseudo-classes resp. :nth-of-type() and :nth-last-of-type().
The latter also aligns with the new range syntax introduced for media queries.

Sebastian

@tabatkins
Copy link
Member

While that allows a range, it doesn't work with values other than 1n, right? (Particularly if they need to be offset from 0.)

I'm not opposed to that, tho; you can always write two pseudo-classes, one setting up the skip and the other setting up the range.

If we were to do this, using the MQ syntax, with <=/etc, sounds best.

@Loirooriol
Copy link
Contributor

For other values we could also consider something like An+B, n <= C.

:nth-child(An+B <= C) seems less useful and can just be :nth-child(An+B):nth-child(n <= C).

But :nth-child(An+B, n <= C) would be like :nth-child(An+B):nth-child(n <= A*C+B) or :nth-child(An+B):nth-child(n >= A*C+B) depending on the sign of A. The former would free authors from doing the math.

@SebastianZ SebastianZ changed the title Alternative syntax for <An+B> in selectors Make it easier to express ranges for <An+B> selectors Jul 31, 2019
@SebastianZ
Copy link
Contributor Author

My main use case was to make it easier to express the first or last n elements, but if we want to allow values other than 1n and offsets with this new range syntax, that's easily doable as @Loirooriol already pointed out. It would then be

C <= An+B <= D

But :nth-child(An+B, n <= C) would be like :nth-child(An+B):nth-child(n <= A*C+B) or :nth-child(An+B):nth-child(n >= A*C+B) depending on the sign of A.

This comma-separated syntax is rather confusing to me, because n <= C actually means n <= A*C+B.

The former would free authors from doing the math.

It makes it easier if you want to your selector to apply exactly C times, though it doesn't allow to restrict the selection to index C. The question here is what's the more common use case.

Sebastian

@Loirooriol
Copy link
Contributor

But as you said, one of the non-trivial aspects of An+B is that n takes non-negative values, and somebody may want to change this range, e.g. start at 1 instead of 0. You cannot do this with 1 <= An+B, but you could with An+B, 1 <= n.

n <= C actually means n <= A*C+B

No, n <= C means n <= C. But you are not selecting the n-th element, you are selecting the An+B one.

@SebastianZ
Copy link
Contributor Author

SebastianZ commented Aug 2, 2019

Ok, so let's make up some examples with your and my syntax for clarification:

break down by example

Match the first three elements

Match the first three elements
current syntax :nth-child(-n+3)
first proposed syntax :nth-child(n <= 3)
second proposed syntax :nth-child(n <= 3)
filter syntax :nth-child(first 3)

Match every even element up to the eighth
/ match every even element up to four

Match every even element up to the eighth
/ match every even element up to four
current syntax :nth-child(even):nth-child(-n+8)
first proposed syntax :nth-child(even <= 8)
second proposed syntax :nth-child(2n, n <= 4)
filter syntax :nth-child(2n, first 4)

Match every third element between the sixth and the fifteenth
/ match every third element up to four starting at the sixth

Match every third element between the sixth and the fifteenth
/ match every third element up to four starting at the sixth
current syntax :nth-child(3n+6):nth-child(-n+15)
first proposed syntax :nth-child(6 <= 3n <= 15)

:nth-child(3n):nth-child(6 <= n <= 15)
second proposed syntax :nth-child(3n+6, n < 4)

:nth-child(3n, 2 <= n <= 5)

:nth-child(3n):nth-child(6 <= n <= 15)
filter syntax :nth-child(3n, skip 1, first 4)

Match every third element starting with the second one and ending at the third last

Match every third element starting with the second one and ending at the third last
current syntax :nth-child(3n+2):nth-last-child(n+3)
first proposed syntax :nth-child(2 <= 3n+2 <= -3)

:nth-child(3n+2):nth-child(2 <= n <= -3)

:nth-child(2 <= 3n+2 <= last-3) (using last keyword)
second proposed syntax -
filter syntax :nth-child(3n, not first 2, not last 2)
full table
matching current syntax first proposed syntax second proposed syntax filter syntax
Match the first three elements :nth-child(-n+3) :nth-child(n <= 3) :nth-child(n <= 3) :nth-child(first 3)
Match every even element up to the eighth / match every even element up to four :nth-child(even):nth-child(-n+8) :nth-child(even <= 8) :nth-child(2n, n <= 4) :nth-child(2n, first 4)
Match every third element between the sixth and the fifteenth / match every third element up to four starting at the sixth :nth-child(3n+6):nth-child(-n+15) :nth-child(6 <= 3n <= 15)

:nth-child(3n):nth-child(6 <= n <= 15)
:nth-child(3n+6, n < 4)

:nth-child(3n, 2 <= n <= 5)

:nth-child(3n):nth-child(6 <= n <= 15)
:nth-child(3n, skip 1, first 4)
Match every third element starting with the second one and ending at the third last :nth-child(3n+2):nth-last-child(n+3) :nth-child(2 <= 3n+2 <= -3)

:nth-child(3n+2):nth-child(2 <= n <= -3)

:nth-child(2 <= 3n+2 <= last-3) (using last keyword)
- :nth-child(3n, not first 2, not last 2)
-->

The last example additionally introduces negative values to count from the last element. One approach is to just use negative numbers (like for Grid Lines). Or, because of the mathematical incorrectness, there could be an additional last keyword, which looks quite readable, IMHO.

Sebastian

@tabatkins
Copy link
Member

I really don't like the syntax that puts the condition on the N; the fact that N goes over 0-Inf is already kinda confusing. I strongly prefer mixing the conditions into the An+B condition, so you filter the outputted element indexes.

For dealing with counting from the end, I think we should just allow negatives. last-3 gives the parser hives (that's a single ident token). It looks a little weird, yeah, but it's familiar from other languages like Python that let you do an array slice like arr[2:-3] (and where "reversed" slices like arr[3:2] do indeed yield nothing).

@Crissov
Copy link
Contributor

Crissov commented Oct 16, 2019

:nth(An+B) can be seen as a shorthand notation of :nth(0 < A*n + B) or :nth(A*n + B > 0), which are equivalent to :nth(A*n > -B) and :nth(-A*n < B).

The variable n is determined by the pseudo-class for existing applications, but in some other scenarios, e. g. :heading-level() #1008, it would make sense to manually provide or restrict its valid values. Both proposals therefore seem reasonable, but for different use cases. Instead of an equality relationship, n could also be restricted by optional minimum and maximum.

  • <n-range> :=
    1. <An+B> Level 3
    2. <An+B-limits> @SebastianZ
    3. <An+B> <n-limits>? @Loirooriol
    4. <An+B-limits> <n-limits>?
  • <An+B-limits> :=
    1. <ceil-left>? <An+B> <floor-right>?
    2. <floor-left>? <An+B> <ceil-right>? @SebastianZ
    3. [<floor-left>? <An+B> <ceil-right>?] | [<ceil-left>? <An+B> <floor-right>?]
    4. <An+B> [to <number>]? @SebastianZ (initial post)
  • <n-limits> :=
    1. , <number> [, <number>]?
    2. , <number>+ | [<number>, <number>]
    3. [[for <number>]? [to <number>]?]!
    4. [for <number-list>] | [[for <number>]? to <number>]
    5. , [<floor-left>? n <ceil-right>?]! | [<ceil-left>? n <floor-right>?]!
    6. , [<floor-left>? n <ceil-right>?]! | [n <in> <number-list>]
  • <floor-left> := last? <number> <less>
  • <ceil-left> := last? <number> <greater>
  • <floor-right> := <greater> last? <number>
  • <ceil-right> := <less> last? <number>
  • <less> :=
    1. '<'
    2. '<' | '=<'
    3. '<' | '<=' MQ
    4. '<' | '=<' | '<='
    5. lesser
  • <greater> :=
    1. '>'
    2. '>' | '=>'
    3. '>' | '>=' MQ
    4. '>' | '=>' | '>='
    5. greater
  • <in> :=
    1. in
    2. '='
  • <number-list> :=
    1. <number>+
    2. <number>#

@tabatkins
Copy link
Member

but in some other scenarios, e. g. :heading-level() #1008, it would make sense to manually provide or restrict its valid values.

What do you mean by this? I don't think :heading() leans either way on whether it wants to clamp N (input) or result. (:heading() doesn't really have a reason to want the An+B term at all, just n and B.)

@Crissov
Copy link
Contributor

Crissov commented Oct 17, 2019

I meant that the number of heading levels (and thus the number of possible values of n) is usually limited globally by specifications of markup languages, whereas n in :nth-…() is open-ended in general, determined locally by the actual document tree. CSS by itself cannot know which values could be valid inside :heading(), so <An+B> alone makes no sense indeed – authors need to specify a valid range or set, which could be filtered further by an <An+B> expression, although this would almost always be clumsy and unnecessary.

@Loirooriol
Copy link
Contributor

@SebastianZ In your example table your proposal seems to fit much better to the description. But that's because all the cases are expressed in the same way, the one which works better with your proposal. But as @Crissov says the proposals represent different use cases, or maybe different ways of thinking.

Then :nth-child(2n <= 8) represents "Match every even element up to the eighth", but :nth-child(2n, n <= 4) means "Match the first 4 even elements".

@tabatkins "the fact that N goes over 0-Inf is already kinda confusing". Precisely, if we put a condition on n, authors can override this confusing behaviour with another one that makes more sense for them. Adding conditions on the result of an+b would presumably keep n >= 0, so it may become even more confusing.

On the other hand counting from the end seems interesting and I agree that it seems more suited for constraints on an+b than on n.

@tabatkins
Copy link
Member

I think giving them control over the N values would be even worse; the 0-Inf, while confusing, makes the intuitive 3n+1-style patterns work properly!

Anyway, stated more plainly:

  • I think having explicit range control is useful
  • the ways to write ranges today are confusing and non-obvious
  • we should only add one way to write ranges
  • I think applying the range to the output hits the expected most-common use-cases more cleanly.

@SebastianZ
Copy link
Contributor Author

... But as @Crissov says the proposals represent different use cases, or maybe different ways of thinking.

Then :nth-child(2n <= 8) represents "Match every even element up to the eighth", but :nth-child(2n, n <= 4) means "Match the first 4 even elements".

Right, those are two different ways of thinking. My approach focuses on the indexes of the elements while your one focuses on the number of elements to be styled.
So @Crissov is right, both are legitimate and cover different use cases.

@tabatkins "the fact that N goes over 0-Inf is already kinda confusing". Precisely, if we put a condition on n, authors can override this confusing behaviour with another one that makes more sense for them. Adding conditions on the result of an+b would presumably keep n >= 0, so it may become even more confusing.

I believe the fact that n is in the range of 0 to infinity is not the issue here. People knowing other languages already know this from array indexes. The hard part of the current syntax is that n isn't the actual index and you have to calculate the index using it.

Sebastian

@SebastianZ
Copy link
Contributor Author

SebastianZ commented Oct 20, 2019

My third example seemed to be wrong. I've corrected it now. And I have also added descriptions that fit @Loirooriol's syntax.

As this issue is related to the An+B microsyntax, I've now also added [css-syntax] to the subject.

Sebastian

@SebastianZ SebastianZ changed the title Make it easier to express ranges for <An+B> selectors [css-syntax] Make it easier to express ranges for <An+B> selectors Oct 20, 2019
@fantasai fantasai added the selectors-4 Current Work label Mar 11, 2020
@SebastianZ
Copy link
Contributor Author

After some great discussion, this feature request unfortunately fell asleep. So let's wake it up again!

Just a quick summary of the two proposals we had so far:

  • My one focuses on the resulting element indexes and borrows its syntax from Media Queries. Basically, it allows to define a minimum and maximum index and looks like this: C <= An+B <= D (with possible variations like in the Media Queries syntax)
    With C and D allowing for negative values, meaning to count the index from the end.
  • @Loirooriol's focuses on the maximum number of elements to style and by that on the n itself. This syntax looks like this: An+B, n <= C

I think the use cases are clearly expressed and the two approaches as well. So maybe we can discuss that on the F2F.

Sebastian

@Loirooriol
Copy link
Contributor

I have updated the table since my proposal can also accept min and max values, they just refer to n. And if the expression is n, it seems omittable.

@SebastianZ
Copy link
Contributor Author

@Loirooriol Thank you for updating the table!

I've also updated it now to show how counting from the last element could be added.

Sebastian

@Loirooriol
Copy link
Contributor

BTW, I think omitting last like :nth-child(2 <= n <= -3) is terribly confusing.
Using negative numbers to refer to the end already seems kinda bad in css-grid, but at least there it doesn't happen in the middle of a math expression. This seems so unexpected.

@tabatkins
Copy link
Member

Negative numbers counting from the end of a sequence is a pretty common idiom in a number of programming languages, including JS ([1,2,3].at(-1), for example).

but :nth-child(2n, n <= 4) means "Match the first 4 even elements".

Note that you can already write this today, with :nth-child(-n + 4 of :nth-child(2n)). And with the updated range syntax from Sebastain, it would be :nth-child(n <= 4 of :nth-child(2n)), which is fairly reasonable imo. This is how you write every other "I want the first 4 things matching X condition" selector; I don't think we really need a special-case form for when the condition is specifically an An+B counter.

:nth-child(2 <= 3n+2 <= last-3)

I think using last and negative values is more confusion than it's worth (plus, as written, that's a single last-3 ident). I'd prefer

:nth-child(2 <= 3n+2 <= last 3)

(I'm also fine with just using a negative number on its own, but if we go with the keyword I prefer this syntax.)

@jimmyfrasche
Copy link

Perhaps it would be clearer to have a simpler syntax in its own selector, like: :bikeshed(<range> [of <sel>])?

The An+B syntax is complicated enough on its own, and the of-clause lets more complicated ranges be expressed in a readable form.

@FremyCompany
Copy link
Contributor

FremyCompany commented Jul 19, 2023

My proposal would be to add a list of filters, instead of ranges:
:nth-child(first 3), :nth-child(last 3)
:nth-child(2n, first 3), :nth-child(2n, last 3) # last 3 even children

We could optionally add a skip operator:
:nth-child(2n+1, skip 1, last 1) # the last odd child, if there is at least two

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-syntax] Make it easier to express ranges for <An+B> selectors, and agreed to the following:

  • RESOLVED: Work on the problem of expressing ranges of children in selectors in a more intuitive way.
  • SUMMARY: Will discuss syntax further in issue.
The full IRC log of that discussion <dbaron> SebastianZ: I previously suggested a new syntax for expressing An+B selectors in :nth-child() etc.
<dbaron> SebastianZ: We were discussing this, we had 2 proposals. They seem orthogonal; we could have both. But I think one I suggested initially is targeting the most common use cases.
<TabAtkins> q+
<dbaron> SebastianZ: Tab was also mentioning some things.
<dbaron> SebastianZ: reusing the range syntax from Media Queries, with one addition: adding a "last" keyword to count from the last element instead of the first.
<dbaron> TabAtkins: The problem we're trying to solve is that right now a selector targeting a range of elements (e.g., 3rd through 7th), it's possible to write using two :nth-child()s, but it's not obvious how, and tricky.
<dbaron> TabAtkins: So the first thing we should agree on is whether to agree on developing a syntax to enable expressing ranges more easily.
<dbaron> TabAtkins: And if so we have some suggestions to go over and possibly decide between.
<dbaron> TabAtkins: I think it's a reasonably common case (e.g., "select the first 3 elements")
<Rossen_> ack TabAtkins
<SebastianZ> See also https://github.com//issues/4140#issuecomment-517857973
<dbaron> TabAtkins: you have to write :nth-child(3-n) or :nth-child(-n+3). I'd prefer n <= 3, like you'd write elsewhere.
<dbaron> TabAtkins: do we think this is worth adding to selectors?
<dbaron> florian: yes
<miriam> +1
<oriol> +1
<dbaron> fantasai: not sold on the syntax, but seems reasonable to come up with something
<fremy> I have written code that would have benefited from this, so sympathetic
<dbaron> +1 to working on the problem
<dbaron> RESOLVED: Work on the problem of expressing ranges of children in selectors in a more intuitive way.
<emilio> q+
<dbaron> TabAtkins: a few suggestions for syntax.
<dbaron> TabAtkins: oriol suggested a different approach from what TabAtkins and SebastianZ are thinking about.
<dbaron> TabAtkins: If you're expressing a range, do you want a range on the "n' variable or a range on the output?
<dbaron> TabAtkins: You want a range on the output rather than the input.
<oriol> q+
<dbaron> TabAtkins: If your range was 2n and your range is 1-5, are you selecting 2,4,6,8,10 or 2,4 ?
<fantasai> TabAtkins: In this case you want 2,4
<fremy> can you type this, @tabatkins
<TabAtkins> 2n <= 5, for example, will return 2,4
<TabAtkins> if a "<= 5" condition can return 6,8,10 I think that's super weird
<astearns> range-limited output?
<dbaron> TabAtkins: this ties in to what syntax we're using
<dbaron> TabAtkins: I think if you see 2n <= 5, you expect all the values of 2n that result in a value <= 5.
<fremy> q?
<fremy> q+
<dbaron> emilio: Before anything else, can we agree on making it part of :nth-child() rather than inventing new pseudo-classes. I think it makes sense as part of :nth-child().
<Rossen_> ack emilio
<dbaron> TabAtkins: I think basically building this into An+B is good.
<Rossen_> ack oriol
<myles> q+
<dbaron> oriol: I prefer my proposal -- when I started ??? with the An+B syntax, I first assumed that "n" would be strictly positive, then I thought all integers -- there's an implicit condition that "n" is nonnegative. I think this implicit condition may be confusing. I was thinking my proposal adding an explicit condition to n could make things more clear.
<fremy> (I'm in the queue for a different topic, so I would not want to interrupt the decision on ruling out new pseudo-classes)
<fremy> q- later
<dbaron> oriol: That said, it may be true that the other proposal maybe covers more frquent cases in a shorter way.
<dbaron> oriol: You can convert from the other proposal to mine by adding another :nth-child() pseudo-class. I'm not opposed to the other one.
<dbaron> oriol: I think this is a good point covers ???.
<Rossen_> q?
<dbaron> s/covers/that my proposal covers/
<TabAtkins> my actual proposed syntax kinda splits the difference tho, I suppose: ranges must be simple, just a number and n. So `:nth-child(n <= 5)` only, not `2n <= 5`. If you want to both, `:nth-child(n <= 5):nth-child(2n)` or `:nth-child(n <= 5 of 2n)`
<dbaron> myles: I think we've been reinventing list comprehensions that are in many proper prorgramming languages. We should take something that already exists.
<dbaron> TabAtkins: This is reinventing the range operators that some operators... different syntax space from list comprehensions.
<dbaron> myles: I'm not saying to dump python's list comprehensions in CSS -- but I'm saying for input vs. output do what the programming languages do.
<dbaron> TabAtkins: I disagree with that on principle -- it's more like a range than a list comprehension.
<Rossen_> ack myles
<dbaron> TabAtkins: the syntax affects whether your ranges are on the input on the output.
<dbaron> TabAtkins: Different languages make map-then-filter or filter-then-map easier, and I don't think there's a clear reason to prefer one based on precedent.
<SebastianZ> q+
<dbaron> TabAtkins: my preferred syntax proposal makes this moot. Rather than full An+B combined with ranges is that when we do ranges, you're limited to N on one side, a number on one side , and a sign in the middle. IF you want An+b stuff as well, you can do the same ?? as if you're combining 2 conditions together. Not ??? An ???. I proposed we only allow n <= 5. You can decide whether you allow the first 5 children that are also
<Rossen_> q?
<dbaron> ... even or the first five even children, depneding on how you write it.
<dbaron> hober: If I understand, I think I agree.
<TabAtkins> `:nth-child(n <= 5):nth-child(2n)` or `:nth-child(n <= 5 of 2n)`
<dbaron> TabAtkins: We can rely on existing CSS facilities.
<ntim_> q+
<ntim_> +1 to fremy
<dbaron> fremy: I think we're not going to have consensus today. But I want say I agree with following other languages. I think we shuold have multiple filters, applied in order author wrote them. I find it hard to understand "2n+3 < 5", no idea what it selects. I think we ned something more clear.
<dbaron> fremy: :nth-child(2n) is for the even things, and then say first three.
<dbaron> fremy: I think this reads better than the mathematic formula.
<dbaron> fremy: I think 2n+1<5 is too difficult to understand.
<ntim_> I agree 2n+5 < 5 is hard to understand
<Rossen_> ack SebastianZ
<Rossen_> ack fremy
<dbaron> SebastianZ: I want to note first proposed syntax also involves "last" keyword as said in beginning, which allows counting from the end. From 3 rd element to 3rd-last element, style everything. Wouldn't be psosible with second proposed syntax.
<dbaron> TabAtkins: We could have negative indexes mean what they do in most programming languages.
<dbaron> fantasai: but we don't do that now
<fantasai> Like, I really love negative indexes representing from the end. But we don't do that now, and so I think it would be confusing to have it work fo rome things and not others
<plinss> we do use it in grid
<dbaron> SebastianZ: With the proposal of the "last" keyword we can also express ranges to an nth-last child. Tab said probably possible with second proposed syntax, but I don't think possible, but I disagree, since we're there focusing on the "n" which means the number of elements.
<TabAtkins> once we drain the queue, i propose we take this back to the issue for syntax discussion
<Rossen_> q?
<dbaron> oriol: with my proposal "n" is just the ? in the expression
<astearns> s/?/entity
<dbaron> oriol: Going from the index of the last child ..
<dbaron> oriol: if we want to express conditions to the end, constrain on the output seems more suited
<fantasai> plinss, good point :) but not in :nth-child(), and in fact some of the :nth-child() patterns rely on us not processing negative indexes :/
<dbaron> ntim_: +1 to fremy about 2n+1 < 5 being hard to understand.
<dbaron> ntim_: hope we don't end up with that
<Rossen_> ack ntim_
<Rossen_> ack fantasai
<fremy> Can I suggest maybe to do like we did before in such cases, make a list of use cases, and then write how each proposal writes that range?
<dbaron> fantasai: I was going to suggest table discussion until friday. Tomorrow we'll have time for people to workshop ideas. We should do that tomorrow.
<dbaron> TabAtkins: This is going too long, we should go back to the issue.
<myles> ScribeNick: myles
<dbaron> SUMMARY: Will discuss syntax further in issue.

@tabatkins
Copy link
Member

We did a breakout at the WG meeting, and participants united behind the following proposal:

  • <an+b> syntax is extended to <current-syntax> | <range-syntax>, where range syntax is n [ < | <= | = | => | > ] <int> or the same with int and N swapped.
  • The n in the range syntax is literal, it's not a variable or a more complicated An+B value.

So the examples in the table could be written as:

  • Match the first three elements: :nth-child(n >= 3)
  • Match every even element up to the eighth / match every even element up to four: either :nth-child(n <= 4 of :nth-child(even)) or :nth-child(n <= 8):nth-child(even)
  • Match every third element between the sixth and the fifteenth / match every third element up to four starting at the sixth: either :nth-child(6 <= n <= 15):nth-child(3n) or (several additional ways depending on how you want to express it)
  • Match every third element starting with the second one and ending at the third last: considerable disagreement on what this actually meant, but if it's "of the 3n elements, take the 2nd to the 3rd last", then :nth-last-child(n >= 3 of :nth-child(n >= 2 of :nth-child(3n))). Other interpretations can also be written, generally slightly simpler.

@bramus
Copy link
Contributor

bramus commented Jul 20, 2023

In addendum to tab’s reply above: Using negative numbers to count back from the end in such an expression also came up (e.g. 2 < n < -3), but is punted for the time being because negative numbers are currently not part of the current <an+b> syntax. Might be food for a follow-up issue.

@FremyCompany
Copy link
Contributor

FremyCompany commented Jul 21, 2023

Hi @tabatkins, I was not in the breakout, but added the filter proposal to the tables.

In my opinion it reads way more naturally than the ranges, so I would like to see it discussed at least, especially since I proposed it before the breakout :) I would not object to a consensus from the group of course, but I don't know how many people made it to the breakout in the end, and if they saw my comment.

I just did mini tables based on the consensus proposal:

Examples (comparison)

Match the first three elements

Match the first three elements
breakout syntax :nth-child(n <= 3)
filter syntax :nth-child(first 3)

Match every even element up to the eighth
/ match every even element up to four

Match every even element up to the eighth
/ match every even element up to four
breakout syntax :nth-child(n <= 4 of :nth-child(even)) or :nth-child(n <= 8):nth-child(even)
filter syntax :nth-child(even, first 4) or :nth-child(first 8, even)

Match every third element between the sixth and the fifteenth
/ match every third element up to four starting at the sixth

Match every third element between the sixth and the fifteenth
/ match every third element up to four starting at the sixth
breakout syntax :nth-child(6 <= n <= 15):nth-child(3n)
filter syntax :nth-child(3n, first 5, not first 1) or :nth-child(first 15, 3n, not first 1)

Match every third element starting with the second one and ending at the third last

Match every third element starting with the second one and ending at the third last
breakout syntax :nth-last-child(n >= 3 of :nth-child(n >= 2 of :nth-child(3n)))
filter syntax :nth-child(3n, not first 2, not last 2)

Concrete proposal

Basically, the proposal is as follows: :nth-child() now accepts a list of filters, each filter is applied on the indexes of the sequence after applying the previous filters, the filters are either legacy An+B or [ "not" ]? ["first" | "last"] <integer>.

Optionally, we can add operator skip which is just not first but reads a bit better sometimes.

Reasoning

I think I have never needed to express a range as "from 3rd to 5th" in my CSS life. I have however many times wanted to exclude the edges of a range. I think a proposal that allows to say "not first 1" is strictly more intuitive and readable than "n < 1" as the latter requires to remember whether CSS child indexes are 0-indexed or not, and also, it combines poorly with "even/odd" which is frequent, too.

Overall, the proposal is also more powerful, because it can easily be extended to more complex filters over time without having to add yet another syntax. For example, if we want to allow subsequent An+B to have of arguments, we could do that later easily, and allow things like :nth-child(1n of :not([hidden]), 2n of .full-row, not last 1) which would return every second .full-row elements, not including those which are hidden in the computation, and ignoring the last instance (because presumably it would need a different styling). I am not convinced that the breakout syntax would allow for this type of later extension.

@SebastianZ
Copy link
Contributor Author

  • Match every third element starting with the second one and ending at the third last: considerable disagreement on what this actually meant

Just to clarify, this meant to target the range between the second and the third last element and of them just every third element.

Sebastian

@tabatkins
Copy link
Member

the range between the second and the third last element and of them just every third element.

If that's the case, then it's just :nth-child(3n of :nth-child(n >= 2):nth-last-child(n >= 3)).

@Loirooriol
Copy link
Contributor

Loirooriol commented Jul 24, 2023

The syntax in Tab's comment should also include the :nth-child(6 <= n <= 15) from the example.

To summarize the desugarings of the breakout proposal into the existing syntax:

:nth-child(n >= k of S) = :nth-child(n + k of S)
:nth-child(n > k of S) = :nth-child(n + (k+1) of S)
:nth-child(n <= k of S) = :nth-child(-n + k of S)
:nth-child(n < k of S) = :nth-child(-n + (k-1) of S)
:nth-child(k1 <= n <= k2 of S) = :nth-child(n + k1 of S):nth-child(-n + k2 of S)
:nth-child(k1 < n <= k2 of S) = :nth-child(n + (k1+1) of S):nth-child(-n + k2 of S)
:nth-child(k1 <= n < k2 of S) = :nth-child(n + k1 of S):nth-child(-n + (k2-1) of S)
:nth-child(k1 < n < k2 of S) = :nth-child(n + (k1+1) of S):nth-child(-n + (k2-1) of S)

Sebastian's proposal can be desugared into this proposal as such (analogous for < et al):

:nth-child(k1 <= An+B <= k2 of S) = :nth-child(k1 <= n <= k2 of S):nth-child(An+B of S)
:nth-child(k1 <= An+B <= last k2 of S) = :nth-child(k1 <= n of S):nth-last-child(n <= k2 of S):nth-child(An+B of S)

And for my proposal it's less straightforward in general, if I didn't make a mistake,

If A*k1+B >= 1,
    :nth-child(An+B, k1 <= n <= k2, of S) = :nth-child(n <= (k2-k1+1) of :nth-child(An + (A*k1+B) of S))

Otherwise,
    :nth-child(An+B, k1 <= n <= k2, of S) = :nth-child(n <= (k2+ceil(B/A)) of :nth-child(An + (A*k1+B) of S))

but as Tab said, the typical case of just wanting to limit the number of selected elements is simply

:nth-child(n <= k of :nth-child(An + B of S))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants