@handlebars/parser 2.2.1 → 2.2.3

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/lib/helpers.js CHANGED
@@ -164,8 +164,18 @@ export function prepareBlock(
164
164
  );
165
165
  }
166
166
 
167
- if (inverseAndProgram.chain) {
168
- inverseAndProgram.program.body[0].closeStrip = close.strip;
167
+ // Only at the outermost block (where close is the real closing tag, not another
168
+ // chain link) propagate its strip flags through all chained else-if blocks.
169
+ if (inverseAndProgram.chain && !close.chain) {
170
+ const closeStrip = close.strip;
171
+ let innerBlock = inverseAndProgram.program.body[0];
172
+ // Walk the full chain so every block gets the real closing tag's strip flags.
173
+ while (innerBlock) {
174
+ innerBlock.closeStrip = closeStrip;
175
+ innerBlock = (innerBlock.inverse && innerBlock.inverse.chained)
176
+ ? innerBlock.inverse.body[0]
177
+ : null;
178
+ }
169
179
  }
170
180
 
171
181
  inverseStrip = inverseAndProgram.strip;
@@ -41,7 +41,9 @@ WhitespaceControl.prototype.Program = function (program) {
41
41
  // If we are on a standalone node, save the indent info for partials
42
42
  if (current.type === 'PartialStatement') {
43
43
  // Pull out the whitespace from the final line
44
- current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1];
44
+ current.indent = /(?:^|[^ \t])([ \t]+)$/.exec(
45
+ body[i - 1].original
46
+ )[1];
45
47
  }
46
48
  }
47
49
  }
@@ -55,7 +57,21 @@ WhitespaceControl.prototype.Program = function (program) {
55
57
  // Always strip the next node
56
58
  omitRight(body, i);
57
59
 
58
- omitLeft((current.inverse || current.program).body);
60
+ // For chained else-if blocks, walk the chain and strip trailing indent
61
+ // from every terminal body so all execution paths lose the close-tag indent.
62
+ let chainNode = current.inverse;
63
+ if (chainNode && chainNode.chained) {
64
+ while (chainNode && chainNode.chained) {
65
+ let lastBlock = chainNode.body[chainNode.body.length - 1];
66
+ omitLeft(lastBlock.program.body);
67
+ chainNode = lastBlock.inverse;
68
+ }
69
+ if (chainNode) {
70
+ omitLeft(chainNode.body);
71
+ }
72
+ } else {
73
+ omitLeft((current.inverse || current.program).body);
74
+ }
59
75
  }
60
76
  }
61
77
 
@@ -158,9 +174,13 @@ function isPrevWhitespace(body, i, isRoot) {
158
174
  }
159
175
 
160
176
  if (prev.type === 'ContentStatement') {
161
- return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(
162
- prev.original
163
- );
177
+ // Matches when the trailing whitespace contains a newline (or, for the
178
+ // root's first node, is the whole string). See omitLeft for the anchoring.
179
+ return (
180
+ sibling || !isRoot
181
+ ? /(?:^|\S)[^\S\n]*\n\s*$/
182
+ : /^\s*$|(?:^|\S)[^\S\n]*\n\s*$/
183
+ ).test(prev.original);
164
184
  }
165
185
  }
