Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
98.48% |
65 / 66 |
|
80.00% |
4 / 5 |
CRAP | |
0.00% |
0 / 1 |
| CounterFormat | |
98.48% |
65 / 66 |
|
80.00% |
4 / 5 |
27 | |
0.00% |
0 / 1 |
| format | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
12 | |||
| toCjkDecimal | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
3 | |||
| toGreek | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
| toAlpha | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
| toRoman | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace 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 | */ |
| 13 | final 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 | } |