isml-linter 5.39.3 → 5.40.1

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +1184 -1159
  2. package/LICENSE +21 -21
  3. package/README.md +245 -245
  4. package/bin/isml-linter.js +32 -32
  5. package/ismllinter.config.js +33 -33
  6. package/package.json +53 -53
  7. package/scaffold_files/ismllinter.config.js +47 -47
  8. package/src/Builder.js +15 -15
  9. package/src/Constants.js +139 -139
  10. package/src/IsmlLinter.js +255 -255
  11. package/src/enums/ParseStatus.js +5 -5
  12. package/src/enums/SfccTagContainer.js +287 -287
  13. package/src/isml_tree/ContainerNode.js +38 -38
  14. package/src/isml_tree/IsmlNode.js +692 -684
  15. package/src/isml_tree/MaskUtils.js +421 -421
  16. package/src/isml_tree/ParseUtils.js +532 -506
  17. package/src/isml_tree/TreeBuilder.js +273 -273
  18. package/src/publicApi.js +24 -24
  19. package/src/rules/line_by_line/enforce-isprint.js +53 -53
  20. package/src/rules/line_by_line/enforce-require.js +35 -35
  21. package/src/rules/line_by_line/lowercase-filename.js +29 -29
  22. package/src/rules/line_by_line/max-lines.js +37 -37
  23. package/src/rules/line_by_line/no-br.js +36 -36
  24. package/src/rules/line_by_line/no-git-conflict.js +43 -43
  25. package/src/rules/line_by_line/no-import-package.js +34 -34
  26. package/src/rules/line_by_line/no-inline-style.js +34 -34
  27. package/src/rules/line_by_line/no-isscript.js +34 -34
  28. package/src/rules/line_by_line/no-space-only-lines.js +47 -47
  29. package/src/rules/line_by_line/no-tabs.js +38 -38
  30. package/src/rules/line_by_line/no-trailing-spaces.js +52 -52
  31. package/src/rules/prototypes/RulePrototype.js +79 -79
  32. package/src/rules/prototypes/SingleLineRulePrototype.js +47 -47
  33. package/src/rules/prototypes/TreeRulePrototype.js +84 -84
  34. package/src/rules/tree/align-isset.js +87 -87
  35. package/src/rules/tree/contextual-attrs.js +105 -105
  36. package/src/rules/tree/custom-tags.js +54 -54
  37. package/src/rules/tree/disallow-tags.js +39 -39
  38. package/src/rules/tree/empty-eof.js +66 -66
  39. package/src/rules/tree/enforce-security.js +85 -85
  40. package/src/rules/tree/eslint-to-isscript.js +179 -179
  41. package/src/rules/tree/indent.js +856 -853
  42. package/src/rules/tree/leading-iscache.js +39 -39
  43. package/src/rules/tree/leading-iscontent.js +35 -35
  44. package/src/rules/tree/max-depth.js +54 -54
  45. package/src/rules/tree/no-deprecated-attrs.js +67 -67
  46. package/src/rules/tree/no-embedded-isml.js +17 -17
  47. package/src/rules/tree/no-hardcode.js +51 -51
  48. package/src/rules/tree/no-iselse-slash.js +35 -35
  49. package/src/rules/tree/no-redundant-context.js +134 -134
  50. package/src/rules/tree/no-require-in-loop.js +63 -63
  51. package/src/rules/tree/one-element-per-line.js +82 -76
  52. package/src/util/CommandLineUtils.js +19 -19
  53. package/src/util/ConfigUtils.js +219 -219
  54. package/src/util/ConsoleUtils.js +327 -327
  55. package/src/util/CustomTagContainer.js +45 -45
  56. package/src/util/ExceptionUtils.js +173 -149
  57. package/src/util/FileUtils.js +79 -79
  58. package/src/util/GeneralUtils.js +60 -60
  59. package/src/util/NativeExtensionUtils.js +6 -6
  60. package/src/util/RuleUtils.js +295 -295
  61. package/src/util/TempRuleUtils.js +232 -232
