-
-
Notifications
You must be signed in to change notification settings - Fork 187
/
CldrModel.php
378 lines (330 loc) · 11.9 KB
/
CldrModel.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
<?php
namespace Neos\Flow\I18n\Cldr;
/*
* This file is part of the Neos.Flow package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
use Neos\Cache\Frontend\VariableFrontend;
/**
* A model representing data from one or few CLDR files.
*
* When more than one file path is provided to the constructor, data from
* all files will be parsed and merged according to the inheritance rules defined
* in CLDR specification. Aliases are also resolved correctly.
*
*/
class CldrModel
{
/**
* An absolute path to the directory where CLDR resides. It is changed only
* in tests.
*
* @var string
*/
protected $cldrBasePath = 'resource://Neos.Neos/Private/I18n/CLDR/Sources/';
/**
* @var VariableFrontend
*/
protected $cache;
/**
* Key used to store / retrieve cached data
*
* @var string
*/
protected $cacheKey;
/**
* @var CldrParser
*/
protected $cldrParser;
/**
* Absolute path or path to the files represented by this class' instance.
*
* @var array<string>
*/
protected $sourcePaths;
/**
* @var array
*/
protected $parsedData;
/**
* Contructs the model
*
* Accepts array of absolute paths to CLDR files. This array can have one
* element (if model represents one CLDR file) or many elements (if group
* of CLDR files is going to be represented by this model).
*
* @param array<string> $sourcePaths
*/
public function __construct(array $sourcePaths)
{
$this->sourcePaths = $sourcePaths;
$this->cacheKey = md5(implode(';', $sourcePaths));
}
/**
* Injects the Flow_I18n_Cldr_CldrModelCache cache
*
* @param VariableFrontend $cache
* @return void
*/
public function injectCache(VariableFrontend $cache)
{
$this->cache = $cache;
}
/**
* @param CldrParser $parser
* @return void
*/
public function injectParser(CldrParser $parser)
{
$this->cldrParser = $parser;
}
/**
* When it's called, CLDR file is parsed or cache is loaded, if available.
*
* @return void
* @throws Exception\InvalidCldrDataException
* @throws \Neos\Cache\Exception
*/
public function initializeObject(): void
{
if ($this->cache->has($this->cacheKey)) {
$this->parsedData = $this->cache->get($this->cacheKey);
} else {
$this->parsedData = $this->parseFiles($this->sourcePaths);
$this->parsedData = $this->resolveAliases($this->parsedData, '');
$this->cache->set($this->cacheKey, $this->parsedData);
}
}
/**
* Returns multi-dimensional array representing desired node and it's children,
* or a string value if the path points to a leaf.
*
* Syntax for paths is very simple. It's a group of array indices joined
* with a slash. It tries to emulate XPath query syntax to some extent.
* Examples:
*
* plurals/pluralRules
* dates/calendars/calendar[@type="gregorian"]
*
* Please see the documentation for CldrParser for details about parsed data
* structure.
*
* @param string $path A path to the node to get
* @return mixed Array or string of matching data, or false on failure
* @see CldrParser
*/
public function getRawData(string $path)
{
if ($path === '/') {
return $this->parsedData;
}
$pathElements = explode('/', trim($path, '/'));
$data = $this->parsedData;
foreach ($pathElements as $key) {
if (isset($data[$key])) {
$data = $data[$key];
} else {
return false;
}
}
return $data;
}
/**
* Returns multi-dimensional array representing desired node and it's children.
*
* This method will return false if the path points to a leaf (i.e. a string,
* not an array).
*
* @param string $path A path to the node to get
* @return mixed Array of matching data, or false on failure
* @see CldrParser
* @see CldrModel::getRawData()
*/
public function getRawArray(string $path)
{
$data = $this->getRawData($path);
if (!is_array($data)) {
return false;
}
return $data;
}
/**
* Returns string value from a path given.
*
* Path must point to leaf. Syntax for paths is same as for getRawData.
*
* @param string $path A path to the element to get
* @return mixed String with desired element, or false on failure
*/
public function getElement(string $path)
{
$data = $this->getRawData($path);
if (is_array($data)) {
return false;
}
return $data;
}
/**
* Returns all nodes with given name found within given path
*
* @param string $path A path to search in
* @param string $nodeName A name of the nodes to return
* @return mixed String with desired element, or false on failure
*/
public function findNodesWithinPath(string $path, string $nodeName)
{
$data = $this->getRawArray($path);
if ($data === false) {
return false;
}
$filteredData = [];
foreach ($data as $nodeString => $children) {
if (static::getNodeName($nodeString) === $nodeName) {
$filteredData[$nodeString] = $children;
}
}
return $filteredData;
}
/**
* Returns node name extracted from node string
*
* The internal representation of CLDR uses array keys like:
* 'calendar[@type="gregorian"]'
* This method helps to extract the node name from such keys.
*
* @param string $nodeString String with node name and optional attribute(s)
* @return string Name of the node
*/
public static function getNodeName(string $nodeString): string
{
$positionOfFirstAttribute = strpos($nodeString, '[@');
if ($positionOfFirstAttribute === false) {
return $nodeString;
}
return substr($nodeString, 0, $positionOfFirstAttribute);
}
/**
* Parses the node string and returns a value of attribute for name provided.
*
* An internal representation of CLDR data used by this class is a simple
* multi dimensional array where keys are nodes' names. If node has attributes,
* they are all stored as one string (e.g. 'calendar[@type="gregorian"]' or
* 'calendar[@type="gregorian"][@alt="proposed-x1001"').
*
* This convenient method extracts a value of desired attribute by its name
* (in example above, in order to get the value 'gregorian', 'type' should
* be passed as the second parameter to this method).
*
* Note: this method does not validate the input!
*
* @param string $nodeString A node key to parse
* @param string $attributeName Name of the attribute to find
* @return mixed Value of desired attribute, or false if there is no such attribute
*/
public static function getAttributeValue(string $nodeString, string $attributeName)
{
$attributeName = '[@' . $attributeName . '="';
$positionOfAttributeName = strpos($nodeString, $attributeName);
if ($positionOfAttributeName === false) {
return false;
}
$positionOfAttributeValue = $positionOfAttributeName + strlen($attributeName);
return substr($nodeString, $positionOfAttributeValue, strpos($nodeString, '"]', $positionOfAttributeValue) - $positionOfAttributeValue);
}
/**
* Parses given CLDR files using CldrParser and merges parsed data.
*
* Merging is done with inheritance in mind, as defined in CLDR specification.
*
* @param array<string> $sourcePaths Absolute paths to CLDR files (can be one file)
* @return array Parsed and merged data
*/
protected function parseFiles(array $sourcePaths): array
{
$parsedFiles = [];
foreach ($sourcePaths as $sourcePath) {
$parsedFiles[] = $this->cldrParser->getParsedData($sourcePath);
}
// Merge all data starting with most generic file so we get proper inheritance
$parsedData = $parsedFiles[0];
$parsedFilesCount = count($parsedFiles);
for ($i = 1; $i < $parsedFilesCount; ++$i) {
$parsedData = $this->mergeTwoParsedFiles($parsedData, $parsedFiles[$i]);
}
return $parsedData;
}
/**
* Merges two sets of data from two separate CLDR files into one array.
*
* Merging is done with inheritance in mind, as defined in CLDR specification.
*
* @param mixed $firstParsedData Part of data from first file (either array or string)
* @param mixed $secondParsedData Part of data from second file (either array or string)
* @return array Data merged from two files
*/
protected function mergeTwoParsedFiles($firstParsedData, $secondParsedData)
{
$mergedData = $firstParsedData;
if (is_array($secondParsedData)) {
foreach ($secondParsedData as $nodeString => $children) {
if (isset($firstParsedData[$nodeString])) {
$mergedData[$nodeString] = $this->mergeTwoParsedFiles($firstParsedData[$nodeString], $children);
} else {
$mergedData[$nodeString] = $children;
}
}
} else {
$mergedData = $secondParsedData;
}
return $mergedData;
}
/**
* Resolves any 'alias' nodes in parsed CLDR data.
*
* CLDR uses 'alias' tag which denotes places where data should be copied
* from. This tag has 'source' attribute pointing (by relative XPath query)
* to the source node - it should be copied with all it's children.
*
* @param mixed $data Part of internal array to resolve aliases for (string if leaf, array otherwise)
* @param string $currentPath Path to currently analyzed part of data
* @return mixed Modified (or unchanged) $data
* @throws Exception\InvalidCldrDataException When found alias tag which has unexpected structure
*/
protected function resolveAliases($data, string $currentPath)
{
if (!is_array($data)) {
return $data;
}
foreach ($data as $nodeString => $nodeChildren) {
if (self::getNodeName($nodeString) === 'alias') {
if (self::getAttributeValue($nodeString, 'source') !== 'locale') {
// Value of source attribute can be 'locale' or particular locale identifier, but we do not support the second mode, ignore it silently
break;
}
$sourcePath = self::getAttributeValue($nodeString, 'path');
// Change relative path to absolute one
$sourcePath = str_replace('../', '', $sourcePath, $countOfJumpsToParentNode);
$sourcePath = str_replace('\'', '"', $sourcePath);
$currentPathNodeNames = explode('/', $currentPath);
for ($i = 0; $i < $countOfJumpsToParentNode; ++$i) {
unset($currentPathNodeNames[count($currentPathNodeNames) - 1]);
}
$sourcePath = implode('/', $currentPathNodeNames) . '/' . $sourcePath;
unset($data[$nodeString]);
$sourceData = $this->getRawData($sourcePath);
if (is_array($sourceData)) {
$data = array_merge($sourceData, $data);
}
break;
} else {
$data[$nodeString] = $this->resolveAliases($data[$nodeString], ($currentPath === '') ? $nodeString : ($currentPath . '/' . $nodeString));
}
}
return $data;
}
}