Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dindent Laravel Package

schleuse/dindent

Dindent is a regex-based HTML beautifier for PHP that indents markup from template engines without rebuilding, sanitizing, or otherwise altering the document—unlike DOM or Tidy. Ideal for debugging messy output. Install via Composer: schleuse/dindent.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation (PHP 8.2+ required):

    composer require schleuse/dindent:^3.1
    

    No additional configuration is required—just autoload via Composer.

    New in v3.1.0 (2026-10-03): bug-fix release e130cb1 — "Reset temporary replacements between calls." If you pinned ^3.0 and share a process across many indent() calls, bump the constraint to ^3.1 (see the Gotchas section before you rely on repeated calls). No API changes, no deprecations.

  2. First Use Case (One-line mode, introduced in v3.0):

    use Schleuse\Dindent\Dindent;
    
    $dirtyHtml = '<div><span>Hello</span></div><p>World</p>';
    
    // Standard indentation
    $indented = Dindent::indent($dirtyHtml);
    // Outputs:
    // <div>
    //   <span>
    //     Hello
    //   </span>
    // </div>
    // <p>
    //   World
    // </p>
    
    // One-line mode (compact output)
    $oneLine = Dindent::indent($dirtyHtml, ['oneLine' => true]);
    // Outputs: <div><span>Hello</span></div><p>World</p>
    
  3. Where to Look First:

    • Source Code — check the v3.1.0 tag.
    • Dindent::indent() method docs (supports oneLine, skipScripts, extractPre, and handles <style> tags).
    • v3.0.0...v3.1.0 diff — a single commit; read it once to understand the placeholder lifecycle.
    • Test Cases for edge cases (<pre>, multi-line comments, repeated invocations).

Implementation Patterns

Workflows

  1. Debugging HTML Responses (One-line mode for APIs):

    // In a controller or middleware (compact for APIs)
    $responseHtml = $response->getContent();
    $indentedHtml = Dindent::indent($responseHtml, ['oneLine' => $request->wantsJson()]);
    Log::debug('Indented HTML:', ['html' => $indentedHtml]);
    
  2. Testing Views (Handle <style> and <pre> tags):

    // In a PHPUnit test (assert style tags are preserved)
    $viewOutput = $this->get('/some-route')->getContent();
    $indented = Dindent::indent($viewOutput);
    $this->assertStringContainsString('<style>', $indented);
    $this->assertStringContainsString('  <pre>', $indented); // Indented but not broken
    
  3. Batch / Queue Processing (safe as of v3.1.0):

    // app/Jobs/FormatHtmlFile.php — one job, many files, same PHP process
    public function handle(): void
    {
        foreach ($this->paths as $path) {
            $html = file_get_contents($path);
    
            // Pre-v3.1.0 this loop could leak placeholder state from file N
            // into file N+1. Fixed in v3.1.0 — replacements reset per call.
            file_put_contents($path, Dindent::indent($html, [
                'oneLine'    => false,
                'extractPre' => true,
            ]));
        }
    }
    
  4. Pre-commit Hooks (Optimized for speed):

    // In an Artisan command
    Artisan::command('dindent:fix', function () {
        foreach ($this->argument('paths') as $file) {
            $html = file_get_contents($file);
            file_put_contents($file, Dindent::indent($html, ['oneLine' => true]));
        }
    });
    

Integration Tips

  • Blade Directives (Support for <style> and one-line mode):

    Blade::directive('indent', function ($expression) {
        return "<?php echo \\Schleuse\\Dindent\\Dindent::indent({$expression}, ['oneLine' => false]); ?>";
    });
    

    Usage in Blade:

    @indent($unformattedHtml) <!-- Standard indentation -->
    @php echo \Schleuse\Dindent\Dindent::indent($html, ['oneLine' => true]); @endphp <!-- Compact -->
    
  • API Responses (Dynamic indentation based on request):

    public function handle($request, Closure $next)
    {
        $response = $next($request);
        if ($response->headers->get('Content-Type') === 'text/html') {
            $options = $request->wantsJson() ? ['oneLine' => true] : [];
            $response->setContent(Dindent::indent($response->getContent(), $options));
        }
        return $response;
    }
    
  • Long-Running Workers / Octane (v3.1.0 is the target here):

    // app/Providers/AppServiceProvider.php
    // Under Octane or a queue worker the process is reused for thousands of
    // requests. Prior to v3.1.0, temporary replacements from a previous
    // invocation could survive into the next one. On v3.1.0+ this is safe;
    // on v3.0.x, resolve a fresh instance per request instead.
    
  • Artisan Commands (Extract <pre> and <textarea>):

    Artisan::command('dindent:fix', function () {
        foreach ($this->argument('paths') as $file) {
            $html = file_get_contents($file);
            $indented = Dindent::indent($html, [
                'oneLine'    => false,
                'extractPre' => true, // Preserve <pre> content
            ]);
            file_put_contents($file, $indented);
        }
    });
    

Gotchas and Tips

