Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
27 / 30
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TextTransform
90.00% covered (success)
90.00%
27 / 30
33.33% covered (danger)
33.33%
1 / 3
20.40
0.00% covered (danger)
0.00%
0 / 1
 apply
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 capitalizeWords
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 toFullWidth
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
7.39
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Text;
6
7/**
8 * CSS Text 3 §2.1 — `text-transform`, applied to a text run before
9 * shaping.
10 *
11 * Shared because both text pipelines need it: HTML inline layout, and
12 * the SVG painter for `<text>`. They used to be the same algorithm in
13 * only one of them.
14 *
15 *  - `uppercase` / `lowercase` are full Unicode case mappings.
16 *  - `capitalize` upper-cases the first grapheme of each
17 *    whitespace-separated word, preserving the original separators.
18 *  - `full-width` (CSS Text 4 §2.1.4) maps ASCII U+0021..U+007E to the
19 *    full-width forms U+FF01..U+FF5E, and ASCII space to the
20 *    ideographic space U+3000.
21 *  - Anything else — `none`, `full-size-kana`, an unknown keyword —
22 *    passes through unchanged.
23 */
24final class TextTransform
25{
26    public static function apply(string $text, ?string $keyword): string
27    {
28        if ($keyword === null || $text === '') {
29            return $text;
30        }
31        return match (strtolower(trim($keyword))) {
32            'uppercase' => mb_strtoupper($text, 'UTF-8'),
33            'lowercase' => mb_strtolower($text, 'UTF-8'),
34            'capitalize' => self::capitalizeWords($text),
35            'full-width' => self::toFullWidth($text),
36            default => $text,
37        };
38    }
39
40    private static function capitalizeWords(string $text): string
41    {
42        // Split on whitespace runs, capitalize the first codepoint of each
43        // non-empty word, and rejoin with the original separators.
44        $parts = preg_split('/(\s+)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
45        if ($parts === false) {
46            return $text;
47        }
48        $out = '';
49        foreach ($parts as $part) {
50            if ($part === '' || preg_match('/^\s+$/u', $part) === 1) {
51                $out .= $part;
52                continue;
53            }
54            $first = mb_substr($part, 0, 1, 'UTF-8');
55            $rest = mb_substr($part, 1, null, 'UTF-8');
56            $out .= mb_strtoupper($first, 'UTF-8') . $rest;
57        }
58        return $out;
59    }
60
61    private static function toFullWidth(string $text): string
62    {
63        $out = '';
64        foreach (mb_str_split($text, 1, 'UTF-8') as $ch) {
65            $cp = mb_ord($ch, 'UTF-8');
66            if ($cp === false) {
67                $out .= $ch;
68                continue;
69            }
70            $out .= match (true) {
71                $cp === 0x0020 => mb_chr(0x3000, 'UTF-8'),
72                $cp >= 0x0021 && $cp <= 0x007E => mb_chr($cp + 0xFEE0, 'UTF-8'),
73                default => $ch,
74            };
75        }
76        return $out;
77    }
78}