ember-eslint-parser 0.7.0 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-eslint-parser",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -68,6 +68,8 @@
68
68
  "lint:js": "eslint . --max-warnings=0",
69
69
  "lint:js:fix": "eslint . --fix --max-warnings=0",
70
70
  "lint:package": "pnpm publint",
71
+ "bench": "vitest bench",
72
+ "bench:compare": "node scripts/bench-compare.mjs",
71
73
  "test": "vitest run"
72
74
  }
73
75
  }
@@ -2,6 +2,9 @@ const eslintScope = require('eslint-scope');
2
2
  const DocumentLines = require('../utils/document');
3
3
  const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transforms');
4
4
 
5
+ // Constant: Program + all Glimmer node types. Computed once at module load.
6
+ const hbsVisitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() };
7
+
5
8
  /**
6
9
  * implements https://eslint.org/docs/latest/extend/custom-parsers
7
10
  * for Handlebars (.hbs) template files.
@@ -64,7 +67,7 @@ module.exports = {
64
67
  };
65
68
 
66
69
  // Build visitor keys: Program + all Glimmer node types
67
- const visitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() };
70
+ const visitorKeys = hbsVisitorKeys;
68
71
 
69
72
  // Create an empty scope manager.
70
73
  // For HBS, all locals are assumed to be defined at runtime,
@@ -3,9 +3,9 @@ const glimmer = require('@glimmer/syntax');
3
3
  const { visitorKeys: glimmerVisitorKeys } = glimmer;
4
4
  const DocumentLines = require('../utils/document');
5
5
  const { Reference, Scope, Variable, Definition } = require('eslint-scope');
6
- const htmlTags = require('html-tags').default;
7
- const svgTags = require('svg-tags');
8
- const mathMLTags = require('mathml-tag-names').mathmlTagNames;
6
+ const htmlTagsSet = new Set(require('html-tags').default);
7
+ const svgTagsSet = new Set(require('svg-tags'));
8
+ const mathMLTagsSet = new Set(require('mathml-tag-names').mathmlTagNames);
9
9
 
10
10
  let TypescriptScope = null;
11
11
  try {
@@ -19,31 +19,6 @@ try {
19
19
  // not available
20
20
  }
21
21
 
22
- const BufferMap = new Map();
23
-
24
- function getBuffer(str) {
25
- let buf = BufferMap.get(str);
26
- if (!buf) {
27
- buf = Buffer.from(str);
28
- BufferMap.set(str, buf);
29
- }
30
- return buf;
31
- }
32
-
33
- function sliceByteRange(str, a, b) {
34
- const buf = getBuffer(str);
35
- return buf.slice(a, b).toString();
36
- }
37
-
38
- function byteToCharIndex(str, byteOffset) {
39
- const buf = getBuffer(str);
40
- return buf.slice(0, byteOffset).toString().length;
41
- }
42
-
43
- function byteLength(str) {
44
- return getBuffer(str).length;
45
- }
46
-
47
22
  /**
48
23
  * finds the nearest node scope
49
24
  * @param scopeManager
@@ -67,27 +42,33 @@ function findParentScope(scopeManager, nodePath) {
67
42
  * tries to find the variable names {name} in any parent scope
68
43
  * if the variable is not found it just returns the nearest scope,
69
44
  * so that it's usage can be registered.
45
+ *
46
+ * Also returns the nearest scope (equivalent to findParentScope) in one pass,
47
+ * avoiding the redundant second traversal that findParentScope would perform.
70
48
  * @param scopeManager
71
49
  * @param nodePath
72
50
  * @param name
73
51
  * @return {{scope: null, variable: *}|{scope: (*|null)}}
74
52
  */
