@@ -18,19 +18,38 @@ export function unquoteDoubleStrings(s) {
1818 return ( s || '' ) . replace ( / " ( [ ^ " ] * ) " / g, '$1' ) ;
1919}
2020
21+ /**
22+ * a, b, ..., z, aa, ab, ...
23+ * @param {number } n
24+ * @param {boolean } upper
25+ * @returns {string }
26+ */
2127function alpha ( n , upper = false ) {
2228 let s = '' , x = Math . max ( 1 , n ) ;
2329 while ( x > 0 ) { x -- ; s = String . fromCharCode ( 97 + ( x % 26 ) ) + s ; x = Math . floor ( x / 26 ) ; }
2430 return upper ? s . toUpperCase ( ) : s ;
2531}
2632
33+ /**
34+ * Roman numerals (1..3999)
35+ * @param {number } n
36+ * @param {boolean } upper
37+ * @returns {string }
38+ */
2739function roman ( n , upper = true ) {
2840 const map = [ [ 1000 , 'M' ] , [ 900 , 'CM' ] , [ 500 , 'D' ] , [ 400 , 'CD' ] , [ 100 , 'C' ] , [ 90 , 'XC' ] , [ 50 , 'L' ] , [ 40 , 'XL' ] , [ 10 , 'X' ] , [ 9 , 'IX' ] , [ 5 , 'V' ] , [ 4 , 'IV' ] , [ 1 , 'I' ] ] ;
2941 let num = Math . max ( 1 , Math . min ( 3999 , n ) ) , out = '' ;
3042 for ( const [ v , sym ] of map ) while ( num >= v ) { out += sym ; num -= v ; }
3143 return upper ? out : out . toLowerCase ( ) ;
3244}
3345
46+ /**
47+ * Format a numeric counter value according to CSS counter-style keyword.
48+ * NOTE: Keeps your original clamp to 0 in decimal variants.
49+ * @param {number } value
50+ * @param {string } style
51+ * @returns {string }
52+ */
3453function formatCounter ( value , style ) {
3554 switch ( ( style || 'decimal' ) . toLowerCase ( ) ) {
3655 case 'decimal' : return String ( Math . max ( 0 , value ) ) ;
@@ -158,10 +177,20 @@ export function buildCounterContext(root) {
158177 build ( rootEl , empty , empty ) ;
159178
160179 return {
180+ /**
181+ * Get top value for counter name at given node.
182+ * @param {Element } node
183+ * @param {string } name
184+ */
161185 get ( node , name ) {
162186 const s = nodeCounters . get ( node ) ?. get ( name ) ;
163187 return s && s . length ? s [ s . length - 1 ] : 0 ;
164188 } ,
189+ /**
190+ * Get full stack for counter name at given node.
191+ * @param {Element } node
192+ * @param {string } name
193+ */
165194 getStack ( node , name ) {
166195 const s = nodeCounters . get ( node ) ?. get ( name ) ;
167196 return s ? s . slice ( ) : [ ] ;
@@ -203,3 +232,92 @@ export function resolveCountersInContent(raw, node, ctx) {
203232 return '- ' ;
204233 }
205234}
235+
236+ /**
237+ * Create a derived counter context that applies a pseudo's counter-reset /
238+ * counter-increment *for this node only*, before resolving content.
239+ * Works with ::before / ::after (and any pseudo with content).
240+ *
241+ * @param {Element } node
242+ * @param {CSSStyleDeclaration|null } pseudoStyle getComputedStyle(node, '::before' | '::after')
243+ * @param {{get(node: Element, name: string): number, getStack(node: Element, name: string): number[]} } baseCtx
244+ */
245+ export function deriveCounterCtxForPseudo ( node , pseudoStyle , baseCtx ) {
246+ const modStacks = new Map ( ) ;
247+
248+ /** Parse "a 1, b -2" -> [{name:'a', num:1}, {name:'b', num:-2}] */
249+ function parseListDecl ( value ) {
250+ const out = [ ] ;
251+ if ( ! value || value === 'none' ) return out ;
252+ for ( const part of String ( value ) . split ( ',' ) ) {
253+ const toks = part . trim ( ) . split ( / \s + / ) ;
254+ const name = toks [ 0 ] ;
255+ const num = Number . isFinite ( Number ( toks [ 1 ] ) ) ? Number ( toks [ 1 ] ) : undefined ;
256+ if ( name ) out . push ( { name, num } ) ;
257+ }
258+ return out ;
259+ }
260+
261+ const resets = parseListDecl ( pseudoStyle ?. counterReset ) ;
262+ const incs = parseListDecl ( pseudoStyle ?. counterIncrement ) ;
263+
264+ function getStackDerived ( name ) {
265+ if ( modStacks . has ( name ) ) return modStacks . get ( name ) . slice ( ) ;
266+
267+ // base stack at this node from the element context
268+ let stack = baseCtx . getStack ( node , name ) ;
269+ stack = stack . length ? stack . slice ( ) : [ ] ;
270+
271+ // counter-reset (push if exists, replace if not)
272+ const r = resets . find ( x => x . name === name ) ;
273+ if ( r ) {
274+ const val = Number . isFinite ( r . num ) ? r . num : 0 ;
275+ if ( stack . length ) {
276+ stack = stack . slice ( ) ;
277+ stack . push ( val ) ;
278+ } else {
279+ stack = [ val ] ;
280+ }
281+ }
282+
283+ // counter-increment (on top; create top=0 if missing)
284+ const inc = incs . find ( x => x . name === name ) ;
285+ if ( inc ) {
286+ const by = Number . isFinite ( inc . num ) ? inc . num : 1 ;
287+ if ( stack . length === 0 ) stack = [ 0 ] ;
288+ stack [ stack . length - 1 ] += by ;
289+ }
290+
291+ modStacks . set ( name , stack . slice ( ) ) ;
292+ return stack ;
293+ }
294+
295+ return {
296+ get ( _node , name ) {
297+ const s = getStackDerived ( name ) ;
298+ return s . length ? s [ s . length - 1 ] : 0 ;
299+ } ,
300+ getStack ( _node , name ) {
301+ return getStackDerived ( name ) ;
302+ }
303+ } ;
304+ }
305+
306+ /**
307+ * Convenience helper: resolve the final text to render for a pseudo's `content`,
308+ * correctly applying the pseudo's own counter-reset/increment before evaluation.
309+ *
310+ * @param {Element } node
311+ * @param {'::before'|'::after' } pseudo
312+ * @param {{get(node: Element, name: string): number, getStack(node: Element, name: string): number[]} } baseCtx
313+ * @returns {string } resolved content (without surrounding double quotes)
314+ */
315+ export function resolvePseudoContent ( node , pseudo , baseCtx ) {
316+ let ps ;
317+ try { ps = getComputedStyle ( node , pseudo ) ; } catch { }
318+ const raw = ps ?. content ;
319+ if ( ! raw || raw === 'none' || raw === 'normal' ) return '' ;
320+ const derived = deriveCounterCtxForPseudo ( node , ps , baseCtx ) ;
321+ let out = resolveCountersInContent ( raw , node , derived ) ;
322+ return unquoteDoubleStrings ( out ) ;
323+ }
0 commit comments