Passed
Push — master ( eff209...0e7332 )
by Edward
05:02
created

PropertyBuilder::addRangeToBuffer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 2
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 fetchBufferedRangeSetsUnsafe(callable $onProgress): void
99
    {
100
        $count = 0;
101
        foreach ($this->rangeBuffer as $prop => $ranges) {
102
            $this->addRangeSetUnsafe($prop, ...$ranges);
103
            $onProgress(++$count);
104
        }
105
        $this->rangeBuffer = [];
106
    }
107
108
    public function buildUnicodeDataDerivatives(callable $onProgress): void
109
    {
110
        $map = [
111
            'C' => ['Cc', 'Cf', 'Co', 'Cs'],
112
            'L' => ['Ll', 'Lm', 'Lo', 'Lt', 'Lu'],
113
            'L&' => ['Lu', 'Ll', 'Lt'],
114
            'M' => ['Mc', 'Me', 'Mn'],
115
            'N' => ['Nd', 'Nl', 'No'],
116
            'P' => ['Pc', 'Pd', 'Pe', 'Pf', 'Pi', 'Po', 'Ps'],
117
            'S' => ['Sc', 'Sk', 'Sm', 'So'],
118
            'Z' => ['Zl', 'Zp', 'Zs'],
119
        ];
120
121
        $notCnRanges = [];
122
        foreach ($map as $targetProp => $sourceProps) {
123
            foreach ($sourceProps as $prop) {
124
                $rangeSet = $this->getRangeSet($prop);
125
                foreach ($rangeSet->getRanges() as $range) {
126
                    $notCnRanges[] = $range;
127
                    $this->addRangeToBuffer($targetProp, $range);
128
                    $onProgress();
129
                }
130
            }
131
        }
132
133
        try {
134
            $targetProp = 'Any';
135
            $anyRangeSet = RangeSet::import([0x00, 0x10FFFF]);
136
        } catch (Throwable $e) {
137
            throw new Exception\RangeSetNotBuiltException($targetProp, $e);
138
        }
139
        $onProgress();
140
        $this->addRangeToBuffer($targetProp, ...$anyRangeSet->getRanges());
141
142
        try {
143
            $targetProp = 'Cn';
144
            $notCnRangeSet = new RangeSet(...$notCnRanges);
145
            $onProgress();
146
            $cnRanges = $this
147
                ->rangeSetCalc
148
                ->xor($notCnRangeSet, $anyRangeSet)
149
                ->getRanges();
150
        } catch (Throwable $e) {
151
            throw new Exception\RangeSetNotBuiltException($targetProp, $e);
152
        }
153
        $onProgress();
154
        $this->addRangeToBuffer($targetProp, ...$cnRanges);
155
    }
156
157
    public function buildScriptsDerivatives(callable $onProgress): void
158
    {
159
        $knownRanges = [];
160
        foreach ($this->scripts as $prop) {
161
            $knownRanges = array_merge($knownRanges, $this->getRangeSet($prop)->getRanges());
162
            $onProgress();
163
        }
164
        try {
165
            $targetProp = 'Unknown';
166
            $knownRangeSet = new RangeSet(...$knownRanges);
167
            $onProgress();
168
            $unknownRanges = $this
169
                ->rangeSetCalc
170
                ->xor($knownRangeSet, $this->getRangeSet('Any'))
171
                ->getRanges();
172
        } catch (Throwable $e) {
173
            throw new Exception\RangeSetNotBuiltException($targetProp, $e);
174
        }
175
        $onProgress();
176
        $this->addRangeToBuffer($targetProp, ...$unknownRanges);
177
    }
178
179
    private function addRangeSet(string $prop, Range ...$ranges): void
180
    {
181
        if (isset($this->rangeSets[$prop])) {
182
            throw new Exception\RangeSetAlreadyExistsException($prop);
183
        }
184
        try {
185
            $this->rangeSets[$prop] = new RangeSet(...$ranges);
186
        } catch (Throwable $e) {
187
            throw new Exception\RangeSetNotBuiltException($prop, $e);
188
        }
189
    }
