diff --git a/src/formatContentPipeline.ts b/src/formatContentPipeline.ts new file mode 100644 index 00000000..ec1a821a --- /dev/null +++ b/src/formatContentPipeline.ts @@ -0,0 +1,168 @@ +import type Formatter from "./formatter"; +import { AdjustSpacesProcessor } from "./processors/adjustSpacesProcessor"; +import { BladeBraceProcessor } from "./processors/bladeBraceProcessor"; +import { BladeCommentProcessor } from "./processors/bladeCommentProcessor"; +import { BladeDirectiveInScriptsProcessor } from "./processors/bladeDirectiveInScriptsProcessor"; +import { BladeDirectiveInStylesProcessor } from "./processors/bladeDirectiveInStylesProcessor"; +import { BreakLineBeforeAndAfterDirectiveProcessor } from "./processors/breakLineBeforeAndAfterDirectiveProcessor"; +import { ComponentAttributeProcessor } from "./processors/componentAttributeProcessor"; +import { ConditionsProcessor } from "./processors/conditionsProcessor"; +import { CurlyBraceForJSProcessor } from "./processors/curlyBraceForJSProcessor"; +import { CustomDirectiveProcessor } from "./processors/customDirectiveProcessor"; +import { EscapedBladeDirectiveProcessor } from "./processors/escapedBladeDirectiveProcessor"; +import { FormatAsPhpProcessor } from "./processors/fomatAsPhpProcessor"; +import { FormatAsBladeProcessor } from "./processors/formatAsBladeProcessor"; +import { FormatAsHtmlProcessor } from "./processors/formatAsHtmlProcessor"; +import { HtmlAttributesProcessor } from "./processors/htmlAttributesProcessor"; +import { HtmlTagsProcessor } from "./processors/htmlTagsProcessor"; +import { IgnoredLinesProcessor } from "./processors/ignoredLinesProcessor"; +import { InlineDirectiveProcessor } from "./processors/inlineDirectiveProcessor"; +import { InlinePhpDirectiveProcessor } from "./processors/inlinePhpDirectiveProcessor"; +import { NonnativeScriptsProcessor } from "./processors/nonnativeScriptsProcessor"; +import { PhpBlockProcessor } from "./processors/phpBlockProcessor"; +import type { Processor } from "./processors/processor"; +import { PropsProcessor } from "./processors/propsProcessor"; +import { RawBladeBraceProcessor } from "./processors/rawBladeBraceProcessor"; +import { RawPhpTagProcessor } from "./processors/rawPhpTagProcessor"; +import { ScriptsProcessor } from "./processors/scriptsProcessor"; +import { ShorthandBindingProcessor } from "./processors/shorthandBindingProcessor"; +import { SortHtmlAttributesProcessor } from "./processors/sortHtmlAttributesProcessor"; +import { SortTailwindClassesProcessor } from "./processors/sortTailwindClassesProcessor"; +import { UnbalancedDirectiveProcessor } from "./processors/unbalancedDirectiveProcessor"; +import { XDataProcessor } from "./processors/xdataProcessor"; +import { XInitProcessor } from "./processors/xinitProcessor"; +import { XslotProcessor } from "./processors/xslotProcessor"; + +export class FormatContentPipeline { + private processors: Processor[]; + + private preProcessors: (typeof Processor)[] = [ + IgnoredLinesProcessor, + NonnativeScriptsProcessor, + CurlyBraceForJSProcessor, + RawPhpTagProcessor, + EscapedBladeDirectiveProcessor, + FormatAsPhpProcessor, + BladeCommentProcessor, + BladeBraceProcessor, + RawBladeBraceProcessor, + ConditionsProcessor, + PropsProcessor, + InlinePhpDirectiveProcessor, + InlineDirectiveProcessor, + BladeDirectiveInScriptsProcessor, + BladeDirectiveInStylesProcessor, + CustomDirectiveProcessor, + UnbalancedDirectiveProcessor, + BreakLineBeforeAndAfterDirectiveProcessor, + ScriptsProcessor, + SortTailwindClassesProcessor, + XInitProcessor, + XDataProcessor, + PhpBlockProcessor, + SortHtmlAttributesProcessor, + HtmlAttributesProcessor, + ComponentAttributeProcessor, + ShorthandBindingProcessor, + XslotProcessor, + HtmlTagsProcessor, + FormatAsHtmlProcessor, + FormatAsBladeProcessor, + ]; + + private postProcessors: (typeof Processor)[] = [ + HtmlTagsProcessor, + XslotProcessor, + ShorthandBindingProcessor, + ComponentAttributeProcessor, + HtmlAttributesProcessor, + PhpBlockProcessor, + PropsProcessor, + XDataProcessor, + XInitProcessor, + ScriptsProcessor, + UnbalancedDirectiveProcessor, + CustomDirectiveProcessor, + BladeDirectiveInStylesProcessor, + BladeDirectiveInScriptsProcessor, + InlineDirectiveProcessor, + InlinePhpDirectiveProcessor, + ConditionsProcessor, + RawBladeBraceProcessor, + BladeBraceProcessor, + BladeCommentProcessor, + EscapedBladeDirectiveProcessor, + RawPhpTagProcessor, + CurlyBraceForJSProcessor, + NonnativeScriptsProcessor, + IgnoredLinesProcessor, + AdjustSpacesProcessor, + ]; + + constructor(private formatter: Formatter) { + this.processors = [ + new IgnoredLinesProcessor(formatter), + new NonnativeScriptsProcessor(formatter), + new CurlyBraceForJSProcessor(formatter), + new RawPhpTagProcessor(formatter), + new EscapedBladeDirectiveProcessor(formatter), + new FormatAsPhpProcessor(formatter), + new BladeCommentProcessor(formatter), + new BladeBraceProcessor(formatter), + new RawBladeBraceProcessor(formatter), + new ConditionsProcessor(formatter), + new PropsProcessor(formatter), + new InlinePhpDirectiveProcessor(formatter), + new InlineDirectiveProcessor(formatter), + new BladeDirectiveInScriptsProcessor(formatter), + new BladeDirectiveInStylesProcessor(formatter), + new CustomDirectiveProcessor(formatter), + new UnbalancedDirectiveProcessor(formatter), + new BreakLineBeforeAndAfterDirectiveProcessor(formatter), + new ScriptsProcessor(formatter), + new SortTailwindClassesProcessor(formatter), + new XInitProcessor(formatter), + new XDataProcessor(formatter), + new PhpBlockProcessor(formatter), + new SortHtmlAttributesProcessor(formatter), + new HtmlAttributesProcessor(formatter), + new ComponentAttributeProcessor(formatter), + new ShorthandBindingProcessor(formatter), + new XslotProcessor(formatter), + new HtmlTagsProcessor(formatter), + new FormatAsHtmlProcessor(formatter), + new FormatAsBladeProcessor(formatter), + new AdjustSpacesProcessor(formatter), + ]; + } + + async formatContent(content: any): Promise { + let target = await Promise.resolve(content); + + // pre process content + const preProcessors = this.processors.filter((processor) => + this.preProcessors.includes(processor.constructor as typeof Processor), + ); + + for (const processor of preProcessors) { + target = await processor.preProcess(target); + } + + // post process content + const postProcessors = this.processors.filter((processor) => + this.postProcessors.includes(processor.constructor as typeof Processor), + ); + postProcessors.sort((a, b) => { + return ( + this.postProcessors.indexOf(a.constructor as typeof Processor) - + this.postProcessors.indexOf(b.constructor as typeof Processor) + ); + }); + + for (const processor of postProcessors) { + target = await processor.postProcess(target); + } + + return target; + } +} diff --git a/src/formatter.ts b/src/formatter.ts index 7834a846..8ce938be 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -1,2787 +1,100 @@ /* eslint-disable class-methods-use-this */ -import { sortClasses } from "@shufo/tailwindcss-class-sorter"; -import Aigle from "aigle"; import detectIndent from "detect-indent"; -import { sortAttributes } from "html-attribute-sorter"; -import beautify, { type JSBeautifyOptions } from "js-beautify"; import _ from "lodash"; -import replaceAsync from "string-replace-async"; import * as vscodeTmModule from "vscode-textmate"; -import xregexp from "xregexp"; -import { formatPhpComment } from "./comment"; import constants from "./constants"; +import { FormatContentPipeline } from "./formatContentPipeline"; import { - conditionalTokens, - cssAtRuleTokens, - directivePrefix, hasStartAndEndToken, indentElseTokens, indentEndTokens, indentStartAndEndTokens, indentStartOrElseTokens, indentStartTokens, - indentStartTokensWithoutPrefix, - inlineFunctionTokens, - inlinePhpDirectives, optionalStartWithoutEndTokens, phpKeywordEndTokens, phpKeywordStartTokens, tokenForIndentStartOrElseTokens, - unbalancedStartTokens, } from "./indent"; import type { BladeFormatterOption, CLIOption, FormatterOption } from "./main"; -import { nestedParenthesisRegex } from "./regex"; -import type { SortHtmlAttributes } from "./runtimeConfig"; -import { adjustSpaces } from "./space"; import * as util from "./util"; -import * as vsctm from "./vsctm"; export default class Formatter { argumentCheck: any; - bladeBraces: any; - - bladeComments: any; - - bladeDirectives: any; - - htmlAttributes: Array; - - currentIndentLevel: number; - - diffs: any; - - indentCharacter: any; - - indentSize: any; - - inlineDirectives: any; - - conditions: any; - - inlinePhpDirectives: any; - - isInsideCommentBlock: any; - - oniguruma: any; - - options: FormatterOption & CLIOption; - - rawBladeBraces: any; - - ignoredLines: any; - - curlyBracesWithJSs: any; - - rawBlocks: any; - - rawPhpTags: any; - - rawPropsBlocks: any; - - result: any; - - nonnativeScripts: Array; - - scripts: any; - - xData: Array; - - xInit: Array; - - xSlot: Array; - - htmlTags: Array; - - shouldBeIndent: any; - - stack: any; - - templatingStrings: any; - - stringLiteralInPhp: Array; - - shorthandBindings: Array; - - componentAttributes: Array; - - customDirectives: Array; - - directivesInScript: Array; - - unbalancedDirectives: Array; - - escapedBladeDirectives: Array; - - phpComments: Array; - - vsctm: any; - - wrapAttributes: any; - - wrapLineLength: any; - - defaultPhpFormatOption: util.FormatPhpOption; - - endOfLine: string; - - bladeDirectivesInStyle: Array; - - constructor(options: BladeFormatterOption) { - this.options = { - ...{ - noPhpSyntaxCheck: false, - trailingCommaPHP: !options.noTrailingCommaPhp, - printWidth: options.wrapLineLength || constants.defaultPrintWidth, - }, - ...options, - }; - this.vsctm = util.optional(this.options).vsctm || vscodeTmModule; - this.oniguruma = util.optional(this.options).oniguruma; - this.indentCharacter = util.optional(this.options).useTabs ? "\t" : " "; - this.indentSize = util.optional(this.options).indentSize || 4; - this.wrapLineLength = - util.optional(this.options).wrapLineLength || constants.defaultPrintWidth; - this.wrapAttributes = util.optional(this.options).wrapAttributes || "auto"; - this.currentIndentLevel = 0; - this.shouldBeIndent = false; - this.isInsideCommentBlock = false; - this.stack = []; - this.ignoredLines = []; - this.curlyBracesWithJSs = []; - this.rawBlocks = []; - this.rawPhpTags = []; - this.inlineDirectives = []; - this.conditions = []; - this.inlinePhpDirectives = []; - this.rawPropsBlocks = []; - this.bladeDirectives = []; - this.bladeDirectivesInStyle = []; - this.bladeComments = []; - this.phpComments = []; - this.bladeBraces = []; - this.rawBladeBraces = []; - this.nonnativeScripts = []; - this.scripts = []; - this.htmlAttributes = []; - this.xData = []; - this.xInit = []; - this.htmlTags = []; - this.templatingStrings = []; - this.stringLiteralInPhp = []; - this.shorthandBindings = []; - this.componentAttributes = []; - this.customDirectives = []; - this.directivesInScript = []; - this.unbalancedDirectives = []; - this.escapedBladeDirectives = []; - this.xSlot = []; - this.result = []; - this.diffs = []; - this.defaultPhpFormatOption = { - noPhpSyntaxCheck: this.options.noPhpSyntaxCheck, - printWidth: this.wrapLineLength, - }; - this.endOfLine = util.getEndOfLine(util.optional(this.options).endOfLine); - } - - formatContent(content: any) { - return new Promise((resolve) => resolve(content)) - .then((target) => this.preserveIgnoredLines(target)) - .then((target) => this.preserveNonnativeScripts(target)) - .then((target) => this.preserveCurlyBraceForJS(target)) - .then((target) => this.preserveRawPhpTags(target)) - .then((target) => this.preserveEscapedBladeDirective(target)) - .then((target) => util.formatAsPhp(target, this.options)) - .then((target) => this.preserveBladeComment(target)) - .then((target) => this.preserveBladeBrace(target)) - .then((target) => this.preserveRawBladeBrace(target)) - .then((target) => this.preserveConditions(target)) - .then((target) => this.preservePropsBlock(target)) - .then((target) => this.preserveInlinePhpDirective(target)) - .then((target) => this.preserveInlineDirective(target)) - .then((target) => this.preserveBladeDirectivesInScripts(target)) - .then((target) => this.preserveBladeDirectivesInStyles(target)) - .then((target) => this.preserveCustomDirective(target)) - .then((target) => this.preserveUnbalancedDirective(target)) - .then((target) => this.breakLineBeforeAndAfterDirective(target)) - .then(async (target) => { - this.bladeDirectives = await this.formatPreservedBladeDirectives( - this.bladeDirectives, - ); - return target; - }) - .then((target) => this.preserveScripts(target)) - .then((target) => this.sortTailwindcssClasses(target)) - .then((target) => this.formatXInit(target)) - .then((target) => this.formatXData(target)) - .then((target) => this.preservePhpBlock(target)) - .then((target) => this.sortHtmlAttributes(target)) - .then((target) => this.preserveHtmlAttributes(target)) - .then((target) => this.preserveComponentAttribute(target)) - .then((target) => this.preserveShorthandBinding(target)) - .then((target) => this.preserveXslot(target)) - .then((target) => this.preserveHtmlTags(target)) - .then((target) => this.formatAsHtml(target)) - .then((target) => this.formatAsBlade(target)) - .then((target) => this.restoreHtmlTags(target)) - .then((target) => this.restoreXslot(target)) - .then((target) => this.restoreShorthandBinding(target)) - .then((target) => this.restoreComponentAttribute(target)) - .then((target) => this.restoreHtmlAttributes(target)) - .then((target) => this.restorePhpBlock(target)) - .then((target) => this.restoreXData(target)) - .then((target) => this.restoreXInit(target)) - .then((target) => this.restoreScripts(target)) - .then((target) => this.restoreUnbalancedDirective(target)) - .then((target) => this.restoreCustomDirective(target)) - .then((target) => this.restoreBladeDirectivesInStyles(target)) - .then((target) => this.restoreBladeDirectivesInScripts(target)) - .then((target) => this.restoreInlineDirective(target)) - .then((target) => this.restoreInlinePhpDirective(target)) - .then((target) => this.restoreConditions(target)) - .then((target) => this.restoreRawBladeBrace(target)) - .then((target) => this.restoreBladeBrace(target)) - .then((target) => this.restoreBladeComment(target)) - .then((target) => this.restoreEscapedBladeDirective(target)) - .then((target) => this.restoreRawPhpTags(target)) - .then((target) => this.restoreCurlyBraceForJS(target)) - .then((target) => this.restoreNonnativeScripts(target)) - .then((target) => this.restoreIgnoredLines(target)) - .then((target) => adjustSpaces(target)) - .then((formattedResult) => util.checkResult(formattedResult)); - } - - formatAsHtml(data: any) { - const options = { - indent_size: util.optional(this.options).indentSize || 4, - wrap_line_length: util.optional(this.options).wrapLineLength || 120, - wrap_attributes: util.optional(this.options).wrapAttributes || "auto", - wrap_attributes_min_attrs: util.optional(this.options) - .wrapAttributesMinAttrs, - indent_inner_html: util.optional(this.options).indentInnerHtml || false, - end_with_newline: util.optional(this.options).endWithNewline || true, - max_preserve_newlines: util.optional(this.options).noMultipleEmptyLines - ? 1 - : undefined, - extra_liners: util.optional(this.options).extraLiners, - css: { - end_with_newline: false, - }, - eol: this.endOfLine, - }; - - const promise = new Promise((resolve) => resolve(data)) - .then((content) => util.preserveDirectives(content)) - .then((preserved) => beautify.html_beautify(preserved, options)) - .then((content) => util.revertDirectives(content)); - - return Promise.resolve(promise); - } - - async sortTailwindcssClasses(content: any) { - if (!this.options.sortTailwindcssClasses) { - return content; - } - - return _.replace( - content, - /(?<=\s+(?!:)class\s*=\s*([\"\']))(.*?)(?=\1)/gis, - (_match, p1, p2) => { - if (_.isEmpty(p2)) { - return p2; - } - - if (this.options.tailwindcssConfigPath) { - const options = { - tailwindConfigPath: this.options.tailwindcssConfigPath, - }; - return sortClasses(p2, options); - } - - if (this.options.tailwindcssConfig) { - const options: any = { - tailwindConfig: this.options.tailwindcssConfig, - }; - return sortClasses(p2, options); - } - - return sortClasses(p2); - }, - ); - } - - async preserveIgnoredLines(content: any) { - return ( - _.chain(content) - // ignore entire file - .replace( - /(^(? - this.storeIgnoredLines(`${p1}${p2.replace(/^\n/, "")}`), - ) - // range ignore - .replace( - /(?:({{--\s*?blade-formatter-disable\s*?--}}||{{--\s*?prettier-ignore-start\s*?--}})).*?(?:({{--\s*?blade-formatter-enable\s*?--}}||{{--\s*?prettier-ignore-end\s*?--}}))/gis, - (match: any) => this.storeIgnoredLines(match), - ) - // line ignore - .replace( - /(?:{{--\s*?blade-formatter-disable-next-line\s*?--}}|{{--\s*?prettier-ignore\s*?--}}|)[\r\n]+[^\r\n]+/gis, - (match: any) => this.storeIgnoredLines(match), - ) - .value() - ); - } - - async preserveCurlyBraceForJS(content: any) { - return _.replace(content, /@{{(.*?)}}/gs, (match: any, p1: any) => - this.storeCurlyBraceForJS(p1), - ); - } - - async preservePhpBlock(content: any) { - return this.preserveRawPhpBlock(content); - } - - async preservePropsBlock(content: any) { - return _.replace( - content, - /@props\(((?:[^\\(\\)]|\([^\\(\\)]*\))*)\)/gs, - (match: any, p1: any) => this.storeRawPropsBlock(p1), - ); - } - - async preserveRawPhpBlock(content: any) { - return _.replace( - content, - /(? this.storeRawBlock(p1), - ); - } - - async preserveHtmlTags(content: string) { - const contentUnformatted = ["textarea", "pre"]; - - return _.replace( - content, - new RegExp( - `<(${contentUnformatted.join( - "|", - )})\\s{0,1}.*?>.*?<\\/(${contentUnformatted.join("|")})>`, - "gis", - ), - (match: string) => this.storeHtmlTag(match), - ); - } - - /** - * preserve custom directives - * @param content - * @returns - */ - preserveCustomDirective(content: string) { - const negativeLookAhead = [ - ..._.without(indentStartTokens, "@unless"), - ...indentEndTokens, - ...indentElseTokens, - ...["@unless\\(.*?\\)"], - ].join("|"); - - const inlineNegativeLookAhead = _.chain([ - ..._.without(indentStartTokens, "@unless", "@for"), - ...indentEndTokens, - ...indentElseTokens, - ...inlineFunctionTokens, - ..._.without(phpKeywordStartTokens, "@for"), - ...["@unless[a-z]*\\(.*?\\)", "@for\\(.*?\\)"], - ...unbalancedStartTokens, - ...cssAtRuleTokens, - ]) - .uniq() - .join("|") - .value(); - - const inlineRegex = new RegExp( - `(?!(${inlineNegativeLookAhead}))(@([a-zA-Z1-9_\\-]+))(?!.*?@end\\3)${nestedParenthesisRegex}.*?(? - this.storeInlineCustomDirective(match), - ); - - // preserve begin~else~end directives - formatted = _.replace( - formatted, - regex, - ( - match: string, - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - p6: string, - p7: string, - ) => { - if (indentStartTokens.includes(p2)) { - return match; - } - - let result: string = match; - - // begin directive - result = _.replace( - result, - new RegExp(`${p2}(${nestedParenthesisRegex})*`, "gim"), - (beginStr: string) => this.storeBeginCustomDirective(beginStr), - ); - // end directive - result = _.replace(result, p7, this.storeEndCustomDirective(p7)); - // else directive - result = _.replace( - result, - new RegExp(`@else${p4}(${nestedParenthesisRegex})*`, "gim"), - (elseStr: string) => this.storeElseCustomDirective(elseStr), - ); - - return result; - }, - ); - - // replace directives recursively - if (regex.test(formatted)) { - formatted = this.preserveCustomDirective(formatted); - } - - return formatted; - } - - preserveInlineDirective(content: string): string { - // preserve inline directives inside html tag - const regex = new RegExp( - `(<[\\w\\-\\_]+?[^>]*?)${directivePrefix}(${indentStartTokensWithoutPrefix.join( - "|", - )})(\\s*?)?(\\([^)]*?\\))?((?:(?!@end\\2).)+)(@end\\2|@endif)(.*?/*>)`, - "gims", - ); - const replaced = _.replace( - content, - regex, - ( - _match: string, - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - p6: string, - p7: string, - ) => { - if (p3 === undefined && p4 === undefined) { - return `${p1}${this.storeInlineDirective( - `${directivePrefix}${p2.trim()}${p5.trim()} ${p6.trim()}`, - )}${p7}`; - } - if (p3 === undefined) { - return `${p1}${this.storeInlineDirective( - `${directivePrefix}${p2.trim()}${p4.trim()}${p5}${p6.trim()}`, - )}${p7}`; - } - if (p4 === undefined) { - return `${p1}${this.storeInlineDirective( - `${directivePrefix}${p2.trim()}${p3}${p5.trim()} ${p6.trim()}`, - )}${p7}`; - } - - return `${p1}${this.storeInlineDirective( - `${directivePrefix}${p2.trim()}${p3}${p4.trim()} ${p5.trim()} ${p6.trim()}`, - )}${p7}`; - }, - ); - - if (regex.test(replaced)) { - return this.preserveInlineDirective(replaced); - } - - return replaced; - } - - async preserveInlinePhpDirective(content: any) { - return _.replace( - content, - // eslint-disable-next-line max-len - new RegExp( - `(?!\\/\\*.*?\\*\\/)(${inlineFunctionTokens.join( - "|", - )})(\\s*?)${nestedParenthesisRegex}`, - "gmsi", - ), - (match: any) => this.storeInlinePhpDirective(match), - ); - } - - preserveBladeDirectivesInScripts(content: any) { - return _.replace( - content, - /(?<=]*?(?)(.*?)(?=<\/script>)/gis, - (match: string) => { - const targetTokens = [...indentStartTokens, ...inlineFunctionTokens]; - - if (new RegExp(targetTokens.join("|"), "gmi").test(match) === false) { - if (/^[\s\n]+$/.test(match)) { - return match.trim(); - } - - return match; - } - - const inlineFunctionDirectives = inlineFunctionTokens.join("|"); - const inlineFunctionRegex = new RegExp( - // eslint-disable-next-line max-len - `(?!\\/\\*.*?\\*\\/)(${inlineFunctionDirectives})(\\s*?)${nestedParenthesisRegex}`, - "gmi", - ); - const endTokens = _.chain(indentEndTokens).without("@endphp"); - - let formatted: string = match; - - formatted = _.replace(formatted, inlineFunctionRegex, (matched: any) => - this.storeBladeDirective( - util.formatRawStringAsPhp(matched, { - ...this.options, - printWidth: util.printWidthForInline, - }), - ), - ); - - formatted = _.replace( - formatted, - new RegExp( - `(${indentStartTokens.join("|")})\\s*?${nestedParenthesisRegex}`, - "gis", - ), - (matched) => - `if ( /*${this.storeBladeDirectiveInScript(matched)}*/ ) {`, - ); - - formatted = _.replace( - formatted, - new RegExp( - `(${[...indentElseTokens, ...indentStartOrElseTokens].join( - "|", - )})(?!\\w+?\\s*?\\(.*?\\))`, - "gis", - ), - (matched) => - `/***script_placeholder***/} /* ${this.storeBladeDirectiveInScript( - matched, - )} */ {`, - ); - - formatted = _.replace( - formatted, - new RegExp(`(${endTokens.join("|")})`, "gis"), - (matched) => - `/***script_placeholder***/} /*${this.storeBladeDirectiveInScript( - matched, - )}*/`, - ); - - formatted = _.replace( - formatted, - /(? this.storeRawBlock(p1), - ); - - // custom directive - formatted = this.preserveCustomDirectiveInScript(formatted); - - return formatted; - }, - ); - } - - preserveBladeDirectivesInStyles(content: string) { - return _.replace( - content, - /(?<=]*?(?)(.*?)(?=<\/style>)/gis, - (inside: string) => { - let result: string = inside; - - const inlineRegex = new RegExp( - `(?!${["@end", "@else", ...cssAtRuleTokens].join( - "|", - )})@(\\w+)\\s*?(?![^\\1]*@end\\1)${nestedParenthesisRegex}`, - "gmi", - ); - - result = _.replace( - result, - inlineRegex, - (match: string) => - `${this.storeBladeDirectiveInStyle( - match, - )} {/* inline_directive */}`, - ); - - const customStartRegex = new RegExp( - `(?!${["@end", "@else", ...cssAtRuleTokens].join( - "|", - )})@(\\w+)\\s*?(${nestedParenthesisRegex})`, - "gmi", - ); - - result = _.replace( - result, - customStartRegex, - (match: string) => - `${this.storeBladeDirectiveInStyle(match)} { /*start*/`, - ); - - const startRegex = new RegExp( - `(${indentStartTokens.join("|")})\\s*?(${nestedParenthesisRegex})`, - "gmi", - ); - - result = _.replace( - result, - startRegex, - (match: string) => - `${this.storeBladeDirectiveInStyle(match)} { /*start*/`, - ); - - const elseRegex = new RegExp( - `(${["@else\\w+", ...indentElseTokens].join( - "|", - )})\\s*?(${nestedParenthesisRegex})?`, - "gmi", - ); - - result = _.replace( - result, - elseRegex, - (match: string) => - `} ${this.storeBladeDirectiveInStyle(match)} { /*else*/`, - ); - - const endRegex = new RegExp( - `${["@end\\w+", ...indentEndTokens].join("|")}`, - "gmi", - ); - - result = _.replace( - result, - endRegex, - (match: string) => - `} /* ${this.storeBladeDirectiveInStyle(match)} */`, - ); - - return result; - }, - ); - } - - /** - * - * @param content string between - * @returns string - */ - preserveCustomDirectiveInScript(content: string): string { - const negativeLookAhead = [ - ..._.without(indentStartTokens, "@unless"), - ...indentEndTokens, - ...indentElseTokens, - ...["@unless\\(.*?\\)"], - ].join("|"); - - const inlineNegativeLookAhead = [ - ..._.without(indentStartTokens, "@unless"), - ...indentEndTokens, - ...indentElseTokens, - ...inlineFunctionTokens, - ...phpKeywordStartTokens, - ...["@unless[a-z]*\\(.*?\\)"], - ...unbalancedStartTokens, - ].join("|"); - - const inlineRegex = new RegExp( - `(?!(${inlineNegativeLookAhead}))(@([a-zA-Z1-9_\\-]+))(?!.*?@end\\3)${nestedParenthesisRegex}.*?(? - this.storeInlineCustomDirective(match), - ); - - // preserve begin~else~end directives - formatted = _.replace( - formatted, - regex, - ( - match: string, - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - p6: string, - p7: string, - ) => { - if (indentStartTokens.includes(p2)) { - return match; - } - - let result: string = match; - - result = _.replace( - result, - new RegExp(`${p2}(${nestedParenthesisRegex})*`, "gim"), - (beginStr: string) => - `if ( /*${this.storeBladeDirectiveInScript(beginStr)}*/ ) {`, - ); - - result = _.replace( - result, - new RegExp(`@else${p4}(${nestedParenthesisRegex})*`, "gim"), - (elseStr: string) => - `/***script_placeholder***/} /* ${this.storeBladeDirectiveInScript( - elseStr, - )} */ {`, - ); - result = _.replace( - result, - p7, - (endStr: string) => - `/***script_placeholder***/} /*${this.storeBladeDirectiveInScript( - endStr, - )}*/`, - ); - - return result; - }, - ); - - // replace directives recursively - if (regex.test(formatted)) { - formatted = this.preserveCustomDirectiveInScript(formatted); - } - - return formatted; - } - - /** - * Recursively insert line break before and after directives - * @param content string - * @returns - */ - breakLineBeforeAndAfterDirective(content: string): string { - // handle directive around html tags - let formattedContent = _.replace( - content, - new RegExp( - `(?<=<.*?(?)(${_.without(indentStartTokens, "@php").join( - "|", - )})(\\s*)${nestedParenthesisRegex}.*?(?=<.*?>)`, - "gmis", - ), - (match) => `\n${match.trim()}\n`, - ); - - // eslint-disable-next-line - formattedContent = _.replace( - formattedContent, - new RegExp( - `(?<=<.*?(?).*?(${_.without(indentEndTokens, "@endphp").join( - "|", - )})(?=<.*?>)`, - "gmis", - ), - (match) => `\n${match.trim()}\n`, - ); - - const unbalancedConditions = ["@case", ...indentElseTokens]; - - formattedContent = _.replace( - formattedContent, - new RegExp( - `(\\s*?)(${unbalancedConditions.join( - "|", - )})(\\s*?)${nestedParenthesisRegex}(\\s*)`, - "gmi", - ), - (match) => `\n${match.trim()}\n`, - // handle else directive - ); - - formattedContent = _.replace( - formattedContent, - new RegExp( - `\\s*?(?!(${_.without(indentElseTokens, "@else").join("|")}))@else\\s+`, - "gim", - ), - (match) => `\n${match.trim()}\n`, - // handle case directive - ); - - formattedContent = _.replace( - formattedContent, - /@case\S*?\s*?@case/gim, - (match) => { - // handle unbalanced echos - return `${match.replace("\n", "")}`; - }, - ); - - const unbalancedEchos = ["@break"]; - - _.forEach(unbalancedEchos, (directive) => { - formattedContent = _.replace( - formattedContent, - new RegExp(`(\\s*?)${directive}\\s+`, "gmi"), - (match) => { - return `\n${match.trim()}\n\n`; - }, - ); - }); - - // other directives - _.forEach(["@default"], (directive) => { - formattedContent = _.replace( - formattedContent, - new RegExp(`(\\s*?)${directive}\\s*`, "gmi"), - (match) => { - return `\n\n${match.trim()}\n`; - }, - ); - }); - - // add line break around balanced directives - const directives = _.chain(indentStartTokens) - .map((x: any) => _.replace(x, /@/, "")) - .value(); - - _.forEach(directives, (directive: any) => { - try { - const recursivelyMatched = xregexp.matchRecursive( - formattedContent, - `\\@${directive}`, - `\\@end${directive}`, - "gmi", - { - valueNames: [null, "left", "match", "right"], - }, - ); - - if (_.isEmpty(recursivelyMatched)) { - return; - } - - for (const matched of recursivelyMatched) { - if (matched.name === "match") { - if (new RegExp(indentStartTokens.join("|")).test(matched.value)) { - formattedContent = _.replace( - formattedContent, - matched.value, - this.breakLineBeforeAndAfterDirective( - util.escapeReplacementString(matched.value), - ), - ); - } - - const innerRegex = new RegExp( - `^(\\s*?)${nestedParenthesisRegex}(.*)`, - "gmis", - ); - - const replaced = _.replace( - `${matched.value}`, - innerRegex, - (_match: string, p1: string, p2: string, p3: string) => { - if (p3.trim() === "") { - return `${p1}(${p2.trim()})\n${p3.trim()}`; - } - - return `${p1}(${p2.trim()})\n${p3.trim()}\n`; - }, - ); - - formattedContent = _.replace( - formattedContent, - matched.value, - util.escapeReplacementString(replaced), - ); - } - } - } catch (error) { - // do nothing to ignore unmatched directive pair - } - }); - - return formattedContent; - } - - async preserveEscapedBladeDirective(content: string) { - return _.replace(content, /@@\w*/gim, (match: string) => - this.storeEscapedBladeDirective(match), - ); - } - - async preserveXslot(content: string) { - return _.replace( - content, - /(?<=<\/?)(x-slot:[\w_\\-]+)(?=(?:[^>]*?[^?])?>)/gm, - (match: string) => this.storeXslot(match), - ); - } - - async preserveBladeComment(content: any) { - return _.replace(content, /\{\{--(.*?)--\}\}/gs, (match: string) => - this.storeBladeComment(match), - ); - } - - preservePhpComment(content: string) { - return _.replace( - content, - /\/\*(?:[^*]|[\r\n]|(?:\*+(?:[^*\/]|[\r\n])))*\*+\//gi, - (match: string) => this.storePhpComment(match), - ); - } - - async preserveBladeBrace(content: any) { - return _.replace(content, /\{\{(.*?)\}\}/gs, (_match: any, p1: any) => { - // if content is blank - if (p1 === "") { - return this.storeBladeBrace(p1, p1.length); - } - - // preserve a space if content contains only space, tab, or new line character - if (!/\S/.test(p1)) { - return this.storeBladeBrace(" ", " ".length); - } - - // any other content - return this.storeBladeBrace(p1.trim(), p1.trim().length); - }); - } - - async preserveRawBladeBrace(content: any) { - return _.replace(content, /\{!!(.*?)!!\}/gs, (_match: any, p1: any) => { - // if content is blank - if (p1 === "") { - return this.storeRawBladeBrace(p1); - } - - // preserve a space if content contains only space, tab, or new line character - if (!/\S/.test(p1)) { - return this.storeRawBladeBrace(" "); - } - - // any other content - return this.storeRawBladeBrace(p1.trim()); - }); - } - - async preserveConditions(content: any) { - const regex = new RegExp( - `(${conditionalTokens.join( - "|", - // eslint-disable-next-line max-len - )})(\\s*?)${nestedParenthesisRegex}`, - "gi", - ); - return _.replace( - content, - regex, - (match: any, p1: any, p2: any, p3: any) => - `${p1}${p2}(${this.storeConditions(p3)})`, - ); - } - - /** - * preserve unbalanced directive like @hasSection - */ - preserveUnbalancedDirective(content: any) { - const regex = new RegExp( - `((${unbalancedStartTokens.join( - "|", - )})(?!.*?\\2)(?:\\s|\\(.*?\\)))+(?=.*?@endif)`, - "gis", - ); - - let replaced: string = _.replace( - content, - regex, - (_match: string, p1: string) => `${this.storeUnbalancedDirective(p1)}`, - ); - - if (regex.test(replaced)) { - replaced = this.preserveUnbalancedDirective(replaced); - } - - return replaced; - } - - async preserveRawPhpTags(content: any) { - return _.replace(content, /<\?php(.*?)\?>/gms, (match: any) => - this.storeRawPhpTags(match), - ); - } - - async preserveNonnativeScripts(content: string) { - return _.replace( - content, - /]*?type=(["'])(?!(text\/javascript|module))[^\1]*?\1[^>]*?>.*?<\/script>/gis, - (match: string) => this.storeNonnativeScripts(match), - ); - } - - async preserveScripts(content: any) { - return _.replace(content, /.*?<\/script>/gis, (match: any) => - this.storeScripts(match), - ); - } - - async preserveHtmlAttributes(content: any) { - return _.replace( - content, - /(?<=<[\w\-\.\:\_]+.*\s)(?!x-bind)([^\s\:][^\s\'\"]+\s*=\s*(["'])(?)/gms, - (match: string) => `${this.storeHtmlAttribute(match)}`, - ); - } - - async sortHtmlAttributes(content: string) { - const strategy: SortHtmlAttributes = - this.options.sortHtmlAttributes ?? "none"; - - if (!_.isEmpty(strategy) && strategy !== "none") { - const regexes = this.options.customHtmlAttributesOrder; - - if (_.isArray(regexes)) { - return sortAttributes(content, { - order: strategy, - customRegexes: regexes, - }); - } - - // when option is string - const customRegexes = _.chain(regexes).split(",").map(_.trim).value(); - - return sortAttributes(content, { order: strategy, customRegexes }); - } - - return content; - } - - async preserveShorthandBinding(content: string) { - return _.replace( - content, - /(?<=<(?!livewire:)[^<]*?(\s|x-bind)):{1}(?)[\w\-_.]*?=(["'])(?!=>)[^\2]*?\2(?=[^>]*?\/*?>)/gim, - (match: any) => `${this.storeShorthandBinding(match)}`, - ); - } - - async preserveComponentAttribute(content: string) { - const prefixes = - Array.isArray(this.options.componentPrefix) && - this.options.componentPrefix.length > 0 - ? this.options.componentPrefix - : ["x-", "livewire:"]; - const regex = new RegExp( - `(?<=<(${prefixes.join( - "|", - )})[^<]*?\\s):{1,2}(?)[\\w\-_.]*?=(["'])(?!=>)[^\\2]*?\\2(?=[^>]*?\/*?>)`, - "gim", - ); - return _.replace( - content, - regex, - (match: any) => `${this.storeComponentAttribute(match)}`, - ); - } - - async formatXData(content: any) { - return _.replace( - content, - /(\s*)x-data="(.*?)"(\s*)/gs, - (_match: any, p1: any, p2: any, p3: any) => - `${p1}x-data="${this.storeXData(p2)}"${p3}`, - ); - } - - async formatXInit(content: any) { - return _.replace( - content, - /(\s*)x-init="(.*?)"(\s*)/gs, - (_match: any, p1: any, p2: any, p3: any) => - `${p1}x-init="${this.storeXInit(p2)}"${p3}`, - ); - } - - preserveStringLiteralInPhp(content: any) { - return _.replace( - content, - /(\"([^\\]|\\.)*?\"|\'([^\\]|\\.)*?\')/gm, - (match: string) => `${this.storeStringLiteralInPhp(match)}`, - ); - } - - storeIgnoredLines(value: any) { - return this.getIgnoredLinePlaceholder(this.ignoredLines.push(value) - 1); - } - - storeCurlyBraceForJS(value: any) { - return this.getCurlyBraceForJSPlaceholder( - this.curlyBracesWithJSs.push(value) - 1, - ); - } - - storeRawBlock(value: any) { - return this.getRawPlaceholder(this.rawBlocks.push(value) - 1); - } - - storeInlineDirective(value: any) { - return this.getInlinePlaceholder( - this.inlineDirectives.push(value) - 1, - value.length, - ); - } - - storeConditions(value: any) { - return this.getConditionPlaceholder(this.conditions.push(value) - 1); - } - - storeInlinePhpDirective(value: any) { - return this.getInlinePhpPlaceholder( - this.inlinePhpDirectives.push(value) - 1, - ); - } - - storeRawPropsBlock(value: any) { - return this.getRawPropsPlaceholder(this.rawPropsBlocks.push(value) - 1); - } - - storeBladeDirective(value: any) { - return this.getBladeDirectivePlaceholder( - this.bladeDirectives.push(value) - 1, - ); - } - - storeBladeDirectiveInStyle(value: string) { - return this.getBladeDirectiveInStylePlaceholder( - (this.bladeDirectivesInStyle.push(value) - 1).toString(), - ); - } - - storeEscapedBladeDirective(value: string) { - return this.getEscapedBladeDirectivePlaceholder( - (this.escapedBladeDirectives.push(value) - 1).toString(), - ); - } - - storeXslot(value: string) { - return this.getXslotPlaceholder((this.xSlot.push(value) - 1).toString()); - } - - storeBladeComment(value: any) { - return this.getBladeCommentPlaceholder(this.bladeComments.push(value) - 1); - } - - storePhpComment(value: string) { - return this.getPhpCommentPlaceholder( - (this.phpComments.push(value) - 1).toString(), - ); - } - - storeHtmlTag(value: string) { - return this.getHtmlTagPlaceholder( - (this.htmlTags.push(value) - 1).toString(), - ); - } - - storeInlineCustomDirective(value: string) { - return this.getInlineCustomDirectivePlaceholder( - (this.customDirectives.push(value) - 1).toString(), - ); - } - - storeBeginCustomDirective(value: string) { - return this.getBeginCustomDirectivePlaceholder( - (this.customDirectives.push(value) - 1).toString(), - ); - } - - storeElseCustomDirective(value: string) { - return this.getElseCustomDirectivePlaceholder( - (this.customDirectives.push(value) - 1).toString(), - ); - } - - storeEndCustomDirective(value: string) { - return this.getEndCustomDirectivePlaceholder( - (this.customDirectives.push(value) - 1).toString(), - ); - } - - storeUnbalancedDirective(value: string) { - return this.getUnbalancedDirectivePlaceholder( - (this.unbalancedDirectives.push(value) - 1).toString(), - ); - } - - storeBladeBrace(value: any, length: any) { - const index = this.bladeBraces.push(value) - 1; - const brace = "{{ }}"; - return this.getBladeBracePlaceholder(index, length + brace.length); - } - - storeRawBladeBrace(value: any) { - const index = this.rawBladeBraces.push(value) - 1; - return this.getRawBladeBracePlaceholder(index); - } - - storeRawPhpTags(value: any) { - const index = this.rawPhpTags.push(value) - 1; - return this.getRawPhpTagPlaceholder(index); - } - - storeNonnativeScripts(value: string) { - const index = this.nonnativeScripts.push(value) - 1; - return this.getNonnativeScriptPlaceholder(index.toString()); - } - - storeScripts(value: any) { - const index = this.scripts.push(value) - 1; - return this.getScriptPlaceholder(index); - } - - storeHtmlAttribute(value: string) { - const index = this.htmlAttributes.push(value) - 1; - - if (value.length > 0) { - return this.getHtmlAttributePlaceholder(index.toString(), value.length); - } - - return this.getHtmlAttributePlaceholder(index.toString(), 0); - } - - storeShorthandBinding(value: any) { - const index = this.shorthandBindings.push(value) - 1; - - return this.getShorthandBindingPlaceholder(index.toString(), value.length); - } - - storeComponentAttribute(value: any) { - const index = this.componentAttributes.push(value) - 1; - - return this.getComponentAttributePlaceholder(index.toString()); - } - - storeXData(value: any) { - const index = this.xData.push(value) - 1; - return this.getXDataPlaceholder(index); - } - - storeXInit(value: any) { - const index = this.xInit.push(value) - 1; - return this.getXInitPlaceholder(index); - } - - storeTemplatingString(value: any) { - const index = this.templatingStrings.push(value) - 1; - return this.getTemplatingStringPlaceholder(index); - } - - storeStringLiteralInPhp(value: any) { - const index = this.stringLiteralInPhp.push(value) - 1; - return this.getStringLiteralInPhpPlaceholder(index); - } - - storeBladeDirectiveInScript(value: string) { - return this.getBladeDirectiveInScriptPlaceholder( - (this.directivesInScript.push(value) - 1).toString(), - ); - } - - getIgnoredLinePlaceholder(replace: any) { - return _.replace("___ignored_line_#___", "#", replace); - } - - getCurlyBraceForJSPlaceholder(replace: any) { - return _.replace("___js_curly_brace_#___", "#", replace); - } - - getRawPlaceholder(replace: any) { - return _.replace("___raw_block_#___", "#", replace); - } - - getInlinePlaceholder(replace: any, length = 0) { - if (length > 0) { - const template = "___inline_directive_#___"; - const gap = length - template.length; - return _.replace( - `___inline_directive_${_.repeat("_", gap > 0 ? gap : 0)}#___`, - "#", - replace, - ); - } - - return _.replace("___inline_directive_+?#___", "#", replace); - } - - getConditionPlaceholder(replace: any) { - return _.replace("___directive_condition_#___", "#", replace); - } - - getInlinePhpPlaceholder(replace: any) { - return _.replace("___inline_php_directive_#___", "#", replace); - } - - getRawPropsPlaceholder(replace: any) { - return _.replace("@__raw_props_block_#__@", "#", replace); - } - - getBladeDirectivePlaceholder(replace: any) { - return _.replace("___blade_directive_#___", "#", replace); - } - - getBladeDirectiveInStylePlaceholder(replace: string) { - return _.replace(".___blade_directive_in_style_#__", "#", replace); - } - - getEscapedBladeDirectivePlaceholder(replace: string) { - return _.replace("___escaped_directive_#___", "#", replace); - } - - getXslotPlaceholder(replace: string) { - return _.replace("x-slot --___#___--", "#", replace); - } - - getBladeCommentPlaceholder(replace: any) { - return _.replace("___blade_comment_#___", "#", replace); - } - - getPhpCommentPlaceholder(replace: string) { - return _.replace("___php_comment_#___", "#", replace); - } - - getBladeBracePlaceholder(replace: any, length = 0) { - if (length > 0) { - const template = "___blade_brace_#___"; - const gap = length - template.length; - return _.replace( - `___blade_brace_${_.repeat("_", gap > 0 ? gap : 0)}#___`, - "#", - replace, - ); - } - - return _.replace("___blade_brace_+?#___", "#", replace); - } - - getRawBladeBracePlaceholder(replace: any) { - return _.replace("___raw_blade_brace_#___", "#", replace); - } - - getRawPhpTagPlaceholder(replace: any) { - return _.replace("___raw_php_tag_#___", "#", replace); - } - - getNonnativeScriptPlaceholder(replace: string) { - return _.replace("", "#", replace); - } - - getScriptPlaceholder(replace: any) { - return _.replace("", "#", replace); - } - - getHtmlTagPlaceholder(replace: string) { - return _.replace("", "#", replace); - } - - getInlineCustomDirectivePlaceholder(replace: string) { - return _.replace("___inline_cd_#___", "#", replace); - } - - getBeginCustomDirectivePlaceholder(replace: string) { - return _.replace("@customdirective(___#___)", "#", replace); - } - - getElseCustomDirectivePlaceholder(replace: string) { - return _.replace("@else(___#___)", "#", replace); - } - - getEndCustomDirectivePlaceholder(replace: string) { - return _.replace("@endcustomdirective(___#___)", "#", replace); - } - - getUnbalancedDirectivePlaceholder(replace: string) { - return _.replace("@if (unbalanced___#___)", "#", replace); - } - - getHtmlAttributePlaceholder(replace: string, length: any) { - if (length && length > 0) { - const template = "___attrs_#___"; - const gap = length - template.length; - return _.replace( - `___attrs${_.repeat("_", gap > 0 ? gap : 1)}#___`, - "#", - replace, - ); - } - - if (_.isNull(length)) { - return _.replace("___attrs_#___", "#", replace); - } - - return _.replace("___attrs_+?#___", "#", replace); - } - - getShorthandBindingPlaceholder(replace: string, length: any = 0) { - if (length && length > 0) { - const template = "___short_binding_#___"; - const gap = length - template.length; - return _.replace( - `___short_binding_${_.repeat("_", gap > 0 ? gap : 1)}#___`, - "#", - replace, - ); - } - return _.replace("___short_binding_+?#___", "#", replace); - } - - getComponentAttributePlaceholder(replace: string) { - return _.replace("___attribute_#___", "#", replace); - } - - getXInitPlaceholder(replace: any) { - return _.replace("___x_init_#___", "#", replace); - } - - getPlaceholder(attribute: string, replace: any, length: any = null) { - if (length && length > 0) { - const template = `___${attribute}_#___`; - const gap = length - template.length; - return _.replace( - `___${attribute}${_.repeat("_", gap > 0 ? gap : 1)}#___`, - "#", - replace, - ); - } - - if (_.isNull(length)) { - return _.replace(`___${attribute}_#___`, "#", replace); - } - - return _.replace(`s___${attribute}_+?#___`, "#", replace); - } - - getXDataPlaceholder(replace: any) { - return _.replace("___x_data_#___", "#", replace); - } - - getTemplatingStringPlaceholder(replace: any) { - return _.replace("___templating_str_#___", "#", replace); - } - - getStringLiteralInPhpPlaceholder(replace: any) { - return _.replace("'___php_content_#___'", "#", replace); - } - - getBladeDirectiveInScriptPlaceholder(replace: any) { - return _.replace("___directives_script_#___", "#", replace); - } - - restoreIgnoredLines(content: any) { - return _.replace( - content, - new RegExp(`${this.getIgnoredLinePlaceholder("(\\d+)")}`, "gm"), - (_match: any, p1: any) => this.ignoredLines[p1], - ); - } - - restoreCurlyBraceForJS(content: any) { - return _.replace( - content, - new RegExp(`${this.getCurlyBraceForJSPlaceholder("(\\d+)")}`, "gm"), - (_match: any, p1: any) => - `@{{ ${beautify.js_beautify(this.curlyBracesWithJSs[p1].trim())} }}`, - ); - } - - restorePhpBlock(content: any) { - return this.restoreRawPhpBlock(content).then((target) => - this.restoreRawPropsBlock(target), - ); - } - - async restoreRawPhpBlock(content: any) { - return replaceAsync( - content, - new RegExp(`${this.getRawPlaceholder("(\\d+)")}`, "gm"), - async (match: any, p1: number) => { - let rawBlock = this.rawBlocks[p1]; - const placeholder = this.getRawPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const isOnSingleLine = this.isInline(rawBlock); - const isMultipleStatements = await this.isMultilineStatement(rawBlock); - if (isOnSingleLine && isMultipleStatements) { - // multiple statements on a single line - rawBlock = ( - await util.formatStringAsPhp(``, this.options) - ).trim(); - } else if (isMultipleStatements) { - // multiple statments on mult lines - - const indentLevel = indent.amount + this.indentSize; - rawBlock = ( - await util.formatStringAsPhp(``, { - ...this.options, - useProjectPrintWidth: true, - adjustPrintWidthBy: indentLevel, - }) - ).trimEnd(); - } else if (!isOnSingleLine) { - // single statement on mult lines - rawBlock = ( - await util.formatStringAsPhp(``, this.options) - ).trimEnd(); - } else { - // single statement on single line - rawBlock = ``; - } - - return _.replace( - rawBlock, - /^(\s*)?<\?php(.*?)\?>/gms, - (_matched: any, _q1: any, q2: any) => { - if (this.isInline(rawBlock)) { - return `@php${q2}@endphp`; - } - - let preserved = this.preserveStringLiteralInPhp(q2); - preserved = this.preservePhpComment(preserved); - let indented = this.indentRawBlock(indent, preserved); - indented = this.restorePhpComment(indented); - const restored = this.restoreStringLiteralInPhp(indented); - - return `@php${restored}@endphp`; - }, - ); - }, - ); - } - - async restoreRawPropsBlock(content: any) { - const regex = this.getRawPropsPlaceholder("(\\d+)"); - return replaceAsync( - content, - new RegExp(regex, "gms"), - async (_match: any, p1: any) => { - const placeholder = this.getRawPropsPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const formatted = `@props(${( - await util.formatRawStringAsPhp(this.rawPropsBlocks[p1], { - ...this.options, - }) - ).trim()})`; - - return this.indentRawPhpBlock(indent, formatted); - }, - ); - } - - isInline(content: any) { - return _.split(content, "\n").length === 1; - } - - async isMultilineStatement(rawBlock: any) { - return ( - (await util.formatStringAsPhp(``, this.options)) - .trimRight() - .split("\n").length > 1 - ); - } - - indentRawBlock(indent: detectIndent.Indent, content: any) { - if (this.isInline(content)) { - return `${indent.indent}${content}`; - } - - const leftIndentAmount = indent.amount; - const indentLevel = leftIndentAmount / this.indentSize; - const prefix = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : (indentLevel + 1) * this.indentSize, - ); - const prefixForEnd = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - - const lines = content.split("\n"); - - return _.chain(lines) - .map((line: any, index: any) => { - if (index === 0) { - return line.trim(); - } - - if (index === lines.length - 1) { - return prefixForEnd + line; - } - - if (line.length === 0) { - return line; - } - - return prefix + line; - }) - .join("\n") - .value(); - } - - indentBladeDirectiveBlock(indent: detectIndent.Indent, content: any) { - if (_.isEmpty(indent.indent)) { - return content; - } - - if (this.isInline(content)) { - return `${indent.indent}${content}`; - } - - const leftIndentAmount = indent.amount; - const indentLevel = leftIndentAmount / this.indentSize; - const prefixSpaces = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - const prefixForEnd = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - - const lines = content.split("\n"); - - return _.chain(lines) - .map((line: any, index: any) => { - if (index === lines.length - 1) { - return prefixForEnd + line; - } - - return prefixSpaces + line; - }) - .value() - .join("\n"); - } - - indentScriptBlock(indent: detectIndent.Indent, content: any) { - if (_.isEmpty(indent.indent)) { - return content; - } - - if (this.isInline(content)) { - return `${content}`; - } - - const leftIndentAmount = indent.amount; - const indentLevel = leftIndentAmount / this.indentSize; - const prefixSpaces = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - const prefixForEnd = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - - const preserved = _.replace(content, /`.*?`/gs, (match: any) => - this.storeTemplatingString(match), - ); - - const lines = preserved.split("\n"); - - const indented = _.chain(lines) - .map((line: any, index: any) => { - if (index === 0) { - return line; - } - - if (index === lines.length - 1) { - return prefixForEnd + line; - } - - if (_.isEmpty(line)) { - return line; - } - - return prefixSpaces + line; - }) - .value() - .join("\n"); - - return this.restoreTemplatingString(`${indented}`); - } - - indentRawPhpBlock(indent: detectIndent.Indent, content: any) { - if (_.isEmpty(indent.indent)) { - return content; - } - - if (this.isInline(content)) { - return `${content}`; - } - - const leftIndentAmount = indent.amount; - const indentLevel = leftIndentAmount / this.indentSize; - const prefixSpaces = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - - const lines = content.split("\n"); - - return _.chain(lines) - .map((line: any, index: any) => { - if (index === 0) { - return line.trim(); - } - - return prefixSpaces + line; - }) - .value() - .join("\n"); - } - - indentComponentAttribute(prefix: string, content: string) { - if (_.isEmpty(prefix)) { - return content; - } - - if (this.isInline(content)) { - return `${content}`; - } - - if (this.isInline(content) && /\S/.test(prefix)) { - return `${content}`; - } - - const leftIndentAmount = detectIndent(prefix).amount; - const indentLevel = leftIndentAmount / this.indentSize; - const prefixSpaces = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - - const lines = content.split("\n"); - - return _.chain(lines) - .map((line: any, index: any) => { - if (index === 0) { - return line.trim(); - } - - return prefixSpaces + line; - }) - .value() - .join("\n"); - } - - indentPhpComment(indent: detectIndent.Indent, content: string) { - if (_.isEmpty(indent.indent)) { - return content; - } - - if (this.isInline(content)) { - return `${content}`; - } - - const leftIndentAmount = indent.amount; - const indentLevel = leftIndentAmount / this.indentSize; - const prefixSpaces = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel * this.indentSize, - ); - - const lines = content.split("\n"); - let withoutCommentLine = false; - - return _.chain(lines) - .map((line: string, index: number) => { - if (index === 0) { - return line.trim(); - } - - if (!line.trim().startsWith("*")) { - withoutCommentLine = true; - return line; - } - - if (line.trim().endsWith("*/") && withoutCommentLine) { - return line; - } - - return prefixSpaces + line; - }) - .join("\n") - .value(); - } - - restoreBladeDirectivesInStyles(content: string) { - return _.replace( - content, - /(?<=]*?(?)(.*?)(?=<\/style>)/gis, - (inside: string) => { - let result: string = inside; - - const inlineRegex = new RegExp( - `${this.getBladeDirectiveInStylePlaceholder( - "(\\d+)", - )} {\\s*?\/\\* inline_directive \\*\/\\s*?}`, - "gmi", - ); - - result = _.replace( - result, - inlineRegex, - (match: string, p1: number) => this.bladeDirectivesInStyle[p1], - ); - - const elseRegex = new RegExp( - `}\\s*?${this.getBladeDirectiveInStylePlaceholder( - "(\\d+)", - )} {\\s*?\/\\*else\\*\/`, - "gmi", - ); - - result = _.replace( - result, - elseRegex, - (match: string, p1: number) => `${this.bladeDirectivesInStyle[p1]}`, - ); - - const startRegex = new RegExp( - `${this.getBladeDirectiveInStylePlaceholder( - "(\\d+)", - )} {\\s*?\/\\*start\\*\/`, - "gmi", - ); - - result = _.replace( - result, - startRegex, - (match: string, p1: number) => `${this.bladeDirectivesInStyle[p1]}`, - ); - - const endRegex = new RegExp( - `}\\s*?\/\\* ${this.getBladeDirectiveInStylePlaceholder( - "(\\d+)", - )} \\*\/`, - "gmi", - ); - - result = _.replace( - result, - endRegex, - (match: string, p1: number) => `${this.bladeDirectivesInStyle[p1]}`, - ); - - return result; - }, - ); - } - - async restoreBladeDirectivesInScripts(content: any) { - const regex = new RegExp( - `${this.getBladeDirectivePlaceholder("(\\d+)")}`, - "gm", - ); - - // restore inline blade directive - let result = _.replace(content, regex, (_match: any, p1: number) => { - const placeholder = this.getBladeDirectivePlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - return this.indentBladeDirectiveBlock(indent, this.bladeDirectives[p1]); - }); - - result = await replaceAsync( - result, - /(?<=]*?(?)(.*?)(?=<\/script>)/gis, - async (match: string) => { - let formatted: string = match; - - // restore begin - formatted = _.replace( - formatted, - new RegExp( - `if \\( \\/\\*(?:(?:${this.getBladeDirectiveInScriptPlaceholder( - "(\\d+)", - )}).*?)\\*\\/ \\) \\{`, - "gis", - ), - (_match: any, p1: any) => `${this.directivesInScript[p1]}`, - ); - - // restore else - formatted = _.replace( - formatted, - new RegExp( - `} \\/\\* (?:${this.getBladeDirectiveInScriptPlaceholder( - "(\\d+)", - )}) \\*\\/ {(\\s*?\\(___directive_condition_\\d+___\\))?`, - "gim", - ), - (_match: any, p1: number, p2: string) => { - if (_.isUndefined(p2)) { - return `${this.directivesInScript[p1].trim()}`; - } - - return `${this.directivesInScript[p1].trim()} ${(p2 ?? "").trim()}`; - }, - ); - - // restore end - formatted = _.replace( - formatted, - new RegExp( - `} \\/\\*(?:${this.getBladeDirectiveInScriptPlaceholder( - "(\\d+)", - )})\\*\\/`, - "gis", - ), - (_match: any, p1: any) => `${this.directivesInScript[p1]}`, - ); - - // restore php block - formatted = await replaceAsync( - formatted, - new RegExp(`${this.getRawPlaceholder("(\\d+)")}`, "gm"), - // eslint-disable-next-line no-shadow - async (match: any, p1: number) => { - let rawBlock = this.rawBlocks[p1]; - const placeholder = this.getRawPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - if ( - this.isInline(rawBlock) && - (await this.isMultilineStatement(rawBlock)) - ) { - rawBlock = ( - await util.formatStringAsPhp( - ``, - this.options, - ) - ).trim(); - } else if (rawBlock.split("\n").length > 1) { - rawBlock = ( - await util.formatStringAsPhp(``, this.options) - ).trim(); - } else { - rawBlock = ``; - } - - return _.replace( - rawBlock, - /^(\s*)?<\?php(.*?)\?>/gms, - (_matched: any, _q1: any, q2: any) => { - if (this.isInline(rawBlock)) { - return `@php${q2}@endphp`; - } - - const preserved = this.preserveStringLiteralInPhp(q2); - const indented = this.indentRawBlock(indent, preserved); - const restored = this.restoreStringLiteralInPhp(indented); - - return `@php${restored}@endphp`; - }, - ); - }, - ); - - // delete place holder - formatted = _.replace( - formatted, - /(?<=[\S]+)(\s*?)\/\*\*\*script_placeholder\*\*\*\/(\s)?/gim, - (_match: any, p1: string, p2: string) => { - if (p2 !== undefined) { - return p2; - } - - const group1 = p1 ?? ""; - const group2 = p2 ?? ""; - - return group1 + group2; - }, - ); - - return formatted; - }, - ); - - if (regex.test(result)) { - result = await this.restoreBladeDirectivesInScripts(result); - } - - return result; - } - - async formatPreservedBladeDirectives(directives: any) { - return Aigle.map(directives, async (content: any) => { - const formattedAsHtml = await this.formatAsHtml(content); - const formatted = await this.formatAsBlade(formattedAsHtml); - return formatted.trimRight("\n"); - }); - } - - restoreBladeComment(content: any) { - return new Promise((resolve) => resolve(content)).then((res: any) => - _.replace( - res, - new RegExp(`${this.getBladeCommentPlaceholder("(\\d+)")}`, "gms"), - (_match: any, p1: any) => - this.bladeComments[p1] - .replace(/{{--(?=\S)/g, "{{-- ") - .replace(/(?<=\S)--}}/g, " --}}"), - ), - ); - } - - restoreXslot(content: string) { - return _.replace( - content, - /x-slot\s*--___(\d+)___--/gms, - (_match: string, p1: number) => this.xSlot[p1], - ).replace(/(?<=)/gm, () => ""); - } - - restorePhpComment(content: string) { - return _.replace( - content, - new RegExp(`${this.getPhpCommentPlaceholder("(\\d+)")};{0,1}`, "gms"), - (_match: string, p1: number) => { - const placeholder = this.getPhpCommentPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - const formatted = formatPhpComment(this.phpComments[p1]); - - return this.indentPhpComment(indent, formatted); - }, - ); - } - - async restoreEscapedBladeDirective(content: any) { - return new Promise((resolve) => resolve(content)).then((res: any) => - _.replace( - res, - new RegExp( - `${this.getEscapedBladeDirectivePlaceholder("(\\d+)")}`, - "gms", - ), - (_match: string, p1: number) => this.escapedBladeDirectives[p1], - ), - ); - } - - async restoreBladeBrace(content: any) { - return new Promise((resolve) => resolve(content)).then((res: any) => - replaceAsync( - res, - new RegExp(`${this.getBladeBracePlaceholder("(\\d+)")}`, "gm"), - async (_match: string, p1: number) => { - const placeholder = this.getBladeBracePlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - const bladeBrace = this.bladeBraces[p1]; - - if (bladeBrace.trim() === "") { - return `{{${bladeBrace}}}`; - } - - if (this.isInline(bladeBrace)) { - return `{{ ${( - await util.formatRawStringAsPhp(bladeBrace, { - ...this.options, - trailingCommaPHP: false, - printWidth: util.printWidthForInline, - }) - ) - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .split("\n") - .map((line) => line.trim()) - .join("") - // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. - .trimRight("\n")} }}`; - } - - return `{{ ${this.indentRawPhpBlock( - indent, - (await util.formatRawStringAsPhp(bladeBrace, this.options)) - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .trim() - .trimEnd(), - )} }}`; - }, - ), - ); - } - - async restoreRawBladeBrace(content: any) { - return new Promise((resolve) => resolve(content)).then((res) => - replaceAsync( - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message - res, - new RegExp(`${this.getRawBladeBracePlaceholder("(\\d+)")}`, "gms"), - async (_match: any, p1: any) => { - const placeholder = this.getRawBladeBracePlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - const bladeBrace = this.rawBladeBraces[p1]; - - if (bladeBrace.trim() === "") { - return `{!!${bladeBrace}!!}`; - } - - return this.indentRawPhpBlock( - indent, - `{!! ${(await util.formatRawStringAsPhp(bladeBrace, this.options)) - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .trim()} !!}`, - ); - }, - ), - ); - } - - restoreInlineDirective(content: any) { - return new Promise((resolve) => resolve(content)).then((res) => - _.replace( - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message - res, - new RegExp(`${this.getInlinePlaceholder("(\\d+)")}`, "gms"), - (_match: any, p1: any) => { - const matched = this.inlineDirectives[p1]; - return matched; - }, - ), - ); - } - - async restoreConditions(content: any) { - return new Promise((resolve) => resolve(content)).then((res: any) => - replaceAsync( - res, - new RegExp(`${this.getConditionPlaceholder("(\\d+)")}`, "gms"), - async (_match: any, p1: any) => { - const placeholder = this.getConditionPlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const matched = this.conditions[p1]; - - return this.formatExpressionInsideBladeDirective(matched, indent); - }, - ), - ); - } - - restoreUnbalancedDirective(content: any) { - return new Promise((resolve) => resolve(content)).then((res: any) => - _.replace( - res, - /@if \(unbalanced___(\d+)___\)/gms, - (_match: any, p1: any) => { - const matched = this.unbalancedDirectives[p1]; - return matched; - }, - ), - ); - } - - async restoreInlinePhpDirective(content: any) { - return new Promise((resolve) => resolve(content)).then((res) => - replaceAsync( - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message - res, - new RegExp(`${this.getInlinePhpPlaceholder("(\\d+)")}`, "gm"), - async (_match: any, p1: any) => { - const matched = this.inlinePhpDirectives[p1]; - const placeholder = this.getInlinePhpPlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - if (matched.includes("@php")) { - return `${( - await util.formatRawStringAsPhp(matched, { - ...this.options, - printWidth: util.printWidthForInline, - }) - ) - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .trim() - // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. - .trimRight("\n")}`; - } - - if (new RegExp(inlinePhpDirectives.join("|"), "gi").test(matched)) { - const formatted = replaceAsync( - matched, - new RegExp( - `(?<=@(${_.map(inlinePhpDirectives, (token) => - token.substring(1), - ).join("|")}).*?\\()(.*)(?=\\))`, - "gis", - ), - async (match2: any, p3: any, p4: any) => { - let wrapLength = this.wrapLineLength; - - if (["button", "class"].includes(p3)) { - wrapLength = 80; - } - - if (p3 === "include") { - wrapLength = - this.wrapLineLength - - "func".length - - p1.length - - indent.amount; - } - - return this.formatExpressionInsideBladeDirective( - p4, - indent, - wrapLength, - ); - }, - ); - - return formatted; - } - - return `${( - await util.formatRawStringAsPhp(matched, { - ...this.options, - printWidth: util.printWidthForInline, - }) - ).trimEnd()}`; - }, - ), - ); - } - - async restoreRawPhpTags(content: any) { - return new Promise((resolve) => resolve(content)).then((res) => - replaceAsync( - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message - res, - new RegExp(`${this.getRawPhpTagPlaceholder("(\\d+)")}`, "gms"), - async (_match: any, p1: any) => { - // const result= this.rawPhpTags[p1]; - try { - const matched = this.rawPhpTags[p1]; - const commentBlockExists = - /(?<=<\?php\s*?)\/\*.*?\*\/(?=\s*?\?>)/gim.test(matched); - const inlinedComment = commentBlockExists && this.isInline(matched); - const placeholder = this.getRawPhpTagPlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - if (inlinedComment) { - return matched; - } - - const result = ( - await util.formatStringAsPhp(this.rawPhpTags[p1], this.options) - ) - .trim() - .trimEnd(); - - if (this.isInline(result)) { - return result; - } - - let preserved = this.preservePhpComment(result); - - if (indent.indent) { - preserved = this.indentRawPhpBlock(indent, preserved); - } - - const restored = this.restorePhpComment(preserved); - - return restored; - } catch (e) { - return `${this.rawPhpTags[p1]}`; - } - }, - ), - ); - } - - restoreNonnativeScripts(content: string) { - return _.replace( - content, - new RegExp(`${this.getNonnativeScriptPlaceholder("(\\d+)")}`, "gmi"), - (_match: any, p1: number) => `${this.nonnativeScripts[p1]}`, - ); - } - - restoreScripts(content: any) { - return new Promise((resolve) => resolve(content)).then((res) => - _.replace( - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message - res, - new RegExp(`${this.getScriptPlaceholder("(\\d+)")}`, "gim"), - (_match: any, p1: number) => { - const script = this.scripts[p1]; - - const placeholder = this.getScriptPlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - const useTabs = util.optional(this.options).useTabs || false; - - const options = { - indent_size: util.optional(this.options).indentSize || 4, - wrap_line_length: util.optional(this.options).wrapLineLength || 120, - wrap_attributes: - util.optional(this.options).wrapAttributes || "auto", - wrap_attributes_min_attrs: util.optional(this.options) - .wrapAttributesMinAttrs, - indent_inner_html: - util.optional(this.options).indentInnerHtml || false, - extra_liners: util.optional(this.options).extraLiners, - indent_with_tabs: useTabs, - end_with_newline: false, - templating: ["php"], - }; - - if (useTabs) { - return this.indentScriptBlock( - indent, - _.replace( - beautify.html_beautify(script, options), - /\t/g, - "\t".repeat(this.indentSize), - ), - ); - } - - return this.indentScriptBlock( - indent, - beautify.html_beautify(script, options), - ); - }, - ), - ); - } - - async restoreCustomDirective(content: string) { - return this.restoreInlineCustomDirective(content) - .then((data: string) => this.restoreBeginCustomDirective(data)) - .then((data: string) => this.restoreElseCustomDirective(data)) - .then((data: string) => this.restoreEndCustomDirective(data)); - } - - async restoreInlineCustomDirective(content: string) { - return replaceAsync( - content, - new RegExp( - `${this.getInlineCustomDirectivePlaceholder("(\\d+)")}`, - "gim", - ), - async (_match: any, p1: number) => { - const placeholder = this.getInlineCustomDirectivePlaceholder( - p1.toString(), - ); - const matchedLine = content.match( - new RegExp(`^(.*?)${_.escapeRegExp(placeholder)}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const matched = `${this.customDirectives[p1]}`; - - return replaceAsync( - matched, - /(@[a-zA-z0-9\-_]+)(.*)/gis, - async (match2: string, p2: string, p3: string) => { - try { - const formatted = ( - await util.formatRawStringAsPhp(`func${p3}`, { - ...this.options, - printWidth: util.printWidthForInline, - }) - ) - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .replace(/,(\s*?\))$/gm, (_m, p4) => p4) - .trim() - .substring(4); - return `${p2}${this.indentComponentAttribute( - indent.indent, - formatted, - )}`; - } catch (error) { - return `${match2}`; - } - }, - ); - }, - ); - } - - async restoreBeginCustomDirective(content: string) { - return replaceAsync( - content, - new RegExp( - `@customdirective\\(___(\\d+)___\\)\\s*?(${nestedParenthesisRegex})*`, - "gim", - ), - async (_match: any, p1: number) => { - const placeholder = this.getBeginCustomDirectivePlaceholder( - p1.toString(), - ); - const matchedLine = content.match( - new RegExp(`^(.*?)${_.escapeRegExp(placeholder)}`, "gmi"), - ) ?? [""]; - - const indent = detectIndent(matchedLine[0]); - const matched = `${this.customDirectives[p1]}`; - - return replaceAsync( - matched, - /(@[a-zA-z0-9\-_]+)(.*)/gis, - async (match2: string, p3: string, p4: string) => { - try { - const formatted = ( - await util.formatRawStringAsPhp(`func${p4}`, { - ...this.options, - trailingCommaPHP: false, - }) - ) - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .trim() - .substring(4); - return `${p3}${this.indentComponentAttribute( - indent.indent, - formatted, - )}`; - } catch (error) { - return `${match2}`; - } - }, - ); - }, - ); - } - - async restoreElseCustomDirective(content: string) { - return _.replace( - content, - /@else\(___(\d+)___\)/gim, - (_match: any, p1: number) => `${this.customDirectives[p1]}`, - ); - } - - async restoreEndCustomDirective(content: string) { - return _.replace( - content, - /@endcustomdirective\(___(\d+)___\)/gim, - (_match: any, p1: number) => `${this.customDirectives[p1]}`, - ); - } - - async restoreHtmlTags(content: any) { - return _.replace( - content, - new RegExp(`${this.getHtmlTagPlaceholder("(\\d+)")}`, "gim"), - (_match: any, p1: number) => { - const placeholder = this.getHtmlTagPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const options = { - indent_size: util.optional(this.options).indentSize || 4, - wrap_line_length: util.optional(this.options).wrapLineLength || 120, - wrap_attributes: util.optional(this.options).wrapAttributes || "auto", - wrap_attributes_min_attrs: util.optional(this.options) - .wrapAttributesMinAttrs, - indent_inner_html: - util.optional(this.options).indentInnerHtml || false, - extra_liners: util.optional(this.options).extraLiners, - end_with_newline: false, - templating: ["php"], - }; - - const matched = this.htmlTags[p1]; - const openingTag = _.first( - matched.match(/(<(textarea|pre).*?(?)(?=.*?<\/\2>)/gis), - ); - - if (openingTag === undefined) { - return `${this.indentScriptBlock( - indent, - beautify.html_beautify(matched, options), - )}`; - } - - const restofTag = matched.substring(openingTag.length, matched.length); - - return `${this.indentScriptBlock( - indent, - beautify.html_beautify(openingTag, options), - )}${restofTag}`; - }, - ); - } - - restoreHtmlAttributes(content: string) { - return _.replace( - content, - // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. - new RegExp(`${this.getHtmlAttributePlaceholder("(\\d+)")}`, "gms"), - (_match: string, p1: number) => this.htmlAttributes[p1], - ); - } - - restoreXData(content: any) { - return _.replace( - content, - new RegExp(`${this.getXDataPlaceholder("(\\d+)")}`, "gm"), - (_match: any, p1: any) => { - const placeholder = this.getXDataPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const lines = this.formatJS(this.xData[p1]).split("\n"); + currentIndentLevel: number; - const indentLevel = - indent.amount / (this.indentCharacter === "\t" ? 4 : 1); + diffs: any; - const firstLine = lines[0]; - const prefix = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel, - ); - const offsettedLines = lines.map((line) => prefix + line); - offsettedLines[0] = firstLine; - return `${offsettedLines.join("\n")}`; - }, - ); - } + indentCharacter: any; - restoreXInit(content: any) { - return _.replace( - content, - new RegExp(`${this.getXInitPlaceholder("(\\d+)")}`, "gm"), - (_match: any, p1: number) => { - const placeholder = this.getXInitPlaceholder(p1.toString()); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); + indentSize: any; - const lines = this.formatJS(this.xInit[p1]).split("\n"); + oniguruma: any; - const indentLevel = - indent.amount / (this.indentCharacter === "\t" ? 4 : 1); + options: FormatterOption & CLIOption; - const firstLine = lines[0]; - const prefix = this.indentCharacter.repeat( - indentLevel < 0 ? 0 : indentLevel, - ); - const offsettedLines = lines.map((line) => prefix + line); - offsettedLines[0] = firstLine; - return `${offsettedLines.join("\n")}`; - }, - ); - } + result: any; - restoreTemplatingString(content: any) { - return _.replace( - content, - new RegExp(`${this.getTemplatingStringPlaceholder("(\\d+)")}`, "gms"), - (_match: any, p1: any) => this.templatingStrings[p1], - ); - } + shouldBeIndent: any; - restoreStringLiteralInPhp(content: any) { - return _.replace( - content, - new RegExp(`${this.getStringLiteralInPhpPlaceholder("(\\d+)")}`, "gms"), - (_match: any, p1: any) => this.stringLiteralInPhp[p1], - ); - } + stack: any; - async restoreComponentAttribute(content: string) { - return replaceAsync( - content, - new RegExp(`${this.getComponentAttributePlaceholder("(\\d+)")}`, "gim"), - async (_match: any, p1: any) => { - const placeholder = this.getComponentAttributePlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); + vsctm: any; - const matched = this.componentAttributes[p1]; - const formatted = await replaceAsync( - matched, - /(:{1,2}.*?=)(["'])(.*?)(?=\2)/gis, - async (match, p2: string, p3: string, p4: string) => { - if (p4 === "") { - return match; - } + wrapAttributes: any; - if (matchedLine[0].startsWith(" { - const placeholder = this.getShorthandBindingPlaceholder(p1); - const matchedLine = content.match( - new RegExp(`^(.*?)${placeholder}`, "gmi"), - ) ?? [""]; - const indent = detectIndent(matchedLine[0]); - - const matched = this.shorthandBindings[p1]; - - const formatted = _.replace( - matched, - /(:{1,2}.*?=)(["'])(.*?)(?=\2)/gis, - (match, p2: string, p3: string, p4: string) => { - const beautifyOpts: JSBeautifyOptions = { - wrap_line_length: this.wrapLineLength - indent.amount, - brace_style: "preserve-inline", - }; - - if (p4 === "") { - return match; - } - - if (this.isInline(p4)) { - try { - return `${p2}${p3}${beautify - .js_beautify(p4.trim(), beautifyOpts) - .trim()}`; - } catch (error) { - return `${p2}${p3}${p4.trim()}`; - } - } - - return `${p2}${p3}${beautify - .js_beautify(p4.trim(), beautifyOpts) - .trim()}`; - }, - ); - - return `${this.indentComponentAttribute(indent.indent, formatted)}`; + initializeOptions(options: BladeFormatterOption) { + this.options = { + ...{ + noPhpSyntaxCheck: false, + trailingCommaPHP: !options.noTrailingCommaPhp, + printWidth: options.wrapLineLength || constants.defaultPrintWidth, }, - ); + ...options, + }; + this.vsctm = util.optional(this.options).vsctm || vscodeTmModule; + this.oniguruma = util.optional(this.options).oniguruma; + this.indentCharacter = util.optional(this.options).useTabs ? "\t" : " "; + this.indentSize = util.optional(this.options).indentSize || 4; + this.wrapLineLength = + util.optional(this.options).wrapLineLength || constants.defaultPrintWidth; + this.wrapAttributes = util.optional(this.options).wrapAttributes || "auto"; + this.endOfLine = util.getEndOfLine(util.optional(this.options).endOfLine); } - async formatAsBlade(content: any) { - // init parameters + initializeProperties() { this.currentIndentLevel = 0; this.shouldBeIndent = false; + this.isInsideCommentBlock = false; + this.stack = []; + this.result = []; + this.diffs = []; + this.defaultPhpFormatOption = { + noPhpSyntaxCheck: this.options.noPhpSyntaxCheck, + printWidth: this.wrapLineLength, + }; + } - const splittedLines = util.splitByLines(content); - - const vsctmModule = await new vsctm.VscodeTextmate( - this.vsctm, - this.oniguruma, - ); - const registry = vsctmModule.createRegistry(); - - const formatted = registry - .loadGrammar("text.html.php.blade") - .then((grammar: any) => vsctmModule.tokenizeLines(splittedLines, grammar)) - .then((tokenizedLines: any) => - this.formatTokenizedLines(splittedLines, tokenizedLines), - ) - .catch((err: any) => { - throw err; - }); - - return formatted; + async formatContent(content: any) { + const pipeline = new FormatContentPipeline(this); + const target = await pipeline.formatContent(content); + const formattedResult = await util.checkResult(target); + return formattedResult; } async formatTokenizedLines(splittedLines: any, tokenizedLines: any) { @@ -2790,7 +103,6 @@ export default class Formatter { for (let i = 0; i < splittedLines.length; i += 1) { const originalLine = splittedLines[i]; const tokenizeLineResult = tokenizedLines[i]; - // eslint-disable-next-line no-await-in-loop await this.processLine(tokenizeLineResult, originalLine); } @@ -3009,79 +321,4 @@ export default class Formatter { decrementIndentLevel(level = 1) { this.currentIndentLevel -= level; } - - async formatExpressionInsideBladeDirective( - matchedExpression: string, - indent: detectIndent.Indent, - wrapLength: number | undefined = undefined, - ) { - const formatTarget = `func(${matchedExpression})`; - const formattedExpression = await util.formatRawStringAsPhp(formatTarget, { - ...this.options, - printWidth: wrapLength ?? this.defaultPhpFormatOption.printWidth, - }); - - if (formattedExpression === formatTarget) { - return matchedExpression; - } - - let inside = formattedExpression - .replace(/([\n\s]*)->([\n\s]*)/gs, "->") - .replace(/(? `${p1}]\n)`, - ) - .replace(/,[\n\s]*?\)/gs, ")") - .replace(/,(\s*?\))$/gm, (match, p1) => p1) - .trim(); - - if (this.options.useTabs || false) { - inside = _.replace( - inside, - /(?<=^ *) {4}/gm, - "\t".repeat(this.indentSize), - ); - } - - inside = inside.replace( - /func\((.*)\)/gis, - (match: string, p1: string) => p1, - ); - if (this.isInline(inside.trim())) { - inside = inside.trim(); - } - - return this.indentRawPhpBlock(indent, inside); - } - - formatJS(jsCode: string): string { - let code: string = jsCode; - const tempVarStore: any = { - js: [], - entangle: [], - }; - for (const directive of Object.keys(tempVarStore)) { - code = code.replace( - new RegExp( - `@${directive}\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)`, - "gs", - ), - (m: any) => { - const index = tempVarStore[directive].push(m) - 1; - return this.getPlaceholder(directive, index, m.length); - }, - ); - } - code = beautify.js_beautify(code, { brace_style: "preserve-inline" }); - - for (const directive of Object.keys(tempVarStore)) { - code = code.replace( - new RegExp(this.getPlaceholder(directive, "_*(\\d+)"), "gms"), - (_match: any, p1: any) => tempVarStore[directive][p1], - ); - } - - return code; - } } diff --git a/src/main.ts b/src/main.ts index 3dd36acb..2039fde4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,12 @@ -import ignore from "ignore"; - import fs from "node:fs"; import nodepath from "node:path"; +import process from "node:process"; import nodeutil from "node:util"; import chalk from "chalk"; import findConfig from "find-config"; import { glob } from "glob"; +import ignore from "ignore"; import _ from "lodash"; -import process from "node:process"; import type { Config as TailwindConfig } from "tailwindcss/types/config"; import FormatError from "./errors/formatError"; import Formatter from "./formatter"; @@ -55,46 +54,24 @@ export type FormatterOption = { export type BladeFormatterOption = CLIOption & FormatterOption; class BladeFormatter { - diffs: any; - - errors: any; - - formattedFiles: any; - - ignoreFile: any; - + diffs: any = []; + errors: any = []; + formattedFiles: any = []; + ignoreFile = ""; options: FormatterOption & CLIOption; - - outputs: any; - - currentTargetPath: string; - - paths: any; - - targetFiles: any; - - fulFillFiles: any; - - static targetFiles: any; - + outputs: any = []; + currentTargetPath = "."; + paths: any = []; + targetFiles: any = []; + fulFillFiles: any = []; + static targetFiles: any = []; runtimeConfigPath: string | null; - - runtimeConfigCache: RuntimeConfig; + runtimeConfigCache: RuntimeConfig = {}; constructor(options: BladeFormatterOption = {}, paths: any = []) { - this.currentTargetPath = "."; - this.paths = paths; this.options = options; - this.targetFiles = []; - this.errors = []; - this.diffs = []; - this.outputs = []; - this.formattedFiles = []; - this.ignoreFile = ""; - this.fulFillFiles = []; - this.targetFiles = []; + this.paths = paths; this.runtimeConfigPath = options.runtimeConfigPath ?? null; - this.runtimeConfigCache = {}; } async format(content: any, opts: BladeFormatterOption = {}) { @@ -119,7 +96,6 @@ class BladeFormatter { } } - // eslint-disable-next-line class-methods-use-this fileExists(filepath: string) { return fs.promises .access(filepath, fs.constants.F_OK) @@ -129,19 +105,12 @@ class BladeFormatter { async readIgnoreFile(filePath: string) { const configFilename = ".bladeignore"; + const workingDir = nodepath.dirname(filePath); + const configFilePath = + this.options.ignoreFilePath || + findConfig(configFilename, { cwd: workingDir }); - let configFilePath: string | null; - const worakingDir = nodepath.dirname(filePath); - - if (this.options.ignoreFilePath) { - configFilePath = this.options.ignoreFilePath; - } else { - configFilePath = findConfig(configFilename, { cwd: worakingDir }); - } - - if (!configFilePath) { - return; - } + if (!configFilePath) return; try { this.ignoreFile = (await fs.promises.readFile(configFilePath)).toString(); @@ -151,118 +120,94 @@ class BladeFormatter { } async findTailwindConfig(filePath: string) { - if (!this.options.sortTailwindcssClasses) { - return; - } + if (!this.options.sortTailwindcssClasses) return; const configFilename = "tailwind.config.js"; - let configFilePath: string | null | undefined; if (this.options.tailwindcssConfigPath) { - if (this.runtimeConfigPath) { - const workingDir = nodepath.dirname(this.runtimeConfigPath); - configFilePath = nodepath.resolve( - workingDir, - this.options.tailwindcssConfigPath, - ); - } else if (nodepath.isAbsolute(this.options.tailwindcssConfigPath)) { - configFilePath = nodepath.resolve(this.options.tailwindcssConfigPath); - } else { - configFilePath = nodepath.resolve(this.options.tailwindcssConfigPath); - } + const workingDir = this.runtimeConfigPath + ? nodepath.dirname(this.runtimeConfigPath) + : ""; + configFilePath = nodepath.resolve( + workingDir, + this.options.tailwindcssConfigPath, + ); } else { - // lookup tailwind config const workingDir = nodepath.dirname(filePath); configFilePath = findConfig(configFilename, { cwd: workingDir }); } - if (!configFilePath) { - return; + if (configFilePath) { + this.options.tailwindcssConfigPath = configFilePath; } - - this.options.tailwindcssConfigPath = configFilePath; } async readRuntimeConfig( filePath: string, ): Promise { - if (_.isEmpty(this.runtimeConfigCache)) { + if (!_.isEmpty(this.runtimeConfigCache)) { this.options = _.merge(this.options, this.runtimeConfigCache); } - let configFile: string | null; - - if (this.options.runtimeConfigPath) { - configFile = this.options.runtimeConfigPath; - } else { - configFile = findRuntimeConfig(filePath); - } - - if (_.isNull(configFile)) { - return; - } + const configFile = + this.options.runtimeConfigPath || findRuntimeConfig(filePath); + if (!configFile) return; this.runtimeConfigPath = configFile; try { const options = await readRuntimeConfig(configFile); - - this.options = _.mergeWith(this.options, options, (obj, src) => { - if (!_.isNil(src)) { - return src; - } - - return obj; - }); - + this.options = _.mergeWith(this.options, options, (obj, src) => + !_.isNil(src) ? src : obj, + ); this.runtimeConfigCache = this.options; if (this.options.sortTailwindcssClasses) { await this.findTailwindConfig(filePath); } } catch (error: any) { - if (error instanceof SyntaxError) { - process.stdout.write( - chalk.red.bold("\nBlade Formatter JSON Syntax Error: \n\n"), - ); - process.stdout.write(nodeutil.format(error)); - process.exit(1); - } + this.handleConfigError(configFile, error); + } + } + handleConfigError(configFile: string, error: any) { + if (error instanceof SyntaxError) { process.stdout.write( - chalk.red.bold( - `\nBlade Formatter Config Error: ${nodepath.basename( - configFile, - )}\n\n`, - ), + chalk.red.bold("\nBlade Formatter JSON Syntax Error: \n\n"), ); - process.stdout.write( - `\`${error.errors[0].instancePath.replace("/", "")}\` ${ - error.errors[0].message - }\n\n`, - ); - if (error.errors[0].params?.allowedValues) { - console.log(error.errors[0].params?.allowedValues); - } + process.stdout.write(nodeutil.format(error)); process.exit(1); } + + process.stdout.write( + chalk.red.bold( + `\nBlade Formatter Config Error: ${nodepath.basename(configFile)}\n\n`, + ), + ); + process.stdout.write( + `\`${error.errors[0].instancePath.replace("/", "")}\` ${error.errors[0].message}\n\n`, + ); + if (error.errors[0].params?.allowedValues) { + console.log(error.errors[0].params?.allowedValues); + } + process.exit(1); } async processPaths() { await Promise.all( - _.map(this.paths, async (path: any) => this.processPath(path)), + this.paths.map(async (path: any) => this.processPath(path)), ); } async processPath(path: any) { - await BladeFormatter.globFiles(path) + const paths = await BladeFormatter.globFiles(path) .then((paths: any) => - _.map(paths, (target: any) => nodepath.relative(".", target)), + paths.map((target: any) => nodepath.relative(".", target)), ) .then((paths) => this.filterFiles(paths)) - .then(this.fulFillFiles) - .then((paths) => this.formatFiles(paths)); + .then(this.fulFillFiles); + await this.formatFiles(paths); } static globFiles(path: any) { @@ -270,17 +215,13 @@ class BladeFormatter { } async filterFiles(paths: any) { - if (this.ignoreFile === "") { - return paths; - } + if (this.ignoreFile === "") return paths; const REGEX_FILES_NOT_IN_CURRENT_DIR = /^\.\.*/; - const filesOutsideTargetDir = _.filter(paths, (path: any) => + const filesOutsideTargetDir = paths.filter((path: any) => REGEX_FILES_NOT_IN_CURRENT_DIR.test(nodepath.relative(".", path)), ); - const filesUnderTargetDir = _.xor(paths, filesOutsideTargetDir); - const filteredFiles = ignore() .add(this.ignoreFile) .filter(filesUnderTargetDir); @@ -290,96 +231,76 @@ class BladeFormatter { static fulFillFiles(paths: any) { BladeFormatter.targetFiles.push(paths); - return Promise.resolve(paths); } async formatFiles(paths: any) { - await Promise.all(_.map(paths, async (path: any) => this.formatFile(path))); + await Promise.all(paths.map(async (path: any) => this.formatFile(path))); } async formatFile(path: any) { await this.findTailwindConfig(path); await this.readRuntimeConfig(path); - await util - .readFile(path) - .then((data: any) => Promise.resolve(data.toString("utf-8"))) - .then((content) => new Formatter(this.options).formatContent(content)) - .then((formatted) => this.checkFormatted(path, formatted)) - .then((formatted) => this.writeToFile(path, formatted)) - .catch((err) => { - this.handleError(path, err); - }); + try { + const content = await util + .readFile(path) + .then((data: any) => data.toString("utf-8")); + const formatted = await new Formatter(this.options).formatContent( + content, + ); + await this.checkFormatted(path, formatted); + await this.writeToFile(path, formatted); + } catch (err) { + this.handleError(path, err); + } } async checkFormatted(path: any, formatted: any) { this.printFormattedOutput(path, formatted); const originalContent = fs.readFileSync(path, "utf-8"); - const originalLines = util.splitByLines(originalContent); const formattedLines = util.splitByLines(formatted); - const diff = util.generateDiff(path, originalLines, formattedLines); + this.diffs.push(diff); this.outputs.push(formatted); if (diff.length > 0) { - if (this.options.progress || this.options.write) { + if (this.options.progress || this.options.write) process.stdout.write(chalk.green("F")); - } - if (this.options.checkFormatted) { process.stdout.write(`${path}\n`); process.exitCode = 1; } - this.formattedFiles.push(path); - } - - if (diff.length === 0) { - if (this.options.progress || this.options.write) { - process.stdout.write(chalk.green(".")); - } + } else if (this.options.progress || this.options.write) { + process.stdout.write(chalk.green(".")); } return Promise.resolve(formatted); } printFormattedOutput(path: any, formatted: any) { - if (this.options.write || this.options.checkFormatted) { - return; - } + if (this.options.write || this.options.checkFormatted) return; process.stdout.write(`${formatted}`); - const isLastFile = _.last(this.paths) === path || _.last(this.targetFiles) === path; - - if (isLastFile) { - return; - } - - // write divider to stdout - if (this.paths.length > 1 || this.targetFiles.length > 1) { + if (!isLastFile && (this.paths.length > 1 || this.targetFiles.length > 1)) { process.stdout.write("\n"); } } writeToFile(path: any, content: any) { - if (!this.options.write) { + if ( + !this.options.write || + this.options.checkFormatted || + !content || + _.isEmpty(content) + ) return; - } - - if (this.options.checkFormatted) { - return; - } - - // preserve original content - if (content.length === 0 || _.isNull(content) || _.isEmpty(content)) { - return; - } fs.writeFile(path, content, (err: any) => { if (err) { @@ -390,18 +311,15 @@ class BladeFormatter { } handleError(path: any, error: any) { - if (this.options.progress || this.options.write) { + if (this.options.progress || this.options.write) process.stdout.write(chalk.red("E")); - } - process.exitCode = 1; this.errors.push({ path, message: error.message, error }); } printPreamble() { - if (this.options.checkFormatted) { + if (this.options.checkFormatted) process.stdout.write("Check formatting... \n"); - } } async printResults() { @@ -412,12 +330,9 @@ class BladeFormatter { } printDescription() { - if (!this.options.write) { - return; - } + if (!this.options.write) return; - const returnLine = "\n\n"; - process.stdout.write(returnLine); + process.stdout.write("\n\n"); process.stdout.write(chalk.bold.green("Fixed: F\n")); process.stdout.write(chalk.bold.red("Errors: E\n")); process.stdout.write(chalk.bold("Not Changed: ") + chalk.bold.green(".\n")); @@ -425,59 +340,49 @@ class BladeFormatter { printFormattedFiles() { if (this.formattedFiles.length === 0) { - if (this.options.checkFormatted) { + if (this.options.checkFormatted) process.stdout.write( chalk.bold("\nAll matched files are formatted! \n"), ); - } - return; } if (!this.options.write) { if (this.options.checkFormatted) { process.stdout.write( - `\nAbove file(s) are formattable. Forgot to run formatter? Use ${chalk.bold( - "--write", - )} option to overwrite.\n`, + `\nAbove file(s) are formattable. Forgot to run formatter? Use ${chalk.bold("--write")} option to overwrite.\n`, ); } - return; } process.stdout.write(chalk.bold("\nFormatted Files: \n")); - _.each(this.formattedFiles, (path: any) => - process.stdout.write(`${chalk.bold(path)}\n`), - ); + for (const path of this.formattedFiles) { + process.stdout.write(`${chalk.bold(path)}\n`); + } } printDifferences() { - if (!this.options.diff) { - return; - } + if (!this.options.diff) return; process.stdout.write(chalk.bold("\nDifferences: \n\n")); - - if (_.filter(this.diffs, (diff: any) => diff.length > 0).length === 0) { + if (this.diffs.every((diff: any) => diff.length === 0)) { process.stdout.write(chalk("No changes found. \n\n")); - return; } - _.each(this.diffs, (diff: any) => util.printDiffs(diff)); + for (const diff of this.diffs) { + util.printDiffs(diff); + } } printErrors() { - if (_.isEmpty(this.errors)) { - return; - } + if (_.isEmpty(this.errors)) return; process.stdout.write(chalk.red.bold("\nErrors: \n\n")); - - _.each(this.errors, (error: any) => - process.stdout.write(`${nodeutil.format(error)}\n`), - ); + for (const error of this.errors) { + process.stdout.write(`${nodeutil.format(error)}\n`); + } } } diff --git a/src/processors/adjustSpacesProcessor.ts b/src/processors/adjustSpacesProcessor.ts new file mode 100644 index 00000000..d01b313f --- /dev/null +++ b/src/processors/adjustSpacesProcessor.ts @@ -0,0 +1,28 @@ +import _ from "lodash"; +import { nestedParenthesisRegex } from "src/regex"; +import { Processor } from "./processor"; + +export class AdjustSpacesProcessor extends Processor { + private bladeComments: string[] = []; + + async preProcess(content: string): Promise {} + + async postProcess(content: string): Promise { + return await this.adjustSpaces(content); + } + + private async adjustSpaces(content: string): Promise { + const directivesRequiredSpace = ["@unless"]; + + return _.replace( + content, + new RegExp( + `(? `${p1} (${p2})`, + ); + } +} diff --git a/src/processors/bladeBraceProcessor.ts b/src/processors/bladeBraceProcessor.ts new file mode 100644 index 00000000..446acaaa --- /dev/null +++ b/src/processors/bladeBraceProcessor.ts @@ -0,0 +1,105 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class BladeBraceProcessor extends Processor { + private bladeBraces: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveBladeBrace(content); + } + + async postProcess(content: string): Promise { + return await this.restoreBladeBrace(content); + } + + private async preserveBladeBrace(content: string): Promise { + return _.replace(content, /\{\{(.*?)\}\}/gs, (_match: any, p1: any) => { + // if content is blank + if (p1 === "") { + return this.storeBladeBrace(p1, p1.length); + } + + // preserve a space if content contains only space, tab, or new line character + if (!/\S/.test(p1)) { + return this.storeBladeBrace(" ", " ".length); + } + + // any other content + return this.storeBladeBrace(p1.trim(), p1.trim().length); + }); + } + + private async restoreBladeBrace(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res: any) => + replaceAsync( + res, + new RegExp(`${this.getBladeBracePlaceholder("(\\d+)")}`, "gm"), + async (_match: string, p1: number) => { + const placeholder = this.getBladeBracePlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + const bladeBrace = this.bladeBraces[p1]; + + if (bladeBrace.trim() === "") { + return `{{${bladeBrace}}}`; + } + + if (util.isInline(bladeBrace)) { + return `{{ ${( + await util.formatRawStringAsPhp(bladeBrace, { + ...this.formatter.options, + trailingCommaPHP: false, + printWidth: util.printWidthForInline, + }) + ) + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .split("\n") + .map((line) => line.trim()) + .join("") + // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. + .trimRight("\n")} }}`; + } + + return `{{ ${util.indentRawPhpBlock( + indent, + ( + await util.formatRawStringAsPhp( + bladeBrace, + this.formatter.options, + ) + ) + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .trim() + .trimEnd(), + this.formatter, + )} }}`; + }, + ), + ); + } + + storeBladeBrace(value: any, length: any) { + const index = this.bladeBraces.push(value) - 1; + const brace = "{{ }}"; + return this.getBladeBracePlaceholder(index, length + brace.length); + } + + getBladeBracePlaceholder(replace: any, length = 0) { + if (length > 0) { + const template = "___blade_brace_#___"; + const gap = length - template.length; + return _.replace( + `___blade_brace_${_.repeat("_", gap > 0 ? gap : 0)}#___`, + "#", + replace, + ); + } + + return _.replace("___blade_brace_+?#___", "#", replace); + } +} diff --git a/src/processors/bladeCommentProcessor.ts b/src/processors/bladeCommentProcessor.ts new file mode 100644 index 00000000..b642bbd3 --- /dev/null +++ b/src/processors/bladeCommentProcessor.ts @@ -0,0 +1,41 @@ +import _ from "lodash"; +import { Processor } from "./processor"; + +export class BladeCommentProcessor extends Processor { + private bladeComments: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveBladeComment(content); + } + + async postProcess(content: string): Promise { + return await this.restoreBladeComment(content); + } + + private async preserveBladeComment(content: string): Promise { + return _.replace(content, /\{\{--(.*?)--\}\}/gs, (match: string) => + this.storeBladeComment(match), + ); + } + + private async restoreBladeComment(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res: any) => + _.replace( + res, + new RegExp(`${this.getBladeCommentPlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => + this.bladeComments[p1] + .replace(/{{--(?=\S)/g, "{{-- ") + .replace(/(?<=\S)--}}/g, " --}}"), + ), + ); + } + + storeBladeComment(value: string) { + return this.getBladeCommentPlaceholder(this.bladeComments.push(value) - 1); + } + + getBladeCommentPlaceholder(replace: any) { + return _.replace("___blade_comment_#___", "#", replace); + } +} diff --git a/src/processors/bladeDirectiveInScriptsProcessor.ts b/src/processors/bladeDirectiveInScriptsProcessor.ts new file mode 100644 index 00000000..41aa12be --- /dev/null +++ b/src/processors/bladeDirectiveInScriptsProcessor.ts @@ -0,0 +1,494 @@ +import Aigle from "aigle"; +import detectIndent from "detect-indent"; +import _ from "lodash"; +import { + indentElseTokens, + indentEndTokens, + indentStartOrElseTokens, + indentStartTokens, + inlineFunctionTokens, + phpKeywordStartTokens, + unbalancedStartTokens, +} from "src/indent"; +import { nestedParenthesisRegex } from "src/regex"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class BladeDirectiveInScriptsProcessor extends Processor { + private bladeDirectives: string[] = []; + private directivesInScript: string[] = []; + private rawBlocks: string[] = []; + private customDirectives: string[] = []; + private stringLiteralInPhp: string[] = []; + + async preProcess(content: string): Promise { + const result = await this.preserveBladeDirectiveInScripts(content); + + // format preserved blade directives + this.bladeDirectives = await this.formatPreservedBladeDirectives( + this.bladeDirectives, + ); + + return result; + } + + async postProcess(content: string): Promise { + const result = await this.restoreBladeDirectiveInScripts(content); + + return result; + } + + private async preserveBladeDirectiveInScripts(content: string): Promise { + return _.replace( + content, + /(?<=]*?(?)(.*?)(?=<\/script>)/gis, + (match: string) => { + const targetTokens = [...indentStartTokens, ...inlineFunctionTokens]; + + if (new RegExp(targetTokens.join("|"), "gmi").test(match) === false) { + if (/^[\s\n]+$/.test(match)) { + return match.trim(); + } + + return match; + } + + const inlineFunctionDirectives = inlineFunctionTokens.join("|"); + const inlineFunctionRegex = new RegExp( + // eslint-disable-next-line max-len + `(?!\\/\\*.*?\\*\\/)(${inlineFunctionDirectives})(\\s*?)${nestedParenthesisRegex}`, + "gmi", + ); + const endTokens = _.chain(indentEndTokens).without("@endphp"); + + let formatted: string = match; + + formatted = _.replace(formatted, inlineFunctionRegex, (matched: any) => + this.storeBladeDirective( + util.formatRawStringAsPhp(matched, { + ...this.formatter.options, + printWidth: util.printWidthForInline, + }), + ), + ); + + formatted = _.replace( + formatted, + new RegExp( + `(${indentStartTokens.join("|")})\\s*?${nestedParenthesisRegex}`, + "gis", + ), + (matched) => + `if ( /*${this.storeBladeDirectiveInScript(matched)}*/ ) {`, + ); + + formatted = _.replace( + formatted, + new RegExp( + `(${[...indentElseTokens, ...indentStartOrElseTokens].join( + "|", + )})(?!\\w+?\\s*?\\(.*?\\))`, + "gis", + ), + (matched) => + `/***script_placeholder***/} /* ${this.storeBladeDirectiveInScript( + matched, + )} */ {`, + ); + + formatted = _.replace( + formatted, + new RegExp(`(${endTokens.join("|")})`, "gis"), + (matched) => + `/***script_placeholder***/} /*${this.storeBladeDirectiveInScript( + matched, + )}*/`, + ); + + formatted = _.replace( + formatted, + /(? this.storeRawBlock(p1), + ); + + // custom directive + formatted = this.preserveCustomDirectiveInScript(formatted); + + return formatted; + }, + ); + } + + /** + * + * @param content string between + * @returns string + */ + preserveCustomDirectiveInScript(content: string): string { + const negativeLookAhead = [ + ..._.without(indentStartTokens, "@unless"), + ...indentEndTokens, + ...indentElseTokens, + ...["@unless\\(.*?\\)"], + ].join("|"); + + const inlineNegativeLookAhead = [ + ..._.without(indentStartTokens, "@unless"), + ...indentEndTokens, + ...indentElseTokens, + ...inlineFunctionTokens, + ...phpKeywordStartTokens, + ...["@unless[a-z]*\\(.*?\\)"], + ...unbalancedStartTokens, + ].join("|"); + + const inlineRegex = new RegExp( + `(?!(${inlineNegativeLookAhead}))(@([a-zA-Z1-9_\\-]+))(?!.*?@end\\3)${nestedParenthesisRegex}.*?(? + this.storeInlineCustomDirective(match), + ); + + // preserve begin~else~end directives + formatted = _.replace( + formatted, + regex, + ( + match: string, + p1: string, + p2: string, + p3: string, + p4: string, + p5: string, + p6: string, + p7: string, + ) => { + if (indentStartTokens.includes(p2)) { + return match; + } + + let result: string = match; + + result = _.replace( + result, + new RegExp(`${p2}(${nestedParenthesisRegex})*`, "gim"), + (beginStr: string) => + `if ( /*${this.storeBladeDirectiveInScript(beginStr)}*/ ) {`, + ); + + result = _.replace( + result, + new RegExp(`@else${p4}(${nestedParenthesisRegex})*`, "gim"), + (elseStr: string) => + `/***script_placeholder***/} /* ${this.storeBladeDirectiveInScript( + elseStr, + )} */ {`, + ); + result = _.replace( + result, + p7, + (endStr: string) => + `/***script_placeholder***/} /*${this.storeBladeDirectiveInScript( + endStr, + )}*/`, + ); + + return result; + }, + ); + + // replace directives recursively + if (regex.test(formatted)) { + formatted = this.preserveCustomDirectiveInScript(formatted); + } + + return formatted; + } + + private async restoreBladeDirectiveInScripts(content: string): Promise { + const regex = new RegExp( + `${this.getBladeDirectivePlaceholder("(\\d+)")}`, + "gm", + ); + + // restore inline blade directive + let result = _.replace(content, regex, (_match: any, p1: number) => { + const placeholder = this.getBladeDirectivePlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + return util.indentBladeDirectiveBlock( + indent, + this.bladeDirectives[p1], + this.formatter, + ); + }); + + result = await replaceAsync( + result, + /(?<=]*?(?)(.*?)(?=<\/script>)/gis, + async (match: string) => { + let formatted: string = match; + + // restore begin + formatted = _.replace( + formatted, + new RegExp( + `if \\( \\/\\*(?:(?:${this.getBladeDirectiveInScriptPlaceholder( + "(\\d+)", + )}).*?)\\*\\/ \\) \\{`, + "gis", + ), + (_match: any, p1: any) => `${this.directivesInScript[p1]}`, + ); + + // restore else + formatted = _.replace( + formatted, + new RegExp( + `} \\/\\* (?:${this.getBladeDirectiveInScriptPlaceholder( + "(\\d+)", + )}) \\*\\/ {(\\s*?\\(___directive_condition_\\d+___\\))?`, + "gim", + ), + (_match: any, p1: number, p2: string) => { + if (_.isUndefined(p2)) { + return `${this.directivesInScript[p1].trim()}`; + } + + return `${this.directivesInScript[p1].trim()} ${(p2 ?? "").trim()}`; + }, + ); + + // restore end + formatted = _.replace( + formatted, + new RegExp( + `} \\/\\*(?:${this.getBladeDirectiveInScriptPlaceholder( + "(\\d+)", + )})\\*\\/`, + "gis", + ), + (_match: any, p1: any) => `${this.directivesInScript[p1]}`, + ); + + // restore php block + formatted = await replaceAsync( + formatted, + new RegExp(`${this.getRawPlaceholder("(\\d+)")}`, "gm"), + // eslint-disable-next-line no-shadow + async (match: any, p1: number) => { + let rawBlock = this.rawBlocks[p1]; + const placeholder = this.getRawPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + if ( + util.isInline(rawBlock) && + (await util.isMultilineStatement(rawBlock, this.formatter)) + ) { + rawBlock = ( + await util.formatStringAsPhp( + ``, + this.formatter.options, + ) + ).trim(); + } else if (rawBlock.split("\n").length > 1) { + rawBlock = ( + await util.formatStringAsPhp( + ``, + this.formatter.options, + ) + ).trim(); + } else { + rawBlock = ``; + } + + return _.replace( + rawBlock, + /^(\s*)?<\?php(.*?)\?>/gms, + (_matched: any, _q1: any, q2: any) => { + if (util.isInline(rawBlock)) { + return `@php${q2}@endphp`; + } + + const preserved = this.preserveStringLiteralInPhp(q2); + const indented = util.indentRawBlock( + indent, + preserved, + this.formatter, + ); + const restored = this.restoreStringLiteralInPhp(indented); + + return `@php${restored}@endphp`; + }, + ); + }, + ); + + // delete place holder + formatted = _.replace( + formatted, + /(?<=[\S]+)(\s*?)\/\*\*\*script_placeholder\*\*\*\/(\s)?/gim, + (_match: any, p1: string, p2: string) => { + if (p2 !== undefined) { + return p2; + } + + const group1 = p1 ?? ""; + const group2 = p2 ?? ""; + + return group1 + group2; + }, + ); + + return formatted; + }, + ); + + if (regex.test(result)) { + result = await this.restoreBladeDirectiveInScripts(result); + } + + return result; + } + + storeBladeDirective(value: any) { + return this.getBladeDirectivePlaceholder( + this.bladeDirectives.push(value) - 1, + ); + } + + storeBladeDirectiveInScript(value: string) { + return this.getBladeDirectiveInScriptPlaceholder( + (this.directivesInScript.push(value) - 1).toString(), + ); + } + + getBladeDirectiveInScriptPlaceholder(replace: any) { + return _.replace("___directives_script_#___", "#", replace); + } + + getBladeDirectivePlaceholder(replace: any) { + return _.replace("___blade_directive_#___", "#", replace); + } + + getInlinePhpPlaceholder(replace: any) { + return _.replace("___inline_php_directive_#___", "#", replace); + } + + async formatExpressionInsideBladeDirective( + matchedExpression: string, + indent: detectIndent.Indent, + wrapLength: number | undefined = undefined, + ) { + const formatTarget = `func(${matchedExpression})`; + const formattedExpression = await util.formatRawStringAsPhp(formatTarget, { + ...this.formatter.options, + printWidth: + wrapLength ?? this.formatter.defaultPhpFormatOption.printWidth, + }); + + if (formattedExpression === formatTarget) { + return matchedExpression; + } + + let inside = formattedExpression + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .replace(/(? `${p1}]\n)`, + ) + .replace(/,[\n\s]*?\)/gs, ")") + .replace(/,(\s*?\))$/gm, (match, p1) => p1) + .trim(); + + if (this.formatter.options.useTabs || false) { + inside = _.replace( + inside, + /(?<=^ *) {4}/gm, + "\t".repeat(this.formatter.indentSize), + ); + } + + inside = inside.replace( + /func\((.*)\)/gis, + (match: string, p1: string) => p1, + ); + if (util.isInline(inside.trim())) { + inside = inside.trim(); + } + + return util.indentRawPhpBlock(indent, inside, this.formatter); + } + + storeRawBlock(value: any) { + return this.getRawPlaceholder(this.rawBlocks.push(value) - 1); + } + + getRawPlaceholder(replace: any) { + return _.replace("___raw_block_#___", "#", replace); + } + + storeInlineCustomDirective(value: string) { + return this.getInlineCustomDirectivePlaceholder( + (this.customDirectives.push(value) - 1).toString(), + ); + } + + getInlineCustomDirectivePlaceholder(replace: string) { + return _.replace("___inline_cd_#___", "#", replace); + } + + preserveStringLiteralInPhp(content: any) { + return _.replace( + content, + /(\"([^\\]|\\.)*?\"|\'([^\\]|\\.)*?\')/gm, + (match: string) => `${this.storeStringLiteralInPhp(match)}`, + ); + } + + storeStringLiteralInPhp(value: any) { + const index = this.stringLiteralInPhp.push(value) - 1; + return this.getStringLiteralInPhpPlaceholder(index); + } + + getStringLiteralInPhpPlaceholder(replace: any) { + return _.replace("'___php_content_#___'", "#", replace); + } + + restoreStringLiteralInPhp(content: any) { + return _.replace( + content, + new RegExp(`${this.getStringLiteralInPhpPlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => this.stringLiteralInPhp[p1], + ); + } + + async formatPreservedBladeDirectives(directives: any) { + return Aigle.map(directives, async (content: any) => { + const formattedAsHtml = await util.formatAsHtml(content, this.formatter); + const formatted = await util.formatAsBlade( + formattedAsHtml, + this.formatter, + ); + return formatted.trimRight("\n"); + }); + } +} diff --git a/src/processors/bladeDirectiveInStylesProcessor.ts b/src/processors/bladeDirectiveInStylesProcessor.ts new file mode 100644 index 00000000..c08153d7 --- /dev/null +++ b/src/processors/bladeDirectiveInStylesProcessor.ts @@ -0,0 +1,175 @@ +import _ from "lodash"; +import { + cssAtRuleTokens, + indentElseTokens, + indentEndTokens, + indentStartTokens, +} from "src/indent"; +import { nestedParenthesisRegex } from "src/regex"; +import { Processor } from "./processor"; + +export class BladeDirectiveInStylesProcessor extends Processor { + private bladeDirectivesInStyle: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveBladeDirectiveInStyles(content); + } + + async postProcess(content: string): Promise { + return await this.restoreBladeDirectiveInStyles(content); + } + + private async preserveBladeDirectiveInStyles(content: string): Promise { + return _.replace( + content, + /(?<=]*?(?)(.*?)(?=<\/style>)/gis, + (inside: string) => { + let result: string = inside; + + const inlineRegex = new RegExp( + `(?!${["@end", "@else", ...cssAtRuleTokens].join( + "|", + )})@(\\w+)\\s*?(?![^\\1]*@end\\1)${nestedParenthesisRegex}`, + "gmi", + ); + + result = _.replace( + result, + inlineRegex, + (match: string) => + `${this.storeBladeDirectiveInStyle( + match, + )} {/* inline_directive */}`, + ); + + const customStartRegex = new RegExp( + `(?!${["@end", "@else", ...cssAtRuleTokens].join( + "|", + )})@(\\w+)\\s*?(${nestedParenthesisRegex})`, + "gmi", + ); + + result = _.replace( + result, + customStartRegex, + (match: string) => + `${this.storeBladeDirectiveInStyle(match)} { /*start*/`, + ); + + const startRegex = new RegExp( + `(${indentStartTokens.join("|")})\\s*?(${nestedParenthesisRegex})`, + "gmi", + ); + + result = _.replace( + result, + startRegex, + (match: string) => + `${this.storeBladeDirectiveInStyle(match)} { /*start*/`, + ); + + const elseRegex = new RegExp( + `(${["@else\\w+", ...indentElseTokens].join( + "|", + )})\\s*?(${nestedParenthesisRegex})?`, + "gmi", + ); + + result = _.replace( + result, + elseRegex, + (match: string) => + `} ${this.storeBladeDirectiveInStyle(match)} { /*else*/`, + ); + + const endRegex = new RegExp( + `${["@end\\w+", ...indentEndTokens].join("|")}`, + "gmi", + ); + + result = _.replace( + result, + endRegex, + (match: string) => + `} /* ${this.storeBladeDirectiveInStyle(match)} */`, + ); + + return result; + }, + ); + } + + private async restoreBladeDirectiveInStyles(content: string): Promise { + return _.replace( + content, + /(?<=]*?(?)(.*?)(?=<\/style>)/gis, + (inside: string) => { + let result: string = inside; + + const inlineRegex = new RegExp( + `${this.getBladeDirectiveInStylePlaceholder( + "(\\d+)", + )} {\\s*?\/\\* inline_directive \\*\/\\s*?}`, + "gmi", + ); + + result = _.replace( + result, + inlineRegex, + (match: string, p1: number) => this.bladeDirectivesInStyle[p1], + ); + + const elseRegex = new RegExp( + `}\\s*?${this.getBladeDirectiveInStylePlaceholder( + "(\\d+)", + )} {\\s*?\/\\*else\\*\/`, + "gmi", + ); + + result = _.replace( + result, + elseRegex, + (match: string, p1: number) => `${this.bladeDirectivesInStyle[p1]}`, + ); + + const startRegex = new RegExp( + `${this.getBladeDirectiveInStylePlaceholder( + "(\\d+)", + )} {\\s*?\/\\*start\\*\/`, + "gmi", + ); + + result = _.replace( + result, + startRegex, + (match: string, p1: number) => `${this.bladeDirectivesInStyle[p1]}`, + ); + + const endRegex = new RegExp( + `}\\s*?\/\\* ${this.getBladeDirectiveInStylePlaceholder( + "(\\d+)", + )} \\*\/`, + "gmi", + ); + + result = _.replace( + result, + endRegex, + (match: string, p1: number) => `${this.bladeDirectivesInStyle[p1]}`, + ); + + return result; + }, + ); + } + + storeBladeDirectiveInStyle(value: string) { + return this.getBladeDirectiveInStylePlaceholder( + (this.bladeDirectivesInStyle.push(value) - 1).toString(), + ); + } + + getBladeDirectiveInStylePlaceholder(replace: string) { + return _.replace(".___blade_directive_in_style_#__", "#", replace); + } +} diff --git a/src/processors/breakLineBeforeAndAfterDirectiveProcessor.ts b/src/processors/breakLineBeforeAndAfterDirectiveProcessor.ts new file mode 100644 index 00000000..04b296bb --- /dev/null +++ b/src/processors/breakLineBeforeAndAfterDirectiveProcessor.ts @@ -0,0 +1,166 @@ +import _ from "lodash"; +import { + indentElseTokens, + indentEndTokens, + indentStartTokens, +} from "src/indent"; +import { nestedParenthesisRegex } from "src/regex"; +import XRegExp from "xregexp"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class BreakLineBeforeAndAfterDirectiveProcessor extends Processor { + private bladeComments: string[] = []; + + async preProcess(content: string): Promise { + return await this.breakLineBeforeAndAfterDirective(content); + } + + async postProcess(content: string): Promise {} + + breakLineBeforeAndAfterDirective(content: string): string { + // handle directive around html tags + let formattedContent = _.replace( + content, + new RegExp( + `(?<=<.*?(?)(${_.without(indentStartTokens, "@php").join( + "|", + )})(\\s*)${nestedParenthesisRegex}.*?(?=<.*?>)`, + "gmis", + ), + (match) => `\n${match.trim()}\n`, + ); + + // eslint-disable-next-line + formattedContent = _.replace( + formattedContent, + new RegExp( + `(?<=<.*?(?).*?(${_.without(indentEndTokens, "@endphp").join( + "|", + )})(?=<.*?>)`, + "gmis", + ), + (match) => `\n${match.trim()}\n`, + ); + + const unbalancedConditions = ["@case", ...indentElseTokens]; + + formattedContent = _.replace( + formattedContent, + new RegExp( + `(\\s*?)(${unbalancedConditions.join( + "|", + )})(\\s*?)${nestedParenthesisRegex}(\\s*)`, + "gmi", + ), + (match) => `\n${match.trim()}\n`, + // handle else directive + ); + + formattedContent = _.replace( + formattedContent, + new RegExp( + `\\s*?(?!(${_.without(indentElseTokens, "@else").join("|")}))@else\\s+`, + "gim", + ), + (match) => `\n${match.trim()}\n`, + // handle case directive + ); + + formattedContent = _.replace( + formattedContent, + /@case\S*?\s*?@case/gim, + (match) => { + // handle unbalanced echos + return `${match.replace("\n", "")}`; + }, + ); + + const unbalancedEchos = ["@break"]; + + _.forEach(unbalancedEchos, (directive) => { + formattedContent = _.replace( + formattedContent, + new RegExp(`(\\s*?)${directive}\\s+`, "gmi"), + (match) => { + return `\n${match.trim()}\n\n`; + }, + ); + }); + + // other directives + _.forEach(["@default"], (directive) => { + formattedContent = _.replace( + formattedContent, + new RegExp(`(\\s*?)${directive}\\s*`, "gmi"), + (match) => { + return `\n\n${match.trim()}\n`; + }, + ); + }); + + // add line break around balanced directives + const directives = _.chain(indentStartTokens) + .map((x: any) => _.replace(x, /@/, "")) + .value(); + + _.forEach(directives, (directive: any) => { + try { + const recursivelyMatched = XRegExp.matchRecursive( + formattedContent, + `\\@${directive}`, + `\\@end${directive}`, + "gmi", + { + valueNames: [null, "left", "match", "right"], + }, + ); + + if (_.isEmpty(recursivelyMatched)) { + return; + } + + for (const matched of recursivelyMatched) { + if (matched.name === "match") { + if (new RegExp(indentStartTokens.join("|")).test(matched.value)) { + formattedContent = _.replace( + formattedContent, + matched.value, + this.breakLineBeforeAndAfterDirective( + util.escapeReplacementString(matched.value), + ), + ); + } + + const innerRegex = new RegExp( + `^(\\s*?)${nestedParenthesisRegex}(.*)`, + "gmis", + ); + + const replaced = _.replace( + `${matched.value}`, + innerRegex, + (_match: string, p1: string, p2: string, p3: string) => { + if (p3.trim() === "") { + return `${p1}(${p2.trim()})\n${p3.trim()}`; + } + + return `${p1}(${p2.trim()})\n${p3.trim()}\n`; + }, + ); + + formattedContent = _.replace( + formattedContent, + matched.value, + util.escapeReplacementString(replaced), + ); + } + } + } catch (error) { + // do nothing to ignore unmatched directive pair + } + }); + + return formattedContent; + } +} diff --git a/src/processors/componentAttributeProcessor.ts b/src/processors/componentAttributeProcessor.ts new file mode 100644 index 00000000..58378ff2 --- /dev/null +++ b/src/processors/componentAttributeProcessor.ts @@ -0,0 +1,108 @@ +import detectIndent from "detect-indent"; +import beautify from "js-beautify"; +import _ from "lodash"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class ComponentAttributeProcessor extends Processor { + private componentAttributes: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveComponentAttribute(content); + } + + async postProcess(content: string): Promise { + return await this.restoreComponentAttribute(content); + } + + private async preserveComponentAttribute(content: string): Promise { + const prefixes = + Array.isArray(this.formatter.options.componentPrefix) && + this.formatter.options.componentPrefix.length > 0 + ? this.formatter.options.componentPrefix + : ["x-", "livewire:"]; + const regex = new RegExp( + `(?<=<(${prefixes.join( + "|", + )})[^<]*?\\s):{1,2}(?)[\\w\-_.]*?=(["'])(?!=>)[^\\2]*?\\2(?=[^>]*?\/*?>)`, + "gim", + ); + return _.replace( + content, + regex, + (match: any) => `${this.storeComponentAttribute(match)}`, + ); + } + + private async restoreComponentAttribute(content: string): Promise { + return replaceAsync( + content, + new RegExp(`${this.getComponentAttributePlaceholder("(\\d+)")}`, "gim"), + async (_match: any, p1: any) => { + const placeholder = this.getComponentAttributePlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const matched = this.componentAttributes[p1]; + const formatted = await replaceAsync( + matched, + /(:{1,2}.*?=)(["'])(.*?)(?=\2)/gis, + async (match, p2: string, p3: string, p4: string) => { + if (p4 === "") { + return match; + } + + if (matchedLine[0].startsWith(" { + return await this.preserveConditions(content); + } + + async postProcess(content: string): Promise { + return await this.restoreConditions(content); + } + + private async preserveConditions(content: string): Promise { + const regex = new RegExp( + `(${conditionalTokens.join( + "|", + // eslint-disable-next-line max-len + )})(\\s*?)${nestedParenthesisRegex}`, + "gi", + ); + return _.replace( + content, + regex, + (match: any, p1: any, p2: any, p3: any) => + `${p1}${p2}(${this.storeConditions(p3)})`, + ); + } + + private async restoreConditions(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res: any) => + replaceAsync( + res, + new RegExp(`${this.getConditionsPlaceholder("(\\d+)")}`, "gms"), + async (_match: any, p1: any) => { + const placeholder = this.getConditionsPlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const matched = this.conditions[p1]; + + return util.formatExpressionInsideBladeDirective( + matched, + indent, + this.formatter, + ); + }, + ), + ); + } + + storeConditions(value: any) { + return this.getConditionsPlaceholder(this.conditions.push(value) - 1); + } + + getConditionsPlaceholder(replace: any) { + return _.replace("___directive_condition_#___", "#", replace); + } +} diff --git a/src/processors/curlyBraceForJSProcessor.ts b/src/processors/curlyBraceForJSProcessor.ts new file mode 100644 index 00000000..3327842e --- /dev/null +++ b/src/processors/curlyBraceForJSProcessor.ts @@ -0,0 +1,40 @@ +import beautify, { type JSBeautifyOptions } from "js-beautify"; +import _ from "lodash"; +import { Processor } from "./processor"; + +export class CurlyBraceForJSProcessor extends Processor { + private curlyBracesWithJSs: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveCurlyBraceForJS(content); + } + + async postProcess(content: string): Promise { + return await this.restoreCurlyBraceForJS(content); + } + + private async preserveCurlyBraceForJS(content: string): Promise { + return _.replace(content, /@{{(.*?)}}/gs, (match: any, p1: any) => + this.storeCurlyBraceForJS(p1), + ); + } + + storeCurlyBraceForJS(value: any) { + return this.getCurlyBraceForJSPlaceholder( + this.curlyBracesWithJSs.push(value) - 1, + ); + } + + getCurlyBraceForJSPlaceholder(replace: any) { + return _.replace("___js_curly_brace_#___", "#", replace); + } + + private async restoreCurlyBraceForJS(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getCurlyBraceForJSPlaceholder("(\\d+)")}`, "gm"), + (_match: any, p1: any) => + `@{{ ${beautify.js_beautify(this.curlyBracesWithJSs[p1].trim())} }}`, + ); + } +} diff --git a/src/processors/customDirectiveProcessor.ts b/src/processors/customDirectiveProcessor.ts new file mode 100644 index 00000000..06f629cb --- /dev/null +++ b/src/processors/customDirectiveProcessor.ts @@ -0,0 +1,273 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import { + cssAtRuleTokens, + indentElseTokens, + indentEndTokens, + indentStartTokens, + inlineFunctionTokens, + phpKeywordStartTokens, + unbalancedStartTokens, +} from "src/indent"; +import { nestedParenthesisRegex } from "src/regex"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class CustomDirectiveProcessor extends Processor { + private customDirectives: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveCustomDirective(content); + } + + async postProcess(content: string): Promise { + return await this.restoreCustomDirective(content); + } + + private async preserveCustomDirective(content: string): Promise { + const negativeLookAhead = [ + ..._.without(indentStartTokens, "@unless"), + ...indentEndTokens, + ...indentElseTokens, + ...["@unless\\(.*?\\)"], + ].join("|"); + + const inlineNegativeLookAhead = _.chain([ + ..._.without(indentStartTokens, "@unless", "@for"), + ...indentEndTokens, + ...indentElseTokens, + ...inlineFunctionTokens, + ..._.without(phpKeywordStartTokens, "@for"), + ...["@unless[a-z]*\\(.*?\\)", "@for\\(.*?\\)"], + ...unbalancedStartTokens, + ...cssAtRuleTokens, + ]) + .uniq() + .join("|") + .value(); + + const inlineRegex = new RegExp( + `(?!(${inlineNegativeLookAhead}))(@([a-zA-Z1-9_\\-]+))(?!.*?@end\\3)${nestedParenthesisRegex}.*?(? + this.storeInlineCustomDirective(match), + ); + + // preserve begin~else~end directives + formatted = _.replace( + formatted, + regex, + ( + match: string, + p1: string, + p2: string, + p3: string, + p4: string, + p5: string, + p6: string, + p7: string, + ) => { + if (indentStartTokens.includes(p2)) { + return match; + } + + let result: string = match; + + // begin directive + result = _.replace( + result, + new RegExp(`${p2}(${nestedParenthesisRegex})*`, "gim"), + (beginStr: string) => this.storeBeginCustomDirective(beginStr), + ); + // end directive + result = _.replace(result, p7, this.storeEndCustomDirective(p7)); + // else directive + result = _.replace( + result, + new RegExp(`@else${p4}(${nestedParenthesisRegex})*`, "gim"), + (elseStr: string) => this.storeElseCustomDirective(elseStr), + ); + + return result; + }, + ); + + // replace directives recursively + if (regex.test(formatted)) { + formatted = this.preserveCustomDirective(formatted); + } + + return formatted; + } + + private async restoreCustomDirective(content: string): Promise { + return this.restoreInlineCustomDirective(content) + .then((data: string) => this.restoreBeginCustomDirective(data)) + .then((data: string) => this.restoreElseCustomDirective(data)) + .then((data: string) => this.restoreEndCustomDirective(data)); + } + + async restoreInlineCustomDirective(content: string) { + return replaceAsync( + content, + new RegExp( + `${this.getInlineCustomDirectivePlaceholder("(\\d+)")}`, + "gim", + ), + async (_match: any, p1: number) => { + const placeholder = this.getInlineCustomDirectivePlaceholder( + p1.toString(), + ); + const matchedLine = content.match( + new RegExp(`^(.*?)${_.escapeRegExp(placeholder)}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const matched = `${this.customDirectives[p1]}`; + + return replaceAsync( + matched, + /(@[a-zA-z0-9\-_]+)(.*)/gis, + async (match2: string, p2: string, p3: string) => { + try { + const formatted = ( + await util.formatRawStringAsPhp(`func${p3}`, { + ...this.formatter.options, + printWidth: util.printWidthForInline, + }) + ) + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .replace(/,(\s*?\))$/gm, (_m, p4) => p4) + .trim() + .substring(4); + return `${p2}${util.indentComponentAttribute( + indent.indent, + formatted, + this.formatter, + )}`; + } catch (error) { + return `${match2}`; + } + }, + ); + }, + ); + } + + async restoreElseCustomDirective(content: string) { + return _.replace( + content, + /@else\(___(\d+)___\)/gim, + (_match: any, p1: number) => `${this.customDirectives[p1]}`, + ); + } + + async restoreEndCustomDirective(content: string) { + return _.replace( + content, + /@endcustomdirective\(___(\d+)___\)/gim, + (_match: any, p1: number) => `${this.customDirectives[p1]}`, + ); + } + + async restoreBeginCustomDirective(content: string) { + return replaceAsync( + content, + new RegExp( + `@customdirective\\(___(\\d+)___\\)\\s*?(${nestedParenthesisRegex})*`, + "gim", + ), + async (_match: any, p1: number) => { + const placeholder = this.getBeginCustomDirectivePlaceholder( + p1.toString(), + ); + const matchedLine = content.match( + new RegExp(`^(.*?)${_.escapeRegExp(placeholder)}`, "gmi"), + ) ?? [""]; + + const indent = detectIndent(matchedLine[0]); + const matched = `${this.customDirectives[p1]}`; + + return replaceAsync( + matched, + /(@[a-zA-z0-9\-_]+)(.*)/gis, + async (match2: string, p3: string, p4: string) => { + try { + const formatted = ( + await util.formatRawStringAsPhp(`func${p4}`, { + ...this.formatter.options, + trailingCommaPHP: false, + }) + ) + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .trim() + .substring(4); + return `${p3}${util.indentComponentAttribute( + indent.indent, + formatted, + this.formatter, + )}`; + } catch (error) { + return `${match2}`; + } + }, + ); + }, + ); + } + + storeInlineCustomDirective(value: string) { + return this.getInlineCustomDirectivePlaceholder( + (this.customDirectives.push(value) - 1).toString(), + ); + } + + storeBeginCustomDirective(value: string) { + return this.getBeginCustomDirectivePlaceholder( + (this.customDirectives.push(value) - 1).toString(), + ); + } + + storeEndCustomDirective(value: string) { + return this.getEndCustomDirectivePlaceholder( + (this.customDirectives.push(value) - 1).toString(), + ); + } + + storeElseCustomDirective(value: string) { + return this.getElseCustomDirectivePlaceholder( + (this.customDirectives.push(value) - 1).toString(), + ); + } + + getInlineCustomDirectivePlaceholder(replace: string) { + return _.replace("___inline_cd_#___", "#", replace); + } + + getBeginCustomDirectivePlaceholder(replace: string) { + return _.replace("@customdirective(___#___)", "#", replace); + } + + getEndCustomDirectivePlaceholder(replace: string) { + return _.replace("@endcustomdirective(___#___)", "#", replace); + } + + getElseCustomDirectivePlaceholder(replace: string) { + return _.replace("@else(___#___)", "#", replace); + } + + getCustomDirectivePlaceholder(replace: any) { + return _.replace("___blade_comment_#___", "#", replace); + } +} diff --git a/src/processors/escapedBladeDirectiveProcessor.ts b/src/processors/escapedBladeDirectiveProcessor.ts new file mode 100644 index 00000000..78dd2573 --- /dev/null +++ b/src/processors/escapedBladeDirectiveProcessor.ts @@ -0,0 +1,43 @@ +import _ from "lodash"; +import { Processor } from "./processor"; + +export class EscapedBladeDirectiveProcessor extends Processor { + private escapedBladeDirectives: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveEscapedBladeDirective(content); + } + + async postProcess(content: string): Promise { + return await this.restoreEscapedBladeDirective(content); + } + + private async preserveEscapedBladeDirective(content: string): Promise { + return _.replace(content, /@@\w*/gim, (match: string) => + this.storeEscapedBladeDirective(match), + ); + } + + private async restoreEscapedBladeDirective(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res: any) => + _.replace( + res, + new RegExp( + `${this.getEscapedBladeDirectivePlaceholder("(\\d+)")}`, + "gms", + ), + (_match: string, p1: number) => this.escapedBladeDirectives[p1], + ), + ); + } + + storeEscapedBladeDirective(value: string) { + return this.getEscapedBladeDirectivePlaceholder( + (this.escapedBladeDirectives.push(value) - 1).toString(), + ); + } + + getEscapedBladeDirectivePlaceholder(replace: any) { + return _.replace("___escaped_directive_#___", "#", replace); + } +} diff --git a/src/processors/fomatAsPhpProcessor.ts b/src/processors/fomatAsPhpProcessor.ts new file mode 100644 index 00000000..9d88fd30 --- /dev/null +++ b/src/processors/fomatAsPhpProcessor.ts @@ -0,0 +1,11 @@ +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class FormatAsPhpProcessor extends Processor { + async preProcess(content: string): Promise { + return await util.formatAsPhp(content, this.formatter.options); + } + + async postProcess(content: string): Promise {} +} diff --git a/src/processors/formatAsBladeProcessor.ts b/src/processors/formatAsBladeProcessor.ts new file mode 100644 index 00000000..f4d8774e --- /dev/null +++ b/src/processors/formatAsBladeProcessor.ts @@ -0,0 +1,10 @@ +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; +export class FormatAsBladeProcessor extends Processor { + async preProcess(content: string): Promise { + return await util.formatAsBlade(content, this.formatter); + } + + async postProcess(content: string): Promise {} +} diff --git a/src/processors/formatAsHtmlProcessor.ts b/src/processors/formatAsHtmlProcessor.ts new file mode 100644 index 00000000..2b3ed252 --- /dev/null +++ b/src/processors/formatAsHtmlProcessor.ts @@ -0,0 +1,44 @@ +import beautify from "js-beautify"; +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class FormatAsHtmlProcessor extends Processor { + async preProcess(content: string): Promise { + return await this.formatAsHtml(content); + } + + async postProcess(content: string): Promise {} + + private async formatAsHtml(content: string): Promise { + const options = { + indent_size: util.optional(this.formatter.options).indentSize || 4, + wrap_line_length: + util.optional(this.formatter.options).wrapLineLength || 120, + wrap_attributes: + util.optional(this.formatter.options).wrapAttributes || "auto", + wrap_attributes_min_attrs: util.optional(this.formatter.options) + .wrapAttributesMinAttrs, + indent_inner_html: + util.optional(this.formatter.options).indentInnerHtml || false, + end_with_newline: + util.optional(this.formatter.options).endWithNewline || true, + max_preserve_newlines: util.optional(this.formatter.options) + .noMultipleEmptyLines + ? 1 + : undefined, + extra_liners: util.optional(this.formatter.options).extraLiners, + css: { + end_with_newline: false, + }, + eol: this.formatter.endOfLine, + }; + + const promise = new Promise((resolve) => resolve(content)) + .then((content) => util.preserveDirectives(content)) + .then((preserved) => beautify.html_beautify(preserved, options)) + .then((content) => util.revertDirectives(content)); + + return Promise.resolve(promise); + } +} diff --git a/src/processors/htmlAttributesProcessor.ts b/src/processors/htmlAttributesProcessor.ts new file mode 100644 index 00000000..18634a34 --- /dev/null +++ b/src/processors/htmlAttributesProcessor.ts @@ -0,0 +1,59 @@ +import _ from "lodash"; +import { Processor } from "./processor"; + +export class HtmlAttributesProcessor extends Processor { + private htmlAttributes: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveHtmlAttributes(content); + } + + async postProcess(content: string): Promise { + return await this.restoreHtmlAttributes(content); + } + + private async preserveHtmlAttributes(content: string): Promise { + return _.replace( + content, + /(?<=<[\w\-\.\:\_]+.*\s)(?!x-bind)([^\s\:][^\s\'\"]+\s*=\s*(["'])(?)/gms, + (match: string) => `${this.storeHtmlAttribute(match)}`, + ); + } + + private async restoreHtmlAttributes(content: string): Promise { + return _.replace( + content, + // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. + new RegExp(`${this.getHtmlAttributePlaceholder("(\\d+)")}`, "gms"), + (_match: string, p1: number) => this.htmlAttributes[p1], + ); + } + + storeHtmlAttribute(value: string) { + const index = this.htmlAttributes.push(value) - 1; + + if (value.length > 0) { + return this.getHtmlAttributePlaceholder(index.toString(), value.length); + } + + return this.getHtmlAttributePlaceholder(index.toString(), 0); + } + + getHtmlAttributePlaceholder(replace: string, length: any) { + if (length && length > 0) { + const template = "___attrs_#___"; + const gap = length - template.length; + return _.replace( + `___attrs${_.repeat("_", gap > 0 ? gap : 1)}#___`, + "#", + replace, + ); + } + + if (_.isNull(length)) { + return _.replace("___attrs_#___", "#", replace); + } + + return _.replace("___attrs_+?#___", "#", replace); + } +} diff --git a/src/processors/htmlTagsProcessor.ts b/src/processors/htmlTagsProcessor.ts new file mode 100644 index 00000000..65c6a99a --- /dev/null +++ b/src/processors/htmlTagsProcessor.ts @@ -0,0 +1,154 @@ +import detectIndent from "detect-indent"; +import beautify from "js-beautify"; +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class HtmlTagsProcessor extends Processor { + private htmlTags: string[] = []; + private templatingStrings: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveHtmlTags(content); + } + + async postProcess(content: string): Promise { + return await this.restoreHtmlTags(content); + } + + private async preserveHtmlTags(content: string): Promise { + const contentUnformatted = ["textarea", "pre"]; + + return _.replace( + content, + new RegExp( + `<(${contentUnformatted.join( + "|", + )})\\s{0,1}.*?>.*?<\\/(${contentUnformatted.join("|")})>`, + "gis", + ), + (match: string) => this.storeHtmlTags(match), + ); + } + + private async restoreHtmlTags(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getHtmlTagsPlaceholder("(\\d+)")}`, "gim"), + (_match: any, p1: number) => { + const placeholder = this.getHtmlTagsPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const options = { + indent_size: util.optional(this.formatter.options).indentSize || 4, + wrap_line_length: + util.optional(this.formatter.options).wrapLineLength || 120, + wrap_attributes: + util.optional(this.formatter.options).wrapAttributes || "auto", + wrap_attributes_min_attrs: util.optional(this.formatter.options) + .wrapAttributesMinAttrs, + indent_inner_html: + util.optional(this.formatter.options).indentInnerHtml || false, + extra_liners: util.optional(this.formatter.options).extraLiners, + end_with_newline: false, + templating: ["php"], + }; + + const matched = this.htmlTags[p1]; + const openingTag = _.first( + matched.match(/(<(textarea|pre).*?(?)(?=.*?<\/\2>)/gis), + ); + + if (openingTag === undefined) { + return `${this.indentScriptBlock( + indent, + beautify.html_beautify(matched, options), + )}`; + } + + const restofTag = matched.substring(openingTag.length, matched.length); + + return `${this.indentScriptBlock( + indent, + beautify.html_beautify(openingTag, options), + )}${restofTag}`; + }, + ); + } + + storeHtmlTags(value: string) { + return this.getHtmlTagsPlaceholder( + (this.htmlTags.push(value) - 1).toString(), + ); + } + + getHtmlTagsPlaceholder(replace: any) { + return _.replace("", "#", replace); + } + + indentScriptBlock(indent: detectIndent.Indent, content: any) { + if (_.isEmpty(indent.indent)) { + return content; + } + + if (util.isInline(content)) { + return `${content}`; + } + + const leftIndentAmount = indent.amount; + const indentLevel = leftIndentAmount / this.formatter.indentSize; + const prefixSpaces = this.formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * this.formatter.indentSize, + ); + const prefixForEnd = this.formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * this.formatter.indentSize, + ); + + const preserved = _.replace(content, /`.*?`/gs, (match: any) => + this.storeTemplatingString(match), + ); + + const lines = preserved.split("\n"); + + const indented = _.chain(lines) + .map((line: any, index: any) => { + if (index === 0) { + return line; + } + + if (index === lines.length - 1) { + return prefixForEnd + line; + } + + if (_.isEmpty(line)) { + return line; + } + + return prefixSpaces + line; + }) + .value() + .join("\n"); + + return this.restoreTemplatingString(`${indented}`); + } + + storeTemplatingString(value: any) { + const index = this.templatingStrings.push(value) - 1; + return this.getTemplatingStringPlaceholder(index); + } + + restoreTemplatingString(content: any) { + return _.replace( + content, + new RegExp(`${this.getTemplatingStringPlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => this.templatingStrings[p1], + ); + } + + getTemplatingStringPlaceholder(replace: any) { + return _.replace("___templating_str_#___", "#", replace); + } +} diff --git a/src/processors/ignoredLinesProcessor.ts b/src/processors/ignoredLinesProcessor.ts new file mode 100644 index 00000000..2ccec4e0 --- /dev/null +++ b/src/processors/ignoredLinesProcessor.ts @@ -0,0 +1,53 @@ +import _ from "lodash"; +import { Processor } from "./processor"; + +export class IgnoredLinesProcessor extends Processor { + private ignoredLines: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveIgnoredLines(content); + } + + async postProcess(content: string): Promise { + return await this.restoreIgnoredLines(content); + } + + private async preserveIgnoredLines(content: string): Promise { + return ( + _.chain(content) + // ignore entire file + .replace( + /(^(? + this.storeIgnoredLines(`${p1}${p2.replace(/^\n/, "")}`), + ) + // range ignore + .replace( + /(?:({{--\s*?blade-formatter-disable\s*?--}}||{{--\s*?prettier-ignore-start\s*?--}})).*?(?:({{--\s*?blade-formatter-enable\s*?--}}||{{--\s*?prettier-ignore-end\s*?--}}))/gis, + (match: any) => this.storeIgnoredLines(match), + ) + // line ignore + .replace( + /(?:{{--\s*?blade-formatter-disable-next-line\s*?--}}|{{--\s*?prettier-ignore\s*?--}}|)[\r\n]+[^\r\n]+/gis, + (match: any) => this.storeIgnoredLines(match), + ) + .value() + ); + } + + private async restoreIgnoredLines(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getIgnoredLinePlaceholder("(\\d+)")}`, "gm"), + (_match: any, p1: any) => this.ignoredLines[p1], + ); + } + + private storeIgnoredLines(value: string) { + return this.getIgnoredLinePlaceholder(this.ignoredLines.push(value) - 1); + } + + private getIgnoredLinePlaceholder(replace: any) { + return _.replace("___ignored_line_#___", "#", replace); + } +} diff --git a/src/processors/inlineDirectiveProcessor.ts b/src/processors/inlineDirectiveProcessor.ts new file mode 100644 index 00000000..25a199a5 --- /dev/null +++ b/src/processors/inlineDirectiveProcessor.ts @@ -0,0 +1,100 @@ +import _ from "lodash"; +import { directivePrefix, indentStartTokensWithoutPrefix } from "src/indent"; +import { Processor } from "./processor"; + +export class InlineDirectiveProcessor extends Processor { + private inlineDirectives: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveInlineDirective(content); + } + + async postProcess(content: string): Promise { + return await this.restoreInlineDirective(content); + } + + private async preserveInlineDirective(content: string): Promise { + // preserve inline directives inside html tag + const regex = new RegExp( + `(<[\\w\\-\\_]+?[^>]*?)${directivePrefix}(${indentStartTokensWithoutPrefix.join( + "|", + )})(\\s*?)?(\\([^)]*?\\))?((?:(?!@end\\2).)+)(@end\\2|@endif)(.*?/*>)`, + "gims", + ); + const replaced = _.replace( + content, + regex, + ( + _match: string, + p1: string, + p2: string, + p3: string, + p4: string, + p5: string, + p6: string, + p7: string, + ) => { + if (p3 === undefined && p4 === undefined) { + return `${p1}${this.storeInlineDirective( + `${directivePrefix}${p2.trim()}${p5.trim()} ${p6.trim()}`, + )}${p7}`; + } + if (p3 === undefined) { + return `${p1}${this.storeInlineDirective( + `${directivePrefix}${p2.trim()}${p4.trim()}${p5}${p6.trim()}`, + )}${p7}`; + } + if (p4 === undefined) { + return `${p1}${this.storeInlineDirective( + `${directivePrefix}${p2.trim()}${p3}${p5.trim()} ${p6.trim()}`, + )}${p7}`; + } + + return `${p1}${this.storeInlineDirective( + `${directivePrefix}${p2.trim()}${p3}${p4.trim()} ${p5.trim()} ${p6.trim()}`, + )}${p7}`; + }, + ); + + if (regex.test(replaced)) { + return this.preserveInlineDirective(replaced); + } + + return replaced; + } + + private async restoreInlineDirective(content: any) { + return new Promise((resolve) => resolve(content)).then((res) => + _.replace( + // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message + res, + new RegExp(`${this.getInlinePlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => { + const matched = this.inlineDirectives[p1]; + return matched; + }, + ), + ); + } + + storeInlineDirective(value: any) { + return this.getInlinePlaceholder( + this.inlineDirectives.push(value) - 1, + value.length, + ); + } + + getInlinePlaceholder(replace: any, length = 0) { + if (length > 0) { + const template = "___inline_directive_#___"; + const gap = length - template.length; + return _.replace( + `___inline_directive_${_.repeat("_", gap > 0 ? gap : 0)}#___`, + "#", + replace, + ); + } + + return _.replace("___inline_directive_+?#___", "#", replace); + } +} diff --git a/src/processors/inlinePhpDirectiveProcessor.ts b/src/processors/inlinePhpDirectiveProcessor.ts new file mode 100644 index 00000000..1d418cea --- /dev/null +++ b/src/processors/inlinePhpDirectiveProcessor.ts @@ -0,0 +1,162 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import { inlineFunctionTokens, inlinePhpDirectives } from "src/indent"; +import { nestedParenthesisRegex } from "src/regex"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class InlinePhpDirectiveProcessor extends Processor { + private inlinePhpDirectives: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveInlinePhpDirective(content); + } + + async postProcess(content: string): Promise { + return await this.restoreInlinePhpDirective(content); + } + + private async preserveInlinePhpDirective(content: string): Promise { + return _.replace( + content, + // eslint-disable-next-line max-len + new RegExp( + `(?!\\/\\*.*?\\*\\/)(${inlineFunctionTokens.join( + "|", + )})(\\s*?)${nestedParenthesisRegex}`, + "gmsi", + ), + (match: any) => this.storeInlinePhpDirective(match), + ); + } + + private async restoreInlinePhpDirective(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res) => + replaceAsync( + // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message + res, + new RegExp(`${this.getInlinePhpPlaceholder("(\\d+)")}`, "gm"), + async (_match: any, p1: any) => { + const matched = this.inlinePhpDirectives[p1]; + const placeholder = this.getInlinePhpPlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + if (matched.includes("@php")) { + return `${( + await util.formatRawStringAsPhp(matched, { + ...this.formatter.options, + printWidth: util.printWidthForInline, + }) + ) + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .trim() + // @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1. + .trimRight("\n")}`; + } + + if (new RegExp(inlinePhpDirectives.join("|"), "gi").test(matched)) { + const formatted = replaceAsync( + matched, + new RegExp( + `(?<=@(${_.map(inlinePhpDirectives, (token) => + token.substring(1), + ).join("|")}).*?\\()(.*)(?=\\))`, + "gis", + ), + async (match2: any, p3: any, p4: any) => { + let wrapLength = this.formatter.wrapLineLength; + + if (["button", "class"].includes(p3)) { + wrapLength = 80; + } + + if (p3 === "include") { + wrapLength = + this.formatter.wrapLineLength - + "func".length - + p1.length - + indent.amount; + } + + return this.formatExpressionInsideBladeDirective( + p4, + indent, + wrapLength, + ); + }, + ); + + return formatted; + } + + return `${( + await util.formatRawStringAsPhp(matched, { + ...this.formatter.options, + printWidth: util.printWidthForInline, + }) + ).trimEnd()}`; + }, + ), + ); + } + + storeInlinePhpDirective(value: any) { + return this.getInlinePhpPlaceholder( + this.inlinePhpDirectives.push(value) - 1, + ); + } + + getInlinePhpPlaceholder(replace: any) { + return _.replace("___inline_php_directive_#___", "#", replace); + } + + async formatExpressionInsideBladeDirective( + matchedExpression: string, + indent: detectIndent.Indent, + wrapLength: number | undefined = undefined, + ) { + const formatTarget = `func(${matchedExpression})`; + const formattedExpression = await util.formatRawStringAsPhp(formatTarget, { + ...this.formatter.options, + printWidth: + wrapLength ?? this.formatter.defaultPhpFormatOption.printWidth, + }); + + if (formattedExpression === formatTarget) { + return matchedExpression; + } + + let inside = formattedExpression + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .replace(/(? `${p1}]\n)`, + ) + .replace(/,[\n\s]*?\)/gs, ")") + .replace(/,(\s*?\))$/gm, (match, p1) => p1) + .trim(); + + if (this.formatter.options.useTabs || false) { + inside = _.replace( + inside, + /(?<=^ *) {4}/gm, + "\t".repeat(this.formatter.indentSize), + ); + } + + inside = inside.replace( + /func\((.*)\)/gis, + (match: string, p1: string) => p1, + ); + if (util.isInline(inside.trim())) { + inside = inside.trim(); + } + + return util.indentRawPhpBlock(indent, inside, this.formatter); + } +} diff --git a/src/processors/nonnativeScriptsProcessor.ts b/src/processors/nonnativeScriptsProcessor.ts new file mode 100644 index 00000000..23311da6 --- /dev/null +++ b/src/processors/nonnativeScriptsProcessor.ts @@ -0,0 +1,39 @@ +import _ from "lodash"; +import { Processor } from "./processor"; + +export class NonnativeScriptsProcessor extends Processor { + private nonnativeScripts: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveNonnativeScripts(content); + } + + async postProcess(content: string): Promise { + return await this.restoreNonnativeScripts(content); + } + + private async preserveNonnativeScripts(content: string): Promise { + return _.replace( + content, + /]*?type=(["'])(?!(text\/javascript|module))[^\1]*?\1[^>]*?>.*?<\/script>/gis, + (match: string) => this.storeNonnativeScripts(match), + ); + } + + storeNonnativeScripts(value: string) { + const index = this.nonnativeScripts.push(value) - 1; + return this.getNonnativeScriptPlaceholder(index.toString()); + } + + private async restoreNonnativeScripts(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getNonnativeScriptPlaceholder("(\\d+)")}`, "gmi"), + (_match: any, p1: number) => `${this.nonnativeScripts[p1]}`, + ); + } + + private getNonnativeScriptPlaceholder(replace: string) { + return _.replace("", "#", replace); + } +} diff --git a/src/processors/phpBlockProcessor.ts b/src/processors/phpBlockProcessor.ts new file mode 100644 index 00000000..afd1fb07 --- /dev/null +++ b/src/processors/phpBlockProcessor.ts @@ -0,0 +1,175 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import { formatPhpComment } from "src/comment"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class PhpBlockProcessor extends Processor { + private rawBlocks: string[] = []; + private rawPropsBlocks: string[] = []; + private stringLiteralInPhp: string[] = []; + private phpComments: string[] = []; + + async preProcess(content: string): Promise { + return await this.preservePhpBlock(content); + } + + async postProcess(content: string): Promise { + return await this.restoreRawPhpBlock(content); + } + + private async preservePhpBlock(content: string): Promise { + return this.preserveRawPhpBlock(content); + } + + async preserveRawPhpBlock(content: any) { + return _.replace( + content, + /(? this.storeRawBlock(p1), + ); + } + + async restoreRawPhpBlock(content: any) { + return replaceAsync( + content, + new RegExp(`${this.getRawPlaceholder("(\\d+)")}`, "gm"), + async (match: any, p1: number) => { + let rawBlock = this.rawBlocks[p1]; + const placeholder = this.getRawPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const isOnSingleLine = util.isInline(rawBlock); + const isMultipleStatements = await util.isMultilineStatement( + rawBlock, + this.formatter, + ); + if (isOnSingleLine && isMultipleStatements) { + // multiple statements on a single line + rawBlock = ( + await util.formatStringAsPhp( + ``, + this.formatter.options, + ) + ).trim(); + } else if (isMultipleStatements) { + // multiple statments on mult lines + + const indentLevel = indent.amount + this.formatter.indentSize; + rawBlock = ( + await util.formatStringAsPhp(``, { + ...this.formatter.options, + useProjectPrintWidth: true, + adjustPrintWidthBy: indentLevel, + }) + ).trimEnd(); + } else if (!isOnSingleLine) { + // single statement on mult lines + rawBlock = ( + await util.formatStringAsPhp( + ``, + this.formatter.options, + ) + ).trimEnd(); + } else { + // single statement on single line + rawBlock = ``; + } + + return _.replace( + rawBlock, + /^(\s*)?<\?php(.*?)\?>/gms, + (_matched: any, _q1: any, q2: any) => { + if (util.isInline(rawBlock)) { + return `@php${q2}@endphp`; + } + + let preserved = this.preserveStringLiteralInPhp(q2); + preserved = this.preservePhpComment(preserved); + let indented = util.indentRawBlock( + indent, + preserved, + this.formatter, + ); + indented = this.restorePhpComment(indented); + const restored = this.restoreStringLiteralInPhp(indented); + + return `@php${restored}@endphp`; + }, + ); + }, + ); + } + + storeRawBlock(value: any) { + return this.getRawPlaceholder(this.rawBlocks.push(value) - 1); + } + + getRawPlaceholder(replace: any) { + return _.replace("___raw_block_#___", "#", replace); + } + + preserveStringLiteralInPhp(content: any) { + return _.replace( + content, + /(\"([^\\]|\\.)*?\"|\'([^\\]|\\.)*?\')/gm, + (match: string) => `${this.storeStringLiteralInPhp(match)}`, + ); + } + + storeStringLiteralInPhp(value: any) { + const index = this.stringLiteralInPhp.push(value) - 1; + return this.getStringLiteralInPhpPlaceholder(index); + } + + getStringLiteralInPhpPlaceholder(replace: any) { + return _.replace("'___php_content_#___'", "#", replace); + } + + preservePhpComment(content: string) { + return _.replace( + content, + /\/\*(?:[^*]|[\r\n]|(?:\*+(?:[^*\/]|[\r\n])))*\*+\//gi, + (match: string) => this.storePhpComment(match), + ); + } + + storePhpComment(value: string) { + return this.getPhpCommentPlaceholder( + (this.phpComments.push(value) - 1).toString(), + ); + } + + getPhpCommentPlaceholder(replace: string) { + return _.replace("___php_comment_#___", "#", replace); + } + + restorePhpComment(content: string) { + return _.replace( + content, + new RegExp(`${this.getPhpCommentPlaceholder("(\\d+)")};{0,1}`, "gms"), + (_match: string, p1: number) => { + const placeholder = this.getPhpCommentPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + const formatted = formatPhpComment(this.phpComments[p1]); + + return util.indentPhpComment(indent, formatted, this.formatter); + }, + ); + } + + restoreStringLiteralInPhp(content: any) { + return _.replace( + content, + new RegExp(`${this.getStringLiteralInPhpPlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => this.stringLiteralInPhp[p1], + ); + } +} diff --git a/src/processors/processor.ts b/src/processors/processor.ts new file mode 100644 index 00000000..3640a949 --- /dev/null +++ b/src/processors/processor.ts @@ -0,0 +1,8 @@ +import type Formatter from "src/formatter"; + +export abstract class Processor { + constructor(protected formatter: Formatter) {} + + abstract preProcess(content: string): Promise; + abstract postProcess(content: string): Promise; +} diff --git a/src/processors/propsProcessor.ts b/src/processors/propsProcessor.ts new file mode 100644 index 00000000..f262a14f --- /dev/null +++ b/src/processors/propsProcessor.ts @@ -0,0 +1,56 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class PropsProcessor extends Processor { + private rawPropsBlocks: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveProps(content); + } + + async postProcess(content: string): Promise { + return await this.restoreRawPropsBlock(content); + } + + private async preserveProps(content: string): Promise { + return _.replace( + content, + /@props\(((?:[^\\(\\)]|\([^\\(\\)]*\))*)\)/gs, + (match: any, p1: any) => this.storeRawPropsBlock(p1), + ); + } + + async restoreRawPropsBlock(content: any) { + const regex = this.getRawPropsPlaceholder("(\\d+)"); + return replaceAsync( + content, + new RegExp(regex, "gms"), + async (_match: any, p1: any) => { + const placeholder = this.getRawPropsPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const formatted = `@props(${( + await util.formatRawStringAsPhp(this.rawPropsBlocks[p1], { + ...this.formatter.options, + }) + ).trim()})`; + + return util.indentRawPhpBlock(indent, formatted, this.formatter); + }, + ); + } + + storeRawPropsBlock(value: any) { + return this.getRawPropsPlaceholder(this.rawPropsBlocks.push(value) - 1); + } + + getRawPropsPlaceholder(replace: any) { + return _.replace("@__raw_props_block_#__@", "#", replace); + } +} diff --git a/src/processors/rawBladeBraceProcessor.ts b/src/processors/rawBladeBraceProcessor.ts new file mode 100644 index 00000000..de20a810 --- /dev/null +++ b/src/processors/rawBladeBraceProcessor.ts @@ -0,0 +1,78 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class RawBladeBraceProcessor extends Processor { + private rawBladeBraces: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveRawBladeBrace(content); + } + + async postProcess(content: string): Promise { + return await this.restoreRawBladeBrace(content); + } + + private async preserveRawBladeBrace(content: string): Promise { + return _.replace(content, /\{!!(.*?)!!\}/gs, (_match: any, p1: any) => { + // if content is blank + if (p1 === "") { + return this.storeRawBladeBrace(p1); + } + + // preserve a space if content contains only space, tab, or new line character + if (!/\S/.test(p1)) { + return this.storeRawBladeBrace(" "); + } + + // any other content + return this.storeRawBladeBrace(p1.trim()); + }); + } + + private async restoreRawBladeBrace(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res) => + replaceAsync( + // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message + res, + new RegExp(`${this.getRawBladeBracePlaceholder("(\\d+)")}`, "gms"), + async (_match: any, p1: any) => { + const placeholder = this.getRawBladeBracePlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + const bladeBrace = this.rawBladeBraces[p1]; + + if (bladeBrace.trim() === "") { + return `{!!${bladeBrace}!!}`; + } + + return util.indentRawPhpBlock( + indent, + `{!! ${( + await util.formatRawStringAsPhp( + bladeBrace, + this.formatter.options, + ) + ) + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .trim()} !!}`, + this.formatter, + ); + }, + ), + ); + } + + storeRawBladeBrace(value: any) { + const index = this.rawBladeBraces.push(value) - 1; + return this.getRawBladeBracePlaceholder(index); + } + + getRawBladeBracePlaceholder(replace: any) { + return _.replace("___raw_blade_brace_#___", "#", replace); + } +} diff --git a/src/processors/rawPhpTagProcessor.ts b/src/processors/rawPhpTagProcessor.ts new file mode 100644 index 00000000..20dc61b7 --- /dev/null +++ b/src/processors/rawPhpTagProcessor.ts @@ -0,0 +1,127 @@ +import detectIndent from "detect-indent"; +import beautify, { type JSBeautifyOptions } from "js-beautify"; +import _ from "lodash"; +import { formatPhpComment } from "src/comment"; +import replaceAsync from "string-replace-async"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class RawPhpTagProcessor extends Processor { + private rawPhpTags: string[] = []; + private phpComments: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveRawPhpTag(content); + } + + async postProcess(content: string): Promise { + return await this.restoreRawPhpTag(content); + } + + private async preserveRawPhpTag(content: string): Promise { + return _.replace(content, /<\?php(.*?)\?>/gms, (match: any) => + this.storeRawPhpTags(match), + ); + } + + private async restoreRawPhpTag(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res) => + replaceAsync( + // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message + res, + new RegExp(`${this.getRawPhpTagPlaceholder("(\\d+)")}`, "gms"), + async (_match: any, p1: any) => { + // const result= this.rawPhpTags[p1]; + try { + const matched = this.rawPhpTags[p1]; + const commentBlockExists = + /(?<=<\?php\s*?)\/\*.*?\*\/(?=\s*?\?>)/gim.test(matched); + const inlinedComment = commentBlockExists && util.isInline(matched); + const placeholder = this.getRawPhpTagPlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + if (inlinedComment) { + return matched; + } + + const result = ( + await util.formatStringAsPhp( + this.rawPhpTags[p1], + this.formatter.options, + ) + ) + .trim() + .trimEnd(); + + if (util.isInline(result)) { + return result; + } + + let preserved = this.preservePhpComment(result); + + if (indent.indent) { + preserved = util.indentRawPhpBlock( + indent, + preserved, + this.formatter, + ); + } + + const restored = this.restorePhpComment(preserved); + + return restored; + } catch (e) { + return `${this.rawPhpTags[p1]}`; + } + }, + ), + ); + } + + storeRawPhpTags(value: any) { + const index = this.rawPhpTags.push(value) - 1; + return this.getRawPhpTagPlaceholder(index); + } + + getRawPhpTagPlaceholder(replace: any) { + return _.replace("___raw_php_tag_#___", "#", replace); + } + + preservePhpComment(content: string) { + return _.replace( + content, + /\/\*(?:[^*]|[\r\n]|(?:\*+(?:[^*\/]|[\r\n])))*\*+\//gi, + (match: string) => this.storePhpComment(match), + ); + } + + restorePhpComment(content: string) { + return _.replace( + content, + new RegExp(`${this.getPhpCommentPlaceholder("(\\d+)")};{0,1}`, "gms"), + (_match: string, p1: number) => { + const placeholder = this.getPhpCommentPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + const formatted = formatPhpComment(this.phpComments[p1]); + + return util.indentPhpComment(indent, formatted, this.formatter); + }, + ); + } + + storePhpComment(value: string) { + return this.getPhpCommentPlaceholder( + (this.phpComments.push(value) - 1).toString(), + ); + } + + getPhpCommentPlaceholder(replace: string) { + return _.replace("___php_comment_#___", "#", replace); + } +} diff --git a/src/processors/scriptsProcessor.ts b/src/processors/scriptsProcessor.ts new file mode 100644 index 00000000..79423aa5 --- /dev/null +++ b/src/processors/scriptsProcessor.ts @@ -0,0 +1,149 @@ +import detectIndent from "detect-indent"; +import beautify from "js-beautify"; +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class ScriptsProcessor extends Processor { + private scripts: string[] = []; + private templatingStrings: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveScripts(content); + } + + async postProcess(content: string): Promise { + return await this.restoreScripts(content); + } + + private async preserveScripts(content: string): Promise { + return _.replace(content, /.*?<\/script>/gis, (match: any) => + this.storeScripts(match), + ); + } + + private async restoreScripts(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res) => + _.replace( + // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message + res, + new RegExp(`${this.getScriptPlaceholder("(\\d+)")}`, "gim"), + (_match: any, p1: number) => { + const script = this.scripts[p1]; + + const placeholder = this.getScriptPlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + const useTabs = + util.optional(this.formatter.options).useTabs || false; + + const options = { + indent_size: util.optional(this.formatter.options).indentSize || 4, + wrap_line_length: + util.optional(this.formatter.options).wrapLineLength || 120, + wrap_attributes: + util.optional(this.formatter.options).wrapAttributes || "auto", + wrap_attributes_min_attrs: util.optional(this.formatter.options) + .wrapAttributesMinAttrs, + indent_inner_html: + util.optional(this.formatter.options).indentInnerHtml || false, + extra_liners: util.optional(this.formatter.options).extraLiners, + indent_with_tabs: useTabs, + end_with_newline: false, + templating: ["php"], + }; + + if (useTabs) { + return this.indentScriptBlock( + indent, + _.replace( + beautify.html_beautify(script, options), + /\t/g, + "\t".repeat(this.formatter.indentSize), + ), + ); + } + + return this.indentScriptBlock( + indent, + beautify.html_beautify(script, options), + ); + }, + ), + ); + } + + private indentScriptBlock(indent: detectIndent.Indent, content: any) { + if (_.isEmpty(indent.indent)) { + return content; + } + + if (util.isInline(content)) { + return `${content}`; + } + + const leftIndentAmount = indent.amount; + const indentLevel = leftIndentAmount / this.formatter.indentSize; + const prefixSpaces = this.formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * this.formatter.indentSize, + ); + const prefixForEnd = this.formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * this.formatter.indentSize, + ); + + const preserved = _.replace(content, /`.*?`/gs, (match: any) => + this.storeTemplatingString(match), + ); + + const lines = preserved.split("\n"); + + const indented = _.chain(lines) + .map((line: any, index: any) => { + if (index === 0) { + return line; + } + + if (index === lines.length - 1) { + return prefixForEnd + line; + } + + if (_.isEmpty(line)) { + return line; + } + + return prefixSpaces + line; + }) + .value() + .join("\n"); + + return this.restoreTemplatingString(`${indented}`); + } + + restoreTemplatingString(content: any) { + return _.replace( + content, + new RegExp(`${this.getTemplatingStringPlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => this.templatingStrings[p1], + ); + } + + storeScripts(value: string) { + const index = this.scripts.push(value) - 1; + return this.getScriptPlaceholder(index); + } + + storeTemplatingString(value: any) { + const index = this.templatingStrings.push(value) - 1; + return this.getTemplatingStringPlaceholder(index); + } + + getScriptPlaceholder(replace: any) { + return _.replace("", "#", replace); + } + + getTemplatingStringPlaceholder(replace: any) { + return _.replace("___templating_str_#___", "#", replace); + } +} diff --git a/src/processors/shorthandBindingProcessor.ts b/src/processors/shorthandBindingProcessor.ts new file mode 100644 index 00000000..81a3aef4 --- /dev/null +++ b/src/processors/shorthandBindingProcessor.ts @@ -0,0 +1,92 @@ +import detectIndent from "detect-indent"; +import type { JSBeautifyOptions } from "js-beautify"; +import beautify from "js-beautify"; +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class ShorthandBindingProcessor extends Processor { + private shorthandBindings: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveShorthandBinding(content); + } + + async postProcess(content: string): Promise { + return await this.restoreShorthandBinding(content); + } + + private async preserveShorthandBinding(content: string): Promise { + return _.replace( + content, + /(?<=<(?!livewire:)[^<]*?(\s|x-bind)):{1}(?)[\w\-_.]*?=(["'])(?!=>)[^\2]*?\2(?=[^>]*?\/*?>)/gim, + (match: any) => `${this.storeShorthandBinding(match)}`, + ); + } + + private async restoreShorthandBinding(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getShorthandBindingPlaceholder("(\\d+)")}`, "gms"), + (_match: any, p1: any) => { + const placeholder = this.getShorthandBindingPlaceholder(p1); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const matched = this.shorthandBindings[p1]; + + const formatted = _.replace( + matched, + /(:{1,2}.*?=)(["'])(.*?)(?=\2)/gis, + (match, p2: string, p3: string, p4: string) => { + const beautifyOpts: JSBeautifyOptions = { + wrap_line_length: this.formatter.wrapLineLength - indent.amount, + brace_style: "preserve-inline", + }; + + if (p4 === "") { + return match; + } + + if (util.isInline(p4)) { + try { + return `${p2}${p3}${beautify + .js_beautify(p4.trim(), beautifyOpts) + .trim()}`; + } catch (error) { + return `${p2}${p3}${p4.trim()}`; + } + } + + return `${p2}${p3}${beautify + .js_beautify(p4.trim(), beautifyOpts) + .trim()}`; + }, + ); + + return `${util.indentComponentAttribute(indent.indent, formatted, this.formatter)}`; + }, + ); + } + + storeShorthandBinding(value: string) { + const index = this.shorthandBindings.push(value) - 1; + + return this.getShorthandBindingPlaceholder(index.toString(), value.length); + } + + getShorthandBindingPlaceholder(replace: string, length: any = 0) { + if (length && length > 0) { + const template = "___short_binding_#___"; + const gap = length - template.length; + return _.replace( + `___short_binding_${_.repeat("_", gap > 0 ? gap : 1)}#___`, + "#", + replace, + ); + } + return _.replace("___short_binding_+?#___", "#", replace); + } +} diff --git a/src/processors/sortHtmlAttributesProcessor.ts b/src/processors/sortHtmlAttributesProcessor.ts new file mode 100644 index 00000000..23fa64ae --- /dev/null +++ b/src/processors/sortHtmlAttributesProcessor.ts @@ -0,0 +1,37 @@ +import { sortAttributes } from "html-attribute-sorter"; +import _ from "lodash"; +import type { SortHtmlAttributes } from "src/runtimeConfig"; +import { Processor } from "./processor"; + +export class SortHtmlAttributesProcessor extends Processor { + private bladeComments: string[] = []; + + async preProcess(content: string): Promise { + return await this.sortHtmlAttribute(content); + } + + async postProcess(content: string): Promise {} + + private async sortHtmlAttribute(content: string): Promise { + const strategy: SortHtmlAttributes = + this.formatter.options.sortHtmlAttributes ?? "none"; + + if (!_.isEmpty(strategy) && strategy !== "none") { + const regexes = this.formatter.options.customHtmlAttributesOrder; + + if (_.isArray(regexes)) { + return sortAttributes(content, { + order: strategy, + customRegexes: regexes, + }); + } + + // when option is string + const customRegexes = _.chain(regexes).split(",").map(_.trim).value(); + + return sortAttributes(content, { order: strategy, customRegexes }); + } + + return content; + } +} diff --git a/src/processors/sortTailwindClassesProcessor.ts b/src/processors/sortTailwindClassesProcessor.ts new file mode 100644 index 00000000..5073d8d9 --- /dev/null +++ b/src/processors/sortTailwindClassesProcessor.ts @@ -0,0 +1,43 @@ +import { sortClasses } from "@shufo/tailwindcss-class-sorter"; +import _ from "lodash"; +import { Processor } from "./processor"; + +export class SortTailwindClassesProcessor extends Processor { + async preProcess(content: string): Promise { + return await this.sortTailwindClasses(content); + } + + async postProcess(content: string): Promise {} + + private async sortTailwindClasses(content: string): Promise { + if (!this.formatter.options.sortTailwindcssClasses) { + return content; + } + + return _.replace( + content, + /(?<=\s+(?!:)class\s*=\s*([\"\']))(.*?)(?=\1)/gis, + (_match, p1, p2) => { + if (_.isEmpty(p2)) { + return p2; + } + + if (this.formatter.options.tailwindcssConfigPath) { + const options = { + tailwindConfigPath: this.formatter.options.tailwindcssConfigPath, + }; + return sortClasses(p2, options); + } + + if (this.formatter.options.tailwindcssConfig) { + const options: any = { + tailwindConfig: this.formatter.options.tailwindcssConfig, + }; + return sortClasses(p2, options); + } + + return sortClasses(p2); + }, + ); + } +} diff --git a/src/processors/unbalancedDirectiveProcessor.ts b/src/processors/unbalancedDirectiveProcessor.ts new file mode 100644 index 00000000..6d244880 --- /dev/null +++ b/src/processors/unbalancedDirectiveProcessor.ts @@ -0,0 +1,59 @@ +import _ from "lodash"; +import { unbalancedStartTokens } from "src/indent"; +import { Processor } from "./processor"; + +export class UnbalancedDirectiveProcessor extends Processor { + private unbalancedDirectives: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveUnbalancedDirective(content); + } + + async postProcess(content: string): Promise { + return await this.restoreUnbalancedDirective(content); + } + + private async preserveUnbalancedDirective(content: any): Promise { + const regex = new RegExp( + `((${unbalancedStartTokens.join( + "|", + )})(?!.*?\\2)(?:\\s|\\(.*?\\)))+(?=.*?@endif)`, + "gis", + ); + + let replaced: string = _.replace( + content, + regex, + (_match: string, p1: string) => `${this.storeUnbalancedDirective(p1)}`, + ); + + if (regex.test(replaced)) { + replaced = await this.preserveUnbalancedDirective(replaced); + } + + return replaced; + } + + private async restoreUnbalancedDirective(content: string): Promise { + return new Promise((resolve) => resolve(content)).then((res: any) => + _.replace( + res, + /@if \(unbalanced___(\d+)___\)/gms, + (_match: any, p1: any) => { + const matched = this.unbalancedDirectives[p1]; + return matched; + }, + ), + ); + } + + storeUnbalancedDirective(value: string) { + return this.getUnbalancedDirectivePlaceholder( + (this.unbalancedDirectives.push(value) - 1).toString(), + ); + } + + getUnbalancedDirectivePlaceholder(replace: string) { + return _.replace("@if (unbalanced___#___)", "#", replace); + } +} diff --git a/src/processors/xdataProcessor.ts b/src/processors/xdataProcessor.ts new file mode 100644 index 00000000..77b8b735 --- /dev/null +++ b/src/processors/xdataProcessor.ts @@ -0,0 +1,61 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class XDataProcessor extends Processor { + private xData: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveXData(content); + } + + async postProcess(content: string): Promise { + return await this.restoreXData(content); + } + + private async preserveXData(content: string): Promise { + return _.replace( + content, + /(\s*)x-data="(.*?)"(\s*)/gs, + (_match: any, p1: any, p2: any, p3: any) => + `${p1}x-data="${this.storeXData(p2)}"${p3}`, + ); + } + + private async restoreXData(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getXDataPlaceholder("(\\d+)")}`, "gm"), + (_match: any, p1: any) => { + const placeholder = this.getXDataPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const lines = util.formatJS(this.xData[p1]).split("\n"); + + const indentLevel = + indent.amount / (this.formatter.indentCharacter === "\t" ? 4 : 1); + + const firstLine = lines[0]; + const prefix = this.formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel, + ); + const offsettedLines = lines.map((line) => prefix + line); + offsettedLines[0] = firstLine; + return `${offsettedLines.join("\n")}`; + }, + ); + } + + storeXData(value: string) { + const index = this.xData.push(value) - 1; + return this.getXDataPlaceholder(index); + } + + getXDataPlaceholder(replace: any) { + return _.replace("___x_data_#___", "#", replace); + } +} diff --git a/src/processors/xinitProcessor.ts b/src/processors/xinitProcessor.ts new file mode 100644 index 00000000..68c2a1c6 --- /dev/null +++ b/src/processors/xinitProcessor.ts @@ -0,0 +1,61 @@ +import detectIndent from "detect-indent"; +import _ from "lodash"; +import * as util from "../util"; +import { Processor } from "./processor"; + +export class XInitProcessor extends Processor { + private xInit: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveXInit(content); + } + + async postProcess(content: string): Promise { + return await this.restoreXInit(content); + } + + private async preserveXInit(content: string): Promise { + return _.replace( + content, + /(\s*)x-init="(.*?)"(\s*)/gs, + (_match: any, p1: any, p2: any, p3: any) => + `${p1}x-init="${this.storeXInit(p2)}"${p3}`, + ); + } + + private async restoreXInit(content: string): Promise { + return _.replace( + content, + new RegExp(`${this.getXInitPlaceholder("(\\d+)")}`, "gm"), + (_match: any, p1: number) => { + const placeholder = this.getXInitPlaceholder(p1.toString()); + const matchedLine = content.match( + new RegExp(`^(.*?)${placeholder}`, "gmi"), + ) ?? [""]; + const indent = detectIndent(matchedLine[0]); + + const lines = util.formatJS(this.xInit[p1]).split("\n"); + + const indentLevel = + indent.amount / (this.formatter.indentCharacter === "\t" ? 4 : 1); + + const firstLine = lines[0]; + const prefix = this.formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel, + ); + const offsettedLines = lines.map((line) => prefix + line); + offsettedLines[0] = firstLine; + return `${offsettedLines.join("\n")}`; + }, + ); + } + + storeXInit(value: string) { + const index = this.xInit.push(value) - 1; + return this.getXInitPlaceholder(index); + } + + getXInitPlaceholder(replace: any) { + return _.replace("___x_init_#___", "#", replace); + } +} diff --git a/src/processors/xslotProcessor.ts b/src/processors/xslotProcessor.ts new file mode 100644 index 00000000..7201f3de --- /dev/null +++ b/src/processors/xslotProcessor.ts @@ -0,0 +1,38 @@ +import _ from "lodash"; +import { Processor } from "./processor"; + +export class XslotProcessor extends Processor { + private xSlot: string[] = []; + + async preProcess(content: string): Promise { + return await this.preserveXslot(content); + } + + async postProcess(content: string): Promise { + return await this.restoreXslot(content); + } + + private async preserveXslot(content: string): Promise { + return _.replace( + content, + /(?<=<\/?)(x-slot:[\w_\\-]+)(?=(?:[^>]*?[^?])?>)/gm, + (match: string) => this.storeXslot(match), + ); + } + + private async restoreXslot(content: string): Promise { + return _.replace( + content, + /x-slot\s*--___(\d+)___--/gms, + (_match: string, p1: number) => this.xSlot[p1], + ).replace(/(?<=)/gm, () => ""); + } + + storeXslot(value: string) { + return this.getXslotPlaceholder((this.xSlot.push(value) - 1).toString()); + } + + getXslotPlaceholder(replace: any) { + return _.replace("x-slot --___#___--", "#", replace); + } +} diff --git a/src/util.ts b/src/util.ts index 5a4dab4f..c78ddcc4 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,6 +5,7 @@ import os from "node:os"; import phpPlugin from "@prettier/plugin-php/standalone"; import chalk from "chalk"; import detectIndent from "detect-indent"; +import beautify from "js-beautify"; /* eslint-disable max-len */ import _ from "lodash"; import * as prettier from "prettier/standalone"; @@ -14,8 +15,10 @@ import { phpKeywordEndTokens, phpKeywordStartTokens, } from "./indent"; +import type { CLIOption, Formatter, FormatterOption } from "./main"; import { nestedParenthesisRegex } from "./regex"; import type { EndOfLine } from "./runtimeConfig"; +import * as vsctm from "./vsctm"; export const optional = (obj: any) => { const chain = { @@ -494,3 +497,352 @@ export function getEndOfLine(endOfLine?: EndOfLine): string { return os.EOL; } } + +export function isInline(content: any) { + return _.split(content, "\n").length === 1; +} + +export function indentRawPhpBlock( + indent: detectIndent.Indent, + content: any, + formatter: Formatter, +) { + if (_.isEmpty(indent.indent)) { + return content; + } + + if (isInline(content)) { + return `${content}`; + } + + const leftIndentAmount = indent.amount; + const indentLevel = leftIndentAmount / formatter.indentSize; + const prefixSpaces = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * formatter.indentSize, + ); + + const lines = content.split("\n"); + + return _.chain(lines) + .map((line: any, index: any) => { + if (index === 0) { + return line.trim(); + } + + return prefixSpaces + line; + }) + .value() + .join("\n"); +} + +export function indentPhpComment( + indent: detectIndent.Indent, + content: string, + formatter: Formatter, +) { + if (_.isEmpty(indent.indent)) { + return content; + } + + if (isInline(content)) { + return `${content}`; + } + + const leftIndentAmount = indent.amount; + const indentLevel = leftIndentAmount / formatter.indentSize; + const prefixSpaces = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * formatter.indentSize, + ); + + const lines = content.split("\n"); + let withoutCommentLine = false; + + return _.chain(lines) + .map((line: string, index: number) => { + if (index === 0) { + return line.trim(); + } + + if (!line.trim().startsWith("*")) { + withoutCommentLine = true; + return line; + } + + if (line.trim().endsWith("*/") && withoutCommentLine) { + return line; + } + + return prefixSpaces + line; + }) + .join("\n") + .value(); +} + +export async function formatExpressionInsideBladeDirective( + matchedExpression: string, + indent: detectIndent.Indent, + formatter: Formatter, + wrapLength: number | undefined = undefined, +) { + const formatTarget = `func(${matchedExpression})`; + const formattedExpression = await formatRawStringAsPhp(formatTarget, { + ...formatter.options, + printWidth: wrapLength ?? formatter.defaultPhpFormatOption.printWidth, + }); + + if (formattedExpression === formatTarget) { + return matchedExpression; + } + + let inside = formattedExpression + .replace(/([\n\s]*)->([\n\s]*)/gs, "->") + .replace(/(? `${p1}]\n)`) + .replace(/,[\n\s]*?\)/gs, ")") + .replace(/,(\s*?\))$/gm, (match, p1) => p1) + .trim(); + + if (formatter.options.useTabs || false) { + inside = _.replace( + inside, + /(?<=^ *) {4}/gm, + "\t".repeat(formatter.indentSize), + ); + } + + inside = inside.replace(/func\((.*)\)/gis, (match: string, p1: string) => p1); + if (isInline(inside.trim())) { + inside = inside.trim(); + } + + return indentRawPhpBlock(indent, inside, formatter); +} + +export function indentBladeDirectiveBlock( + indent: detectIndent.Indent, + content: any, + formatter: Formatter, +) { + if (_.isEmpty(indent.indent)) { + return content; + } + + if (isInline(content)) { + return `${indent.indent}${content}`; + } + + const leftIndentAmount = indent.amount; + const indentLevel = leftIndentAmount / formatter.indentSize; + const prefixSpaces = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * formatter.indentSize, + ); + const prefixForEnd = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * formatter.indentSize, + ); + + const lines = content.split("\n"); + + return _.chain(lines) + .map((line: any, index: any) => { + if (index === lines.length - 1) { + return prefixForEnd + line; + } + + return prefixSpaces + line; + }) + .value() + .join("\n"); +} + +export async function isMultilineStatement( + rawBlock: any, + formatter: Formatter, +) { + return ( + (await formatStringAsPhp(``, formatter.options)) + .trimRight() + .split("\n").length > 1 + ); +} + +export function indentRawBlock( + indent: detectIndent.Indent, + content: any, + formatter: Formatter, +) { + if (isInline(content)) { + return `${indent.indent}${content}`; + } + + const leftIndentAmount = indent.amount; + const indentLevel = leftIndentAmount / formatter.indentSize; + const prefix = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : (indentLevel + 1) * formatter.indentSize, + ); + const prefixForEnd = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * formatter.indentSize, + ); + + const lines = content.split("\n"); + + return _.chain(lines) + .map((line: any, index: any) => { + if (index === 0) { + return line.trim(); + } + + if (index === lines.length - 1) { + return prefixForEnd + line; + } + + if (line.length === 0) { + return line; + } + + return prefix + line; + }) + .join("\n") + .value(); +} + +export function indentComponentAttribute( + prefix: string, + content: string, + formatter: Formatter, +) { + if (_.isEmpty(prefix)) { + return content; + } + + if (isInline(content)) { + return `${content}`; + } + + if (isInline(content) && /\S/.test(prefix)) { + return `${content}`; + } + + const leftIndentAmount = detectIndent(prefix).amount; + const indentLevel = leftIndentAmount / formatter.indentSize; + const prefixSpaces = formatter.indentCharacter.repeat( + indentLevel < 0 ? 0 : indentLevel * formatter.indentSize, + ); + + const lines = content.split("\n"); + + return _.chain(lines) + .map((line: any, index: any) => { + if (index === 0) { + return line.trim(); + } + + return prefixSpaces + line; + }) + .value() + .join("\n"); +} + +export function formatAsHtml(data: any, formatter: Formatter) { + const options = { + indent_size: optional(formatter.options).indentSize || 4, + wrap_line_length: optional(formatter.options).wrapLineLength || 120, + wrap_attributes: optional(formatter.options).wrapAttributes || "auto", + wrap_attributes_min_attrs: optional(formatter.options) + .wrapAttributesMinAttrs, + indent_inner_html: optional(formatter.options).indentInnerHtml || false, + end_with_newline: optional(formatter.options).endWithNewline || true, + max_preserve_newlines: optional(formatter.options).noMultipleEmptyLines + ? 1 + : undefined, + extra_liners: optional(formatter.options).extraLiners, + css: { + end_with_newline: false, + }, + eol: formatter.endOfLine, + }; + + const promise = new Promise((resolve) => resolve(data)) + .then((content) => preserveDirectives(content)) + .then((preserved) => beautify.html_beautify(preserved, options)) + .then((content) => revertDirectives(content)); + + return Promise.resolve(promise); +} + +export async function formatAsBlade(content: any, formatter: Formatter) { + // init parameters + formatter.currentIndentLevel = 0; + formatter.shouldBeIndent = false; + + const splittedLines = splitByLines(content); + + const vsctmModule = await new vsctm.VscodeTextmate( + formatter.vsctm, + formatter.oniguruma, + ); + const registry = vsctmModule.createRegistry(); + + const formatted = registry + .loadGrammar("text.html.php.blade") + .then((grammar: any) => vsctmModule.tokenizeLines(splittedLines, grammar)) + .then((tokenizedLines: any) => + formatter.formatTokenizedLines(splittedLines, tokenizedLines), + ) + .catch((err: any) => { + throw err; + }); + + return formatted; +} + +export function formatJS(jsCode: string): string { + let code: string = jsCode; + const tempVarStore: any = { + js: [], + entangle: [], + }; + for (const directive of Object.keys(tempVarStore)) { + code = code.replace( + new RegExp( + `@${directive}\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)`, + "gs", + ), + (m: any) => { + const index = tempVarStore[directive].push(m) - 1; + return getPlaceholder(directive, index, m.length); + }, + ); + } + code = beautify.js_beautify(code, { brace_style: "preserve-inline" }); + + for (const directive of Object.keys(tempVarStore)) { + code = code.replace( + new RegExp(getPlaceholder(directive, "_*(\\d+)"), "gms"), + (_match: any, p1: any) => tempVarStore[directive][p1], + ); + } + + return code; +} + +export function getPlaceholder( + attribute: string, + replace: any, + length: any = null, +) { + if (length && length > 0) { + const template = `___${attribute}_#___`; + const gap = length - template.length; + return _.replace( + `___${attribute}${_.repeat("_", gap > 0 ? gap : 1)}#___`, + "#", + replace, + ); + } + + if (_.isNull(length)) { + return _.replace(`___${attribute}_#___`, "#", replace); + } + + return _.replace(`s___${attribute}_+?#___`, "#", replace); +}