Skip to content

Commit 2e77531

Browse files
Add framework option (#186)
Co-authored-by: Shinigami92 <[email protected]>
1 parent 9ee3cd5 commit 2e77531

21 files changed

+289
-51
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ These additional options are specific to pug templates and can be configured in
142142
| `pugSortAttributes` | choice | `'as-is'` | Sort attributes that are not on _beginning_ and _end_ patterns.<ul><li>`'as-is'` -> Keep the attributes untouched.<br>Example: `Foo(a c d b)`</li><li>`'asc'` -> Sort attributes ascending.<br>Example: `Foo(a b c d)`</li><li>`'desc'` -> Sort attributes descending.<br>Example: `Foo(d c b a)`</li></ul> |
143143
| `pugWrapAttributesThreshold` | choice | `-1` | Define the maximum amount of attributes that an element can appear with on one line before it gets wrapped.<ul><li>`-1` -> Only wrap attributes as needed.<br>Example:<pre>input(type="text")<br>input(type="text", value="my_value", name="my_name")</pre></li><li>`0` -> Always wrap attributes.<br>Example:<pre>input(<br>&nbsp;&nbsp;type="text"<br>)<br>input(<br>&nbsp;&nbsp;type="text",<br>&nbsp;&nbsp;value="my_value",<br>&nbsp;&nbsp;name="my_name"<br>)</pre></li><li>`1` -> Allow one unwrapped attribute, wrap two and more.<br>Example:<pre>input(type="text")<br>input(<br>&nbsp;&nbsp;type="text",<br>&nbsp;&nbsp;value="my_value",<br>&nbsp;&nbsp;name="my_name"<br>)</pre></li><li>`2 .. Infinity` -> Same as above, just with different thresholds.</li></ul> |
144144
| `pugWrapAttributesPattern` | array | `[]` | Define a regex pattern to match attributes against that should always trigger wrapping. |
145+
| `pugFramework` | choice | `'none'` | Specify the used framework within the project.<br>Options are `'none'`, `'vue'`, `'svelte'` and `'angular'`.<br>If not set, or set to `'none'`, the plugin tries to find the correct framework by reading `process.env.npm_package_*`. |
145146
| `pugSingleFileComponentIndentation` | boolean | `false` | Indent pug in template tags in single file components such as from vue or svelte. |
146147
| `pugEmptyAttributes` | choice | `'as-is'` | Change behavior of boolean attributes.<ul><li>`'as-is'` -> Nothing is changed.<br>Example: `foo(a, b="", c)`</li><li>`'none'` -> Every attribute with empty quotes will have them removed.<br>Example: `foo(a, b, c)`</li><li>`'all'` -> Every boolean attribute will be expressed with empty quotes.<br>Example: `foo(a="", b="", c="")`</li></ul> |
147148
| `pugEmptyAttributesForceQuotes` | array | `[]` | Define a list of patterns for attributes that will be forced to have empty quotes even with "none" selected. |

src/options/converge.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function convergeOptions(options: ParserOptions & PugParserOptions): PugP
3636
pugIdNotation: options.pugIdNotation,
3737
pugEmptyAttributes: options.pugEmptyAttributes,
3838
pugEmptyAttributesForceQuotes: options.pugEmptyAttributesForceQuotes,
39-
pugSingleFileComponentIndentation: options.pugSingleFileComponentIndentation && options.embeddedInHtml
39+
pugSingleFileComponentIndentation: options.pugSingleFileComponentIndentation && options.embeddedInHtml,
40+
pugFramework: options.pugFramework
4041
};
4142
}

src/options/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import type { PugEmptyAttributes, PugEmptyAttributesForceQuotes } from './empty-
2525
import { PUG_EMPTY_ATTRIBUTES_FORCE_QUOTES_OPTION, PUG_EMPTY_ATTRIBUTES_OPTION } from './empty-attributes';
2626
import type { PugClassNotation } from './pug-class-notation';
2727
import { PUG_CLASS_NOTATION } from './pug-class-notation';
28+
import type { PugFramework } from './pug-framework';
29+
import { PUG_FRAMEWORK } from './pug-framework';
2830
import type { PugIdNotation } from './pug-id-notation';
2931
import { PUG_ID_NOTATION } from './pug-id-notation';
3032
import { PUG_SINGLE_FILE_COMPONENT_INDENTATION } from './pug-single-file-component-indentation';
@@ -76,6 +78,8 @@ export interface PugParserOptions
7678
pugEmptyAttributesForceQuotes: PugEmptyAttributesForceQuotes;
7779

7880
pugSingleFileComponentIndentation: boolean;
81+
82+
pugFramework: PugFramework;
7983
}
8084

