Completed
Pull Request — master (#69)
by Raúl
02:30 queued 01:15
created

Parser::getProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace Sepia\PoParser;
4
5
use Sepia\PoParser\Catalog\EntryFactory;
6
use Sepia\PoParser\Exception\ParseException;
7
use Sepia\PoParser\SourceHandler\FileSystem;
8
use Sepia\PoParser\SourceHandler\SourceHandler;
9
use Sepia\PoParser\SourceHandler\StringSource;
10
11
/**
12
 *    Copyright (c) 2012 Raúl Ferràs [email protected]
13
 *    All rights reserved.
14
 *
15
 *    Redistribution and use in source and binary forms, with or without
16
 *    modification, are permitted provided that the following conditions
17
 *    are met:
18
 *    1. Redistributions of source code must retain the above copyright
19
 *       notice, this list of conditions and the following disclaimer.
20
 *    2. Redistributions in binary form must reproduce the above copyright
21
 *       notice, this list of conditions and the following disclaimer in the
22
 *       documentation and/or other materials provided with the distribution.
23
 *    3. Neither the name of copyright holders nor the names of its
24
 *       contributors may be used to endorse or promote products derived
25
 *       from this software without specific prior written permission.
26
 *
27
 *    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28
 *    ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
29
 *    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
30
 *    PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
31
 *    BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32
 *    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33
 *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34
 *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35
 *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
37
 *    POSSIBILITY OF SUCH DAMAGE.
38
 *
39
 * https://github.com/raulferras/PHP-po-parser
40
 *
41
 * Class to parse .po file and extract its strings.
42
 *
43
 * @version 5.0
44
 */
45
class Parser
46
{
47
    /** @var SourceHandler */
48
    protected $sourceHandler;
49
50
    /** @var int */
51
    protected $lineNumber;
52
53
    /** @var string */
54
    protected $property;
55
56
    /**
57
     * Reads and parses a string
58
     *
59
     * @param string $string po content
60
     *
61
     * @throws \Exception.
62
     * @return Parser
63
     */
64
    public static function parseString($string)
65
    {
66
        $parser = new Parser(new StringSource($string));
67
        $parser->parse();
68
69
        return $parser;
70
    }
71
72
    /**
73
     * Reads and parses a file
74
     *
75
     * @param string $filePath
76
     *
77
     * @throws \Exception.
78
     * @return Catalog
79
     */
80
    public static function parseFile($filePath)
81
    {
82
        $parser = new Parser(new FileSystem($filePath));
83
84
        return $parser->parse();
85
    }
86
87
    public function __construct(SourceHandler $sourceHandler)
88
    {
89
        $this->sourceHandler = $sourceHandler;
90
    }
91
92
    /**
93
     * Reads and parses strings of a .po file.
94
     *
95
     * @param SourceHandler . Optional
96
     *
97
     * @throws \Exception, \InvalidArgumentException, ParseException
98
     * @return Catalog
99
     */
100
    public function parse()
101
    {
102
        $catalog = new Catalog();
103
        $this->lineNumber = 0;
104
        $entry = array();
105
        $this->mode = null;     // current mode
0 ignored issues
show
Bug introduced by
The property mode does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
106
        $this->property = null; // current property
107
108
        // Flags
109
        $headersFound = false;
110
111
        while (!$this->sourceHandler->ended()) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
112
113
            $line = trim($this->sourceHandler->getNextLine());
114
115
            if ($this->shouldIgnoreLine($line, $entry)) {
116
                $this->lineNumber++;
117
                continue;
118
            }
119
120
            if ($this->shouldCloseEntry($line, $entry)) {
121
                if (!$headersFound && $this->isHeader($entry)) {
122
                    $headersFound = true;
123
                    $catalog->addHeaders(
124
                        $this->parseHeaders($entry['msgstr'])
125
                    );
126
                } else {
127
                    $catalog->addEntry(EntryFactory::createFromArray($entry));
128
                }
129
130
                $entry = array();
131
                $this->property = null;
132
133
                if (empty($line)) {
134
                    $this->lineNumber++;
135
                    continue;
136
                }
137
            }
138
139
            $entry = $this->parseLine($line, $entry);
140
141
            $this->lineNumber++;
142
            continue;
143
        }
144
        $this->sourceHandler->close();
145
146
        // add final entry
147
        if (count($entry)) {
148
            $catalog->addEntry(EntryFactory::createFromArray($entry));
149
        }
150
151
        return $catalog;
152
    }
153
154
    /**
155
     * @param string $line
156
     * @param array  $entry
157
     *
158
     * @return array
159
     * @throws ParseException
160
     */
161
    protected function parseLine($line, $entry)
162
    {
163
        $firstChar = strlen($line) > 0 ? $line[0] : '';
164
165
        switch ($firstChar) {
166
            case '#':
167
                $entry = $this->parseComment($line, $entry);
168
                break;
169
170
            case 'm':
171
                $entry = $this->parseProperty($line, $entry);
172
                break;
173
174
            case '"':
175
                $entry = $this->parseMultiline($line, $entry);
176
                break;
177
        }
178
179
        return $entry;
180
    }
181
182
    /**
183
     * @param string $line
184
     * @param array  $entry
185
     *
186
     * @return array
187
     * @throws ParseException
188
     */
189
    protected function parseProperty($line, array $entry)
190
    {
191
        list($key, $value) = $this->getProperty($line);
192
193
        if (!isset($entry[$key])) {
194
            $entry[$key] = '';
195
        }
196
197
        switch (true) {
198
            case $key === 'msgctxt':
199
            case $key === 'msgid':
200
            case $key === 'msgid_plural':
201
            case $key === 'msgstr':
202
                $entry[$key] .= $this->unquote($value);
203
                $this->property = $key;
204
                break;
205
206
            case strpos($key, 'msgstr[') !== false:
207
                $entry[$key] .= $this->unquote($value);
208
                $this->property = $key;
209
                break;
210
211
            default:
212
                throw new ParseException(sprintf('Could not parse %s at line %d', $key, $this->lineNumber));
213
        }
214
215
        return $entry;
216
    }
217
218
    /**
219
     * @param string $line
220
     * @param array  $entry
221
     *
222
     * @return array
223
     * @throws ParseException
224
     */
225
    protected function parseMultiline($line, $entry)
226
    {
227
        switch (true) {
228
            case $this->property === 'msgctxt':
229
            case $this->property === 'msgid':
230
            case $this->property === 'msgid_plural':
231
            case $this->property === 'msgstr':
232
            case strpos($this->property, 'msgstr[') !== false:
233
                $entry[$this->property] .= $this->unquote($line);
234
                break;
235
236
            default:
237
                throw new ParseException(
238
                    sprintf('Error parsing property %s as multiline.', $this->property)
239
                );
240
        }
241
242
        return $entry;
243
    }
244
245
    /**
246
     * @param string $line
247
     * @param array  $entry
248
     *
249
     * @return array
250
     * @throws ParseException
251
     */
252
    protected function parseComment($line, $entry)
253
    {
254
        $comment = trim(substr($line, 0, 2));
255
256
        switch ($comment) {
257
            case '#,':
258
                $line = trim(substr($line, 2));
259
                $entry['flags'] = preg_split('/,\s*/', $line);
260
                break;
261
262
            case '#.':
263
                $entry['ccomment'] = !isset($entry['ccomment']) ? array() : $entry['ccomment'];
264
                $entry['ccomment'][] = trim(substr($line, 2));
265
                break;
266
267
268
            case '#|':  // Previous string
269
            case '#~':  // Old entry
270
            case '#~|': // Previous string old
271
                $mode = array(
272
                    '#|' => 'previous',
273
                    '#~' => 'obsolete',
274
                    '#~|' => 'previous-obsolete'
275
                );
276
277
                $line = trim(substr($line, 2));
278
                $property = $mode[$comment];
279
                if (!isset($entry[$property])) {
280
                    $subEntry = array();
281
                } else {
282
                    $subEntry = $entry[$property];
283
                }
284
285
                $subEntry = $this->parseLine($line, $subEntry);
286
                //$subEntry = $this->parseProperty($line, $subEntry);
287
                $entry[$property] = $subEntry;
288
                break;
289
290
291
            case '#':
292
            default:
293
                $entry['tcomment'] = !isset($entry['tcomment']) ? array() : $entry['tcomment'];
294
                $entry['tcomment'][] = trim(substr($line, 1));
295
                break;
296
        }
297
298
        return $entry;
299
    }
300
301
    /**
302
     * @param string $msgstr
303
     *
304
     * @return array
305
     */
306
    protected function parseHeaders($msgstr)
307
    {
308
        $headers = array_filter(explode('\\n', $msgstr));
309
310
        return $headers;
311
    }
312
313
    /**
314
     * @param string $line
315
     * @param array  $entry
316
     *
317
     * @return bool
318
     */
319
    protected function shouldIgnoreLine($line, array $entry)
320
    {
321
        return empty($line) && count($entry) === 0;
322
    }
323
324
    /**
325
     * @param string $line
326
     * @param array  $entry
327
     *
328
     * @return bool
329
     */
330
    protected function shouldCloseEntry($line, array $entry)
331
    {
332
        $tokens = $this->getProperty($line);
333
        $property = $tokens[0];
334
335
        return ($line === '' || ($property === 'msgid' && isset($entry['msgid'])));
336
    }
337
338
    /**
339
     * @param string $value
340
     * @return string
341
     */
342
    protected function unquote($value)
343
    {
344
        return preg_replace('/^\"|\"$/', '', $value);
345
    }
346
347
    /**
348
     * Checks if entry is a header by
349
     *
350
     * @param array $entry
351
     *
352
     * @return bool
353
     */
354
    protected function isHeader(array $entry)
355
    {
356
        if (empty($entry) || !isset($entry['msgstr'])) {
357
            return false;
358
        }
359
360
        if (!isset($entry['msgid']) || !empty($entry['msgid'])) {
361
            return false;
362
        }
363
364
        $standardHeaders = array(
365
            'Project-Id-Version:',
366
            'Report-Msgid-Bugs-To:',
367
            'POT-Creation-Date:',
368
            'PO-Revision-Date:',
369
            'Last-Translator:',
370
            'Language-Team:',
371
            'MIME-Version:',
372
            'Content-Type:',
373
            'Content-Transfer-Encoding:',
374
            'Plural-Forms:',
375
        );
376
377
        $headers = explode('\n', $entry['msgstr']);
378
        // Remove text after double colon
379
        $headers = array_map(
380
            function ($header) {
381
                $pattern = '/(.*?:)(.*)/i';
382
                $replace = '${1}';
383
                return preg_replace($pattern, $replace, $header);
384
            },
385
            $headers
386
        );
387
388
        if (count(array_intersect($standardHeaders, $headers)) > 0) {
389
            return true;
390
        }
391
392
        // If it does not contain any of the standard headers
393
        // Let's see if it contains any custom header.
394
        $customHeaders = array_filter(
395
            $headers,
396
            function ($header) {
397
                return preg_match('/^X\-(.*):/i', $header) === 1;
398
            }
399
        );
400
401
        return count($customHeaders) > 0;
402
    }
403
404
    /**
405
     * @param string $line
406
     *
407
     * @return array
408
     */
409
    protected function getProperty($line)
410
    {
411
        $tokens = preg_split('/\s+/ ', $line, 2);
412
413
        return $tokens;
414
    }
415
}
416