166
186
  function isNextWhitespace(body, i, isRoot) {
@@ -225,7 +245,13 @@ function omitLeft(body, i, multiple) {
225
245
 
226
246
  // We omit the last node if it's whitespace only and not preceded by a non-content node.
227
247
  let original = current.value;
228
- current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, '');
248
+ // The match must start after a non-whitespace character (or at the start):
249
+ // a plain /\s+$/ is retried at every position of a whitespace run, which
250
+ // takes quadratic time on long runs.
251
+ current.value = current.value.replace(
252
+ multiple ? /(^|\S)\s+$/ : /(^|[^ \t])[ \t]+$/,
253
+ '$1'
254
+ );
229
255
  current.leftStripped = current.value !== original;
230
256
  return current.leftStripped;
231
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handlebars/parser",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "The parser for the Handlebars language",
5
5
  "homepage": "https://github.com/handlebars-lang/handlebars-parser#readme",
6
6
  "bugs": {
@@ -46,7 +46,7 @@
46
46
  "mocha": "^10.7.3",
47
47
  "npm-run-all": "^4.1.5",
48
48
  "prettier": "^2.1.1",
49
- "release-plan": "^0.16.0",
49
+ "release-plan": "^0.18.0",
50
50
  "typescript": "^5.6.3"
51
51
  },
52
52
  "engines": {
@@ -57,13 +57,13 @@
57
57
  "registry": "https://registry.npmjs.org"
58
58
  },
59
59
  "scripts": {
60
- "lint": "eslint .",
61
60
  "build": "npm-run-all build:parser build:esm build:cjs",
62
- "build:cjs": "tsc --module nodenext --moduleResolution nodenext --target es5 --outDir dist/cjs",
61
+ "build:cjs": "tsc --module commonjs --moduleResolution node --target es5 --outDir dist/cjs",
63
62
  "build:esm": "tsc --module es2015 --target es5 --outDir dist/esm",
64
63
  "build:jison": "jison -m js src/handlebars.yy src/handlebars.l -o lib/parser.js",
65
64
  "build:parser": "npm-run-all build:jison build:parser-suffix",
66
65
  "build:parser-suffix": "combine-files lib/parser.js,src/parser-suffix.js lib/parser.js",
66
+ "lint": "eslint .",
67
67
  "pretest": "pnpm run build",
68
68
  "test": "mocha --inline-diffs spec",
69
69
  "pretest:bail": "pnpm run build",
package/spec/ast.js CHANGED
@@ -17,6 +17,17 @@ describe('ast', function () {
17
17
  equals(ast.body[0].value, '');
18
18
  equals(ast.body[1].program.body[0].value, 'foo');
19
19
  });
20
+
21
+ it('tilde on one else-if does not strip trailing whitespace in later branches', function () {
22
+ let ast = parse('{{#if a}}A{{else if b}}B {{~else if c}}C {{/if}}'),
23
+ block = ast.body[0],
24
+ bBlock = block.inverse.body[0],
25
+ cBlock = bBlock.inverse.body[0];
26
+
27
+ equals(block.program.body[0].value, 'A');
28
+ equals(bBlock.program.body[0].value, 'B'); // tilde strips trailing space from b
29
+ equals(cBlock.program.body[0].value, 'C '); // no tilde on {{/if}}, space preserved
30
+ });
20
31
  });
21
32
 
22
33
  describe('parseWithoutProcessing', function () {
@@ -36,6 +47,35 @@ describe('ast', function () {
36
47
  equals(ast.body[1].program.body[0].value, ' \nfoo\n ');
37
48
  });
38
49
  });
50
+
51
+ describe('performance', function () {
52
+ // Each template below takes several seconds to parse, and so exceeds the
53
+ // test timeout, if finding trailing whitespace is not linear in its length.
54
+ let spaces = ' '.repeat(100000),
55
+ newlines = '\n'.repeat(100000);
56
+
57
+ it('should strip whitespace in linear time', function () {
58
+ let ast = parse(spaces + 'x' + spaces + '{{~foo}}');
59
+
60
+ equals(ast.body[0].value, spaces + 'x');
61
+ });
62
+
63
+ it('should detect standalone lines in linear time', function () {
64
+ let ast = parse(newlines + 'x{{foo}}');
65
+ equals(ast.body[0].value, newlines + 'x');
66
+
67
+ ast = parse('{{foo}}' + newlines + 'x{{foo}}');
68
+ equals(ast.body[1].value, newlines + 'x');
69
+ });
70
+
71
+ it('should find partial indentation in linear time', function () {
72
+ let ast = parse(spaces + 'x\n {{> dude}}\n');
73
+
74
+ equals(ast.body[0].value, spaces + 'x\n');
75
+ equals(ast.body[1].indent, ' ');
76
+ equals(ast.body[2].value, '');
77
+ });
78
+ });
39
79
  });
40
80
 
41
81
  describe('node details', function () {
@@ -236,6 +276,20 @@ describe('ast', function () {
236
276
 
237
277
  equals(ast.body[2].value, '');
238
278
  });
279
+
280
+ it('strips close-tag indent from all chained else-if branches', function () {
281
+ let ast = parse(
282
+ ' {{#if a}}\n foo\n {{else if b}}\n bar\n {{else if c}}\n baz\n {{/if}}'
283
+ ),
284
+ block = ast.body[1],
285
+ bBlock = block.inverse.body[0],
286
+ cBlock = bBlock.inverse.body[0];
287
+
288
+ equals(ast.body[0].value, '');
289
+ equals(block.program.body[0].value, ' foo\n');
290
+ equals(bBlock.program.body[0].value, ' bar\n');
291
+ equals(cBlock.program.body[0].value, ' baz\n'); // indent before {{/if}} stripped
292
+ });
239
293
  });
240
294
  describe('partials - parseWithoutProcessing', function () {
241
295
  it('simple partial', function () {
package/tsconfig.json CHANGED
@@ -28,4 +28,4 @@
28
28
  "node_modules",
29
29
  ".vscode"
30
30
  ]
31
- }
31
+ }