isml-linter 5.38.2 → 5.39.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +63 -33
  2. package/README.md +2 -1
  3. package/package.json +2 -2
  4. package/src/Constants.js +2 -2
  5. package/src/IsmlLinter.js +14 -0
  6. package/src/enums/{SfccTags.js → SfccTagContainer.js} +0 -0
  7. package/src/isml_tree/ContainerNode.js +1 -1
  8. package/src/isml_tree/IsmlNode.js +88 -83
  9. package/src/isml_tree/MaskUtils.js +2 -2
  10. package/src/isml_tree/ParseUtils.js +15 -13
  11. package/src/isml_tree/TreeBuilder.js +47 -18
  12. package/src/rules/prototypes/TreeRulePrototype.js +4 -8
  13. package/src/rules/tree/align-isset.js +2 -2
  14. package/src/rules/tree/contextual-attrs.js +4 -4
  15. package/src/rules/tree/custom-tags.js +6 -6
  16. package/src/rules/tree/disallow-tags.js +2 -2
  17. package/src/rules/tree/empty-eof.js +11 -11
  18. package/src/rules/tree/enforce-security.js +2 -2
  19. package/src/rules/tree/eslint-to-isscript.js +13 -12
  20. package/src/rules/tree/indent.js +73 -73
  21. package/src/rules/tree/leading-iscache.js +1 -1
  22. package/src/rules/tree/leading-iscontent.js +1 -1
  23. package/src/rules/tree/no-deprecated-attrs.js +4 -4
  24. package/src/rules/tree/no-embedded-isml.js +2 -2
  25. package/src/rules/tree/no-hardcode.js +6 -6
  26. package/src/rules/tree/no-iselse-slash.js +3 -3
  27. package/src/rules/tree/no-redundant-context.js +8 -8
  28. package/src/rules/tree/no-require-in-loop.js +8 -8
  29. package/src/rules/tree/one-element-per-line.js +3 -3
  30. package/src/util/ConsoleUtils.js +44 -2
  31. package/src/util/{CustomTagUtils.js → CustomTagContainer.js} +0 -0
  32. package/src/util/ExceptionUtils.js +27 -0
  33. package/src/util/RuleUtils.js +16 -6
  34. package/src/util/TempRuleUtils.js +4 -4
@@ -1,47 +1,47 @@
1
1
  const MAX_TEXT_DISPLAY_SIZE = 30;
2
2
 
3
- const ConfigUtils = require('../util/ConfigUtils');
4
- const Constants = require('../Constants');
5
- const SfccTags = require('../enums/SfccTags');
6
- const ParseUtils = require('./ParseUtils');
7
- const MaskUtils = require('./MaskUtils');
3
+ const ConfigUtils = require('../util/ConfigUtils');
4
+ const Constants = require('../Constants');
5
+ const SfccTagContainer = require('../enums/SfccTagContainer');
6
+ const ParseUtils = require('./ParseUtils');
7
+ const MaskUtils = require('./MaskUtils');
8
8
 
9
9
  let ID_COUNTER = 0;
10
10
 