Pitfalls

  1. PHP 8.2 Requirement (⚠️ Breaking from v2):

    • Update php.ini or use a PHP 8.2+ runtime (Laravel Valet, Docker, [email protected]).
    • Check CI/CD pipelines (GitHub Actions, GitLab CI) for PHP version compatibility.
  2. State Leaking Between Calls — fixed in v3.1.0 (⚠️ the reason to upgrade):

    • Dindent protects <pre>/<script>/<style> content by swapping it out for placeholders and restoring it afterwards. Before e130cb1, those replacements were not reset between calls, so a second indent() in the same process could restore placeholders from the first call — producing truncated or duplicated content, or placeholder tokens leaking into output.
    • Symptoms are non-deterministic and only show up in reused processes: queue workers, Octane, artisan commands looping over files, or multiple indent() calls inside one test method.
    • On v3.1.0+: fixed, repeated calls are isolated.
    • On v3.0.x: use a fresh instance per call, or restart the worker.
    // v3.0.x workaround
    $indented = (new Dindent())->indent($html);
    
  3. Self-Closing Tags in One-Line Mode: Dindent preserves self-closing tags (e.g., <img/>) even in compact mode:

    $html = '<div><img src="x" /><span>Text</span></div>';
    Dindent::indent($html, ['oneLine' => true]);
    // Outputs: <div><img src="x" /><span>Text</span></div>
    
  4. <style> and <script> Handling:

    • Fixed in v3.0: <style> tags are now properly indented (previously ignored).
    • Warning: Complex CSS/JS may break if indented. Use skipScripts to opt out:
      Dindent::indent($html, ['skipScripts' => true]);
      
  5. <pre> and <textarea> Extraction:

    • Content inside <pre> or <textarea> is preserved as-is (not indented).
    • Gotcha: If you want to indent <pre> content, use extractPre: false:
      Dindent::indent($html, ['extractPre' => false]);
      
  6. Performance in One-Line Mode:

    • One-line mode is ~30% faster for large HTML strings.
    • Caveat: Still avoid processing >5MB files in loops. v3.1.0 makes loops correct, not cheap.

Debugging

  • Repeated-Call Regression Test (the exact scenario v3.1.0 fixed):

    public function test_repeated_calls_are_isolated()
    {
        $first  = '<div><pre>  keep this  </pre></div>';
        $second = '<div><p>plain</p></div>';
    
        Dindent::indent($first);
        $out = Dindent::indent($second);
    
        $this->assertStringContainsString('<p>plain</p>', $out);
        $this->assertStringNotContainsString('keep this', $out); // no bleed-through
    }
    
  • Edge Cases (multi-line comments and <pre>):

    $html = <<<HTML
    <div>
      <!--
        Multi-line
        comment
      -->
      <pre>  Preserve this indentation!
      </pre>
    </div>
    HTML;
    $indented = Dindent::indent($html);
    // Multi-line comments and <pre> content remain intact.
    
  • Logging Differences:

    $original = '<div>Test</div>';
    $standard = Dindent::indent($original);
    $compact  = Dindent::indent($original, ['oneLine' => true]);
    $this->assertNotEquals($standard, $compact, 'Modes produce different outputs!');
    

Config Quirks

  • Custom Indentation with One-Line Mode:

    class CustomDindent extends Dindent {
        public static function indent($html, array $options = []) {
            $options['indent']  = '    '; // 4 spaces
            $options['oneLine'] = false;
            return parent::indent($html, $options);
        }
    }
    
  • Extension Points (extractPre and skipScripts):

    Dindent::indent($html, [
        'extractPre'  => true,  // Default: preserve <pre> content
        'skipScripts' => false, // Default: indent <script> and <style>
    ]);
    
  • Composer Constraint: ^3.0 will resolve to 3.1.x on a fresh composer update, but a committed lockfile on 3.0.0 will not move. Run composer update schleuse/dindent explicitly — the only change is the statefulness fix, so the blast radius is small.

Pro Tips

  1. Visual Diffs for One-Line Mode:

    echo "Standard:"; echo "$standardHtml" | diff -u - <(echo "$compactHtml")
    
  2. CI Integration (test both modes):

    public function test_indentation_modes()
    {
        $html = '<div><p>Test</p></div>';
        $standard = Dindent::indent($html);
        $compact  = Dindent::indent($html, ['oneLine' => true]);
        $this->assertStringContainsString("\n  <p>", $standard);
        $this->assertStringNotContainsString("\n", $compact);
    }
    
  3. Partial Indentation with <pre>: Indent everything except <pre> blocks, then assert they survived byte-for-byte:

    preg_match_all('/<pre>(.*?)<\/pre>/s', $html, $preBlocks);
    $indented = Dindent::indent($html, ['extractPre' => true]);
    foreach ($preBlocks[1] as $block) {
        $this->assertStringContainsString($block, $indented);
    }
    
  4. Pin the Fix in CI: Add a smoke test that calls indent() twice in the same process (see Debugging) so a future downgrade or lockfile drift can't silently reintroduce the v3.0.x behaviour.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package