Completed
Push — feature/5.0.1 ( ac6a3e...d50cc3 )
by Raúl
01:16
created

Parser::unquote()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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