11
11
  class IsmlNode {
12
12
 
13
13
  /**
14
- * @param {String} value node opening tag value, including attributes
14
+ * @param {String} head node opening tag value, including attributes
15
15
  * @param {Number} lineNumber node starting line number
16
16
  * @param {Number} columnNumber node starting column number
17
17
  * @param {Number} globalPos node starting position since the beginning of the file
18
18
  * @param {Boolean} isEmbeddedNode whether the node is part of an embedded "sub tree", as a tag attribute
19
19
  */
20
- constructor(value = '(root)', lineNumber = 0, columnNumber, globalPos, isEmbeddedNode) {
21
- this.id = ID_COUNTER++;
22
- this.value = value; // '<div class="my_class">'
23
- this.lineNumber = lineNumber; // 7
24
- this.columnNumber = columnNumber; // 12
25
- this.endLineNumber = lineNumber + ParseUtils.getLineBreakQty(value.trim()); // 9
26
- this.globalPos = globalPos; // 184
27
- this.depth = 0; // Isml dom tree node depth
28
- this.suffixValue = ''; // '</div>'
29
- this.suffixLineNumber = null; // 9
30
- this.suffixColumnNumber = null; // 12
31
- this.suffixGlobalPos = null; // 207
32
- this.parent = null; // Parent isml node;
33
- this.children = []; // Child isml nodes;
34
- this.childNo = 0;
35
- this.isEmbeddedNode = !!isEmbeddedNode;
36
- }
37
-
38
- // Suffix is the element corresponding closing tag, such as </div>
39
- setSuffix(value, lineNumber, columnNumber, globalPos) {
40
- this.suffixValue += value;
41
- this.suffixLineNumber = lineNumber;
42
- this.suffixColumnNumber = columnNumber;
43
- this.suffixGlobalPos = globalPos;
44
- this.suffixEndLineNumber = lineNumber;
20
+ constructor(head = '(root)', lineNumber = 0, columnNumber, globalPos, isEmbeddedNode) {
21
+ this.id = ID_COUNTER++;
22
+ this.head = head; // '<div class="my_class">'
23
+ this.lineNumber = lineNumber; // 7
24
+ this.columnNumber = columnNumber; // 12
25
+ this.endLineNumber = lineNumber + ParseUtils.getLineBreakQty(head.trim()); // 9
26
+ this.globalPos = globalPos; // 184
27
+ this.depth = 0; // Isml dom tree node depth
28
+ this.tail = ''; // '</div>'
29
+ this.tailLineNumber = null; // 9
30
+ this.tailColumnNumber = null; // 12
31
+ this.tailGlobalPos = null; // 207
32
+ this.parent = null; // Parent isml node;
33
+ this.children = []; // Child isml nodes;
34
+ this.childNo = 0;
35
+ this.isEmbeddedNode = !!isEmbeddedNode;
36
+ }
37
+
38
+ // Tail is the element corresponding closing tag, such as </div>
39
+ setTail(value, lineNumber, columnNumber, globalPos) {
40
+ this.tail += value;
41
+ this.tailLineNumber = lineNumber;
42
+ this.tailColumnNumber = columnNumber;
43
+ this.tailGlobalPos = globalPos;
44
+ this.tailEndLineNumber = lineNumber;
45
45
  }
46
46
 
47
47
  // Returns a string. Examples: 'div', 'isprint', 'doctype';
@@ -50,9 +50,9 @@ class IsmlNode {
50
50
  return 'text';
51
51
  }
52
52
 
53
- const value = this.value.trim();
53
+ const head = this.head.trim();
54
54
 
55
- if (value.startsWith('<!--')) {
55
+ if (head.startsWith('<!--')) {
56
56
  return 'html_comment';
57
57
  } else if (this.isDocType()) {
58
58
  return 'doctype';
@@ -60,9 +60,9 @@ class IsmlNode {
60
60
  return 'dynamic_element';
61
61
  } else if (this.isContainer()) {
62
62
  return 'multi_clause';
63
- } else if (!value) {
63
+ } else if (!head) {
64
64
  return 'empty';
65
- } else if (value === '(root)') {
65
+ } else if (head === '(root)') {
66
66
  return 'root';
67
67
  } else if (!this.isTag()) {
68
68
  return 'text';
@@ -70,23 +70,23 @@ class IsmlNode {
70
70
 
71
71
  const regex = /<[a-zA-Z\d_]*(\s|>|\/)/g;
72
72
 
73
- return value.match(regex)[0].slice(1, -1);
73
+ return head.match(regex)[0].slice(1, -1);
74
74
  }
75
75
 
76
76
  getLastLineNumber() {
77
- return this.suffixLineNumber ?
78
- this.suffixLineNumber :
77
+ return this.tailLineNumber ?
78
+ this.tailLineNumber :
79
79
  this.endLineNumber;
80
80
  }
81
81
 
82
82
  isDocType() {
83
- return this.value.toLowerCase().trim().startsWith('<!doctype ');
83
+ return this.head.toLowerCase().trim().startsWith('<!doctype ');
84
84
  }
85
85
 
86
86
  // Checks if the node type is dynamically set, such in:
87
87
  // <${aPdictVariable} />
88
88
  isDynamicElement() {
89
- return this.value.trim().startsWith('<${');
89
+ return this.head.trim().startsWith('<${');
90
90
  }
91
91
 
92
92
  isInSameLineAsParent() {
@@ -98,7 +98,7 @@ class IsmlNode {
98
98
  }
99
99
 
100
100
  isMultiLineOpeningTag() {
101
- return this.isTag() && ParseUtils.getLineBreakQty(this.value.trim()) > 0;
101
+ return this.isTag() && ParseUtils.getLineBreakQty(this.head.trim()) > 0;
102
102
  }
103
103
 
104
104
  isInSameLineAsPreviousSibling() {
@@ -135,28 +135,28 @@ class IsmlNode {
135
135
  }
136
136
 
137
137
  addChild(newNode) {
138
- if (newNode.value.trim()) {
138
+ if (newNode.head.trim()) {
139
139
  newNode.depth = this.depth + 1;
140
140
  newNode.parent = this;
141
141
  newNode.childNo = this.children.length;
142
142
  this.children.push(newNode);
143
143
  this.newestChildNode = newNode;
144
144
  } else {
145
- this.suffixValue = newNode.value + this.suffixValue;
145
+ this.tail = newNode.head + this.tail;
146
146
  }
147
147
 
148
148
  newNode.isEmbeddedNode = this.isEmbeddedNode;
149
149
  }
150
150
 
151
- getLastChild() { return this.children[this.children.length - 1]; }
151
+ getLastChild() { return this.children[this.children.length - 1]; }
152
152
  getChildrenQty() { return this.children.length; }
153
- hasChildren() { return this.children.length > 0; }
153
+ hasChildren() { return this.children.length > 0; }
154
154
 
155
155
  getIndentationSize() {
156
156
  return getNodeIndentationSize(this, true);
157
157
  }
158
158
 
159
- getSuffixIndentationSize() {
159
+ getTailIndentationSize() {
160
160
  return getNodeIndentationSize(this, false);
161
161
  }
162
162
 
@@ -174,14 +174,14 @@ class IsmlNode {
174
174
  }
175
175
 
176
176
  isTag() {
177
- const value = this.value.trim();
177
+ const value = this.head.trim();
178
178
 
179
179
  return value.startsWith('<') &&
180
180
  value.endsWith('>');
181
181
  }
182
182
 
183
183
  isHtmlTag() {
184
- const value = this.value.trim();
184
+ const value = this.head.trim();
185
185
 
186
186
  return value.startsWith('<') &&
187
187
  !value.startsWith('<is') &&
@@ -189,11 +189,11 @@ class IsmlNode {
189
189
  }
190
190
 
191
191
  isIsmlTag() {
192
- return this.value.trim().startsWith('<is');
192
+ return this.head.trim().startsWith('<is');
193
193
  }
194
194
 
195
195
  isStandardIsmlTag() {
196
- return !!SfccTags[this.getType()];
196
+ return !!SfccTagContainer[this.getType()];
197
197
  }
198
198
 
199
199
  isCustomIsmlTag() {
@@ -217,14 +217,14 @@ class IsmlNode {
217
217
  // For an unwrapped ${someVariable} element, returns true;
218
218
  // For tags and hardcoded strings , returns false;
219
219
  isExpression() {
220
- const value = this.value.trim();
220
+ const value = this.head.trim();
221
221
 
222
222
  return value.startsWith('${') &&
223
223
  value.endsWith('}');
224
224
  }
225
225
 
226
226
  isIsmlComment() {
227
- const value = this.value.trim();
227
+ const value = this.head.trim();
228
228
 
229
229
  return value === '<iscomment>';
230
230
  }
@@ -234,14 +234,14 @@ class IsmlNode {
234
234
  }
235
235
 
236
236
  isHtmlComment() {
237
- const value = this.value.trim();
237
+ const value = this.head.trim();
238
238
 
239
239
  return value.startsWith('<!--') &&
240
240
  value.endsWith('-->');
241
241
  }
242
242
 
243
243
  isConditionalComment() {
244
- const value = this.value.trim();
244
+ const value = this.head.trim();
245
245
 
246
246
  return value.startsWith('<!--[');
247
247
  }
@@ -281,7 +281,7 @@ class IsmlNode {
281
281
  }
282
282
 
283
283
  getTrailingValue() {
284
- return this.suffixValue || this.value;
284
+ return this.tail || this.head;
285
285
  }
286
286
 
287
287
  // Checks if node is HTML 5 void element;
@@ -298,9 +298,9 @@ class IsmlNode {
298
298
  this.isDocType() ||
299
299
  this.isVoidElement() ||
300
300
  this.isHtmlComment() ||
301
- this.isTag() && this.value.trim().endsWith('/>')) ||
301
+ this.isTag() && this.head.trim().endsWith('/>')) ||
302
302
  this.isCustomIsmlTag() ||
303
- this.isIsmlTag() && SfccTags[this.getType()] && SfccTags[this.getType()]['self-closing'];
303
+ this.isIsmlTag() && SfccTagContainer[this.getType()] && SfccTagContainer[this.getType()]['self-closing'];
304
304
  }
305
305
 
306
306
  isOfType(type) {
@@ -320,7 +320,7 @@ class IsmlNode {
320
320
  }
321
321
 
322
322
  isEmpty() {
323
- return !this.value.trim();
323
+ return !this.head.trim();
324
324
  }
325
325
 
326
326
  isFirstChild() {
@@ -331,7 +331,7 @@ class IsmlNode {
331
331
  const firstChild = this.parent.children[0];
332
332
 
333
333
  return this.lineNumber === firstChild.lineNumber &&
334
- this.value === firstChild.value;
334
+ this.head === firstChild.head;
335
335
  }
336
336
 
337
337
  isLastChild() {
@@ -370,7 +370,7 @@ class IsmlNode {
370
370
  }
371
371
 
372
372
  if (!this.isRoot() && !this.isContainer()) {
373
- stream += this.value;
373
+ stream += this.head;
374
374
  }
375
375
 
376
376
  for (let i = 0; i < this.children.length; i++) {
@@ -379,7 +379,7 @@ class IsmlNode {
379
379
  }
380
380
 
381
381
  if (!this.isRoot() && !this.isContainer()) {
382
- stream += this.suffixValue;
382
+ stream += this.tail;
383
383
  }
384
384
 
385
385
  return stream;
@@ -410,11 +410,11 @@ class IsmlNode {
410
410
  */
411
411
 
412
412
  const getAttributes = node => {
413
- const trimmedValue = node.value.trim();
414
- const nodeValue = trimmedValue.substring(1, trimmedValue.length - 1);
415
- const firstSpaceAfterTagPos = ParseUtils.getFirstEmptyCharPos(trimmedValue);
416
- const leadingEmptySpaceQty = ParseUtils.getNextNonEmptyCharPos(nodeValue);
417
- const afterTagContent = nodeValue.substring(leadingEmptySpaceQty + firstSpaceAfterTagPos);
413
+ const trimmedHead = node.head.trim();
414
+ const nodeHead = trimmedHead.substring(1, trimmedHead.length - 1);
415
+ const firstSpaceAfterTagPos = ParseUtils.getFirstEmptyCharPos(trimmedHead);
416
+ const leadingEmptySpaceQty = ParseUtils.getNextNonEmptyCharPos(nodeHead);
417
+ const afterTagContent = nodeHead.substring(leadingEmptySpaceQty + firstSpaceAfterTagPos);
418
418
  const stringifiedAttributeList = getStringifiedAttributeArray(afterTagContent);
419
419
  const attributeList = [];
420
420
 
@@ -428,13 +428,13 @@ const getAttributes = node => {
428
428
 
429
429
  // Used for debugging purposes only;
430
430
  const getDisplayText = node => {
431
- let displayText = node.value;
431
+ let displayText = node.head;
432
432
 
433
433
  displayText = displayText
434
434
  .replace(new RegExp(Constants.EOL, 'g'), '')
435
435
  .replace(/ +(?= )/g, '');
436
436
 
437
- if (node.value.length > MAX_TEXT_DISPLAY_SIZE - 3) {
437
+ if (node.head.length > MAX_TEXT_DISPLAY_SIZE - 3) {
438
438
  displayText = displayText.substring(0, MAX_TEXT_DISPLAY_SIZE - 3) + '...';
439
439
  }
440
440
 
@@ -478,7 +478,12 @@ const getStringifiedAttributeArray = content => {
478
478
  }
479
479
 
480
480
  for (let i = 0; i < maskedAttributeList.length; i++) {
481
- const fullAttribute = content.substring(attrStartPosList[i], attrStartPosList[i] + maskedAttributeList[i].length);
481
+ let fullAttribute = content.substring(attrStartPosList[i] - 1, attrStartPosList[i] + maskedAttributeList[i].length).trim();
482
+
483
+ if (fullAttribute.endsWith('/')) {
484
+ fullAttribute = fullAttribute.slice(0, -1) + ' ';
485
+ }
486
+
482
487
  result.push(fullAttribute);
483
488
  }
484
489
 
@@ -490,16 +495,16 @@ const parseAttribute = (node, attributeList, index) => {
490
495
  const isAttributeANestedIsmlTag = attribute.startsWith('<is');
491
496
  const isExpressionAttribute = attribute.startsWith('${') && attribute.endsWith('}');
492
497
  const trimmedAttribute = attribute.trim();
493
- const trimmedNodeValue = node.value.trim();
494
- const localPos = getAttributeLocalPos(trimmedNodeValue, trimmedAttribute);
495
- const leadingContent = trimmedNodeValue.substring(0, localPos);
498
+ const trimmedNodeHead = node.head.trim();
499
+ const localPos = getAttributeLocalPos(trimmedNodeHead, trimmedAttribute);
500
+ const leadingContent = trimmedNodeHead.substring(0, localPos);
496
501
  const leadingLineBreakQty = ParseUtils.getLineBreakQty(leadingContent);
497
502
  const isInSameLineAsTagName = leadingLineBreakQty === 0;
498
503
  const assignmentCharPos = trimmedAttribute.indexOf('=');
499
504
  const name = assignmentCharPos >= 0 ? trimmedAttribute.substring(0, assignmentCharPos) : trimmedAttribute;
500
505
  const value = assignmentCharPos >= 0 ? trimmedAttribute.substring(assignmentCharPos + 2, trimmedAttribute.length - 1) : null;
501
506
  const valueList = getAttributeValueList(value);
502
- const attrLocalPos = trimmedNodeValue.indexOf(trimmedAttribute);
507
+ const attrLocalPos = trimmedNodeHead.indexOf(trimmedAttribute);
503
508
  const valueLocalPos = trimmedAttribute.indexOf(value);
504
509
  const lineNumber = node.lineNumber + leadingLineBreakQty;
505
510
  const globalPos = node.globalPos + localPos + leadingLineBreakQty - lineNumber + 1;
@@ -513,7 +518,7 @@ const parseAttribute = (node, attributeList, index) => {
513
518
  leadingContent.length - leadingContent.lastIndexOf(Constants.EOL);
514
519
 
515
520
  const isFirstInLine = !isInSameLineAsTagName
516
- && trimmedNodeValue
521
+ && trimmedNodeHead
517
522
  .split(Constants.EOL)
518
523
  .find(attrLine => attrLine.indexOf(attributeNameFirstLine) >= 0)
519
524
  .trim()
@@ -570,26 +575,26 @@ const parseAttribute = (node, attributeList, index) => {
570
575
  /**
571
576
  * Two attributes can have the same name, and that is handled here;
572
577
  */
573
- const getAttributeLocalPos = (trimmedNodeValue, trimmedAttribute) => {
578
+ const getAttributeLocalPos = (trimmedNodeHead, trimmedAttribute) => {
574
579
  const maskedTrimmedAttribute = MaskUtils.maskQuoteContent(trimmedAttribute);
575
- const maskedTrimmedNodeValue = MaskUtils.maskQuoteContent(trimmedNodeValue);
580
+ const maskedTrimmedNodeHead = MaskUtils.maskQuoteContent(trimmedNodeHead);
576
581
 
577
- let attributeLocalPos = maskedTrimmedNodeValue.indexOf(maskedTrimmedAttribute);
578
- const isCorrectAttribute = trimmedNodeValue.indexOf(trimmedAttribute) === attributeLocalPos;
579
- let remainingNodeValue = maskedTrimmedNodeValue.substring(attributeLocalPos + 1);
582
+ let attributeLocalPos = maskedTrimmedNodeHead.indexOf(maskedTrimmedAttribute);
583
+ const isCorrectAttribute = trimmedNodeHead.indexOf(trimmedAttribute) === attributeLocalPos;
584
+ let remainingNodeHead = maskedTrimmedNodeHead.substring(attributeLocalPos + 1);
580
585
 
581
586
  while (!isCorrectAttribute) {
582
- const tempLocalPos = remainingNodeValue.indexOf(maskedTrimmedAttribute) + 1;
587
+ const tempLocalPos = remainingNodeHead.indexOf(maskedTrimmedAttribute) + 1;
583
588
 
584
589
  attributeLocalPos += tempLocalPos;
585
590
 
586
- const remainingContent = trimmedNodeValue.substring(attributeLocalPos);
591
+ const remainingContent = trimmedNodeHead.substring(attributeLocalPos);
587
592
 
588
593
  if (remainingContent.startsWith(trimmedAttribute)) {
589
594
  break;
590
595
  }
591
596
 
592
- remainingNodeValue = remainingNodeValue.substring(tempLocalPos);
597
+ remainingNodeHead = remainingNodeHead.substring(tempLocalPos);
593
598
  }
594
599
 
595
600
  return attributeLocalPos;
@@ -601,7 +606,7 @@ const getNodeIndentationSize = (node, isNodeHead) => {
601
606
  return 0;
602
607
  }
603
608
 
604
- const content = isNodeHead ? node.value : node.suffixValue;
609
+ const content = isNodeHead ? node.head : node.tail;
605
610
  const precedingEmptySpacesLength = content.search(/\S|$/);
606
611
  const fullPrecedingEmptySpaces = content.substring(0, precedingEmptySpacesLength);
607
612
  const lineBreakLastPos = Math.max(fullPrecedingEmptySpaces.lastIndexOf(Constants.EOL), 0);
@@ -20,7 +20,6 @@ const maskIgnorableContent = (content, shouldMaskBorders, templatePath) => {
20
20
  maskedContent = maskInBetweenIsscriptTags(maskedContent);
21
21
  maskedContent = maskInBetweenForTagWithAttributes(maskedContent, 'script', 'type=\'text/javascript\'');
22
22
  maskedContent = maskInBetweenForTagWithAttributes(maskedContent, 'script', 'type="text/javascript"');
23
- maskedContent = maskQuoteContent(maskedContent);
24
23
  maskedContent = maskInBetweenForTagWithAttributes(maskedContent, 'isscript');
25
24
 
26
25
  checkTagBalance(maskedContent, content, templatePath);
@@ -28,6 +27,7 @@ const maskIgnorableContent = (content, shouldMaskBorders, templatePath) => {
28
27
  maskedContent = maskInBetweenForTagWithAttributes(maskedContent, 'script');
29
28
  maskedContent = maskInBetweenForTagWithAttributes(maskedContent, 'style');
30
29
  maskedContent = maskInBetween2(maskedContent, '<', '>');
30
+ maskedContent = maskQuoteContent(maskedContent);
31
31
  maskedContent = maskIsifTagContent(maskedContent);
32
32
  maskedContent = maskIsprintTagContent(maskedContent);
33
33
 
@@ -306,7 +306,7 @@ const maskIsifTagContent = content => {
306
306
  if (remainingContent.startsWith(closingTag)) {
307
307
  isWithinIsifTag = false;
308
308
  maskedContent += closingTag;
309
- i += closingTag.length;
309
+ i += closingTag.length - 1;
310
310
  }
311
311
  }
312
312
 
@@ -4,12 +4,12 @@
4
4
  * simply analyze it and return relevant information;
5
5
  */
6
6
 
7
- const path = require('path');
8
- const Constants = require('../Constants');
9
- const ExceptionUtils = require('../util/ExceptionUtils');
10
- const SfccTags = require('../enums/SfccTags');
11
- const GeneralUtils = require('../util/GeneralUtils');
12
- const MaskUtils = require('./MaskUtils');
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
13
 
14
14
  const getNextNonEmptyChar = content => {
15
15
  return content.replace(new RegExp(Constants.EOL, 'g'), '').trim()[0];
@@ -89,14 +89,14 @@ const checkBalance = (node, templatePath) => {
89
89
  if (!node.isRoot() &&
90
90
  node.parent && !node.parent.isContainer() &&
91
91
  (node.isHtmlTag() || node.isIsmlTag()) &&
92
- !node.isSelfClosing() && !node.suffixValue
92
+ !node.isSelfClosing() && !node.tail
93
93
  && !node.parent.isOfType('iscomment')
94
94
  ) {
95
95
  throw ExceptionUtils.unbalancedElementError(
96
96
  node.getType(),
97
97
  node.lineNumber,
98
98
  node.globalPos,
99
- node.value.trim().length,
99
+ node.head.trim().length,
100
100
  templatePath
101
101
  );
102
102
  }
@@ -174,6 +174,8 @@ const parseTagOrExpressionElement = (state, newElement) => {
174
174
 
175
175
  if (isTag) {
176
176
  newElement.tagType = getElementType(trimmedElement);
177
+
178
+ newElement.isCustomTag = newElement.type === 'ismlTag' && !SfccTagContainer[newElement.tagType];
177
179
  }
178
180
 
179
181
  newElement.isSelfClosing = isSelfClosing(trimmedElement);
@@ -193,13 +195,13 @@ const parseTextElement = (state, newElement) => {
193
195
 
194
196
  const getElementType = trimmedElement => {
195
197
  if (trimmedElement.startsWith('</')) {
196
- const suffixElementType = trimmedElement.slice(2, -1);
198
+ const tailElementType = trimmedElement.slice(2, -1);
197
199
 
198
- if (suffixElementType.startsWith('${')) {
200
+ if (tailElementType.startsWith('${')) {
199
201
  return 'dynamic_element';
200
202
  }
201
203
 
202
- return suffixElementType;
204
+ return tailElementType;
203
205
  } else {
204
206
 
205
207
  const typeValueLastPos = Math.min(...[
@@ -229,10 +231,10 @@ function isSelfClosing(trimmedElement) {
229
231
  const isHtmlComment = trimmedElement.startsWith('<!--') && trimmedElement.endsWith('-->');
230
232
  const isClosingTag = trimmedElement.endsWith('/>');
231
233
  const isIsmlTag = trimmedElement.startsWith('<is');
232
- const isStandardIsmlTag = !!SfccTags[elementType];
234
+ const isStandardIsmlTag = !!SfccTagContainer[elementType];
233
235
  const isCustomIsmlTag = isIsmlTag && !isStandardIsmlTag;
234
236
  const isExpression = trimmedElement.startsWith('${') && trimmedElement.endsWith('}');
235
- const isSfccSelfClosingTag = SfccTags[elementType] && SfccTags[elementType]['self-closing'];
237
+ const isSfccSelfClosingTag = SfccTagContainer[elementType] && SfccTagContainer[elementType]['self-closing'];
236
238
 
237
239
  // 'isif' tag is never self-closing;
238
240
  if (['isif'].indexOf(elementType) >= 0) {
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const IsmlNode = require('./IsmlNode');
3
3
  const ParseUtils = require('./ParseUtils');
4
+ const MaskUtils = require('./MaskUtils');
4
5
  const ContainerNode = require('./ContainerNode');
5
6
  const ExceptionUtils = require('../util/ExceptionUtils');
6
7
  const GeneralUtils = require('../util/GeneralUtils');
@@ -13,6 +14,9 @@ const parse = (content, templatePath, isCrlfLineBreak, isEmbeddedNode) => {
13
14
 
14
15
  for (let i = 0; i < elementList.length; i++) {
15
16
  const element = elementList[i];
17
+
18
+ validateNodeHead(element, templatePath);
19
+
16
20
  const newNode = new IsmlNode(element.value, element.lineNumber, element.columnNumber, element.globalPos, isEmbeddedNode);
17
21
 
18
22
  const containerResult = parseContainerElements(element, currentParent, newNode, templatePath);
@@ -38,7 +42,7 @@ const parseContainerElements = (element, currentParent, newNode, templatePath) =
38
42
  currentParent.getType(),
39
43
  currentParent.lineNumber,
40
44
  currentParent.globalPos,
41
- currentParent.value.trim().length,
45
+ currentParent.head.trim().length,
42
46
  templatePath
43
47
  );
44
48
  }
@@ -84,12 +88,12 @@ const parseContainerElements = (element, currentParent, newNode, templatePath) =
84
88
  currentParent.getType(),
85
89
  currentParent.lineNumber,
86
90
  currentParent.globalPos,
87
- currentParent.value.trim().length,
91
+ currentParent.head.trim().length,
88
92
  templatePath
89
93
  );
90
94
  }
91
95
 
92
- currentParent.setSuffix(element.value, element.lineNumber, element.columnNumber, element.globalPos);
96
+ currentParent.setTail(element.value, element.lineNumber, element.columnNumber, element.globalPos);
93
97
  currentParent = currentParent.parent.parent;
94
98
 
95
99
  return {
@@ -115,8 +119,15 @@ const parseNonContainerElements = (element, currentParent, newNode, templatePath
115
119
  }
116
120
  } else if (element.isClosingTag) {
117
121
 
122
+ const parentLastChild = currentParent.getLastChild();
123
+
118
124
  if (element.tagType === currentParent.getType()) {
119
- currentParent.setSuffix(element.value, element.lineNumber, element.columnNumber, element.globalPos);
125
+ currentParent.setTail(element.value, element.lineNumber, element.columnNumber, element.globalPos);
126
+
127
+ } else if (element.isCustomTag && element.tagType === parentLastChild.getType()) {
128
+ parentLastChild.setTail(element.value, element.lineNumber, element.columnNumber, element.globalPos);
129
+
130
+ currentParent = parentLastChild;
120
131
 
121
132
  } else if (element.isClosingTag && currentParent.isRoot()) {
122
133
  throw ExceptionUtils.unbalancedElementError(
@@ -158,22 +169,22 @@ const postProcess = (node, data = {}) => {
158
169
  for (let i = 0; i < node.children.length; i++) {
159
170
  const child = node.children[i];
160
171
 
161
- if (child.value.indexOf('template="util/modules"') >= 0) {
172
+ if (child.head.indexOf('template="util/modules"') >= 0) {
162
173
  data.moduleDefinition = {
163
- value : child.value,
174
+ value : child.head,
164
175
  lineNumber : child.lineNumber,
165
176
  globalPos : child.globalPos,
166
- length : child.value.trim().length
177
+ length : child.head.trim().length
167
178
  };
168
179
  }
169
180
 
170
181
  if (child.isCustomIsmlTag()) {
171
182
  data.customModuleArray = data.customModuleArray || [];
172
183
  data.customModuleArray.push({
173
- value : child.value,
184
+ value : child.head,
174
185
  lineNumber : child.lineNumber,
175
186
  globalPos : child.globalPos,
176
- length : child.value.trim().length
187
+ length : child.head.trim().length
177
188
  });
178
189
  }
179
190
 
@@ -211,10 +222,10 @@ const build = (templatePath, content, isCrlfLineBreak) => {
211
222
 
212
223
  /**
213
224
  * In the main part of tree build, a node A might hold the next node B's indentation in the last part of
214
- * A, be it in its value or suffix value. This function removes that trailing indentation from A and
225
+ * A, be it in its value or tail value. This function removes that trailing indentation from A and
215
226
  * adds it to B as a leading indentation;
216
227
  */
217
- function rectifyNodeIndentation(node, child) {
228
+ const rectifyNodeIndentation = (node, child) => {
218
229
  const previousSibling = child.getPreviousSibling();
219
230
 
220
231
  if (child.isContainer()) {
@@ -222,21 +233,39 @@ function rectifyNodeIndentation(node, child) {
222
233
  }
223
234
 
224
235
  if (previousSibling && previousSibling.isOfType('text')) {
225
- const trailingLineBreakQty = ParseUtils.getTrailingEmptyCharsQty(previousSibling.value);
236
+ const trailingLineBreakQty = ParseUtils.getTrailingEmptyCharsQty(previousSibling.head);
226
237
 
227
- previousSibling.value = previousSibling.value.substring(0, previousSibling.value.length - trailingLineBreakQty);
228
- child.value = ParseUtils.getBlankSpaceString(trailingLineBreakQty) + child.value;
238
+ previousSibling.head = previousSibling.head.substring(0, previousSibling.head.length - trailingLineBreakQty);
239
+ child.head = ParseUtils.getBlankSpaceString(trailingLineBreakQty) + child.head;
229
240
  }
230
241
 
231
242
  if (child.isLastChild() && child.isOfType('text')) {
232
243
  let trailingLineBreakQty = 0;
233
244
 
234
- trailingLineBreakQty = ParseUtils.getTrailingEmptyCharsQty(child.value);
235
- child.value = child.value.substring(0, child.value.length - trailingLineBreakQty);
245
+ trailingLineBreakQty = ParseUtils.getTrailingEmptyCharsQty(child.head);
246
+ child.head = child.head.substring(0, child.head.length - trailingLineBreakQty);
247
+
248
+ node.tail = ParseUtils.getBlankSpaceString(trailingLineBreakQty) + node.tail;
249
+ }
250
+ };
251
+
252
+ const validateNodeHead = (element, templatePath) => {
253
+
254
+ if (element.type !== 'text') {
255
+ const trimmedElement = element.value.trim();
256
+ const maskedElement = MaskUtils.maskQuoteContent(trimmedElement);
236
257
 
237
- node.suffixValue = ParseUtils.getBlankSpaceString(trailingLineBreakQty) + node.suffixValue;
258
+ if (maskedElement.endsWith('_')) {
259
+ throw ExceptionUtils.unbalancedQuotesError(
260
+ element.tagType,
261
+ element.lineNumber,
262
+ element.globalPos,
263
+ trimmedElement.length,
264
+ templatePath
265
+ );
266
+ }
238
267
  }
239
- }
268
+ };
240
269
 
241
270
  module.exports.build = build;
242
271
  module.exports.parse = parse;