Completed
Push — master ( 81c05c...94aadf )
by Oscar
9s
created

VueJs   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 0
loc 221
rs 10
c 0
b 0
f 0
wmc 20
lcom 1
cbo 2

8 Methods

Rating   Name   Duplication   Size   Complexity  
B fromString() 0 41 3
A convertHtmlToDom() 0 11 1
A getScriptTranslationsFromString() 0 10 1
A getTemplateTranslations() 0 16 1
A getTemplateAttributeFakeJs() 0 16 3
C getVueAttributeExpressions() 0 32 7
A getTemplateFakeJs() 0 14 2
A parseOneTemplateLine() 0 18 2
1
<?php
2
3
namespace Gettext\Extractors;
4
5
use DOMAttr;
6
use DOMDocument;
7
use DOMElement;
8
use Gettext\Translations;
9
use Gettext\Utils\JsFunctionsScanner;
10
11
/**
12
 * Class to get gettext strings from VueJS template files.
13
 */
14
class VueJs extends JsCode implements ExtractorInterface
15
{
16
    /**
17
     * @inheritdoc
18
     * @throws \Exception
19
     */
20
    public static function fromString($string, Translations $translations, array $options = [])
21
    {
22
        $options += self::$options;
23
24
        // Ok, this is the weirdest hack, but let me explain:
25
        // On Linux (Mac is fine), when converting HTML to DOM, new lines get trimmed after the first tag.
26
        // So if there are new lines between <template> and next element, they are lost
27
        // So we insert a "." which is a text node, and it will prevent that newlines are stripped between elements.
28
        // Same thing happens between template and script tag.
29
        $string = str_replace('<template>', '<template>.', $string);
30
        $string = str_replace('</template>', '</template>.', $string);
31
32
        // Normalize newlines
33
        $string = str_replace(["\r\n", "\n\r", "\r"], "\n", $string);
34
35
        // VueJS files are valid HTML files, we will operate with the DOM here
36
        $dom = self::convertHtmlToDom($string);
37
38
        // Parse the script part as a regular JS code
39
        $script = $dom->getElementsByTagName('script')->item(0);
40
        if ($script) {
41
            self::getScriptTranslationsFromString(
42
                $script->textContent,
43
                $translations,
44
                $options,
45
                $script->getLineNo() - 1
46
            );
47
        }
48
49
        // Template part is parsed separately, all variables will be extracted
50
        // and handled as a regular JS code
51
        $template = $dom->getElementsByTagName('template')->item(0);
52
        if ($template) {
53
            self::getTemplateTranslations(
54
                $template,
55
                $translations,
56
                $options,
57
                $template->getLineNo() - 1
58
            );
59
        }
60
    }
61
62
    /**
63
     * @param string $html
64
     * @return DOMDocument
65
     */
66
    private static function convertHtmlToDom($html)
67
    {
68
        $dom = new DOMDocument;
69
70
        libxml_use_internal_errors(true);
71
        $dom->loadHTML($html);
72
73
        libxml_clear_errors();
74
75
        return $dom;
76
    }
77
78
    /**
79
     * Extract translations from script part
80
     *
81
     * @param string $scriptContents Only script tag contents, not the whole template
82
     * @param Translations $translations
83
     * @param array $options
84
     * @param int $lineOffset Number of lines the script is offset in the vue template file
85
     * @throws \Exception
86
     */
87
    private static function getScriptTranslationsFromString(
88
        $scriptContents,
89
        Translations $translations,
90
        array $options = [],
91
        $lineOffset = 0
92
    ) {
93
        $functions = new JsFunctionsScanner($scriptContents);
94
        $options['lineOffset'] = $lineOffset;
95
        $functions->saveGettextFunctions($translations, $options);
96
    }
97
98
    /**
99
     * Parse template to extract all translations (element content and dynamic element attributes)
100
     *
101
     * @param DOMElement $dom
102
     * @param Translations $translations
103
     * @param array $options
104
     * @param int $lineOffset Line number where the template part starts in the vue file
105
     * @throws \Exception
106
     */
107
    private static function getTemplateTranslations(
108
        DOMElement $dom,
109
        Translations $translations,
110
        array $options,
111
        $lineOffset = 0
112
    ) {
113
        // Build a JS string from all template attribute expressions
114
        $fakeAttributeJs = self::getTemplateAttributeFakeJs($dom);
115
116
        // 1 line offset is necessary because parent template element was ignored when converting to DOM
117
        self::getScriptTranslationsFromString($fakeAttributeJs, $translations, $options, $lineOffset);
118
119
        // Build a JS string from template element content expressions
120
        $fakeTemplateJs = self::getTemplateFakeJs($dom);
121
        self::getScriptTranslationsFromString($fakeTemplateJs, $translations, $options, $lineOffset);
122
    }
123
124
    /**
125
     * Extract JS expressions from element attribute bindings (excluding text within elements)
126
     * For example: <span :title="__('extract this')"> skip element content </span>
127
     *
128
     * @param DOMElement $dom
129
     * @return string JS code
130
     */
131
    private static function getTemplateAttributeFakeJs(DOMElement $dom)
132
    {
133
        $expressionsByLine = self::getVueAttributeExpressions($dom);
134
135
        $maxLines = max(array_keys($expressionsByLine));
136
        $fakeJs = '';
137
138
        for ($line = 1; $line <= $maxLines; $line++) {
139
            if (isset($expressionsByLine[$line])) {
140
                $fakeJs .= implode("; ", $expressionsByLine[$line]);
141
            }
142
            $fakeJs .= "\n";
143
        }
144
145
        return $fakeJs;
146
    }
147
148
    /**
149
     * Loop DOM element recursively and parse out all dynamic vue attributes which are basically JS expressions
150
     *
151
     * @param DOMElement $dom
152
     * @param array $expressionByLine [lineNumber => [jsExpression, ..], ..]
153
     * @return array [lineNumber => [jsExpression, ..], ..]
154
     */
155
    private static function getVueAttributeExpressions(DOMElement $dom, array &$expressionByLine = [])
156
    {
157
        $children = $dom->childNodes;
158
159
        for ($i = 0; $i < $children->length; $i++) {
160
            $node = $children->item($i);
161
162
            if (!($node instanceof DOMElement)) {
163
                continue;
164
            }
165
166
            $attrList = $node->attributes;
167
168
            for ($j = 0; $j < $attrList->length; $j++) {
0 ignored issues
show
Bug introduced by
The property length does not seem to exist in DOMNamedNodeMap.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
169
                /** @var DOMAttr $domAttr */
170
                $domAttr = $attrList->item($j);
171
172
                // Check if this is a dynamic vue attribute
173
                if (strpos($domAttr->name, ':') === 0 || strpos($domAttr->name, 'v-bind:') === 0) {
174
                    $line = $domAttr->getLineNo();
175
                    $expressionByLine += [$line => []];
176
                    $expressionByLine[$line][] = $domAttr->value;
177
                }
178
            }
179
180
            if ($node->hasChildNodes()) {
181
                $expressionByLine = self::getVueAttributeExpressions($node, $expressionByLine);
182
            }
183
        }
184
185
        return $expressionByLine;
186
    }
187
188
    /**
189
     * Extract JS expressions from within template elements (excluding attributes)
190
     * For example: <span :title="skip attributes"> {{__("extract element content")}} </span>
191
     *
192
     * @param DOMElement $dom
193
     * @return string JS code
194
     */
195
    private static function getTemplateFakeJs(DOMElement $dom)
196
    {
197
        $fakeJs = '';
198
        $lines = explode("\n", $dom->textContent);
199
200
        // Build a fake JS file from template by extracting JS expressions within each template line
201
        foreach ($lines as $line) {
202
            $expressionMatched = self::parseOneTemplateLine($line);
203
204
            $fakeJs .= implode("; ", $expressionMatched) . "\n";
205
        }
206
207
        return $fakeJs;
208
    }
209
210
    /**
211
     * Match JS expressions in a template line
212
     *
213
     * @param string $line
214
     * @return string[]
215
     */
216
    private static function parseOneTemplateLine($line)
217
    {
218
        $line = trim($line);
219
220
        if (!$line) {
221
            return [];
222
        }
223
224
        $regex = '#\{\{(.*?)\}\}#';
225
226
        preg_match_all($regex, $line, $matches);
227
228
        $matched = array_map(function ($v) {
229
            return trim($v, '\'"{}');
230
        }, $matches[1]);
231
232
        return $matched;
233
    }
234
}
235