php-collective/symfony-djot
Symfony integration for Djot, the lightweight markup language. Provides bundle configuration, service wiring, and Twig rendering so you can parse and display Djot content in Symfony applications with minimal setup.
composer require php-collective/symfony-djot:^0.2. Note this is a Symfony bundle wrapping the Djot parser; in a Laravel app you won't get Twig filters or bundle auto-registration, so plan to bind the converter in a service provider yourself (or use the underlying php-collective/djot-php parser directly and treat this package as the config/extension layer).safe_mode flag, the enabled-extensions list, and the converter options (significant_newlines, soft_break_mode, xhtml). That config is the whole API surface you'll touch day-to-day.safe_mode now defaults to true, so a plain convert() call is safe to output with {!! !!} without extra sanitising.// app/Providers/DjotServiceProvider.php
$this->app->singleton('djot', fn () => new Converter(safeMode: true)); // user content
$this->app->singleton('djot.raw', fn () => new Converter(safeMode: false)); // trusted content
Two converters, two Blade directives. Mirror the package's |djot / |djot_raw split with Blade directives so intent is visible at the call site:
Blade::directive('djot', fn ($expr) => "<?php echo app('djot')->convert($expr); ?>");
Blade::directive('djotRaw', fn ($expr) => "<?php echo app('djot.raw')->convert($expr); ?>");
Then @djot($comment->body) for user content and @djotRaw($page->body) for CMS/admin-authored content only.
Render at write time, not read time. Djot→HTML is fast but not free on an index of 50 posts. Store both body_djot and body_html on save (or cache keyed by md5($body)), and re-render on update. This also keeps safe-mode decisions frozen per record instead of per request.
Pick extensions per surface, not globally. The six new extensions are content-type specific:
admonition, tabs, code_group — documentation and knowledge-base pages.inline_footnotes, heading_reference — long-form articles; heading_reference gives you auto-anchors you can harvest for a table of contents.heading_level_shift — essential when embedding a Djot fragment into a page that already has an <h1>; set the shift on the embedded-content converter only.Use the new converter options deliberately. soft_break_mode is the one you want for chat-style/comment rendering where a single newline should become <br>. significant_newlines matters if you're round-tripping prose where blank lines are meaningful. xhtml is for HTML email (Laravel mailables) and export pipelines — turn it on only in the mailable renderer, not globally.
Integration points. API resources returning { djot, html } pairs; mailables that convert Djot to HTML then @include it into a Blade mail template with xhtml: true; an import command that converts legacy Markdown bodies through Djot once, offline.
safe_mode flipping to true means any existing pipeline that relied on raw HTML passthrough will now see it escaped or stripped. Grep your codebase and DB for Djot content containing raw HTML (<div, <iframe, <script) and decide per source. Anything from a CMS editor gets |djot_raw; anything from $request never does.safe_mode: true output is already escaped, so output it with {!! !!} — using {{ }} will show literal <p>. Conversely, djot_raw output must also use {!! !!} but is only acceptable on trusted sources. There is no configuration that makes djot_raw safe for user input; the filter name is a warning, not a suggestion.heading_level_shift or heading_reference retroactively changes heading tags and anchor IDs on already-stored content. If you store rendered HTML, you need a backfill/re-render job, and your TOC links may break. Add a renderer_version column or config hash if you plan to toggle extensions.xhtml. Void/self-closing tag differences (<br> vs <br />) break string-equality assertions. Pin the option in test setup rather than inheriting it from config.0.x releases are allowed to break. Pin ^0.2 and read the release notes on every bump — this one already changed a default that silently weakens or breaks output.php artisan tinker with app('djot')->convert($sample) to isolate parser behaviour from your Blade/view layer. For anything odd, dump the AST (the underlying parser exposes it) instead of guessing from HTML.config/djot.php, remember php artisan config:cache in deploy — the classic "works locally, ignored in production" failure.How can I help you explore Laravel packages today?