@falsefalse/prettier-plugin-handlebars 0.0.2 → 0.0.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/LICENSE +2 -1
- package/README.md +19 -15
- package/dist/{errors.d.ts → core/errors.d.ts} +7 -0
- package/dist/{errors.js → core/errors.js} +10 -0
- package/dist/core/html.d.ts +2 -0
- package/dist/core/html.js +32 -0
- package/dist/core/source.d.ts +11 -0
- package/dist/core/source.js +27 -0
- package/dist/{whitespace.d.ts → core/whitespace.d.ts} +5 -0
- package/dist/{whitespace.js → core/whitespace.js} +18 -0
- package/dist/dialects/handlebars/tokens.d.ts +23 -6
- package/dist/dialects/handlebars/tokens.js +15 -8
- package/dist/expression.js +8 -8
- package/dist/parse/lex.d.ts +86 -0
- package/dist/parse/lex.js +274 -0
- package/dist/parse/lookahead.d.ts +35 -0
- package/dist/parse/lookahead.js +305 -0
- package/dist/parse/nodes.d.ts +38 -0
- package/dist/parse/nodes.js +161 -0
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js +271 -931
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +3 -2
- package/dist/printer.d.ts +1 -6
- package/dist/printer.js +72 -35
- package/dist/types.d.ts +2 -2
- package/docs/EDITOR_SETUP.md +0 -1
- package/package.json +3 -6
- /package/dist/{scan.d.ts → core/scan.d.ts} +0 -0
- /package/dist/{scan.js → core/scan.js} +0 -0
package/dist/plugin.d.ts
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.defaultOptions = exports.options = exports.printers = exports.parsers = exports.languages = void 0;
|
|
4
|
+
const source_1 = require("./core/source");
|
|
4
5
|
const parser_1 = require("./parser");
|
|
5
6
|
const printer_1 = require("./printer");
|
|
6
7
|
exports.languages = [
|
|
@@ -17,8 +18,8 @@ exports.parsers = {
|
|
|
17
18
|
'handlebars': {
|
|
18
19
|
parse: parser_1.parse,
|
|
19
20
|
astFormat: 'handlebars-ast',
|
|
20
|
-
locStart:
|
|
21
|
-
locEnd:
|
|
21
|
+
locStart: source_1.locStart,
|
|
22
|
+
locEnd: source_1.locEnd,
|
|
22
23
|
},
|
|
23
24
|
};
|
|
24
25
|
exports.printers = {
|
package/dist/printer.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Printer } from 'prettier';
|
|
2
2
|
import type { Node } from './types';
|
|
3
|
-
/** Only prettier's core options reach the printer; this formatter is opinionated. */
|
|
4
|
-
export type PrintOptions = Pick<ParserOptions<Node>, 'singleQuote'> & {
|
|
5
|
-
/** Quote holding the value being printed into. Unusable by anything nested in it. */
|
|
6
|
-
enclosingQuote?: '"' | "'";
|
|
7
|
-
};
|
|
8
3
|
export declare const printer: Printer<Node>;
|
package/dist/printer.js
CHANGED
|
@@ -35,9 +35,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.printer = void 0;
|
|
37
37
|
const doc_1 = require("prettier/doc");
|
|
38
|
-
const
|
|
38
|
+
const html_1 = require("./core/html");
|
|
39
39
|
const tokens_1 = require("./dialects/handlebars/tokens");
|
|
40
|
-
const whitespace = __importStar(require("./whitespace"));
|
|
40
|
+
const whitespace = __importStar(require("./core/whitespace"));
|
|
41
41
|
const { dedent, fill, group, hardline, ifBreak, indent, join, line, literalline, softline } = doc_1.builders;
|
|
42
42
|
const { removeLines } = doc_1.utils;
|
|
43
43
|
/* A run of blank lines collapses to one, which is two hardlines. */
|
|
@@ -192,13 +192,15 @@ function contentSpan(pieces) {
|
|
|
192
192
|
end -= 1;
|
|
193
193
|
return [start, end];
|
|
194
194
|
}
|
|
195
|
-
function printExpression(expression, breakable) {
|
|
196
|
-
return expression.type === 'SubExpression'
|
|
195
|
+
function printExpression(expression, breakable, options) {
|
|
196
|
+
return expression.type === 'SubExpression'
|
|
197
|
+
? printCall(expression, '(', ')', options, breakable)
|
|
198
|
+
: printSource(expression.source, options);
|
|
197
199
|
}
|
|
198
|
-
function printCallParts(call, breakable) {
|
|
199
|
-
const parts = call.params.map((param) => printExpression(param, breakable));
|
|
200
|
+
function printCallParts(call, breakable, options) {
|
|
201
|
+
const parts = call.params.map((param) => printExpression(param, breakable, options));
|
|
200
202
|
for (const pair of call.hash) {
|
|
201
|
-
parts.push([pair.key, '=', printExpression(pair.value, breakable)]);
|
|
203
|
+
parts.push([pair.key, '=', printExpression(pair.value, breakable, options)]);
|
|
202
204
|
}
|
|
203
205
|
if (call.blockParams && call.blockParams.length > 0) {
|
|
204
206
|
parts.push(['as |', call.blockParams.join(' '), '|']);
|
|
@@ -206,22 +208,22 @@ function printCallParts(call, breakable) {
|
|
|
206
208
|
return parts;
|
|
207
209
|
}
|
|
208
210
|
/** Whitespace inside a mustache does not render, so it is the formatter's: all-or-nothing. */
|
|
209
|
-
function printCall(call, open, close, breakable = true) {
|
|
210
|
-
const parts = printCallParts(call, breakable);
|
|
211
|
+
function printCall(call, open, close, options, breakable = true) {
|
|
212
|
+
const parts = printCallParts(call, breakable, options);
|
|
211
213
|
if (parts.length === 0) {
|
|
212
|
-
return [open, printExpression(call.path, breakable), close];
|
|
214
|
+
return [open, printExpression(call.path, breakable, options), close];
|
|
213
215
|
}
|
|
214
216
|
if (!breakable) {
|
|
215
|
-
return [open, printExpression(call.path, false), ' ', join(' ', parts), close];
|
|
217
|
+
return [open, printExpression(call.path, false, options), ' ', join(' ', parts), close];
|
|
216
218
|
}
|
|
217
219
|
/* One group, so a call that does not fit breaks every one of its parts. */
|
|
218
|
-
return group([open, indent([printExpression(call.path, true), line, join(line, parts)]), softline, close]);
|
|
220
|
+
return group([open, indent([printExpression(call.path, true, options), line, join(line, parts)]), softline, close]);
|
|
219
221
|
}
|
|
220
222
|
/* The three inline statements are one call in different delimiters: a mustache in `{{}}` (or
|
|
221
223
|
* `{{{}}}` when unescaped), a partial in `{{> }}`, a decorator in `{{*}}`. */
|
|
222
|
-
function printStatement(node, prefix) {
|
|
224
|
+
function printStatement(node, prefix, options) {
|
|
223
225
|
const triple = node.type === 'MustacheStatement' && node.triple;
|
|
224
|
-
return printCall(node, [triple ? '{{{' : '{{', trim(node.trimOpen), prefix], [trim(node.trimClose), triple ? '}}}' : '}}']);
|
|
226
|
+
return printCall(node, [triple ? '{{{' : '{{', trim(node.trimOpen), prefix], [trim(node.trimClose), triple ? '}}}' : '}}'], options);
|
|
225
227
|
}
|
|
226
228
|
function printComment(node) {
|
|
227
229
|
/* `block` already covers a multiline body - the parser sets it for either - so re-testing
|
|
@@ -240,7 +242,7 @@ function printComment(node) {
|
|
|
240
242
|
* the surrounding structure instead of staying frozen at the column it was written at.
|
|
241
243
|
* Common indentation is stripped and re-applied, which keeps the body's *relative* shape. */
|
|
242
244
|
if (/^\n/u.test(body)) {
|
|
243
|
-
const lines =
|
|
245
|
+
const lines = whitespace.stripCommonIndent(body.replace(/^\n/u, '').replace(trailingWhitespace, '').split('\n'));
|
|
244
246
|
return lines.every((line) => line === '')
|
|
245
247
|
? [open, hardline, close]
|
|
246
248
|
: [open, indent([hardline, join(hardline, lines)]), hardline, close];
|
|
@@ -261,24 +263,55 @@ function printComment(node) {
|
|
|
261
263
|
const tail = whitespace.html.test(body[body.length - 1] ?? '') ? '' : ' ';
|
|
262
264
|
return [open, lead, join(literalline, body.split('\n')), tail, close];
|
|
263
265
|
}
|
|
266
|
+
/* Handlebars escapes only the quote that closes the literal: `\n` is a backslash and an `n`,
|
|
267
|
+
* and `\\` is two backslashes. So unescaping is one replacement and re-escaping is its inverse,
|
|
268
|
+
* and a value can always be written in either quote. */
|
|
269
|
+
const isQuoted = (source) => source.length >= 2 && (source[0] === '"' || source[0] === "'") && source[source.length - 1] === source[0];
|
|
270
|
+
const unquote = (source) => source.slice(1, -1).split(`\\${source[0]}`).join(source[0]);
|
|
271
|
+
/* House style: `"` around an attribute value, `'` around a string literal in a mustache. The two
|
|
272
|
+
* are complementary, so a literal inside an attribute already wears the quote the attribute did
|
|
273
|
+
* not, and neither has to give way - which is what makes a template read the same throughout.
|
|
274
|
+
* Both still yield to the quote they sit inside, and to whichever one needs no escaping. */
|
|
275
|
+
const ATTRIBUTE_QUOTE = '"';
|
|
276
|
+
const LITERAL_QUOTE = "'";
|
|
277
|
+
const otherQuote = (quote) => (quote === '"' ? "'" : '"');
|
|
264
278
|
/**
|
|
265
|
-
* The quote
|
|
279
|
+
* The house quote, unless the text already holds it or it is the one enclosing this.
|
|
266
280
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
281
|
+
* A quote ends the run it opened, so the enclosing one is unusable at any price: `class="{{t
|
|
282
|
+
* "x"}}"` ends the attribute at the second `"` and the rest of the tag becomes something else.
|
|
283
|
+
* Text holding the other quote is only worth an escape, and an attribute has none to spend, so
|
|
284
|
+
* both callers want the quote their text does not hold.
|
|
269
285
|
*/
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
function pickQuote(text, house, enclosing) {
|
|
287
|
+
const usable = [house, otherQuote(house)].filter((quote) => quote !== enclosing);
|
|
288
|
+
return usable.find((quote) => !text.includes(quote)) ?? usable[0] ?? house;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A literal or path as it will be printed. A quoted path is a name written as a string -
|
|
292
|
+
* `{{> 'card'}}` - because the expression reader hands the head back as a path however it was
|
|
293
|
+
* written, so both go through the same quoting.
|
|
294
|
+
*
|
|
295
|
+
* Returns a string rather than a `Doc`: a block's closer has to repeat this exactly, and `Doc`
|
|
296
|
+
* is not something it can repeat.
|
|
297
|
+
*/
|
|
298
|
+
function printSource(source, options) {
|
|
299
|
+
if (!isQuoted(source) || options.opaqueQuotes) {
|
|
300
|
+
return source;
|
|
301
|
+
}
|
|
302
|
+
const value = unquote(source);
|
|
303
|
+
const quote = pickQuote(value, LITERAL_QUOTE, options.enclosingQuote);
|
|
304
|
+
return quote + value.split(quote).join(`\\${quote}`) + quote;
|
|
275
305
|
}
|
|
276
306
|
function printAttribute(attribute, options) {
|
|
277
307
|
if (attribute.type === 'RawAttribute') {
|
|
278
308
|
return attribute.raw;
|
|
279
309
|
}
|
|
310
|
+
/* The block's body is text and mustaches, not attributes: `title="` is half of one TextNode,
|
|
311
|
+
* so nothing downstream can tell which quote - if any - a literal in there sits inside. The
|
|
312
|
+
* text keeps the author's quotes because it is content; a literal keeps them for company. */
|
|
280
313
|
if (attribute.type === 'AttributeBlock') {
|
|
281
|
-
return printAny(attribute.block, options);
|
|
314
|
+
return printAny(attribute.block, { ...options, opaqueQuotes: true });
|
|
282
315
|
}
|
|
283
316
|
if (!attribute.value) {
|
|
284
317
|
return attribute.name;
|
|
@@ -288,8 +321,10 @@ function printAttribute(attribute, options) {
|
|
|
288
321
|
* rendered value. The parser marks the value's text as whitespace-significant, which is what
|
|
289
322
|
* keeps a block's body from being laid out at the printer's indent level instead of the
|
|
290
323
|
* author's - and what lets prettier see where the value's own lines end. */
|
|
324
|
+
/* Against the value's raw text, not just its TextNode parts: a quote inside a mustache is
|
|
325
|
+
* printed too, so `class='{{t "x"}}'` cannot be re-quoted with `"` without ending early. */
|
|
291
326
|
const { parts } = attribute.value;
|
|
292
|
-
const quote =
|
|
327
|
+
const quote = pickQuote(attribute.value.raw, ATTRIBUTE_QUOTE, options.enclosingQuote);
|
|
293
328
|
const nested = { ...options, enclosingQuote: quote };
|
|
294
329
|
return [attribute.name, '=', quote, ...parts.map((part) => printAny(part, nested)), quote];
|
|
295
330
|
}
|
|
@@ -299,7 +334,7 @@ function printAttribute(attribute, options) {
|
|
|
299
334
|
* a stray bracket rather than a break.
|
|
300
335
|
*/
|
|
301
336
|
function printOpenTag(node, options) {
|
|
302
|
-
const marker = node.selfClosing && !
|
|
337
|
+
const marker = node.selfClosing && !(0, html_1.isVoidElement)(node.tag) ? ' />' : '>';
|
|
303
338
|
if (node.attributes.length === 0) {
|
|
304
339
|
return ['<', node.tag, marker];
|
|
305
340
|
}
|
|
@@ -382,11 +417,11 @@ function printBlock(node, options, tail = []) {
|
|
|
382
417
|
const sections = [
|
|
383
418
|
{
|
|
384
419
|
program: node.program,
|
|
385
|
-
open: (breakable) => printCall(node, ['{{', trim(node.trimOpen), prefix], [trim(node.trimClose), '}}'], breakable),
|
|
420
|
+
open: (breakable) => printCall(node, ['{{', trim(node.trimOpen), prefix], [trim(node.trimClose), '}}'], options, breakable),
|
|
386
421
|
},
|
|
387
422
|
...(node.inverseChain ?? []).map((branch) => ({
|
|
388
423
|
program: branch.program,
|
|
389
|
-
open: (breakable) => printCall(branch, ['{{', trim(branch.trimOpen), `${elseKeyword} `], [trim(branch.trimClose), '}}'], breakable),
|
|
424
|
+
open: (breakable) => printCall(branch, ['{{', trim(branch.trimOpen), `${elseKeyword} `], [trim(branch.trimClose), '}}'], options, breakable),
|
|
390
425
|
})),
|
|
391
426
|
];
|
|
392
427
|
/* An empty `{{else}}` prints nothing - unless it carries `~`, which strips whitespace that
|
|
@@ -400,7 +435,7 @@ function printBlock(node, options, tail = []) {
|
|
|
400
435
|
const close = [
|
|
401
436
|
'{{',
|
|
402
437
|
trim(node.closeTrimOpen),
|
|
403
|
-
tokens_1.handlebarsDialect.getBlockClosePrefix(node.path.source),
|
|
438
|
+
tokens_1.handlebarsDialect.getBlockClosePrefix(printSource(node.path.source, options)),
|
|
404
439
|
trim(node.closeTrimClose),
|
|
405
440
|
'}}',
|
|
406
441
|
tail,
|
|
@@ -484,11 +519,11 @@ function printAny(node, options) {
|
|
|
484
519
|
case 'TextNode':
|
|
485
520
|
return assemble(textPieces(node));
|
|
486
521
|
case 'MustacheStatement':
|
|
487
|
-
return printStatement(node, '');
|
|
522
|
+
return printStatement(node, '', options);
|
|
488
523
|
case 'PartialStatement':
|
|
489
|
-
return printStatement(node, '> ');
|
|
524
|
+
return printStatement(node, '> ', options);
|
|
490
525
|
case 'DecoratorStatement':
|
|
491
|
-
return printStatement(node, '*');
|
|
526
|
+
return printStatement(node, '*', options);
|
|
492
527
|
case 'CommentStatement':
|
|
493
528
|
return printComment(node);
|
|
494
529
|
/* `childPieces` intercepts these, so this arm only keeps the switch exhaustive. It goes
|
|
@@ -514,10 +549,12 @@ const visitorKeys = {
|
|
|
514
549
|
ElseBranch: ['program'],
|
|
515
550
|
};
|
|
516
551
|
exports.printer = {
|
|
517
|
-
/* Only the root reaches this: everything below recurses through printAny.
|
|
518
|
-
|
|
552
|
+
/* Only the root reaches this: everything below recurses through printAny. Prettier's options
|
|
553
|
+
* are not among the arguments because nothing here reads them - the quoting is this
|
|
554
|
+
* formatter's own, and width and indentation belong to the doc printer. */
|
|
555
|
+
print(path) {
|
|
519
556
|
const node = path.node;
|
|
520
|
-
return node.type === 'Program' && path.parent === null ? printRoot(node.body,
|
|
557
|
+
return node.type === 'Program' && path.parent === null ? printRoot(node.body, {}) : printAny(node, {});
|
|
521
558
|
},
|
|
522
559
|
getVisitorKeys(node, nonTraversableKeys) {
|
|
523
560
|
const type = typeof node === 'object' && node !== null && 'type' in node ? String(node.type) : '';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SourceRange } from '
|
|
2
|
-
export type { SourceRange } from '
|
|
1
|
+
import type { SourceRange } from './core/source';
|
|
2
|
+
export type { SourceRange } from './core/source';
|
|
3
3
|
export type Node = Program | ElementNode | TextNode | MustacheStatement | BlockStatement | PartialStatement | DecoratorStatement | CommentStatement | UnmatchedNode;
|
|
4
4
|
export interface Program extends SourceRange {
|
|
5
5
|
type: 'Program';
|
package/docs/EDITOR_SETUP.md
CHANGED
|
@@ -7,7 +7,6 @@ The plugin works through Prettier. Editors must be configured so Prettier is sel
|
|
|
7
7
|
Install:
|
|
8
8
|
|
|
9
9
|
- [Prettier - Code formatter](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
|
10
|
-
- Optional companion extension: [HBS Master](https://marketplace.visualstudio.com/items?itemName=poliklot.hbs-master)
|
|
11
10
|
|
|
12
11
|
Recommended workspace settings:
|
|
13
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falsefalse/prettier-plugin-handlebars",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Whitespace-aware Handlebars formatter for Prettier",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"Claude <noreply@anthropic.com>"
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
],
|
|
46
46
|
"scripts": {
|
|
47
47
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
48
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
49
|
-
"typecheck": "tsc
|
|
48
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
49
|
+
"typecheck": "tsc",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"test:watch": "vitest",
|
|
52
52
|
"check": "npm run build && npm run typecheck && npm test && npm run gates",
|
|
@@ -67,8 +67,5 @@
|
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"prettier": ">=3.0.0"
|
|
70
|
-
},
|
|
71
|
-
"dependencies": {
|
|
72
|
-
"template-format-core": "^0.1.1"
|
|
73
70
|
}
|
|
74
71
|
}
|
|
File without changes
|
|
File without changes
|