Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle svelte syntax #204

Merged
merged 12 commits into from
May 1, 2021
102 changes: 81 additions & 21 deletions src/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import type { PugFramework } from './options/pug-framework';
import type { PugIdNotation } from './options/pug-id-notation';
import { isAngularAction, isAngularBinding, isAngularDirective, isAngularInterpolation } from './utils/angular';
import {
detectDangerousQuoteCombination,
detectFramework,
handleBracketSpacing,
isMultilineInterpolation,
Expand All @@ -79,6 +80,7 @@ import {
previousTagToken,
unwrapLineFeeds
} from './utils/common';
import { isSvelteInterpolation } from './utils/svelte';
import { isVueEventBinding, isVueExpression, isVueVForWithOf, isVueVOnExpression } from './utils/vue';

const logger: Logger = createLogger(console);
Expand Down Expand Up @@ -393,6 +395,7 @@ export class PugPrinter {
private formatText(text: string): string {
let result: string = '';
while (text) {
// Find double curly brackets
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor this entire method into smaller parts (that can be reused and are easier to grasp)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this code hurts, but I personally decided for now to await feedback from svelte community if there are further errors and printed warnings in their console output
Based on this feedback I can refactor this messy code
But now it's working and the svelte community can start working our formatter

const start: number = text.indexOf('{{');
if (start !== -1) {
result += text.slice(0, start);
Expand All @@ -401,21 +404,13 @@ export class PugPrinter {
if (end !== -1) {
let code: string = text.slice(0, end);
try {
// Index of primary quote
const q1: number = code.indexOf(this.quotes);
// Index of secondary (other) quote
const q2: number = code.indexOf(this.otherQuotes);
// Index of backtick
const qb: number = code.indexOf('`');
if (q1 >= 0 && q2 >= 0 && q2 > q1 && (qb < 0 || q1 < qb)) {
logger.log({
code,
quotes: this.quotes,
otherQuotes: this.otherQuotes,
q1,
q2,
qb
});
const dangerousQuoteCombination: boolean = detectDangerousQuoteCombination(
code,
this.quotes,
this.otherQuotes,
logger
);
if (dangerousQuoteCombination) {
logger.warn(
'The following expression could not be formatted correctly. Please try to fix it yourself and if there is a problem, please open a bug issue:',
code
Expand Down Expand Up @@ -487,8 +482,59 @@ export class PugPrinter {
text = '';
}
} else {
result += text;
text = '';
// Find single curly brackets for svelte
const start2: number = text.indexOf('{');
if (this.options.pugFramework === 'svelte' && start2 !== -1) {
result += text.slice(0, start2);
text = text.slice(start2 + 1);
const end2: number = text.indexOf('}');
if (end2 !== -1) {
let code: string = text.slice(0, end2);
try {
const dangerousQuoteCombination: boolean = detectDangerousQuoteCombination(
code,
this.quotes,
this.otherQuotes,
logger
);
if (dangerousQuoteCombination) {
logger.warn(
'The following expression could not be formatted correctly. Please try to fix it yourself and if there is a problem, please open a bug issue:',
code
);
result += handleBracketSpacing(this.options.pugBracketSpacing, code);
text = text.slice(end2 + 1);
continue;
} else {
code = this.frameworkFormat(code);
}
} catch (error: unknown) {
logger.warn('[PugPrinter:formatText]: ', error);
try {
code = format(code, {
parser: 'babel',
...this.codeInterpolationOptions,
semi: false
});
if (code[0] === ';') {
code = code.slice(1);
}
} catch (error: unknown) {
logger.warn(error);
}
}
code = unwrapLineFeeds(code);
result += handleBracketSpacing(this.options.pugBracketSpacing, code, ['{', '}']);
text = text.slice(end2 + 1);
} else {
result += '{';
result += text;
text = '';
}
} else {
result += text;
text = '';
}
}
}
return result;
Expand Down Expand Up @@ -533,23 +579,35 @@ export class PugPrinter {
return this.formatDelegatePrettier(val, '__ng_directive');
}

private formatAngularInterpolation(val: string): string {
private formatFrameworkInterpolation(
val: string,
parser: '__ng_interpolation', // TODO: may be changed to allow a special parser for svelte
[opening, closing]: ['{{', '}}'] | ['{', '}']
): string {
val = val.slice(1, -1); // Remove quotes
val = val.slice(2, -2); // Remove braces
val = val.slice(opening.length, -closing.length); // Remove braces
val = val.trim();
if (val.includes(`\\${this.otherQuotes}`)) {
logger.warn(
'The following expression could not be formatted correctly. Please try to fix it yourself and if there is a problem, please open a bug issue:',
val
);
} else {
val = format(val, { parser: '__ng_interpolation', ...this.codeInterpolationOptions });
val = format(val, { parser, ...this.codeInterpolationOptions });
val = unwrapLineFeeds(val);
}
val = handleBracketSpacing(this.options.pugBracketSpacing, val);
val = handleBracketSpacing(this.options.pugBracketSpacing, val, [opening, closing]);
return this.quoteString(val);
}

private formatAngularInterpolation(val: string): string {
return this.formatFrameworkInterpolation(val, '__ng_interpolation', ['{{', '}}']);
}

private formatSvelteInterpolation(val: string): string {
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
return this.formatFrameworkInterpolation(val, '__ng_interpolation', ['{', '}']);
}

//#endregion

// ######## ####### ## ## ######## ## ## ######## ######## ####### ###### ######## ###### ###### ####### ######## ######
Expand Down Expand Up @@ -809,6 +867,8 @@ export class PugPrinter {
val = this.formatAngularDirective(val);
} else if (isAngularInterpolation(val)) {
val = this.formatAngularInterpolation(val);
} else if (isSvelteInterpolation(val)) {
val = this.formatSvelteInterpolation(val);
} else if (isStyleAttribute(token.name, token.val)) {
val = this.formatStyleAttribute(val);
} else if (isQuoted(val)) {
Expand Down
12 changes: 3 additions & 9 deletions src/utils/angular.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isQuoted, isWrappedWith } from './common';

/**
* Indicates whether the attribute name is an Angular binding.
*
Expand Down Expand Up @@ -79,13 +81,5 @@ export function isAngularDirective(name: string): boolean {
* @returns `true` if `val` passes the angular interpolation check, otherwise `false`.
*/
export function isAngularInterpolation(val: string): boolean {
return (
val.length >= 5 &&
((val[0] === '"' && val[val.length - 1] === '"') || (val[0] === "'" && val[val.length - 1] === "'")) &&
val[1] === '{' &&
val[2] === '{' &&
val[val.length - 2] === '}' &&
val[val.length - 3] === '}' &&
!val.includes('{{', 3)
);
return val.length >= 5 && isQuoted(val) && isWrappedWith(val, '{{', '}}', 1) && !val.includes('{{', 3);
}
44 changes: 44 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AttributeToken, TagToken, Token } from 'pug-lexer';
import type { Logger } from '../logger';
import type { PugFramework } from '../options/pug-framework';

/**
Expand Down Expand Up @@ -82,6 +83,19 @@ export function isStyleAttribute(name: string, val: string): boolean {
return name === 'style' && isQuoted(val);
}

/**
* Indicates whether the value is surrounded by the `start` and `end` parameters.
*
* @param val Value of a tag attribute.
* @param start The left hand side of the wrapping.
* @param end The right hand side of the wrapping.
* @param offset The offset from left and right where to search from.
* @returns Whether the value is wrapped wit start and end from the offset or not.
*/
export function isWrappedWith(val: string, start: string, end: string, offset: number = 0): boolean {
return val.startsWith(start, offset) && val.endsWith(end, val.length - offset);
}

/**
* Indicates whether the value is surrounded by quotes.
*
Expand Down Expand Up @@ -196,6 +210,36 @@ export function makeString(
return enclosingQuote + newContent + enclosingQuote;
}

/**
* See [issue #9](https://github.com/prettier/plugin-pug/issues/9) for more details.
*
* @param code Code that is checked.
* @param quotes Quotes.
* @param otherQuotes Opposite of quotes.
* @param logger A logger.
* @returns Whether dangerous quote combinations where detected or not.
*/
export function detectDangerousQuoteCombination(
code: string,
quotes: "'" | '"',
otherQuotes: "'" | '"',
logger: Logger
): boolean {
// Index of primary quote
const q1: number = code.indexOf(quotes);
// Index of secondary (other) quote
const q2: number = code.indexOf(otherQuotes);
// Index of backtick
const qb: number = code.indexOf('`');

if (q1 >= 0 && q2 >= 0 && q2 > q1 && (qb < 0 || q1 < qb)) {
logger.log({ code, quotes, otherQuotes, q1, q2, qb });
return true;
}

return false;
}

/**
* Try to detect used framework within the project by reading `process.env.npm_package_*`.
*
Expand Down
22 changes: 22 additions & 0 deletions src/utils/svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { isQuoted, isWrappedWith } from './common';

/**
* Indicates whether the attribute value is a Svelte interpolation.
*
* ---
*
* Example interpolation:
* ```
* a(href="{ cat.id }")
* ```
*
* In this case `val` is `"{ cat.id }"`.
*
* ---
*
* @param val Value of tag attribute.
* @returns `true` if `val` passes the svelte interpolation check, otherwise `false`.
*/
export function isSvelteInterpolation(val: string): boolean {
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
return val.length >= 3 && isQuoted(val) && isWrappedWith(val, '{', '}', 1) && !val.includes('{', 2);
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
}
17 changes: 17 additions & 0 deletions tests/frameworks/svelte/formatted.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
main
h1 Hello { name }!
p
| Visit the
a(href="https://svelte.dev/tutorial")
| Svelte tutorial
| to learn how to build Svelte
| apps.
p
+each('cats as cat')
a(href="{ cat.id }") { cat.name }
| !{ ' ' }

p
+if('!user.loggedIn')
button(on:click="{ toggle }")
| Log out
22 changes: 22 additions & 0 deletions tests/frameworks/svelte/svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { format } from 'prettier';
import { plugin } from './../../../src/index';

describe('Frameworks', () => {
describe('Svelte', () => {
test('should format svelte', () => {
const expected: string = readFileSync(resolve(__dirname, 'formatted.pug'), 'utf8');
const code: string = readFileSync(resolve(__dirname, 'unformatted.pug'), 'utf8');
const actual: string = format(code, {
parser: 'pug',
plugins: [plugin],

// @ts-expect-error
pugFramework: 'svelte'
});

expect(actual).toBe(expected);
});
});
});
17 changes: 17 additions & 0 deletions tests/frameworks/svelte/unformatted.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
main
h1 Hello { name}!
p
| Visit the
a(href='https://svelte.dev/tutorial')
| Svelte tutorial
| to learn how to build Svelte
| apps.
p
+each('cats as cat')
a(href='{ cat.id }') { cat.name }
| !{ ' ' }

p
+if('!user.loggedIn')
button(on:click='{ toggle}')
| Log out