isml-linter 5.39.1 → 5.40.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 (61) hide show
  1. package/CHANGELOG.md +1177 -1142
  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 -658
  15. package/src/isml_tree/MaskUtils.js +421 -419
  16. package/src/isml_tree/ParseUtils.js +515 -434
  17. package/src/isml_tree/TreeBuilder.js +273 -271
  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 -43
  43. package/src/rules/tree/leading-iscontent.js +35 -39
  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 +149 -136
  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,658 +1,692 @@
1
- const MAX_TEXT_DISPLAY_SIZE = 30;
2
-
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
-
9
- let ID_COUNTER = 0;
10
-
11
- class IsmlNode {
12
-
13
- /**
14
- * @param {String} head node opening tag value, including attributes
15
- * @param {Number} lineNumber node starting line number
16
- * @param {Number} columnNumber node starting column number
17
- * @param {Number} globalPos node starting position since the beginning of the file
18
- * @param {Boolean} isEmbeddedNode whether the node is part of an embedded "sub tree", as a tag attribute
19
- */
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
- }
46
-
47
- // Returns a string. Examples: 'div', 'isprint', 'doctype';
48
- getType() {
49
- if (this.parent && this.parent.isOfType('iscomment')) {
50
- return 'text';
51
- }
52
-
53
- const head = this.head.trim();
54
-
55
- if (head.startsWith('<!--')) {
56
- return 'html_comment';
57
- } else if (this.isDocType()) {
58
- return 'doctype';
59
- } else if (this.isDynamicElement()) {
60
- return 'dynamic_element';
61
- } else if (this.isContainer()) {
62
- return 'multi_clause';
63
- } else if (!head) {
64
- return 'empty';
65
- } else if (head === '(root)') {
66
- return 'root';
67
- } else if (!this.isTag()) {
68
- return 'text';
69
- }
70
-
71
- const regex = /<[a-zA-Z\d_]*(\s|>|\/)/g;
72
-
73
- return head.match(regex)[0].slice(1, -1);
74
- }
75
-
76
- getLastLineNumber() {
77
- return this.tailLineNumber ?
78
- this.tailLineNumber :
79
- this.endLineNumber;
80
- }
81
-
82
- isDocType() {
83
- return this.head.toLowerCase().trim().startsWith('<!doctype ');
84
- }
85
-
86
- // Checks if the node type is dynamically set, such in:
87
- // <${aPdictVariable} />
88
- isDynamicElement() {
89
- return this.head.trim().startsWith('<${');
90
- }
91
-
92
- isInSameLineAsParent() {
93
- return this.parent && !this.parent.isContainer() && this.parent.lineNumber === this.lineNumber;
94
- }
95
-
96
- isInSameLineAsParentEnd() {
97
- return this.parent && !this.parent.isContainer() && this.parent.endLineNumber === this.lineNumber;
98
- }
99
-
100
- isMultiLineOpeningTag() {
101
- return this.isTag() && ParseUtils.getLineBreakQty(this.head.trim()) > 0;
102
- }
103
-
104
- isInSameLineAsPreviousSibling() {
105
- const previousSibling = this.getPreviousSibling();
106
- return previousSibling && previousSibling.getLastLineNumber() === this.lineNumber;
107
- }
108
-
109
- /**
110
- * Gets an array of attributes. For <div class="my_class_1 my my_class_2" style="fancy">, returns:
111
- *
112
- * [
113
- * {
114
- * name : 'class',
115
- * value : 'my_class_1 my my_class_2',
116
- * valueList : ['my_class_1', 'my_class_2']
117
- * },
118
- * {
119
- * name : 'style',
120
- * value : 'fancy',
121
- * valueList : ['fancy']
122
- * }
123
- * ]
124
- */
125
- getAttributeList() {
126
- if (!this.isHtmlTag() && !this.isIsmlTag() || this.isConditionalComment()) {
127
- return [];
128
- }
129
-
130
- return getAttributes(this);
131
- }
132
-
133
- getAttr(name) {
134
- return this.getAttributeList().find( attr => attr.label === name );
135
- }
136
-
137
- addChild(newNode) {
138
- if (newNode.head.trim()) {
139
- newNode.depth = this.depth + 1;
140
- newNode.parent = this;
141
- newNode.childNo = this.children.length;
142
- this.children.push(newNode);
143
- this.newestChildNode = newNode;
144
- } else {
145
- this.tail = newNode.head + this.tail;
146
- }
147
-
148
- newNode.isEmbeddedNode = this.isEmbeddedNode;
149
- }
150
-
151
- getLastChild() { return this.children[this.children.length - 1]; }
152
- getChildrenQty() { return this.children.length; }
153
- hasChildren() { return this.children.length > 0; }
154
-
155
- getIndentationSize() {
156
- return getNodeIndentationSize(this, true);
157
- }
158
-
159
- getTailIndentationSize() {
160
- return getNodeIndentationSize(this, false);
161
- }
162
-
163
- isRoot() { return !this.parent; }
164
-
165
- // Always returns false. It is true only for the container elements, please check ContainerNode class;
166
- isContainer() { return false; }
167
-
168
- isContainerChild() {
169
- return this.parent && this.parent.isContainer();
170
- }
171
-
172
- isScriptContent() {
173
- return this.parent && this.parent.isOfType('isscript');
174
- }
175
-
176
- isTag() {
177
- const value = this.head.trim();
178
-
179
- return value.startsWith('<') &&
180
- value.endsWith('>');
181
- }
182
-
183
- isHtmlTag() {
184
- const value = this.head.trim();
185
-
186
- return value.startsWith('<') &&
187
- !value.startsWith('<is') &&
188
- value.endsWith('>');
189
- }
190
-
191
- isIsmlTag() {
192
- return this.head.trim().startsWith('<is');
193
- }
194
-
195
- isStandardIsmlTag() {
196
- return !!SfccTagContainer[this.getType()];
197
- }
198
-
199
- isCustomIsmlTag() {
200
- return this.isIsmlTag() && !this.isStandardIsmlTag();
201
- }
202
-
203
- isDescendantOf(nodeType) {
204
- let iterator = this.parent;
205
-
206
- while (iterator !== null) {
207
- if (iterator.isOfType(nodeType)) {
208
- return true;
209
- }
210
-
211
- iterator = iterator.parent;
212
- }
213
-
214
- return false;
215
- }
216
-
217
- // For an unwrapped ${someVariable} element, returns true;
218
- // For tags and hardcoded strings , returns false;
219
- isExpression() {
220
- const value = this.head.trim();
221
-
222
- return value.startsWith('${') &&
223
- value.endsWith('}');
224
- }
225
-
226
- isIsmlComment() {
227
- const value = this.head.trim();
228
-
229
- return value === '<iscomment>';
230
- }
231
-
232
- isIsscriptContent() {
233
- return this.parent && this.parent.getType() === 'isscript';
234
- }
235
-
236
- isHtmlComment() {
237
- const value = this.head.trim();
238
-
239
- return value.startsWith('<!--') &&
240
- value.endsWith('-->');
241
- }
242
-
243
- isConditionalComment() {
244
- const value = this.head.trim();
245
-
246
- return value.startsWith('<!--[');
247
- }
248
-
249
- isCommentContent() {
250
- return this.parent && this.parent.isIsmlComment();
251
- }
252
-
253
- getPreviousSibling() {
254
- if (!this.parent || !this.parent.isContainer() && this.isFirstChild() || this.parent.isContainer() && this.parent.isFirstChild() && this.isFirstChild()) {
255
- return null;
256
- }
257
-
258
- const sibling = this.parent.isContainer() && this.isFirstChild() ?
259
- this.parent.parent.children[this.parent.childNo - 1] :
260
- this.parent.children[this.childNo - 1];
261
-
262
- if (sibling.isContainer()) {
263
- return sibling.children[0];
264
- }
265
-
266
- return sibling;
267
- }
268
-
269
- getNextSibling() {
270
- if (!this.parent || this.parent.children.length < this.childNo) {
271
- return null;
272
- }
273
-
274
- const sibling = this.parent.children[this.childNo + 1];
275
-
276
- if (sibling.isContainer()) {
277
- return sibling.children[0];
278
- }
279
-
280
- return sibling;
281
- }
282
-
283
- getTrailingValue() {
284
- return this.tail || this.head;
285
- }
286
-
287
- // Checks if node is HTML 5 void element;
288
- isVoidElement() {
289
- const config = ConfigUtils.load();
290
-
291
- return !config.disableHtml5 && Constants.voidElementsArray.indexOf(this.getType()) >= 0;
292
- }
293
-
294
- // For <div /> , returns true;
295
- // For <div></div>, returns false;
296
- isSelfClosing() {
297
- return !this.isContainer() && (
298
- this.isDocType() ||
299
- this.isVoidElement() ||
300
- this.isHtmlComment() ||
301
- this.isTag() && this.head.trim().endsWith('/>')) ||
302
- this.isCustomIsmlTag() ||
303
- this.isIsmlTag() && SfccTagContainer[this.getType()] && SfccTagContainer[this.getType()]['self-closing'];
304
- }
305
-
306
- isOfType(type) {
307
- return typeof type === 'string' ?
308
- !this.isRoot() && this.getType() === type :
309
- type.some( elem => elem === this.getType());
310
- }
311
-
312
- isOneOfTypes(typeArray) {
313
- for (let i = 0; i < typeArray.length; i++) {
314
- if (this.isOfType(typeArray[i])) {
315
- return true;
316
- }
317
- }
318
-
319
- return false;
320
- }
321
-
322
- isEmpty() {
323
- return !this.head.trim();
324
- }
325
-
326
- isFirstChild() {
327
- if (this.isRoot()) {
328
- return false;
329
- }
330
-
331
- const firstChild = this.parent.children[0];
332
-
333
- return this.lineNumber === firstChild.lineNumber &&
334
- this.head === firstChild.head;
335
- }
336
-
337
- isLastChild() {
338
- return !this.parent || this.parent.getLastChild() === this;
339
- }
340
-
341
- removeChild(node) {
342
- let index = null;
343
-
344
- for (let i = 0; i < this.children.length; i++) {
345
- const child = this.children[i];
346
-
347
- if (child.id === node.id) {
348
- index = i;
349
- break;
350
- }
351
- }
352
-
353
- const removedNode = this.children[index];
354
- this.children.splice(index, 1);
355
- return removedNode;
356
- }
357
-
358
- // No position or line number is set. To get
359
- // it, it is necessary to re-parse the tree;
360
- addChildNodeToPos(node, index) {
361
- node.parent = this;
362
- node.depth = this.depth + 1;
363
- this.children.splice(index, 0, node);
364
- }
365
-
366
- toString(stream = '') {
367
-
368
- if (!this.isContainer() && this.isEmpty() && !this.isLastChild()) {
369
- return stream;
370
- }
371
-
372
- if (!this.isRoot() && !this.isContainer()) {
373
- stream += this.head;
374
- }
375
-
376
- for (let i = 0; i < this.children.length; i++) {
377
- const child = this.children[i];
378
- stream = child.toString(stream);
379
- }
380
-
381
- if (!this.isRoot() && !this.isContainer()) {
382
- stream += this.tail;
383
- }
384
-
385
- return stream;
386
- }
387
-
388
- // Used for debugging purposes only;
389
- print() {
390
- const indentSize = this.depth;
391
- let indentation = '';
392
-
393
- for (let i = 0; i < indentSize; ++i) {
394
- indentation += ' ';
395
- }
396
-
397
- console.log(this.depth + ' :: ' + this.lineNumber + ' :: ' + indentation + getDisplayText(this));
398
-
399
- for (let i = 0; i < this.children.length; i++) {
400
- this.children[i].print();
401
- }
402
- }
403
- }
404
-
405
- /**
406
- * PRIVATE FUNCTIONS
407
- *
408
- * The following are "private" spyOnAllFunctions, which
409
- * will be available for use only within IsmlNode methods;
410
- */
411
-
412
- const getAttributes = node => {
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
- const stringifiedAttributeList = getStringifiedAttributeArray(afterTagContent);
419
- const attributeList = [];
420
-
421
- for (let i = 0; i < stringifiedAttributeList.length; i++) {
422
- const attr = parseAttribute(node, stringifiedAttributeList, i);
423
- attributeList.push(attr);
424
- }
425
-
426
- return attributeList;
427
- };
428
-
429
- // Used for debugging purposes only;
430
- const getDisplayText = node => {
431
- let displayText = node.head;
432
-
433
- displayText = displayText
434
- .replace(new RegExp(Constants.EOL, 'g'), '')
435
- .replace(/ +(?= )/g, '');
436
-
437
- if (node.head.length > MAX_TEXT_DISPLAY_SIZE - 3) {
438
- displayText = displayText.substring(0, MAX_TEXT_DISPLAY_SIZE - 3) + '...';
439
- }
440
-
441
- return displayText.trim();
442
- };
443
-
444
- const getProcessedContent = content => {
445
- let processedContent = content;
446
-
447
- processedContent = MaskUtils.maskExpressionContent(processedContent);
448
- processedContent = MaskUtils.maskIsifTagContent(processedContent);
449
- processedContent = MaskUtils.maskIsprintTagContent(processedContent);
450
- processedContent = MaskUtils.maskJsonContent(processedContent);
451
- processedContent = MaskUtils.maskQuoteContent(processedContent);
452
- processedContent = processedContent.replace(new RegExp(Constants.EOL, 'g'), ' ');
453
-
454
- if (processedContent.endsWith('/')) {
455
- processedContent = processedContent.slice(0, -1) + ' ';
456
- }
457
-
458
- return processedContent;
459
- };
460
-
461
- const getStringifiedAttributeArray = content => {
462
- const maskedContent = getProcessedContent(content);
463
- const attrStartPosList = [];
464
- const result = [];
465
-
466
- const maskedAttributeList = maskedContent
467
- .replace(/><+/g, '> <')
468
- .replace(/\s\s+/g, ' ')
469
- .split(' ')
470
- .filter(attr => attr);
471
-
472
- for (let i = 0; i < maskedContent.length; i++) {
473
- if (i === 0 && maskedContent[i] !== ' ') {
474
- attrStartPosList.push(i);
475
- } else if (maskedContent[i - 1] === ' ' && maskedContent[i] !== ' ' || maskedContent[i - 1] === '>' && maskedContent[i] === '<') {
476
- attrStartPosList.push(i);
477
- }
478
- }
479
-
480
- for (let i = 0; i < maskedAttributeList.length; i++) {
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
-
487
- result.push(fullAttribute);
488
- }
489
-
490
- return result;
491
- };
492
-
493
- const parseAttribute = (node, attributeList, index) => {
494
- const attribute = attributeList[index];
495
- const isAttributeANestedIsmlTag = attribute.startsWith('<is');
496
- const isExpressionAttribute = attribute.startsWith('${') && attribute.endsWith('}');
497
- const trimmedAttribute = attribute.trim();
498
- const trimmedNodeHead = node.head.trim();
499
- const localPos = getAttributeLocalPos(trimmedNodeHead, trimmedAttribute);
500
- const leadingContent = trimmedNodeHead.substring(0, localPos);
501
- const leadingLineBreakQty = ParseUtils.getLineBreakQty(leadingContent);
502
- const isInSameLineAsTagName = leadingLineBreakQty === 0;
503
- const assignmentCharPos = trimmedAttribute.indexOf('=');
504
- const name = assignmentCharPos >= 0 ? trimmedAttribute.substring(0, assignmentCharPos) : trimmedAttribute;
505
- const value = assignmentCharPos >= 0 ? trimmedAttribute.substring(assignmentCharPos + 2, trimmedAttribute.length - 1) : null;
506
- const valueList = getAttributeValueList(value);
507
- const attrLocalPos = trimmedNodeHead.indexOf(trimmedAttribute);
508
- const valueLocalPos = trimmedAttribute.indexOf(value);
509
- const lineNumber = node.lineNumber + leadingLineBreakQty;
510
- const globalPos = node.globalPos + localPos + leadingLineBreakQty - lineNumber + 1;
511
- const hasMultilineValue = value && value.indexOf(Constants.EOL) >= 0;
512
- const isFirstValueInSameLineAsAttributeName = value && ParseUtils.getLeadingLineBreakQty(value) === 0;
513
- const quoteChar = getQuoteChar(trimmedAttribute);
514
- const attributeNameFirstLine = name.split(Constants.EOL)[0];
515
-
516
- const columnNumber = isInSameLineAsTagName ?
517
- node.columnNumber + leadingContent.length :
518
- leadingContent.length - leadingContent.lastIndexOf(Constants.EOL);
519
-
520
- const isFirstInLine = !isInSameLineAsTagName
521
- && trimmedNodeHead
522
- .split(Constants.EOL)
523
- .find(attrLine => attrLine.indexOf(attributeNameFirstLine) >= 0)
524
- .trim()
525
- .indexOf(attributeNameFirstLine) === 0;
526
-
527
- if (isAttributeANestedIsmlTag || isExpressionAttribute) {
528
- return {
529
- name : trimmedAttribute,
530
- value : null,
531
- valueList : null,
532
- localPos,
533
- globalPos,
534
- lineNumber,
535
- columnNumber,
536
- index,
537
- isInSameLineAsTagName,
538
- isFirstInLine,
539
- isFirstValueInSameLineAsAttributeName,
540
- isExpressionAttribute,
541
- hasMultilineValue,
542
- isNestedIsmlTag : isAttributeANestedIsmlTag,
543
- length : trimmedAttribute.length + ParseUtils.getLineBreakQty(trimmedAttribute),
544
- fullContent : trimmedAttribute,
545
- quoteChar,
546
- node
547
- };
548
-
549
- } else {
550
- return {
551
- name,
552
- value,
553
- valueList,
554
- localPos,
555
- globalPos,
556
- lineNumber,
557
- columnNumber,
558
- index,
559
- isInSameLineAsTagName,
560
- isFirstInLine,
561
- isFirstValueInSameLineAsAttributeName,
562
- isExpressionAttribute,
563
- hasMultilineValue,
564
- isNestedIsmlTag: isAttributeANestedIsmlTag,
565
- length : trimmedAttribute.length,
566
- attrGlobalPos : node.globalPos + attrLocalPos,
567
- valueGlobalPos : node.globalPos + valueLocalPos,
568
- fullContent : trimmedAttribute,
569
- quoteChar,
570
- node
571
- };
572
- }
573
- };
574
-
575
- /**
576
- * Two attributes can have the same name, and that is handled here;
577
- */
578
- const getAttributeLocalPos = (trimmedNodeHead, trimmedAttribute) => {
579
- const maskedTrimmedAttribute = MaskUtils.maskQuoteContent(trimmedAttribute);
580
- const maskedTrimmedNodeHead = MaskUtils.maskQuoteContent(trimmedNodeHead);
581
-
582
- let attributeLocalPos = maskedTrimmedNodeHead.indexOf(maskedTrimmedAttribute);
583
- const isCorrectAttribute = trimmedNodeHead.indexOf(trimmedAttribute) === attributeLocalPos;
584
- let remainingNodeHead = maskedTrimmedNodeHead.substring(attributeLocalPos + 1);
585
-
586
- while (!isCorrectAttribute) {
587
- const tempLocalPos = remainingNodeHead.indexOf(maskedTrimmedAttribute) + 1;
588
-
589
- attributeLocalPos += tempLocalPos;
590
-
591
- const remainingContent = trimmedNodeHead.substring(attributeLocalPos);
592
-
593
- if (remainingContent.startsWith(trimmedAttribute)) {
594
- break;
595
- }
596
-
597
- remainingNodeHead = remainingNodeHead.substring(tempLocalPos);
598
- }
599
-
600
- return attributeLocalPos;
601
- };
602
-
603
- const getNodeIndentationSize = (node, isNodeHead) => {
604
-
605
- if (node.isContainer()) {
606
- return 0;
607
- }
608
-
609
- const content = isNodeHead ? node.head : node.tail;
610
- const precedingEmptySpacesLength = content.search(/\S|$/);
611
- const fullPrecedingEmptySpaces = content.substring(0, precedingEmptySpacesLength);
612
- const lineBreakLastPos = Math.max(fullPrecedingEmptySpaces.lastIndexOf(Constants.EOL), 0);
613
- const precedingEmptySpaces = content.substring(lineBreakLastPos, precedingEmptySpacesLength).replace(new RegExp(Constants.EOL, 'g'), '');
614
- const lastLineBreakPos = Math.max(precedingEmptySpaces.lastIndexOf(Constants.EOL), 0);
615
- const indentationSize = precedingEmptySpaces.substring(lastLineBreakPos).length;
616
-
617
- return Math.max(indentationSize, 0);
618
- };
619
-
620
- const getAttributeValueList = fullContent => {
621
-
622
- if (!fullContent) {
623
- return null;
624
- }
625
-
626
- const maskedFullContent = MaskUtils.maskIgnorableContent(fullContent).trim();
627
- const maskedValueList = maskedFullContent.split(/[\s\n]+/).filter( val => val );
628
- const trimmedFullContent = fullContent.trim();
629
- const valueList = [];
630
-
631
- for (let i = 0; i < maskedValueList.length; i++) {
632
- const maskedValueElement = maskedValueList[i];
633
- const index = maskedFullContent.indexOf(maskedValueElement);
634
- const value = trimmedFullContent.substring(index, index + maskedValueElement.length);
635
-
636
- valueList.push(value);
637
- }
638
-
639
- return valueList;
640
- };
641
-
642
- const getQuoteChar = trimmedAttribute => {
643
- const assignmentCharPos = trimmedAttribute.indexOf('=');
644
-
645
- if (assignmentCharPos >= 0) {
646
- if (trimmedAttribute.substring(assignmentCharPos + 1).startsWith('\'')) {
647
- return '\'';
648
- }
649
-
650
- if (trimmedAttribute.substring(assignmentCharPos + 1).startsWith('"')) {
651
- return '"';
652
- }
653
- }
654
-
655
- return '';
656
- };
657
-
658
- module.exports = IsmlNode;
1
+ const MAX_TEXT_DISPLAY_SIZE = 30;
2
+
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
+ const ExceptionUtils = require('../util/ExceptionUtils');
9
+
10
+ let ID_COUNTER = 0;
11
+
12
+ class IsmlNode {
13
+
14
+ /**
15
+ * @param {String} head node opening tag value, including attributes
16
+ * @param {Number} lineNumber node starting line number
17
+ * @param {Number} columnNumber node starting column number
18
+ * @param {Number} globalPos node starting position since the beginning of the file
19
+ * @param {Boolean} isEmbeddedNode whether the node is part of an embedded "sub tree", as a tag attribute
20
+ */
21
+ constructor(head = '(root)', lineNumber = 0, columnNumber, globalPos, isEmbeddedNode) {
22
+ this.id = ID_COUNTER++;
23
+ this.head = head; // '<div class="my_class">'
24
+ this.lineNumber = lineNumber; // 7
25
+ this.columnNumber = columnNumber; // 12
26
+ this.endLineNumber = lineNumber + ParseUtils.getLineBreakQty(head.trim()); // 9
27
+ this.globalPos = globalPos; // 184
28
+ this.depth = 0; // Isml dom tree node depth
29
+ this.tail = ''; // '</div>'
30
+ this.tailLineNumber = null; // 9
31
+ this.tailColumnNumber = null; // 12
32
+ this.tailGlobalPos = null; // 207
33
+ this.parent = null; // Parent isml node;
34
+ this.children = []; // Child isml nodes;
35
+ this.childNo = 0;
36
+ this.isEmbeddedNode = !!isEmbeddedNode;
37
+ }
38
+
39
+ // Tail is the element corresponding closing tag, such as </div>
40
+ setTail(value, lineNumber, columnNumber, globalPos) {
41
+ this.tail += value;
42
+ this.tailLineNumber = lineNumber;
43
+ this.tailColumnNumber = columnNumber;
44
+ this.tailGlobalPos = globalPos;
45
+ this.tailEndLineNumber = lineNumber;
46
+ }
47
+
48
+ // Returns a string. Examples: 'div', 'isprint', 'doctype';
49
+ getType() {
50
+ if (this.parent && this.parent.isOfType('iscomment')) {
51
+ return 'text';
52
+ }
53
+
54
+ const head = this.head.trim();
55
+
56
+ if (head.startsWith('<!--')) {
57
+ return 'html_comment';
58
+ } else if (this.isDocType()) {
59
+ return 'doctype';
60
+ } else if (this.isDynamicElement()) {
61
+ return 'dynamic_element';
62
+ } else if (this.isContainer()) {
63
+ return 'container';
64
+ } else if (!head) {
65
+ return 'empty';
66
+ } else if (head === '(root)') {
67
+ return 'root';
68
+ } else if (!this.isTag()) {
69
+ return 'text';
70
+ }
71
+
72
+ const regex = /<[a-zA-Z\d_-]*(\s|>|\/)/g;
73
+
74
+ return head.match(regex)[0].slice(1, -1);
75
+ }
76
+
77
+ getLastLineNumber() {
78
+ return this.tailLineNumber ?
79
+ this.tailLineNumber :
80
+ this.endLineNumber;
81
+ }
82
+
83
+ isDocType() {
84
+ return this.head.toLowerCase().trim().startsWith('<!doctype ');
85
+ }
86
+
87
+ // Checks if the node type is dynamically set, such in:
88
+ // <${aPdictVariable} />
89
+ isDynamicElement() {
90
+ return this.head.trim().startsWith('<${');
91
+ }
92
+
93
+ isInSameLineAsParent() {
94
+ return this.parent && !this.parent.isContainer() && this.parent.lineNumber === this.lineNumber;
95
+ }
96
+
97
+ isInSameLineAsParentEnd() {
98
+ return this.parent && !this.parent.isContainer() && this.parent.endLineNumber === this.lineNumber;
99
+ }
100
+
101
+ isMultiLineOpeningTag() {
102
+ return this.isTag() && ParseUtils.getLineBreakQty(this.head.trim()) > 0;
103
+ }
104
+
105
+ isInSameLineAsPreviousSibling() {
106
+ const previousSibling = this.getPreviousSibling();
107
+ return previousSibling && previousSibling.getLastLineNumber() === this.lineNumber;
108
+ }
109
+
110
+ /**
111
+ * Gets an array of attributes. For <div class="my_class_1 my my_class_2" style="fancy">, returns:
112
+ *
113
+ * [
114
+ * {
115
+ * name : 'class',
116
+ * value : 'my_class_1 my my_class_2',
117
+ * valueList : ['my_class_1', 'my_class_2']
118
+ * },
119
+ * {
120
+ * name : 'style',
121
+ * value : 'fancy',
122
+ * valueList : ['fancy']
123
+ * }
124
+ * ]
125
+ */
126
+ getAttributeList() {
127
+ if (!this.isHtmlTag() && !this.isIsmlTag() || this.isConditionalComment()) {
128
+ return [];
129
+ }
130
+
131
+ return getAttributes(this);
132
+ }
133
+
134
+ getAttr(name) {
135
+ return this.getAttributeList().find( attr => attr.label === name );
136
+ }
137
+
138
+ addChild(newNode) {
139
+ if (newNode.head.trim()) {
140
+ newNode.depth = this.depth + 1;
141
+ newNode.parent = this;
142
+ newNode.childNo = this.children.length;
143
+ this.children.push(newNode);
144
+ this.newestChildNode = newNode;
145
+ } else {
146
+ this.tail = newNode.head + this.tail;
147
+ }
148
+
149
+ newNode.isEmbeddedNode = this.isEmbeddedNode;
150
+ }
151
+
152
+ getLastChild() { return this.children[this.children.length - 1]; }
153
+ getChildrenQty() { return this.children.length; }
154
+ hasChildren() { return this.children.length > 0; }
155
+
156
+ getIndentationSize() {
157
+ return getNodeIndentationSize(this, true);
158
+ }
159
+
160
+ getTailIndentationSize() {
161
+ return getNodeIndentationSize(this, false);
162
+ }
163
+
164
+ isRoot() { return !this.parent; }
165
+
166
+ // Always returns false. It is true only for the container elements, please check ContainerNode class;
167
+ isContainer() { return false; }
168
+
169
+ isContainerChild() {
170
+ return this.parent && this.parent.isContainer();
171
+ }
172
+
173
+ isScriptContent() {
174
+ return this.parent && this.parent.isOfType('isscript');
175
+ }
176
+
177
+ isTag() {
178
+ const value = this.head.trim();
179
+
180
+ return value.startsWith('<') &&
181
+ value.endsWith('>');
182
+ }
183
+
184
+ isHtmlTag() {
185
+ const value = this.head.trim();
186
+
187
+ return value.startsWith('<') &&
188
+ !value.startsWith('<is') &&
189
+ value.endsWith('>');
190
+ }
191
+
192
+ isIsmlTag() {
193
+ return this.head.trim().startsWith('<is');
194
+ }
195
+
196
+ isStandardIsmlTag() {
197
+ return !!SfccTagContainer[this.getType()];
198
+ }
199
+
200
+ isCustomIsmlTag() {
201
+ return this.isIsmlTag() && !this.isStandardIsmlTag();
202
+ }
203
+
204
+ isDescendantOf(nodeType) {
205
+ let iterator = this.parent;
206
+
207
+ while (iterator !== null) {
208
+ if (iterator.isOfType(nodeType)) {
209
+ return true;
210
+ }
211
+
212
+ iterator = iterator.parent;
213
+ }
214
+
215
+ return false;
216
+ }
217
+
218
+ // For an unwrapped ${someVariable} element, returns true;
219
+ // For tags and hardcoded strings , returns false;
220
+ isExpression() {
221
+ const value = this.head.trim();
222
+
223
+ return value.startsWith('${') &&
224
+ value.endsWith('}');
225
+ }
226
+
227
+ isIsmlComment() {
228
+ const value = this.head.trim();
229
+
230
+ return value === '<iscomment>';
231
+ }
232
+
233
+ isIsscriptContent() {
234
+ return this.parent && this.parent.getType() === 'isscript';
235
+ }
236
+
237
+ isHtmlComment() {
238
+ const value = this.head.trim();
239
+
240
+ return value.startsWith('<!--') &&
241
+ value.endsWith('-->');
242
+ }
243
+
244
+ isConditionalComment() {
245
+ const value = this.head.trim();
246
+
247
+ return value.startsWith('<!--[');
248
+ }
249
+
250
+ isCommentContent() {
251
+ return this.parent && this.parent.isIsmlComment();
252
+ }
253
+
254
+ isHardCodedText() {
255
+ return !this.isRoot()
256
+ && !this.isContainer()
257
+ && !this.isConditionalComment()
258
+ && !this.isTag()
259
+ && !this.isExpression();
260
+ }
261
+
262
+ getPreviousSibling() {
263
+ if (!this.parent || !this.parent.isContainer() && this.isFirstChild() || this.parent.isContainer() && this.parent.isFirstChild() && this.isFirstChild()) {
264
+ return null;
265
+ }
266
+
267
+ const sibling = this.parent.isContainer() && this.isFirstChild() ?
268
+ this.parent.parent.children[this.parent.childNo - 1] :
269
+ this.parent.children[this.childNo - 1];
270
+
271
+ if (sibling.isContainer()) {
272
+ return sibling.children[0];
273
+ }
274
+
275
+ return sibling;
276
+ }
277
+
278
+ getNextSibling() {
279
+ if (!this.parent || this.parent.children.length < this.childNo) {
280
+ return null;
281
+ }
282
+
283
+ const sibling = this.parent.children[this.childNo + 1];
284
+
285
+ if (sibling.isContainer()) {
286
+ return sibling.children[0];
287
+ }
288
+
289
+ return sibling;
290
+ }
291
+
292
+ getTrailingValue() {
293
+ return this.tail || this.head;
294
+ }
295
+
296
+ // Checks if node is HTML 5 void element;
297
+ isVoidElement() {
298
+ const config = ConfigUtils.load();
299
+
300
+ return !config.disableHtml5 && Constants.voidElementsArray.indexOf(this.getType()) >= 0;
301
+ }
302
+
303
+ // For <div /> , returns true;
304
+ // For <div></div>, returns false;
305
+ isSelfClosing() {
306
+ return !this.isContainer() && (
307
+ this.isDocType() ||
308
+ this.isVoidElement() ||
309
+ this.isHtmlComment() ||
310
+ this.isTag() && this.head.trim().endsWith('/>')) ||
311
+ this.isCustomIsmlTag() ||
312
+ this.isIsmlTag() && SfccTagContainer[this.getType()] && SfccTagContainer[this.getType()]['self-closing'];
313
+ }
314
+
315
+ isOfType(type) {
316
+ return typeof type === 'string' ?
317
+ !this.isRoot() && this.getType() === type :
318
+ type.some( elem => elem === this.getType());
319
+ }
320
+
321
+ isOneOfTypes(typeArray) {
322
+ for (let i = 0; i < typeArray.length; i++) {
323
+ if (this.isOfType(typeArray[i])) {
324
+ return true;
325
+ }
326
+ }
327
+
328
+ return false;
329
+ }
330
+
331
+ isEmpty() {
332
+ return !this.head.trim();
333
+ }
334
+
335
+ isFirstChild() {
336
+ if (this.isRoot()) {
337
+ return false;
338
+ }
339
+
340
+ const firstChild = this.parent.children[0];
341
+
342
+ return this.lineNumber === firstChild.lineNumber &&
343
+ this.head === firstChild.head;
344
+ }
345
+
346
+ isLastChild() {
347
+ return !this.parent || this.parent.getLastChild() === this;
348
+ }
349
+
350
+ removeChild(node) {
351
+ let index = null;
352
+
353
+ for (let i = 0; i < this.children.length; i++) {
354
+ const child = this.children[i];
355
+
356
+ if (child.id === node.id) {
357
+ index = i;
358
+ break;
359
+ }
360
+ }
361
+
362
+ const removedNode = this.children[index];
363
+ this.children.splice(index, 1);
364
+ return removedNode;
365
+ }
366
+
367
+ // No position or line number is set. To get
368
+ // it, it is necessary to re-parse the tree;
369
+ addChildNodeToPos(node, index) {
370
+ node.parent = this;
371
+ node.depth = this.depth + 1;
372
+ this.children.splice(index, 0, node);
373
+ }
374
+
375
+ getRoot() {
376
+ let rootNode = this;
377
+
378
+ while (rootNode.parent) {
379
+ rootNode = rootNode.parent;
380
+ }
381
+
382
+ return rootNode;
383
+ }
384
+
385
+ toString(stream = '') {
386
+
387
+ if (!this.isContainer() && this.isEmpty() && !this.isLastChild()) {
388
+ return stream;
389
+ }
390
+
391
+ if (!this.isRoot() && !this.isContainer()) {
392
+ stream += this.head;
393
+ }
394
+
395
+ for (let i = 0; i < this.children.length; i++) {
396
+ const child = this.children[i];
397
+ stream = child.toString(stream);
398
+ }
399
+
400
+ if (!this.isRoot() && !this.isContainer()) {
401
+ stream += this.tail;
402
+ }
403
+
404
+ return stream;
405
+ }
406
+
407
+ // Used for debugging purposes only;
408
+ print() {
409
+ const indentSize = this.depth;
410
+ let indentation = '';
411
+
412
+ for (let i = 0; i < indentSize; ++i) {
413
+ indentation += ' ';
414
+ }
415
+
416
+ console.log(this.depth + ' :: ' + this.lineNumber + ' :: ' + indentation + getDisplayText(this));
417
+
418
+ for (let i = 0; i < this.children.length; i++) {
419
+ this.children[i].print();
420
+ }
421
+ }
422
+ }
423
+
424
+ /**
425
+ * PRIVATE FUNCTIONS
426
+ *
427
+ * The following are "private" spyOnAllFunctions, which
428
+ * will be available for use only within IsmlNode methods;
429
+ */
430
+
431
+ const getAttributes = node => {
432
+ const trimmedHead = node.head.trim();
433
+ const nodeHead = trimmedHead.substring(1, trimmedHead.length - 1);
434
+ const firstSpaceAfterTagPos = ParseUtils.getFirstEmptyCharPos(trimmedHead);
435
+ const leadingEmptySpaceQty = ParseUtils.getNextNonEmptyCharPos(nodeHead);
436
+ const afterTagContent = nodeHead.substring(leadingEmptySpaceQty + firstSpaceAfterTagPos);
437
+ const stringifiedAttributeList = getStringifiedAttributeArray(afterTagContent);
438
+ const attributeList = [];
439
+
440
+ for (let i = 0; i < stringifiedAttributeList.length; i++) {
441
+ const attr = parseAttribute(node, stringifiedAttributeList, i);
442
+ attributeList.push(attr);
443
+ }
444
+
445
+ return attributeList;
446
+ };
447
+
448
+ // Used for debugging purposes only;
449
+ const getDisplayText = node => {
450
+ let displayText = node.head;
451
+
452
+ displayText = displayText
453
+ .replace(new RegExp(Constants.EOL, 'g'), '')
454
+ .replace(/ +(?= )/g, '');
455
+
456
+ if (node.head.length > MAX_TEXT_DISPLAY_SIZE - 3) {
457
+ displayText = displayText.substring(0, MAX_TEXT_DISPLAY_SIZE - 3) + '...';
458
+ }
459
+
460
+ return displayText.trim();
461
+ };
462
+
463
+ const getProcessedContent = content => {
464
+ let processedContent = content;
465
+
466
+ processedContent = MaskUtils.maskExpressionContent(processedContent);
467
+ processedContent = MaskUtils.maskIsifTagContent(processedContent);
468
+ processedContent = MaskUtils.maskIsprintTagContent(processedContent);
469
+ processedContent = MaskUtils.maskJsonContent(processedContent);
470
+ processedContent = MaskUtils.maskQuoteContent(processedContent);
471
+ processedContent = processedContent.replace(new RegExp(Constants.EOL, 'g'), ' ');
472
+
473
+ if (processedContent.endsWith('/')) {
474
+ processedContent = processedContent.slice(0, -1) + ' ';
475
+ }
476
+
477
+ return processedContent;
478
+ };
479
+
480
+ const getStringifiedAttributeArray = content => {
481
+ const maskedContent = getProcessedContent(content);
482
+ const attrStartPosList = [];
483
+ const result = [];
484
+
485
+ const maskedAttributeList = maskedContent
486
+ .replace(/><+/g, '> <')
487
+ .replace(/\s\s+/g, ' ')
488
+ .split(' ')
489
+ .filter(attr => attr);
490
+
491
+ for (let i = 0; i < maskedContent.length; i++) {
492
+ if (i === 0 && maskedContent[i] !== ' ') {
493
+ attrStartPosList.push(i);
494
+ } else if (maskedContent[i - 1] === ' ' && maskedContent[i] !== ' ' || maskedContent[i - 1] === '>' && maskedContent[i] === '<') {
495
+ attrStartPosList.push(i);
496
+ }
497
+ }
498
+
499
+ for (let i = 0; i < maskedAttributeList.length; i++) {
500
+ let fullAttribute = content.substring(attrStartPosList[i] - 1, attrStartPosList[i] + maskedAttributeList[i].length).trim();
501
+
502
+ if (fullAttribute.endsWith('/')) {
503
+ fullAttribute = fullAttribute.slice(0, -1) + ' ';
504
+ }
505
+
506
+ result.push(fullAttribute);
507
+ }
508
+
509
+ return result;
510
+ };
511
+
512
+ const parseAttribute = (node, attributeList, index) => {
513
+ const attribute = attributeList[index];
514
+ const isAttributeANestedIsmlTag = attribute.startsWith('<is');
515
+ const isExpressionAttribute = attribute.startsWith('${') && attribute.endsWith('}');
516
+ const trimmedAttribute = attribute.trim();
517
+ const trimmedNodeHead = node.head.trim();
518
+ const localPos = getAttributeLocalPos(node, trimmedNodeHead, trimmedAttribute);
519
+ const leadingContent = trimmedNodeHead.substring(0, localPos);
520
+ const leadingLineBreakQty = ParseUtils.getLineBreakQty(leadingContent);
521
+ const isInSameLineAsTagName = leadingLineBreakQty === 0;
522
+ const assignmentCharPos = trimmedAttribute.indexOf('=');
523
+ const name = assignmentCharPos >= 0 ? trimmedAttribute.substring(0, assignmentCharPos) : trimmedAttribute;
524
+ const value = assignmentCharPos >= 0 ? trimmedAttribute.substring(assignmentCharPos + 2, trimmedAttribute.length - 1) : null;
525
+ const valueList = getAttributeValueList(value);
526
+ const attrLocalPos = trimmedNodeHead.indexOf(trimmedAttribute);
527
+ const valueLocalPos = trimmedAttribute.indexOf(value);
528
+ const lineNumber = node.lineNumber + leadingLineBreakQty;
529
+ const globalPos = node.globalPos + localPos + leadingLineBreakQty - lineNumber + 1;
530
+ const hasMultilineValue = value && value.indexOf(Constants.EOL) >= 0;
531
+ const isFirstValueInSameLineAsAttributeName = value && ParseUtils.getLeadingLineBreakQty(value) === 0;
532
+ const quoteChar = getQuoteChar(trimmedAttribute);
533
+ const attributeNameFirstLine = name.split(Constants.EOL)[0];
534
+
535
+ const columnNumber = isInSameLineAsTagName ?
536
+ node.columnNumber + leadingContent.length :
537
+ leadingContent.length - leadingContent.lastIndexOf(Constants.EOL);
538
+
539
+ const isFirstInLine = !isInSameLineAsTagName
540
+ && trimmedNodeHead
541
+ .split(Constants.EOL)
542
+ .find(attrLine => attrLine.indexOf(attributeNameFirstLine) >= 0)
543
+ .trim()
544
+ .indexOf(attributeNameFirstLine) === 0;
545
+
546
+ if (isAttributeANestedIsmlTag || isExpressionAttribute) {
547
+ return {
548
+ name : trimmedAttribute,
549
+ value : null,
550
+ valueList : null,
551
+ localPos,
552
+ globalPos,
553
+ lineNumber,
554
+ columnNumber,
555
+ index,
556
+ isInSameLineAsTagName,
557
+ isFirstInLine,
558
+ isFirstValueInSameLineAsAttributeName,
559
+ isExpressionAttribute,
560
+ hasMultilineValue,
561
+ isNestedIsmlTag : isAttributeANestedIsmlTag,
562
+ length : trimmedAttribute.length + ParseUtils.getLineBreakQty(trimmedAttribute),
563
+ fullContent : trimmedAttribute,
564
+ quoteChar,
565
+ node
566
+ };
567
+
568
+ } else {
569
+ return {
570
+ name,
571
+ value,
572
+ valueList,
573
+ localPos,
574
+ globalPos,
575
+ lineNumber,
576
+ columnNumber,
577
+ index,
578
+ isInSameLineAsTagName,
579
+ isFirstInLine,
580
+ isFirstValueInSameLineAsAttributeName,
581
+ isExpressionAttribute,
582
+ hasMultilineValue,
583
+ isNestedIsmlTag: isAttributeANestedIsmlTag,
584
+ length : trimmedAttribute.length,
585
+ attrGlobalPos : node.globalPos + attrLocalPos,
586
+ valueGlobalPos : node.globalPos + valueLocalPos,
587
+ fullContent : trimmedAttribute,
588
+ quoteChar,
589
+ node
590
+ };
591
+ }
592
+ };
593
+
594
+ /**
595
+ * Two attributes can have the same name, and that is handled here;
596
+ */
597
+ const getAttributeLocalPos = (node, trimmedNodeHead, trimmedAttribute) => {
598
+ const maskedTrimmedAttribute = MaskUtils.maskQuoteContent(trimmedAttribute);
599
+ const maskedTrimmedNodeHead = MaskUtils.maskQuoteContent(trimmedNodeHead);
600
+
601
+ let attributeLocalPos = maskedTrimmedNodeHead.indexOf(maskedTrimmedAttribute);
602
+ const isCorrectAttribute = trimmedNodeHead.indexOf(trimmedAttribute) === attributeLocalPos;
603
+ let remainingNodeHead = maskedTrimmedNodeHead.substring(attributeLocalPos + 1);
604
+
605
+ const maxLoops = 500;
606
+ let loopQty = 0;
607
+
608
+ while (!isCorrectAttribute) {
609
+ const tempLocalPos = remainingNodeHead.indexOf(maskedTrimmedAttribute) + 1;
610
+
611
+ attributeLocalPos += tempLocalPos;
612
+
613
+ const remainingContent = trimmedNodeHead.substring(attributeLocalPos);
614
+
615
+ if (remainingContent.startsWith(trimmedAttribute)) {
616
+ break;
617
+ }
618
+
619
+ remainingNodeHead = remainingNodeHead.substring(tempLocalPos);
620
+
621
+ loopQty++;
622
+
623
+ if (loopQty >= maxLoops) {
624
+ throw ExceptionUtils.parseError(
625
+ node.getType(),
626
+ node.lineNumber,
627
+ node.globalPos,
628
+ node.head.length,
629
+ node.getRoot().tree.templatePath
630
+ );
631
+ }
632
+ }
633
+
634
+ return attributeLocalPos;
635
+ };
636
+
637
+ const getNodeIndentationSize = (node, isNodeHead) => {
638
+
639
+ if (node.isContainer()) {
640
+ return 0;
641
+ }
642
+
643
+ const content = isNodeHead ? node.head : node.tail;
644
+ const precedingEmptySpacesLength = content.search(/\S|$/);
645
+ const fullPrecedingEmptySpaces = content.substring(0, precedingEmptySpacesLength);
646
+ const lineBreakLastPos = Math.max(fullPrecedingEmptySpaces.lastIndexOf(Constants.EOL), 0);
647
+ const precedingEmptySpaces = content.substring(lineBreakLastPos, precedingEmptySpacesLength).replace(new RegExp(Constants.EOL, 'g'), '');
648
+ const lastLineBreakPos = Math.max(precedingEmptySpaces.lastIndexOf(Constants.EOL), 0);
649
+ const indentationSize = precedingEmptySpaces.substring(lastLineBreakPos).length;
650
+
651
+ return Math.max(indentationSize, 0);
652
+ };
653
+
654
+ const getAttributeValueList = fullContent => {
655
+
656
+ if (!fullContent) {
657
+ return null;
658
+ }
659
+
660
+ const maskedFullContent = MaskUtils.maskIgnorableContent(fullContent).trim();
661
+ const maskedValueList = maskedFullContent.split(/[\s\n]+/).filter( val => val );
662
+ const trimmedFullContent = fullContent.trim();
663
+ const valueList = [];
664
+
665
+ for (let i = 0; i < maskedValueList.length; i++) {
666
+ const maskedValueElement = maskedValueList[i];
667
+ const index = maskedFullContent.indexOf(maskedValueElement);
668
+ const value = trimmedFullContent.substring(index, index + maskedValueElement.length);
669
+
670
+ valueList.push(value);
671
+ }
672
+
673
+ return valueList;
674
+ };
675
+
676
+ const getQuoteChar = trimmedAttribute => {
677
+ const assignmentCharPos = trimmedAttribute.indexOf('=');
678
+
679
+ if (assignmentCharPos >= 0) {
680
+ if (trimmedAttribute.substring(assignmentCharPos + 1).startsWith('\'')) {
681
+ return '\'';
682
+ }
683
+
684
+ if (trimmedAttribute.substring(assignmentCharPos + 1).startsWith('"')) {
685
+ return '"';
686
+ }
687
+ }
688
+
689
+ return '';
690
+ };
691
+
692
+ module.exports = IsmlNode;