Completed
Push — feature/5.0.1 ( d50cc3...537aa6 )
by Raúl
01:17
created

Parser::parseHeaders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
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->mode = null;
132
                $this->property = null;
133
134
                if (empty($line)) {
135
                    $this->lineNumber++;
136
                    continue;
137
                }
138
            }
139
140
            $firstChar = strlen($line) > 0 ? $line[0] : '';
141
142
            switch ($firstChar) {
143
                case '#':
144
                    $entry = $this->parseComment($line, $entry);
145
                    break;
146
147
                case 'm':
148
                    $entry = $this->parseProperty($line, $entry);
149
                    break;
150
151
                case '"':
152
                    $entry = $this->parseMultiline($line, $entry);
153
                    break;
154
            }
155
156
            $this->lineNumber++;
157
            continue;
158
        }
159
        $this->sourceHandler->close();
160
161
        // add final entry
162
        if (count($entry)) {
163
            $catalog->addEntry(EntryFactory::createFromArray($entry));
164
        }
165
166
        return $catalog;
167
    }
168
169
    /**
170
     * @param string $line
171
     * @param array  $entry
172
     *
173
     * @return bool
174
     */
175
    protected function shouldIgnoreLine($line, array $entry)
176
    {
177
        return empty($line) && count($entry) === 0;
178
    }
179
180
    /**
181
     * @param string $line
182
     * @param array  $entry
183
     *
184
     * @return bool
185
     */
186
    protected function shouldCloseEntry($line, array $entry)
187
    {
188
        $lineKey = '';
189
190
        return ($line === '' || ($lineKey === 'msgid' && isset($entry['msgid'])));
191
    }
192
193
    /**
194
     * Checks if entry is a header by
195
     *
196
     * @param array $entry
197
     *
198
     * @return bool
199
     */
200
    protected function isHeader(array $entry)
201
    {
202
        if (empty($entry) || !isset($entry['msgstr'])) {
203
            return false;
204
        }
205
206
        if (!isset($entry['msgid']) || !empty($entry['msgid'])) {
207
            return false;
208
        }
209
210
        $standardHeaders = array(
211
            'Project-Id-Version:',
212
            'Report-Msgid-Bugs-To:',
213
            'POT-Creation-Date:',
214
            'PO-Revision-Date:',
215
            'Last-Translator:',
216
            'Language-Team:',
217
            'MIME-Version:',
218
            'Content-Type:',
219
            'Content-Transfer-Encoding:',
220
            'Plural-Forms:',
221
        );
222
223
        $headers = explode('\n', $entry['msgstr']);
224
        // Remove text after double colon
225
        $headers = array_map(
226
            function ($header) {
227
                $pattern = '/(.*?:)(.*)/i';
228
                $replace = '${1}';
229
                return preg_replace($pattern, $replace, $header);
230
            },
231
            $headers
232
        );
233
234
        if (count(array_intersect($standardHeaders, $headers)) > 0) {
235
            return true;
236
        }
237
238
        // If it does not contain any of the standard headers
239
        // Let's see if it contains any custom header.
240
        $customHeaders = array_filter(
241
            $headers,
242
            function ($header) {
243
                return preg_match('/^X\-(.*):/i', $header) === 1;
244
            }
245
        );
246
247
        return count($customHeaders) > 0;
248
    }
249
250
    /**
251
     * @param string $line
252
     *
253
     * @return array
254
     */
255
    protected function getProperty($line)
256
    {
257
        $tokens = preg_split('/\s+/ ', $line, 2);
258
259
        return $tokens;
260
    }
261
262
    /**
263
     * @param string $line
264
     * @param array  $entry
265
     *
266
     * @return array
267
     * @throws ParseException
268
     */
269
    private function parseProperty($line, array $entry)
270
    {
271
        list($key, $value) = $this->getProperty($line);
272
273
        if (!isset($entry[$key])) {
274
            $entry[$key] = '';
275
        }
276
277
        switch (true) {
278
            case $key === 'msgctxt':
279
            case $key === 'msgid':
280
            case $key === 'msgid_plural':
281
            case $key === 'msgstr':
282
                $entry[$key] .= $this->unquote($value);
283
                $this->property = $key;
284
                break;
285
286
            case strpos($key, 'msgstr[') !== false:
287
                $entry[$key] .= $this->unquote($value);
288
                $this->property = $key;
289
                break;
290
291
            default:
292
                throw new ParseException(sprintf('Could not parse %s at line %d', $key, $this->lineNumber));
293
        }
294
295
        return $entry;
296
    }
297
298
    /**
299
     * @param string $line
300
     * @param array  $entry
301
     *
302
     * @return array
303
     * @throws ParseException
304
     */
305
    private function parseMultiline($line, $entry)
306
    {
307
        switch (true) {
308
            case $this->property === 'msgctxt':
309
            case $this->property === 'msgid':
310
            case $this->property === 'msgid_plural':
311
            case $this->property === 'msgstr':
312
            case strpos($this->property, 'msgstr[') !== false:
313
                $entry[$this->property] .= $this->unquote($line);
314
                break;
315
316
            default:
317
                throw new ParseException(
318
                    sprintf('Error parsing property %s as multiline.', $this->property)
319
                );
320
        }
321
322
        return $entry;
323
    }
324
325
    /**
326
     * @param string $line
327
     * @param array  $entry
328
     *
329
     * @return array
330
     */
331
    private function parseComment($line, $entry)
332
    {
333
        $comment = trim(substr($line, 0, 2));
334
335
        switch ($comment) {
336
            case '#,':
337
                $line = trim(substr($line, 2));
338
                $entry['flags'] = preg_split('/,\s*/', $line);
339
                break;
340
341
            case '#.':
342
                $entry['ccomment'] = !isset($entry['ccomment']) ? array() : $entry['ccomment'];
343
                $entry['ccomment'][] = trim(substr($line, 2));
344
                break;
345
346
            case '#':
347
            default:
348
                $entry['tcomment'] = !isset($entry['tcomment']) ? array() : $entry['tcomment'];
349
                $entry['tcomment'][] = trim(substr($line, 1));
350
                break;
351
        }
352
353
        return $entry;
354
    }
355
356
    private function parseHeaders($msgstr)
357
    {
358
        $headers = array_filter(explode('\\n', $msgstr));
359
360
361
362
        return $headers;
363
    }
364
365
    /**
366
     * @param string $value
367
     * @return string
368
     */
369
    private function unquote($value)
370
    {
371
        return preg_replace('/^\"|\"$/', '', $value);
372
    }
373
}
374