Completed
Push — feature/5.0.1 ( 4d2f9f...e57849 )
by Raúl
01:19
created

Parser::parse()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 51
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 51
rs 6.5978
c 0
b 0
f 0
cc 8
eloc 30
nc 14
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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