Passed
Push — master ( 0e7332...8f4353 )
by Edward
04:01
created

PropertyBuilder::buildUnicodeDataDerivatives()   A

Complexity

Conditions 5
Paths 15

Size

Total Lines 44
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 34
c 2
b 1
f 0
dl 0
loc 44
rs 9.0648
cc 5
nc 15
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Remorhaz\UniLex\Tool\RegExp;
6
7
use PhpParser\BuilderFactory;
8
use PhpParser\Comment\Doc;
9
use PhpParser\Node\Scalar\LNumber;
10
use PhpParser\Node\Stmt\Declare_;
11
use PhpParser\Node\Stmt\DeclareDeclare;
12
use PhpParser\Node\Stmt\Return_;
13
use PhpParser\PrettyPrinterAbstract;
14
use ReflectionClass;
15
use Remorhaz\UniLex\RegExp\FSM\Range;
16
use Remorhaz\UniLex\RegExp\FSM\RangeSet;
17
use Remorhaz\UniLex\RegExp\FSM\RangeSetCalc;
18
use SplFileObject;
19
use Throwable;
20
21
use function array_diff;
22
use function array_keys;
23
use function array_merge;
24
use function count;
25
use function var_export;
26
27
final class PropertyBuilder
28
{
29
30
    private $rangeSetCalc;
31
32
    private $rangeSets = [];
33
34
    private $scripts = [];
35
36
    private $phpBuilder;
37
38
    private $printer;
39
40
    /**
41
     * @var Range[][]
42
     */
43
    private $rangeBuffer = [];
44
45
    public function __construct(RangeSetCalc $rangeSetCalc, PrettyPrinterAbstract $printer)
46
    {
47
        $this->rangeSetCalc = $rangeSetCalc;
48
        $this->phpBuilder = new BuilderFactory();
49
        $this->printer = $printer;
50
    }
51
52
    public function parseUnicodeData(SplFileObject $file, callable $onProgress): void
53
    {
54
        foreach (new UnicodeDataRangeIterator($file, $onProgress) as $prop => $range) {
55
            $this->addRangeToBuffer($prop, $range);
56
        }
57
    }
58
59
    public function parseScripts(SplFileObject $file, callable $onProgress): void
60
    {
61
        $otherProperties = array_keys($this->rangeBuffer);
62
        $this->parseProperties($file, $onProgress);
63
        $this->scripts = array_diff(array_keys($this->rangeBuffer), $otherProperties);
64
    }
65
66
    public function parseProperties(SplFileObject $file, callable $onProgress): void
67
    {
68
        foreach (new PropertiesRangeIterator($file, $onProgress) as $prop => $range) {
69
            $this->addRangeToBuffer($prop, $range);
70
        }
71
    }
72
73
    private function addRangeToBuffer(string $prop, Range ...$ranges): void
74
    {
75
        $this->rangeBuffer[$prop] = array_merge($this->rangeBuffer[$prop] ?? [], $ranges);
76
    }
77
78
    public function getRangeBufferSize(): int
79
    {
80
        return count($this->rangeBuffer);
81
    }
82
83
    public function getFileCount(): int
84
    {
85
        return count($this->rangeSets) + 1;
86
    }
87
88
    public function fetchBufferedRangeSets(callable $onProgress): void
89
    {
90
        $count = 0;
91
        foreach ($this->rangeBuffer as $prop => $ranges) {
92
            $this->addRangeSet($prop, ...$ranges);
93
            $onProgress(++$count);
94
        }
95
        $this->rangeBuffer = [];
96
    }
97
98
    public function buildUnicodeDataDerivatives(callable $onProgress): void
99
    {
100
        $map = [
101
            'C' => ['Cc', 'Cf', 'Co', 'Cs'],
102
            'L' => ['Ll', 'Lm', 'Lo', 'Lt', 'Lu'],
103
            'L&' => ['Lu', 'Ll', 'Lt'],
104
            'M' => ['Mc', 'Me', 'Mn'],
105
            'N' => ['Nd', 'Nl', 'No'],
106
            'P' => ['Pc', 'Pd', 'Pe', 'Pf', 'Pi', 'Po', 'Ps'],
107
            'S' => ['Sc', 'Sk', 'Sm', 'So'],
108
            'Z' => ['Zl', 'Zp', 'Zs'],
109
        ];
110
111
        $notCnRanges = [];
112
        foreach ($map as $targetProp => $sourceProps) {
113
            foreach ($sourceProps as $prop) {
114
                $rangeSet = $this->getRangeSet($prop);
115
                $notCnRanges = array_merge($notCnRanges, $rangeSet->getRanges());
116
                $this->addRangeToBuffer($targetProp, ...$rangeSet->getRanges());
117
            }
118
        }
119
120
        try {
121
            $targetProp = 'Any';
122
            $anyRangeSet = RangeSet::import([0x00, 0x10FFFF]);
123
        } catch (Throwable $e) {
124
            throw new Exception\RangeSetNotBuiltException($targetProp, $e);
125
        }
126
        $onProgress();
127
        $this->addRangeToBuffer($targetProp, ...$anyRangeSet->getRanges());
128
129
        try {
130
            $targetProp = 'Cn';
131
            $notCnRangeSet = new RangeSet(...$notCnRanges);
132
            $onProgress();
133
            $cnRanges = $this
134
                ->rangeSetCalc
135
                ->xor($notCnRangeSet, $anyRangeSet)
136
                ->getRanges();
137
        } catch (Throwable $e) {
138
            throw new Exception\RangeSetNotBuiltException($targetProp, $e);
139
        }
140
        $onProgress();
141
        $this->addRangeToBuffer($targetProp, ...$cnRanges);
142
    }
143
144
    public function buildScriptsDerivatives(callable $onProgress): void
145
    {
146
        $knownRanges = [];
147
        foreach ($this->scripts as $prop) {
148
            $knownRanges = array_merge($knownRanges, $this->getRangeSet($prop)->getRanges());
149
            $onProgress();
150
        }
151
        try {
152
            $targetProp = 'Unknown';
153
            $knownRangeSet = new RangeSet(...$knownRanges);
154
            $onProgress();
155
            $unknownRanges = $this
156
                ->rangeSetCalc
157
                ->xor($knownRangeSet, $this->getRangeSet('Any'))
158
                ->getRanges();
159
        } catch (Throwable $e) {
160
            throw new Exception\RangeSetNotBuiltException($targetProp, $e);
161
        }
162
        $onProgress();
163
        $this->addRangeToBuffer($targetProp, ...$unknownRanges);
164
    }
165
166
    private function addRangeSet(string $prop, Range ...$ranges): void
167
    {
168
        if (isset($this->rangeSets[$prop])) {
169
            throw new Exception\RangeSetAlreadyExistsException($prop);
170
        }
171
        try {
172
            $this->rangeSets[$prop] = new RangeSet(...$ranges);
173
        } catch (Throwable $e) {
174
            throw new Exception\RangeSetNotBuiltException($prop, $e);
175
        }
176
    }
177
178
    private function getRangeSet(string $prop): RangeSet
179
    {
180
        if (isset($this->rangeSets[$prop])) {
181
            return $this->rangeSets[$prop];
182
        }
183
184
        throw new Exception\RangeSetNotFoundException($prop);
185
    }
186
187
    public function writeFiles(string $targetRootDir, callable $onProgress): void
188
    {
189
        $fileIndex = [];
190
        foreach ($this->rangeSets as $prop => $rangeSet) {
191
            $baseName = "/Properties/{$prop}.php";
192
            $fileName = $targetRootDir . $baseName;
193
            try {
194
                $code = $this->buildPropertyFile($rangeSet);
195
                \Safe\file_put_contents($fileName, $code);
196
            } catch (Throwable $e) {
197
                throw new Exception\FileNotWrittenException($fileName);
198
            }
199
            $fileIndex[$prop] = $baseName;
200
            $onProgress();
201
        }
202
        $fileName = $targetRootDir . "/PropertyIndex.php";
203
        try {
204
            $code = $this->buildIndexFile($fileIndex);
205
            \Safe\file_put_contents($fileName, $code);
206
        } catch (Throwable $e) {
207
            throw new Exception\FileNotWrittenException($fileName);
208
        }
209
        $onProgress();
210
    }
211
212
    private function buildIndexFile(array $index): string
213
    {
214
        $array = var_export($index, true);
215
216
        return "<?php\n\nreturn {$array};\n";
217
    }
218
219
    private function buildPropertyFile(RangeSet $rangeSet): string
220
    {
221
        $rangeClass = new ReflectionClass(Range::class);
222
        $rangeSetClass = new ReflectionClass(RangeSet::class);
223
224
        $phpNodes = [];
225
        $declare = new Declare_([new DeclareDeclare('strict_types', $this->phpBuilder->val(1))]);
226
        $declare->setDocComment(new Doc('/** @noinspection PhpUnhandledExceptionInspection */'));
227
        $phpNodes[] = $declare;
228
        $phpNodes[] = $this->phpBuilder->namespace(__NAMESPACE__ . '\\Properties')->getNode();
229
        $phpNodes[] = $this->phpBuilder->use($rangeClass->getName())->getNode();
230
        $phpNodes[] = $this->phpBuilder->use($rangeSetClass->getName())->getNode();
231
        $phpRanges = [];
232
233
        foreach ($rangeSet->getRanges() as $range) {
234
            $rangeStart = $range->getStart();
235
            $rangeFinish = $range->getFinish();
236
            $phpRangeStart = $this->phpBuilder->val($rangeStart);
237
            $phpRangeStart->setAttribute('kind', LNumber::KIND_HEX);
238
            $phpRangeArgs = [$phpRangeStart];
239
            if ($rangeStart != $rangeFinish) {
240
                $phpRangeFinish = $this->phpBuilder->val($rangeFinish);
241
                $phpRangeFinish->setAttribute('kind', LNumber::KIND_HEX);
242
                $phpRangeArgs[] = $phpRangeFinish;
243
            }
244
            $phpRanges[] = $this->phpBuilder->new($rangeClass->getShortName(), $phpRangeArgs);
245
        }
246
        $phpReturn = new Return_(
247
            $this->phpBuilder->staticCall($rangeSetClass->getShortName(), 'loadUnsafe', $phpRanges)
248
        );
249
        $phpReturn->setDocComment(new Doc('/** phpcs:disable Generic.Files.LineLength.TooLong */'));
250
        $phpNodes[] = $phpReturn;
251
252
        return $this->printer->prettyPrintFile($phpNodes);
253
    }
254
}
255