190
191
    private function addRangeSetUnsafe(string $prop, Range ...$ranges): void
192
    {
193
        if (isset($this->rangeSets[$prop])) {
194
            throw new Exception\RangeSetAlreadyExistsException($prop);
195
        }
196
        $this->rangeSets[$prop] = RangeSet::loadUnsafe(...$ranges);
197
    }
198
199
    private function getRangeSet(string $prop): RangeSet
200
    {
201
        if (isset($this->rangeSets[$prop])) {
202
            return $this->rangeSets[$prop];
203
        }
204
205
        throw new Exception\RangeSetNotFoundException($prop);
206
    }
207
208
    public function writeFiles(string $targetRootDir, callable $onProgress): void
209
    {
210
        $fileIndex = [];
211
        foreach ($this->rangeSets as $prop => $rangeSet) {
212
            $baseName = "/Properties/{$prop}.php";
213
            $fileName = $targetRootDir . $baseName;
214
            try {
215
                $code = $this->buildPropertyFile($rangeSet);
216
                \Safe\file_put_contents($fileName, $code);
217
            } catch (Throwable $e) {
218
                throw new Exception\FileNotWrittenException($fileName);
219
            }
220
            $fileIndex[$prop] = $baseName;
221
            $onProgress();
222
        }
223
        $fileName = $targetRootDir . "/PropertyIndex.php";
224
        try {
225
            $code = $this->buildIndexFile($fileIndex);
226
            \Safe\file_put_contents($fileName, $code);
227
        } catch (Throwable $e) {
228
            throw new Exception\FileNotWrittenException($fileName);
229
        }
230
        $onProgress();
231
    }
232
233
    private function buildIndexFile(array $index): string
234
    {
235
        $array = var_export($index, true);
236
237
        return "<?php\n\nreturn {$array};\n";
238
    }
239
240
    private function buildPropertyFile(RangeSet $rangeSet): string
241
    {
242
        $rangeClass = new ReflectionClass(Range::class);
243
        $rangeSetClass = new ReflectionClass(RangeSet::class);
244
245
        $phpNodes = [];
246
        $declare = new Declare_([new DeclareDeclare('strict_types', $this->phpBuilder->val(1))]);
247
        $declare->setDocComment(new Doc('/** @noinspection PhpUnhandledExceptionInspection */'));
248
        $phpNodes[] = $declare;
249
        $phpNodes[] = $this->phpBuilder->namespace(__NAMESPACE__ . '\\Properties')->getNode();
250
        $phpNodes[] = $this->phpBuilder->use($rangeClass->getName())->getNode();
251
        $phpNodes[] = $this->phpBuilder->use($rangeSetClass->getName())->getNode();
252
        $phpRanges = [];
253
254
        foreach ($rangeSet->getRanges() as $range) {
255
            $rangeStart = $range->getStart();
256
            $rangeFinish = $range->getFinish();
257
            $phpRangeStart = $this->phpBuilder->val($rangeStart);
258
            $phpRangeStart->setAttribute('kind', LNumber::KIND_HEX);
259
            $phpRangeArgs = [$phpRangeStart];
260
            if ($rangeStart != $rangeFinish) {
261
                $phpRangeFinish = $this->phpBuilder->val($rangeFinish);
262
                $phpRangeFinish->setAttribute('kind', LNumber::KIND_HEX);
263
                $phpRangeArgs[] = $phpRangeFinish;
264
            }
265
            $phpRanges[] = $this->phpBuilder->new($rangeClass->getShortName(), $phpRangeArgs);
266
        }
267
        $phpReturn = new Return_(
268
            $this->phpBuilder->staticCall($rangeSetClass->getShortName(), 'loadUnsafe', $phpRanges)
269
        );
270
        $phpReturn->setDocComment(new Doc('/** phpcs:disable Generic.Files.LineLength.TooLong */'));
271
        $phpNodes[] = $phpReturn;
272
273
        return $this->printer->prettyPrintFile($phpNodes);
274
    }
275
}
276