Skip to content

Commit 36ff82c

Browse files
authored
Add curly braces when attribute shorthand is used but not allowed (#119)
+ Some refactorings to make code more clear Closes #110
1 parent e7d6360 commit 36ff82c

File tree

10 files changed

+133
-71
lines changed

10 files changed

+133
-71
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test/**/*.html

src/print/index.ts

+35-35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FastPath, Doc, doc, ParserOptions } from 'prettier';
2-
import { Node, MustacheTagNode, IfBlockNode } from './nodes';
2+
import { Node, IfBlockNode, AttributeNode } from './nodes';
33
import { isASTNode, isPreTagContent } from './helpers';
44
import { extractAttributes } from '../lib/extractAttributes';
55
import { getText } from '../lib/getText';
@@ -12,6 +12,8 @@ import {
1212
isInlineElement,
1313
isInlineNode,
1414
isEmptyNode,
15+
isLoneMustacheTag,
16+
isOrCanBeConvertedToShorthand,
1517
} from './node-helpers';
1618
import {
1719
isLine,
@@ -224,43 +226,26 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
224226
return node.expression.name;
225227
}
226228
case 'Attribute': {
227-
const hasLoneMustacheTag =
228-
node.value !== true &&
229-
node.value.length === 1 &&
230-
node.value[0].type === 'MustacheTag';
231-
let isAttributeShorthand =
232-
node.value !== true &&
233-
node.value.length === 1 &&
234-
node.value[0].type === 'AttributeShorthand';
235-
236-
// Convert a={a} into {a}
237-
if (hasLoneMustacheTag) {
238-
const expression = (node.value as [MustacheTagNode])[0].expression;
239-
isAttributeShorthand =
240-
expression.type === 'Identifier' && expression.name === node.name;
241-
}
242-
243-
if (isAttributeShorthand && options.svelteAllowShorthand) {
244-
return concat([line, '{', node.name, '}']);
229+
if (isOrCanBeConvertedToShorthand(node)) {
230+
if (options.svelteStrictMode) {
231+
return concat([line, node.name, '="{', node.name, '}"']);
232+
} else if (options.svelteAllowShorthand) {
233+
return concat([line, '{', node.name, '}']);
234+
} else {
235+
return concat([line, node.name, '={', node.name, '}']);
236+
}
245237
} else {
246-
const def: Doc[] = [line, node.name];
247-
if (node.value !== true) {
248-
def.push('=');
249-
const quotes = !hasLoneMustacheTag || options.svelteStrictMode;
250-
251-
quotes && def.push('"');
252-
253-
const valueDocs = path.map((childPath) => childPath.call(print), 'value');
254-
255-
if (!quotes || !formattableAttributes.includes(node.name)) {
256-
def.push(concat(valueDocs));
257-
} else {
258-
def.push(indent(group(concat(trim(valueDocs, isLine)))));
259-
}
238+
if (node.value === true) {
239+
return concat([line, node.name]);
240+
}
260241

261-
quotes && def.push('"');
242+
const quotes = !isLoneMustacheTag(node.value) || options.svelteStrictMode;
243+
const attrNodeValue = printAttributeNodeValue(path, print, quotes, node);
244+
if (quotes) {
245+
return concat([line, node.name, '=', '"', attrNodeValue, '"']);
246+
} else {
247+
return concat([line, node.name, '=', attrNodeValue]);
262248
}
263-
return concat(def);
264249
}
265250
}
266251
case 'MustacheTag':
@@ -488,6 +473,21 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
488473
throw new Error('unknown node type: ' + node.type);
489474
}
490475

476+
function printAttributeNodeValue(
477+
path: FastPath<any>,
478+
print: PrintFn,
479+
quotes: boolean,
480+
node: AttributeNode,
481+
) {
482+
const valueDocs = path.map((childPath) => childPath.call(print), 'value');
483+
484+
if (!quotes || !formattableAttributes.includes(node.name)) {
485+
return concat(valueDocs);
486+
} else {
487+
return indent(group(concat(trim(valueDocs, isLine))));
488+
}
489+
}
490+
491491
function printChildren(path: FastPath, print: PrintFn): Doc[] {
492492
let childDocs: Doc[] = [];
493493
let currentGroup: { doc: Doc; node: Node }[] = [];

src/print/node-helpers.ts

+59-35
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,78 @@
1-
import { Node } from './nodes';
1+
import { Node, MustacheTagNode, AttributeShorthandNode, AttributeNode } from './nodes';
22
import { inlineElements, TagName } from '../lib/elements';
33

44
export function isInlineElement(node: Node) {
5-
return node.type === 'Element' && inlineElements.includes(node.name as TagName);
5+
return node.type === 'Element' && inlineElements.includes(node.name as TagName);
66
}
77

88
export function isWhitespaceChar(ch: string) {
9-
return ' \t\n\r'.indexOf(ch) >= 0;
9+
return ' \t\n\r'.indexOf(ch) >= 0;
1010
}
1111

1212
export function canBreakAfter(node: Node) {
13-
switch (node.type) {
14-
case 'Text':
15-
return isWhitespaceChar(node.raw[node.raw.length - 1]);
16-
case 'Element':
17-
return !isInlineElement(node);
18-
default:
19-
return true;
20-
}
13+
switch (node.type) {
14+
case 'Text':
15+
return isWhitespaceChar(node.raw[node.raw.length - 1]);
16+
case 'Element':
17+
return !isInlineElement(node);
18+
default:
19+
return true;
20+
}
2121
}
2222

2323
export function canBreakBefore(node: Node) {
24-
switch (node.type) {
25-
case 'Text':
26-
return isWhitespaceChar(node.raw[0]);
27-
case 'Element':
28-
return !isInlineElement(node);
29-
default:
30-
return true;
31-
}
24+
switch (node.type) {
25+
case 'Text':
26+
return isWhitespaceChar(node.raw[0]);
27+
case 'Element':
28+
return !isInlineElement(node);
29+
default:
30+
return true;
31+
}
3232
}
3333

3434
export function isInlineNode(node: Node): boolean {
35-
switch (node.type) {
36-
case 'Text':
37-
const text = node.raw || node.data;
38-
const isAllWhitespace = text.trim() === ''
39-
40-
return !isAllWhitespace || text === '';
41-
case 'MustacheTag':
42-
case 'EachBlock':
43-
case 'IfBlock':
44-
return true;
45-
case 'Element':
46-
return isInlineElement(node);
47-
default:
48-
return false;
49-
}
35+
switch (node.type) {
36+
case 'Text':
37+
const text = node.raw || node.data;
38+
const isAllWhitespace = text.trim() === '';
39+
40+
return !isAllWhitespace || text === '';
41+
case 'MustacheTag':
42+
case 'EachBlock':
43+
case 'IfBlock':
44+
return true;
45+
case 'Element':
46+
return isInlineElement(node);
47+
default:
48+
return false;
49+
}
5050
}
5151

5252
export function isEmptyNode(node: Node): boolean {
53-
return node.type === 'Text' && (node.raw || node.data).trim() === '';
53+
return node.type === 'Text' && (node.raw || node.data).trim() === '';
54+
}
55+
56+
export function isLoneMustacheTag(node: true | Node[]): node is [MustacheTagNode] {
57+
return node !== true && node.length === 1 && node[0].type === 'MustacheTag';
58+
}
59+
60+
export function isAttributeShorthand(node: true | Node[]): node is [AttributeShorthandNode] {
61+
return node !== true && node.length === 1 && node[0].type === 'AttributeShorthand';
62+
}
63+
64+
/**
65+
* True if node is of type `{a}` or `a={a}`
66+
*/
67+
export function isOrCanBeConvertedToShorthand(node: AttributeNode): boolean {
68+
if (isAttributeShorthand(node.value)) {
69+
return true;
70+
}
71+
72+
if (isLoneMustacheTag(node.value)) {
73+
const expression = node.value[0].expression;
74+
return expression.type === 'Identifier' && expression.name === node.name;
75+
}
76+
77+
return false;
5478
}

test/formatting/index.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import test from 'ava';
2-
import { readdirSync, readFileSync } from 'fs';
2+
import { readdirSync, readFileSync, existsSync } from 'fs';
33
import { format } from 'prettier';
44

55
const dirs = readdirSync('test/formatting/samples');
@@ -13,13 +13,24 @@ for (const dir of dirs) {
1313
`test/formatting/samples/${dir}/output.html`,
1414
'utf-8',
1515
).replace(/\r?\n/g, '\n');
16+
const options = readOptions(`test/formatting/samples/${dir}/options.json`);
1617

1718
test(`formatting: ${dir}`, t => {
1819
const actualOutput = format(input, {
1920
parser: 'svelte' as any,
2021
plugins: [require.resolve('../../src')],
2122
tabWidth: 4,
23+
...options,
2224
});
2325
t.is(expectedOutput, actualOutput);
2426
});
2527
}
28+
29+
function readOptions(fileName: string) {
30+
if (!existsSync(fileName)) {
31+
return {};
32+
}
33+
34+
const fileContents = readFileSync(fileName, 'utf-8');
35+
return JSON.parse(fileContents);
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<input value />
2+
<input value="string" />
3+
<input {value} />
4+
<input value="{value}" />
5+
<input value={value} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelteAllowShorthand": false
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<input value />
2+
<input value="string" />
3+
<input value={value} />
4+
<input value={value} />
5+
<input value={value} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<input value />
2+
<input value="string" />
3+
<input {value} />
4+
<input value={value} />
5+
<input value="{value}" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"svelteStrictMode": true
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<input value />
2+
<input value="string" />
3+
<input value="{value}" />
4+
<input value="{value}" />
5+
<input value="{value}" />

0 commit comments

Comments
 (0)