Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.48% covered (success)
98.48%
65 / 66
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CounterFormat
98.48% covered (success)
98.48%
65 / 66
80.00% covered (warning)
80.00%
4 / 5
27
0.00% covered (danger)
0.00%
0 / 1
 format
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
12
 toCjkDecimal
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
3
 toGreek
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 toAlpha
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 toRoman
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\HtmlToPdf\Layout;
6
7/**
8 * Centralised CSS Counter Styles 3 §6 formatters used by both list-marker
9 * painting (`<ol type="i">`, `list-style-type: lower-roman`) and `@page`
10 * `counter(page, <style>)` substitution. Phase-1 styles only — the spec's
11 * algorithmic counter-style families (`@counter-style`) come later.
12 */
13final class CounterFormat
14{
15    /**
16     * Format the 1-based ordinal `$value` per the named counter style.
17     * Returns the raw decimal string when `$style` is unrecognised — same
18     * fallback browsers apply for unknown `list-style-type` keywords.
19     */
20    public static function format(int $value, string $style): string
21    {
22        return match (strtolower($style)) {
23            'decimal' => (string) $value,
24            'decimal-leading-zero' => str_pad((string) $value, 2, '0', STR_PAD_LEFT),
25            'lower-alpha', 'lower-latin' => self::toAlpha($value, lower: true),
26            'upper-alpha', 'upper-latin' => self::toAlpha($value, lower: false),
27            'lower-roman' => strtolower(self::toRoman($value)),
28            'upper-roman' => self::toRoman($value),
29            'lower-greek' => self::toGreek($value),
30            'cjk-decimal' => self::toCjkDecimal($value),
31            // CSS Counter Styles 3 §7.2 — disclosure triangles for
32            // `<summary>::marker`. The value is ignored (these are
33            // fixed-symbol systems).
34            'disclosure-open' => "\u{25BC}",
35            'disclosure-closed' => "\u{25B6}",
36            default => (string) $value,
37        };
38    }
39
40    /**
41     * CSS Counter Styles 3 §7.1.5 `cjk-decimal` — the digital style
42     * using Chinese ideographic digits 〇 一 二 三 四 五 六 七 八 九.
43     * Multi-digit numbers concatenate (e.g. 23 → 二三). Negative
44     * values fall back to the decimal string per the spec's
45     * fixed-system fallback.
46     */
47    public static function toCjkDecimal(int $n): string
48    {
49        if ($n < 0) {
50            return (string) $n;
51        }
52        $digits = [
53            '0' => "\u{3007}",
54            '1' => "\u{4E00}",
55            '2' => "\u{4E8C}",
56            '3' => "\u{4E09}",
57            '4' => "\u{56DB}",
58            '5' => "\u{4E94}",
59            '6' => "\u{516D}",
60            '7' => "\u{4E03}",
61            '8' => "\u{516B}",
62            '9' => "\u{4E5D}",
63        ];
64        $out = '';
65        foreach (str_split((string) $n) as $d) {
66            $out .= $digits[$d];
67        }
68        return $out;
69    }
70
71    /**
72     * CSS Counter Styles 3 §7.1.4 `lower-greek` — alphabetic over the
73     * 24-letter Greek lowercase set α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π
74     * ρ σ τ υ φ χ ψ ω (note: σ, not the final-sigma ς, per spec).
75     */
76    public static function toGreek(int $n): string
77    {
78        if ($n < 1) {
79            return (string) $n;
80        }
81        // 24 lowercase Greek letters — Unicode U+03B1..U+03C9 skipping
82        // U+03C2 (final sigma).
83        static $letters = [
84            "α", "β", "γ", "δ", "ε", "ζ", "η", "θ",
85            "ι", "κ", "λ", "μ", "ν", "ξ", "ο", "π",
86            "ρ", "σ", "τ", "υ", "φ", "χ", "ψ", "ω",
87        ];
88        $base = count($letters);
89        $out = '';
90        while ($n > 0) {
91            $n--;
92            $out = $letters[$n % $base] . $out;
93            $n = intdiv($n, $base);
94        }
95        return $out;
96    }
97
98    /**
99     * Bijective base-26: 1→"a", 26→"z", 27→"aa", … (or upper-case
100     * when `$lower` is false).
101     */
102    public static function toAlpha(int $n, bool $lower): string
103    {
104        if ($n < 1) {
105            return (string) $n;
106        }
107        $base = $lower ? ord('a') : ord('A');
108        $out = '';
109        while ($n > 0) {
110            $n--;
111            $out = chr($base + ($n % 26)) . $out;
112            $n = intdiv($n, 26);
113        }
114        return $out;
115    }
116
117    /** Standard subtractive Roman-numeral formatting for 1-3999. */
118    public static function toRoman(int $n): string
119    {
120        if ($n < 1 || $n > 3999) {
121            return (string) $n;
122        }
123        static $map = [
124            1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD',
125            100 => 'C', 90 => 'XC', 50 => 'L', 40 => 'XL',
126            10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I',
127        ];
128        $out = '';
129        foreach ($map as $value => $symbol) {
130            while ($n >= $value) {
131                $out .= $symbol;
132                $n -= $value;
133            }
134        }
135        return $out;
136    }
137}