Test Failed
Push — master ( c19a70...4a5f5f )
by Sebastian
05:20
created

Localization_Parser_Language::parseExplanation()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 20
c 1
b 0
f 0
nc 10
nop 2
dl 0
loc 41
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AppLocalize;
6
7
use AppLocalize\Parser\Text;
8
9
abstract class Localization_Parser_Language
10
{
11
    const ERROR_SOURCE_FILE_NOT_FOUND = 40501;
12
    const ERROR_FAILED_READING_SOURCE_FILE = 40502;
13
14
    /**
15
     * @var bool
16
     */
17
    protected $debug = false;
18
    
19
    /**
20
     * @var Localization_Parser
21
     */
22
    protected $parser;
23
24
   /**
25
    * The function names that are included in the search.
26
    * @var array
27
    */
28
    protected $functionNames = array();
29
    
30
   /**
31
    * The tokens definitions.
32
    * @var array
33
    */
34
    protected $tokens = array();
35
    
36
   /**
37
    * The total amount of tokens found in the content.
38
    * @var integer
39
    */
40
    protected $totalTokens = 0;
41
    
42
   /**
43
    * All texts that have been collected.
44
    * @var Text[]
45
    */
46
    protected $texts = array();
47
    
48
   /**
49
    * @var string
50
    */
51
    protected $content = '';
52
53
   /**
54
    * @var string|NULL
55
    */
56
    protected $id;
57
    
58
   /**
59
    * @var Localization_Parser_Warning[]
60
    */
61
    protected $warnings = array();
62
    
63
   /**
64
    * The source file that was parsed (if any)
65
    * @var string
66
    */
67
    protected $sourceFile = '';
68
    
69
    public function __construct(Localization_Parser $parser)
70
    {
71
        $this->parser = $parser;
72
        $this->functionNames = $this->getFunctionNames();
73
    }
74
    
75
    abstract protected function getTokens() : array;
76
    
77
   /**
78
    * Retrieves the ID of the language.
79
    * @return string E.g. "PHP", "Javascript"
80
    */
81
    public function getID() : string
82
    {
83
        if(!isset($this->id)) {
84
            $this->id = str_replace(Localization_Parser_Language::class.'_', '', get_class($this));
85
        }
86
        
87
        return $this->id;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->id could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
88
    }
89
    
90
    public function hasSourceFile() : bool
91
    {
92
        return !empty($this->sourceFile);
93
    }
94
    
95
    public function getSourceFile() : string
96
    {
97
        return $this->sourceFile;
98
    }
99
    
100
   /**
101
    * Parses the code from a file.
102
    * 
103
    * @param string $path
104
    * @throws Localization_Exception
105
    */
106
    public function parseFile(string $path) : void
107
    {
108
        if(!file_exists($path)) 
109
        {
110
            throw new Localization_Exception(
111
                sprintf('Source code file [%s] not found', basename($path)),
112
                sprintf(
113
                    'Tried looking for the file in path [%s].',
114
                    $path
115
                ),
116
                self::ERROR_SOURCE_FILE_NOT_FOUND
117
            );
118
        }
119
        
120
        $this->sourceFile = $path;
121
        $this->content = file_get_contents($path);
122
        
123
        if($this->content !== false) {
124
            $this->parse();
125
            return;
126
        }
127
        
128
        throw new Localization_Exception(
129
            sprintf('Source code file [%s] could not be read', basename($path)),
130
            sprintf(
131
                'Tried opening the file located at [%s].',
132
                $path
133
            ),
134
            self::ERROR_FAILED_READING_SOURCE_FILE
135
        );
136
    }
137
    
138
   /**
139
    * Parses a source code string.
140
    * @param string $content
141
    */
142
    public function parseString($content) : void
143
    {
144
        $this->content = $content;
145
        $this->sourceFile = '';
146
        
147
        $this->parse();
148
    }
149
    
150
    protected function parse()
151
    {
152
        $this->texts = array();
153
        $this->warnings = array();
154
        $this->tokens = $this->getTokens();
155
        $this->totalTokens = count($this->tokens);
156
        
157
        for($i = 0; $i < $this->totalTokens; $i++)
158
        {
159
            $token = $this->createToken($this->tokens[$i]);
160
            
161
            if($token->isTranslationFunction()) {
162
                $this->parseToken($i+1, $token);
163
            }
164
        }
165
    }
166
167
    /**
168
     * @return Text[]
169
     */
170
    public function getTexts() : array
171
    {
172
        return $this->texts;
173
    }
174
    
175
    protected function addResult(string $text, int $line=0, string $explanation='')
176
    {
177
        $this->log(sprintf('Line [%1$s] | Found string [%2$s]', $line, $text));
178
179
        $this->texts[] = new Text($text, $line, $explanation);
180
    }
181
182
   /**
183
    * Retrieves a list of all the function names that are
184
    * used as translation functions in the language.
185
    * @return array
186
    */
187
    public function getFunctionNames() : array
188
    {
189
        return $this->createToken('dummy')->getFunctionNames();
190
    }
191
192
    protected function log($message)
193
    {
194
        Localization::log(sprintf('%1$s parser | %2$s', $this->getID(), $message));
195
    }
196
197
   /**
198
    * Adds a warning message when a text cannot be parsed correctly for some reason.
199
    * 
200
    * @param Localization_Parser_Token $token
201
    * @param string $message
202
    * @return Localization_Parser_Warning
203
    */
204
    protected function addWarning(Localization_Parser_Token $token, string $message) : Localization_Parser_Warning
205
    {
206
        $warning = new Localization_Parser_Warning($this, $token, $message);
207
        
208
        $this->warnings[] = $warning;
209
        
210
        return $warning;
211
    }
212
    
213
   /**
214
    * Whether any warnings were generated during parsing.
215
    * @return bool
216
    */
217
    public function hasWarnings() : bool
218
    {
219
        return !empty($this->warnings);
220
    }
221
    
222
   /**
223
    * Retrieves all warnings that were generated during parsing,
224
    * if any.
225
    * 
226
    * @return Localization_Parser_Warning[]
227
    */
228
    public function getWarnings() : array
229
    {
230
        return $this->warnings;
231
    }
232
    
233
   /**
234
    * Creates a token instance: this retrieves information on
235
    * the language token being parsed.
236
    * 
237
    * @param array|string $definition The token definition.
238
    * @param Localization_Parser_Token|NULL $parentToken
239
    * @return Localization_Parser_Token
240
    */
241
    protected function createToken($definition, Localization_Parser_Token $parentToken=null) : Localization_Parser_Token
242
    {
243
        $class = Localization_Parser_Token::class.'_'.$this->getID();
244
        
245
        return new $class($definition, $parentToken);
246
    }
247
248
   /**
249
    * Parses a translation function token.
250
    * 
251
    * @param int $number
252
    * @param Localization_Parser_Token $token
253
    */
254
    protected function parseToken(int $number, Localization_Parser_Token $token) : void
255
    {
256
        $textParts = array();
257
        $max = $number + 200;
258
        $open = false;
259
        $explanation = '';
260
        
261
        for($i = $number; $i < $max; $i++)
262
        {
263
            if(!isset($this->tokens[$i])) {
264
                break;
265
            }
266
            
267
            $subToken = $this->createToken($this->tokens[$i], $token);
268
            
269
            if(!$open && $subToken->isOpeningFuncParams())
270
            {
271
                $open = true;
272
                continue;
273
            }
274
            
275
            if($open && $subToken->isClosingFuncParams()) {
276
                break;
277
            }
278
            
279
            // additional parameters in the translation function, we don't want to capture these now.
280
            if($open && $subToken->isArgumentSeparator())
281
            {
282
                if($token->isExplanationFunction()) {
283
                    $leftover = array_slice($this->tokens, $i+1);
284
                    $explanation = $this->parseExplanation($token, $leftover);
285
                }
286
                break;
287
            }
288
            
289
            if($open && $subToken->isEncapsedString())
290
            {
291
                $textParts[] = $this->trimText($subToken->getValue());
292
                continue;
293
            }
294
            
295
            if($open && $subToken->isVariableOrFunction()) {
296
                $textParts = null;
297
                $this->addWarning($subToken, t('Variables or functions are not supported in translation functions.'));
298
                break;
299
            }
300
        }
301
        
302
        if(empty($textParts)) {
303
            return;
304
        }
305
        
306
        $text = implode('', $textParts);
307
        
308
        $this->addResult($text, $token->getLine(), $explanation);
309
    }
310
311
    private function parseExplanation(Localization_Parser_Token $token, array $tokens) : string
312
    {
313
        $textParts = array();
314
        $max = 200;
315
316
        for($i = 0; $i < $max; $i++)
317
        {
318
            if(!isset($tokens[$i])) {
319
                break;
320
            }
321
322
            $subToken = $this->createToken($tokens[$i], $token);
323
324
            if($subToken->isClosingFuncParams()) {
325
                break;
326
            }
327
328
            // additional parameters in the translation function, we don't want to capture these now.
329
            if($subToken->isArgumentSeparator())
330
            {
331
                break;
332
            }
333
334
            if($subToken->isEncapsedString())
335
            {
336
                $textParts[] = $this->trimText($subToken->getValue());
337
                continue;
338
            }
339
340
            if($subToken->isVariableOrFunction()) {
341
                $textParts = null;
342
                $this->addWarning($subToken, t('Variables or functions are not supported in translation functions.'));
343
                break;
344
            }
345
        }
346
347
        if(empty($textParts)) {
348
            return '';
349
        }
350
351
        return implode('', $textParts);
352
    }
353
354
    protected function debug($text)
355
    {
356
        if($this->debug) {
357
            echo $text;
358
        }
359
    }
360
361
    /**
362
     * Used to trim the text from the code. Also strips slashes
363
     * from the text, as it comes raw from the code.
364
     *
365
     * @param string $text
366
     * @return string
367
     */
368
    public function trimText($text)
369
    {
370
        return stripslashes(trim($text, "'\""));
371
    }
372
}