@@ -1,506 +1,532 @@
1
-
2
- /**
3
- * The functions defined in this file do not create or modify a state, they
4
- * simply analyze it and return relevant information;
5
- */
6
-
7
- const path = require('path');
8
- const Constants = require('../Constants');
9
- const ExceptionUtils = require('../util/ExceptionUtils');
10
- const SfccTagContainer = require('../enums/SfccTagContainer');
11
- const GeneralUtils = require('../util/GeneralUtils');
12
- const MaskUtils = require('./MaskUtils');
13
-
14
- const getNextNonEmptyChar = content => {
15
- return content.replace(new RegExp(Constants.EOL, 'g'), '').trim()[0];
16
- };
17
-
18
- const getCharOccurrenceQty = (string, char) => (string.match(new RegExp(char, 'g')) || []).length;
19
-
20
- const getLineBreakQty = string => getCharOccurrenceQty(string, Constants.EOL);
21
-
22
- const getNextNonEmptyCharPos = content => {
23
- const firstNonEmptyChar = getNextNonEmptyChar(content);
24
- const index = content.indexOf(firstNonEmptyChar);
25
-
26
- return Math.max(index, 0);
27
- };
28
-
29
- const getLeadingEmptyChars = string => {
30
- const leadingBlankSpacesQty = getNextNonEmptyCharPos(string);
31
-
32
- return string.substring(0, leadingBlankSpacesQty);
33
- };
34
-
35
- const getElementColumnNumber = (newElement, state) => {
36
- if (newElement.value.indexOf(Constants.EOL) >= 0) {
37
- const firstNonEmptyCharPos = getNextNonEmptyCharPos(newElement.value);
38
-
39
- return firstNonEmptyCharPos === 0 ?
40
- 1 :
41
- newElement.value
42
- .substring(0, firstNonEmptyCharPos)
43
- .split('').reverse().join('')
44
- .indexOf(Constants.EOL) + 1;
45
-
46
- } else if (state.elementList.length === 0) {
47
- return getNextNonEmptyCharPos(newElement.value) + 1;
48
- } else {
49
- let columnNumber = 1;
50
-
51
- for (let i = state.elementList.length - 1; i >= 0; i--) {
52
- const element = state.elementList[i];
53
-
54
- if (element.value.indexOf(Constants.EOL) >= 0) {
55
- columnNumber += element.value.length - 1;
56
- break;
57
-
58
- } else if (i === 0) {
59
- columnNumber += element.value.length + 1;
60
- break;
61
-
62
- } else {
63
- columnNumber += element.value.length;
64
- }
65
- }
66
-
67
- return columnNumber;
68
- }
69
- };
70
-
71
- const getLeadingLineBreakQty = string => {
72
- const leadingString = getLeadingEmptyChars(string);
73
-
74
- return this.getLineBreakQty(leadingString);
75
- };
76
-
77
- const getTrailingEmptyCharsQty = string => {
78
- const invertedString = string.split('').reverse().join('').replace(Constants.EOL, '_');
79
-
80
- return Math.max(getLeadingEmptyChars(invertedString).length, 0);
81
- };
82
-
83
- const checkBalance = (node, templatePath) => {
84
-
85
- for (let i = 0; i < node.children.length; i++) {
86
- checkBalance(node.children[i]);
87
- }
88
-
89
- if (!node.isRoot() &&
90
- node.parent && !node.parent.isContainer() &&
91
- (node.isHtmlTag() || node.isIsmlTag()) &&
92
- !node.isSelfClosing() && !node.tail
93
- && !node.parent.isOfType('iscomment')
94
- ) {
95
- throw ExceptionUtils.unbalancedElementError(
96
- node.getType(),
97
- node.lineNumber,
98
- node.globalPos,
99
- node.head.trim().length,
100
- templatePath
101
- );
102
- }
103
- };
104
-
105
- const parseNextElement = state => {
106
- const newElement = getNewElement(state);
107
-
108
- const trimmedElement = newElement.value.trim();
109
- const previousElement = state.elementList[state.elementList.length - 1] || {};
110
-
111
- if (previousElement.tagType === 'iscomment' && !previousElement.isClosingTag && trimmedElement !== '</iscomment>') {
112
- newElement.lineNumber = getLineBreakQty(state.pastContent) + getLeadingLineBreakQty(newElement.value) + 1;
113
- newElement.globalPos = state.pastContent.length + getLeadingEmptyChars(newElement.value).length;
114
- newElement.type = 'text';
115
- newElement.isSelfClosing = true;
116
-
117
- if (state.isCrlfLineBreak) {
118
- newElement.globalPos -= getLineBreakQty(newElement.value);
119
- }
120
- } else {
121
- trimmedElement.startsWith('<') || trimmedElement.startsWith('${') ?
122
- parseTagOrExpressionElement(state, newElement) :
123
- parseTextElement(state, newElement);
124
- }
125
-
126
- newElement.columnNumber = getElementColumnNumber(newElement, state);
127
-
128
- if (state.isCrlfLineBreak) {
129
- newElement.globalPos += newElement.lineNumber - 1;
130
- }
131
-
132
- state.elementList.push(newElement);
133
-
134
- return newElement;
135
- };
136
-
137
- const parseTagOrExpressionElement = (state, newElement) => {
138
- const trimmedElement = newElement.value.trim().toLowerCase();
139
- const isTag = trimmedElement.startsWith('<') && !trimmedElement.startsWith('<!--');
140
- const isExpression = trimmedElement.startsWith('${');
141
- const isHtmlOrIsmlComment = trimmedElement.startsWith('<!--');
142
-
143
- if (isTag) {
144
- if (trimmedElement.startsWith('<is') || trimmedElement.startsWith('</is')) {
145
- newElement.type = 'ismlTag';
146
- } else if (trimmedElement.startsWith('<!DOCTYPE')) {
147
- newElement.type = 'doctype';
148
- } else {
149
- newElement.type = 'htmlTag';
150
- }
151
- } else if (isHtmlOrIsmlComment) {
152
- newElement.type = 'htmlOrIsmlComment';
153
- } else if (isExpression) {
154
- newElement.type = 'expression';
155
- } else {
156
- newElement.type = 'text';
157
- }
158
-
159
- if (isTag) {
160
- newElement.tagType = getElementType(trimmedElement);
161
-
162
- newElement.isCustomTag = newElement.type === 'ismlTag' && !SfccTagContainer[newElement.tagType];
163
- }
164
-
165
- newElement.isSelfClosing = isSelfClosing(trimmedElement);
166
-
167
- newElement.isClosingTag = isTag && trimmedElement.startsWith('</');
168
- newElement.lineNumber = getLineBreakQty(state.pastContent) + getLeadingLineBreakQty(newElement.value) + 1;
169
- newElement.globalPos = state.pastContent.length + getLeadingEmptyChars(newElement.value).length;
170
- };
171
-
172
- const parseTextElement = (state, newElement) => {
173
- newElement.type = 'text';
174
- newElement.lineNumber = getLineBreakQty(state.pastContent.substring(0, state.pastContent.length - state.cutSpot))
175
- + getLeadingLineBreakQty(newElement.value) + 1;
176
- newElement.globalPos = state.pastContent.length - state.cutSpot + getLeadingEmptyChars(newElement.value).length;
177
- newElement.isSelfClosing = true;
178
- };
179
-
180
- const getElementType = trimmedElement => {
181
- if (trimmedElement.startsWith('</')) {
182
- const tailElementType = trimmedElement.slice(2, -1);
183
-
184
- if (tailElementType.startsWith('${')) {
185
- return 'dynamic_element';
186
- }
187
-
188
- return tailElementType;
189
- } else {
190
-
191
- const typeValueLastPos = Math.min(...[
192
- trimmedElement.indexOf(' '),
193
- trimmedElement.indexOf('/'),
194
- trimmedElement.indexOf('>')
195
- ].filter(j => j >= 0));
196
-
197
- const elementType = trimmedElement.substring(1, typeValueLastPos).trim();
198
-
199
- if (elementType.startsWith('${')) {
200
- return 'dynamic_element';
201
- }
202
-
203
- return elementType;
204
- }
205
- };
206
-
207
- function isSelfClosing(trimmedElement) {
208
- const ConfigUtils = require('../util/ConfigUtils');
209
-
210
- const config = ConfigUtils.load();
211
- const isTag = trimmedElement.startsWith('<') && !trimmedElement.startsWith('<!--');
212
- const elementType = getElementType(trimmedElement);
213
- const isDocType = trimmedElement.toLowerCase().startsWith('<!doctype ');
214
- const isVoidElement = !config.disableHtml5 && Constants.voidElementsArray.indexOf(elementType) >= 0;
215
- const isHtmlComment = trimmedElement.startsWith('<!--') && trimmedElement.endsWith('-->');
216
- const isClosingTag = trimmedElement.endsWith('/>');
217
- const isIsmlTag = trimmedElement.startsWith('<is');
218
- const isStandardIsmlTag = !!SfccTagContainer[elementType];
219
- const isCustomIsmlTag = isIsmlTag && !isStandardIsmlTag;
220
- const isExpression = trimmedElement.startsWith('${') && trimmedElement.endsWith('}');
221
- const isSfccSelfClosingTag = SfccTagContainer[elementType] && SfccTagContainer[elementType]['self-closing'];
222
-
223
- // 'isif' tag is never self-closing;
224
- if (['isif'].indexOf(elementType) >= 0) {
225
- return false;
226
- }
227
-
228
- return isDocType ||
229
- isVoidElement ||
230
- isExpression ||
231
- isHtmlComment ||
232
- isTag && isClosingTag ||
233
- isCustomIsmlTag ||
234
- isIsmlTag && isSfccSelfClosingTag;
235
- }
236
-
237
- const getNextOpeningTagOrExpressionInitPos = content => {
238
- return Math.min(...[
239
- content.indexOf('<'),
240
- content.indexOf('<--'),
241
- content.indexOf('${')
242
- ].filter(j => j >= 0)) + 1;
243
- };
244
-
245
- const getNextClosingTagOrExpressionEndPos = content => {
246
- return Math.min(...[
247
- content.indexOf('>'),
248
- content.indexOf('-->'),
249
- content.indexOf('}')
250
- ].filter(j => j >= 0)) + 1;
251
- };
252
-
253
- const getInitialState = (templateContent, templatePath, isCrlfLineBreak) => {
254
- const originalContent = GeneralUtils.toLF(templateContent);
255
- const originalShadowContent = MaskUtils.maskIgnorableContent(originalContent, null, templatePath);
256
-
257
- return {
258
- templatePath : templatePath,
259
- templateName : templatePath ? path.basename(templatePath) : '',
260
- originalContent : originalContent,
261
- originalShadowContent : originalShadowContent,
262
- remainingContent : originalContent,
263
- remainingShadowContent : originalShadowContent,
264
- pastContent : '',
265
- elementList : [],
266
- cutSpot : null,
267
- isCrlfLineBreak
268
- };
269
- };
270
-
271
- const initLoopState = state => {
272
- state.nextOpeningTagOrExpressionInitPos = getNextOpeningTagOrExpressionInitPos(state.remainingShadowContent);
273
- state.nextClosingTagOrExpressionEndPos = getNextClosingTagOrExpressionEndPos(state.remainingShadowContent);
274
- state.cutSpot = null;
275
- };
276
-
277
- const finishLoopState = state => {
278
- const newElement = state.elementList[state.elementList.length - 1];
279
-
280
- // If there is no element left (only blank spaces and / or line breaks);
281
- if (!isFinite(state.nextClosingTagOrExpressionEndPos)) {
282
- state.nextClosingTagOrExpressionEndPos = state.remainingShadowContent.length - 1;
283
- }
284
-
285
- if (!state.cutSpot) {
286
- state.remainingShadowContent = state.remainingShadowContent.substring(newElement.value.length);
287
- state.remainingContent = state.remainingContent.substring(newElement.value.length);
288
- state.pastContent = state.originalContent.substring(0, state.pastContent.length + newElement.value.length);
289
- }
290
- };
291
-
292
- const mergeTrailingSpacesWithLastElement = state => {
293
- const elementList = state.elementList;
294
- const lastElement = elementList[elementList.length - 1];
295
- const secondLastElement = elementList[elementList.length - 2];
296
-
297
- if (lastElement.value.trim().length === 0) {
298
- secondLastElement.value += lastElement.value;
299
- elementList.pop();
300
- }
301
- };
302
-
303
- const adjustTrailingSpaces = state => {
304
-
305
- // Note that last element is not iterated over;
306
- for (let i = 0; i < state.elementList.length - 1; i++) {
307
- const previousElement = i > 0 ? state.elementList[i - 1] : null;
308
- const currentElement = state.elementList[i];
309
-
310
- if (currentElement.type === 'text'
311
- && previousElement
312
- && previousElement.lineNumber !== currentElement.lineNumber
313
- && previousElement.tagType !== 'isscript'
314
- ) {
315
-
316
- const trailingSpacesQty = currentElement.value
317
- .replace(/\r\n/g, '_')
318
- .split('')
319
- .reverse()
320
- .join('')
321
- .search(/\S/);
322
-
323
- if (trailingSpacesQty > 0) {
324
- const trailingSpaces = currentElement.value.slice(-trailingSpacesQty);
325
-
326
- currentElement.value = currentElement.value.slice(0, -trailingSpacesQty);
327
- const nextElement = state.elementList[i + 1];
328
- nextElement.value = trailingSpaces + nextElement.value;
329
- }
330
- }
331
- }
332
- };
333
-
334
- // TODO Refactor this function;
335
- const checkIfNextElementIsATagOrHtmlComment = (content, state) => {
336
- const previousElementType = state.elementList.length > 0 && state.elementList[state.elementList.length - 1].tagType;
337
- const isIscommentContent = previousElementType === 'iscomment';
338
- const isIsscriptContent = previousElementType === 'isscript';
339
- const isScriptContent = previousElementType === 'script';
340
-
341
- return !isIscommentContent && !isScriptContent && !isIsscriptContent && content.startsWith('<') && content.substring(1).match(/^[A-z]/i) || content.startsWith('</') || content.startsWith('<!');
342
- };
343
-
344
- const getIscommentContent = state => {
345
- for (let i = 0; i < state.remainingContent.length; i++) {
346
- const remainingString = state.remainingContent.substring(i);
347
-
348
- if (remainingString.startsWith('</iscomment>')) {
349
- return state.remainingContent.substring(0, i);
350
- }
351
- }
352
-
353
- return state.remainingContent;
354
- };
355
-
356
- const checkIfCurrentElementIsWithinIscomment = state => {
357
- let depth = 0;
358
-
359
- for (let i = state.elementList.length - 1; i >= 0 ; i--) {
360
- const element = state.elementList[i];
361
-
362
- if (element.tagType === 'iscomment') {
363
- depth += element.isClosingTag ? -1 : 1;
364
- }
365
- }
366
-
367
- return depth > 0 && !state.remainingContent.trimStart().startsWith('</iscomment>');
368
- };
369
-
370
- // TODO Refactor this function
371
- const getNewElement = state => {
372
-
373
- const trimmedContent = state.remainingContent.trimStart();
374
- const isWithinIscomment = checkIfCurrentElementIsWithinIscomment(state);
375
- const isNextElementATag = trimmedContent.startsWith('<');
376
- const isNextElementAnExpression = trimmedContent.startsWith('${');
377
- const isTextElement = !isNextElementATag && !isNextElementAnExpression;
378
- let lastContiguousMaskedCharPos;
379
- let elementValue;
380
-
381
- if (isWithinIscomment) {
382
- elementValue = getIscommentContent(state);
383
-
384
- } else if (isTextElement) {
385
-
386
- for (let i = 0; i < state.remainingContent.length; i++) {
387
- const remainingString = state.remainingContent.substring(i);
388
- const isNextElementATagOrHtmlComment = checkIfNextElementIsATagOrHtmlComment(remainingString, state);
389
-
390
- if (isNextElementATagOrHtmlComment || remainingString.startsWith('${')) {
391
- lastContiguousMaskedCharPos = i;
392
- break;
393
- }
394
- }
395
-
396
- elementValue = state.remainingContent.substring(0, lastContiguousMaskedCharPos);
397
- } else {
398
- if (state.elementList.length > 0 && state.elementList[state.elementList.length - 1].type === 'text') {
399
- const localMaskedContent0 = MaskUtils.maskExpressionContent(state.remainingContent);
400
- const localMaskedContent1 = MaskUtils.maskInBetween(localMaskedContent0, '<', '>');
401
-
402
- for (let i = 0; i < localMaskedContent1.length; i++) {
403
- if (isNextElementATag && localMaskedContent1[i] === '>') {
404
- lastContiguousMaskedCharPos = i + 1;
405
- break;
406
- }
407
-
408
- if (isNextElementAnExpression && localMaskedContent1[i] === '}') {
409
- lastContiguousMaskedCharPos = i + 1;
410
- break;
411
- }
412
- }
413
-
414
- } else {
415
- let remainingMaskedContent = state.remainingContent;
416
-
417
- if (isNextElementATag) {
418
- remainingMaskedContent = MaskUtils.maskExpressionContent(remainingMaskedContent);
419
- remainingMaskedContent = MaskUtils.maskQuoteContent(remainingMaskedContent);
420
- }
421
-
422
- for (let i = 0; i < remainingMaskedContent.length; i++) {
423
- if (isNextElementATag && remainingMaskedContent[i] === '>') {
424
- lastContiguousMaskedCharPos = i + 1;
425
- break;
426
- }
427
- }
428
- }
429
-
430
- elementValue = state.remainingShadowContent.startsWith('_') ?
431
- state.remainingContent.substring(0, lastContiguousMaskedCharPos) :
432
- state.remainingContent.substring(0, state.nextClosingTagOrExpressionEndPos);
433
- }
434
-
435
- return {
436
- value : elementValue,
437
- type : undefined,
438
- globalPos : undefined,
439
- lineNumber : undefined,
440
- isSelfClosing : undefined,
441
- isClosingTag : undefined,
442
- tagType : undefined
443
- };
444
- };
445
-
446
- const getElementList = (templateContent, templatePath, isCrlfLineBreak) => {
447
-
448
- const state = getInitialState(templateContent, templatePath, isCrlfLineBreak);
449
- const elementList = state.elementList;
450
-
451
- do {
452
- initLoopState(state);
453
- parseNextElement(state);
454
- finishLoopState(state);
455
- } while (state.remainingShadowContent.length > 0);
456
-
457
- adjustTrailingSpaces(state);
458
- mergeTrailingSpacesWithLastElement(state);
459
-
460
- return elementList;
461
- };
462
-
463
- const getBlankSpaceString = length => {
464
- let result = '';
465
-
466
- for (let i = 0; i < length; i++) {
467
- result += ' ';
468
- }
469
-
470
- return result;
471
- };
472
-
473
- const getColumnNumber = content => {
474
- const leadingContent = content.substring(0, getNextNonEmptyCharPos(content));
475
- const lastLineBreakPos = leadingContent.lastIndexOf(Constants.EOL);
476
- const precedingEmptySpaces = leadingContent.substring(lastLineBreakPos + 1);
477
-
478
- return precedingEmptySpaces.length + 1;
479
- };
480
-
481
- const getFirstEmptyCharPos = content => {
482
- const firstLineBreakPos = content.indexOf(Constants.EOL);
483
- const firstBlankSpacePos = content.indexOf(' ');
484
-
485
- if (firstLineBreakPos === -1 && firstBlankSpacePos === -1) {
486
- return content.length;
487
- } else if (firstLineBreakPos >= 0 && firstBlankSpacePos === -1) {
488
- return firstLineBreakPos;
489
- } else if (firstLineBreakPos === -1 && firstBlankSpacePos >= 0) {
490
- return firstBlankSpacePos;
491
- } else if (firstLineBreakPos >= 0 && firstBlankSpacePos >= 0) {
492
- return Math.min(firstLineBreakPos, firstBlankSpacePos);
493
- }
494
- };
495
-
496
- module.exports.getElementList = getElementList;
497
- module.exports.checkBalance = checkBalance;
498
- module.exports.getLineBreakQty = getLineBreakQty;
499
- module.exports.getCharOccurrenceQty = getCharOccurrenceQty;
500
- module.exports.getNextNonEmptyCharPos = getNextNonEmptyCharPos;
501
- module.exports.getLeadingEmptyChars = getLeadingEmptyChars;
502
- module.exports.getLeadingLineBreakQty = getLeadingLineBreakQty;
503
- module.exports.getTrailingEmptyCharsQty = getTrailingEmptyCharsQty;
504
- module.exports.getBlankSpaceString = getBlankSpaceString;
505
- module.exports.getColumnNumber = getColumnNumber;
506
- module.exports.getFirstEmptyCharPos = getFirstEmptyCharPos;
1
+
2
+ /**
3
+ * The functions defined in this file do not create or modify a state, they
4
+ * simply analyze it and return relevant information;
5
+ */
6
+
7
+ const path = require('path');
8
+ const Constants = require('../Constants');
9
+ const ExceptionUtils = require('../util/ExceptionUtils');
10
+ const SfccTagContainer = require('../enums/SfccTagContainer');
11
+ const GeneralUtils = require('../util/GeneralUtils');
12
+ const MaskUtils = require('./MaskUtils');
13
+
14
+ const getNextNonEmptyChar = content => {
15
+ return content.replace(new RegExp(Constants.EOL, 'g'), '').trim()[0];
16
+ };
17
+
18
+ const getCharOccurrenceQty = (string, char) => (string.match(new RegExp(char, 'g')) || []).length;
19
+
20
+ const getLineBreakQty = string => getCharOccurrenceQty(string, Constants.EOL);
21
+
22
+ const getNextNonEmptyCharPos = content => {
23
+ const firstNonEmptyChar = getNextNonEmptyChar(content);
24
+ const index = content.indexOf(firstNonEmptyChar);
25
+
26
+ return Math.max(index, 0);
27
+ };
28
+
29
+ const getLeadingEmptyChars = string => {
30
+ const leadingBlankSpacesQty = getNextNonEmptyCharPos(string);
31
+
32
+ return string.substring(0, leadingBlankSpacesQty);
33
+ };
34
+
35
+ const getElementColumnNumber = (newElement, state) => {
36
+ if (newElement.value.indexOf(Constants.EOL) >= 0) {
37
+ const firstNonEmptyCharPos = getNextNonEmptyCharPos(newElement.value);
38
+
39
+ return firstNonEmptyCharPos === 0 ?
40
+ 1 :
41
+ newElement.value
42
+ .substring(0, firstNonEmptyCharPos)
43
+ .split('').reverse().join('')
44
+ .indexOf(Constants.EOL) + 1;
45
+
46
+ } else if (state.elementList.length === 0) {
47
+ return getNextNonEmptyCharPos(newElement.value) + 1;
48
+ } else {
49
+ let columnNumber = 1;
50
+
51
+ for (let i = state.elementList.length - 1; i >= 0; i--) {
52
+ const element = state.elementList[i];
53
+
54
+ if (element.value.indexOf(Constants.EOL) >= 0) {
55
+ columnNumber += element.value.length - 1;
56
+ break;
57
+
58
+ } else if (i === 0) {
59
+ columnNumber += element.value.length + 1;
60
+ break;
61
+
62
+ } else {
63
+ columnNumber += element.value.length;
64
+ }
65
+ }
66
+
67
+ return columnNumber;
68
+ }
69
+ };
70
+
71
+ const getLeadingLineBreakQty = string => {
72
+ const leadingString = getLeadingEmptyChars(string);
73
+
74
+ return this.getLineBreakQty(leadingString);
75
+ };
76
+
77
+ const getTrailingEmptyCharsQty = string => {
78
+ const invertedString = string.split('').reverse().join('').replace(Constants.EOL, '_');
79
+
80
+ return Math.max(getLeadingEmptyChars(invertedString).length, 0);
81
+ };
82
+
83
+ const checkBalance = (node, templatePath) => {
84
+
85
+ for (let i = 0; i < node.children.length; i++) {
86
+ checkBalance(node.children[i]);
87
+ }
88
+
89
+ if (!node.isRoot() &&
90
+ node.parent && !node.parent.isContainer() &&
91
+ (node.isHtmlTag() || node.isIsmlTag()) &&
92
+ !node.isSelfClosing() && !node.tail
93
+ && !node.parent.isOfType('iscomment')
94
+ ) {
95
+ throw ExceptionUtils.unbalancedElementError(
96
+ node.getType(),
97
+ node.lineNumber,
98
+ node.globalPos,
99
+ node.head.trim().length,
100
+ templatePath
101
+ );
102
+ }
103
+ };
104
+
105
+ const parseNextElement = state => {
106
+ const newElement = getNewElement(state);
107
+
108
+ const trimmedElement = newElement.value.trim();
109
+ const previousElement = state.elementList[state.elementList.length - 1] || {};
110
+ const isIscommentContent = previousElement.tagType === 'iscomment' && !previousElement.isClosingTag && trimmedElement !== '</iscomment>';
111
+ const isIsscriptContent = previousElement.tagType === 'isscript' && !previousElement.isClosingTag && trimmedElement !== '</isscript>';
112
+
113
+ if (isIsscriptContent || isIscommentContent) {
114
+ newElement.lineNumber = getLineBreakQty(state.pastContent) + getLeadingLineBreakQty(newElement.value) + 1;
115
+ newElement.globalPos = state.pastContent.length + getLeadingEmptyChars(newElement.value).length;
116
+ newElement.type = 'text';
117
+ newElement.isSelfClosing = true;
118
+
119
+ if (state.isCrlfLineBreak && isIscommentContent) {
120
+ newElement.globalPos -= getLineBreakQty(newElement.value);
121
+ }
122
+ } else {
123
+ trimmedElement.startsWith('<') || trimmedElement.startsWith('${') ?
124
+ parseTagOrExpressionElement(state, newElement) :
125
+ parseTextElement(state, newElement);
126
+ }
127
+
128
+ newElement.columnNumber = getElementColumnNumber(newElement, state);
129
+
130
+ if (state.isCrlfLineBreak) {
131
+ newElement.globalPos += newElement.lineNumber - 1;
132
+ }
133
+
134
+ state.elementList.push(newElement);
135
+
136
+ if (newElement.type === 'htmlTag' && newElement.value.indexOf('<isif') >= 0 && newElement.value.indexOf('</isif') < 0) {
137
+ throw ExceptionUtils.invalidNestedIsifError(
138
+ newElement.tagType,
139
+ newElement.lineNumber,
140
+ newElement.globalPos,
141
+ state.templatePath
142
+ );
143
+ }
144
+
145
+ return newElement;
146
+ };
147
+
148
+ const parseTagOrExpressionElement = (state, newElement) => {
149
+ const trimmedElement = newElement.value.trim().toLowerCase();
150
+ const isTag = trimmedElement.startsWith('<') && !trimmedElement.startsWith('<!--');
151
+ const isExpression = trimmedElement.startsWith('${');
152
+ const isHtmlOrIsmlComment = trimmedElement.startsWith('<!--');
153
+
154
+ if (isTag) {
155
+ if (trimmedElement.startsWith('<is') || trimmedElement.startsWith('</is')) {
156
+ newElement.type = 'ismlTag';
157
+ } else if (trimmedElement.startsWith('<!DOCTYPE')) {
158
+ newElement.type = 'doctype';
159
+ } else {
160
+ newElement.type = 'htmlTag';
161
+ }
162
+ } else if (isHtmlOrIsmlComment) {
163
+ newElement.type = 'htmlOrIsmlComment';
164
+ } else if (isExpression) {
165
+ newElement.type = 'expression';
166
+ } else {
167
+ newElement.type = 'text';
168
+ }
169
+
170
+ if (isTag) {
171
+ newElement.tagType = getElementType(trimmedElement);
172
+
173
+ newElement.isCustomTag = newElement.type === 'ismlTag' && !SfccTagContainer[newElement.tagType];
174
+ }
175
+
176
+ newElement.isSelfClosing = isSelfClosing(trimmedElement);
177
+
178
+ newElement.isClosingTag = isTag && trimmedElement.startsWith('</');
179
+ newElement.lineNumber = getLineBreakQty(state.pastContent) + getLeadingLineBreakQty(newElement.value) + 1;
180
+ newElement.globalPos = state.pastContent.length + getLeadingEmptyChars(newElement.value).length;
181
+ };
182
+
183
+ const parseTextElement = (state, newElement) => {
184
+ newElement.type = 'text';
185
+ newElement.lineNumber = getLineBreakQty(state.pastContent.substring(0, state.pastContent.length - state.cutSpot))
186
+ + getLeadingLineBreakQty(newElement.value) + 1;
187
+ newElement.globalPos = state.pastContent.length - state.cutSpot + getLeadingEmptyChars(newElement.value).length;
188
+ newElement.isSelfClosing = true;
189
+ };
190
+
191
+ const getElementType = trimmedElement => {
192
+ if (trimmedElement.startsWith('</')) {
193
+ const tailElementType = trimmedElement.slice(2, -1);
194
+
195
+ if (tailElementType.startsWith('${')) {
196
+ return 'dynamic_element';
197
+ }
198
+
199
+ return tailElementType;
200
+ } else {
201
+
202
+ const typeValueLastPos = Math.min(...[
203
+ trimmedElement.indexOf(' '),
204
+ trimmedElement.indexOf('/'),
205
+ trimmedElement.indexOf(Constants.EOL),
206
+ trimmedElement.indexOf('>')
207
+ ].filter(j => j >= 0));
208
+
209
+ const elementType = trimmedElement.substring(1, typeValueLastPos).trim();
210
+
211
+ if (elementType.startsWith('${')) {
212
+ return 'dynamic_element';
213
+ }
214
+
215
+ return elementType;
216
+ }
217
+ };
218
+
219
+ function isSelfClosing(trimmedElement) {
220
+ const ConfigUtils = require('../util/ConfigUtils');
221
+
222
+ const config = ConfigUtils.load();
223
+ const isTag = trimmedElement.startsWith('<') && !trimmedElement.startsWith('<!--');
224
+ const elementType = getElementType(trimmedElement);
225
+ const isDocType = trimmedElement.toLowerCase().startsWith('<!doctype ');
226
+ const isVoidElement = !config.disableHtml5 && Constants.voidElementsArray.indexOf(elementType) >= 0;
227
+ const isHtmlComment = trimmedElement.startsWith('<!--') && trimmedElement.endsWith('-->');
228
+ const isClosingTag = trimmedElement.endsWith('/>');
229
+ const isIsmlTag = trimmedElement.startsWith('<is');
230
+ const isStandardIsmlTag = !!SfccTagContainer[elementType];
231
+ const isCustomIsmlTag = isIsmlTag && !isStandardIsmlTag;
232
+ const isExpression = trimmedElement.startsWith('${') && trimmedElement.endsWith('}');
233
+ const isSfccSelfClosingTag = SfccTagContainer[elementType] && SfccTagContainer[elementType]['self-closing'];
234
+
235
+ // 'isif' tag is never self-closing;
236
+ if (['isif'].indexOf(elementType) >= 0) {
237
+ return false;
238
+ }
239
+
240
+ return !!(isDocType ||
241
+ isVoidElement ||
242
+ isExpression ||
243
+ isHtmlComment ||
244
+ isTag && isClosingTag ||
245
+ isCustomIsmlTag ||
246
+ isIsmlTag && isSfccSelfClosingTag);
247
+ }
248
+
249
+ const getNextOpeningTagOrExpressionInitPos = content => {
250
+ return Math.min(...[
251
+ content.indexOf('<'),
252
+ content.indexOf('<--'),
253
+ content.indexOf('${')
254
+ ].filter(j => j >= 0)) + 1;
255
+ };
256
+
257
+ const getNextClosingTagOrExpressionEndPos = content => {
258
+ return Math.min(...[
259
+ content.indexOf('>'),
260
+ content.indexOf('-->'),
261
+ content.indexOf('}')
262
+ ].filter(j => j >= 0)) + 1;
263
+ };
264
+
265
+ const getInitialState = (templateContent, templatePath, isCrlfLineBreak) => {
266
+ const originalContent = GeneralUtils.toLF(templateContent);
267
+ const originalShadowContent = MaskUtils.maskIgnorableContent(originalContent, null, templatePath);
268
+
269
+ return {
270
+ templatePath : templatePath,
271
+ templateName : templatePath ? path.basename(templatePath) : '',
272
+ originalContent : originalContent,
273
+ originalShadowContent : originalShadowContent,
274
+ remainingContent : originalContent,
275
+ remainingShadowContent : originalShadowContent,
276
+ pastContent : '',
277
+ elementList : [],
278
+ cutSpot : null,
279
+ isCrlfLineBreak
280
+ };
281
+ };
282
+
283
+ const initLoopState = state => {
284
+ state.nextOpeningTagOrExpressionInitPos = getNextOpeningTagOrExpressionInitPos(state.remainingShadowContent);
285
+ state.nextClosingTagOrExpressionEndPos = getNextClosingTagOrExpressionEndPos(state.remainingShadowContent);
286
+ state.cutSpot = null;
287
+ };
288
+
289
+ const finishLoopState = state => {
290
+ const newElement = state.elementList[state.elementList.length - 1];
291
+
292
+ // If there is no element left (only blank spaces and / or line breaks);
293
+ if (!isFinite(state.nextClosingTagOrExpressionEndPos)) {
294
+ state.nextClosingTagOrExpressionEndPos = state.remainingShadowContent.length - 1;
295
+ }
296
+
297
+ if (!state.cutSpot) {
298
+ state.remainingShadowContent = state.remainingShadowContent.substring(newElement.value.length);
299
+ state.remainingContent = state.remainingContent.substring(newElement.value.length);
300
+ state.pastContent = state.originalContent.substring(0, state.pastContent.length + newElement.value.length);
301
+ }
302
+ };
303
+
304
+ const mergeTrailingSpacesWithLastElement = state => {
305
+ const elementList = state.elementList;
306
+ const lastElement = elementList[elementList.length - 1];
307
+ const secondLastElement = elementList[elementList.length - 2];
308
+
309
+ if (lastElement.value.trim().length === 0) {
310
+ secondLastElement.value += lastElement.value;
311
+ elementList.pop();
312
+ }
313
+ };
314
+
315
+ const adjustTrailingSpaces = state => {
316
+
317
+ // Note that last element is not iterated over;
318
+ for (let i = 0; i < state.elementList.length - 1; i++) {
319
+ const previousElement = i > 0 ? state.elementList[i - 1] : null;
320
+ const currentElement = state.elementList[i];
321
+
322
+ if (currentElement.type === 'text'
323
+ && previousElement
324
+ && previousElement.lineNumber !== currentElement.lineNumber
325
+ && previousElement.tagType !== 'isscript'
326
+ ) {
327
+
328
+ const trailingSpacesQty = currentElement.value
329
+ .replace(/\r\n/g, '_')
330
+ .split('')
331
+ .reverse()
332
+ .join('')
333
+ .search(/\S/);
334
+
335
+ if (trailingSpacesQty > 0) {
336
+ const trailingSpaces = currentElement.value.slice(-trailingSpacesQty);
337
+
338
+ currentElement.value = currentElement.value.slice(0, -trailingSpacesQty);
339
+ const nextElement = state.elementList[i + 1];
340
+ nextElement.value = trailingSpaces + nextElement.value;
341
+ }
342
+ }
343
+ }
344
+ };
345
+
346
+ // TODO Refactor this function;
347
+ const checkIfNextElementIsATagOrHtmlComment = (content, state) => {
348
+ const previousElementType = state.elementList.length > 0 && state.elementList[state.elementList.length - 1].tagType;
349
+ const isIscommentContent = previousElementType === 'iscomment';
350
+ const isIsscriptContent = previousElementType === 'isscript';
351
+ const isScriptContent = previousElementType === 'script';
352
+
353
+ return !isIscommentContent && !isScriptContent && !isIsscriptContent && content.startsWith('<') && content.substring(1).match(/^[A-z]/i) || content.startsWith('</') || content.startsWith('<!');
354
+ };
355
+
356
+ const getWrapperTagContent = (state, wrapperTagType) => {
357
+ for (let i = 0; i < state.remainingContent.length; i++) {
358
+ const remainingString = state.remainingContent.substring(i);
359
+
360
+ if (remainingString.startsWith(`</${wrapperTagType}>`)) {
361
+ return state.remainingContent.substring(0, i);
362
+ }
363
+ }
364
+
365
+ return state.remainingContent;
366
+ };
367
+
368
+ const checkIfCurrentElementWrappedByTag = (state, wrapperTagType) => {
369
+ let depth = 0;
370
+
371
+ for (let i = state.elementList.length - 1; i >= 0 ; i--) {
372
+ const element = state.elementList[i];
373
+
374
+ if (element.tagType === wrapperTagType) {
375
+ depth += element.isClosingTag ? -1 : 1;
376
+ }
377
+ }
378
+
379
+ return depth > 0 && !state.remainingContent.trimStart().startsWith(`</${wrapperTagType}>`);
380
+ };
381
+
382
+ const getTextLastContiguousMaskedCharPos = (state, isNextElementATag, isNextElementAnExpression) => {
383
+ const localMaskedContent0 = MaskUtils.maskExpressionContent(state.remainingContent);
384
+ const localMaskedContent1 = MaskUtils.maskInBetween(localMaskedContent0, '<', '>');
385
+
386
+ for (let i = 0; i < localMaskedContent1.length; i++) {
387
+ if (isNextElementATag && localMaskedContent1[i] === '>') {
388
+ return i + 1;
389
+ }
390
+
391
+ if (isNextElementAnExpression && localMaskedContent1[i] === '}') {
392
+ return i + 1;
393
+ }
394
+ }
395
+ };
396
+
397
+ // TODO Refactor this function
398
+ const getNewElement = state => {
399
+
400
+ const trimmedContent = state.remainingContent.trimStart();
401
+ const isWithinIscomment = checkIfCurrentElementWrappedByTag(state, 'iscomment');
402
+ const isWithinIsscript = checkIfCurrentElementWrappedByTag(state, 'isscript');
403
+ const isNextElementATag = trimmedContent.startsWith('<');
404
+ const isNextElementAnExpression = trimmedContent.startsWith('${');
405
+ const isTextElement = !isNextElementATag && !isNextElementAnExpression;
406
+ let lastContiguousMaskedCharPos;
407
+ let elementValue;
408
+
409
+ if (isWithinIscomment) {
410
+ elementValue = getWrapperTagContent(state, 'iscomment');
411
+
412
+ } else if (isWithinIsscript) {
413
+ elementValue = getWrapperTagContent(state, 'isscript');
414
+
415
+ } else if (isTextElement) {
416
+
417
+ for (let i = 0; i < state.remainingContent.length; i++) {
418
+ const remainingString = state.remainingContent.substring(i);
419
+ const isNextElementATagOrHtmlComment = checkIfNextElementIsATagOrHtmlComment(remainingString, state);
420
+
421
+ if (isNextElementATagOrHtmlComment || remainingString.startsWith('${')) {
422
+ lastContiguousMaskedCharPos = i;
423
+ break;
424
+ }
425
+ }
426
+
427
+ elementValue = state.remainingContent.substring(0, lastContiguousMaskedCharPos);
428
+ } else {
429
+ if (state.elementList.length > 0 && state.elementList[state.elementList.length - 1].type === 'text') {
430
+
431
+ lastContiguousMaskedCharPos = getTextLastContiguousMaskedCharPos(state, isNextElementATag, isNextElementAnExpression);
432
+ } else {
433
+ let remainingMaskedContent = state.remainingContent;
434
+
435
+ if (isNextElementATag) {
436
+ remainingMaskedContent = MaskUtils.maskExpressionContent(remainingMaskedContent);
437
+ remainingMaskedContent = MaskUtils.maskQuoteContent(remainingMaskedContent);
438
+ }
439
+
440
+ for (let i = 0; i < remainingMaskedContent.length; i++) {
441
+ if (isNextElementATag && remainingMaskedContent[i] === '>') {
442
+ lastContiguousMaskedCharPos = i + 1;
443
+ break;
444
+ }
445
+ }
446
+ }
447
+
448
+ elementValue = state.remainingShadowContent.startsWith('_') ?
449
+ state.remainingContent.substring(0, lastContiguousMaskedCharPos) :
450
+ state.remainingContent.substring(0, state.nextClosingTagOrExpressionEndPos);
451
+ }
452
+
453
+ return {
454
+ value : elementValue,
455
+ type : undefined,
456
+ globalPos : undefined,
457
+ lineNumber : undefined,
458
+ isSelfClosing : undefined,
459
+ isClosingTag : undefined,
460
+ tagType : undefined
461
+ };
462
+ };
463
+
464
+ const getElementList = (templateContent, templatePath, isCrlfLineBreak) => {
465
+
466
+ const state = getInitialState(templateContent, templatePath, isCrlfLineBreak);
467
+ const elementList = state.elementList;
468
+ let previousStateContent = state.remainingShadowContent;
469
+
470
+ do {
471
+ initLoopState(state);
472
+ parseNextElement(state);
473
+ finishLoopState(state);
474
+
475
+ if (previousStateContent.length === state.remainingShadowContent.length) {
476
+ throw ExceptionUtils.unkownError(templatePath);
477
+ }
478
+
479
+ previousStateContent = state.remainingShadowContent;
480
+
481
+ } while (state.remainingShadowContent.length > 0);
482
+
483
+ adjustTrailingSpaces(state);
484
+ mergeTrailingSpacesWithLastElement(state);
485
+
486
+ return elementList;
487
+ };
488
+
489
+ const getBlankSpaceString = length => {
490
+ let result = '';
491
+
492
+ for (let i = 0; i < length; i++) {
493
+ result += ' ';
494
+ }
495
+
496
+ return result;
497
+ };
498
+
499
+ const getColumnNumber = content => {
500
+ const leadingContent = content.substring(0, getNextNonEmptyCharPos(content));
501
+ const lastLineBreakPos = leadingContent.lastIndexOf(Constants.EOL);
502
+ const precedingEmptySpaces = leadingContent.substring(lastLineBreakPos + 1);
503
+
504
+ return precedingEmptySpaces.length + 1;
505
+ };
506
+
507
+ const getFirstEmptyCharPos = content => {
508
+ const firstLineBreakPos = content.indexOf(Constants.EOL);
509
+ const firstBlankSpacePos = content.indexOf(' ');
510
+
511
+ if (firstLineBreakPos === -1 && firstBlankSpacePos === -1) {
512
+ return content.length;
513
+ } else if (firstLineBreakPos >= 0 && firstBlankSpacePos === -1) {
514
+ return firstLineBreakPos;
515
+ } else if (firstLineBreakPos === -1 && firstBlankSpacePos >= 0) {
516
+ return firstBlankSpacePos;
517
+ } else if (firstLineBreakPos >= 0 && firstBlankSpacePos >= 0) {
518
+ return Math.min(firstLineBreakPos, firstBlankSpacePos);
519
+ }
520
+ };
521
+
522
+ module.exports.getElementList = getElementList;
523
+ module.exports.checkBalance = checkBalance;
524
+ module.exports.getLineBreakQty = getLineBreakQty;
525
+ module.exports.getCharOccurrenceQty = getCharOccurrenceQty;
526
+ module.exports.getNextNonEmptyCharPos = getNextNonEmptyCharPos;
527
+ module.exports.getLeadingEmptyChars = getLeadingEmptyChars;
528
+ module.exports.getLeadingLineBreakQty = getLeadingLineBreakQty;
529
+ module.exports.getTrailingEmptyCharsQty = getTrailingEmptyCharsQty;
530
+ module.exports.getBlankSpaceString = getBlankSpaceString;
531
+ module.exports.getColumnNumber = getColumnNumber;
532
+ module.exports.getFirstEmptyCharPos = getFirstEmptyCharPos;