`,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("raw blade brace losts indentation https://github.com/shufo/vscode-blade-formatter/issues/474", async () => {
+ const content = [
+ ` `,
+ `
`,
+ "",
+ "
",
+ `
`,
+ ` {!! Form::button(trans('forms.save-changes'), [`,
+ ` 'class' => 'btn btn-success btn-block margin-bottom-1 mt-3 mb-2 btn-save',`,
+ ` 'type' => 'button',`,
+ ` 'data-toggle' => 'modal',`,
+ ` 'data-target' => '#confirmSave',`,
+ ` 'data-title' => trans('modals.edit_user__modal_text_confirm_title'),`,
+ ` 'data-message' => trans('modals.edit_user__modal_text_confirm_message'),`,
+ "]) !!}",
+ "
",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ ` `,
+ `
`,
+ "",
+ "
",
+ `
`,
+ ` {!! Form::button(trans('forms.save-changes'), [`,
+ ` 'class' => 'btn btn-success btn-block margin-bottom-1 mt-3 mb-2 btn-save',`,
+ ` 'type' => 'button',`,
+ ` 'data-toggle' => 'modal',`,
+ ` 'data-target' => '#confirmSave',`,
+ ` 'data-title' => trans('modals.edit_user__modal_text_confirm_title'),`,
+ ` 'data-message' => trans('modals.edit_user__modal_text_confirm_message'),`,
+ " ]) !!}",
+ "
",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("Long raw blade brace line should be formatted into multiple lines", async () => {
+ const content = [
+ `{!! Form::button(trans('forms.save-changes'), [ 'class' => 'btn btn-success btn-block margin-bottom-1 mt-3 mb-2 btn-save', 'type' => 'button', 'data-toggle' => 'modal', 'data-target' => '#confirmSave', 'data-title' => trans('modals.edit_user__modal_text_confirm_title'), 'data-message' => trans('modals.edit_user__modal_text_confirm_message'),]) !!}`,
+ ].join("\n");
+
+ const expected = [
+ `{!! Form::button(trans('forms.save-changes'), [`,
+ ` 'class' => 'btn btn-success btn-block margin-bottom-1 mt-3 mb-2 btn-save',`,
+ ` 'type' => 'button',`,
+ ` 'data-toggle' => 'modal',`,
+ ` 'data-target' => '#confirmSave',`,
+ ` 'data-title' => trans('modals.edit_user__modal_text_confirm_title'),`,
+ ` 'data-message' => trans('modals.edit_user__modal_text_confirm_message'),`,
+ "]) !!}",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("do not preserve unnecessary spaces in blade braces", async () => {
+ const content = [
+ // escaped brade braces
+ "{{}}",
+ "{{ }}",
+ "{{ ",
+ " ",
+ " }}",
+ "{{",
+ "",
+ "auth()->user()->some() }}",
+ "{{ auth()->user()->some() }}
",
+ // raw blade braces
+ "{!!!!}",
+ "{!! !!}",
+ "{!! ",
+ " ",
+ " !!}",
+ "{!!",
+ "",
+ "auth()->user()->some() !!}",
+ "{!! auth()->user()->some() !!}
",
+ ].join("\n");
+
+ const expected = [
+ // escaped brade braces
+ "{{}}",
+ "{{ }}",
+ "{{ }}",
+ "{{ auth()->user()->some() }}",
+ "{{ auth()->user()->some() }}
",
+ // raw blade braces
+ "{!!!!}",
+ "{!! !!}",
+ "{!! !!}",
+ "{!! auth()->user()->some() !!}",
+ "{!! auth()->user()->some() !!}
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/builtin-directives.test.ts b/__tests__/formatter/builtin-directives.test.ts
new file mode 100644
index 00000000..b6beefc4
--- /dev/null
+++ b/__tests__/formatter/builtin-directives.test.ts
@@ -0,0 +1,870 @@
+import assert from "node:assert";
+import { describe, test } from "vitest";
+import { BladeFormatter, Formatter } from "../../src/main.js";
+import * as util from "../support/util";
+
+const formatter = () => {
+ return new Formatter({ indentSize: 4 });
+};
+
+describe("formatter builtin directives test", () => {
+ const builtInDirectives = [
+ "auth",
+ "component",
+ "empty",
+ "can",
+ "canany",
+ "cannot",
+ "forelse",
+ "guest",
+ "isset",
+ "push",
+ "section",
+ "slot",
+ "verbatim",
+ "prepend",
+ "error",
+ ];
+
+ for (const directive of builtInDirectives) {
+ test(`builtin directive test: ${directive}`, () => {
+ const content = [
+ "",
+ `@${directive}($foo)`,
+ "@if ($user)",
+ "{{ $user->name }}",
+ "@endif",
+ `@end${directive}`,
+ " ",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @${directive}($foo)`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ ` @end${directive}`,
+ " ",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+ }
+
+ const phpDirectives = ["if", "while"];
+
+ for (const directive of phpDirectives) {
+ test("php builtin directive test", () => {
+ const content = [
+ "",
+ `@${directive}($foo)`,
+ "@if ($user)",
+ "{{ $user->name }}",
+ "@endif",
+ `@end${directive}`,
+ " ",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @${directive} ($foo)`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ ` @end${directive}`,
+ " ",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+ }
+
+ const tokenWithoutParams = ["auth", "guest", "production", "once"];
+
+ for (const directive of tokenWithoutParams) {
+ test("token without param directive test", () => {
+ const content = [
+ "",
+ `@${directive}`,
+ "@if ($user)",
+ "{{ $user->name }}",
+ "@endif",
+ `@end${directive}`,
+ "
",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @${directive}`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ ` @end${directive}`,
+ "
",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+ }
+
+ test("section directive test", () => {
+ const content = [
+ "",
+ `@section('foo')`,
+ `@section('bar')`,
+ "@if($user)",
+ "{{ $user->name }}",
+ "@endif",
+ "@endsection",
+ "
",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @section('foo')`,
+ ` @section('bar')`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ " @endsection",
+ "
",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("directive token should case insensitive", () => {
+ const content = [
+ "",
+ `@Section('foo')`,
+ `@section('bar')`,
+ "@if($user)",
+ "{{ $user->name }}",
+ "@foreach($users as $user)",
+ "{{ $user->id }}",
+ "@endForeach",
+ "@endIf",
+ "@endSection",
+ "
",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @Section('foo')`,
+ ` @section('bar')`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @foreach ($users as $user)",
+ " {{ $user->id }}",
+ " @endForeach",
+ " @endIf",
+ " @endSection",
+ "
",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("multiple section directive test", () => {
+ const content = [
+ "",
+ `@section('foo')`,
+ `@section('bar')`,
+ `@section('baz')`,
+ "@if($user)",
+ "{{ $user->name }}",
+ "@endif",
+ "@endsection",
+ "
",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @section('foo')`,
+ ` @section('bar')`,
+ ` @section('baz')`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ " @endsection",
+ "
",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("hasSection", () => {
+ const content = [
+ "",
+ `@hasSection('navigation')`,
+ "@if ($user)",
+ "{{ $user->name }}",
+ "@endif",
+ "@endif",
+ " ",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @hasSection('navigation')`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ " @endif",
+ " ",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("@for directive should work", async () => {
+ const content = [
+ "@for ($i=0;$i<=5;$i++)",
+ ``,
+ "
",
+ "@endfor",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "@for ($i = 0; $i <= 5; $i++)",
+ ` `,
+ "
",
+ "@endfor",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("@foreach directive should work", async () => {
+ const content = [
+ "@foreach($users as $user)",
+ ``,
+ "
",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "@foreach ($users as $user)",
+ ` `,
+ "
",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("@foreach directive should work with variable key", async () => {
+ const content = [
+ `@foreach($users["foo"] as $user)`,
+ ``,
+ "
",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ const expected = [
+ `@foreach ($users['foo'] as $user)`,
+ ` `,
+ "
",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("@foreach directive should work with children methods", async () => {
+ const content = [
+ "@foreach($user->blogs() as $blog)",
+ ``,
+ "
",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "@foreach ($user->blogs() as $blog)",
+ ` `,
+ "
",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("@switch directive should work", async () => {
+ const content = [
+ "@switch($i)",
+ "@case(1)",
+ " First case...",
+ " @break",
+ "",
+ "@case(2)",
+ " Second case...",
+ " @break",
+ "",
+ "@case(3)",
+ "@case(4)",
+ " Third case...",
+ " @break",
+ "",
+ "@default",
+ " Default case...",
+ "@endswitch",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "@switch($i)",
+ " @case(1)",
+ " First case...",
+ " @break",
+ "",
+ " @case(2)",
+ " Second case...",
+ " @break",
+ "",
+ " @case(3)",
+ " @case(4)",
+ " Third case...",
+ " @break",
+ "",
+ " @default",
+ " Default case...",
+ "@endswitch",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("forelse directive should work", async () => {
+ const content = [
+ "@forelse($students as $student)",
+ "foo
",
+ "@empty",
+ "empty",
+ "@endforelse",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "@forelse($students as $student)",
+ " foo
",
+ "@empty",
+ " empty",
+ "@endforelse",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should format within @php directive", async () => {
+ const content = [
+ " @php",
+ " if ($user) {",
+ ` $user->name = 'foo';`,
+ " }",
+ " @endphp",
+ ].join("\n");
+
+ const expected = [
+ " @php",
+ " if ($user) {",
+ ` $user->name = 'foo';`,
+ " }",
+ " @endphp",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("forelse inside if directive should work #254", async () => {
+ const content = [
+ "@if (true)",
+ "",
+ "@forelse($elems as $elem)",
+ " ",
+ "@empty",
+ " ",
+ "@endforelse",
+ "
",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ "@if (true)",
+ " ",
+ " @forelse($elems as $elem)",
+ " ",
+ " @empty",
+ " ",
+ " @endforelse",
+ "
",
+ "@endif",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("directives with optional endtags", async () => {
+ const content = [
+ `@extends('layouts.test')`,
+ `@section('title', 'This is title')`,
+ `@section('content')`,
+ ` `,
+ " This is content.",
+ "
",
+ "@endsection",
+ "",
+ "@if (true)",
+ ` @push('some-stack', $some->getContent())`,
+ " @section($aSection, $some->content)",
+ ` @push('some-stack')`,
+ " more",
+ " @endpush",
+ ` @prepend($stack->name, 'here we go')`,
+ "@endif",
+ "",
+ ].join("\n");
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, content);
+ });
+ });
+
+ test("@class directive", async () => {
+ let content = [
+ `$isActive])> `,
+ ].join("\n");
+ let expected = [
+ ` $isActive])> `,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ `$isActive,`,
+ " ])> ",
+ ].join("\n");
+
+ expected = [
+ ` $isActive])> `,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ `$isActive,`,
+ ` 'text-gray-500' => !$isActive,`,
+ ` 'bg-red' => $hasError,`,
+ "])> ",
+ ].join("\n");
+
+ expected = [
+ " $isActive,`,
+ ` 'text-gray-500' => !$isActive,`,
+ ` 'bg-red' => $hasError,`,
+ "])> ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ "",
+ `$isActive,`,
+ ` 'text-gray-500' => !$isActive,`,
+ ` 'bg-red' => $hasError,`,
+ "])> ",
+ "
",
+ ].join("\n");
+
+ expected = [
+ "",
+ " $isActive,`,
+ ` 'text-gray-500' => !$isActive,`,
+ ` 'bg-red' => $hasError,`,
+ " ])> ",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("inline @json directive", async () => {
+ const content = [
+ "`,
+ "foo",
+ " ",
+ ``,
+ "
",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " foo",
+ " ",
+ ``,
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("inline @error directive should keep its format", async () => {
+ const content = [
+ ``,
+ " Choose restaurant",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " Choose restaurant",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@checked directive", async () => {
+ const content = [
+ ` active)) />`,
+ ].join("\n");
+
+ const expected = [
+ ` active)) />`,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@selected directive", async () => {
+ const content = [
+ ``,
+ "@foreach ($product->versions as $version)",
+ ``,
+ "{{ $version }}",
+ " ",
+ "@endforeach",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " @foreach ($product->versions as $version)",
+ ` `,
+ " {{ $version }}",
+ " ",
+ " @endforeach",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@disabled directive", async () => {
+ const content = [
+ `isNotEmpty() )>Submit `,
+ ].join("\n");
+
+ const expected = [
+ `isNotEmpty())>Submit `,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@includeIf, @includeWhen, @includeUnless and @includeFirst directive", async () => {
+ const content = [
+ "",
+ `@includeIf('livewire.cx.equipment-list-internal.account',['status'=>'complete',`,
+ `'foo'=>$user,'bar'=>$bbb,'baz'=>$myVariable])`,
+ "
",
+ "",
+ `@includeWhen($boolean,'livewire.cx.equipment-list-internal.account',['status'=>'complete',`,
+ `'foo'=>$user,'bar'=>$bbb,'baz'=>$myVariable])`,
+ "
",
+ "",
+ `@includeUnless($boolean,'livewire.cx.equipment-list-internal.account',['status'=>'complete',`,
+ `'foo'=>$user,'bar'=>$bbb,'baz'=>$myVariable])`,
+ "
",
+ "",
+ `@includeFirst(['custom.admin','admin'],['status'=>'complete',`,
+ `'foo'=>$user,'bar'=>$bbb,'baz'=>$myVariable])`,
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @includeIf('livewire.cx.equipment-list-internal.account', [`,
+ ` 'status' => 'complete',`,
+ ` 'foo' => $user,`,
+ ` 'bar' => $bbb,`,
+ ` 'baz' => $myVariable,`,
+ " ])",
+ "
",
+ "",
+ ` @includeWhen($boolean, 'livewire.cx.equipment-list-internal.account', [`,
+ ` 'status' => 'complete',`,
+ ` 'foo' => $user,`,
+ ` 'bar' => $bbb,`,
+ ` 'baz' => $myVariable,`,
+ " ])",
+ "
",
+ "",
+ ` @includeUnless($boolean, 'livewire.cx.equipment-list-internal.account', [`,
+ ` 'status' => 'complete',`,
+ ` 'foo' => $user,`,
+ ` 'bar' => $bbb,`,
+ ` 'baz' => $myVariable,`,
+ " ])",
+ "
",
+ "",
+ " @includeFirst(",
+ ` ['custom.admin', 'admin'],`,
+ ` ['status' => 'complete', 'foo' => $user, 'bar' => $bbb, 'baz' => $myVariable]`,
+ " )",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@aware directive #576", async () => {
+ const content = [
+ `@aware(['color'=>'gray'])`,
+ "@aware([",
+ ` 'variant' => 'primary',`,
+ ` 'colors' => [`,
+ ` 'primary' => 'btn-primary',`,
+ ` 'secondary' => 'btn-secondary',`,
+ ` 'danger' => 'btn-danger',`,
+ " ]",
+ "])",
+ ].join("\n");
+
+ const expected = [
+ `@aware(['color' => 'gray'])`,
+ "@aware([",
+ ` 'variant' => 'primary',`,
+ ` 'colors' => [`,
+ ` 'primary' => 'btn-primary',`,
+ ` 'secondary' => 'btn-secondary',`,
+ ` 'danger' => 'btn-danger',`,
+ " ],",
+ "])",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@pushonce directive", async () => {
+ const content = [
+ `@pushOnce('scripts')`,
+ "",
+ "@endPushOnce",
+ ].join("\n");
+
+ const expected = [
+ `@pushOnce('scripts')`,
+ " ",
+ "@endPushOnce",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@prependonce directive", async () => {
+ const content = [
+ `@prependOnce('scripts')`,
+ "",
+ "@endPrependOnce",
+ ].join("\n");
+
+ const expected = [
+ `@prependOnce('scripts')`,
+ " ",
+ "@endPrependOnce",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("nested hasSection~endif", async () => {
+ const content = [
+ "",
+ ` @hasSection('navigation')`,
+ ` @hasSection('techdocs')`,
+ " {{ $user->name }}",
+ " @endif",
+ " @endif",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @hasSection('navigation')`,
+ ` @hasSection('techdocs')`,
+ " {{ $user->name }}",
+ " @endif",
+ " @endif",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@disabled directive with method access https://github.com/shufo/vscode-blade-formatter/issues/429", async () => {
+ const content = [
+ "@disabled(!auth()->user()->ownsTest($variable)) @if ($this->$variable) ... @else ... @endif",
+ "@disabled(!auth()->user()->ownsTest($variable))",
+ ].join("\n");
+
+ const expected = [
+ "@disabled(!auth()->user()->ownsTest($variable)) @if ($this->$variable)",
+ " ...",
+ "@else",
+ " ...",
+ "@endif",
+ "@disabled(!auth()->user()->ownsTest($variable))",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@foreach directive with nested method", async () => {
+ const content = [
+ "@foreach (auth()->user()->currentxy->shops() as $shop)",
+ "foo",
+ "@endforeach",
+ ].join("\n");
+
+ const expected = [
+ "@foreach (auth()->user()->currentxy->shops() as $shop)",
+ " foo",
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("unless directive with arrowed method", async () => {
+ const content = [
+ "@unless (auth()->user()->hasVerifiedEmail())",
+ " Please check and verify your email to access the system
",
+ "@endunless",
+ ].join("\n");
+
+ const expected = [
+ "@unless (auth()->user()->hasVerifiedEmail())",
+ " Please check and verify your email to access the system
",
+ "@endunless",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ sortHtmlAttributes: "idiomatic",
+ });
+ });
+});
diff --git a/__tests__/formatter/components.test.ts b/__tests__/formatter/components.test.ts
new file mode 100644
index 00000000..0ef6364e
--- /dev/null
+++ b/__tests__/formatter/components.test.ts
@@ -0,0 +1,145 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter components test", () => {
+ test("component attribute name #346", async () => {
+ let content = [` `].join("\n");
+ let expected = [` `, ""].join("\n");
+
+ util.doubleFormatCheck(content, expected);
+
+ content = [" "].join("\n");
+ expected = [` `, ""].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("directive inside component attribute", async () => {
+ const content = [
+ `@section('body')`,
+ ` `,
+ "@endsection",
+ ``,
+ " Submit",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ `@section('body')`,
+ ` `,
+ "@endsection",
+ ``,
+ " Submit",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("colon prefixed attribute #552", async () => {
+ const content = [
+ "",
+ "@if ($user)",
+ "Is HR",
+ "@endif",
+ " ",
+ ``,
+ ` `,
+ " ",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @if ($user)",
+ " Is HR",
+ " @endif",
+ " ",
+ ``,
+ ` `,
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@component directive indentation", async () => {
+ const content = [
+ "",
+ "
",
+ `@component('path.to.component', [`,
+ ` 'title' => 'My title',`,
+ `'description' => '',`,
+ ` 'header' => [`,
+ ` 'transparent' => true,`,
+ " ],",
+ ` 'footer' => [`,
+ ` 'hide' => true,`,
+ " ],",
+ " ])",
+ "
",
+ " some content",
+ "
",
+ " @endcomponent",
+ "
",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "
",
+ ` @component('path.to.component', [`,
+ ` 'title' => 'My title',`,
+ ` 'description' => '',`,
+ ` 'header' => [`,
+ ` 'transparent' => true,`,
+ " ],",
+ ` 'footer' => [`,
+ ` 'hide' => true,`,
+ " ],",
+ " ])",
+ "
",
+ " some content",
+ "
",
+ " @endcomponent",
+ "
",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("Without component prefix declared, return syntax incorrect code", async () => {
+ const content = ['', " "].join(
+ "\n",
+ );
+
+ const expected = [
+ '',
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ componentPrefix: [],
+ });
+ });
+
+ test("Component prefix option correct format", async () => {
+ const content = ['', " "].join(
+ "\n",
+ );
+
+ const expected = [
+ '',
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ componentPrefix: ["foo:"],
+ });
+ });
+});
diff --git a/__tests__/formatter/curly-braces.test.ts b/__tests__/formatter/curly-braces.test.ts
new file mode 100644
index 00000000..992060ed
--- /dev/null
+++ b/__tests__/formatter/curly-braces.test.ts
@@ -0,0 +1,33 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter curly braces test", () => {
+ test("double curly brace expression for js framework", async () => {
+ const content = [
+ ``,
+ ` `,
+ ` @{{ ok? 'YES': 'NO' }} `,
+ " ",
+ ` @{{ message.split('').reverse().join('') }}`,
+ " ",
+ ` @{{item.roles.map(role=>role.name).join(', ')}}`,
+ " ",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ ` `,
+ ` @{{ ok ? 'YES' : 'NO' }} `,
+ " ",
+ ` @{{ message.split('').reverse().join('') }}`,
+ " ",
+ ` @{{ item.roles.map(role => role.name).join(', ') }}`,
+ " ",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/custom-directives.test.ts b/__tests__/formatter/custom-directives.test.ts
new file mode 100644
index 00000000..c339b8fa
--- /dev/null
+++ b/__tests__/formatter/custom-directives.test.ts
@@ -0,0 +1,218 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter custom directives test", () => {
+ test("@button directive", async () => {
+ let content = [`@button(['class'=>'btn btn-primary p-btn-wide',])`].join(
+ "\n",
+ );
+ let expected = [
+ `@button(['class' => 'btn btn-primary p-btn-wide'])`,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ "@button([",
+ `'class'=>'btn btn-primary p-btn-wide',`,
+ "])",
+ ].join("\n");
+
+ expected = [
+ "@button([",
+ ` 'class' => 'btn btn-primary p-btn-wide',`,
+ "])",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ "",
+ `@button(['class' => 'btn btn-primary p-btn-wide',])`,
+ "
",
+ ].join("\n");
+
+ expected = [
+ "",
+ ` @button(['class' => 'btn btn-primary p-btn-wide'])`,
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ "",
+ "@button([",
+ `'class' => 'btn btn-primary p-btn-wide',`,
+ `'text' => 'Save',`,
+ "])",
+ "
",
+ ].join("\n");
+
+ expected = [
+ "",
+ " @button([",
+ ` 'class' => 'btn btn-primary p-btn-wide',`,
+ ` 'text' => 'Save',`,
+ " ])",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ content = [
+ "",
+ "
",
+ "@button([",
+ `'class' => 'btn btn-primary p-btn-wide',`,
+ `'text' => 'Save',`,
+ "])",
+ "
",
+ "
",
+ ].join("\n");
+
+ expected = [
+ "",
+ "
",
+ " @button([",
+ ` 'class' => 'btn btn-primary p-btn-wide',`,
+ ` 'text' => 'Save',`,
+ " ])",
+ "
",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@set directive #542", async () => {
+ const content = [
+ `@set($myVariableWithVeryVeryVeryVeryVeryLongName = ($myFirstCondition || $mySecondCondition)?'My text':'My alternative text')`,
+ ].join("\n");
+
+ const expected = [
+ `@set($myVariableWithVeryVeryVeryVeryVeryLongName = $myFirstCondition || $mySecondCondition ? 'My text' : 'My alternative text')`,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("line break custom directive", async () => {
+ const content = [
+ `@disk('local') foo @elsedisk('s3') bar @else baz @enddisk`,
+ ].join("\n");
+
+ const expected = [
+ `@disk('local')`,
+ " foo",
+ `@elsedisk('s3')`,
+ " bar",
+ "@else",
+ " baz",
+ "@enddisk",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("upper case/lower case mixed custom directive", async () => {
+ const content = [
+ "",
+ "@largestFirst(1, 2)",
+ "Lorem ipsum",
+ "@elseLargestFirst(5, 3)",
+ "dolor sit amet",
+ "@else",
+ "consectetur adipiscing elit",
+ "@endLargestFirst",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @largestFirst(1, 2)",
+ " Lorem ipsum",
+ " @elseLargestFirst(5, 3)",
+ " dolor sit amet",
+ " @else",
+ " consectetur adipiscing elit",
+ " @endLargestFirst",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("custom directive with raw string parameter should be work", async () => {
+ const content = ["@popper(This should be work)"].join("\n");
+ const expected = ["@popper(This should be work)", ""].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@formField directive", async () => {
+ const content = [
+ `@formField('input', [`,
+ `'name' => 'page_title',`,
+ `'label' => 'Page title',`,
+ `'maxlength' => 200`,
+ "])",
+ ].join("\n");
+
+ const expected = [
+ `@formField('input', [`,
+ ` 'name' => 'page_title',`,
+ ` 'label' => 'Page title',`,
+ ` 'maxlength' => 200,`,
+ "])",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("comma should not inserted for lastline of inline custom directive ", async () => {
+ const content = [
+ "@livewire(",
+ ` $block['path'],`,
+ " [",
+ ` 'componentSettings' => $block['properties'],`,
+ ` 'componentKey' => $block['key'],`,
+ ` 'site' => $site ?? null,`,
+ ` 'post' => $post ?? null,`,
+ ` 'theme' => $theme,`,
+ ` 'editing' => false,`,
+ ` 'preview' => $preview,`,
+ " ],",
+ " key($key)",
+ ")",
+ ].join("\n");
+
+ const expected = [
+ "@livewire(",
+ ` $block['path'],`,
+ " [",
+ ` 'componentSettings' => $block['properties'],`,
+ ` 'componentKey' => $block['key'],`,
+ ` 'site' => $site ?? null,`,
+ ` 'post' => $post ?? null,`,
+ ` 'theme' => $theme,`,
+ ` 'editing' => false,`,
+ ` 'preview' => $preview,`,
+ " ],",
+ " key($key)",
+ ")",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/escape.test.ts b/__tests__/formatter/escape.test.ts
new file mode 100644
index 00000000..33b9c16e
--- /dev/null
+++ b/__tests__/formatter/escape.test.ts
@@ -0,0 +1,39 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter escape test", () => {
+ test("escaped blade directive", async () => {
+ const content = [
+ "",
+ "",
+ `@@if("foo")`,
+ "@@endif",
+ "
",
+ "",
+ "",
+ "@@isAdmin",
+ "@@endisAdmin",
+ `@@escaped("foo")`,
+ "@@endescaped",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ` @@if("foo")`,
+ " @@endif",
+ "
",
+ "",
+ "",
+ " @@isAdmin",
+ " @@endisAdmin",
+ ` @@escaped("foo")`,
+ " @@endescaped",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/escaped-character.test.ts b/__tests__/formatter/escaped-character.test.ts
new file mode 100644
index 00000000..cd8362ee
--- /dev/null
+++ b/__tests__/formatter/escaped-character.test.ts
@@ -0,0 +1,58 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter escaped character test", () => {
+ test("special character in replacement parameter #565", async () => {
+ const content = [
+ `@section('foo')`,
+ " ",
+ "@endsection",
+ ].join("\n");
+
+ const expected = [
+ `@section('foo')`,
+ " ",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("dollar sign with nested directive #569", async () => {
+ const content = [
+ `@section('foo')`,
+ " ",
+ " @if(true)",
+ " foo",
+ " @endif",
+ "@endsection",
+ ].join("\n");
+
+ const expected = [
+ `@section('foo')`,
+ " ",
+ " @if (true)",
+ " foo",
+ " @endif",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/exception.test.ts b/__tests__/formatter/exception.test.ts
new file mode 100644
index 00000000..88d8bca2
--- /dev/null
+++ b/__tests__/formatter/exception.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, test } from "vitest";
+import { BladeFormatter } from "../../src/main.js";
+import * as util from "../support/util";
+
+describe("formatter exception test", () => {
+ test("formatter throws exception on syntax error", async () => {
+ const content = [
+ `@permission('post.edit')`,
+ `Edit Post `,
+ "@endpermission",
+ ].join("\n");
+
+ await expect(new BladeFormatter().format(content)).rejects.toThrow(
+ "SyntaxError",
+ );
+ });
+
+ test("it should not throw exception even if inline component attribute has syntax error", async () => {
+ const content = [` `].join("\n");
+ const expected = [` `, ""].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ await expect(new BladeFormatter().format(content)).resolves.not.toThrow(
+ "SyntaxError",
+ );
+ });
+
+ test("syntax error on multiline component attribute throws a syntax error", async () => {
+ const content = [
+ ` `,
+ ].join("\n");
+
+ await expect(new BladeFormatter().format(content)).rejects.toThrow(
+ "SyntaxError",
+ );
+ });
+
+ test("it should throw exception when unclosed parentheses exists", async () => {
+ const content = [
+ `@section("content"`,
+ " dummy
",
+ "@endsection",
+ ].join("\n");
+
+ await expect(new BladeFormatter().format(content)).rejects.toThrow(
+ "SyntaxError",
+ );
+ });
+});
diff --git a/__tests__/formatter/if-else.test.ts b/__tests__/formatter/if-else.test.ts
new file mode 100644
index 00000000..21d464f4
--- /dev/null
+++ b/__tests__/formatter/if-else.test.ts
@@ -0,0 +1,51 @@
+import assert from "node:assert";
+import { describe, test } from "vitest";
+import { Formatter } from "../../src/main.js";
+
+const formatter = () => {
+ return new Formatter({ indentSize: 4 });
+};
+
+describe("formatter if-else test", () => {
+ const elseEnabledDirectives = ["can", "canany", "cannot"];
+
+ for (const directive of elseEnabledDirectives) {
+ test(`else directives test - ${directive}`, async () => {
+ const content = [
+ "",
+ `@${directive}(["update",'read'],$user)`,
+ "@if ($user)",
+ "{{ $user->name }}",
+ "@endif",
+ `@else${directive}(['delete'], $user)`,
+ "foo",
+ "@else",
+ "bar",
+ `@end${directive}`,
+ " ",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @${directive}(['update', 'read'], $user)`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ ` @else${directive}(['delete'], $user)`,
+ " foo",
+ " @else",
+ " bar",
+ ` @end${directive}`,
+ " ",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+ }
+});
diff --git a/__tests__/formatter/ignore.test.ts b/__tests__/formatter/ignore.test.ts
new file mode 100644
index 00000000..96637250
--- /dev/null
+++ b/__tests__/formatter/ignore.test.ts
@@ -0,0 +1,159 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter ignore test", () => {
+ test("ignore formatting between blade-formatter-disable and blade-formatter-enable", async () => {
+ const content = [
+ "@if ($condition < 1)",
+ " {{ $user }}",
+ " {{-- blade-formatter-disable --}}",
+ " {{ $foo}}",
+ " {{-- blade-formatter-enable --}}",
+ "@elseif (!condition())",
+ " {{ $user }}",
+ "@elseif ($condition < 3)",
+ " {{ $user }}",
+ " {{-- blade-formatter-disable --}}",
+ " {{ $bar}}",
+ " {{-- blade-formatter-enable --}}",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ "@if ($condition < 1)",
+ " {{ $user }}",
+ " {{-- blade-formatter-disable --}}",
+ " {{ $foo}}",
+ " {{-- blade-formatter-enable --}}",
+ "@elseif (!condition())",
+ " {{ $user }}",
+ "@elseif ($condition < 3)",
+ " {{ $user }}",
+ " {{-- blade-formatter-disable --}}",
+ " {{ $bar}}",
+ " {{-- blade-formatter-enable --}}",
+ "@endif",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("ignore formatting after blade-formatter-disable-next-line", async () => {
+ const content = [
+ "",
+ "@if ($condition < 1)",
+ " {{-- blade-formatter-disable-next-line --}}",
+ " {{ $user }}",
+ "@elseif ($condition < 3)",
+ " {{ $user }}",
+ "@endif",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @if ($condition < 1)",
+ " {{-- blade-formatter-disable-next-line --}}",
+ " {{ $user }}",
+ " @elseif ($condition < 3)",
+ " {{ $user }}",
+ " @endif",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("ignore formatting entire file if blade-formatter-disable on a first line", async () => {
+ const content = [
+ "{{-- blade-formatter-disable --}}",
+ "",
+ "{{-- blade-formatter-disable --}}",
+ " {{ $foo}}",
+ "{{-- blade-formatter-enable --}}",
+ "@if ($condition < 1)",
+ " {{-- blade-formatter-disable-next-line --}}",
+ " {{ $user }}",
+ "@elseif ($condition < 3)",
+ " {{ $user }}",
+ "@endif",
+ "
",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "{{-- blade-formatter-disable --}}",
+ "",
+ "{{-- blade-formatter-disable --}}",
+ " {{ $foo}}",
+ "{{-- blade-formatter-enable --}}",
+ "@if ($condition < 1)",
+ " {{-- blade-formatter-disable-next-line --}}",
+ " {{ $user }}",
+ "@elseif ($condition < 3)",
+ " {{ $user }}",
+ "@endif",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("prettier ignore syntax", async () => {
+ const content = [
+ "",
+ ``,
+ "lorem ipsum dolor sit amet",
+ "
",
+ "foo",
+ "
",
+ "
",
+ "",
+ "{{-- prettier-ignore-start --}}",
+ ``,
+ "lorem ipsum dolor sit amet",
+ "
",
+ "foo",
+ "
",
+ "
",
+ "{{-- prettier-ignore-end --}}",
+ "",
+ "",
+ ` `,
+ "",
+ "{{-- prettier-ignore --}}",
+ ` `,
+ ].join("\n");
+
+ const expected = [
+ "",
+ ``,
+ "lorem ipsum dolor sit amet",
+ "
",
+ "foo",
+ "
",
+ "
",
+ "",
+ "{{-- prettier-ignore-start --}}",
+ ``,
+ "lorem ipsum dolor sit amet",
+ "
",
+ "foo",
+ "
",
+ "
",
+ "{{-- prettier-ignore-end --}}",
+ "",
+ "",
+ ` `,
+ "",
+ "{{-- prettier-ignore --}}",
+ ` `,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/indent.test.ts b/__tests__/formatter/indent.test.ts
new file mode 100644
index 00000000..82946922
--- /dev/null
+++ b/__tests__/formatter/indent.test.ts
@@ -0,0 +1,69 @@
+import assert from "node:assert";
+import { describe, test } from "vitest";
+import { Formatter } from "../../src/main.js";
+
+const formatter = () => {
+ return new Formatter({ indentSize: 4 });
+};
+
+describe("indent formatter test", () => {
+ test("basic blade directive indent", () => {
+ const content = [
+ "",
+ "",
+ "@if($user)",
+ "{{ $user->name }}",
+ "@endif",
+ "
",
+ " ",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " ",
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ "
",
+ " ",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("nested directive indent", () => {
+ const content = [
+ "",
+ "@foreach($users as $user)",
+ "@if($user)",
+ "{{ $user->name }}",
+ "@endif",
+ "@endforeach",
+ " ",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @foreach ($users as $user)",
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ " @endforeach",
+ " ",
+ "",
+ ].join("\n");
+
+ return formatter()
+ .formatContent(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+});
diff --git a/__tests__/formatter/inlined.test.ts b/__tests__/formatter/inlined.test.ts
new file mode 100644
index 00000000..6d3d870f
--- /dev/null
+++ b/__tests__/formatter/inlined.test.ts
@@ -0,0 +1,124 @@
+import assert from "node:assert";
+import fs from "node:fs";
+import path from "node:path";
+import { describe, expect, test } from "vitest";
+import { BladeFormatter } from "../../src/main.js";
+import * as cmd from "../support/cmd";
+import * as util from "../support/util";
+
+describe("formatter inlined test", () => {
+ test("directive in html attribute should not occurs error", async () => {
+ const content = [
+ "@if (count($topics))",
+ ` `,
+ " @foreach ($topics as $topic)",
+ ` `,
+ " @endforeach",
+ " ",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ "@if (count($topics))",
+ ` `,
+ " @foreach ($topics as $topic)",
+ " `,
+ " ",
+ " @endforeach",
+ " ",
+ "@endif",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should consider directive in html tag", async () => {
+ const cmdResult = await cmd.execute(
+ path.resolve("bin", "blade-formatter.js"),
+ [path.resolve("__tests__", "fixtures", "inline_php_tag.blade.php")],
+ );
+
+ const formatted = fs.readFileSync(
+ path.resolve(
+ "__tests__",
+ "fixtures",
+ "formatted_inline_php_tag.blade.php",
+ ),
+ );
+
+ expect(cmdResult).toEqual(formatted.toString("utf-8"));
+ });
+
+ test("should not occurs error on inline if to end directive on long line", async () => {
+ const content = [
+ "",
+ `@if (count($users) && $users->has('friends')) {{ $user->name }} @endif`,
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ ` @if (count($users) && $users->has('friends'))`,
+ " {{ $user->name }}",
+ " @endif",
+ "
",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("raw php inlined comment #493", async () => {
+ const content = [
+ "",
+ "",
+ `@foreach ($preview['new'] as $game)`,
+ ` `,
+ "@endforeach",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ `@foreach ($preview['new'] as $game)`,
+ ` `,
+ "@endforeach",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("inline @json directive", async () => {
+ const content = [
+ `@section('footer')`,
+ " ",
+ "@endsection",
+ ].join("\n");
+
+ const expected = [
+ `@section('footer')`,
+ " ",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/line-break.test.ts b/__tests__/formatter/line-break.test.ts
new file mode 100644
index 00000000..753aa05a
--- /dev/null
+++ b/__tests__/formatter/line-break.test.ts
@@ -0,0 +1,408 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter line break test", () => {
+ test("it should line break before and after directives", async () => {
+ const content = [
+ "",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1) foo",
+ " @endif",
+ "
",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ "
",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1) foo",
+ " @endif",
+ "
",
+ "
",
+ " @foreach ($collection as $item)",
+ " {{ $item }} @endforeach",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ "",
+ " @endif",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1) foo",
+ "",
+ " @endif",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " @endif",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ "
",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ "
",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ "
",
+ "
",
+ " @foreach ($collection as $item)",
+ " {{ $item }}",
+ " @endforeach",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " @endif",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("complex line break", async () => {
+ const content = [
+ "",
+ "@if ($user) @if ($condition) aaa @endif",
+ "@endif",
+ ` @can('edit') bbb`,
+ " @endcan",
+ `@auth('user') ccc`,
+ "@endauth",
+ "
",
+ "",
+ `@section('title') aaa @endsection`,
+ "
",
+ `@foreach($users as $user) @foreach($shops as $shop) {{ $user["id"] . $shop["id"] }} @endforeach @endforeach
`,
+ `@if($users) @foreach($shops as $shop) {{ $user["id"] . $shop["id"] }} @endforeach @endif
`,
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @if ($user)",
+ " @if ($condition)",
+ " aaa",
+ " @endif",
+ " @endif",
+ ` @can('edit')`,
+ " bbb",
+ " @endcan",
+ ` @auth('user')`,
+ " ccc",
+ " @endauth",
+ "
",
+ "",
+ ` @section('title')`,
+ " aaa",
+ " @endsection",
+ "
",
+ "",
+ " @foreach ($users as $user)",
+ " @foreach ($shops as $shop)",
+ ` {{ $user['id'] . $shop['id'] }}`,
+ " @endforeach",
+ " @endforeach",
+ "
",
+ "",
+ " @if ($users)",
+ " @foreach ($shops as $shop)",
+ ` {{ $user['id'] . $shop['id'] }}`,
+ " @endforeach",
+ " @endif",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("line break around @case, @break and @default", async () => {
+ const content = [
+ "@switch($type) @case(1) $a = 3; @break @case(2) @case(3) $a = 4; @break @default $a = null; @endswitch",
+ "",
+ "@switch($type) @case(1) $a = 3; @break @case(2) @case(3) $a = 4; @break @default $a = null; @endswitch",
+ "
",
+ "@switch($type) @case(1) $a = 3; @break @case(2) @case(3) $a = 4; @break @default $a = null; @endswitch",
+ `@section('aaa')`,
+ "@switch($type)",
+ "@case(1)",
+ "$a = 3;",
+ "@break",
+ "",
+ "@case(2)",
+ "@case(3)",
+ "$a = 4;",
+ "@break",
+ "",
+ "@default",
+ "$a = null;",
+ "@endswitch",
+ "@endsection",
+ "",
+ "@switch($i)",
+ " @case(1)",
+ " @switch($j)",
+ " @case(1)",
+ " First case...",
+ " @break",
+ " @case(2)",
+ " Second case...",
+ " @break",
+ " @default",
+ " Default case...",
+ " @endswitch",
+ " @break",
+ " @case(2)",
+ " hogehoge...",
+ " @break",
+ "@endswitch",
+ "
",
+ "@switch($type)",
+ " @case(1)",
+ " $a = 3;",
+ " @break",
+ "",
+ " @case(2)",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ "@case(3)",
+ " $a = 4;",
+ "@break",
+ "",
+ "@default",
+ " $a = null;",
+ "@endswitch",
+ ].join("\n");
+
+ const expected = [
+ "@switch($type)",
+ " @case(1)",
+ " $a = 3;",
+ " @break",
+ "",
+ " @case(2)",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ " @default",
+ " $a = null;",
+ "@endswitch",
+ "",
+ " @switch($type)",
+ " @case(1)",
+ " $a = 3;",
+ " @break",
+ "",
+ " @case(2)",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ " @default",
+ " $a = null;",
+ " @endswitch",
+ "
",
+ "@switch($type)",
+ " @case(1)",
+ " $a = 3;",
+ " @break",
+ "",
+ " @case(2)",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ " @default",
+ " $a = null;",
+ "@endswitch",
+ `@section('aaa')`,
+ " @switch($type)",
+ " @case(1)",
+ " $a = 3;",
+ " @break",
+ "",
+ " @case(2)",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ " @default",
+ " $a = null;",
+ " @endswitch",
+ "@endsection",
+ "",
+ " @switch($i)",
+ " @case(1)",
+ " @switch($j)",
+ " @case(1)",
+ " First case...",
+ " @break",
+ "",
+ " @case(2)",
+ " Second case...",
+ " @break",
+ "",
+ " @default",
+ " Default case...",
+ " @endswitch",
+ " @break",
+ "",
+ " @case(2)",
+ " hogehoge...",
+ " @break",
+ "",
+ " @endswitch",
+ "
",
+ "@switch($type)",
+ " @case(1)",
+ " $a = 3;",
+ " @break",
+ "",
+ " @case(2)",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ " @case(3)",
+ " $a = 4;",
+ " @break",
+ "",
+ " @default",
+ " $a = null;",
+ "@endswitch",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("else token auto line breaking", async () => {
+ const content = [
+ "@if (count($users) === 1)",
+ " Foo",
+ "@elseif (count($users) > 1)Bar",
+ "@elseif (count($users) > 2)Bar2",
+ "@else Baz@endif",
+ `@can('update') foo @elsecan('read') bar @endcan`,
+ ].join("\n");
+
+ const expected = [
+ "@if (count($users) === 1)",
+ " Foo",
+ "@elseif (count($users) > 1)",
+ " Bar",
+ "@elseif (count($users) > 2)",
+ " Bar2",
+ "@else",
+ " Baz",
+ "@endif",
+ `@can('update')`,
+ " foo",
+ `@elsecan('read')`,
+ " bar",
+ "@endcan",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("line breaking with html tag", async () => {
+ const content = [
+ "",
+ `
@can('auth')`,
+ `foo @elsecan('aaa') bar @endcan
`,
+ "
@foreach($users as $user)",
+ "{{$user}} bar @endforeach
",
+ `@if($user)`,
+ "{!!$user!!} @elseif ($authorized) foo @else bar @endif
",
+ ` `,
+ "@for ($i = 0; $i < 5; $i++)",
+ "aaa",
+ "@endfor
",
+ "@if($user)",
+ "{!!$user!!} @elseif ($authorized) foo @else bar @endif",
+ "",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "
",
+ ` @can('auth')`,
+ " foo",
+ ` @elsecan('aaa')`,
+ " bar",
+ " @endcan",
+ "
",
+ "
",
+ " @foreach ($users as $user)",
+ " {{ $user }} bar",
+ " @endforeach",
+ "
",
+ "
",
+ ``,
+ " @if ($user)",
+ " {!! $user !!}",
+ " @elseif ($authorized)",
+ " foo",
+ " @else",
+ " bar",
+ " @endif",
+ "
",
+ ` `,
+ "",
+ " @for ($i = 0; $i < 5; $i++)",
+ " aaa",
+ " @endfor",
+ "
",
+ "",
+ " @if ($user)",
+ " {!! $user !!}",
+ " @elseif ($authorized)",
+ " foo",
+ " @else",
+ " bar",
+ " @endif",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("preserve line break of multi-line comment", async () => {
+ const content = [
+ "{{-- ",
+ "foo",
+ "--}}",
+ "",
+ "bar",
+ "",
+ "{{--",
+ "baz",
+ "--}}",
+ ].join("\n");
+
+ const expected = [
+ "{{-- ",
+ "foo",
+ "--}}",
+ "",
+ "bar",
+ "",
+ "{{--",
+ "baz",
+ "--}}",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/livewire.test.ts b/__tests__/formatter/livewire.test.ts
new file mode 100644
index 00000000..55eff74c
--- /dev/null
+++ b/__tests__/formatter/livewire.test.ts
@@ -0,0 +1,29 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter escaped character test", () => {
+ test("livewire tag", async () => {
+ const content = [
+ ``,
+ " @foreach ($this->relations as $k => $relation )",
+ `
`,
+ ` `,
+ "
",
+ " @endforeach",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " @foreach ($this->relations as $k => $relation)",
+ `
`,
+ ` `,
+ "
",
+ " @endforeach",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/mixed.test.ts b/__tests__/formatter/mixed.test.ts
new file mode 100644
index 00000000..d494c025
--- /dev/null
+++ b/__tests__/formatter/mixed.test.ts
@@ -0,0 +1,41 @@
+import assert from "node:assert";
+import { describe, test } from "vitest";
+import { BladeFormatter } from "../../src/main.js";
+
+describe("formatter mixed content test", () => {
+ test("mixed html tag and directive #5", async () => {
+ const content = [
+ `@extends('dashboard')`,
+ "",
+ `@section('content')`,
+ "@if( $member->isAdmin() )",
+ ``,
+ "@endif",
+ "Test! ",
+ "@if( $member->isAdmin() )",
+ "
",
+ "@endif",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ const expected = [
+ `@extends('dashboard')`,
+ "",
+ `@section('content')`,
+ " @if ($member->isAdmin())",
+ ` `,
+ " @endif",
+ " Test! ",
+ " @if ($member->isAdmin())",
+ "
",
+ " @endif",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+});
diff --git a/__tests__/formatter/nested-parenthesis.test.ts b/__tests__/formatter/nested-parenthesis.test.ts
new file mode 100644
index 00000000..4ec0f8b9
--- /dev/null
+++ b/__tests__/formatter/nested-parenthesis.test.ts
@@ -0,0 +1,74 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter nested parenthesis test", () => {
+ test("inline nested parenthesis #350", async () => {
+ const content = [
+ "@if ($user)",
+ " ",
+ ` {{ asset(auth()->user()->getUserMedia('first', 'second')) }}`,
+ ` {{ asset4(asset1(asset2(asset3(auth()->user($aaaa['bbb'])->aaa("aaa"))))) }}`,
+ ` {{ asset(auth()->user($aaaa["bbb"])->aaa('aaa')) }}`,
+ " {{ $user }}",
+ ` {{ auth()->user( ["bar","ccc"])->foo("aaa") }}`,
+ ` {{ asset(auth()->user(['bar', 'ccc'])->tooooooooooooooooooooooooooooooooooolongmethod('aaa')->chained()->tooooooooooooooooooooooooooo()->long()) }}`,
+ "
",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ "@if ($user)",
+ " ",
+ ` {{ asset(auth()->user()->getUserMedia('first', 'second')) }}`,
+ ` {{ asset4(asset1(asset2(asset3(auth()->user($aaaa['bbb'])->aaa('aaa'))))) }}`,
+ ` {{ asset(auth()->user($aaaa['bbb'])->aaa('aaa')) }}`,
+ " {{ $user }}",
+ ` {{ auth()->user(['bar', 'ccc'])->foo('aaa') }}`,
+ ` {{ asset(auth()->user(['bar', 'ccc'])->tooooooooooooooooooooooooooooooooooolongmethod('aaa')->chained()->tooooooooooooooooooooooooooo()->long()) }}`,
+ "
",
+ "@endif",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("3 more level nested parenthesis #340", async () => {
+ const content = [
+ "",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ " @if (count($foo->bar(Auth::user($baz->method()), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ " @foreach (Auth::users($my->users($as->foo)) as $user)",
+ " foo",
+ " @endif",
+ " @isset($user->foo($user->bar($user->baz())))",
+ " foo",
+ " @endisset",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @if (count($foo->bar(Auth::user(), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ " @if (count($foo->bar(Auth::user($baz->method()), Request::path())) >= 1)",
+ " foo",
+ " @endif",
+ " @foreach (Auth::users($my->users($as->foo)) as $user)",
+ " foo",
+ " @endif",
+ " @isset($user->foo($user->bar($user->baz())))",
+ " foo",
+ " @endisset",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/nested.test.ts b/__tests__/formatter/nested.test.ts
new file mode 100644
index 00000000..c9f1b035
--- /dev/null
+++ b/__tests__/formatter/nested.test.ts
@@ -0,0 +1,111 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter nested test", () => {
+ test("nested unless condition", async () => {
+ const content = [
+ ``,
+ " ",
+ ` @unless(isset($primaryTicketingLinkData) && $primaryTicketingLinkData['isSoldOut'] && $ticketCount <= 0)`,
+ ` @include('events.partials.wanted-tickets-button')`,
+ " @endunless",
+ " ",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " ",
+ ` @unless (isset($primaryTicketingLinkData) && $primaryTicketingLinkData['isSoldOut'] && $ticketCount <= 0)`,
+ ` @include('events.partials.wanted-tickets-button')`,
+ " @endunless",
+ " ",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ wrapAttributes: "force-expand-multiline",
+ });
+ });
+
+ test("nested @forelse https://github.com/shufo/vscode-blade-formatter/issues/425", async () => {
+ const content = [
+ "@forelse($users as $user)",
+ "@if ($user)",
+ "foo",
+ "@forelse($users as $user)",
+ " foo",
+ " @empty",
+ " bar",
+ " @endforelse",
+ " @endif",
+ "baz",
+ "@empty",
+ "something goes here",
+ "@endforelse",
+ ].join("\n");
+
+ const expected = [
+ "@forelse($users as $user)",
+ " @if ($user)",
+ " foo",
+ " @forelse($users as $user)",
+ " foo",
+ " @empty",
+ " bar",
+ " @endforelse",
+ " @endif",
+ " baz",
+ "@empty",
+ " something goes here",
+ "@endforelse",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("nested condition", async () => {
+ const content = [
+ "@if (",
+ ` count( auth(" ( ) ")->user() ->currentXY->shopsXY()`,
+ ") > 1)",
+ ` Test `,
+ "@else",
+ ` Test `,
+ "@endif",
+ `@if (foo(count( auth(" ( ) ")->user() ->currentXY->shopsXY()) > 1))`,
+ ` Test `,
+ "@else",
+ ` Test `,
+ "@endif",
+ "@if (count(auth()->user()->currentXY->shopsXY()) > 1)",
+ ` Test `,
+ "@else",
+ ` Test `,
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ `@if (count(auth(' ( ) ')->user()->currentXY->shopsXY()) > 1)`,
+ ` Test `,
+ "@else",
+ ` Test `,
+ "@endif",
+ `@if (foo(count(auth(' ( ) ')->user()->currentXY->shopsXY()) > 1))`,
+ ` Test `,
+ "@else",
+ ` Test `,
+ "@endif",
+ "@if (count(auth()->user()->currentXY->shopsXY()) > 1)",
+ ` Test `,
+ "@else",
+ ` Test `,
+ "@endif",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/options.test.ts b/__tests__/formatter/options.test.ts
new file mode 100644
index 00000000..586e998c
--- /dev/null
+++ b/__tests__/formatter/options.test.ts
@@ -0,0 +1,470 @@
+import assert from "node:assert";
+import { describe, expect, test } from "vitest";
+import { BladeFormatter } from "../../src/main.js";
+import * as util from "../support/util";
+
+describe("formatter options test", () => {
+ test("force expand multilines", async () => {
+ const content = [
+ '',
+ "@if (Auth::check())",
+ "@php($user = Auth::user())",
+ "{{ $user->name }}",
+ "@endif",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @if (Auth::check())",
+ " @php($user = Auth::user())",
+ " {{ $user->name }}",
+ " @endif",
+ "
",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter({ wrapAttributes: "force-expand-multiline" })
+ .format(content)
+ .then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("textarea wrapping https://github.com/shufo/vscode-blade-formatter/issues/414", async () => {
+ const content = [
+ ``,
+ ` ",
+ "",
+ ].join("\n");
+
+ const alignedMultipleExpected = [
+ ``,
+ ` `,
+ " ...",
+ " ",
+ " ",
+ ` `,
+ "
",
+ " ",
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, alignedMultipleExpected, {
+ wrapAttributes: "aligned-multiple",
+ });
+
+ const forceAlignedExpected = [
+ ``,
+ ` `,
+ " ...",
+ " ",
+ " ",
+ ` `,
+ "
",
+ " ",
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, forceAlignedExpected, {
+ wrapAttributes: "force-aligned",
+ });
+ });
+
+ test("no multiple empty lines formatter option", async () => {
+ // prettier-ignore
+ const content = ["foo", "", "", "bar", "", "", "", "baz"].join("\n");
+
+ // prettier-ignore
+ const expected = ["foo", "", "bar", "", "baz", ""].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ noMultipleEmptyLines: true,
+ });
+ });
+
+ test("disable no multiple empty lines formatter option", async () => {
+ // prettier-ignore
+ const content = ["foo", "", "", "bar", "", "", "", "baz"].join("\n");
+
+ // prettier-ignore
+ const expected = ["foo", "", "", "bar", "", "", "", "baz", ""].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ noMultipleEmptyLines: false,
+ });
+ });
+
+ test("it should use tabs inside script tag if useTabs option passed", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, { useTabs: true });
+ });
+
+ test("it should order html attributes if --sort-html-attributes option passed", async () => {
+ const content = [
+ ``,
+ "foo",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " foo",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ sortHtmlAttributes: "idiomatic",
+ });
+ });
+
+ test("it should use tab for indent inside inline directive", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ useTabs: true,
+ indentSize: 1,
+ });
+
+ const expected2 = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected2, {
+ useTabs: true,
+ indentSize: 2,
+ });
+ });
+
+ test("no php syntax check option", async () => {
+ const content = [
+ `{{ 'john' |ucfirst | substr:0,1 }}`,
+ "@if (foo)",
+ "foo",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ `{{ 'john' |ucfirst | substr:0,1 }}`,
+ "@if (foo)",
+ " foo",
+ "@endif",
+ "",
+ ].join("\n");
+
+ const options = { noPhpSyntaxCheck: true };
+ await util.doubleFormatCheck(content, expected, options);
+ await expect(
+ new BladeFormatter(options).format(content),
+ ).resolves.not.toThrow("SyntaxError");
+ });
+
+ test("no php syntax check option with multi-lined inline directive", async () => {
+ const content = [
+ `@include('components.artwork_grid_item', [`,
+ ` 'item' => $item,`,
+ ` 'isotope_item_selector_class' => 'item',`,
+ ` 'class_names' => 'col-xs-6 px-5',`,
+ ` 'hide_dating' => true`,
+ ` 'hide_zoom' => true,`,
+ "])",
+ ].join("\n");
+
+ const expected = [
+ `@include('components.artwork_grid_item', [`,
+ ` 'item' => $item,`,
+ ` 'isotope_item_selector_class' => 'item',`,
+ ` 'class_names' => 'col-xs-6 px-5',`,
+ ` 'hide_dating' => true`,
+ ` 'hide_zoom' => true,`,
+ "])",
+ "",
+ ].join("\n");
+
+ const options = { noPhpSyntaxCheck: true };
+ await util.doubleFormatCheck(content, expected, options);
+ });
+
+ test("customs html attributes order option", async () => {
+ const content = [
+ ``,
+ "foo",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " foo",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ sortHtmlAttributes: "custom",
+ customHtmlAttributesOrder: ["id", "aria-.+", "src", "class"],
+ });
+ });
+
+ test("--end-of-line option", async () => {
+ const content = [
+ ``,
+ "foo",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ ``,
+ " foo",
+ "
",
+ "",
+ ].join("\r\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ endOfLine: "CRLF",
+ });
+ });
+
+ test("wrapAttributesMinAttrs option", async () => {
+ const content = [
+ ``,
+ "foo",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " foo",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ wrapAttributesMinAttrs: 0,
+ wrapAttributes: "force-expand-multiline",
+ });
+ });
+
+ test("script tag with wrapAttributesMinAttrs option", async () => {
+ const content = [
+ `@push('scripts')`,
+ " ",
+ "@endpush",
+ ].join("\n");
+
+ const expected = [
+ `@push('scripts')`,
+ " ",
+ "@endpush",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ wrapAttributesMinAttrs: 0,
+ wrapAttributes: "force-expand-multiline",
+ });
+ });
+
+ test("nonnative script tag with wrapAttributesMinAttrs option", async () => {
+ const content = [
+ `@push('scripts')`,
+ ` ",
+ "@endpush",
+ ].join("\n");
+
+ const expected = [
+ `@push('scripts')`,
+ ` ",
+ "@endpush",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ wrapAttributesMinAttrs: 0,
+ wrapAttributes: "force-expand-multiline",
+ });
+ });
+
+ test("content sensitive html tag with wrapAttributesMinAttrs option", async () => {
+ const content = [
+ "",
+ "foo",
+ " ",
+ "",
+ "bar",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "foo",
+ " ",
+ "",
+ "bar",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ wrapAttributesMinAttrs: 0,
+ wrapAttributes: "force-expand-multiline",
+ });
+ });
+
+ test("extra liners option", async () => {
+ const content = [
+ "",
+ "",
+ `@section('header')`,
+ "",
+ "foo",
+ " ",
+ "@endsection",
+ "",
+ "",
+ ``,
+ "Click Here",
+ " ",
+ "",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ` @section('header')`,
+ " ",
+ " foo",
+ " ",
+ " @endsection",
+ "",
+ "",
+ ` `,
+ " Click Here",
+ " ",
+ "",
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ extraLiners: [],
+ });
+ });
+});
diff --git a/__tests__/formatter/php.test.ts b/__tests__/formatter/php.test.ts
new file mode 100644
index 00000000..651cb3a6
--- /dev/null
+++ b/__tests__/formatter/php.test.ts
@@ -0,0 +1,551 @@
+import assert from "node:assert";
+import { describe, test } from "vitest";
+import { BladeFormatter } from "../../src/main.js";
+import * as util from "../support/util";
+
+describe("formatter php test", () => {
+ const predefinedConstants = [
+ "PHP_VERSION",
+ "PHP_RELEASE_VERSION",
+ "PHP_VERSION_ID",
+ "PHP_OS_FAMILY",
+ "PHP_FLOAT_DIG",
+ ];
+
+ for (const constant of predefinedConstants) {
+ test("should format php predefined constants", async () => {
+ const content = [`{{ ${constant} }}`].join("\n");
+ const expected = [`{{ ${constant} }}`, ""].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+ }
+
+ test("should format null safe operator", async () => {
+ const content = ["{{ $entity->executors->first()?->name() }}"].join("\n");
+
+ const expected = ["{{ $entity->executors->first()?->name() }}", ""].join(
+ "\n",
+ );
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should format named arguments", async () => {
+ const content = ["{{ foo(double_encode: true) }}"].join("\n");
+
+ const expected = ["{{ foo(double_encode: true) }}", ""].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should break chained method in directive", async () => {
+ const content = [
+ "@if (auth()",
+ "->user()",
+ "->subscribed('default'))",
+ "aaa",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ "@if (auth()->user()->subscribed('default'))",
+ " aaa",
+ "@endif",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should break chained method in directive 2", async () => {
+ const content = [
+ "@foreach (request()->users() as $user)",
+ "aaa",
+ "@endif",
+ ].join("\n");
+
+ const expected = [
+ "@foreach (request()->users() as $user)",
+ " aaa",
+ "@endif",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("string literal with line break in raw php directive", async () => {
+ const content = [
+ "",
+ "
",
+ " @php",
+ ` $myvar = "lorem`,
+ ` ipsum";`,
+ ` $foo = "lorem`,
+ "",
+ "multiline",
+ ` ipsum";`,
+ " @endphp",
+ "
",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "
",
+ " @php",
+ ` $myvar = "lorem`,
+ ` ipsum";`,
+ ` $foo = "lorem`,
+ "",
+ "multiline",
+ ` ipsum";`,
+ " @endphp",
+ "
",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("escaped quote in raw php directive #669", async () => {
+ const content = [
+ " @php",
+ " if ($condition1) {",
+ ` $var1 = '...';`,
+ ` $var2 = '...';`,
+ " } elseif ($condition2) {",
+ ` $var1 = '...';`,
+ ` $var2 = 'I have a \\' in me';`,
+ " } else {",
+ ` $var1 = '...';`,
+ ` $var2 = '...';`,
+ " }",
+ " @endphp",
+ ].join("\n");
+
+ const expected = [
+ " @php",
+ " if ($condition1) {",
+ ` $var1 = '...';`,
+ ` $var2 = '...';`,
+ " } elseif ($condition2) {",
+ ` $var1 = '...';`,
+ ` $var2 = 'I have a \\' in me';`,
+ " } else {",
+ ` $var1 = '...';`,
+ ` $var2 = '...';`,
+ " }",
+ " @endphp",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("html tag in raw php block", async () => {
+ const content = [
+ "@php",
+ `$icon = " ";`,
+ `$icon = " ";`,
+ `$icon = ' ';`,
+ "@endphp",
+ ].join("\n");
+
+ const expected = [
+ "@php",
+ ` $icon = " ";`,
+ ` $icon = " ";`,
+ ` $icon = ' ';`,
+ "@endphp",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("indent inside @php directive", async () => {
+ const content = [
+ "@php",
+ "$a = 1;",
+ "$b = 2;",
+ "@endphp",
+ "",
+ "@php",
+ "$a = 1;",
+ "$b = 2;",
+ "@endphp",
+ "@php",
+ `$icon = " ";`,
+ `$icon = " ";`,
+ `$icon = ' ';`,
+ "@endphp",
+ "
",
+ "",
+ ].join("\n");
+
+ const expected = [
+ "@php",
+ " $a = 1;",
+ " $b = 2;",
+ "@endphp",
+ "",
+ " @php",
+ " $a = 1;",
+ " $b = 2;",
+ " @endphp",
+ " @php",
+ ` $icon = " ";`,
+ ` $icon = " ";`,
+ ` $icon = ' ';`,
+ " @endphp",
+ "
",
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("multi-line comment in raw php tag", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("fix shufo/prettier-plugin-blade#166", async () => {
+ const content = [
+ "@php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ "@endphp",
+ "{{ $post->title }} by {{ $user->name }} ",
+ ].join("\n");
+
+ const expected = [
+ "@php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ "@endphp",
+ "{{ $post->title }} by {{ $user->name }} ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("raw php comment block", async () => {
+ const content = [
+ "",
+ " ",
+ " ",
+ " ",
+ " ",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " ",
+ " ",
+ " ",
+ " ",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("php directive comment block", async () => {
+ const content = [
+ "",
+ " @php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ " @endphp",
+ " @php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */ echo 1;",
+ " @endphp",
+ " @php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ " @endphp",
+ " @php",
+ " /**",
+ " AppModelsUser $user",
+ " AppModelsPost $post",
+ " */",
+ " @endphp",
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "",
+ " @php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ " @endphp",
+ " @php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */ echo 1;",
+ " @endphp",
+ " @php",
+ " /**",
+ " * @var AppModelsUser $user",
+ " * @var AppModelsPost $post",
+ " */",
+ " @endphp",
+ " @php",
+ " /**",
+ " AppModelsUser $user",
+ " AppModelsPost $post",
+ " */",
+ " @endphp",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@php blocks should wrap long statements (#908)", async () => {
+ const content = [
+ "@php",
+ `$categories = App\\Models\\Category::whereIn('id', $catids)`,
+ `->orderBy('description')`,
+ `->orderBy('description')`,
+ `->orderBy('description')`,
+ "->get();",
+ "@endphp",
+ ].join("\n");
+
+ const expected = [
+ "@php",
+ ` $categories = App\\Models\\Category::whereIn('id', $catids)`,
+ ` ->orderBy('description')`,
+ ` ->orderBy('description')`,
+ ` ->orderBy('description')`,
+ " ->get();",
+ "@endphp",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@php blocks should attempt to respect to current indent level", async () => {
+ // This statement is 119 chars long and should fit on 1 line w/ the default print width of 120.
+ // Once it's indented within the @php block, though, the line will exceed 120, so it should have
+ // been indented.
+ let content = [
+ "@php",
+ `$categories = App\\Models\\Category::whereIn('idss', $catids)`,
+ `->orderBy('description')`,
+ `->orderBy('description')`,
+ "->getNone();",
+ "@endphp",
+ ].join("\n");
+
+ let expected = [
+ "@php",
+ ` $categories = App\\Models\\Category::whereIn('idss', $catids)`,
+ ` ->orderBy('description')`,
+ ` ->orderBy('description')`,
+ " ->getNone();",
+ "@endphp",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+
+ // Now wrap the @php in a and make the PHP statement 4 chars shorter to confirm that it
+ // still works at higher indent levels.
+ content = [
+ "
",
+ "@php",
+ `$categories = App\\Models\\Category::whereIn('idss', $catids)`,
+ `->orderBy('description')`,
+ `->orderBy('description')`,
+ "->get();",
+ "@endphp",
+ "
",
+ ].join("\n");
+
+ expected = [
+ "
",
+ " @php",
+ ` $categories = App\\Models\\Category::whereIn('idss', $catids)`,
+ ` ->orderBy('description')`,
+ ` ->orderBy('description')`,
+ " ->get();",
+ " @endphp",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("@php blocks respect indent level for deeply indented code (issue #915)", async () => {
+ const content = [
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "@php",
+ "$percent = $item['historical'] ?? null ? round((100 * ($item['today'] - $item['historical'])) / $item['historical']) : null;",
+ "",
+ "$color = $percent < 0 ? '#8b0000' : '#006400';",
+ "@endphp",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ ].join("\n");
+
+ // this is slightly different than the code presented in #915 because that
+ // code was wrapped to 80 columns, but these tests all use 120
+ const expected = [
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ " @php",
+ " $percent =",
+ " $item['historical'] ?? null",
+ " ? round((100 * ($item['today'] - $item['historical'])) / $item['historical'])",
+ " : null;",
+ "",
+ " $color = $percent < 0 ? '#8b0000' : '#006400';",
+ " @endphp",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/props.test.ts b/__tests__/formatter/props.test.ts
new file mode 100644
index 00000000..1ac1b86e
--- /dev/null
+++ b/__tests__/formatter/props.test.ts
@@ -0,0 +1,56 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter props test", () => {
+ test("long props", async () => {
+ const content = [
+ `@props(['name', 'title' => 'Please Confirm', 'message' => 'Are you sure?', 'level' => 'info', 'icon' => 'heroicon-o-question-mark-circle', 'cancelButtonText' => 'No', 'cancelButtonType' => 'muted', 'affirmButtonText' => 'Yes', 'affirmButtonType' => 'success', 'affirmButtonDisabled' => false])`,
+ ].join("\n");
+
+ const expected = [
+ "@props([",
+ ` 'name',`,
+ ` 'title' => 'Please Confirm',`,
+ ` 'message' => 'Are you sure?',`,
+ ` 'level' => 'info',`,
+ ` 'icon' => 'heroicon-o-question-mark-circle',`,
+ ` 'cancelButtonText' => 'No',`,
+ ` 'cancelButtonType' => 'muted',`,
+ ` 'affirmButtonText' => 'Yes',`,
+ ` 'affirmButtonType' => 'success',`,
+ ` 'affirmButtonDisabled' => false,`,
+ "])",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("nested long props", async () => {
+ const content = [
+ "
",
+ `@props(['name', 'title' => 'Please Confirm', 'message' => 'Are you sure?', 'level' => 'info', 'icon' => 'heroicon-o-question-mark-circle', 'cancelButtonText' => 'No', 'cancelButtonType' => 'muted', 'affirmButtonText' => 'Yes', 'affirmButtonType' => 'success', 'affirmButtonDisabled' => false])`,
+ "
",
+ ].join("\n");
+
+ const expected = [
+ "
",
+ " @props([",
+ ` 'name',`,
+ ` 'title' => 'Please Confirm',`,
+ ` 'message' => 'Are you sure?',`,
+ ` 'level' => 'info',`,
+ ` 'icon' => 'heroicon-o-question-mark-circle',`,
+ ` 'cancelButtonText' => 'No',`,
+ ` 'cancelButtonType' => 'muted',`,
+ ` 'affirmButtonText' => 'Yes',`,
+ ` 'affirmButtonType' => 'success',`,
+ ` 'affirmButtonDisabled' => false,`,
+ " ])",
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/scripts.test.ts b/__tests__/formatter/scripts.test.ts
new file mode 100644
index 00000000..f4a1ff88
--- /dev/null
+++ b/__tests__/formatter/scripts.test.ts
@@ -0,0 +1,556 @@
+import assert from "node:assert";
+import { describe, expect, test } from "vitest";
+import { BladeFormatter } from "../../src/main.js";
+import * as util from "../support/util";
+
+describe("formatter scripts test", () => {
+ test("should format blade directive in scripts", async () => {
+ const content = [
+ " ",
+ ].join("\n");
+
+ const expected = [
+ " ",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should format multiple blade directive in script tag", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should format inline directive in scripts #231", async () => {
+ const content = [
+ ``,
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("should format inline function directives in scripts", async () => {
+ const content = [
+ `",
+ ].join("\n");
+
+ const expected = [
+ `",
+ "",
+ ].join("\n");
+
+ return new BladeFormatter().format(content).then((result: any) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("@forelse-@empty-@endforelse directive in scripts", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("inline script tag should keep its element #508", async () => {
+ const content = [
+ `
foo
bar
blah
`,
+ ].join("\n");
+
+ const expected = [
+ "
foo
",
+ "
bar
",
+ "",
+ "
blah
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("inline @php directive in script tag", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("nested directive in script tag", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("custom directive in script tag", async () => {
+ const content = [
+ `",
+ ].join("\n");
+
+ const expected = [
+ `",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("arrow identifier in tag", async () => {
+ const content = [
+ `",
+ ].join("\n");
+ const expected = [
+ `",
+ "",
+ ].join("\n");
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("script tag indentation with multiline attribute", async () => {
+ const content = [
+ "",
+ ].join("\n");
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ wrapAttributes: "force-expand-multiline",
+ });
+ });
+
+ test("script tag type with not js code", async () => {
+ const content = [
+ `@section('section')`,
+ ` ",
+ ` ",
+ ` ",
+ "@endsection",
+ ].join("\n");
+
+ const expected = [
+ `@section('section')`,
+ ` ",
+ ` ",
+ ` ",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("script tag with module type", async () => {
+ const content = [
+ `@push('scripts')`,
+ ` ",
+ "@endpush",
+ ].join("\n");
+
+ const expected = [
+ `@push('scripts')`,
+ ` ",
+ "@endpush",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("elseif statement in script tag", async () => {
+ const content = [
+ "",
+ ].join("\n");
+
+ const expected = [
+ "",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("it should not throws error if non-native script type ontains directive", async () => {
+ const content = [
+ `",
+ `",
+ `",
+ `",
+ `",
+ ].join("\n");
+
+ const expected = [
+ `",
+ `",
+ `",
+ `",
+ `",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ await expect(new BladeFormatter().format(content)).resolves.not.toThrow(
+ "Can't format blade",
+ );
+ });
+});
diff --git a/__tests__/formatter/slot.test.ts b/__tests__/formatter/slot.test.ts
new file mode 100644
index 00000000..0ccd3bd9
--- /dev/null
+++ b/__tests__/formatter/slot.test.ts
@@ -0,0 +1,83 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter slot test", () => {
+ test("slot without endslot directive https://github.com/shufo/vscode-blade-formatter/issues/304", async () => {
+ const content = [
+ `@component('components.article.intro')`,
+ ` @slot('date', $article->formatDate)`,
+ ` @slot('read_mins', $article->readTime)`,
+ " @if ($author)",
+ ` @slot('authors', [['link' => $author_link, 'name' => $author]])`,
+ " @endif",
+ ` @slot('intro_text')`,
+ " {!! $article->introduction !!}",
+ " @endslot",
+ " @endcomponent",
+ ].join("\n");
+
+ const expected = [
+ `@component('components.article.intro')`,
+ ` @slot('date', $article->formatDate)`,
+ ` @slot('read_mins', $article->readTime)`,
+ " @if ($author)",
+ ` @slot('authors', [['link' => $author_link, 'name' => $author]])`,
+ " @endif",
+ ` @slot('intro_text')`,
+ " {!! $article->introduction !!}",
+ " @endslot",
+ "@endcomponent",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+
+ test("unmatched x-slot close tag", async () => {
+ const content = [
+ "
",
+ ` `,
+ " Foo bar",
+ " ",
+ ` `,
+ " Foo bar",
+ " ",
+ " ",
+ " Foo bar",
+ " ",
+ " ",
+ " Foo bar",
+ " ",
+ " Foo bar",
+ " ",
+ " Foo bar",
+ " ",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ "
",
+ ` `,
+ " Foo bar",
+ " ",
+ ` `,
+ " Foo bar",
+ " ",
+ " ",
+ " Foo bar",
+ " ",
+ " ",
+ " Foo bar",
+ " ",
+ " Foo bar",
+ " ",
+ " Foo bar",
+ " ",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/styles.test.ts b/__tests__/formatter/styles.test.ts
new file mode 100644
index 00000000..414f6a09
--- /dev/null
+++ b/__tests__/formatter/styles.test.ts
@@ -0,0 +1,55 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter styles test", () => {
+ test("css at rule https://github.com/shufo/vscode-blade-formatter/issues/430", async () => {
+ const content = [
+ `@section('css')`,
+ " ",
+ "@endsection",
+ ].join("\n");
+
+ const expected = [
+ `@section('css')`,
+ " ",
+ "@endsection",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/tailwind.test.ts b/__tests__/formatter/tailwind.test.ts
new file mode 100644
index 00000000..40c21d34
--- /dev/null
+++ b/__tests__/formatter/tailwind.test.ts
@@ -0,0 +1,210 @@
+import assert from "node:assert";
+import path from "node:path";
+import { describe, test } from "vitest";
+import { Formatter } from "../../src/main.js";
+import * as util from "../support/util";
+
+describe("formatter tailwind test", () => {
+ test("sort tailwindcss classs option can work", () => {
+ const content = [
+ `
`,
+ "
",
+ ].join("\n");
+ const expected = [
+ `
`,
+ "
",
+ "",
+ ].join("\n");
+
+ return new Formatter({ sortTailwindcssClasses: true })
+ .formatContent(content)
+ .then((result: string) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("sort tailwindcss classs with various character", () => {
+ const content = [
+ `
`,
+ ].join("\n");
+ const expected = [
+ `
`,
+ "",
+ ].join("\n");
+
+ return new Formatter({ sortTailwindcssClasses: true })
+ .formatContent(content)
+ .then((result: string) => {
+ assert.equal(result, expected);
+ });
+ });
+
+ test("long tailwindcss classs", async () => {
+ const content = [
+ `
`,
+ "
",
+ ].join("\n");
+
+ const expected = [
+ `
`,
+ "
",
+ "",
+ ].join("\n");
+
+ const result = await new Formatter({
+ sortTailwindcssClasses: true,
+ }).formatContent(content);
+ assert.equal(result, expected);
+ const result2 = await new Formatter({
+ sortTailwindcssClasses: true,
+ }).formatContent(result);
+ assert.equal(result2, result);
+ });
+
+ test("tailwindcss classs with new line", async () => {
+ const content = [
+ `
`,
+ "
",
+ ].join("\n");
+
+ const expected = [
+ `
`,
+ "
",
+ "",
+ ].join("\n");
+
+ const result = await new Formatter({
+ sortTailwindcssClasses: true,
+ }).formatContent(content);
+ assert.equal(result, expected);
+ const result2 = await new Formatter({
+ sortTailwindcssClasses: true,
+ }).formatContent(result);
+ assert.equal(result2, result);
+ });
+
+ test("inline directive with tailwindcss class sort", async () => {
+ const content = [
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ ].join("\n");
+
+ const expected = [
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ `
`,
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ sortTailwindcssClasses: true,
+ });
+ });
+
+ test("line breaked inline directive with tailwindcss class sort", async () => {
+ const content = [
+ "
({$getJsonMaskConfiguration()})," : null }}`,
+ " state: $wire.{{ $isLazy()",
+ ` ? 'entangle(' . $getStatePath() . ').defer'`,
+ ` : $applyStateBindingModifiers('entangle(' . $getStatePath() . ')') }},`,
+ ` })"`,
+ ` type="text"`,
+ " wire:ignore",
+ ` @if ($isLazy()) x-on:blur="$wire.$refresh" @endif`,
+ " {{ $getExtraAlpineAttributeBag() }}",
+ " @endunless />",
+ ].join("\n");
+
+ const expected = [
+ "
({$getJsonMaskConfiguration()})," : null }}`,
+ " state: $wire.{{ $isLazy()",
+ ` ? 'entangle(' . $getStatePath() . ').defer'`,
+ ` : $applyStateBindingModifiers('entangle(' . $getStatePath() . ')') }},`,
+ ` })"`,
+ ` type="text"`,
+ " wire:ignore",
+ ` @if ($isLazy()) x-on:blur="$wire.$refresh" @endif`,
+ " {{ $getExtraAlpineAttributeBag() }}",
+ " @endunless />",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected, {
+ sortHtmlAttributes: "alphabetical",
+ });
+ });
+
+ test("tailwind config path option", async () => {
+ const content = [
+ `
`,
+ `
Random Stuff `,
+ "",
+ ].join("\n");
+
+ const expected = [
+ `
`,
+ `
Random Stuff `,
+ "",
+ "",
+ ].join("\n");
+
+ const configPath = path.resolve(
+ "__tests__",
+ "fixtures",
+ "tailwind",
+ "tailwind.config.example.js",
+ );
+ await util.doubleFormatCheck(content, expected, {
+ sortTailwindcssClasses: true,
+ tailwindcssConfigPath: configPath,
+ });
+ });
+
+ test("tailwind config object option", async () => {
+ const content = [
+ `
`,
+ `
Random Stuff `,
+ "",
+ ].join("\n");
+
+ const expected = [
+ `
`,
+ `
Random Stuff `,
+ "",
+ "",
+ ].join("\n");
+
+ const config = require(
+ path.resolve(
+ "__tests__",
+ "fixtures",
+ "tailwind",
+ "tailwind.config.example.js",
+ ),
+ );
+ await util.doubleFormatCheck(content, expected, {
+ sortTailwindcssClasses: true,
+ tailwindcssConfig: config,
+ });
+ });
+});
diff --git a/__tests__/formatter/third-party-directives.test.ts b/__tests__/formatter/third-party-directives.test.ts
new file mode 100644
index 00000000..69df32ae
--- /dev/null
+++ b/__tests__/formatter/third-party-directives.test.ts
@@ -0,0 +1,70 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter third party directives test", () => {
+ test("support laravel-permission directives", async () => {
+ const directives = [
+ {
+ start: "@role",
+ end: "@endrole",
+ },
+ {
+ start: "@hasrole",
+ end: "@endhasrole",
+ },
+ {
+ start: "@hasanyrole",
+ end: "@endhasanyrole",
+ },
+ {
+ start: "@hasallroles",
+ end: "@endhasallroles",
+ },
+ {
+ start: "@unlessrole",
+ end: "@endunlessrole",
+ },
+ {
+ start: "@hasexactroles",
+ end: "@endhasexactroles",
+ },
+ ];
+
+ for (const target of directives) {
+ const content = [
+ `
`,
+ `${target.start}('foo')`,
+ "
bar
",
+ `${target.end}`,
+ "
",
+ ].join("\n");
+ const expected = [
+ `
`,
+ ` ${target.start}('foo')`,
+ "
bar
",
+ ` ${target.end}`,
+ "
",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ }
+ });
+
+ test("@permission directive", async () => {
+ const content = [
+ `@permission('post.edit')`,
+ `
Edit Post `,
+ "@endpermission",
+ ].join("\n");
+
+ const expected = [
+ `@permission('post.edit')`,
+ `
Edit Post `,
+ "@endpermission",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});
diff --git a/__tests__/formatter/unbalanced.test.ts b/__tests__/formatter/unbalanced.test.ts
new file mode 100644
index 00000000..f7df8810
--- /dev/null
+++ b/__tests__/formatter/unbalanced.test.ts
@@ -0,0 +1,75 @@
+import { describe, test } from "vitest";
+import * as util from "../support/util";
+
+describe("formatter unbalanced test", () => {
+ test("overrided unbalanced directive #554", async () => {
+ const content = [
+ `
`,
+ " ",
+ ` {{ __('Definition') }} `,
+ ` {{ __('Job') }} `,
+ ` {{ __('Serial Numbers') }} `,
+ ` {{ __('Works from') }} `,
+ ` {{ __('T.I.P.') }} `,
+ ` {{ __('DOC.') }} `,
+ ` {{ __('PROMO') }} `,
+ ` @hasAccess('platform.systems.broadcasts')`,
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasAccess",
+ ` @hasSection('techdocs')`,
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasSection",
+ " ",
+ " ",
+ "
",
+ ` @hasSection('navigation')`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ ` @hasSection('techdocs')`,
+ ` @hasSection('foo')`,
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasSection",
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasSection",
+ " @endhasSection",
+ " ",
+ ].join("\n");
+
+ const expected = [
+ `
`,
+ " ",
+ ` {{ __('Definition') }} `,
+ ` {{ __('Job') }} `,
+ ` {{ __('Serial Numbers') }} `,
+ ` {{ __('Works from') }} `,
+ ` {{ __('T.I.P.') }} `,
+ ` {{ __('DOC.') }} `,
+ ` {{ __('PROMO') }} `,
+ ` @hasAccess('platform.systems.broadcasts')`,
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasAccess",
+ ` @hasSection('techdocs')`,
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasSection",
+ " ",
+ " ",
+ "
",
+ ` @hasSection('navigation')`,
+ " @if ($user)",
+ " {{ $user->name }}",
+ " @endif",
+ ` @hasSection('techdocs')`,
+ ` @hasSection('foo')`,
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasSection",
+ ` {{ __('NOTIFICATION') }} `,
+ " @endhasSection",
+ " @endhasSection",
+ " ",
+ "",
+ ].join("\n");
+
+ await util.doubleFormatCheck(content, expected);
+ });
+});