@falsefalse/prettier-plugin-handlebars 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Poliklot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @falsefalse/prettier-plugin-handlebars
2
+
3
+ A Prettier plugin for classic Handlebars templates with mixed HTML markup.
4
+
5
+ This started as a fork of another plugin — see [Prior work](#prior-work). It is **opinionated**:
6
+ it exposes no options of its own, only Prettier's core `printWidth`, `tabWidth`, `useTabs` and
7
+ `singleQuote`. Everything else is a decision the formatter has already made.
8
+
9
+ ## The rule everything follows
10
+
11
+ > Whitespace that renders belongs to the author. Whitespace that does not belongs to the
12
+ > formatter.
13
+
14
+ Between two siblings, whitespace reaches the page, so it is reproduced exactly — a space stays a
15
+ space, a newline stays a newline, a run of blank lines collapses to one. The formatter never
16
+ invents a gap the author did not write, and never drops one they did.
17
+
18
+ Inside a tag or a mustache, whitespace never reaches the page, so it is the formatter's: it is
19
+ normalised, and driven by width, all-or-nothing.
20
+
21
+ Two consequences worth stating plainly:
22
+
23
+ - **A one-liner stays a one-liner.** `{{#if a}}x{{else}}y{{/if}}` is one line because the author
24
+ wrote it as one line, not because it happens to fit.
25
+ - **Anything the author broke stays broken.** Reflowing it would move rendered whitespace.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install --save-dev prettier github:falsefalse/prettier-plugin-handlebars
31
+ ```
32
+
33
+ ```js
34
+ /** @type {import("prettier").Config} */
35
+ module.exports = {
36
+ plugins: ['@falsefalse/prettier-plugin-handlebars'],
37
+ overrides: [{ files: ['*.hbs', '*.handlebars'], options: { parser: 'handlebars' } }],
38
+ };
39
+ ```
40
+
41
+ The `overrides` entry is what makes Prettier pick this parser; without it `.hbs` files are either
42
+ skipped or handed to the HTML parser. See [docs/EDITOR_SETUP.md](./docs/EDITOR_SETUP.md).
43
+
44
+ ## What it formats
45
+
46
+ - HTML elements, void elements, custom elements, comments, `pre` / `textarea` / `script` / `style`
47
+ - `{{mustache}}`, `{{{triple}}}`, `{{! comments }}`, `{{!-- block comments --}}`
48
+ - block helpers, `{{else}}`, `{{else if …}}`, inverted `{{^…}}`
49
+ - partials `{{> name}}`, dynamic partials `{{> (lookup . "n")}}`, block partials `{{#> layout}}`
50
+ - inline partials `{{#*inline "name"}}`, decorators `{{*log}}`
51
+ - Mustache inheritance — `{{< layout}}`, `{{$block}}`
52
+ - raw blocks `{{{{raw}}}}…{{{{/raw}}}}`
53
+ - whitespace control markers, `{{~v~}}`, `{{~#if a~}}`
54
+ - Handlebars inside attribute values, and blocks in attribute position
55
+ - hash params written `k=v`, `k= v` or `k = v`, printed consistently
56
+ - subexpressions to any depth, broken by width all the way down
57
+ - `prettier-ignore`, `prettier-ignore-start` / `-end`
58
+
59
+ ## Unclosed input is rejected
60
+
61
+ Everything that opens must close. There is no recovery: a formatter that guesses at a missing
62
+ `}}` prints markup the author did not write, and one that passes a mismatched tag through leaves
63
+ the rest of the file unformatted with nothing to show for it.
64
+
65
+ ```
66
+ $ prettier --write page.hbs
67
+ [error] page.hbs: SyntaxError: unclosed tag: expected </span> (3:3)
68
+ [error] 1 | <div>
69
+ [error] 2 | <p>x</p>
70
+ [error] > 3 | <span>y
71
+ [error] | ^^^^^^
72
+ [error] 4 | </div>
73
+ ```
74
+
75
+ The error carries a source range, so editors can put the cursor on it.
76
+
77
+ This includes the HTML spec's optional end tags: `<ul><li>a<li>b</ul>` is rejected. One rule with
78
+ no list of exceptions beats a list of exceptions that has to be kept in step with the spec.
79
+
80
+ What is checked is structure: delimiters balance, tags nest, a block matches its own closer —
81
+ not Handlebars' expression grammar. `{{}}`, `{{{x}}}}` and `{{foo xa"y}}` are all
82
+ delimiter-balanced, so they pass through unchanged for Handlebars itself to reject at compile
83
+ time. The formatter does not make them worse, and it is not a second implementation of the
84
+ language.
85
+
86
+ ### When the markup only balances at render time
87
+
88
+ Two escape hatches, in order of preference.
89
+
90
+ Hide the markup behind a call, so the parser sees balanced source and the browser still gets what
91
+ you meant:
92
+
93
+ ```hbs
94
+ {{#if twoColumns}}{{{concat '<div class="row">'}}}{{/if}}
95
+
96
+ {{#if twoColumns}}{{{concat '</div>'}}}{{/if}}
97
+ ```
98
+
99
+ Or fence the region off entirely. Nothing inside is parsed, so nothing inside can be rejected:
100
+
101
+ ```hbs
102
+ {{! prettier-ignore-start }}
103
+ <div>deliberately unbalanced
104
+ {{! prettier-ignore-end }}
105
+ ```
106
+
107
+ ## Options
108
+
109
+ None. `printWidth`, `tabWidth`, `useTabs` and `singleQuote` are read from Prettier's core config;
110
+ this plugin adds nothing.
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ npm ci
116
+ npm run check # build + tests + both fuzz gates
117
+ ```
118
+
119
+ `npm run check` runs the test suite plus two fuzz gates, each two-sided: every generated case
120
+ must format idempotently, without losing source, and **without changing what the template
121
+ renders** — compiled with the real Handlebars runtime, not approximated. Every malformed case
122
+ must be refused with a location.
123
+
124
+ `scripts/run-property-gate.mjs` checks the same properties over a real corpus:
125
+
126
+ ```bash
127
+ npm run corpus:gate -- --git ../your-repo --width 95 path/to/templates
128
+ ```
129
+
130
+ The last gate is a person reading the diff the formatter would produce. The property gates prove
131
+ correctness; they cannot see bad taste, which is the failure mode that actually matters here.
132
+
133
+ [docs/REWRITE-PLAN.md](./docs/REWRITE-PLAN.md) is the design record — why the printer looks like
134
+ this, and what the previous one got wrong.
135
+
136
+ ## Prior work
137
+
138
+ This plugin began as a fork of
139
+ [Poliklot/prettier-plugin-handlebars](https://github.com/Poliklot/prettier-plugin-handlebars),
140
+ published as `@poliklot/prettier-plugin-handlebars` and MIT-licensed, © Poliklot. The parser and
141
+ printer have since been rewritten — [docs/REWRITE-PLAN.md](./docs/REWRITE-PLAN.md) is that record
142
+ — and the package now ships under its own name, but the shape of the project, a Handlebars-aware
143
+ `.hbs` formatter with no options of its own, starts there.
144
+
145
+ ## Docs
146
+
147
+ - [Editor setup](./docs/EDITOR_SETUP.md)
148
+ - [Troubleshooting](./docs/TROUBLESHOOTING.md)
149
+ - [Printer rewrite plan](./docs/REWRITE-PLAN.md) — the design record
@@ -0,0 +1,54 @@
1
+ import type { TemplateBlockPrefix, TemplateToken } from 'template-format-core';
2
+ export interface HandlebarsToken extends TemplateToken {
3
+ /**
4
+ * Whether the tokenizer found a closing delimiter, rather than running to the end of the
5
+ * input. Recorded by the one place that knows: re-deriving it by string-matching the token's
6
+ * tail reports true for any unterminated token running to a text end that already ends in
7
+ * `}}`, letting `{{foo "bar}}` past the malformed guard to print as `{{foo "bar}}}}`.
8
+ */
9
+ terminated: boolean;
10
+ }
11
+ export declare const handlebarsDialect: {
12
+ openDelimiter: string;
13
+ parseToken: typeof parseHandlebarsToken;
14
+ findNextOpen: typeof findNextHandlebarsOpen;
15
+ isEscapedOpen: typeof isEscapedHandlebarsOpen;
16
+ isDynamicElementStart: typeof isDynamicHandlebarsElementStart;
17
+ consumeRawBlock: typeof consumeHandlebarsRawBlock;
18
+ getBlockExpression: typeof getHandlebarsBlockExpression;
19
+ getBlockPrefix: typeof getHandlebarsBlockPrefix;
20
+ getPrintedBlockPrefix: typeof getPrintedHandlebarsBlockPrefix;
21
+ getElseKeyword: typeof getHandlebarsElseKeyword;
22
+ getBlockClosePrefix: typeof getHandlebarsBlockClosePrefix;
23
+ shouldPreserveTokenVerbatim: typeof shouldPreserveHandlebarsTokenVerbatim;
24
+ };
25
+ declare function parseHandlebarsToken(text: string, position: number): HandlebarsToken;
26
+ /**
27
+ * Whether a comment at `position` is written in block form. `{{~!-- x --~}}` is one as much as
28
+ * `{{!-- x --}}`, so the `~` is skipped: anchoring on a literal `{{!--` reads the
29
+ * whitespace-control form as a line comment and demotes it to `{{! !-- x -- }}`.
30
+ */
31
+ export declare function isHandlebarsBlockComment(text: string, position: number): boolean;
32
+ declare function isEscapedHandlebarsOpen(text: string, position: number): boolean;
33
+ declare function findNextHandlebarsOpen(text: string, position: number): number;
34
+ declare function isDynamicHandlebarsElementStart(text: string, position: number): boolean;
35
+ /**
36
+ * The name a raw block opens with, or `''` if `position` is not one. Tildes are tolerated on the
37
+ * open because the lexer takes them there; the closer below is stricter for the same reason.
38
+ */
39
+ export declare function handlebarsRawBlockName(text: string, position: number, openEnd: number): string;
40
+ /**
41
+ * The only closer Handlebars accepts: no whitespace inside it and no tilde on either side -
42
+ * `{{{{~/raw}}}}` and `{{{{ / raw }}}}` are both lexical errors. One literal, so there is nothing
43
+ * to escape and nothing to drift - the two hand-written patterns this replaces disagreed about
44
+ * exactly that whitespace.
45
+ */
46
+ export declare function handlebarsRawBlockCloser(name: string): string;
47
+ declare function consumeHandlebarsRawBlock(text: string, position: number): number | null;
48
+ declare function getHandlebarsBlockExpression(token: TemplateToken): string;
49
+ declare function getHandlebarsBlockPrefix(token: TemplateToken): TemplateBlockPrefix;
50
+ declare function getPrintedHandlebarsBlockPrefix(prefix: TemplateBlockPrefix): string;
51
+ declare function getHandlebarsElseKeyword(): string;
52
+ declare function getHandlebarsBlockClosePrefix(path: string): string;
53
+ declare function shouldPreserveHandlebarsTokenVerbatim(token: TemplateToken): boolean;
54
+ export {};
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.handlebarsDialect = void 0;
37
+ exports.isHandlebarsBlockComment = isHandlebarsBlockComment;
38
+ exports.handlebarsRawBlockName = handlebarsRawBlockName;
39
+ exports.handlebarsRawBlockCloser = handlebarsRawBlockCloser;
40
+ const template_format_core_1 = require("template-format-core");
41
+ const scan_1 = require("../../scan");
42
+ const whitespace = __importStar(require("../../whitespace"));
43
+ /* Deliberately not typed `: TemplateDialect`. That interface demands nine more members than the
44
+ * parser and printer ever ask for, each a second copy of Handlebars syntax to keep in step by
45
+ * hand - `getLineCommentTag` and `printComment` disagree about the same thing. */
46
+ exports.handlebarsDialect = {
47
+ openDelimiter: '{{',
48
+ parseToken: parseHandlebarsToken,
49
+ findNextOpen: findNextHandlebarsOpen,
50
+ isEscapedOpen: isEscapedHandlebarsOpen,
51
+ isDynamicElementStart: isDynamicHandlebarsElementStart,
52
+ consumeRawBlock: consumeHandlebarsRawBlock,
53
+ getBlockExpression: getHandlebarsBlockExpression,
54
+ getBlockPrefix: getHandlebarsBlockPrefix,
55
+ getPrintedBlockPrefix: getPrintedHandlebarsBlockPrefix,
56
+ getElseKeyword: getHandlebarsElseKeyword,
57
+ getBlockClosePrefix: getHandlebarsBlockClosePrefix,
58
+ shouldPreserveTokenVerbatim: shouldPreserveHandlebarsTokenVerbatim,
59
+ };
60
+ /**
61
+ * The path a block opens on, which is where its name ends. A plain whitespace split cut
62
+ * `{{#[my block]}}` down to `[my`, and that never matched the `[my block]` the block's own
63
+ * `{{/[my block]}}` reports - a Handlebars path segment may hold spaces inside `[...]`.
64
+ */
65
+ function readPathName(inner) {
66
+ const text = inner.trim();
67
+ let brackets = false;
68
+ for (let pos = 0; pos < text.length; pos += 1) {
69
+ const char = text[pos];
70
+ if (char === '[') {
71
+ brackets = true;
72
+ }
73
+ else if (char === ']') {
74
+ brackets = false;
75
+ }
76
+ else if (!brackets && whitespace.handlebars.test(char)) {
77
+ return text.slice(0, pos);
78
+ }
79
+ }
80
+ return text;
81
+ }
82
+ function parseHandlebarsToken(text, position) {
83
+ const triple = text.startsWith('{{{', position);
84
+ const openLength = triple ? 3 : 2;
85
+ const close = triple ? '}}}' : '}}';
86
+ const isBlockComment = isHandlebarsBlockComment(text, position);
87
+ const blockClose = isBlockComment ? findHandlebarsBlockCommentClose(text, position + openLength, close) : null;
88
+ /* A comment body is text, not an expression: Handlebars ends a line comment at the first close
89
+ * delimiter, full stop. The quote-aware scanner lets an unbalanced quote run the token past
90
+ * its real `}}`, so `{{! "q }}\n{{#if a}}y{{/if}}` swallows the block and renders nothing -
91
+ * stable across passes, and invisible to every gate. */
92
+ const isLineComment = !isBlockComment && /^~?!/u.test(text.slice(position + openLength, position + openLength + 2));
93
+ const closeIdx = isBlockComment
94
+ ? blockClose?.index ?? -1
95
+ : isLineComment
96
+ ? text.indexOf(close, position + openLength)
97
+ : findHandlebarsClose(text, position + openLength, close);
98
+ const end = isBlockComment
99
+ ? blockClose?.end ?? text.length
100
+ : closeIdx >= 0
101
+ ? closeIdx + close.length
102
+ : text.length;
103
+ const rawContent = text.slice(position + openLength, closeIdx >= 0 ? closeIdx : undefined);
104
+ const rawInner = rawContent.trim();
105
+ const trimOpen = rawInner.startsWith('~');
106
+ /* A block comment's closing `~` sits after the `--`, so it is outside `rawContent`. */
107
+ const trimClose = blockClose ? blockClose.trimClose : rawInner.endsWith('~');
108
+ const inner = rawInner.replace(/^~/, '').replace(/~$/, '').trim();
109
+ const baseToken = {
110
+ rawContent,
111
+ rawInner,
112
+ start: position,
113
+ end,
114
+ triple,
115
+ trimOpen,
116
+ trimClose,
117
+ terminated: isBlockComment ? blockClose !== null : closeIdx >= 0,
118
+ };
119
+ if (inner.startsWith('!')) {
120
+ return { kind: 'comment', content: inner, name: undefined, ...baseToken };
121
+ }
122
+ if (inner.startsWith('>')) {
123
+ return { kind: 'partial', content: inner.slice(1).trim(), name: undefined, ...baseToken };
124
+ }
125
+ if (inner.startsWith('<')) {
126
+ const name = readPathName(inner.slice(1));
127
+ return { kind: 'blockStart', content: inner, name, specialForm: 'parent', ...baseToken };
128
+ }
129
+ if (inner.startsWith('#>')) {
130
+ const name = readPathName(inner.slice(2));
131
+ return { kind: 'blockStart', content: inner, name, specialForm: 'blockPartial', ...baseToken };
132
+ }
133
+ if (inner.startsWith('#*')) {
134
+ const name = readPathName(inner.slice(2));
135
+ return { kind: 'blockStart', content: inner, name, specialForm: 'decoratorBlock', ...baseToken };
136
+ }
137
+ if (inner.startsWith('*')) {
138
+ return { kind: 'mustache', content: inner, name: undefined, specialForm: 'decorator', ...baseToken };
139
+ }
140
+ if (inner.startsWith('#')) {
141
+ const name = readPathName(inner.slice(1));
142
+ return { kind: 'blockStart', content: inner, name, ...baseToken };
143
+ }
144
+ if (inner.startsWith('^')) {
145
+ const name = readPathName(inner.slice(1));
146
+ /* Bare `{{^}}` is the shorthand for `{{else}}`; only `{{^name}}` opens an inverted block. */
147
+ if (!name) {
148
+ return { kind: 'else', content: inner, name: 'else', ...baseToken };
149
+ }
150
+ return { kind: 'blockStart', content: inner, name, specialForm: 'inverseBlock', ...baseToken };
151
+ }
152
+ if (inner.startsWith('$')) {
153
+ const name = readPathName(inner.slice(1));
154
+ return { kind: 'blockStart', content: inner, name, specialForm: 'mustacheBlock', ...baseToken };
155
+ }
156
+ if (inner.startsWith('/')) {
157
+ const name = inner.slice(1).trim();
158
+ return { kind: 'blockEnd', content: inner, name, ...baseToken };
159
+ }
160
+ if (inner === 'else' || inner.startsWith('else ')) {
161
+ return {
162
+ kind: 'else',
163
+ content: inner,
164
+ name: 'else',
165
+ specialForm: inner === 'else' ? undefined : 'elseIf',
166
+ ...baseToken,
167
+ };
168
+ }
169
+ return { kind: 'mustache', content: inner, name: undefined, ...baseToken };
170
+ }
171
+ /**
172
+ * Whether a comment at `position` is written in block form. `{{~!-- x --~}}` is one as much as
173
+ * `{{!-- x --}}`, so the `~` is skipped: anchoring on a literal `{{!--` reads the
174
+ * whitespace-control form as a line comment and demotes it to `{{! !-- x -- }}`.
175
+ */
176
+ function isHandlebarsBlockComment(text, position) {
177
+ const openLength = text.startsWith('{{{', position) ? 3 : 2;
178
+ return /^~?!--/u.test(text.slice(position + openLength, position + openLength + 4));
179
+ }
180
+ /** The first `--}}` or `--~}}`, whichever comes first. */
181
+ function findHandlebarsBlockCommentClose(text, position, close) {
182
+ const plain = text.indexOf(`--${close}`, position);
183
+ const trimmed = text.indexOf(`--~${close}`, position);
184
+ if (trimmed >= 0 && (plain < 0 || trimmed < plain)) {
185
+ return { index: trimmed, end: trimmed + close.length + 3, trimClose: true };
186
+ }
187
+ return plain < 0 ? null : { index: plain, end: plain + close.length + 2, trimClose: false };
188
+ }
189
+ function findHandlebarsClose(text, position, closeDelimiter) {
190
+ return (0, scan_1.scanPastQuotes)(text, position, {
191
+ stopsAt: (index) => text.startsWith(closeDelimiter, index),
192
+ opensQuote: (index) => (0, template_format_core_1.isTemplateExpressionQuoteStart)(text, index, position),
193
+ });
194
+ }
195
+ function isEscapedHandlebarsOpen(text, position) {
196
+ if (!text.startsWith('{{', position)) {
197
+ return false;
198
+ }
199
+ let slashCount = 0;
200
+ for (let index = position - 1; index >= 0 && text[index] === '\\'; index -= 1) {
201
+ slashCount += 1;
202
+ }
203
+ return slashCount % 2 === 1;
204
+ }
205
+ function findNextHandlebarsOpen(text, position) {
206
+ let searchPos = position;
207
+ while (searchPos < text.length) {
208
+ const candidate = text.indexOf('{{', searchPos);
209
+ if (candidate === -1) {
210
+ return -1;
211
+ }
212
+ if (!isEscapedHandlebarsOpen(text, candidate)) {
213
+ return candidate;
214
+ }
215
+ searchPos = candidate + 2;
216
+ }
217
+ return -1;
218
+ }
219
+ function isDynamicHandlebarsElementStart(text, position) {
220
+ return text.startsWith('<{{', position) || text.startsWith('</{{', position);
221
+ }
222
+ /**
223
+ * The name a raw block opens with, or `''` if `position` is not one. Tildes are tolerated on the
224
+ * open because the lexer takes them there; the closer below is stricter for the same reason.
225
+ */
226
+ function handlebarsRawBlockName(text, position, openEnd) {
227
+ const inner = text.slice(position + 4, openEnd).trim().replace(/^~/u, '').replace(/~$/u, '').trim();
228
+ return inner.startsWith('/') ? '' : (inner.split(whitespace.handlebarsRun)[0] ?? '');
229
+ }
230
+ /**
231
+ * The only closer Handlebars accepts: no whitespace inside it and no tilde on either side -
232
+ * `{{{{~/raw}}}}` and `{{{{ / raw }}}}` are both lexical errors. One literal, so there is nothing
233
+ * to escape and nothing to drift - the two hand-written patterns this replaces disagreed about
234
+ * exactly that whitespace.
235
+ */
236
+ function handlebarsRawBlockCloser(name) {
237
+ return `{{{{/${name}}}}}`;
238
+ }
239
+ function consumeHandlebarsRawBlock(text, position) {
240
+ if (!text.startsWith('{{{{', position)) {
241
+ return null;
242
+ }
243
+ const openIdx = text.indexOf('}}}}', position + 4);
244
+ if (openIdx === -1) {
245
+ return text.length;
246
+ }
247
+ const name = handlebarsRawBlockName(text, position, openIdx);
248
+ if (!name) {
249
+ return null;
250
+ }
251
+ const closer = handlebarsRawBlockCloser(name);
252
+ const closeIdx = text.indexOf(closer, openIdx + 4);
253
+ if (closeIdx === -1) {
254
+ return text.length;
255
+ }
256
+ return closeIdx + closer.length;
257
+ }
258
+ function getHandlebarsBlockExpression(token) {
259
+ if (token.specialForm === 'blockPartial' || token.specialForm === 'decoratorBlock') {
260
+ return token.content.slice(2).trim();
261
+ }
262
+ return token.content.slice(1).trim();
263
+ }
264
+ function getHandlebarsBlockPrefix(token) {
265
+ if (token.specialForm === 'blockPartial') {
266
+ return '#>';
267
+ }
268
+ if (token.specialForm === 'decoratorBlock') {
269
+ return '#*';
270
+ }
271
+ if (token.specialForm === 'inverseBlock') {
272
+ return '^';
273
+ }
274
+ if (token.specialForm === 'parent') {
275
+ return '<';
276
+ }
277
+ if (token.specialForm === 'mustacheBlock') {
278
+ return '$';
279
+ }
280
+ return '#';
281
+ }
282
+ function getPrintedHandlebarsBlockPrefix(prefix) {
283
+ if (prefix === '#>' || prefix === '<') {
284
+ return `${prefix} `;
285
+ }
286
+ return prefix;
287
+ }
288
+ function getHandlebarsElseKeyword() {
289
+ return 'else';
290
+ }
291
+ function getHandlebarsBlockClosePrefix(path) {
292
+ return `/${path}`;
293
+ }
294
+ function shouldPreserveHandlebarsTokenVerbatim(token) {
295
+ return token.specialForm === 'elseIf';
296
+ }
@@ -0,0 +1,24 @@
1
+ interface Position {
2
+ line: number;
3
+ column: number;
4
+ }
5
+ /**
6
+ * A construct the template opened and never closed, or closed with the wrong thing.
7
+ *
8
+ * The formatter refuses rather than guessing: guessing at a missing `}}` means printing markup
9
+ * the author did not write, and quietly passing a mismatched tag through means the rest of the
10
+ * file goes unformatted with nothing to show for it.
11
+ */
12
+ export declare class TemplateSyntaxError extends SyntaxError {
13
+ readonly start: number;
14
+ readonly end: number;
15
+ /** Prettier renders a code frame from this, and editors put the cursor on it. */
16
+ loc?: {
17
+ start: Position;
18
+ end: Position;
19
+ };
20
+ constructor(message: string, start: number, end: number);
21
+ /** Offsets are all the parser knows; line and column need the whole text. */
22
+ locate(text: string): this;
23
+ }
24
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TemplateSyntaxError = void 0;
4
+ function positionAt(text, offset) {
5
+ const upTo = text.slice(0, Math.max(0, Math.min(offset, text.length)));
6
+ return { line: upTo.split('\n').length, column: upTo.length - (upTo.lastIndexOf('\n') + 1) + 1 };
7
+ }
8
+ /**
9
+ * A construct the template opened and never closed, or closed with the wrong thing.
10
+ *
11
+ * The formatter refuses rather than guessing: guessing at a missing `}}` means printing markup
12
+ * the author did not write, and quietly passing a mismatched tag through means the rest of the
13
+ * file goes unformatted with nothing to show for it.
14
+ */
15
+ class TemplateSyntaxError extends SyntaxError {
16
+ constructor(message, start, end) {
17
+ super(message);
18
+ this.start = start;
19
+ this.end = end;
20
+ /* Editor integrations grep stderr for `: SyntaxError: <message> (line:col)` to place the
21
+ * cursor -- JsPrettier for Sublime Text does, and a subclass name misses that pattern. */
22
+ this.name = 'SyntaxError';
23
+ }
24
+ /** Offsets are all the parser knows; line and column need the whole text. */
25
+ locate(text) {
26
+ const start = positionAt(text, this.start);
27
+ this.loc = { start, end: positionAt(text, this.end) };
28
+ this.message = `${this.message} (${start.line}:${start.column})`;
29
+ return this;
30
+ }
31
+ }
32
+ exports.TemplateSyntaxError = TemplateSyntaxError;
@@ -0,0 +1,8 @@
1
+ import type { Call } from './types';
2
+ /**
3
+ * Parses one call's source into structured parts whose ranges are absolute in the template.
4
+ *
5
+ * `offset` is where `source` begins in the template, so a subexpression buried in a hash value
6
+ * can still be located exactly.
7
+ */
8
+ export declare function parseCall(source: string, offset?: number): Call;