8185
/**
@@ -104,5 +108,6 @@ export const options: SupportOptions = {
104108
pugClassNotation: PUG_CLASS_NOTATION,
105109
pugIdNotation: PUG_ID_NOTATION,
106110
pugEmptyAttributesForceQuotes: PUG_EMPTY_ATTRIBUTES_FORCE_QUOTES_OPTION,
107-
pugSingleFileComponentIndentation: PUG_SINGLE_FILE_COMPONENT_INDENTATION
111+
pugSingleFileComponentIndentation: PUG_SINGLE_FILE_COMPONENT_INDENTATION,
112+
pugFramework: PUG_FRAMEWORK
108113
};

src/options/pug-framework.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { ChoiceSupportOption } from 'prettier';
2+
import { CATEGORY_PUG } from '.';
3+
4+
/** Pug Framework. */
5+
export const PUG_FRAMEWORK: ChoiceSupportOption = {
6+
since: '1.14.0',
7+
category: CATEGORY_PUG,
8+
type: 'choice',
9+
default: 'none',
10+
description: 'Define which framework is used in the project.',
11+
choices: [
12+
{ value: 'none', description: 'No framework specifically.' },
13+
{ value: 'vue', description: 'Uses Vue.js.' },
14+
{ value: 'svelte', description: 'Uses Svelte.' },
15+
{ value: 'angular', description: 'Uses Angular.' }
16+
]
17+
};
18+
19+
/** Pug class notation. */
20+
export type PugFramework = 'none' | 'vue' | 'svelte' | 'angular';

