# Chisel

An opinionated Prettier plugin for Laravel Blade templates.

## Installation

Requires Node.js 18 or newer.

Install Prettier and the plugin in your project:

```bash
npm i -D prettier prettier-plugin-blade@^3
```

Optional integrations:

```bash
npm i -D @prettier/plugin-php prettier-plugin-tailwindcss
```

## Migration

Chisel v3 (`prettier-plugin-blade`) is a ground-up rewrite. Expect output changes compared to previous versions.

If you rely on previous versions, you should specify the exact version you'd like to use in your `package.json`.

## Quick Start

Create or update your `.prettierrc`:

```json
{
  "plugins": [
    "prettier-plugin-blade"
  ],
  "overrides": [
    {
      "files": ["*.blade.php"],
      "options": {
        "parser": "blade"
      }
    }
  ]
}
```

Format:

```bash
npx prettier --write "resources/views/**/*.blade.php"
```

### CLI Flags

You can pass Blade plugin options directly to the Prettier CLI as kebab-case flags:

```bash
npx prettier "resources/views/**/*.blade.php" \
  --write \
  --plugin prettier-plugin-blade \
  --plugin @prettier/plugin-php \
  --parser blade \
  --blade-php-formatting safe \
  --blade-php-formatting-targets echo \
  --blade-php-formatting-targets directiveArgs \
  --blade-component-prefixes x \
  --blade-component-prefixes flux \
  --blade-directive-arg-spacing space \
  --blade-directive-arg-spacing-overrides if \
  --blade-directive-arg-spacing-overrides section=0 \
  --blade-echo-spacing tight
```

Notes:

- Use kebab-case in CLI flags (for example `bladePhpFormatting` -> `--blade-php-formatting`).
- Load `@prettier/plugin-php` when using `bladePhpFormatting: "safe"` from the CLI.
- Array options use repeated flags in CLI (for example: `--blade-inline-intent-elements p --blade-inline-intent-elements svg --blade-inline-intent-elements svg:*`).
- `bladeDirectiveCaseMap` takes a JSON object string when passed via CLI, but shell quoting is easy to get wrong. Prefer setting it in `.prettierrc`.

## Blade Plugins

Use `bladeSyntaxPlugins` to enable framework-specific or package-specific Blade behavior.

Available plugins:

