1 | var SyntaxReferenceError = require('./error').SyntaxReferenceError;
|
---|
2 | var SyntaxMatchError = require('./error').SyntaxMatchError;
|
---|
3 | var names = require('../utils/names');
|
---|
4 | var generic = require('./generic');
|
---|
5 | var parse = require('../definition-syntax/parse');
|
---|
6 | var generate = require('../definition-syntax/generate');
|
---|
7 | var walk = require('../definition-syntax/walk');
|
---|
8 | var prepareTokens = require('./prepare-tokens');
|
---|
9 | var buildMatchGraph = require('./match-graph').buildMatchGraph;
|
---|
10 | var matchAsTree = require('./match').matchAsTree;
|
---|
11 | var trace = require('./trace');
|
---|
12 | var search = require('./search');
|
---|
13 | var getStructureFromConfig = require('./structure').getStructureFromConfig;
|
---|
14 | var cssWideKeywords = buildMatchGraph('inherit | initial | unset');
|
---|
15 | var cssWideKeywordsWithExpression = buildMatchGraph('inherit | initial | unset | <-ms-legacy-expression>');
|
---|
16 |
|
---|
17 | function dumpMapSyntax(map, compact, syntaxAsAst) {
|
---|
18 | var result = {};
|
---|
19 |
|
---|
20 | for (var name in map) {
|
---|
21 | if (map[name].syntax) {
|
---|
22 | result[name] = syntaxAsAst
|
---|
23 | ? map[name].syntax
|
---|
24 | : generate(map[name].syntax, { compact: compact });
|
---|
25 | }
|
---|
26 | }
|
---|
27 |
|
---|
28 | return result;
|
---|
29 | }
|
---|
30 |
|
---|
31 | function dumpAtruleMapSyntax(map, compact, syntaxAsAst) {
|
---|
32 | const result = {};
|
---|
33 |
|
---|
34 | for (const [name, atrule] of Object.entries(map)) {
|
---|
35 | result[name] = {
|
---|
36 | prelude: atrule.prelude && (
|
---|
37 | syntaxAsAst
|
---|
38 | ? atrule.prelude.syntax
|
---|
39 | : generate(atrule.prelude.syntax, { compact })
|
---|
40 | ),
|
---|
41 | descriptors: atrule.descriptors && dumpMapSyntax(atrule.descriptors, compact, syntaxAsAst)
|
---|
42 | };
|
---|
43 | }
|
---|
44 |
|
---|
45 | return result;
|
---|
46 | }
|
---|
47 |
|
---|
48 | function valueHasVar(tokens) {
|
---|
49 | for (var i = 0; i < tokens.length; i++) {
|
---|
50 | if (tokens[i].value.toLowerCase() === 'var(') {
|
---|
51 | return true;
|
---|
52 | }
|
---|
53 | }
|
---|
54 |
|
---|
55 | return false;
|
---|
56 | }
|
---|
57 |
|
---|
58 | function buildMatchResult(match, error, iterations) {
|
---|
59 | return {
|
---|
60 | matched: match,
|
---|
61 | iterations: iterations,
|
---|
62 | error: error,
|
---|
63 | getTrace: trace.getTrace,
|
---|
64 | isType: trace.isType,
|
---|
65 | isProperty: trace.isProperty,
|
---|
66 | isKeyword: trace.isKeyword
|
---|
67 | };
|
---|
68 | }
|
---|
69 |
|
---|
70 | function matchSyntax(lexer, syntax, value, useCommon) {
|
---|
71 | var tokens = prepareTokens(value, lexer.syntax);
|
---|
72 | var result;
|
---|
73 |
|
---|
74 | if (valueHasVar(tokens)) {
|
---|
75 | return buildMatchResult(null, new Error('Matching for a tree with var() is not supported'));
|
---|
76 | }
|
---|
77 |
|
---|
78 | if (useCommon) {
|
---|
79 | result = matchAsTree(tokens, lexer.valueCommonSyntax, lexer);
|
---|
80 | }
|
---|
81 |
|
---|
82 | if (!useCommon || !result.match) {
|
---|
83 | result = matchAsTree(tokens, syntax.match, lexer);
|
---|
84 | if (!result.match) {
|
---|
85 | return buildMatchResult(
|
---|
86 | null,
|
---|
87 | new SyntaxMatchError(result.reason, syntax.syntax, value, result),
|
---|
88 | result.iterations
|
---|
89 | );
|
---|
90 | }
|
---|
91 | }
|
---|
92 |
|
---|
93 | return buildMatchResult(result.match, null, result.iterations);
|
---|
94 | }
|
---|
95 |
|
---|
96 | var Lexer = function(config, syntax, structure) {
|
---|
97 | this.valueCommonSyntax = cssWideKeywords;
|
---|
98 | this.syntax = syntax;
|
---|
99 | this.generic = false;
|
---|
100 | this.atrules = {};
|
---|
101 | this.properties = {};
|
---|
102 | this.types = {};
|
---|
103 | this.structure = structure || getStructureFromConfig(config);
|
---|
104 |
|
---|
105 | if (config) {
|
---|
106 | if (config.types) {
|
---|
107 | for (var name in config.types) {
|
---|
108 | this.addType_(name, config.types[name]);
|
---|
109 | }
|
---|
110 | }
|
---|
111 |
|
---|
112 | if (config.generic) {
|
---|
113 | this.generic = true;
|
---|
114 | for (var name in generic) {
|
---|
115 | this.addType_(name, generic[name]);
|
---|
116 | }
|
---|
117 | }
|
---|
118 |
|
---|
119 | if (config.atrules) {
|
---|
120 | for (var name in config.atrules) {
|
---|
121 | this.addAtrule_(name, config.atrules[name]);
|
---|
122 | }
|
---|
123 | }
|
---|
124 |
|
---|
125 | if (config.properties) {
|
---|
126 | for (var name in config.properties) {
|
---|
127 | this.addProperty_(name, config.properties[name]);
|
---|
128 | }
|
---|
129 | }
|
---|
130 | }
|
---|
131 | };
|
---|
132 |
|
---|
133 | Lexer.prototype = {
|
---|
134 | structure: {},
|
---|
135 | checkStructure: function(ast) {
|
---|
136 | function collectWarning(node, message) {
|
---|
137 | warns.push({
|
---|
138 | node: node,
|
---|
139 | message: message
|
---|
140 | });
|
---|
141 | }
|
---|
142 |
|
---|
143 | var structure = this.structure;
|
---|
144 | var warns = [];
|
---|
145 |
|
---|
146 | this.syntax.walk(ast, function(node) {
|
---|
147 | if (structure.hasOwnProperty(node.type)) {
|
---|
148 | structure[node.type].check(node, collectWarning);
|
---|
149 | } else {
|
---|
150 | collectWarning(node, 'Unknown node type `' + node.type + '`');
|
---|
151 | }
|
---|
152 | });
|
---|
153 |
|
---|
154 | return warns.length ? warns : false;
|
---|
155 | },
|
---|
156 |
|
---|
157 | createDescriptor: function(syntax, type, name, parent = null) {
|
---|
158 | var ref = {
|
---|
159 | type: type,
|
---|
160 | name: name
|
---|
161 | };
|
---|
162 | var descriptor = {
|
---|
163 | type: type,
|
---|
164 | name: name,
|
---|
165 | parent: parent,
|
---|
166 | syntax: null,
|
---|
167 | match: null
|
---|
168 | };
|
---|
169 |
|
---|
170 | if (typeof syntax === 'function') {
|
---|
171 | descriptor.match = buildMatchGraph(syntax, ref);
|
---|
172 | } else {
|
---|
173 | if (typeof syntax === 'string') {
|
---|
174 | // lazy parsing on first access
|
---|
175 | Object.defineProperty(descriptor, 'syntax', {
|
---|
176 | get: function() {
|
---|
177 | Object.defineProperty(descriptor, 'syntax', {
|
---|
178 | value: parse(syntax)
|
---|
179 | });
|
---|
180 |
|
---|
181 | return descriptor.syntax;
|
---|
182 | }
|
---|
183 | });
|
---|
184 | } else {
|
---|
185 | descriptor.syntax = syntax;
|
---|
186 | }
|
---|
187 |
|
---|
188 | // lazy graph build on first access
|
---|
189 | Object.defineProperty(descriptor, 'match', {
|
---|
190 | get: function() {
|
---|
191 | Object.defineProperty(descriptor, 'match', {
|
---|
192 | value: buildMatchGraph(descriptor.syntax, ref)
|
---|
193 | });
|
---|
194 |
|
---|
195 | return descriptor.match;
|
---|
196 | }
|
---|
197 | });
|
---|
198 | }
|
---|
199 |
|
---|
200 | return descriptor;
|
---|
201 | },
|
---|
202 | addAtrule_: function(name, syntax) {
|
---|
203 | if (!syntax) {
|
---|
204 | return;
|
---|
205 | }
|
---|
206 |
|
---|
207 | this.atrules[name] = {
|
---|
208 | type: 'Atrule',
|
---|
209 | name: name,
|
---|
210 | prelude: syntax.prelude ? this.createDescriptor(syntax.prelude, 'AtrulePrelude', name) : null,
|
---|
211 | descriptors: syntax.descriptors
|
---|
212 | ? Object.keys(syntax.descriptors).reduce((res, descName) => {
|
---|
213 | res[descName] = this.createDescriptor(syntax.descriptors[descName], 'AtruleDescriptor', descName, name);
|
---|
214 | return res;
|
---|
215 | }, {})
|
---|
216 | : null
|
---|
217 | };
|
---|
218 | },
|
---|
219 | addProperty_: function(name, syntax) {
|
---|
220 | if (!syntax) {
|
---|
221 | return;
|
---|
222 | }
|
---|
223 |
|
---|
224 | this.properties[name] = this.createDescriptor(syntax, 'Property', name);
|
---|
225 | },
|
---|
226 | addType_: function(name, syntax) {
|
---|
227 | if (!syntax) {
|
---|
228 | return;
|
---|
229 | }
|
---|
230 |
|
---|
231 | this.types[name] = this.createDescriptor(syntax, 'Type', name);
|
---|
232 |
|
---|
233 | if (syntax === generic['-ms-legacy-expression']) {
|
---|
234 | this.valueCommonSyntax = cssWideKeywordsWithExpression;
|
---|
235 | }
|
---|
236 | },
|
---|
237 |
|
---|
238 | checkAtruleName: function(atruleName) {
|
---|
239 | if (!this.getAtrule(atruleName)) {
|
---|
240 | return new SyntaxReferenceError('Unknown at-rule', '@' + atruleName);
|
---|
241 | }
|
---|
242 | },
|
---|
243 | checkAtrulePrelude: function(atruleName, prelude) {
|
---|
244 | let error = this.checkAtruleName(atruleName);
|
---|
245 |
|
---|
246 | if (error) {
|
---|
247 | return error;
|
---|
248 | }
|
---|
249 |
|
---|
250 | var atrule = this.getAtrule(atruleName);
|
---|
251 |
|
---|
252 | if (!atrule.prelude && prelude) {
|
---|
253 | return new SyntaxError('At-rule `@' + atruleName + '` should not contain a prelude');
|
---|
254 | }
|
---|
255 |
|
---|
256 | if (atrule.prelude && !prelude) {
|
---|
257 | return new SyntaxError('At-rule `@' + atruleName + '` should contain a prelude');
|
---|
258 | }
|
---|
259 | },
|
---|
260 | checkAtruleDescriptorName: function(atruleName, descriptorName) {
|
---|
261 | let error = this.checkAtruleName(atruleName);
|
---|
262 |
|
---|
263 | if (error) {
|
---|
264 | return error;
|
---|
265 | }
|
---|
266 |
|
---|
267 | var atrule = this.getAtrule(atruleName);
|
---|
268 | var descriptor = names.keyword(descriptorName);
|
---|
269 |
|
---|
270 | if (!atrule.descriptors) {
|
---|
271 | return new SyntaxError('At-rule `@' + atruleName + '` has no known descriptors');
|
---|
272 | }
|
---|
273 |
|
---|
274 | if (!atrule.descriptors[descriptor.name] &&
|
---|
275 | !atrule.descriptors[descriptor.basename]) {
|
---|
276 | return new SyntaxReferenceError('Unknown at-rule descriptor', descriptorName);
|
---|
277 | }
|
---|
278 | },
|
---|
279 | checkPropertyName: function(propertyName) {
|
---|
280 | var property = names.property(propertyName);
|
---|
281 |
|
---|
282 | // don't match syntax for a custom property
|
---|
283 | if (property.custom) {
|
---|
284 | return new Error('Lexer matching doesn\'t applicable for custom properties');
|
---|
285 | }
|
---|
286 |
|
---|
287 | if (!this.getProperty(propertyName)) {
|
---|
288 | return new SyntaxReferenceError('Unknown property', propertyName);
|
---|
289 | }
|
---|
290 | },
|
---|
291 |
|
---|
292 | matchAtrulePrelude: function(atruleName, prelude) {
|
---|
293 | var error = this.checkAtrulePrelude(atruleName, prelude);
|
---|
294 |
|
---|
295 | if (error) {
|
---|
296 | return buildMatchResult(null, error);
|
---|
297 | }
|
---|
298 |
|
---|
299 | if (!prelude) {
|
---|
300 | return buildMatchResult(null, null);
|
---|
301 | }
|
---|
302 |
|
---|
303 | return matchSyntax(this, this.getAtrule(atruleName).prelude, prelude, false);
|
---|
304 | },
|
---|
305 | matchAtruleDescriptor: function(atruleName, descriptorName, value) {
|
---|
306 | var error = this.checkAtruleDescriptorName(atruleName, descriptorName);
|
---|
307 |
|
---|
308 | if (error) {
|
---|
309 | return buildMatchResult(null, error);
|
---|
310 | }
|
---|
311 |
|
---|
312 | var atrule = this.getAtrule(atruleName);
|
---|
313 | var descriptor = names.keyword(descriptorName);
|
---|
314 |
|
---|
315 | return matchSyntax(this, atrule.descriptors[descriptor.name] || atrule.descriptors[descriptor.basename], value, false);
|
---|
316 | },
|
---|
317 | matchDeclaration: function(node) {
|
---|
318 | if (node.type !== 'Declaration') {
|
---|
319 | return buildMatchResult(null, new Error('Not a Declaration node'));
|
---|
320 | }
|
---|
321 |
|
---|
322 | return this.matchProperty(node.property, node.value);
|
---|
323 | },
|
---|
324 | matchProperty: function(propertyName, value) {
|
---|
325 | var error = this.checkPropertyName(propertyName);
|
---|
326 |
|
---|
327 | if (error) {
|
---|
328 | return buildMatchResult(null, error);
|
---|
329 | }
|
---|
330 |
|
---|
331 | return matchSyntax(this, this.getProperty(propertyName), value, true);
|
---|
332 | },
|
---|
333 | matchType: function(typeName, value) {
|
---|
334 | var typeSyntax = this.getType(typeName);
|
---|
335 |
|
---|
336 | if (!typeSyntax) {
|
---|
337 | return buildMatchResult(null, new SyntaxReferenceError('Unknown type', typeName));
|
---|
338 | }
|
---|
339 |
|
---|
340 | return matchSyntax(this, typeSyntax, value, false);
|
---|
341 | },
|
---|
342 | match: function(syntax, value) {
|
---|
343 | if (typeof syntax !== 'string' && (!syntax || !syntax.type)) {
|
---|
344 | return buildMatchResult(null, new SyntaxReferenceError('Bad syntax'));
|
---|
345 | }
|
---|
346 |
|
---|
347 | if (typeof syntax === 'string' || !syntax.match) {
|
---|
348 | syntax = this.createDescriptor(syntax, 'Type', 'anonymous');
|
---|
349 | }
|
---|
350 |
|
---|
351 | return matchSyntax(this, syntax, value, false);
|
---|
352 | },
|
---|
353 |
|
---|
354 | findValueFragments: function(propertyName, value, type, name) {
|
---|
355 | return search.matchFragments(this, value, this.matchProperty(propertyName, value), type, name);
|
---|
356 | },
|
---|
357 | findDeclarationValueFragments: function(declaration, type, name) {
|
---|
358 | return search.matchFragments(this, declaration.value, this.matchDeclaration(declaration), type, name);
|
---|
359 | },
|
---|
360 | findAllFragments: function(ast, type, name) {
|
---|
361 | var result = [];
|
---|
362 |
|
---|
363 | this.syntax.walk(ast, {
|
---|
364 | visit: 'Declaration',
|
---|
365 | enter: function(declaration) {
|
---|
366 | result.push.apply(result, this.findDeclarationValueFragments(declaration, type, name));
|
---|
367 | }.bind(this)
|
---|
368 | });
|
---|
369 |
|
---|
370 | return result;
|
---|
371 | },
|
---|
372 |
|
---|
373 | getAtrule: function(atruleName, fallbackBasename = true) {
|
---|
374 | var atrule = names.keyword(atruleName);
|
---|
375 | var atruleEntry = atrule.vendor && fallbackBasename
|
---|
376 | ? this.atrules[atrule.name] || this.atrules[atrule.basename]
|
---|
377 | : this.atrules[atrule.name];
|
---|
378 |
|
---|
379 | return atruleEntry || null;
|
---|
380 | },
|
---|
381 | getAtrulePrelude: function(atruleName, fallbackBasename = true) {
|
---|
382 | const atrule = this.getAtrule(atruleName, fallbackBasename);
|
---|
383 |
|
---|
384 | return atrule && atrule.prelude || null;
|
---|
385 | },
|
---|
386 | getAtruleDescriptor: function(atruleName, name) {
|
---|
387 | return this.atrules.hasOwnProperty(atruleName) && this.atrules.declarators
|
---|
388 | ? this.atrules[atruleName].declarators[name] || null
|
---|
389 | : null;
|
---|
390 | },
|
---|
391 | getProperty: function(propertyName, fallbackBasename = true) {
|
---|
392 | var property = names.property(propertyName);
|
---|
393 | var propertyEntry = property.vendor && fallbackBasename
|
---|
394 | ? this.properties[property.name] || this.properties[property.basename]
|
---|
395 | : this.properties[property.name];
|
---|
396 |
|
---|
397 | return propertyEntry || null;
|
---|
398 | },
|
---|
399 | getType: function(name) {
|
---|
400 | return this.types.hasOwnProperty(name) ? this.types[name] : null;
|
---|
401 | },
|
---|
402 |
|
---|
403 | validate: function() {
|
---|
404 | function validate(syntax, name, broken, descriptor) {
|
---|
405 | if (broken.hasOwnProperty(name)) {
|
---|
406 | return broken[name];
|
---|
407 | }
|
---|
408 |
|
---|
409 | broken[name] = false;
|
---|
410 | if (descriptor.syntax !== null) {
|
---|
411 | walk(descriptor.syntax, function(node) {
|
---|
412 | if (node.type !== 'Type' && node.type !== 'Property') {
|
---|
413 | return;
|
---|
414 | }
|
---|
415 |
|
---|
416 | var map = node.type === 'Type' ? syntax.types : syntax.properties;
|
---|
417 | var brokenMap = node.type === 'Type' ? brokenTypes : brokenProperties;
|
---|
418 |
|
---|
419 | if (!map.hasOwnProperty(node.name) || validate(syntax, node.name, brokenMap, map[node.name])) {
|
---|
420 | broken[name] = true;
|
---|
421 | }
|
---|
422 | }, this);
|
---|
423 | }
|
---|
424 | }
|
---|
425 |
|
---|
426 | var brokenTypes = {};
|
---|
427 | var brokenProperties = {};
|
---|
428 |
|
---|
429 | for (var key in this.types) {
|
---|
430 | validate(this, key, brokenTypes, this.types[key]);
|
---|
431 | }
|
---|
432 |
|
---|
433 | for (var key in this.properties) {
|
---|
434 | validate(this, key, brokenProperties, this.properties[key]);
|
---|
435 | }
|
---|
436 |
|
---|
437 | brokenTypes = Object.keys(brokenTypes).filter(function(name) {
|
---|
438 | return brokenTypes[name];
|
---|
439 | });
|
---|
440 | brokenProperties = Object.keys(brokenProperties).filter(function(name) {
|
---|
441 | return brokenProperties[name];
|
---|
442 | });
|
---|
443 |
|
---|
444 | if (brokenTypes.length || brokenProperties.length) {
|
---|
445 | return {
|
---|
446 | types: brokenTypes,
|
---|
447 | properties: brokenProperties
|
---|
448 | };
|
---|
449 | }
|
---|
450 |
|
---|
451 | return null;
|
---|
452 | },
|
---|
453 | dump: function(syntaxAsAst, pretty) {
|
---|
454 | return {
|
---|
455 | generic: this.generic,
|
---|
456 | types: dumpMapSyntax(this.types, !pretty, syntaxAsAst),
|
---|
457 | properties: dumpMapSyntax(this.properties, !pretty, syntaxAsAst),
|
---|
458 | atrules: dumpAtruleMapSyntax(this.atrules, !pretty, syntaxAsAst)
|
---|
459 | };
|
---|
460 | },
|
---|
461 | toString: function() {
|
---|
462 | return JSON.stringify(this.dump());
|
---|
463 | }
|
---|
464 | };
|
---|
465 |
|
---|
466 | module.exports = Lexer;
|
---|