src/options/pug-id-notation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const PUG_ID_NOTATION: ChoiceSupportOption = {
77
category: CATEGORY_PUG,
88
type: 'choice',
99
default: 'literal',
10-
description: 'Define how the Id should be formatted',
10+
description: 'Define how the id should be formatted.',
1111
choices: [
1212
{ value: 'literal', description: 'Forces all valid ids to be printed as literals.' },
1313
{ value: 'as-is', description: 'Disables id formatting.' }

src/printer.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BuiltInParserName, RequiredOptions } from 'prettier';
1+
import type { BuiltInParserName, Options, RequiredOptions } from 'prettier';
22
import { format } from 'prettier';
33
import type {
44
AndAttributesToken,
@@ -65,9 +65,11 @@ import type { ArrowParens } from './options/common';
6565
import type { PugEmptyAttributes, PugEmptyAttributesForceQuotes } from './options/empty-attributes';
6666
import { formatEmptyAttribute } from './options/empty-attributes/utils';
6767
import type { PugClassNotation } from './options/pug-class-notation';
68+
import type { PugFramework } from './options/pug-framework';
6869
import type { PugIdNotation } from './options/pug-id-notation';
6970
import { isAngularAction, isAngularBinding, isAngularDirective, isAngularInterpolation } from './utils/angular';
7071
import {
72+
detectFramework,
7173
handleBracketSpacing,
7274
isMultilineInterpolation,
7375
isQuoted,
@@ -115,6 +117,7 @@ export interface PugPrinterOptions {
115117
readonly pugEmptyAttributes: PugEmptyAttributes;
116118
readonly pugEmptyAttributesForceQuotes: PugEmptyAttributesForceQuotes;
117119
readonly pugSingleFileComponentIndentation: boolean;
120+
readonly pugFramework: PugFramework;
118121
}
119122

120123
/**
@@ -153,6 +156,8 @@ export class PugPrinter {
153156
private readonly indentString: string;
154157
private indentLevel: number = 0;
155158

159+
private readonly framework: PugFramework = 'none';
160+
156161
private readonly quotes: "'" | '"';
157162
private readonly otherQuotes: "'" | '"';
158163

@@ -197,6 +202,7 @@ export class PugPrinter {
197202
if (options.pugSingleFileComponentIndentation) {
198203
this.indentLevel++;
199204
}
205+
this.framework = options.pugFramework !== 'none' ? options.pugFramework : detectFramework();
200206

201207
this.quotes = this.options.pugSingleQuote ? "'" : '"';
202208
this.otherQuotes = this.options.pugSingleQuote ? '"' : "'";
@@ -367,6 +373,21 @@ export class PugPrinter {
367373
}
368374
}
369375

376+
private frameworkFormat(code: string): string {
377+
const options: Options = { ...this.codeInterpolationOptions };
378+
switch (this.framework) {
379+
case 'angular':
380+
options.parser = '__ng_interpolation';
381+
break;
382+
case 'svelte':
383+
case 'vue':
384+
default:
385+
options.parser = 'babel';
386+
options.semi = false;
387+
}
388+
return format(code, options);
389+
}
390+
370391
private formatText(text: string): string {
371392
let result: string = '';
372393
while (text) {
@@ -401,7 +422,7 @@ export class PugPrinter {
401422
text = text.slice(end + 2);
402423
continue;
403424
} else {
404-
code = format(code, { parser: '__ng_interpolation', ...this.codeInterpolationOptions });
425+
code = this.frameworkFormat(code);
405426
}
406427
} catch (error: unknown) {
407428
if (typeof error === 'string') {
@@ -415,20 +436,26 @@ export class PugPrinter {
415436
`code: \`${code.trim()}\``
416437
);
417438
} else if (error.includes("Unexpected token '('")) {
418-
logger.warn(
419-
"[PugPrinter:formatText]: Found unexpected token '('. If you are using Vue, you can ignore this message.",
420-
`code: \`${code.trim()}\``
421-
);
422-
} else if (error.includes('Missing expected )')) {
423-
logger.warn(
424-
'[PugPrinter:formatText]: Missing expected ). If you are using Vue, you can ignore this message.',
425-
`code: \`${code.trim()}\``
426-
);
427-
} else if (error.includes('Missing expected :')) {
428-
logger.warn(
429-
'[PugPrinter:formatText]: Missing expected :. If you are using Vue, you can ignore this message.',
430-
`code: \`${code.trim()}\``
431-
);
439+
if (this.framework !== 'vue') {
440+
logger.warn(
441+
'[PugPrinter:formatText]: Found unexpected token `(`.',
442+
`code: \`${code.trim()}\``
443+
);
444+
}
445+
} else if (error.includes('Missing expected `)`')) {
446+
if (this.framework !== 'vue') {
447+
logger.warn(
448+
'[PugPrinter:formatText]: Missing expected `)`.',
449+
`code: \`${code.trim()}\``
450+
);
451+
}
452+
} else if (error.includes('Missing expected `:`')) {
453+
if (this.framework !== 'vue') {
454+
logger.warn(
455+
'[PugPrinter:formatText]: Missing expected `:`.',
456+
`code: \`${code.trim()}\``
457+
);
458+
}
432459
} else {
433460
logger.warn('[PugPrinter:formatText]: ', error);
434461
}

src/utils/common.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AttributeToken, TagToken, Token } from 'pug-lexer';
2+
import type { PugFramework } from '../options/pug-framework';
23

34
/**
45
* Returns the previous tag token if there was one.
@@ -191,3 +192,24 @@ export function makeString(
191192
);
192193
return enclosingQuote + newContent + enclosingQuote;
193194
}
195+
196+
/**
197+
* Try to detect used framework within the project by reading `process.env.npm_package_*`.
198+
*
199+
* @returns PugFramework.
200+
*/
201+
export function detectFramework(): PugFramework {
202+
try {
203+
const npmPackages: string[] = Object.keys(process.env).filter((key) => key.startsWith('npm_package_'));
204+
if (npmPackages.some((pack) => pack.includes('_vue'))) {
205+
return 'vue';
206+
} else if (npmPackages.some((pack) => pack.includes('_svelte'))) {
207+
return 'svelte';
208+
} else if (npmPackages.some((pack) => pack.includes('_angular'))) {
209+
return 'angular';
210+
}
211+
} catch {
212+
return 'none';
213+
}
214+
return 'none';
215+
}

tests/frameworks/detection.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { detectFramework } from '../../src/utils/common';
2+
3+
describe('Frameworks', () => {
4+
describe('Detection', () => {
5+
const backupProcessEnv: Record<string, string | undefined> = process.env;
6+
7+
afterEach(() => {
8+
process.env = { ...backupProcessEnv };
9+
});
10+
11+
test('should fallback to none if no framework detected via process.env', () => {
12+
expect(detectFramework()).toBe('none');
13+
});
14+
test('should detect vue from process.env', () => {
15+
process.env.npm_package_dependencies_vue = 'some version';
16+
expect(detectFramework()).toBe('vue');
17+
});
18+
test('should detect svelte from process.env', () => {
19+
process.env.npm_package_devDependencies_svelte = 'some version';
20+
expect(detectFramework()).toBe('svelte');
21+
});
22+
test('should detect angular from process.env', () => {
23+
process.env.npm_package_dependencies__angular_core = 'some version';
24+
expect(detectFramework()).toBe('angular');
25+
});
26+
});
27+
});

tests/frameworks/vue/v-bind/v-bind.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ describe('Frameworks', () => {
88
test('should format v-bind', () => {
99
const expected: string = readFileSync(resolve(__dirname, 'formatted.pug'), 'utf8');
1010
const code: string = readFileSync(resolve(__dirname, 'unformatted.pug'), 'utf8');
11-
const actual: string = format(code, { parser: 'pug', plugins: [plugin] });
11+
const actual: string = format(code, {
12+
parser: 'pug',
13+
plugins: [plugin],
14+
15+
// @ts-expect-error
16+
pugFramework: 'vue'
17+
});
1218

1319
expect(actual).toBe(expected);
1420
});

tests/frameworks/vue/v-model/v-model.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ describe('Frameworks', () => {
88
test('should format v-model', () => {
99
const expected: string = readFileSync(resolve(__dirname, 'formatted.pug'), 'utf8');
1010
const code: string = readFileSync(resolve(__dirname, 'unformatted.pug'), 'utf8');
11-
const actual: string = format(code, { parser: 'pug', plugins: [plugin] });
11+
const actual: string = format(code, {
12+
parser: 'pug',
13+
plugins: [plugin],
14+
15+
// @ts-expect-error
16+
pugFramework: 'vue'
17+
});
1218

1319
expect(actual).toBe(expected);
1420
});

0 commit comments

Comments
 (0)