import type { BuiltInParserName , Options , RequiredOptions } from 'prettier' ; import { format } from 'prettier' ; import type { parsers as AngularParsers } from 'prettier/parser-angular' ; import type { parsers as BabelParsers } from 'prettier/parser-babel' ; import type { parsers as HtmlParsers } from 'prettier/parser-html' ; import type { parsers as PostCssParsers } from 'prettier/parser-postcss' ; import type { AndAttributesToken , AttributeToken , BlockcodeToken , BlockToken , CallToken , CaseToken , ClassToken , CodeToken , ColonToken , CommentToken , DefaultToken , DoctypeToken , DotToken , EachOfToken , EachToken , ElseIfToken , ElseToken , EndAttributesToken , EndPipelessTextToken , EndPugInterpolationToken , EosToken , ExtendsToken , FilterToken , IdToken , IfToken , IncludeToken , IndentToken , InterpolatedCodeToken , InterpolationToken , LexTokenType , MixinBlockToken , MixinToken , NewlineToken , OutdentToken , PathToken , SlashToken , StartAttributesToken , StartPipelessTextToken , StartPugInterpolationToken , TagToken , TextHtmlToken , TextToken , Token , WhenToken , WhileToken , YieldToken , } from 'pug-lexer' ; import { types } from 'util' ; import type { DoctypeShortcut } from './doctype-shortcut-registry' ; import { DOCTYPE_SHORTCUT_REGISTRY } from './doctype-shortcut-registry' ; import type { Logger } from './logger' ; import { createLogger , LogLevel } from './logger' ; import type { PugSortAttributes } from './options/attribute-sorting' ; import { compareAttributeToken , partialSort , } from './options/attribute-sorting/utils' ; import type { ArrowParens } from './options/common' ; import type { PugEmptyAttributes , PugEmptyAttributesForceQuotes , } from './options/empty-attributes' ; import { formatEmptyAttribute } from './options/empty-attributes/utils' ; import type { PugAttributeSeparator } from './options/pug-attribute-separator' ; import { resolvePugAttributeSeparatorOption } from './options/pug-attribute-separator' ; import type { PugClassLocation } from './options/pug-class-location' ; import type { PugClassNotation } from './options/pug-class-notation' ; import type { PugCommentPreserveSpaces } from './options/pug-comment-preserve-spaces' ; import { formatPugCommentPreserveSpaces } from './options/pug-comment-preserve-spaces' ; 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 , isQuoted , isSingleLineWithInterpolation , isStyleAttribute , makeString , previousNormalAttributeToken , previousTagToken , previousTypeAttributeToken , unwrapLineFeeds , } from './utils/common' ; import { getScriptParserName } from './utils/script-mime-types' ; import { isSvelteInterpolation } from './utils/svelte' ; import { isVueEventBinding , isVueExpression , isVueVBindExpression , isVueVForWithOf , isVueVOnExpression , } from './utils/vue' ; const logger = createLogger ( console ) ; if ( process . env . NODE_ENV === 'test' ) { logger . setLogLevel ( LogLevel . DEBUG ) ; } /**
 * Options for `@prettier/plugin-pug`.
 */ export interface PugPrinterOptions { readonly printWidth : number ; readonly pugPrintWidth : number ; readonly singleQuote : boolean ; readonly pugSingleQuote : boolean ; readonly tabWidth : number ; readonly pugTabWidth : number ; readonly useTabs : boolean ; readonly pugUseTabs : boolean ; readonly bracketSpacing : boolean ; readonly pugBracketSpacing : boolean ; readonly arrowParens : ArrowParens ; readonly pugArrowParens : ArrowParens ; readonly semi : boolean ; readonly pugSemi : boolean ; readonly bracketSameLine : boolean ; readonly pugBracketSameLine : boolean ; readonly pugAttributeSeparator : PugAttributeSeparator ; readonly pugCommentPreserveSpaces : PugCommentPreserveSpaces ; readonly pugSortAttributes : PugSortAttributes ; readonly pugSortAttributesBeginning : string [ ] ; readonly pugSortAttributesEnd : string [ ] ; readonly pugWrapAttributesThreshold : number ; readonly pugWrapAttributesPattern : string ; readonly pugClassNotation : PugClassNotation ; readonly pugClassLocation : PugClassLocation ; readonly pugIdNotation : PugIdNotation ; readonly pugEmptyAttributes : PugEmptyAttributes ; readonly pugEmptyAttributesForceQuotes : PugEmptyAttributesForceQuotes ; readonly pugSingleFileComponentIndentation : boolean ; readonly pugFramework : PugFramework ; readonly pugExplicitDiv : boolean ; } /**
 * Format delegate Prettier options.
 */ interface FormatDelegatePrettierOptions { trimTrailingSemicolon ? : boolean ; } /**
 * Supported parsers for the `formatDelegatePrettier` function.
 */ type FormatDelegatePrettierSupportedParser = keyof Pick < typeof AngularParsers & typeof BabelParsers & typeof HtmlParsers & typeof PostCssParsers , 'css' | 'vue' | '__vue_event_binding' | '__vue_expression' | '__js_expression' | '__ng_binding' | '__ng_action' | '__ng_directive' > ; /**
 * The printer class.
 */ export class PugPrinter { private result : string = '' ; /**
     * The index of the current token inside the `tokens` array.
     */ private currentIndex : number = - 1 ; private currentLineLength : number = 0 ; private readonly indentString : string ; private indentLevel : number = 0 ; private readonly framework : PugFramework = 'auto' ; private readonly quotes : "'" | '"' ; private readonly otherQuotes : "'" | '"' ; private readonly alwaysUseAttributeSeparator : boolean ; private readonly neverUseAttributeSeparator : boolean ; private readonly wrapAttributesPattern : RegExp | null ; private readonly codeInterpolationOptions : Pick < RequiredOptions , 'singleQuote' | 'bracketSpacing' | 'arrowParens' | 'printWidth' | 'endOfLine' | 'useTabs' | 'tabWidth' | 'bracketSameLine' > ; private currentTagPosition : number = 0 ; private possibleIdPosition : number = 0 ; private possibleClassPosition : number = 0 ; private previousAttributeRemapped : boolean = false ; /**
     * Specifies whether attributes should be wrapped in a tag or not.
     */ private wrapAttributes : boolean = false ; private pipelessText : boolean = false ; private pipelessComment : boolean = false ; private currentlyInPugInterpolation : boolean = false ; private readonly classLiteralToAttribute : string [ ] = [ ] ; private readonly classLiteralAfterAttributes : string [ ] = [ ] ; param content param tokens param options public constructor ( private readonly content , private tokens , private readonly options ) { this . indentString = options . pugUseTabs ? '\t' : ' ' . repeat ( options . pugTabWidth ) ; if ( options . pugSingleFileComponentIndentation ) { this . indentLevel ++ ; } this . framework = options . pugFramework !== 'auto' ? options . pugFramework : detectFramework ( ) ; this . quotes = this . options . pugSingleQuote ? "'" : '"' ; this . otherQuotes = this . options . pugSingleQuote ? '"' : "'" ; const pugAttributeSeparator = resolvePugAttributeSeparatorOption ( options . pugAttributeSeparator ) ; this . alwaysUseAttributeSeparator = pugAttributeSeparator === 'always' ; this . neverUseAttributeSeparator = pugAttributeSeparator === 'none' ; const wrapAttributesPattern = options . pugWrapAttributesPattern ; this . wrapAttributesPattern = wrapAttributesPattern ? new RegExp ( wrapAttributesPattern ) : null ; this . codeInterpolationOptions = { singleQuote : options . pugSingleQuote ?? options . singleQuote , bracketSpacing : options . pugBracketSpacing ?? options . bracketSpacing , arrowParens : options . pugArrowParens ?? options . arrowParens , printWidth : 9000 , endOfLine : 'lf' , useTabs : options . pugUseTabs ?? options . useTabs , tabWidth : options . pugTabWidth ?? options . tabWidth , bracketSameLine : options . pugBracketSameLine ?? options . bracketSameLine , } ; } private get computedIndent ( ) : string { switch ( this . previousToken ?. type ) { case 'newline' : case 'outdent' : return this . indentString . repeat ( this . indentLevel ) ; case 'indent' : return this . indentString ; case 'start-pug-interpolation' : return '' ; } return this . options . pugSingleFileComponentIndentation ? this . indentString : '' ; } private get previousToken ( ) : Token | undefined { return this . tokens [ this . currentIndex - 1 ] ; } private get nextToken ( ) : Token | undefined { return this . tokens [ this . currentIndex + 1 ] ; } returns public build ( ) { if ( logger . isDebugEnabled ( ) ) { logger . debug ( '[PugPrinter]:' , JSON . stringify ( this . tokens ) ) ; } const results = [ ] ; if ( this . tokens [ 0 ] ?. type === 'text' ) { results . push ( '| ' ) ; } else if ( this . tokens [ 0 ] ?. type === 'eos' ) { return '' ; } let token = this . getNextToken ( ) ; while ( token ) { logger . debug ( '[PugPrinter]:' , JSON . stringify ( token ) ) ; try { switch ( token . type ) { case 'attribute' : case 'class' : case 'end-attributes' : case 'id' : case 'eos' : this . result = results . join ( '' ) ; this [ token . type ] ( token ) ; results . length = 0 ; results . push ( this . result ) ; break ; case 'tag' : case 'start-attributes' : case 'interpolation' : case 'call' : case ':' : this . result = results . join ( '' ) ; default : { if ( typeof this [ token . type ] !== 'function' ) { throw new Error ( 'Unhandled token: ' + JSON . stringify ( token ) ) ; } results . push ( this [ token . type ] ( token ) ) ; break ; } } } catch ( error ) { throw new Error ( error ) ; } token = this . getNextToken ( ) ; } return results . join ( '' ) ; } private getNextToken ( ) { this . currentIndex ++ ; return this . tokens [ this . currentIndex ] ?? null ; } private quoteString ( val ) { return `${ this . quotes }${ val }${ this . quotes }` ; } private checkTokenType ( token , possibilities , invert = false ) { return ! ! token && possibilities . includes ( token . type ) !== invert ; } private tokenNeedsSeparator ( token ) { return this . neverUseAttributeSeparator ? false : this . alwaysUseAttributeSeparator || /^(\(|\[|:).*/ . test ( token . name ) ; } private getUnformattedContentLines ( firstToken , lastToken ) { const { start } = firstToken . loc ; const { end } = lastToken . loc ; const lines = this . content . split ( /\r\n|\n|\r/ ) ; const startLine = start . line - 1 ; const endLine = end . line - 1 ; const parts = [ ] ; const firstLine = lines [ startLine ] ; if ( firstLine !== undefined ) { parts . push ( firstLine . slice ( start . column - 1 ) ) ; } for ( let lineNumber = startLine + 1 ; lineNumber < endLine ; lineNumber ++ ) { const line = lines [ lineNumber ] ; if ( line !== undefined ) { parts . push ( line ) ; } } const lastLine = lines [ endLine ] ; if ( lastLine !== undefined ) { parts . push ( lastLine . slice ( 0 , end . column - 1 ) ) ; } return parts ; } private replaceTagWithLiteralIfPossible ( search , replace ) { if ( this . options . pugExplicitDiv ) { return ; } const currentTagEnd = Math . max ( this . possibleIdPosition , this . possibleClassPosition ) ; const tag = this . result . slice ( this . currentTagPosition , currentTagEnd ) ; const replaced = tag . replace ( search , replace ) ; if ( replaced !== tag ) { const prefix = this . result . slice ( 0 , this . currentTagPosition ) ; const suffix = this . result . slice ( currentTagEnd ) ; this . result = `${ prefix }${ replaced }${ suffix }` ; const diff = tag . length - replaced . length ; this . possibleIdPosition -= diff ; this . possibleClassPosition -= diff ; } } private frameworkFormat ( code ) { const options = { ... this . codeInterpolationOptions } ; switch ( this . framework ) { case 'angular' : options . parser = '__ng_interpolation' ; break ; case 'svelte' : case 'vue' : default : options . parser = 'babel' ; options . semi = false ; } let result = format ( code , options ) ; if ( result [ 0 ] === ';' ) { result = result . slice ( 1 ) ; } return result ; } private formatText ( text ) { let result = '' ; while ( text ) { const start = text . indexOf ( '{{' ) ; if ( start !== - 1 ) { result += text . slice ( 0 , start ) ; text = text . slice ( start + 2 ) ; const end = text . indexOf ( '}}' ) ; if ( end !== - 1 ) { let code = text . slice ( 0 , end ) ; try { const dangerousQuoteCombination = 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 ( end + 2 ) ; continue ; } else { code = this . frameworkFormat ( code ) ; } } catch ( error ) { if ( typeof error === 'string' ) { if ( error . includes ( 'Unexpected token Lexer Error' ) ) { if ( ! error . includes ( 'Unexpected character [`]' ) ) { logger . debug ( '[PugPrinter:formatText]: Using fallback strategy' ) ; } } else if ( error . includes ( 'Bindings cannot contain assignments' ) ) { logger . warn ( '[PugPrinter:formatText]: Bindings should not contain assignments:' , `code: \`${ code . trim ( ) }\`` ) ; } else if ( error . includes ( "Unexpected token '('" ) ) { if ( this . framework !== 'vue' ) { logger . warn ( '[PugPrinter:formatText]: Found unexpected token `(`.' , `code: \`${ code . trim ( ) }\`` ) ; } } else if ( error . includes ( 'Missing expected `)`' ) ) { if ( this . framework !== 'vue' ) { logger . warn ( '[PugPrinter:formatText]: Missing expected `)`.' , `code: \`${ code . trim ( ) }\`` ) ; } } else if ( error . includes ( 'Missing expected `:`' ) ) { if ( this . framework !== 'vue' ) { logger . warn ( '[PugPrinter:formatText]: Missing expected `:`.' , `code: \`${ code . trim ( ) }\`` ) ; } } else { logger . warn ( '[PugPrinter:formatText]: ' , error ) ; } } else { logger . warn ( '[PugPrinter:formatText]: ' , error ) ; } try { code = format ( code , { parser : 'babel' , ... this . codeInterpolationOptions , semi : false , } ) ; if ( code [ 0 ] === ';' ) { code = code . slice ( 1 ) ; } } catch ( error ) { logger . warn ( error ) ; } } code = unwrapLineFeeds ( code ) ; result += handleBracketSpacing ( this . options . pugBracketSpacing , code ) ; text = text . slice ( end + 2 ) ; } else { result += '{{' ; result += text ; text = '' ; } } else { const start2 = text . indexOf ( '{' ) ; if ( this . options . pugFramework === 'svelte' && start2 !== - 1 ) { result += text . slice ( 0 , start2 ) ; text = text . slice ( start2 + 1 ) ; const end2 = text . indexOf ( '}' ) ; if ( end2 !== - 1 ) { let code = text . slice ( 0 , end2 ) ; try { const dangerousQuoteCombination = 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 ) { logger . warn ( '[PugPrinter:formatText]: ' , error ) ; try { code = format ( code , { parser : 'babel' , ... this . codeInterpolationOptions , semi : false , } ) ; if ( code [ 0 ] === ';' ) { code = code . slice ( 1 ) ; } } catch ( error ) { 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 ; } private formatDelegatePrettier ( val , parser , { trimTrailingSemicolon = false } = { } ) { val = val . trim ( ) ; const options = { ... this . codeInterpolationOptions } ; const wasQuoted = isQuoted ( val ) ; if ( wasQuoted ) { options . singleQuote = ! this . options . pugSingleQuote ; val = val . slice ( 1 , - 1 ) ; } val = format ( val , { parser , ... options } ) ; if ( this . quotes === '"' ) { val = val . replace ( /"/g , '\\"' ) ; } else { val = val . replace ( /'/g , "\\'" ) ; } val = unwrapLineFeeds ( val ) ; if ( trimTrailingSemicolon && val [ val . length - 1 ] === ';' ) { val = val . slice ( 0 , - 1 ) ; } return wasQuoted ? this . quoteString ( val ) : val ; } private formatStyleAttribute ( val ) { return this . formatDelegatePrettier ( val , 'css' , { trimTrailingSemicolon : true , } ) ; } private formatVueEventBinding ( val ) { return this . formatDelegatePrettier ( val , '__vue_event_binding' , { trimTrailingSemicolon : true , } ) ; } private formatVueExpression ( val ) { return this . formatDelegatePrettier ( val , '__vue_expression' ) ; } private formatAngularBinding ( val ) { return this . formatDelegatePrettier ( val , '__ng_binding' ) ; } private formatAngularAction ( val ) { return this . formatDelegatePrettier ( val , '__ng_action' ) ; } private formatAngularDirective ( val ) { return this . formatDelegatePrettier ( val , '__ng_directive' ) ; } private formatFrameworkInterpolation ( val , parser , [ opening , closing ] ) { val = val . slice ( 1 , - 1 ) ; val = val . slice ( opening . length , - closing . length ) ; 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 { const options = { ... this . codeInterpolationOptions , singleQuote : ! this . options . pugSingleQuote , } ; val = format ( val , { parser , ... options } ) ; val = unwrapLineFeeds ( val ) ; } val = handleBracketSpacing ( this . options . pugBracketSpacing , val , [ opening , closing , ] ) ; return this . quoteString ( val ) ; } private formatAngularInterpolation ( val ) { return this . formatFrameworkInterpolation ( val , '__ng_interpolation' , [ '{{' , '}}' , ] ) ; } private formatSvelteInterpolation ( val ) { return this . formatFrameworkInterpolation ( val , '__ng_interpolation' , [ '{' , '}' , ] ) ; } private tag ( token ) { let val = token . val ; if ( val === 'div' && ! this . options . pugExplicitDiv && this . nextToken && ( ( this . nextToken . type === 'class' && this . options . pugClassLocation === 'before-attributes' ) || this . nextToken . type === 'id' ) ) { val = '' ; } this . currentLineLength += val . length ; const result = `${ this . computedIndent }${ val }` ; logger . debug ( 'tag' , { result , val : token . val , length : token . val . length } , this . currentLineLength ) ; this . currentTagPosition = this . result . length + this . computedIndent . length ; this . possibleIdPosition = this . result . length + result . length ; this . possibleClassPosition = this . result . length + result . length ; return result ; } private [ 'start-attributes' ] ( token ) { let result = '' ; if ( this . nextToken ?. type === 'attribute' ) { this . previousAttributeRemapped = false ; this . possibleClassPosition = this . result . length ; result = '(' ; logger . debug ( this . currentLineLength ) ; let tempToken = this . nextToken ; let tempIndex = this . currentIndex + 1 ; let hasLiteralAttributes = false ; let numNormalAttributes = 0 ; while ( tempToken . type === 'attribute' ) { if ( ! this . currentlyInPugInterpolation && ! this . wrapAttributes && this . wrapAttributesPattern ?. test ( tempToken . name ) ) { this . wrapAttributes = true ; } switch ( tempToken . name ) { case 'class' : case 'id' : { hasLiteralAttributes = true ; const val = tempToken . val . toString ( ) ; if ( isQuoted ( val ) ) { this . currentLineLength -= 2 ; } this . currentLineLength += 1 + val . length ; logger . debug ( { tokenName : tempToken . name , length : tempToken . name . length , } , this . currentLineLength ) ; break ; } default : { this . currentLineLength += tempToken . name . length ; if ( numNormalAttributes > 0 ) { this . currentLineLength += 1 ; if ( this . tokenNeedsSeparator ( tempToken ) ) { this . currentLineLength += 1 ; } } logger . debug ( { tokenName : tempToken . name , length : tempToken . name . length , } , this . currentLineLength ) ; const val = tempToken . val . toString ( ) ; if ( val . length > 0 && val !== 'true' ) { this . currentLineLength += 1 + val . length ; logger . debug ( { tokenVal : val , length : val . length } , this . currentLineLength ) ; } numNormalAttributes ++ ; break ; } } tempToken = this . tokens [ ++ tempIndex ] as AttributeToken | EndAttributesToken ; } logger . debug ( 'after token' , this . currentLineLength ) ; if ( hasLiteralAttributes ) { if ( this . previousToken ?. type === 'tag' && this . previousToken . val === 'div' && ! this . options . pugExplicitDiv ) { this . currentLineLength -= 3 ; } } if ( numNormalAttributes > 0 ) { this . currentLineLength += 2 ; } if ( this . options . pugClassLocation === 'after-attributes' ) { let tempClassIndex = tempIndex ; let tempClassToken = this . tokens [ ++ tempClassIndex ] as EndAttributesToken | ClassToken ; while ( tempClassToken . type === 'class' ) { const val = tempClassToken . val . toString ( ) ; this . currentLineLength += 1 + val . length ; logger . debug ( { tokenVal : val , length : val . length } , this . currentLineLength ) ; tempClassToken = this . tokens [ ++ tempClassIndex ] as EndAttributesToken | ClassToken ; } } logger . debug ( 'final line length' , { currentLineLength : this . currentLineLength , } ) ; if ( ! this . currentlyInPugInterpolation && ! this . wrapAttributes && ( this . currentLineLength > this . options . pugPrintWidth || ( this . options . pugWrapAttributesThreshold >= 0 && numNormalAttributes > this . options . pugWrapAttributesThreshold ) ) ) { this . wrapAttributes = true ; } if ( this . options . pugSortAttributes !== 'as-is' || this . options . pugSortAttributesEnd . length > 0 || this . options . pugSortAttributesBeginning . length > 0 ) { const startAttributesIndex = this . tokens . indexOf ( token ) ; const endAttributesIndex = tempIndex ; if ( endAttributesIndex - startAttributesIndex > 2 ) { this . tokens = partialSort ( this . tokens , startAttributesIndex + 1 , endAttributesIndex , ( a , b ) => compareAttributeToken ( a as AttributeToken , b as AttributeToken , this . options . pugSortAttributes , this . options . pugSortAttributesBeginning , this . options . pugSortAttributesEnd ) ) ; } } } return result ; } private attribute ( token ) { formatEmptyAttribute ( token , this . options . pugEmptyAttributes , this . options . pugEmptyAttributesForceQuotes ) ; if ( typeof token . val === 'string' ) { if ( isQuoted ( token . val ) && token . val [ 0 ] !== '`' ) { if ( token . name === 'class' && this . options . pugClassNotation === 'literal' ) { const val = token . val . slice ( 1 , - 1 ) . trim ( ) ; const classes = val . split ( /\s+/ ) ; const specialClasses = [ ] ; const normalClasses = [ ] ; const validClassNameRegex = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/ ; for ( const className of classes ) { if ( ! validClassNameRegex . test ( className ) ) { specialClasses . push ( className ) ; } else { if ( this . options . pugClassLocation === 'after-attributes' ) { this . classLiteralAfterAttributes . push ( className ) ; } else { normalClasses . push ( className ) ; } } } if ( normalClasses . length > 0 ) { const position = this . possibleClassPosition ; this . result = [ this . result . slice ( 0 , position ) , '.' , normalClasses . join ( '.' ) , this . result . slice ( position ) , ] . join ( '' ) ; this . possibleClassPosition += 1 + normalClasses . join ( '.' ) . length ; if ( this . options . pugClassLocation === 'before-attributes' ) { this . replaceTagWithLiteralIfPossible ( /div\./ , '.' ) ; } } if ( specialClasses . length > 0 ) { token . val = makeString ( specialClasses . join ( ' ' ) , this . quotes ) ; this . previousAttributeRemapped = false ; } else { this . previousAttributeRemapped = true ; return ; } } else if ( token . name === 'id' && this . options . pugIdNotation !== 'as-is' ) { let val = token . val ; val = val . slice ( 1 , - 1 ) ; val = val . trim ( ) ; const validIdNameRegex = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/ ; if ( ! validIdNameRegex . test ( val ) ) { val = makeString ( val , this . quotes ) ; this . result += 'id' ; if ( token . mustEscape === false ) { this . result += '!' ; } this . result += `=${ val }` ; return ; } const position = this . possibleIdPosition ; const literal = `#${ val }` ; this . result = [ this . result . slice ( 0 , position ) , literal , this . result . slice ( position ) , ] . join ( '' ) ; this . possibleClassPosition += literal . length ; this . replaceTagWithLiteralIfPossible ( /div#/ , '#' ) ; this . previousAttributeRemapped = true ; return ; } } } const hasNormalPreviousToken = previousNormalAttributeToken ( this . tokens , this . currentIndex ) ; if ( this . previousToken ?. type === 'attribute' && ( ! this . previousAttributeRemapped || hasNormalPreviousToken ) ) { if ( this . tokenNeedsSeparator ( token ) ) { this . result += ',' ; } if ( ! this . wrapAttributes ) { this . result += ' ' ; } } this . previousAttributeRemapped = false ; if ( this . wrapAttributes ) { this . result += '\n' ; this . result += this . indentString . repeat ( this . indentLevel + 1 ) ; } this . result += `${ token . name }` ; if ( typeof token . val === 'boolean' ) { if ( token . val !== true ) { this . result += `=${ token . val }` ; } } else if ( token . name === 'class' && this . options . pugClassNotation === 'attribute' ) { const val = isQuoted ( token . val ) ? token . val . slice ( 1 , - 1 ) . trim ( ) : token . val ; const classes = val . split ( /\s+/ ) ; if ( this . classLiteralToAttribute . length ) { for ( let i = this . classLiteralToAttribute . length - 1 ; i > - 1 ; i -- ) { const className = this . classLiteralToAttribute . splice ( i , 1 ) [ 0 ] ; if ( className ) { classes . unshift ( className ) ; } } } this . result += `=${ isQuoted ( token . val ) ? this . quoteString ( classes . join ( ' ' ) ) : classes . join ( ' ' ) }` ; } else { let val = token . val ; if ( isMultilineInterpolation ( val ) ) { } else if ( isSingleLineWithInterpolation ( val ) ) { } else if ( isVueVForWithOf ( token . name , val ) ) { val = this . formatDelegatePrettier ( val , 'vue' ) ; } else if ( isVueExpression ( token . name ) ) { val = this . formatVueExpression ( val ) ; } else if ( isVueEventBinding ( token . name ) ) { val = this . formatVueEventBinding ( val ) ; } else if ( isVueVBindExpression ( token . name ) ) { val = this . formatDelegatePrettier ( val , '__js_expression' ) ; } else if ( isVueVOnExpression ( token . name ) ) { val = this . formatDelegatePrettier ( val , '__js_expression' ) ; } else if ( isAngularBinding ( token . name ) ) { val = this . formatAngularBinding ( val ) ; } else if ( isAngularAction ( token . name ) ) { val = this . formatAngularAction ( val ) ; } else if ( isAngularDirective ( token . name ) ) { 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 { const rightTrimmedVal = val . trimRight ( ) ; if ( isQuoted ( rightTrimmedVal ) ) { val = makeString ( rightTrimmedVal . slice ( 1 , - 1 ) , this . quotes ) ; } else if ( val === 'true' ) { return ; } else if ( token . mustEscape ) { val = format ( val , { parser : '__js_expression' , ... this . codeInterpolationOptions , } ) ; const lines = val . split ( '\n' ) ; const codeIndentLevel = this . wrapAttributes ? this . indentLevel + 1 : this . indentLevel ; if ( lines . length > 1 ) { val = lines [ 0 ] ?? '' ; for ( let index = 1 ; index < lines . length ; index ++ ) { val += '\n' ; val += this . indentString . repeat ( codeIndentLevel ) ; val += lines [ index ] ; } } } else { val = val . trim ( ) ; val = val . replace ( /\s\s+/g , ' ' ) ; if ( val [ 0 ] === '{' && val [ 1 ] === ' ' ) { val = `{${ val . slice ( 2 , val . length ) }` ; } } } if ( token . mustEscape === false ) { this . result += '!' ; } this . result += `=${ val }` ; } } private [ 'end-attributes' ] ( token ) { if ( this . wrapAttributes && this . result [ this . result . length - 1 ] !== '(' ) { if ( ! this . options . pugBracketSameLine ) { this . result += '\n' ; } this . result += this . indentString . repeat ( this . indentLevel ) ; } this . wrapAttributes = false ; if ( this . classLiteralToAttribute . length ) { if ( this . previousToken ?. type === 'start-attributes' ) { this . result += '(' ; } else if ( this . previousToken ?. type === 'attribute' ) { this . result += ' ' ; } const classes = this . classLiteralToAttribute . splice ( 0 , this . classLiteralToAttribute . length ) ; this . result += `class=${ this . quoteString ( classes . join ( ' ' ) ) }` ; if ( this . previousToken ?. type === 'start-attributes' ) { this . result += ')' ; } } if ( this . result [ this . result . length - 1 ] === '(' ) { this . result = this . result . slice ( 0 , - 1 ) ; } else if ( this . previousToken ?. type === 'attribute' ) { if ( this . options . pugBracketSameLine ) { this . result = this . result . trimRight ( ) ; } this . result += ')' ; } if ( this . result [ this . result . length - 1 ] === ')' && this . classLiteralAfterAttributes . length > 0 ) { const classes = this . classLiteralAfterAttributes . splice ( 0 , this . classLiteralAfterAttributes . length ) ; this . result += `.${ classes . join ( '.' ) }` ; } if ( this . options . pugClassLocation === 'after-attributes' ) { this . possibleClassPosition = this . result . length ; } if ( this . nextToken ?. type === 'text' || this . nextToken ?. type === 'path' ) { this . result += ' ' ; } } private indent ( token ) { const result = `\n${ this . indentString . repeat ( this . indentLevel ) }` ; this . indentLevel ++ ; this . currentLineLength = result . length - 1 + 1 + this . options . pugTabWidth ; logger . debug ( 'indent' , { result , indentLevel : this . indentLevel , pugTabWidth : this . options . pugTabWidth , } , this . currentLineLength ) ; return result ; } private outdent ( token ) { let result = '' ; if ( this . previousToken && this . previousToken . type !== 'outdent' ) { if ( token . loc . start . line - this . previousToken . loc . end . line > 1 ) { result += '\n' ; } result += '\n' ; } this . indentLevel -- ; this . currentLineLength = 1 + this . indentString . repeat ( this . indentLevel ) . length ; logger . debug ( 'outdent' , { result , indentLevel : this . indentLevel } , this . currentLineLength ) ; return result ; } private class ( token ) { if ( this . options . pugClassNotation === 'attribute' ) { this . classLiteralToAttribute . push ( token . val ) ; if ( this . previousToken ?. type !== 'tag' && this . previousToken ?. type !== 'class' ) { this . result += 'div' ; } if ( this . nextToken && [ 'text' , 'newline' , 'indent' , 'eos' ] . includes ( this . nextToken ?. type ) ) { const classes = this . classLiteralToAttribute . splice ( 0 , this . classLiteralToAttribute . length ) ; this . result += `(class=${ this . quoteString ( classes . join ( ' ' ) ) })` ; if ( this . nextToken ?. type === 'text' ) { this . result += ' ' ; } } } else { const val = `.${ token . val }` ; this . currentLineLength += val . length ; logger . debug ( 'before class' , { result : this . result , val , length : val . length , previousToken : this . previousToken , } , this . currentLineLength ) ; switch ( this . previousToken ?. type ) { case undefined : case 'newline' : case 'outdent' : case 'indent' : { const optionalDiv = this . options . pugExplicitDiv || this . options . pugClassLocation === 'after-attributes' ? 'div' : '' ; let result = `${ this . computedIndent }${ optionalDiv }` ; if ( this . options . pugClassLocation === 'after-attributes' ) { this . classLiteralAfterAttributes . push ( val . slice ( 1 ) ) ; } else { result += val ; } this . currentLineLength += optionalDiv . length ; this . possibleIdPosition = this . result . length + this . computedIndent . length + optionalDiv . length ; this . result += result ; this . possibleClassPosition = this . result . length ; break ; } case 'end-attributes' : { const prefix = this . result . slice ( 0 , this . possibleClassPosition ) ; this . result = [ prefix , val , this . result . slice ( this . possibleClassPosition ) , ] . join ( '' ) ; this . possibleClassPosition += val . length ; break ; } default : { if ( this . options . pugClassLocation === 'after-attributes' ) { this . classLiteralAfterAttributes . push ( val . slice ( 1 ) ) ; } else { const prefix = this . result . slice ( 0 , this . possibleClassPosition ) ; this . result = [ prefix , val , this . result . slice ( this . possibleClassPosition ) , ] . join ( '' ) ; this . possibleClassPosition += val . length ; } break ; } } if ( this . options . pugClassLocation === 'after-attributes' && this . classLiteralAfterAttributes . length > 0 ) { let result = this . result . slice ( 0 , this . possibleClassPosition ) ; if ( [ 'text' , 'newline' , 'indent' , 'outdent' , 'eos' , 'code' , ':' , undefined , ] . includes ( this . nextToken ?. type ) ) { const classes = this . classLiteralAfterAttributes . splice ( 0 , this . classLiteralAfterAttributes . length ) ; result += '.' + classes . join ( '.' ) ; } this . result = [ result , this . result . slice ( this . possibleClassPosition ) , ] . join ( '' ) ; this . possibleClassPosition = this . result . length ; this . replaceTagWithLiteralIfPossible ( /div\./ , '.' ) ; } logger . debug ( 'after class' , { result : this . result , val , length : val . length } , this . currentLineLength ) ; if ( this . nextToken ?. type === 'text' ) { this . currentLineLength += 1 ; this . result += ' ' ; } } } private eos ( token ) { while ( this . result [ this . result . length - 1 ] === '\n' ) { this . result = this . result . slice ( 0 , - 1 ) ; } this . result += '\n' ; } private comment ( commentToken ) { let result = this . computedIndent ; if ( /^ prettier-ignore($|[: ])/ . test ( commentToken . val ) ) { let token = this . getNextToken ( ) ; if ( token ) { let skipNewline = token . type === 'newline' ; let ignoreLevel = 0 ; while ( token ) { const { type } = token ; if ( type === 'newline' && ignoreLevel === 0 ) { if ( skipNewline ) { skipNewline = false ; } else { break ; } } else if ( type === 'indent' ) { ignoreLevel ++ ; } else if ( type === 'outdent' ) { ignoreLevel -- ; if ( ignoreLevel <= 0 ) { if ( ignoreLevel < 0 ) { this . indentLevel -- ; } break ; } } token = this . getNextToken ( ) ; } if ( token ) { const lines = this . getUnformattedContentLines ( commentToken , token ) ; const lastLine = 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 += formatPugCommentPreserveSpaces ( commentToken . val , this . options . pugCommentPreserveSpaces ) ; if ( this . nextToken ?. type === 'start-pipeless-text' ) { this . pipelessComment = true ; } } return result ; } private newline ( token ) { let result = '' ; if ( this . previousToken && token . loc . start . line - this . previousToken . loc . end . line > 1 ) { result += '\n' ; } result += '\n' ; this . currentLineLength = 1 + this . indentString . repeat ( this . indentLevel ) . length ; logger . debug ( 'newline' , { result , indentLevel : this . indentLevel } , this . currentLineLength ) ; return result ; } private text ( token ) { let result = '' ; let val = token . val ; let needsTrailingWhitespace = false ; if ( this . pipelessText ) { switch ( this . previousToken ?. type ) { case 'newline' : if ( val . trim ( ) . length > 0 ) { result += this . indentString . repeat ( this . indentLevel + 1 ) ; } break ; case 'start-pipeless-text' : result += this . indentString ; break ; } if ( this . pipelessComment ) { val = formatPugCommentPreserveSpaces ( val , this . options . pugCommentPreserveSpaces , true ) ; } } else { if ( this . nextToken && val [ val . length - 1 ] === ' ' ) { switch ( this . nextToken . type ) { case 'interpolated-code' : case 'start-pug-interpolation' : needsTrailingWhitespace = true ; break ; } } val = val . replace ( /\s\s+/g , ' ' ) ; switch ( this . previousToken ?. type ) { case 'newline' : result += this . indentString . repeat ( this . indentLevel ) ; if ( /^ .+$/ . test ( val ) ) { result += '|\n' ; result += this . indentString . repeat ( this . indentLevel ) ; } result += '|' ; if ( /.*\S.*/ . test ( token . val ) || this . nextToken ?. type === 'start-pug-interpolation' ) { result += ' ' ; } break ; case 'indent' : case 'outdent' : result += this . computedIndent ; if ( /^ .+$/ . test ( val ) ) { result += '|\n' ; result += this . indentString . repeat ( this . indentLevel ) ; } result += '|' ; if ( /.*\S.*/ . test ( token . val ) ) { result += ' ' ; } break ; case 'interpolated-code' : case 'end-pug-interpolation' : if ( /^ .+$/ . test ( val ) ) { result += ' ' ; } break ; } val = val . trim ( ) ; val = this . formatText ( val ) ; val = val . replace ( /#(\{|\[)/g , '\\#$1' ) ; } if ( this . checkTokenType ( this . previousToken , [ 'tag' , 'id' , 'interpolation' , 'call' , '&attributes' , 'filter' , ] ) ) { val = ` ${ val }` ; } result += val ; if ( needsTrailingWhitespace ) { result += ' ' ; } return result ; } private [ 'interpolated-code' ] ( token ) { let result = '' ; switch ( this . previousToken ?. type ) { case 'tag' : case 'class' : case 'id' : case 'end-attributes' : result = ' ' ; break ; case 'start-pug-interpolation' : result = '| ' ; break ; case 'indent' : case 'newline' : case 'outdent' : result = this . computedIndent ; result += this . pipelessText ? this . indentString : '| ' ; break ; } result += token . mustEscape ? '#' : '!' ; result += handleBracketSpacing ( this . options . pugBracketSpacing , token . val . trim ( ) , [ '{' , '}' ] ) ; return result ; } private code ( token ) { let result = this . computedIndent ; if ( ! token . mustEscape && token . buffer ) { result += '!' ; } result += token . buffer ? '=' : '-' ; let useSemi = this . options . pugSemi ; if ( useSemi && ( token . mustEscape || token . buffer ) ) { useSemi = false ; } let val = token . val ; try { const valBackup = val ; val = format ( val , { parser : 'babel' , ... this . codeInterpolationOptions , semi : useSemi , endOfLine : 'lf' , } ) ; val = val . slice ( 0 , - 1 ) ; if ( val [ 0 ] === ';' ) { val = val . slice ( 1 ) ; } if ( val . includes ( '\n' ) ) { val = valBackup ; } } catch ( error ) { logger . warn ( '[PugPrinter]:' , error ) ; } result += ` ${ val }` ; return result ; } private id ( token ) { const val = `#${ token . val }` ; this . currentLineLength += val . length ; switch ( this . previousToken ?. type ) { case undefined : case 'newline' : case 'outdent' : case 'indent' : { const optionalDiv = this . options . pugExplicitDiv ? 'div' : '' ; const result = `${ this . computedIndent }${ optionalDiv }${ val }` ; this . currentLineLength += optionalDiv . length ; this . result += result ; this . possibleClassPosition = this . result . length ; break ; } default : { const prefix = this . result . slice ( 0 , this . possibleIdPosition ) ; this . possibleClassPosition += val . length ; this . result = [ prefix , val , this . result . slice ( this . possibleIdPosition ) , ] . join ( '' ) ; break ; } } } private [ 'start-pipeless-text' ] ( token ) { this . pipelessText = true ; let result = `\n${ this . indentString . repeat ( this . indentLevel ) }` ; if ( this . previousToken ?. type === 'dot' ) { const lastTagToken = previousTagToken ( this . tokens , this . currentIndex ) ; let parser ; switch ( lastTagToken ?. val ) { case 'script' : parser = getScriptParserName ( previousTypeAttributeToken ( this . tokens , this . currentIndex ) ) ; break ; case 'style' : parser = 'css' ; break ; default : break ; } if ( parser ) { let index = this . currentIndex + 1 ; let tok = this . tokens [ index ] ; let rawText = '' ; let usedInterpolatedCode = false ; while ( tok && tok ?. type !== 'end-pipeless-text' ) { switch ( tok . type ) { case 'text' : rawText += tok . val ; break ; case 'newline' : rawText += '\n' ; break ; case 'interpolated-code' : usedInterpolatedCode = true ; rawText += tok . mustEscape ? '#' : '!' ; rawText += `{${ tok . val }}` ; break ; default : logger . warn ( '[PugPrinter:start-pipeless-text]:' , 'Unhandled token for pipeless script tag:' , JSON . stringify ( tok ) ) ; break ; } index ++ ; tok = this . tokens [ index ] ; } try { result = format ( rawText , { parser , ... this . codeInterpolationOptions , } ) ; } catch ( error ) { if ( ! usedInterpolatedCode ) { logger . error ( error ) ; throw error ; } const warningContext = [ '[PugPrinter:start-pipeless-text]:' , 'The following expression could not be formatted correctly.' , 'This is likely a syntax error or an issue caused by the missing execution context.' , 'If you think this is a bug, please open a bug issue.' , ] ; warningContext . push ( `\ncode: \`${ rawText . trim ( ) }\`` ) ; warningContext . push ( '\nYou used interpolated code in your pipeless script tag, so you may ignore this warning.' ) ; if ( types . isNativeError ( error ) ) { warningContext . push ( `\nFound ${ parser } ${ error . name }: ${ error . message }.` ) ; } else { logger . debug ( 'typeof error:' , typeof error ) ; warningContext . push ( `\nUnexpected error for parser ${ parser }.` , error as string ) ; } logger . warn ( ... warningContext ) ; result = rawText ; } result = result . trimRight ( ) ; const indentString = this . indentString . repeat ( this . indentLevel + 1 ) ; result = result . split ( '\n' ) . map ( ( line ) => ( line ? indentString + line : '' ) ) . join ( '\n' ) ; result = `\n${ result }` ; tok = this . tokens [ index - 1 ] ; if ( tok ?. type === 'text' && tok . val === '' ) { result += '\n' ; } this . currentIndex = index - 1 ; } } return result ; } private [ 'end-pipeless-text' ] ( token ) { this . pipelessText = false ; this . pipelessComment = false ; return '' ; } private doctype ( token ) { let result = `${ this . computedIndent }doctype` ; if ( token . val ) { result += ` ${ token . val }` ; } return result ; } private dot ( token ) { return '.' ; } private block ( token ) { let result = `${ this . computedIndent }block ` ; if ( token . mode !== 'replace' ) { result += `${ token . mode } ` ; } result += token . val ; return result ; } private extends ( token ) { const indent = this . options . pugSingleFileComponentIndentation ? this . indentString : '' ; return `${ indent }extends ` ; } private path ( token ) { let result = '' ; if ( this . checkTokenType ( this . previousToken , [ 'include' , 'filter' ] ) ) { result += ' ' ; } result += token . val ; return result ; } private [ 'start-pug-interpolation' ] ( token ) { let result = '' ; if ( this . tokens [ this . currentIndex - 2 ] ?. type === 'newline' && this . previousToken ?. type === 'text' && this . previousToken . val . trim ( ) . length === 0 ) { result += this . indentString . repeat ( this . indentLevel + 1 ) ; } this . currentlyInPugInterpolation = true ; result += '#[' ; return result ; } private [ 'end-pug-interpolation' ] ( token ) { this . currentlyInPugInterpolation = false ; return ']' ; } private interpolation ( token ) { const result = `${ this . computedIndent }#{${ token . val }}` ; this . currentLineLength += result . length ; this . possibleIdPosition = this . result . length + result . length ; this . possibleClassPosition = this . result . length + result . length ; return result ; } private include ( token ) { return `${ this . computedIndent }include` ; } private filter ( token ) { return `${ this . computedIndent }:${ token . val }` ; } private call ( token ) { let result = `${ this . computedIndent }+${ token . val }` ; let args = token . args ; if ( args ) { args = args . trim ( ) ; args = args . replace ( /\s\s+/g , ' ' ) ; result += `(${ args })` ; } this . currentLineLength += result . length ; this . possibleIdPosition = this . result . length + result . length ; this . possibleClassPosition = this . result . length + result . length ; return result ; } private mixin ( token ) { let result = `${ this . computedIndent }mixin ${ token . val }` ; let args = token . args ; if ( args ) { args = args . trim ( ) ; args = args . replace ( /\s\s+/g , ' ' ) ; result += `(${ args })` ; } return result ; } private if ( token ) { let result = this . computedIndent ; const match = /^!\((.*)\)$/ . exec ( token . val ) ; logger . debug ( '[PugPrinter]:' , match ) ; result += ! match ? `if ${ token . val }` : `unless ${ match [ 1 ] }` ; return result ; } private [ 'mixin-block' ] ( token ) { return `${ this . computedIndent }block` ; } private else ( token ) { return `${ this . computedIndent }else` ; } private [ '&attributes' ] ( token ) { const result = `&attributes(${ token . val })` ; this . currentLineLength += result . length ; return result ; } private [ 'text-html' ] ( token ) { const match = /^<(.*?)>(.*)<\/(.*?)>$/ . exec ( token . val ) ; logger . debug ( '[PugPrinter]:' , match ) ; if ( match ) { return `${ this . computedIndent }${ match [ 1 ] } ${ match [ 2 ] }` ; } const entry = Object . entries ( DOCTYPE_SHORTCUT_REGISTRY ) . find ( ( [ key ] ) => key === token . val . toLowerCase ( ) ) ; if ( entry ) { return `${ this . computedIndent }${ entry [ 1 ] }` ; } return `${ this . computedIndent }${ token . val }` ; } private each ( token ) { let result = `${ this . computedIndent }each ${ token . val }` ; if ( token . key !== null ) { result += `, ${ token . key }` ; } result += ` in ${ token . code }` ; return result ; } private eachOf ( token ) { let value = token . value . trim ( ) ; value = format ( value , { parser : 'babel' , ... this . codeInterpolationOptions , semi : false , } ) ; if ( value [ 0 ] === ';' ) { value = value . slice ( 1 ) ; } value = unwrapLineFeeds ( value ) ; const code = token . code . trim ( ) ; return `${ this . computedIndent }each ${ value } of ${ code }` ; } private while ( token ) { return `${ this . computedIndent }while ${ token . val }` ; } private case ( token ) { return `${ this . computedIndent }case ${ token . val }` ; } private when ( token ) { return `${ this . computedIndent }when ${ token . val }` ; } private [ ':' ] ( token ) { this . possibleIdPosition = this . result . length + 2 ; this . possibleClassPosition = this . result . length + 2 ; return ': ' ; } private default ( token ) { return `${ this . computedIndent }default` ; } private [ 'else-if' ] ( token ) { return `${ this . computedIndent }else if ${ token . val }` ; } private blockcode ( token ) { return `${ this . computedIndent }-` ; } private yield ( token ) { return `${ this . computedIndent }yield` ; } private slash ( token ) { let result = '/' ; if ( this . nextToken ?. type === 'text' ) { result += ' ' ; } return result ; } }