From 1feb66687adf36524b209afbf2ec32f83bcfb4bb Mon Sep 17 00:00:00 2001 From: Christopher Quadflieg Date: Tue, 2 Jul 2019 00:16:15 +0200 Subject: [PATCH] Support wrap attributes --- src/index.ts | 108 +++++++++++++------ src/pug-token.ts | 4 +- test/indents/formatted-2-spaces.pug | 16 ++- test/indents/formatted-3-spaces.pug | 16 ++- test/indents/formatted-4-spaces.pug | 16 ++- test/indents/formatted-tabs.pug | 16 ++- test/literals/class-literals/formatted.pug | 26 +++++ test/literals/class-literals/unformatted.pug | 7 ++ test/literals/id-literals/formatted.pug | 16 ++- test/literals/id-literals/unformatted.pug | 4 + test/quotes/double-to-single/formatted.pug | 9 +- test/quotes/single-to-double/formatted.pug | 9 +- 12 files changed, 201 insertions(+), 46 deletions(-) diff --git a/src/index.ts b/src/index.ts index 172be913..8d6970a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { AST, Doc, FastPath, Options, Parser, ParserOptions, Plugin, util } from // @ts-ignore import * as lex from 'pug-lexer'; import { createLogger, Logger, LogLevel } from './logger'; -import { Token } from './pug-token'; +import { AttributeToken, EndAttributesToken, Token } from './pug-token'; const { makeString } = util; @@ -31,6 +31,21 @@ function quotationType(code: string): QuotationType | undefined { return; } +function previousNormalAttributeToken(tokens: Token[], index: number): AttributeToken | undefined { + for (let i: number = index - 1; i > 0; i--) { + const token: Token = tokens[i]; + if (token.type === 'start-attributes') { + return; + } + if (token.type === 'attribute') { + if (token.name !== 'class' && token.name !== 'id') { + return token; + } + } + } + return; +} + export const plugin: Plugin = { languages: [ { @@ -77,7 +92,7 @@ export const plugin: Plugin = { 'pug-ast': { print( path: FastPath, - { singleQuote, tabWidth, useTabs }: ParserOptions, + { printWidth, singleQuote, tabWidth, useTabs }: ParserOptions, print: (path: FastPath) => Doc ): Doc { const tokens: Token[] = path.stack[0]; @@ -90,10 +105,15 @@ export const plugin: Plugin = { } let pipelessText: boolean = false; + let startTagPosition: number = 0; + let startAttributePosition: number = 0; + let previousAttributeRemapped: boolean = false; + let wrapAttributes: boolean = false; + for (let index: number = 0; index < tokens.length; index++) { const token: Token = tokens[index]; - const previousToken = tokens[index - 1]; - const nextToken = tokens[index + 1]; + const previousToken: Token | undefined = tokens[index - 1]; + const nextToken: Token | undefined = tokens[index + 1]; logger.debug('[printers:pug-ast:print]:', JSON.stringify(token)); switch (token.type) { case 'tag': @@ -111,10 +131,26 @@ export const plugin: Plugin = { if (!(token.val === 'div' && (nextToken.type === 'class' || nextToken.type === 'id'))) { result += token.val; } + startTagPosition = result.length; break; case 'start-attributes': if (nextToken && nextToken.type === 'attribute') { + previousAttributeRemapped = false; + startAttributePosition = result.length; result += '('; + const start: number = result.lastIndexOf('\n') + 1; + let lineLength: number = result.substring(start).length; + console.info(lineLength, printWidth); + let tempToken: AttributeToken | EndAttributesToken = nextToken; + let tempIndex: number = index + 1; + while (tempToken.type === 'attribute') { + lineLength += tempToken.name.length + 1 + tempToken.val.toString().length; + console.info(lineLength, printWidth); + tempToken = tokens[++tempIndex] as AttributeToken | EndAttributesToken; + } + if (lineLength > printWidth) { + wrapAttributes = true; + } } break; case 'attribute': @@ -137,14 +173,17 @@ export const plugin: Plugin = { continue; } // Write css-class in front of attributes - const position = result.lastIndexOf('('); + const position: number = startAttributePosition; result = [result.slice(0, position), `.${className}`, result.slice(position)].join( '' ); + startAttributePosition += 1 + className.length; } if (specialClasses.length > 0) { token.val = makeString(specialClasses.join(' '), singleQuote ? "'" : '"', false); + previousAttributeRemapped = false; } else { + previousAttributeRemapped = true; break; } } else if ( @@ -157,29 +196,35 @@ export const plugin: Plugin = { val = val.substring(1, val.length - 1); val = val.trim(); // Write css-id in front of css-classes - let lastPositionOfNewline = result.lastIndexOf('\n'); - if (lastPositionOfNewline === -1) { - // If no newline was found, set position to zero - lastPositionOfNewline = 0; - } - let position: number = result.indexOf('.', lastPositionOfNewline); - const firstPositionOfStartAttributes: number = result.indexOf( - '(', - lastPositionOfNewline - ); - if ( - position === -1 || - (firstPositionOfStartAttributes !== -1 && position > firstPositionOfStartAttributes) - ) { - position = firstPositionOfStartAttributes; - } + const position: number = startTagPosition; result = [result.slice(0, position), `#${val}`, result.slice(position)].join(''); + startAttributePosition += 1 + val.length; result = result.replace(/div#/, '#'); + if (previousToken.type === 'attribute' && previousToken.name !== 'class') { + previousAttributeRemapped = true; + } break; } - if (previousToken && previousToken.type === 'attribute') { - result += ', '; + const hasNormalPreviousToken: AttributeToken | undefined = previousNormalAttributeToken( + tokens, + index + ); + if ( + previousToken && + previousToken.type === 'attribute' && + (!previousAttributeRemapped || hasNormalPreviousToken) + ) { + result += ','; + if (!wrapAttributes) { + result += ' '; + } + } + previousAttributeRemapped = false; + + if (wrapAttributes) { + result += '\n'; + result += indent.repeat(indentLevel + 1); } result += `${token.name}`; @@ -202,16 +247,8 @@ export const plugin: Plugin = { // Swap single and double quotes val = val.replace(/['"]/g, (match) => (match === '"' ? "'" : '"')); } - } else if (val.startsWith("'")) { - if (!singleQuote) { - // Swap single and double quotes - val = val.replace(/['"]/g, (match) => (match === '"' ? "'" : '"')); - } - } else if (val.startsWith('"')) { - if (singleQuote) { - // Swap single and double quotes - val = val.replace(/['"]/g, (match) => (match === '"' ? "'" : '"')); - } + } else if (/^["'](.*)["']$/.test(val)) { + val = makeString(val.slice(1, -1), singleQuote ? "'" : '"', false); } else if (val === 'true') { // The value is exactly true and is not quoted break; @@ -226,6 +263,11 @@ export const plugin: Plugin = { } break; case 'end-attributes': + if (wrapAttributes) { + result += '\n'; + result += indent.repeat(indentLevel); + } + wrapAttributes = false; if (result.endsWith('(')) { // There were no attributes result = result.substring(0, result.length - 1); diff --git a/src/pug-token.ts b/src/pug-token.ts index 76143333..3f9d5ab8 100644 --- a/src/pug-token.ts +++ b/src/pug-token.ts @@ -14,7 +14,7 @@ export interface StartAttributesToken { loc: Loc; } -export interface Attribute { +export interface AttributeToken { type: 'attribute'; loc: Loc; name: string; @@ -152,7 +152,7 @@ export interface FilterToken { export type Token = | TagToken | StartAttributesToken - | Attribute + | AttributeToken | EndAttributesToken | IndentToken | ClassToken diff --git a/test/indents/formatted-2-spaces.pug b/test/indents/formatted-2-spaces.pug index 6fa2698f..7cb4ebb7 100644 --- a/test/indents/formatted-2-spaces.pug +++ b/test/indents/formatted-2-spaces.pug @@ -19,10 +19,22 @@ html .flex-box.header {{ $t('mylangkey.things') }} .flex-box.select(v-if="things[0].visible") - v-autocomplete(name="things[0].name", v-model="things[0].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[0].name", + v-model="things[0].value", + item-text="name", + item-value="id", + :items="mythings" + ) .flex-box.select(v-if="things[1].visible") - v-autocomplete(name="things[1].name", v-model="things[1].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[1].name", + v-model="things[1].value", + item-text="name", + item-value="id", + :items="mythings" + ) #use-of-id(test) span Hello diff --git a/test/indents/formatted-3-spaces.pug b/test/indents/formatted-3-spaces.pug index 668dbbf5..4221dc09 100644 --- a/test/indents/formatted-3-spaces.pug +++ b/test/indents/formatted-3-spaces.pug @@ -19,10 +19,22 @@ html .flex-box.header {{ $t('mylangkey.things') }} .flex-box.select(v-if="things[0].visible") - v-autocomplete(name="things[0].name", v-model="things[0].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[0].name", + v-model="things[0].value", + item-text="name", + item-value="id", + :items="mythings" + ) .flex-box.select(v-if="things[1].visible") - v-autocomplete(name="things[1].name", v-model="things[1].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[1].name", + v-model="things[1].value", + item-text="name", + item-value="id", + :items="mythings" + ) #use-of-id(test) span Hello diff --git a/test/indents/formatted-4-spaces.pug b/test/indents/formatted-4-spaces.pug index bd718611..d1620991 100644 --- a/test/indents/formatted-4-spaces.pug +++ b/test/indents/formatted-4-spaces.pug @@ -19,10 +19,22 @@ html .flex-box.header {{ $t('mylangkey.things') }} .flex-box.select(v-if="things[0].visible") - v-autocomplete(name="things[0].name", v-model="things[0].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[0].name", + v-model="things[0].value", + item-text="name", + item-value="id", + :items="mythings" + ) .flex-box.select(v-if="things[1].visible") - v-autocomplete(name="things[1].name", v-model="things[1].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[1].name", + v-model="things[1].value", + item-text="name", + item-value="id", + :items="mythings" + ) #use-of-id(test) span Hello diff --git a/test/indents/formatted-tabs.pug b/test/indents/formatted-tabs.pug index 1e781e2f..b9074cd7 100644 --- a/test/indents/formatted-tabs.pug +++ b/test/indents/formatted-tabs.pug @@ -19,10 +19,22 @@ html .flex-box.header {{ $t('mylangkey.things') }} .flex-box.select(v-if="things[0].visible") - v-autocomplete(name="things[0].name", v-model="things[0].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[0].name", + v-model="things[0].value", + item-text="name", + item-value="id", + :items="mythings" + ) .flex-box.select(v-if="things[1].visible") - v-autocomplete(name="things[1].name", v-model="things[1].value", item-text="name", item-value="id", :items="mythings") + v-autocomplete( + name="things[1].name", + v-model="things[1].value", + item-text="name", + item-value="id", + :items="mythings" + ) #use-of-id(test) span Hello diff --git a/test/literals/class-literals/formatted.pug b/test/literals/class-literals/formatted.pug index 868beb3a..bf5ebb21 100644 --- a/test/literals/class-literals/formatted.pug +++ b/test/literals/class-literals/formatted.pug @@ -13,7 +13,33 @@ div .content.text-success .content.text-success.bg-dark .content.text-success.bg-dark +.content.aaa.ad.text-success(:with="code()") .content.aaa.ad.text-success.bg-dark(class="#ad") +.content.aaa.ad.text-success.bg-dark( + other, + attributes="with many long text", + class="#ad", + so="it", + will="wrap", + the="line" +) +.content.aaa.ad.text-success.bg-dark( + another="long line", + :but="withCode()", + class="#ad" +) - const classes = ['foo', 'bar', 'baz'] a.bing.fizz(class=classes) a.bang.fizz.bib(class=classes, class=['bing']) + +v-text-field.search( + :label="$t('search.label')", + prepend-inner-icon="search", + v-model="search", + single-line +) + +span#testid.testclass.ab.cd.ef.hi.jk( + other="attribute", + other2="attribute2" +) diff --git a/test/literals/class-literals/unformatted.pug b/test/literals/class-literals/unformatted.pug index 0ba9dd50..90ce1e28 100644 --- a/test/literals/class-literals/unformatted.pug +++ b/test/literals/class-literals/unformatted.pug @@ -13,7 +13,14 @@ div.content(class="text-success bg-dark") .content(class="text-success") .content(class="text-success bg-dark") .content(class="text-success bg-dark") +.content.aaa.ad(:with="code()", class="text-success") .content.aaa.ad(class="text-success #ad bg-dark") +.content.aaa.ad(other, attributes="with many long text", class="text-success #ad bg-dark", so="it", will="wrap", the="line") +.content.aaa.ad(another="long line", :but="withCode()", class="text-success #ad bg-dark") - const classes = ['foo', 'bar', 'baz'] a(class=classes, class="bing", class="fizz") a.bang(class=classes, class=['bing'], class="fizz bib") + +v-text-field(:label="$t('search.label')", class="search", prepend-inner-icon="search", v-model="search", single-line) + +span.testclass(class="ab", id="testid", class="cd", other="attribute", class="ef", class="hi", class="jk", other2="attribute2") diff --git a/test/literals/id-literals/formatted.pug b/test/literals/id-literals/formatted.pug index 429d483e..7fbe8b11 100644 --- a/test/literals/id-literals/formatted.pug +++ b/test/literals/id-literals/formatted.pug @@ -3,8 +3,22 @@ a#main-link #content span#testid.testclass +span#testid.testclass.ab.cd +span#testid.testclass.ab.cd(other="attribute") +span#testid.testclass.ab.cd(other="attribute") +span#testid.testclass.ab.cd( + other="attribute", + with="many", + many="attributes", + in="this line" +) span#testid.testclass span#testid.testclass span#testid.testclass.testclass2 -v-text-field#search(:label="$t('search.label')", prepend-inner-icon="search", v-model="search", single-line) +v-text-field#search( + :label="$t('search.label')", + prepend-inner-icon="search", + v-model="search", + single-line +) diff --git a/test/literals/id-literals/unformatted.pug b/test/literals/id-literals/unformatted.pug index 2ca1f48c..5990c460 100644 --- a/test/literals/id-literals/unformatted.pug +++ b/test/literals/id-literals/unformatted.pug @@ -3,6 +3,10 @@ a#main-link div#content span.testclass(id="testid") +span.testclass(id="testid", class="ab cd") +span.testclass(id="testid", class="ab cd", other="attribute") +span.testclass(class="ab", id="testid", class="cd", other="attribute") +span.testclass(id="testid", class="ab cd", other="attribute" with="many", many="attributes", in="this line") span#testid.testclass span.testclass#testid span.testclass#testid(class="testclass2") diff --git a/test/quotes/double-to-single/formatted.pug b/test/quotes/double-to-single/formatted.pug index 05a6c0e3..8517c038 100644 --- a/test/quotes/double-to-single/formatted.pug +++ b/test/quotes/double-to-single/formatted.pug @@ -2,4 +2,11 @@ .a(color='primary') .grey--text {{ $t("my-language-key.title") }} - v-text-field(:label='$t("my-language-key")', type='text', name='aName', v-model='model', :rules='[(model.values !== null && model.values.length <= 15) || $t("my-other-language-key.error")]', :disabled='invalid') + v-text-field( + :label='$t("my-language-key")', + type='text', + name='aName', + v-model='model', + :rules='[(model.values !== null && model.values.length <= 15) || $t("my-other-language-key.error")]', + :disabled='invalid' + ) diff --git a/test/quotes/single-to-double/formatted.pug b/test/quotes/single-to-double/formatted.pug index 8d18895c..77876731 100644 --- a/test/quotes/single-to-double/formatted.pug +++ b/test/quotes/single-to-double/formatted.pug @@ -2,4 +2,11 @@ .a(color="primary") .grey--text {{ $t('my-language-key.title') }} - v-text-field(:label="$t('my-language-key')", type="text", name="aName", v-model="model", :rules="[(model.values !== null && model.values.length <= 15) || $t('my-other-language-key.error')]", :disabled="invalid") + v-text-field( + :label="$t('my-language-key')", + type="text", + name="aName", + v-model="model", + :rules="[(model.values !== null && model.values.length <= 15) || $t('my-other-language-key.error')]", + :disabled="invalid" + )