75
53
  function findVarInParentScopes(scopeManager, nodePath, name) {
76
- let scope = null;
54
+ let defScope = null;
55
+ let currentScope = null;
77
56
  let path = nodePath;
78
57
  while (path) {
79
- scope = scopeManager.acquire(path.node, true);
80
- if (scope && scope.set.has(name)) {
81
- break;
58
+ const s = scopeManager.acquire(path.node, true);
59
+ if (s) {
60
+ if (!currentScope) currentScope = s;
61
+ if (s.set.has(name)) {
62
+ defScope = s;
63
+ break;
64
+ }
82
65
  }
83
66
  path = path.parentPath;
84
67
  }
85
- const currentScope = findParentScope(scopeManager, nodePath);
86
- if (!scope) {
68
+ if (!defScope) {
87
69
  return { scope: currentScope };
88
70
  }
89
-
90
- return { scope: currentScope, variable: scope.set.get(name) };
71
+ return { scope: currentScope, variable: defScope.set.get(name) };
91
72
  }
92
73
 
93
74
  /**
@@ -115,9 +96,12 @@ function registerNodeInScope(node, scope, variable) {
115
96
  /**
116
97
  * Builds the complete Glimmer visitor keys map with "Glimmer" prefix and
117
98
  * additional keys needed for traversal (blockParamNodes, parts, etc).
99
+ * Result is cached since glimmerVisitorKeys is a constant.
118
100
  * @return {object}
119
101
  */
102
+ let _cachedGlimmerVisitorKeys = null;
120
103
  function buildGlimmerVisitorKeys() {
104
+ if (_cachedGlimmerVisitorKeys) return _cachedGlimmerVisitorKeys;
121
105
  const keys = {};
122
106
  for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
123
107
  keys[`Glimmer${k}`] = [...v];
@@ -127,6 +111,7 @@ function buildGlimmerVisitorKeys() {
127
111
  }
128
112
  keys.GlimmerProgram = ['body', 'blockParamNodes'];
129
113
  keys.GlimmerTemplate = ['body'];
114
+ _cachedGlimmerVisitorKeys = keys;
130
115
  return keys;
131
116
  }
132
117
 
@@ -170,7 +155,7 @@ function traverse(visitorKeys, node, visitor) {
170
155
  queue.push({
171
156
  node: item,
172
157
  parent: currentPath.node,
173
- context: Object.assign({}, currentPath.context),
158
+ context: currentPath.context,
174
159
  parentKey: visitorKey,
175
160
  parentPath: currentPath,
176
161
  });
@@ -179,7 +164,7 @@ function traverse(visitorKeys, node, visitor) {
179
164
  queue.push({
180
165
  node: child,
181
166
  parent: currentPath.node,
182
- context: Object.assign({}, currentPath.context),
167
+ context: currentPath.context,
183
168
  parentKey: visitorKey,
184
169
  parentPath: currentPath,
185
170
  });
@@ -200,8 +185,14 @@ function isAlphaNumeric(code) {
200
185
  );
201
186
  }
202
187
 
203
- function isWhiteSpace(code) {
204
- return code === ' ' || code === '\t' || code === '\r' || code === '\n' || code === '\v';
188
+ function isWhiteSpaceCode(code) {
189
+ return (
190
+ code === 32 /* space */ ||
191
+ code === 9 /* tab */ ||
192
+ code === 13 /* carriageReturn */ ||
193
+ code === 10 /* lineFeed */ ||
194
+ code === 11 /* verticalTab */
195
+ );
205
196
  }
206
197
 
207
198
  /**
@@ -213,8 +204,7 @@ function isWhiteSpace(code) {
213
204
  */
214
205
  function tokenize(template, doc, startOffset) {
215
206
  const tokens = [];
216
- let current = '';
217
- let start = 0;
207
+ let wordStart = -1;
218
208
  function pushToken(value, type, range) {
219
209
  const t = {
220
210
  type,
@@ -229,24 +219,28 @@ function tokenize(template, doc, startOffset) {
229
219
  };
230
220
  tokens.push(t);
231
221
  }
232
- for (const [i, c] of [...template].entries()) {
233
- if (isAlphaNumeric(c.codePointAt(0))) {
234
- if (current.length === 0) {
235
- start = i;
222
+ for (let i = 0; i < template.length; i++) {
223
+ const code = template.charCodeAt(i);
224
+ if (isAlphaNumeric(code)) {
225
+ if (wordStart < 0) {
226
+ wordStart = i;
236
227
  }
237
- current += c;
238
228
  } else {
239
- let range = [startOffset + start, startOffset + i];
240
- if (current.length > 0) {
241
- pushToken(current, 'word', range);
242
- current = '';
229
+ if (wordStart >= 0) {
230
+ pushToken(template.slice(wordStart, i), 'word', [startOffset + wordStart, startOffset + i]);
231
+ wordStart = -1;
243
232
  }
244
- range = [startOffset + i, startOffset + i + 1];
245
- if (!isWhiteSpace(c)) {
246
- pushToken(c, 'Punctuator', range);
233
+ if (!isWhiteSpaceCode(code)) {
234
+ pushToken(template[i], 'Punctuator', [startOffset + i, startOffset + i + 1]);
247
235
  }
248
236
  }
249
237
  }
238
+ if (wordStart >= 0) {
239
+ pushToken(template.slice(wordStart), 'word', [
240
+ startOffset + wordStart,
241
+ startOffset + template.length,
242
+ ]);
243
+ }
250
244
  return tokens;
251
245
  }
252
246
 
@@ -306,27 +300,55 @@ function removeFromParent(nodes) {
306
300
  * @return {object[]}
307
301
  */
308
302
  function buildTokenStream(rawTokens, comments, textNodes) {
309
- let tokens = rawTokens.filter(
310
- (t) => !comments.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
311
- );
312
- tokens = tokens.filter(
313
- (t) => !textNodes.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
303
+ // Build sorted interval arrays for O(log n) exclusion checks
304
+ const commentIntervals = comments.map((c) => c.range).sort((a, b) => a[0] - b[0]);
305
+ const textNodeIntervals = textNodes.map((t) => t.range).sort((a, b) => a[0] - b[0]);
306
+
307
+ /**
308
+ * Binary-search: is the token's range fully covered by any interval in `intervals`?
309
+ * Intervals must be sorted by start offset.
310
+ * @param {number[]} tokenRange
311
+ * @param {number[][]} intervals
312
+ */
313
+ function isCovered(tokenRange, intervals) {
314
+ let lo = 0;
315
+ let hi = intervals.length - 1;
316
+ while (lo <= hi) {
317
+ const mid = (lo + hi) >> 1;
318
+ const iv = intervals[mid];
319
+ if (iv[0] <= tokenRange[0] && iv[1] >= tokenRange[1]) {
320
+ return true;
321
+ }
322
+ if (iv[0] > tokenRange[0]) {
323
+ hi = mid - 1;
324
+ } else {
325
+ lo = mid + 1;
326
+ }
327
+ }
328
+ return false;
329
+ }
330
+
331
+ // Single-pass filter: drop tokens covered by a comment or text node
332
+ const filteredTokens = rawTokens.filter(
333
+ (t) => !isCovered(t.range, commentIntervals) && !isCovered(t.range, textNodeIntervals)
314
334
  );
315
335
 
316
- const sortedTextNodes = [...textNodes];
317
- let currentTextNode = sortedTextNodes.pop();
318
- for (let i = tokens.length - 1; i >= 0; i--) {
319
- const t = tokens[i];
320
- while (currentTextNode && t.range[0] < currentTextNode.range[0]) {
321
- tokens.splice(i + 1, 0, currentTextNode);
322
- currentTextNode = sortedTextNodes.pop();
336
+ // Merge text nodes (already sorted by position from the AST) into filteredTokens
337
+ // using a single linear merge pass instead of repeated splice calls.
338
+ const sortedTextNodes = [...textNodes].sort((a, b) => a.range[0] - b.range[0]);
339
+ const result = [];
340
+ let ti = 0;
341
+ for (const token of filteredTokens) {
342
+ while (ti < sortedTextNodes.length && sortedTextNodes[ti].range[0] < token.range[0]) {
343
+ result.push(sortedTextNodes[ti++]);
323
344
  }
345
+ result.push(token);
324
346
  }
325
- if (currentTextNode) {
326
- tokens.unshift(currentTextNode);
347
+ while (ti < sortedTextNodes.length) {
348
+ result.push(sortedTextNodes[ti++]);
327
349
  }
328
350
 
329
- return tokens;
351
+ return result;
330
352
  }
331
353
 
332
354
  /**
@@ -399,6 +421,18 @@ function processGlimmerTemplate({ templateContent, codeLines, templateRange, tok
399
421
  });
400
422
  }
401
423
 
424
+ // Nullify empty hashes before the type is renamed
425
+ if (
426
+ (n.type === 'MustacheStatement' ||
427
+ n.type === 'BlockStatement' ||
428
+ n.type === 'SubExpression') &&
429
+ n.hash &&
430
+ n.hash.pairs &&
431
+ n.hash.pairs.length === 0
432
+ ) {
433
+ n.hash = null;
434
+ }
435
+
402
436
  n.type = `Glimmer${n.type}`;
403
437
  }
404
438
 
@@ -409,17 +443,6 @@ function processGlimmerTemplate({ templateContent, codeLines, templateRange, tok
409
443
  comment.type = 'Block';
410
444
  }
411
445
 
412
- // Nullify empty hashes
413
- for (const n of allNodes) {
414
- if (
415
- ['GlimmerMustacheStatement', 'GlimmerBlockStatement', 'GlimmerSubExpression'].includes(n.type)
416
- ) {
417
- if (n.hash && n.hash.pairs && n.hash.pairs.length === 0) {
418
- n.hash = null;
419
- }
420
- }
421
- }
422
-
423
446
  // Build final token stream
424
447
  ast.tokens = buildTokenStream(
425
448
  tokenize(tokenSource || templateContent, codeLines, offset),
@@ -442,23 +465,18 @@ function processGlimmerTemplate({ templateContent, codeLines, templateRange, tok
442
465
  */
443
466
  module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates(info, code) {
444
467
  const templateInfos = info.templateInfos.map((r) => ({
445
- range: [r.contentRange.startByte, r.contentRange.endByte],
446
- templateRange: [r.range.startByte, r.range.endByte],
447
- utf16Range: [byteToCharIndex(code, r.range.startByte), byteToCharIndex(code, r.range.endByte)],
468
+ utf16Range: [r.range.startUtf16Codepoint, r.range.endUtf16Codepoint],
448
469
  }));
449
470
  const codeLines = new DocumentLines(code);
450
471
  const allComments = [];
451
- const templateVisitorKeys = {};
452
472
 
453
473
  for (const tpl of templateInfos) {
454
- const range = tpl.utf16Range;
455
- const template = code.slice(...range);
474
+ const template = code.slice(...tpl.utf16Range);
456
475
 
457
476
  const { ast, comments } = processGlimmerTemplate({
458
477
  templateContent: template,
459
478
  codeLines,
460
479
  templateRange: [...tpl.utf16Range],
461
- tokenSource: sliceByteRange(code, ...tpl.templateRange),
462
480
  });
463
481
 
464
482
  ast.content = template;
@@ -466,12 +484,8 @@ module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates(
466
484
  tpl.ast = ast;
467
485
  }
468
486
 
469
- for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
470
- templateVisitorKeys[`Glimmer${k}`] = [...v];
471
- }
472
-
473
487
  return {
474
- templateVisitorKeys,
488
+ templateVisitorKeys: buildGlimmerVisitorKeys(),
475
489
  templateInfos,
476
490
  comments: allComments,
477
491
  };
@@ -499,6 +513,9 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi
499
513
  result.ast.tokens.splice(firstIdx, lastIdx - firstIdx + 1, ...ti.ast.tokens);
500
514
  }
501
515
 
516
+ // Build a Map keyed by range start for O(1) lookup during traversal
517
+ const templateInfoByStart = new Map(templateInfos.map((t) => [t.utf16Range[0], t]));
518
+
502
519
  // eslint-disable-next-line complexity
503
520
  traverse(visitorKeys, result.ast, (path) => {
504
521
  const node = path.node;
@@ -515,12 +532,11 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi
515
532
  range = [node.declaration.range[0], node.declaration.range[1]];
516
533
  }
517
534
 
518
- const template = templateInfos.find(
519
- (t) =>
520
- t.utf16Range[0] === range[0] &&
521
- (t.utf16Range[1] === range[1] || t.utf16Range[1] === range[1] + 1)
522
- );
523
- if (!template) {
535
+ const template = templateInfoByStart.get(range[0]);
536
+ if (
537
+ !template ||
538
+ (template.utf16Range[1] !== range[1] && template.utf16Range[1] !== range[1] + 1)
539
+ ) {
524
540
  return null;
525
541
  }
526
542
  counter++;
@@ -570,9 +586,9 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi
570
586
  const registerUndef =
571
587
  isUpperCase(n.name[0]) ||
572
588
  node.name.includes('.') ||
573
- (!htmlTags.includes(node.name) &&
574
- !svgTags.includes(node.name) &&
575
- !mathMLTags.includes(node.name));
589
+ (!htmlTagsSet.has(node.name) &&
590
+ !svgTagsSet.has(node.name) &&
591
+ !mathMLTagsSet.has(node.name));
576
592
 
577
593
  if (!ignore && (variable || registerUndef)) {
578
594
  registerNodeInScope(n, scope, variable);
@@ -622,7 +638,7 @@ module.exports.convertAst = function convertAst(result, preprocessedResult, visi
622
638
  };
623
639
 
624
640
  const replaceRange = function replaceRange(s, start, end, substitute) {
625
- return sliceByteRange(s, 0, start) + substitute + sliceByteRange(s, end);
641
+ return s.slice(0, start) + substitute + s.slice(end);
626
642
  };
627
643
  module.exports.replaceRange = replaceRange;
628
644
 
@@ -725,25 +741,25 @@ module.exports.transformForLint = function transformForLint(code, fileName) {
725
741
  for (const tplInfo of result.reverse()) {
726
742
  const content = tplInfo.contents.replace(/`/g, '\\`').replace(/\$/g, '\\$');
727
743
  if (tplInfo.type === 'class-member') {
728
- const tplLength = tplInfo.range.endByte - tplInfo.range.startByte;
729
- const spaces = tplLength - byteLength(content) - 'static{`'.length - '`}'.length;
744
+ const tplLength = tplInfo.range.endUtf16Codepoint - tplInfo.range.startUtf16Codepoint;
745
+ const spaces = tplLength - content.length - 'static{`'.length - '`}'.length;
730
746
  const total = content + ' '.repeat(spaces);
731
747
  const replacementCode = `static{\`${total}\`}`;
732
748
  jsCode = replaceRange(
733
749
  jsCode,
734
- tplInfo.range.startByte,
735
- tplInfo.range.endByte,
750
+ tplInfo.range.startUtf16Codepoint,
751
+ tplInfo.range.endUtf16Codepoint,
736
752
  replacementCode
737
753
  );
738
754
  } else {
739
- const tplLength = tplInfo.range.endByte - tplInfo.range.startByte;
740
- const spaces = tplLength - byteLength(content) - '`'.length - '`'.length;
755
+ const tplLength = tplInfo.range.endUtf16Codepoint - tplInfo.range.startUtf16Codepoint;
756
+ const spaces = tplLength - content.length - '`'.length - '`'.length;
741
757
  const total = content + ' '.repeat(spaces);
742
758
  const replacementCode = `\`${total}\``;
743
759
  jsCode = replaceRange(
744
760
  jsCode,
745
- tplInfo.range.startByte,
746
- tplInfo.range.endByte,
761
+ tplInfo.range.startUtf16Codepoint,
762
+ tplInfo.range.endUtf16Codepoint,
747
763
  replacementCode
748
764
  );
749
765
  }
@@ -80,12 +80,32 @@ try {
80
80
  let jsCode = code;
81
81
  const sourceFile = ts.createSourceFile('__x__.ts', code, ts.ScriptTarget.Latest);
82
82
  const length = jsCode.length;
83
- for (const b of sourceFile.statements) {
84
- if (b.kind === ts.SyntaxKind.ImportDeclaration && b.moduleSpecifier.text.endsWith('.gts')) {
85
- const value = b.moduleSpecifier.text.replace(/\.gts$/, '.mts');
86
- jsCode = replaceRange(jsCode, b.moduleSpecifier.pos + 2, b.moduleSpecifier.end - 1, value);
83
+ function visit(node) {
84
+ if (
85
+ node.kind === ts.SyntaxKind.ImportDeclaration &&
86
+ node.moduleSpecifier.text.endsWith('.gts')
87
+ ) {
88
+ const value = node.moduleSpecifier.text.replace(/\.gts$/, '.mts');
89
+ jsCode = replaceRange(
90
+ jsCode,
91
+ node.moduleSpecifier.pos + 2,
92
+ node.moduleSpecifier.end - 1,
93
+ value
94
+ );
95
+ }
96
+ if (
97
+ node.kind === ts.SyntaxKind.CallExpression &&
98
+ node.expression.kind === ts.SyntaxKind.ImportKeyword
99
+ ) {
100
+ const arg = node.arguments[0];
101
+ if (arg && arg.kind === ts.SyntaxKind.StringLiteral && arg.text.endsWith('.gts')) {
102
+ const value = arg.text.replace(/\.gts$/, '.mts');
103
+ jsCode = replaceRange(jsCode, arg.getStart(sourceFile) + 1, arg.end - 1, value); // +1/-1 to skip surrounding quotes
104
+ }
87
105
  }
106
+ ts.forEachChild(node, visit);
88
107
  }
108
+ visit(sourceFile);
89
109
  if (length !== jsCode.length) {
90
110
  throw new Error('bad replacement');
91
111
  }
@@ -28,12 +28,20 @@ class DocumentLines {
28
28
  */
29
29
  offsetToPosition(position) {
30
30
  const lineStarts = this.lineStarts;
31
- let line = 0;
32
- while (line + 1 < lineStarts.length && lineStarts[line + 1] <= position) {
33
- line++;
31
+ let lo = 0;
32
+ let hi = lineStarts.length - 1;
33
+ // Upper-biased midpoint is required here: we want the *rightmost* line whose
34
+ // start is ≤ position. With a lower-biased mid, when lo + 1 === hi and the
35
+ // condition is true, mid === lo and lo never advances, causing an infinite loop.
36
+ while (lo < hi) {
37
+ const mid = (lo + hi + 1) >> 1;
38
+ if (lineStarts[mid] <= position) {
39
+ lo = mid;
40
+ } else {
41
+ hi = mid - 1;
42
+ }
34
43
  }
35
- const column = position - lineStarts[line];
36
- return { line: line + 1, column };
44
+ return { line: lo + 1, column: position - lineStarts[lo] };
37
45
  }
38
46
  }
39
47
 
@@ -46,27 +54,20 @@ function computeLineStarts(text) {
46
54
  let pos = 0;
47
55
  let lineStart = 0;
48
56
  while (pos < text.length) {
49
- const ch = text.codePointAt(pos);
50
- pos++;
51
- switch (ch) {
52
- case 13 /* carriageReturn */: {
53
- if (text.codePointAt(pos) === 10 /* lineFeed */) {
54
- pos++;
55
- }
56
- }
57
- // falls through
58
- case 10 /* lineFeed */: {
59
- result.push(lineStart);
60
- lineStart = pos;
61
- break;
62
- }
63
- default: {
64
- if (ch > 127 /* maxAsciiCharacter */ && isLineBreak(ch)) {
65
- result.push(lineStart);
66
- lineStart = pos;
67
- }
68
- break;
57
+ const ch = text.charCodeAt(pos++);
58
+ if (ch === 13 /* carriageReturn */) {
59
+ if (text.charCodeAt(pos) === 10 /* lineFeed */) {
60
+ pos++;
69
61
  }
62
+ result.push(lineStart);
63
+ lineStart = pos;
64
+ } else if (
65
+ ch === 10 /* lineFeed */ ||
66
+ ch === 8232 /* lineSeparator */ ||
67
+ ch === 8233 /* paragraphSeparator */
68
+ ) {
69
+ result.push(lineStart);
70
+ lineStart = pos;
70
71
  }
71
72
  }
72
73
  result.push(lineStart);