Completed
Push — master ( 07f861...31eb2d )
by Iurii
02:00
created

Extractor::extractFromString()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
cc 4
eloc 12
nc 4
nop 2
1
<?php
2
3
/**
4
 * @package Extractor
5
 * @author Iurii Makukh <[email protected]>
6
 * @copyright Copyright (c) 2015, Iurii Makukh
7
 * @license https://www.gnu.org/licenses/gpl.html GNU/GPLv3
8
 */
9
10
namespace gplcart\modules\extractor\models;
11
12
use gplcart\core\Model;
13
14
/**
15
 * Methods to extract translatable strings from various source files
16
 */
17
class Extractor extends Model
18
{
19
20
    /**
21
     * Max parsing width in columns
22
     */
23
    const MAX_COLS = 500;
24
25
    /**
26
     * Max rows to parse per file
27
     */
28
    const MAX_LINES = 5000;
29
30
    /**
31
     * Pattern to extract strings from JS function GplCart.text()
32
     */
33
    const PATTERN_JS = '/GplCart.text\s*\(\s*([\'"])(.+?)\1\s*([\),])/s';
34
35
    /**
36
     * Pattern to extract strings from TWIG function {{ text() }}
37
     */
38
    const PATTERN_TWIG = '/text\s*\(\s*([\'"])(.+?)\1\s*([\),])/s';
39
40
    /**
41
     * Pattern to extract strings using inline @text annotation
42
     */
43
    const PATTERN_PHPDOC = '/\/\*(?:\*|\s)+@text(?:\*|\s)+\*\/\s*([\'"])(.+?)\1\s*/';
44
45
    /**
46
     * Pattern to extract strings from Language::text() method
47
     */
48
    const PATTERN_PHP = '/->text\s*\(\s*([\'"])(.+?)\1\s*([\),])/s';
49
50
    /**
51
     * Constructor
52
     */
53
    public function __construct()
54
    {
55
        parent::__construct();
56
    }
57
58
    /**
59
     * Returns an extractor by a file extension or an array of extractors keyed by extension
60
     * @param null|string $extension
61
     * @return array|string
62
     */
63
    public function get($extension = null)
64
    {
65
        $extractor = &gplcart_static(__METHOD__ . "$extension");
66
67
        if (isset($extractor)) {
68
            return $extractor;
69
        }
70
71
        $extractors = $this->getDefault();
72
73
        if (isset($extension)) {
74
            $extractor = empty($extractors[$extension]) ? '' : $extractors[$extension];
75
        } else {
76
            $extractor = $extractors;
77
        }
78
79
        $this->hook->attach('module.extractor.get', $extractor, $extension, $this);
80
        return $extractor;
81
    }
82
83
    /**
84
     * Returns an array of default extractors keyed by supported file extension
85
     * @return array
86
     */
87
    protected function getDefault()
88
    {
89
        return array(
90
            'json' => array($this, 'extractFromFileJson'),
91
            'js' => array(static::PATTERN_JS),
92
            'twig' => array(static::PATTERN_TWIG, static::PATTERN_JS),
93
            'php' => array(static::PATTERN_PHP, static::PATTERN_JS, static::PATTERN_PHPDOC)
94
        );
95
    }
96
97
    /**
98
     * Extract strings from module.json
99
     * @param string $file
100
     * @return array
101
     */
102
    protected function extractFromFileJson($file)
103
    {
104
        $extracted = array();
105
        if (basename($file) === 'module.json') {
106
            $content = json_decode(file_get_contents($file), true);
107
            if (!empty($content['name'])) {
108
                $extracted[] = $content['name'];
109
            }
110
111
            if (!empty($content['description'])) {
112
                $extracted[] = $content['description'];
113
            }
114
        }
115
116
        return $extracted;
117
    }
118
119
    /**
120
     * Returns an array of extracted strings from a file
121
     * @param string $file
122
     * @return array
123
     */
124
    public function extractFromFile($file)
125
    {
126
        $extractor = $this->get(pathinfo($file, PATHINFO_EXTENSION));
127
128
        if (empty($extractor)) {
129
            return array();
130
        }
131
132
        if (is_callable($extractor)) {
133
            return call_user_func_array($extractor, array($file));
134
        }
135
136
        $handle = fopen($file, 'r');
137
138
        if (!is_resource($handle)) {
139
            return array();
140
        }
141
142
        $lines = self::MAX_LINES;
143
144
        $extracted = array();
145
        while ($lines && $line = fgets($handle, self::MAX_COLS)) {
146
            $extracted = array_merge($extracted, $this->extractFromString($line, $extractor));
147
            $lines--;
148
        }
149
150
        fclose($handle);
151
        return $extracted;
152
    }
153
154
    /**
155
     * Returns an array of extracted strings from a source string
156
     * @param string $string
157
     * @param string|array $regexp_patterns
158
     * @return array
159
     */
160
    public function extractFromString($string, $regexp_patterns)
161
    {
162
        $result = null;
163
        $this->hook->attach('module.extractor.extract', $string, $regexp_patterns, $result, $this);
164
165
        if (isset($result)) {
166
            return $result;
167
        }
168
169
        $extracted = array();
170
        foreach ((array) $regexp_patterns as $pattern) {
171
            $matches = array();
172
            preg_match_all($pattern, $string, $matches);
173
            if (!empty($matches[2])) {
174
                $extracted = array_merge($extracted, $this->clean($matches[2]));
175
            }
176
        }
177
178
        return $extracted;
179
    }
180
181
    /**
182
     * Clean up an array of extracted strings or a single string
183
     * @param array|string $items
184
     * @return array
185
     */
186
    protected function clean($items)
187
    {
188
        $cleaned = array();
189
        foreach ((array) $items as $item) {
190
            // Remove double/single quotes and whitespaces from the beginning and end of a string
191
            $str = trim(preg_replace('/^(\'(.*)\'|"(.*)")$/', '$2$3', $item));
192
            if ($str !== '') {
193
                $cleaned[] = $str;
194
            }
195
        }
196
197
        return $cleaned;
198
    }
199
200
    /**
201
     * Returns an array of scanned files to extract from or counts them
202
     * @param array $options
203
     * @return integer|array
204
     */
205
    public function scan(array $options)
206
    {
207
        $result = null;
208
        $this->hook->attach('module.extractor.scan', $options, $result, $this);
209
210
        if (isset($result)) {
211
            return $result;
212
        }
213
214
        $scanned = array();
215
        foreach ((array) $options['directory'] as $directory) {
216
            $scanned = array_merge($scanned, $this->scanRecursive($directory));
217
        }
218
219
        $files = array_filter($scanned, function($file) {
220
            return $this->isSupportedFile($file);
221
        });
222
223
        if (!empty($options['count'])) {
224
            return count($files);
225
        }
226
227
        sort($files);
228
        return $files;
229
    }
230
231
    /**
232
     * Whether the file can be parsed
233
     * @param string $file
234
     * @return bool
235
     */
236
    public function isSupportedFile($file)
237
    {
238
        return is_file($file) && in_array(pathinfo($file, PATHINFO_EXTENSION), array_keys($this->get()));
239
    }
240
241
    /**
242
     * Returns an array of directories to be scanned
243
     * @return array
244
     */
245
    public function getScannedDirectories()
246
    {
247
        $directories = array(
248
            GC_CORE_DIR,
249
            GC_CONFIG_DEFAULT_DIR,
250
            $this->config->getModuleDirectory('frontend'),
251
            $this->config->getModuleDirectory('backend')
252
        );
253
254
        $this->hook->attach('module.extractor.directories', $directories, $this);
255
        return $directories;
256
    }
257
258
    /**
259
     * Recursive scans files in a directory
260
     * @param string $directory
261
     * @param array $results
262
     * @return array
263
     */
264
    protected function scanRecursive($directory, &$results = array())
265
    {
266
        $directory = str_replace('\\', '/', $directory);
267
268
        if (strpos($directory, '/override/') !== false) {
269
            return $results; // Exclude "override" directories
270
        }
271
272
        if (strpos($directory, '/vendor/') !== false) {
273
            return $results; // Exclude "vendor" directories
274
        }
275
276
        foreach (scandir($directory) as $file) {
277
            $path = "$directory/$file";
278
            if (!is_dir($path)) {
279
                $results[] = $path;
280
            } else if ($file != "." && $file != "..") {
281
                $this->scanRecursive($path, $results);
282
                $results[] = $path;
283
            }
284
        }
285
286
        return $results;
287
    }
288
289
}
290