markup-carve/carve-php
Carve PHP is a fast, extensible parser for the Carve lightweight markup language. It converts Carve documents to clean HTML through a simple API, making it easy to add Markdown-style content rendering to Laravel and other PHP applications.
Install via Composer. The package is Composer-ready:
composer require markup-carve/carve-php
It is a small, low-dependency library, so there is usually no service provider or facade to register — a plain use statement is enough.
Read these first, in order:
README.md for the canonical parse/render call.src/ for the public entry class (typically a parser plus a separate HTML renderer) and the list of supported node types.tests/ for executable examples of every supported construct — this is the most reliable documentation for a package with a small user base.Smallest working call. The exact class names must be confirmed in src/, but the shape is a parse-then-render pipeline:
use Carve\Carve; // verify namespace/class against README or src/
$html = Carve::parse($post->body)->render();
First use case: rendering a trusted prose column (blog post, docs page, changelog) to HTML in a Blade view. Wrap the call once, put it behind {!! !!}, and you have a working pipeline in a few lines. Do not start with user-submitted content — settle the sanitization story first (see Gotchas).
Wrap the parser behind a helper or Blade directive. Avoid scattering parser calls through controllers and views. A single entry point keeps options, caching, and sanitization in one place:
// app/Support/Carve.php
public static function toHtml(string $text): string
{
return Carve::parse($text)->render();
}
Then in Blade: {!! \App\Support\Carve::toHtml($post->body) !!}.
Cache the rendered HTML, not the parse. Parsing on every request is wasted work for content that changes rarely. Store body_html alongside body and regenerate on save:
protected static function booted(): void
{
static::saving(fn ($post) => $post->body_html = Carve::parse($post->body)->render());
}
This also decouples rendering from request time and makes the output searchable/indexable if you want.
Render at the edge, store the source. Keep the Carve source as the system of record. If you later swap renderers, add extensions, or fix an escaping bug, you can re-render everything from source; you cannot reverse HTML back into markup.
Post-process the AST for features HTML alone can't give you. A parse → tree → render pipeline means you can walk the tree before rendering to add heading IDs and anchor links, lazy-load images, rewrite relative URLs, or wrap tables in scroll containers. This is the cleanest integration point and avoids fragile regex over final HTML.
Use extensions for repeated domain constructs. Callouts, embeds, and video shortcodes are better expressed as a custom node plus a renderer than as raw HTML embedded in prose. Check the package's extension registration mechanism in src/ — this is where the "extensible design" pays off.
Serve it through an API resource consistently. If the frontend and API both render content, return the same rendered HTML (or the same AST-derived JSON) from one place. Two pipelines will drift.
Test with golden files. Keep paired input/output fixtures for the constructs you rely on. Because Carve is Markdown-inspired rather than Markdown-compatible, this suite is also your migration safety net if you import existing content.
{!! !!}.{!! !!}. The output is intentionally unescaped HTML; the escaping decision must happen inside your wrapper, not at each call site.mbstring. Multibyte input plus string offsets is a classic source of mangled output. Ensure mbstring is installed and that your database columns and connection charset are UTF-8.php artisan vendor:publish exists. Check the source for option names; extension/feature flags in small packages are often undocumented and differ between releases.composer.json resolves to the release you expect, and add the package to your dependency-update routine. A future-dated release in package metadata is worth confirming against the repository tags before you lock it in.How can I help you explore Laravel packages today?