- `statamic`
- `log1x/sage-directives`: for improved compatibility with [https://github.com/Log1x/sage-directives](https://github.com/Log1x/sage-directives)

Example:

```json
{
  "plugins": [
    "prettier-plugin-blade",
    "@prettier/plugin-php"
  ],
  "overrides": [
    {
      "files": ["*.blade.php"],
      "options": {
        "parser": "blade",
        "bladePhpFormatting": "safe",
        "bladeSyntaxPlugins": [
            "statamic",
            "log1x/sage-directives"
        ]
      }
    }
  ]
}
```

## PHP Formatting

To format embedded PHP fragments inside Blade, install `@prettier/plugin-php` and include it in `plugins`:

```json
{
  "plugins": [
    "prettier-plugin-blade",
    "@prettier/plugin-php"
  ],
  "overrides": [
    {
      "files": ["*.blade.php"],
      "options": {
        "parser": "blade",
        "bladePhpFormatting": "safe"
      }
    }
  ]
}
```

Notes:

- Without `@prettier/plugin-php`, the formatter still works. It falls back gracefully and leaves PHP fragments unchanged.
- `bladePhpFormatting` modes documented here: `"off"`, `"safe"`.

## Enable Tailwind CSS Class Sorting

Install the Tailwind CSS plugin and include it in `plugins`:

```json
{
  "plugins": [
    "prettier-plugin-blade",
    "prettier-plugin-tailwindcss"
  ],
  "overrides": [
    {
      "files": ["*.blade.php"],
      "options": {
        "parser": "blade"
      }
    }
  ]
}
```

## VS Code Setup

1. Install the `Prettier - Code formatter` extension (`esbenp.prettier-vscode`).
2. Ensure your project has local `devDependencies` for Prettier and this plugin.
3. Add workspace settings in `.vscode/settings.json`:

```json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "prettier.requireConfig": true,
  "files.associations": {
    "*.blade.php": "blade"
  },
  "[blade]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}
```

## Options

### Blade Plugin Options

| Option | Type | Default | Values |
| --- | --- | --- | --- |
| `bladePhpFormatting` | `choice` | `"safe"` | `"off"`, `"safe"`, `"aggressive"` |
| `bladePhpFormattingTargets` | `string[]` | `["directiveArgs", "echo", "phpBlock", "phpTag"]` | `echo`, `directiveArgs`, `phpBlock`, `phpTag`; use `[]` (or CLI `none`) to disable all |
| `bladeSyntaxPlugins` | `string[]` | `["statamic"]` | plugin names (for example `["statamic"]`) |
| `bladeDirectiveCase` | `choice` | `"preserve"` | `"preserve"`, `"canonical"`, `"lower"` |
| `bladeDirectiveCaseMap` | `string` | `""` | JSON object string, e.g. `{"disk":"Disk"}` |
| `bladeDirectiveArgSpacing` | `choice` | `"space"` | `"preserve"`, `"none"`, `"space"` |
| `bladeDirectiveArgSpacingOverrides` | `string[]` | `["if", "elseif", "unless", "while", "for", "foreach", "forelse", "switch", "case"]` | tokens like `if`, `section=none`, `php=2`, `custom=preserve` |
| `bladeDirectiveBlockStyle` | `choice` | `"preserve"` | `"preserve"`, `"inline-if-short"`, `"multiline"` |
| `bladeBlankLinesAroundDirectives` | `choice` | `"preserve"` | `"preserve"`, `"always"` |
| `bladeEchoSpacing` | `choice` | `"preserve"` | `"preserve"`, `"space"`, `"tight"` |
| `bladeSlotClosingTag` | `choice` | `"canonical"` | `"canonical"`, `"preserve"` |
| `bladeInlineIntentElements` | `string[]` | `["p", "svg", "svg:*"]` | elements and namespace wildcards |
| `bladeComponentPrefixes` | `string[]` | `["x", "s", "statamic", "flux", "livewire", "native"]` | component prefixes |
| `bladeInsertOptionalClosingTags` | `boolean` | `false` | `true`, `false` |
| `bladeKeepHeadAndBodyAtRoot` | `boolean` | `true` | `true`, `false` |

`bladePhpFormattingTargets` aliases supported in each array entry:

- `echoes` -> `echo`
- `directive-args`, `directive_args` -> `directiveArgs`
- `php-block`, `php_block` -> `phpBlock`
- `php-tag`, `php_tag` -> `phpTag`

`bladeComponentPrefixes` behavior:

- Bare prefix tokens expand to both Blade component separator forms:
  - `x` -> `x-`, `x:`
  - `widget` -> `widget-`, `widget:`
- Explicit separator tokens are preserved as-is:
  - `x-` matches only dash form tags
  - `x:` matches only colon form tags

`bladeInlineIntentElements` SVG behavior:

- `svg` keeps single-line `<svg>...</svg>` container intent when the source is inline.
- `svg:*` keeps inline attribute and style intent for SVG namespace elements such as `<path>` and `<line>`.
- Remove one or both entries to opt out of those SVG-specific inline layouts.

### Directive Arg Spacing

Directive argument spacing has one base rule plus optional per-directive overrides.

- `bladeDirectiveArgSpacing` is the base rule.
- `bladeDirectiveArgSpacingOverrides` is a `string[]` of `directive[=rule]` options.
- Directive names, without a specified rule, default to `space`.
- Directive names are case-insensitive and may be written with or without `@`.
- Rules may be `"preserve"`, `"none"`, `"space"`, or a non-negative integer.
- Providing `bladeDirectiveArgSpacingOverrides` replaces the built-in defaults (`if`, `elseif`, `unless`, `while`, `for`, `foreach`, `forelse`, `switch`, `case`).

Example:

```json
{
  "bladeDirectiveArgSpacing": "none",
  "bladeDirectiveArgSpacingOverrides": ["if", "section=1", "php=2"]
}
```

With that configuration:

- `@if($x)` becomes `@if ($x)`
- `@include ($view)` becomes `@include($view)`
- `@section($name)` becomes `@section ($name)`
- `@php($expr)` becomes `@php  ($expr)`

### `bladeBlankLinesAroundDirectives`

This option controls blank lines between directive branches inside a structured directive block.

Examples of branch separators this option affects:

- `@if ... @else ... @endif`
- `@switch ... @case ... @default ... @endswitch`
- `@section ... @endsection`

Primary scope:

- It decides how much vertical space to print between one branch and the next branch marker.
- In `preserve` mode it can also keep some existing blank lines between structured siblings inside a directive body when those blank lines were already present in source.

That means it can affect spacing before `@else`, `@elseif`, `@endif`, `@case`, `@default`, `@endswitch`, and similar closers/openers inside the same directive block.

Supported values:

- `"preserve"`
  - Keeps an existing blank line between directive branches if one existed in the source.
  - Otherwise prints a single newline between branches.
- `"always"`
  - Always inserts a blank line between directive branches.
  - In practice this means two line breaks between branches.

Example input:

```blade
@if($x)
<p>a</p>
@else
<p>b</p>
@endif
```

With `bladeBlankLinesAroundDirectives: "preserve"`:

```blade
@if ($x)
  <p>a</p>
@else
  <p>b</p>
@endif
```

With `bladeBlankLinesAroundDirectives: "always"`:

```blade
@if ($x)
  <p>a</p>

@else
  <p>b</p>

@endif
```

Notes:

- This option is most visible when directive blocks print in multiline form.
- If a block is kept inline, there are no multiline branch separators for this option to manage.
- In `preserve` mode, some authored blank lines inside a directive body can also survive between structured siblings when the source already had them.

### Relevant Core Prettier Options

This plugin also respects standard Prettier options, including:

- `printWidth`
- `tabWidth`
- `useTabs`
- `singleQuote`
- `singleAttributePerLine`
- `bracketSameLine`
- `endOfLine`
- `htmlWhitespaceSensitivity`

## Example Full Config

```json
{
  "plugins": [
    "prettier-plugin-blade",
    "@prettier/plugin-php",
    "prettier-plugin-tailwindcss"
  ],
  "overrides": [
    {
      "files": ["*.blade.php"],
      "options": {
        "parser": "blade",
        "htmlWhitespaceSensitivity": "css",
        "bladePhpFormatting": "safe",
        "bladePhpFormattingTargets": ["directiveArgs", "echo", "phpBlock", "phpTag"],
        "bladeSyntaxPlugins": ["statamic"],
        "bladeDirectiveCase": "preserve",
        "bladeDirectiveArgSpacing": "space",
        "bladeDirectiveArgSpacingOverrides": ["if", "elseif", "unless", "while", "for", "foreach", "forelse", "switch", "case"],
        "bladeDirectiveBlockStyle": "preserve",
        "bladeBlankLinesAroundDirectives": "preserve",
        "bladeEchoSpacing": "space",
        "bladeSlotClosingTag": "canonical",
        "bladeInlineIntentElements": ["p", "svg", "svg:*"],
        "bladeComponentPrefixes": ["x", "s", "statamic", "flux", "livewire", "native"],
        "bladeInsertOptionalClosingTags": false,
        "bladeKeepHeadAndBodyAtRoot": true
      }
    }
  ]
}
```

## Troubleshooting

### Tailwind CSS classes are not sorting

- Confirm `prettier-plugin-tailwindcss` is installed.
- Confirm it is listed in `plugins`.
- Confirm class values are static strings (not mixed with Blade interpolation).

### PHP formatting is not running

- Install `@prettier/plugin-php`.
- Set `bladePhpFormatting` to `"safe"`.
- Ensure target is enabled in `bladePhpFormattingTargets`.

### VS Code formats with wrong Prettier

- Use local project dependencies.
- Set `"prettier.requireConfig": true`.
- Set `"prettier.prettierPath"` when needed.
