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

Support prettier-ignore comments inside pug templates #125

Merged
merged 7 commits into from
Oct 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ max_line_length = off
trim_trailing_whitespace = false
indent_style = space
indent_size = 2

[*.pug]
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
indent_style = space
indent_size = 2
58 changes: 41 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,6 @@ Please note that the [plugin ecosystem in Prettier](https://prettier.io/docs/en/

Plugin for Prettier to format pug code

You can disable code formatting for a particular code block by adding <nobr>`<!-- prettier-ignore -->`</nobr> before ` ```pug `.

````markdown
Pug code with custom formatting:

<!-- prettier-ignore -->
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
```pug
div.text( color = "primary", disabled ="true" )
```

Prettified code:

```pug
.text(color="primary", disabled)
```
````

## Getting started

Simply install `prettier` and `@prettier/plugin-pug` as your project’s npm devDependencies:
Expand Down Expand Up @@ -77,6 +60,47 @@ yarn add --dev prettier @prettier/plugin-pug
yarn prettier --write "**/*.pug"
```

### Selectively ignoring automatic formatting

You can disable code formatting for a particular element by adding <nobr>`//- prettier-ignore`</nobr> comments in your pug templates:

```pug
div.text( color = "primary", disabled ="true" )
//- prettier-ignore
div.text( color = "primary", disabled ="true" )
//- prettier-ignore: because of reasons
div
div.text( color = "primary", disabled ="true" )
```

Prettified output:

```pug
.text(color="primary", disabled)
//- prettier-ignore
div.text( color = "primary", disabled ="true" )
//- prettier-ignore: because of reasons
div
div.text( color = "primary", disabled ="true" )
```

You can also disable code formatting in Markdown for a particular ` ```pug ` block by adding <nobr>`<!-- prettier-ignore -->`</nobr> before the block:

````markdown
Pug code with preserved custom formatting:

<!-- prettier-ignore -->
```pug
div.text( color = "primary", disabled ="true" )
```

Pug code with automatic formatting:

```pug
.text(color="primary", disabled)
```
````

### Pug versions of standard prettier options

By default, the same formatting options are used as configured through the standard prettier options.
Expand Down
17 changes: 12 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ if (process.env.NODE_ENV === 'test') {
logger.setLogLevel(LogLevel.DEBUG);
}

type FastPathStackEntry = {
content: string;
tokens: Token[];
};

export const plugin: Plugin = {
languages: [
{
Expand All @@ -38,13 +43,14 @@ export const plugin: Plugin = {
],
parsers: {
pug: {
parse(text: string, parsers: { [parserName: string]: Parser }, options: ParserOptions): Token[] {
parse(text: string, parsers: { [parserName: string]: Parser }, options: ParserOptions): FastPathStackEntry {
logger.debug('[parsers:pug:parse]:', { text });
const tokens: lex.Token[] = lex(text.trimLeft());
const content: string = text.trimLeft();
const tokens: lex.Token[] = lex(content);
// logger.debug('[parsers:pug:parse]: tokens', JSON.stringify(tokens, undefined, 2));
// const ast: AST = parse(tokens, {});
// logger.debug('[parsers:pug:parse]: ast', JSON.stringify(ast, undefined, 2));
return tokens;
return { content, tokens };
},
astFormat: 'pug-ast',
hasPragma(text: string): boolean {
Expand All @@ -67,9 +73,10 @@ export const plugin: Plugin = {
printers: {
'pug-ast': {
print(path: FastPath, options: ParserOptions & PugParserOptions, print: (path: FastPath) => Doc): Doc {
const tokens: Token[] = path.stack[0];
const entry: FastPathStackEntry = path.stack[0];
const { content, tokens } = entry;
const pugOptions: PugPrinterOptions = convergeOptions(options);
const printer: PugPrinter = new PugPrinter(tokens, pugOptions);
const printer: PugPrinter = new PugPrinter(content, tokens, pugOptions);
const result: string = printer.build();
logger.debug('[printers:pug-ast:print]:', result);
return result;
Expand Down
117 changes: 93 additions & 24 deletions src/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ export interface PugPrinterOptions {
export class PugPrinter {
private result: string = '';

private currentIndex: number = 0;
/**
* The index of the current token inside the `tokens` array
*/
// Start at -1, because `getNextToken()` increases it before retreval
private currentIndex: number = -1;
private currentLineLength: number = 0;

private readonly indentString: string;
Expand Down Expand Up @@ -128,7 +132,11 @@ export class PugPrinter {
private pipelessText: boolean = false;
private pipelessComment: boolean = false;

public constructor(private tokens: Token[], private readonly options: PugPrinterOptions) {
public constructor(
private readonly content: string,
private tokens: Token[],
private readonly options: PugPrinterOptions
) {
this.indentString = options.pugUseTabs ? '\t' : ' '.repeat(options.pugTabWidth);
this.quotes = this.options.pugSingleQuote ? "'" : '"';
this.otherQuotes = this.options.pugSingleQuote ? '"' : "'";
Expand All @@ -148,24 +156,15 @@ export class PugPrinter {
};
}

private get previousToken(): Token | undefined {
return this.tokens[this.currentIndex - 1];
}

private get nextToken(): Token | undefined {
return this.tokens[this.currentIndex + 1];
}

public build(): string {
const results: string[] = [];
if (this.tokens[0]?.type === 'text') {
results.push('| ');
} else if (this.tokens[0]?.type === 'eos') {
return '';
}
for (let index: number = 0; index < this.tokens.length; index++) {
this.currentIndex = index;
const token: Token = this.tokens[index];
let token: Token | null = this.getNextToken();
while (token) {
logger.debug('[PugPrinter]:', JSON.stringify(token));
try {
switch (token.type) {
Expand Down Expand Up @@ -197,6 +196,7 @@ export class PugPrinter {
} catch {
throw new Error('Unhandled token: ' + JSON.stringify(token));
}
token = this.getNextToken();
}
return results.join('');
}
Expand All @@ -222,6 +222,19 @@ export class PugPrinter {
return '';
}

private get previousToken(): Token | undefined {
return this.tokens[this.currentIndex - 1];
}

private get nextToken(): Token | undefined {
return this.tokens[this.currentIndex + 1];
}

private getNextToken(): Token | null {
this.currentIndex++;
return this.tokens[this.currentIndex] ?? null;
}

private quoteString(val: string): string {
return `${this.quotes}${val}${this.quotes}`;
}
Expand All @@ -236,6 +249,21 @@ export class PugPrinter {
: this.alwaysUseAttributeSeparator || /^(\(|\[|:).*/.test(token.name);
}

private getUnformattedContentLines(firstToken: Token, lastToken: Token): string[] {
const { start } = firstToken.loc;
const { end } = lastToken.loc;
const lines: string[] = this.content.split(/\r\n|\n|\r/);
const startLine: number = start.line - 1;
const endLine: number = end.line - 1;
const parts: string[] = [];
parts.push(lines[startLine].slice(start.column - 1));
for (let line: number = startLine + 1; line < endLine; line++) {
parts.push(lines[line]);
}
parts.push(lines[endLine].slice(0, end.column - 1));
return parts;
}

private formatDelegatePrettier(
val: string,
parser: '__vue_expression' | '__ng_binding' | '__ng_action' | '__ng_directive'
Expand Down Expand Up @@ -744,18 +772,59 @@ export class PugPrinter {
this.result += '\n';
}

private comment(token: CommentToken): string {
private comment(commentToken: CommentToken): string {
let result: string = this.computedIndent;
if (this.checkTokenType(this.previousToken, ['newline', 'indent', 'outdent'], true)) {
result += ' ';
}
result += '//';
if (!token.buffer) {
result += '-';
}
result += formatCommentPreserveSpaces(token.val, this.options.commentPreserveSpaces);
if (this.nextToken?.type === 'start-pipeless-text') {
this.pipelessComment = true;
// See if this is a `//- prettier-ignore` comment, which would indicate that the part of the template
// that follows should be left unformatted. Support the same format as typescript-eslint is using for descriptons:
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/ban-ts-comment.md#allow-with-description
if (/^ prettier-ignore($|[: ])/.test(commentToken.val)) {
Shinigami92 marked this conversation as resolved.
Show resolved Hide resolved
// Use a separate token processing loop to find the end of the stream of tokens to be ignored by formatting,
// and uses their `loc` properties to retrieve the original pug code to be used instead.
let token: Token | null = this.getNextToken();
if (token) {
let skipNewline: boolean = token.type === 'newline';
let ignoreLevel: number = 0;
while (token) {
const { type } = token;
if (type === 'newline' && ignoreLevel === 0) {
// Skip first newline after `prettier-ignore` comment
if (skipNewline) {
skipNewline = false;
} else {
break;
}
} else if (type === 'indent') {
ignoreLevel++;
} else if (type === 'outdent') {
ignoreLevel--;
if (ignoreLevel === 0) {
break;
}
}
token = this.getNextToken();
}
if (token) {
const lines: string[] = this.getUnformattedContentLines(commentToken, token);
// Trim the last line, since indentation of formatted pug is handled separately.
const lastLine: string | undefined = lines.pop();
if (lastLine !== undefined) {
lines.push(lastLine.trimRight());
}
result += lines.join('\n');
}
}
} else {
if (this.checkTokenType(this.previousToken, ['newline', 'indent', 'outdent'], true)) {
result += ' ';
}
result += '//';
if (!commentToken.buffer) {
result += '-';
}
result += formatCommentPreserveSpaces(commentToken.val, this.options.commentPreserveSpaces);
if (this.nextToken?.type === 'start-pipeless-text') {
this.pipelessComment = true;
}
}
return result;
}
Expand Down
31 changes: 31 additions & 0 deletions tests/prettier-ignore/formatted.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
div
.wrapper-1(
attributes1="foo",
:attribute2="bar",
attribute3="something too long to keep on one line"
)
//- prettier-ignore
div
.wrapper-2(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
//- prettier-ignore: this is already ignored be the previous statement above
div
.wrapper-3(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
div
.wrapper-4(
attributes1="foo",
:attribute2="bar",
attribute3="something too long to keep on one line"
)
div
.wrapper-5(
attributes1="foo",
:attribute2="bar",
attribute3="something too long to keep on one line"
)
//- prettier-ignore
div
.wrapper-6(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
//- prettier-ignore: this is already ignored be the previous statement above
div
.wrapper-7(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
div
14 changes: 14 additions & 0 deletions tests/prettier-ignore/prettier-ignore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { format } from 'prettier';
import { plugin } from './../../src/index';

describe('prettier-ignore', () => {
test('should handle // prettier-ignore statements', () => {
lehni marked this conversation as resolved.
Show resolved Hide resolved
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' as any, plugins: [plugin] });

expect(actual).toBe(expected);
});
});
19 changes: 19 additions & 0 deletions tests/prettier-ignore/unformatted.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
div
.wrapper-1(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
//- prettier-ignore
div
.wrapper-2(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
//- prettier-ignore: this is already ignored be the previous statement above
div
.wrapper-3(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
div
.wrapper-4(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
div
.wrapper-5(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
//- prettier-ignore
div
.wrapper-6(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
//- prettier-ignore: this is already ignored be the previous statement above
div
.wrapper-7(attributes1="foo" :attribute2="bar" attribute3 = "something too long to keep on one line")
div