single-file-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Author: Gildas Lormeau
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ // derived from https://github.com/dryoma/postcss-media-query-parser
26
+
27
+ /*
28
+ * The MIT License (MIT)
29
+ *
30
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ * of this software and associated documentation files (the "Software"), to deal
32
+ * in the Software without restriction, including without limitation the rights
33
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ * copies of the Software, and to permit persons to whom the Software is
35
+ * furnished to do so, subject to the following conditions:
36
+ *
37
+ * The above copyright notice and this permission notice shall be included in
38
+ * all copies or substantial portions of the Software.
39
+ *
40
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
46
+ * THE SOFTWARE.
47
+ */
48
+
49
+ /**
50
+ * Parses a media feature expression, e.g. `max-width: 10px`, `(color)`
51
+ *
52
+ * @param {string} string - the source expression string, can be inside parens
53
+ * @param {Number} index - the index of `string` in the overall input
54
+ *
55
+ * @return {Array} an array of Nodes, the first element being a media feature,
56
+ * the second - its value (may be missing)
57
+ */
58
+
59
+ function parseMediaFeature(string, index = 0) {
60
+ const modesEntered = [{
61
+ mode: "normal",
62
+ character: null,
63
+ }];
64
+ const result = [];
65
+ let lastModeIndex = 0, mediaFeature = "", colon = null, mediaFeatureValue = null, indexLocal = index;
66
+
67
+ let stringNormalized = string;
68
+ // Strip trailing parens (if any), and correct the starting index
69
+ if (string[0] === "(" && string[string.length - 1] === ")") {
70
+ stringNormalized = string.substring(1, string.length - 1);
71
+ indexLocal++;
72
+ }
73
+
74
+ for (let i = 0; i < stringNormalized.length; i++) {
75
+ const character = stringNormalized[i];
76
+
77
+ // If entering/exiting a string
78
+ if (character === "'" || character === "\"") {
79
+ if (modesEntered[lastModeIndex].isCalculationEnabled === true) {
80
+ modesEntered.push({
81
+ mode: "string",
82
+ isCalculationEnabled: false,
83
+ character,
84
+ });
85
+ lastModeIndex++;
86
+ } else if (modesEntered[lastModeIndex].mode === "string" &&
87
+ modesEntered[lastModeIndex].character === character &&
88
+ stringNormalized[i - 1] !== "\\"
89
+ ) {
90
+ modesEntered.pop();
91
+ lastModeIndex--;
92
+ }
93
+ }
94
+
95
+ // If entering/exiting interpolation
96
+ if (character === "{") {
97
+ modesEntered.push({
98
+ mode: "interpolation",
99
+ isCalculationEnabled: true,
100
+ });
101
+ lastModeIndex++;
102
+ } else if (character === "}") {
103
+ modesEntered.pop();
104
+ lastModeIndex--;
105
+ }
106
+
107
+ // If a : is met outside of a string, function call or interpolation, than
108
+ // this : separates a media feature and a value
109
+ if (modesEntered[lastModeIndex].mode === "normal" && character === ":") {
110
+ const mediaFeatureValueStr = stringNormalized.substring(i + 1);
111
+ mediaFeatureValue = {
112
+ type: "value",
113
+ before: /^(\s*)/.exec(mediaFeatureValueStr)[1],
114
+ after: /(\s*)$/.exec(mediaFeatureValueStr)[1],
115
+ value: mediaFeatureValueStr.trim(),
116
+ };
117
+ // +1 for the colon
118
+ mediaFeatureValue.sourceIndex =
119
+ mediaFeatureValue.before.length + i + 1 + indexLocal;
120
+ colon = {
121
+ type: "colon",
122
+ sourceIndex: i + indexLocal,
123
+ after: mediaFeatureValue.before,
124
+ value: ":", // for consistency only
125
+ };
126
+ break;
127
+ }
128
+
129
+ mediaFeature += character;
130
+ }
131
+
132
+ // Forming a media feature node
133
+ mediaFeature = {
134
+ type: "media-feature",
135
+ before: /^(\s*)/.exec(mediaFeature)[1],
136
+ after: /(\s*)$/.exec(mediaFeature)[1],
137
+ value: mediaFeature.trim(),
138
+ };
139
+ mediaFeature.sourceIndex = mediaFeature.before.length + indexLocal;
140
+ result.push(mediaFeature);
141
+
142
+ if (colon !== null) {
143
+ colon.before = mediaFeature.after;
144
+ result.push(colon);
145
+ }
146
+
147
+ if (mediaFeatureValue !== null) {
148
+ result.push(mediaFeatureValue);
149
+ }
150
+
151
+ return result;
152
+ }
153
+
154
+ /**
155
+ * Parses a media query, e.g. `screen and (color)`, `only tv`
156
+ *
157
+ * @param {string} string - the source media query string
158
+ * @param {Number} index - the index of `string` in the overall input
159
+ *
160
+ * @return {Array} an array of Nodes and Containers
161
+ */
162
+
163
+ function parseMediaQuery(string, index = 0) {
164
+ const result = [];
165
+
166
+ // How many times the parser entered parens/curly braces
167
+ let localLevel = 0;
168
+ // Has any keyword, media type, media feature expression or interpolation
169
+ // ('element' hereafter) started
170
+ let insideSomeValue = false, node;
171
+
172
+ function resetNode() {
173
+ return {
174
+ before: "",
175
+ after: "",
176
+ value: "",
177
+ };
178
+ }
179
+
180
+ node = resetNode();
181
+
182
+ for (let i = 0; i < string.length; i++) {
183
+ const character = string[i];
184
+ // If not yet entered any element
185
+ if (!insideSomeValue) {
186
+ if (character.search(/\s/) !== -1) {
187
+ // A whitespace
188
+ // Don't form 'after' yet; will do it later
189
+ node.before += character;
190
+ } else {
191
+ // Not a whitespace - entering an element
192
+ // Expression start
193
+ if (character === "(") {
194
+ node.type = "media-feature-expression";
195
+ localLevel++;
196
+ }
197
+ node.value = character;
198
+ node.sourceIndex = index + i;
199
+ insideSomeValue = true;
200
+ }
201
+ } else {
202
+ // Already in the middle of some element
203
+ node.value += character;
204
+
205
+ // Here parens just increase localLevel and don't trigger a start of
206
+ // a media feature expression (since they can't be nested)
207
+ // Interpolation start
208
+ if (character === "{" || character === "(") { localLevel++; }
209
+ // Interpolation/function call/media feature expression end
210
+ if (character === ")" || character === "}") { localLevel--; }
211
+ }
212
+
213
+ // If exited all parens/curlies and the next symbol
214
+ if (insideSomeValue && localLevel === 0 &&
215
+ (character === ")" || i === string.length - 1 ||
216
+ string[i + 1].search(/\s/) !== -1)
217
+ ) {
218
+ if (["not", "only", "and"].indexOf(node.value) !== -1) {
219
+ node.type = "keyword";
220
+ }
221
+ // if it's an expression, parse its contents
222
+ if (node.type === "media-feature-expression") {
223
+ node.nodes = parseMediaFeature(node.value, node.sourceIndex);
224
+ }
225
+ result.push(Array.isArray(node.nodes) ?
226
+ new Container(node) : new Node(node));
227
+ node = resetNode();
228
+ insideSomeValue = false;
229
+ }
230
+ }
231
+
232
+ // Now process the result array - to specify undefined types of the nodes
233
+ // and specify the `after` prop
234
+ for (let i = 0; i < result.length; i++) {
235
+ node = result[i];
236
+ if (i > 0) { result[i - 1].after = node.before; }
237
+
238
+ // Node types. Might not be set because contains interpolation/function
239
+ // calls or fully consists of them
240
+ if (node.type === undefined) {
241
+ if (i > 0) {
242
+ // only `and` can follow an expression
243
+ if (result[i - 1].type === "media-feature-expression") {
244
+ node.type = "keyword";
245
+ continue;
246
+ }
247
+ // Anything after 'only|not' is a media type
248
+ if (result[i - 1].value === "not" || result[i - 1].value === "only") {
249
+ node.type = "media-type";
250
+ continue;
251
+ }
252
+ // Anything after 'and' is an expression
253
+ if (result[i - 1].value === "and") {
254
+ node.type = "media-feature-expression";
255
+ continue;
256
+ }
257
+
258
+ if (result[i - 1].type === "media-type") {
259
+ // if it is the last element - it might be an expression
260
+ // or 'and' depending on what is after it
261
+ if (!result[i + 1]) {
262
+ node.type = "media-feature-expression";
263
+ } else {
264
+ node.type = result[i + 1].type === "media-feature-expression" ?
265
+ "keyword" : "media-feature-expression";
266
+ }
267
+ }
268
+ }
269
+
270
+ if (i === 0) {
271
+ // `screen`, `fn( ... )`, `#{ ... }`. Not an expression, since then
272
+ // its type would have been set by now
273
+ if (!result[i + 1]) {
274
+ node.type = "media-type";
275
+ continue;
276
+ }
277
+
278
+ // `screen and` or `#{...} (max-width: 10px)`
279
+ if (result[i + 1] &&
280
+ (result[i + 1].type === "media-feature-expression" ||
281
+ result[i + 1].type === "keyword")
282
+ ) {
283
+ node.type = "media-type";
284
+ continue;
285
+ }
286
+ if (result[i + 2]) {
287
+ // `screen and (color) ...`
288
+ if (result[i + 2].type === "media-feature-expression") {
289
+ node.type = "media-type";
290
+ result[i + 1].type = "keyword";
291
+ continue;
292
+ }
293
+ // `only screen and ...`
294
+ if (result[i + 2].type === "keyword") {
295
+ node.type = "keyword";
296
+ result[i + 1].type = "media-type";
297
+ continue;
298
+ }
299
+ }
300
+ if (result[i + 3]) {
301
+ // `screen and (color) ...`
302
+ if (result[i + 3].type === "media-feature-expression") {
303
+ node.type = "keyword";
304
+ result[i + 1].type = "media-type";
305
+ result[i + 2].type = "keyword";
306
+ continue;
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ return result;
313
+ }
314
+
315
+ /**
316
+ * Parses a media query list. Takes a possible `url()` at the start into
317
+ * account, and divides the list into media queries that are parsed separately
318
+ *
319
+ * @param {string} string - the source media query list string
320
+ *
321
+ * @return {Array} an array of Nodes/Containers
322
+ */
323
+
324
+ function parseMediaList(string) {
325
+ const result = [];
326
+ let interimIndex = 0, levelLocal = 0;
327
+
328
+ // Check for a `url(...)` part (if it is contents of an @import rule)
329
+ const doesHaveUrl = /^(\s*)url\s*\(/.exec(string);
330
+ if (doesHaveUrl !== null) {
331
+ let i = doesHaveUrl[0].length;
332
+ let parenthesesLv = 1;
333
+ while (parenthesesLv > 0) {
334
+ const character = string[i];
335
+ if (character === "(") { parenthesesLv++; }
336
+ if (character === ")") { parenthesesLv--; }
337
+ i++;
338
+ }
339
+ result.unshift(new Node({
340
+ type: "url",
341
+ value: string.substring(0, i).trim(),
342
+ sourceIndex: doesHaveUrl[1].length,
343
+ before: doesHaveUrl[1],
344
+ after: /^(\s*)/.exec(string.substring(i))[1],
345
+ }));
346
+ interimIndex = i;
347
+ }
348
+
349
+ // Start processing the media query list
350
+ for (let i = interimIndex; i < string.length; i++) {
351
+ const character = string[i];
352
+
353
+ // Dividing the media query list into comma-separated media queries
354
+ // Only count commas that are outside of any parens
355
+ // (i.e., not part of function call params list, etc.)
356
+ if (character === "(") { levelLocal++; }
357
+ if (character === ")") { levelLocal--; }
358
+ if (levelLocal === 0 && character === ",") {
359
+ const mediaQueryString = string.substring(interimIndex, i);
360
+ const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
361
+ result.push(new Container({
362
+ type: "media-query",
363
+ value: mediaQueryString.trim(),
364
+ sourceIndex: interimIndex + spaceBefore.length,
365
+ nodes: parseMediaQuery(mediaQueryString, interimIndex),
366
+ before: spaceBefore,
367
+ after: /(\s*)$/.exec(mediaQueryString)[1],
368
+ }));
369
+ interimIndex = i + 1;
370
+ }
371
+ }
372
+
373
+ const mediaQueryString = string.substring(interimIndex);
374
+ const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
375
+ result.push(new Container({
376
+ type: "media-query",
377
+ value: mediaQueryString.trim(),
378
+ sourceIndex: interimIndex + spaceBefore.length,
379
+ nodes: parseMediaQuery(mediaQueryString, interimIndex),
380
+ before: spaceBefore,
381
+ after: /(\s*)$/.exec(mediaQueryString)[1],
382
+ }));
383
+
384
+ return result;
385
+ }
386
+
387
+ function Container(opts) {
388
+ this.constructor(opts);
389
+
390
+ this.nodes = opts.nodes;
391
+
392
+ if (this.after === undefined) {
393
+ this.after = this.nodes.length > 0 ?
394
+ this.nodes[this.nodes.length - 1].after : "";
395
+ }
396
+
397
+ if (this.before === undefined) {
398
+ this.before = this.nodes.length > 0 ?
399
+ this.nodes[0].before : "";
400
+ }
401
+
402
+ if (this.sourceIndex === undefined) {
403
+ this.sourceIndex = this.before.length;
404
+ }
405
+
406
+ this.nodes.forEach(node => {
407
+ node.parent = this; // eslint-disable-line no-param-reassign
408
+ });
409
+ }
410
+
411
+ Container.prototype = Object.create(Node.prototype);
412
+ Container.constructor = Node;
413
+
414
+ /**
415
+ * Iterate over descendant nodes of the node
416
+ *
417
+ * @param {RegExp|string} filter - Optional. Only nodes with node.type that
418
+ * satisfies the filter will be traversed over
419
+ * @param {function} cb - callback to call on each node. Takes these params:
420
+ * node - the node being processed, i - it's index, nodes - the array
421
+ * of all nodes
422
+ * If false is returned, the iteration breaks
423
+ *
424
+ * @return (boolean) false, if the iteration was broken
425
+ */
426
+ Container.prototype.walk = function walk(filter, cb) {
427
+ const hasFilter = typeof filter === "string" || filter instanceof RegExp;
428
+ const callback = hasFilter ? cb : filter;
429
+ const filterReg = typeof filter === "string" ? new RegExp(filter) : filter;
430
+
431
+ for (let i = 0; i < this.nodes.length; i++) {
432
+ const node = this.nodes[i];
433
+ const filtered = hasFilter ? filterReg.test(node.type) : true;
434
+ if (filtered && callback && callback(node, i, this.nodes) === false) {
435
+ return false;
436
+ }
437
+ if (node.nodes && node.walk(filter, cb) === false) { return false; }
438
+ }
439
+ return true;
440
+ };
441
+
442
+ /**
443
+ * Iterate over immediate children of the node
444
+ *
445
+ * @param {function} cb - callback to call on each node. Takes these params:
446
+ * node - the node being processed, i - it's index, nodes - the array
447
+ * of all nodes
448
+ * If false is returned, the iteration breaks
449
+ *
450
+ * @return (boolean) false, if the iteration was broken
451
+ */
452
+ Container.prototype.each = function each(cb = () => { }) {
453
+ for (let i = 0; i < this.nodes.length; i++) {
454
+ const node = this.nodes[i];
455
+ if (cb(node, i, this.nodes) === false) { return false; }
456
+ }
457
+ return true;
458
+ };
459
+
460
+ /**
461
+ * A very generic node. Pretty much any element of a media query
462
+ */
463
+
464
+ function Node(opts) {
465
+ this.after = opts.after;
466
+ this.before = opts.before;
467
+ this.type = opts.type;
468
+ this.value = opts.value;
469
+ this.sourceIndex = opts.sourceIndex;
470
+ }
471
+
472
+ export {
473
+ parseMediaList
474
+ };