Skip to content

Commit 4c9e21d

Browse files
committed
Improve counter simulation
1 parent 050365f commit 4c9e21d

2 files changed

Lines changed: 325 additions & 26 deletions

File tree

src/modules/counter.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
*/
2127
function 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+
*/
2739
function 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+
*/
3453
function 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

Comments
 (0)