Issues (10)

src/PoCompiler.php (6 issues)

1
<?php
2
3
namespace Sepia\PoParser;
4
5
use Sepia\PoParser\Catalog\Catalog;
6
use Sepia\PoParser\Catalog\Entry;
7
use Sepia\PoParser\Catalog\Header;
8
9
class PoCompiler
10
{
11
    /** @var string */
12
    const TOKEN_OBSOLETE = '#~ ';
13
14
    /** @var int */
15
    protected $wrappingColumn;
16
17
    /** @var string */
18
    protected $lineEnding;
19
20
    /** @var string */
21
    protected $tokenCarriageReturn;
22
23
    /**
24
     * PoCompiler constructor.
25
     *
26
     * @param int    $wrappingColumn
27
     * @param string $lineEnding
28
     */
29
    public function __construct($wrappingColumn = 80, $lineEnding = "\n")
30
    {
31
        $this->wrappingColumn = $wrappingColumn;
32
        $this->lineEnding = $lineEnding;
33
        $this->tokenCarriageReturn = chr(13);
34
    }
35
36
    /**
37
     * Compiles entries into a string
38
     *
39
     * @param Catalog $catalog
40
     *
41
     * @return string
42
     * @throws \Exception
43
     * @todo Write obsolete messages at the end of the file.
44
     */
45
    public function compile(Catalog $catalog)
46
    {
47
        $output = '';
48
49
        if (\count($catalog->getHeaders()) > 0) {
50
            $output .= 'msgid ""'.$this->eol();
51
            $output .= 'msgstr ""'.$this->eol();
52
            foreach ($catalog->getHeaders() as $header) {
53
                $output .= '"'.$header.'\n"'.$this->eol();
54
            }
55
            $output .= $this->eol();
56
        }
57
58
59
        $entriesCount = \count($catalog->getEntries());
60
        $counter = 0;
61
        foreach ($catalog->getEntries() as $entry) {
62
            if ($entry->isObsolete() === false) {
63
                $output .= $this->buildPreviousEntry($entry);
64
                $output .= $this->buildTranslatorComment($entry);
65
                $output .= $this->buildDeveloperComment($entry);
66
                $output .= $this->buildReference($entry);
67
            }
68
69
            $output .= $this->buildFlags($entry);
70
71
//            if (isset($entry['@'])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
72
//                $output .= "#@ ".$entry['@'].$this->eol();
73
//            }
74
75
            $output .= $this->buildContext($entry);
76
            $output .= $this->buildMsgId($entry);
77
            $output .= $this->buildMsgIdPlural($entry);
78
            $output .= $this->buildMsgStr($entry, $catalog->getHeader());
79
80
81
            $counter++;
82
            // Avoid inserting an extra newline at end of file
83
            if ($counter < $entriesCount) {
84
                $output .= $this->eol();
85
            }
86
        }
87
88
        return $output;
89
    }
90
91
    /**
92
     * @return string
93
     */
94
    protected function eol()
95
    {
96
        return $this->lineEnding;
97
    }
98
99
    /**
100
     * @param $entry
101
     *
102
     * @return string
103
     */
104
    protected function buildPreviousEntry(Entry $entry)
105
    {
106
        $previous = $entry->getPreviousEntry();
107
        if ($previous === null) {
108
            return '';
109
        }
110
111
        return '#| msgid '.$this->cleanExport($previous->getMsgId()).$this->eol();
112
    }
113
114
    /**
115
     * @param $entry
116
     *
117
     * @return string
118
     */
119
    protected function buildTranslatorComment(Entry $entry)
120
    {
121
        if ($entry->getTranslatorComments() === null) {
0 ignored issues
show
The condition $entry->getTranslatorComments() === null is always false.
Loading history...
122
            return '';
123
        }
124
125
        $output = '';
126
        foreach ($entry->getTranslatorComments() as $comment) {
127
            $output .= '# '.$comment.$this->eol();
128
        }
129
130
        return $output;
131
    }
132
133
    protected function buildDeveloperComment(Entry $entry)
134
    {
135
        if ($entry->getDeveloperComments() === null) {
0 ignored issues
show
The condition $entry->getDeveloperComments() === null is always false.
Loading history...
136
            return '';
137
        }
138
139
        $output = '';
140
        foreach ($entry->getDeveloperComments() as $comment) {
141
            $output .= '#. '.$comment.$this->eol();
142
        }
143
144
        return $output;
145
    }
146
147
    protected function buildReference(Entry $entry)
148
    {
149
        $reference = $entry->getReference();
150
        if ($reference === null || \count($reference) === 0) {
151
            return '';
152
        }
153
154
        $output = '';
155
        foreach ($reference as $ref) {
156
            $output .= '#: '.$ref.$this->eol();
157
        }
158
159
        return $output;
160
    }
161
162
    protected function buildFlags(Entry $entry)
163
    {
164
        $flags = $entry->getFlags();
165
        if ($flags === null || \count($flags) === 0) {
166
            return '';
167
        }
168
169
        return '#, '.\implode(', ', $flags).$this->eol();
170
    }
171
172
    protected function buildContext(Entry $entry)
173
    {
174
        if ($entry->getMsgCtxt() === null) {
175
            return '';
176
        }
177
178
        return
179
            ($entry->isObsolete() ? '#~ ' : '').
180
            'msgctxt '.$this->cleanExport($entry->getMsgCtxt()).$this->eol();
181
    }
182
183
    protected function buildMsgId(Entry $entry)
184
    {
185
        if ($entry->getMsgId() === null) {
0 ignored issues
show
The condition $entry->getMsgId() === null is always false.
Loading history...
186
            return '';
187
        }
188
189
        return $this->buildProperty('msgid', $entry->getMsgId(), $entry->isObsolete());
190
    }
191
192
    protected function buildMsgStr(Entry $entry, Header $headers)
193
    {
194
        $value = $entry->getMsgStr();
195
        $plurals = $entry->getMsgStrPlurals();
196
197
        if ($value === null && $plurals === null) {
0 ignored issues
show
The condition $value === null is always false.
Loading history...
198
            return '';
199
        }
200
201
        if ($entry->isPlural()) {
202
            $output = '';
203
            $nPlurals = $headers->getPluralFormsCount();
204
            $pluralsFound = \count($plurals);
205
            $maxIterations = \max($nPlurals, $pluralsFound);
206
            for ($i = 0; $i < $maxIterations; $i++) {
207
                $value = isset($plurals[$i]) ? $plurals[$i] : '';
208
                $output .= $entry->isObsolete() ? self::TOKEN_OBSOLETE : '';
209
                $output .= 'msgstr['.$i.'] '.$this->cleanExport($value).$this->eol();
210
            }
211
212
            return $output;
213
        }
214
215
        return $this->buildProperty('msgstr', $value, $entry->isObsolete());
216
    }
217
218
    /**
219
     * @param Entry $entry
220
     *
221
     * @return string
222
     */
223
    protected function buildMsgIdPlural(Entry $entry)
224
    {
225
        $value = $entry->getMsgIdPlural();
226
        if ($value === null) {
0 ignored issues
show
The condition $value === null is always false.
Loading history...
227
            return '';
228
        }
229
230
        $output = '';
231
        $output .= $entry->isObsolete() ? self::TOKEN_OBSOLETE : '';
232
        $output .= 'msgid_plural '.$this->cleanExport($value).$this->eol();
233
        return $output;
234
    }
235
236
    protected function buildProperty($property, $value, $obsolete = false)
237
    {
238
        $tokens = $this->wrapString($value);
239
240
        $output = '';
241
        if (\count($tokens) > 1) {
242
            \array_unshift($tokens, '');
243
        }
244
245
        foreach ($tokens as $i => $token) {
246
            $output .= $obsolete ? self::TOKEN_OBSOLETE : '';
247
            $output .= ($i === 0) ? $property.' ' : '';
248
            $output .= $this->cleanExport($token).$this->eol();
249
        }
250
251
        return $output;
252
    }
253
254
    /**
255
     * Prepares a string to be outputed into a file.
256
     *
257
     * @param string $string The string to be converted.
258
     *
259
     * @return string
260
     */
261
    protected function cleanExport($string)
262
    {
263
        // only quotation mark (" or \42) and backslash (\ or \134) chars needs to be escaped
264
        $string = sprintf('"%s"', addcslashes($string, "\42\134"));
265
266
        // Replace newline character with \n after addcslashes to prevent escaping backslash.
267
        return str_replace($this->tokenCarriageReturn, '\n', $string);
268
    }
269
270
    /**
271
     * @param string $value
272
     * @return array
273
     */
274
    private function wrapString($value)
275
    {
276
        // value that are most likely never present in the $value
277
        $fileSeparator = chr(28);
278
279
        /**
280
         * Replace newline character with the same value that is used for wrapping ($fileSeparator)
281
         * and with another character ($this->tokenCarriageReturn) that is later replaced back and thus
282
         * newline won't be escaped by addcslashes function
283
         */
284
        $value = str_replace("\n", $this->tokenCarriageReturn . $fileSeparator, $value);
285
        $wrapped = \wordwrap($value, $this->wrappingColumn, ' ' . $fileSeparator);
286
287
        return \explode(chr(28), $wrapped);
288
    }
289
}
290