Dart Blade Parser
#Blade Parser
Note: This project is stable and production-ready. The core parser and formatter are well-tested with 1450+ tests covering edge cases, performance, and real-world scenarios. API breaking changes will follow semantic versioning.
A pure Dart parser for Laravel Blade templates. Produces a typed AST with full support for Blade directives, components, Alpine.js, and Livewire attributes. Includes robust error recovery, JSON serialization, and an idempotent formatter.
#Overview
This library tokenizes and parses Blade templates into a traversable abstract syntax tree. It handles the full complexity of modern Blade templates including nested directives, component slots, Alpine.js event handlers, and Livewire wire:model bindings.
Unlike simple regex-based approaches, this parser uses an iterative state machine lexer and recursive descent parser that correctly handles context-aware parsing: emails vs directives, quoted attributes, raw text elements (<script>, <style>), and verbatim blocks.
#Prettier Plugin
This parser powers a Prettier plugin for Blade templates:
npm install --save-dev prettier-plugin-laravel-blade prettier echo '{ "plugins": ["prettier-plugin-laravel-blade"] }' > .prettierrc npx prettier --write "**/*.blade.php"
See prettier-plugin-laravel-blade/ for full documentation and configuration options.
#Installation
Add to your pubspec.yaml:
dependencies: blade_parser: ^1.0.0
Then install:
dart pub get
#Quick Start
#CLI Commands
Parse and format Blade templates from the command line using the unified blade CLI:
# Parse commands dart run blade parse template.blade.php # Parse to tree (default) dart run blade parse template.blade.php --json # Parse to JSON (full AST) cat template.blade.php | dart run blade parse --stdin # Parse from stdin echo "@if(\$user) Hello @endif" | dart run blade parse --stdin # Format commands dart run blade format template.blade.php # Print formatted to stdout dart run blade format templates/ --write # Format all files in directory dart run blade format "templates/**/*.blade.php" --write # Format with glob pattern dart run blade format templates/ --check # Check formatting (CI mode) cat template.blade.php | dart run blade format --stdin # Format from stdin # Format with custom options dart run blade format template.blade.php --indent-size 2 --indent-style tabs dart run blade format template.blade.php --directive-spacing none --slot-formatting block dart run blade format templates/ --config .blade.json --write # Show help dart run blade --help dart run blade parse --help dart run blade format --help
Parse Command Options:
--tree(default): Human-readable tree structure with node types and positions--json: Complete AST as JSON for programmatic processing--stdin: Read from standard input instead of a file
Format Command Options:
--writeor-w: Write formatted output back to files (default is stdout)--checkor-c: Check if files need formatting without modifying them (for CI/CD)--config <path>: Load configuration from JSON file (.blade.json)--indent-size <n>: Number of spaces for indentation (default: 4)--indent-style <style>: Use 'spaces' or 'tabs' (default: spaces)--quote-style <style>: Quote style - 'single', 'double', or 'preserve' (default: preserve)--directive-spacing <style>: Directive spacing - 'between_blocks', 'none', or 'preserve' (default: between_blocks)--slot-formatting <style>: Slot formatting - 'compact' or 'block' (default: compact)--stdin: Format from standard input--verboseor-v: Verbose output with file-by-file progress
#Development Commands
Common just commands for development workflow:
Testing:
# Run all tests just test # Run specific test file just test-file test/unit/parser/parser_test.dart # Run tests matching pattern just test-name "directive" # Generate coverage report with summary just coverage # Generate HTML coverage and open in browser just coverage-html
Code Quality:
# Lint code (dart analyze) just lint # Format code and apply automatic fixes just fix # Run all checks (lint + format + test) just check
Performance:
# Run performance benchmarks just test-perf # Measures throughput, memory usage, and nesting depth
Formatting:
# Format test fixtures (messy Blade files) just format-fixtures # Reset test fixtures to messy state just reset-fixtures
Documentation:
# Generate API documentation (outputs to doc/api/)
just docs
Build & Distribution:
# Compile CLI to native binary for current platform just compile # Cross-compile for multiple platforms (Linux, macOS) just cross-compile # Outputs to build/ directory: blade-linux-x64, blade-macos-arm64, etc.
Development Tools:
# Run interactive playground (Flutter web app) just playground # Run acid test suite (parse all 117 fixtures) just acid # Get/upgrade dependencies just deps # dart pub get just deps-upgrade # dart pub upgrade just deps-outdated # Dry-run publish to pub.dev just publish-check # Full pre-publish checks just pre-publish
See justfile for complete command reference and additional options.
#Features
- 108 Blade directives (@if, @foreach, @section, @component, @auth, @livewireStyles, etc.)
- Blade components (
<x-alert>) with named and default slots - Alpine.js attributes (x-data, x-show, @click, :bind) with modifier parsing
- Livewire attributes (wire:click, wire:model.live) with full modifier support
- Echo statements ({{ }}, {!! !!}, {{{ }}})
- Multiple error reporting with positions and hints
- Error recovery continues parsing after syntax errors
- Visitor pattern for AST traversal
- JSON serialization for interoperability
- Zero external parsing dependencies
- Idempotent formatter with configurable style
- 117 test fixtures (17,500+ lines) with 1450+ tests covering real-world and synthetic cases
#Use Cases
Formatter: Standardize indentation, normalize spacing, tidy attribute quoting. The included formatter is idempotent and deterministic.
Linter: Build static analysis tools to enforce style rules, security policies (flag raw echoes, require @csrf), or best practices (prefer @forelse, limit nesting depth).
Code Search & Refactoring: Find and rename components, migrate directive patterns, or transform attributes with AST-safe operations.
CI/CD Quality Gates: Parse all templates in CI to catch syntax errors before deployment. Use format checking to enforce consistent code style.
Documentation & Metrics: Generate component usage catalogs, directive frequency reports, or include/extends dependency graphs.
IDE Integration: Foundation for editor tooling like syntax highlighting, structure outline, go-to-definition, and on-save formatting.
#Code Examples
#Parse a Template
import 'package:blade_parser/blade_parser.dart';
void main() {
final parser = BladeParser();
final result = parser.parse('''
<div>
@if(\$user->isAdmin())
<p>Welcome, admin!</p>
@else
<p>Welcome, {{ \$user->name }}!</p>
@endif
</div>
''');
if (result.isSuccess) {
print('Parsed ${result.ast!.children.length} nodes');
} else {
for (final error in result.errors) {
print('${error.position.line}:${error.position.column}: ${error.message}');
}
}
}
#Traverse with Visitor Pattern
class DirectiveCounter extends RecursiveAstVisitor<void> {
int count = 0;
@override
void visitDirective(DirectiveNode node) {
count++;
print('Found @${node.name} at line ${node.startPosition.line}');
super.visitDirective(node);
}
@override
void defaultVisit(AstNode node) {}
}
void main() {
final parser = BladeParser();
final result = parser.parse('''
@foreach(\$users as \$user)
@if(\$user->active)
<p>{{ \$user->name }}</p>
@endif
@endforeach
''');
final visitor = DirectiveCounter();
result.ast!.accept(visitor);
print('Total directives: ${visitor.count}');
}
#Export to JSON
import 'dart:convert';
import 'package:blade_parser/blade_parser.dart';
void main() {
final parser = BladeParser();
final result = parser.parse('<x-alert type="success">Done!</x-alert>');
final json = result.ast!.toJson();
print(JsonEncoder.withIndent(' ').convert(json));
}
#Format Templates
import 'package:blade_parser/blade_parser.dart';
void main() {
final formatter = BladeFormatter(
config: FormatterConfig(
indentSize: 4,
indentStyle: IndentStyle.spaces,
),
);
final messy = '''
<div class="container" >
{{ \$user->name }}\n @if( \$condition )
<p > Hello </p >
@endif
</div>
''';
final result = formatter.formatWithResult(messy);
if (!result.hasErrors && result.wasChanged) {
print(result.formatted);
// Output:
// <div class="container">
// {{ \$user->name }}
// @if(\$condition)
// <p>Hello</p>
// @endif
// </div>
}
}
#Check if Formatting Needed
final formatter = BladeFormatter();
if (formatter.needsFormatting(source)) {
print('File needs formatting');
exit(1); // For CI/CD
}
#Formatter
The formatter produces deterministic, idempotent output: format(format(x)) == format(x).
#Configuration
final config = FormatterConfig( indentSize: 2, // Default: 4 indentStyle: IndentStyle.spaces, // or IndentStyle.tabs quoteStyle: QuoteStyle.preserve, // or QuoteStyle.single, QuoteStyle.double directiveSpacing: DirectiveSpacing.betweenBlocks, // Default: betweenBlocks slotFormatting: SlotFormatting.compact, // Default: compact slotNameStyle: SlotNameStyle.colon, // Default: colon slotSpacing: SlotSpacing.after, // Default: after maxLineLength: 120, // Default: 120 wrapAttributes: WrapAttributes.auto, // Default: auto attributeSort: AttributeSort.none, // Default: none closingBracketStyle: ClosingBracketStyle.sameLine, // Default: sameLine selfClosingStyle: SelfClosingStyle.preserve, // Default: preserve htmlBlockSpacing: HtmlBlockSpacing.betweenBlocks, // Default: betweenBlocks echoSpacing: EchoSpacing.spaced, // Default: spaced trailingNewline: true, // Default: true inlineSelfClosingComponents: false, // Default: false ); final formatter = BladeFormatter(config: config);
Configuration Options:
indentSize: Number of spaces per indent level (default: 4)indentStyle: UseIndentStyle.spacesorIndentStyle.tabs(default: spaces)quoteStyle: How to format attribute quotesQuoteStyle.preserve: Keep original quote style (default)QuoteStyle.single: Convert to single quotesQuoteStyle.double: Convert to double quotes
directiveSpacing: Control blank lines between Blade directivesDirectiveSpacing.betweenBlocks: Add blank line between closing and opening directives (default)DirectiveSpacing.none: No blank lines between directives (compact)DirectiveSpacing.preserve: Preserve blank lines as written
directiveParenthesisSpacing: Control space between directive names and parenthesesDirectiveParenthesisSpacing.spaced: Add space (@if ($var))DirectiveParenthesisSpacing.compact: No space (@if($var))DirectiveParenthesisSpacing.preserve: Keep original spacing (default)
slotFormatting: Control formatting style for component slotsSlotFormatting.compact: Smart detection - compact for simple slots (default)SlotFormatting.block: Always use block formatting with extra blank lines
slotNameStyle: Control how slot names are renderedSlotNameStyle.colon: Always use colon syntax<x-slot:header>(default)SlotNameStyle.attribute: Always use attribute syntax<x-slot name="header">SlotNameStyle.preserve: Preserve the original syntax from the source
slotSpacing: Control blank lines around slot elementsSlotSpacing.after: Add a blank line after each slot (default)SlotSpacing.before: Add a blank line before each slotSlotSpacing.around: Add blank lines both before and after slotsSlotSpacing.none: No blank lines around slots
maxLineLength: Maximum line length before wrapping attributes (default: 120)wrapAttributes: Control when to wrap attributes to multiple linesWrapAttributes.auto: Wrap when line exceeds maxLineLength (default)WrapAttributes.always: Always wrap multiple attributes to separate linesWrapAttributes.never: Never wrap attributes
attributeSort: Control attribute sorting orderAttributeSort.none: Preserve original order (default)AttributeSort.alphabetical: Sort attributes alphabeticallyAttributeSort.byType: Sort by type (HTML → data-* → Alpine → Livewire → other)
closingBracketStyle: Control closing bracket placement when attributes wrapClosingBracketStyle.sameLine: Keep>on same line as last attribute (default)ClosingBracketStyle.newLine: Put>on its own line
selfClosingStyle: Control empty element formattingSelfClosingStyle.preserve: Keep original style (default)SelfClosingStyle.always: Convert empty elements to self-closing (<div />)SelfClosingStyle.never: Convert self-closing to explicit close (<div></div>)
htmlBlockSpacing: Control blank lines between block-level HTML siblingsHtmlBlockSpacing.betweenBlocks: Add blank line between block elements (default)HtmlBlockSpacing.none: No blank lines between block elementsHtmlBlockSpacing.preserve: Preserve blank lines as written
echoSpacing: Control spacing inside echo bracesEchoSpacing.spaced: Always add spaces:{{ $var }}(default)EchoSpacing.compact: No spaces:{{$var}}EchoSpacing.preserve: Preserve original spacing
trailingNewline: Whether to add a trailing newline (default: true)inlineSelfClosingComponents: Whether a self-closing component that is the sole child of an element may collapse onto its parent's linefalse: Standalone components keep the author's block layout, staying on their own line (default)true: Standalone components collapse inline (matches blade-formatter / Prettier defaults)- Components in a text flow (e.g.
Click <x-foo /> here) always stay inline regardless of this setting
#API
// Format and throw on parse errors
final formatted = formatter.format(source);
// Format and get detailed result
final result = formatter.formatWithResult(source);
if (result.isSuccess) {
print(result.formatted);
print('Changed: ${result.wasChanged}');
} else {
for (final error in result.errors) {
print('Error: ${error.message}');
}
}
// Check if formatting needed (useful for CI/CD)
if (formatter.needsFormatting(source)) {
stderr.writeln('File needs formatting');
exit(1);
}
#Features
- Configurable indentation (size and style: spaces or tabs)
- Smart inline vs block formatting (simple content stays inline)
- Configurable directive spacing (blank lines between directives)
- Configurable slot formatting (compact vs block style)
- Slot name style - Control colon syntax (
<x-slot:header>) vs attribute syntax (<x-slot name="header">) - Slot spacing - Control blank lines around slot elements (after, before, around, none)
- Normalizes attribute quoting (single, double, or preserve)
- Line wrapping - Wrap long attribute lists when exceeding maxLineLength
- Multi-line attributes - Format attributes one per line when wrapping
- Attribute sorting - Sort alphabetically or by type (HTML, data-*, Alpine, Livewire)
- Closing bracket style - Control
>placement when wrapping (same line or new line) - Self-closing normalization - Convert between
<div />and<div></div>styles - Ignore comments - Disable formatting for sections with
{{-- blade-formatter:off --}} - Preserves boolean attributes
- Preserves line breaks between echo statements
- Adds final newline
- Spacing between top-level blocks
- Guaranteed idempotency (verified with idempotency tests)
#Before/After Examples
#Basic Formatting
Before:
<div class="container" > {{$user->name}} @if( $condition ) <p > Hello </p > @endif </div>
After:
<div class="container"> {{ $user->name }} @if($condition) <p>Hello</p> @endif </div>
#Directive Spacing
With DirectiveSpacing.betweenBlocks (default):
@foreach($users as $user) <p>{{ $user->name }}</p> @endforeach @while($count > 0) <p>Count: {{ $count }}</p> @endwhile
With DirectiveSpacing.none:
@foreach($users as $user) <p>{{ $user->name }}</p> @endforeach @while($count > 0) <p>Count: {{ $count }}</p> @endwhile
#Slot Formatting
With SlotFormatting.compact (default):
<x-card> <x-slot:header> <h2>Card Title</h2> </x-slot> <p>Card content</p> </x-card>
With SlotFormatting.block:
<x-card> <x-slot:header> <h2>Card Title</h2> </x-slot> <p>Card content</p> </x-card>
#Slot Name Style
With SlotNameStyle.colon (default):
<x-card> <x-slot:header> <h2>Card Title</h2> </x-slot> </x-card>
With SlotNameStyle.attribute:
<x-card> <x-slot name="header"> <h2>Card Title</h2> </x-slot> </x-card>
#Slot Spacing
With SlotSpacing.after (default):
<x-card> <x-slot:header> <h2>Title</h2> </x-slot> <x-slot:footer> <button>Save</button> </x-slot> <p>Content</p> </x-card>
With SlotSpacing.none:
<x-card> <x-slot:header> <h2>Title</h2> </x-slot> <x-slot:footer> <button>Save</button> </x-slot> <p>Content</p> </x-card>
#Attribute Wrapping
With WrapAttributes.auto (default) and line exceeds maxLineLength:
Before:
<x-button type="submit" class="btn btn-primary" wire:click="save" wire:loading.attr="disabled">Save</x-button>
After:
<x-button type="submit" class="btn btn-primary" wire:click="save" wire:loading.attr="disabled">Save</x-button>
#Attribute Sorting
With AttributeSort.byType:
Before:
<input wire:model="email" x-on:blur="validate" data-testid="email" type="email" class="form-input" id="email">
After (sorted by type: HTML → data- → Alpine → Livewire):*
<input class="form-input" id="email" type="email" data-testid="email" x-on:blur="validate" wire:model="email">
#Ignore Comments
Use special comments to disable formatting for specific sections:
{{-- blade-formatter:off --}} <table> <tr><td>1</td><td>2</td><td>3</td></tr> <tr><td>4</td><td>5</td><td>6</td></tr> </table> {{-- blade-formatter:on --}} <!-- Also works with HTML comments --> <!-- blade-formatter:off --> <pre> Preserved whitespace </pre> <!-- blade-formatter:on -->
Supported patterns (case-insensitive):
blade-formatter:off/blade-formatter:onblade-formatter-disable/blade-formatter-enableformat:off/format:on
#Architecture
The parser uses a two-stage architecture:
#Lexer (Tokenization)
An iterative state machine scanner that emits tokens. Key features:
- Iterative (non-recursive) to avoid stack overflow
- Disambiguates
@in emails vs directives - Handles
@@escapes and@{{ }}escaped echoes - Raw text mode for
<script>,<style>,<textarea> - Recognizes Alpine.js shorthand (
@click,:bind) - Parses Livewire modifiers (
wire:model.live.debounce.500ms)
#Parser (AST Construction)
Recursive descent parser building typed AST. Key features:
- Multiple error reporting with positions and hints
- Error recovery via synchronization
- Tag stack validation for matching open/close tags
- Component slot synthesis (named and default slots)
- Specialized handling for control flow, loops, and template inheritance
#AST Node Types
- DocumentNode: Root containing all top-level nodes
- DirectiveNode: Blade directives (
@if,@foreach, etc.) - EchoNode: Echo statements (
{{ }},{!! !!}) - HtmlElementNode: HTML tags with attributes
- ComponentNode: Blade components (
<x-alert>) with slots - SlotNode: Component slot definitions
- TextNode: Plain text content
- CommentNode: HTML and Blade comments
- ErrorNode: Placeholder for parse errors
#Attributes
- StandardAttribute: Regular HTML attributes
- AlpineAttribute: Alpine.js directives with modifiers
- LivewireAttribute: Livewire attributes with modifiers
#Supported Directives (108)
#Control Flow
@if, @elseif, @else, @endif, @unless, @endunless, @isset, @endisset, @empty, @endempty, @switch, @case, @default, @endswitch
#Loops
@foreach, @endforeach, @forelse, @empty, @endforelse, @for, @endfor, @while, @endwhile, @continue, @break
#Template Inheritance
@extends, @section, @endsection, @yield, @parent, @show, @overwrite
#Stacks
@push, @endpush, @prepend, @endprepend, @stack, @once, @endonce, @pushOnce, @endPushOnce, @pushIf, @prependOnce, @endPrependOnce
#Components
@component, @endcomponent, @slot, @endslot, @props, @aware
#Includes
@include, @includeIf, @includeWhen, @includeUnless, @includeFirst, @each
#Authorization
@auth, @endauth, @guest, @endguest, @can, @endcan, @cannot, @endcannot, @canany, @endcanany
#Environment
@env, @endenv, @production, @endproduction, @session, @endsession
#Livewire & Filament
@livewireStyles, @livewireScripts, @livewireScriptConfig, @script, @endscript, @assets, @endassets, @filamentStyles, @filamentScripts
#Utilities
@php, @endphp, @verbatim, @endverbatim, @inject, @use, @json, @method, @csrf, @vite, @dd, @dump, @class, @style, @checked, @selected, @disabled, @readonly, @required, @fragment, @endfragment
#Performance
Measured on typical hardware with the benchmark suite. All claims verified by automated benchmarks in CI.
Throughput:
- 100,000+ lines/sec on simple templates (benchmarks show 47,000-637,000 lines/sec)
- 500,000+ components/sec on component-heavy templates
- Maintains performance across template sizes (linear scaling)
Memory:
- Linear growth: <2KB per line for large files
- ~3MB for 100,000 line templates
- Efficient memory usage with proper GC behavior
Nesting:
- No degradation up to 20 levels
- Successfully parses 1,000+ nested levels in <2 seconds
- Iterative lexer avoids stack overflow issues
Benchmarks are verified in CI: Run just test-perf to measure performance on your hardware. See test/performance/ for benchmark source code.
#Test Fixtures
117 test fixtures (17,500+ lines) covering real-world and synthetic cases.
- Real-world fixtures: Production templates from Laravel apps (chatflow, reflow, boatflow, unlimit, crescat, kassalapp)
- Synthetic fixtures: Systematically generated feature tests covering all directives and edge cases
- Format fixtures: 8 intentionally messy files for idempotency and formatting tests
Browse the catalog: test/fixtures/INDEX.md
Format fixtures for testing:
- test/fixtures/format/ - Messy templates for formatter tests
- test/fixtures/format/README.md - Usage instructions and workflow
#Tools
The project includes several tools for development, testing, and demonstration:
#Playground (Interactive Web App)
Interactive Flutter web app for live parsing with JSON/tree visualization and error display. Parse Blade templates in your browser with real-time feedback.
just playground
# Opens Chrome with the Flutter web app
Features:
- Live parsing as you type
- JSON and tree visualization
- Error highlighting with positions
- Share example templates
Located in tool/playground/.
#Acid Test (Bulk Parse Testing)
Bulk parse testing across all 117 fixtures with console and HTML reporting. Validates parser correctness across the entire fixture suite.
just acid
# Runs all fixtures and opens HTML report in browser
Output:
- Console summary with pass/fail counts
- HTML report with detailed results per fixture
- Parse time metrics
- Error details for failed fixtures
Located in tool/acid/.
#Format File (Standalone Formatter)
Standalone formatter script for formatting individual Blade files.
# Preview formatting changes dart run tool/format_file.dart template.blade.php # Format and write back to file dart run tool/format_file.dart template.blade.php --write
Options:
- No flags: Print formatted output to stdout
--write: Write formatted output back to file
Located in tool/format_file.dart.
#Limitations
- Formatter does not reformat PHP expressions inside directives/echoes
- True streaming/incremental parsing not implemented (stub exists)
- Some component error positions are coarse but recoverable
- Not a PHP evaluator or security engine (parses syntax safely)
#API Documentation
Generate API docs locally:
just docs
# Outputs to doc/api/
#Platform Support
Works on all Dart platforms:
- Flutter (iOS, Android, Web, Desktop)
- Dart CLI
- Dart Web (dart2js)
#Contributing
Contributions are welcome. Ensure:
- All tests pass (
just test) - Code follows Dart style guidelines (
just lint) - New features include tests
- Formatter is idempotent on new fixtures
#License
MIT License. See LICENSE for details.