PropertyBuilder::getFileCount()   A
last analyzed

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