Localization_Parser_Language::hasSourceFile()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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