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

Markdown Dom Laravel Package

prinsfrank/markdown-dom

prinsfrank/markdown-dom is a PHP package that converts Markdown into a DOM structure, letting you traverse, query, and manipulate parsed Markdown programmatically before rendering. Useful for custom transformations, content processing, and DOM-based workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

prinsfrank/markdown-dom is a framework-agnostic PHP library that parses Markdown into a traversable document object model (document → block/inline nodes → text) instead of returning an HTML string. The value is the tree: once you have nodes you can walk, inspect, mutate, and render them — instead of regex-hacking HTML strings after the fact. In a Laravel app it slots in wherever you currently call Str::markdown() or Illuminate\Mail\Markdown and then find yourself doing brittle string surgery on the result.

Minimal steps:

composer require prinsfrank/markdown-dom
# and a parser adapter — the DOM package defines nodes, not the Markdown parser
composer require prinsfrank/markdown-dom-league-common

Look first at:

  1. README on GitHub — the API surface is deliberately small; the examples are the fastest path to a working spike.
  2. The MarkdownParser contract — this is the seam you bind, inject, and fake in tests.
  3. The Node namespace — this is the vocabulary you'll program against (headings, paragraphs, links, images, lists, code blocks).
  4. The markdown-dom-league-common adapter — it wraps league/commonmark, which is already a transitive dependency of Laravel via Markdown mail, so you inherit its extension set and config.

Your first use case should be user-authored Markdown (comments, release notes, internal docs) that you render to HTML but with post-processing the string approach makes painful: adding rel="nofollow"/target="_blank" to external links, setting loading="lazy" on images, wrapping tables for responsive CSS, or collecting headings into a table of contents. The first two are a 10-line visitor here and a regex risk everywhere else.

Implementation Patterns

Parse once, render on write. Parsing into a DOM is materially more expensive than string concatenation, so don't do it on every request. Parse on save (model saving/saved observer), render to HTML, and persist the HTML alongside the source Markdown. Re-render only when the source or the parser config changes. If you must render on read, cache keyed by hash('xxh128', $markdown . serialize($config)) and bust the cache on deploy when you change extensions.

Bind the parser, don't new it. Register the adapter in a service provider and inject MarkdownParser where needed:

// app/Providers/AppServiceProvider.php
$this->app->bind(MarkdownParser::class, LeagueCommonMarkParser::class);

This is also how you keep the third-party dependency behind an application-owned contract, so swapping to league/commonmark directly (or another renderer) is a one-class change.

Confine rendering to a Blade component. A <x-markdown :text="$comment->body" /> component keeps the one {!! !!} you audit in a single place, rather than scattered across views. Never echo raw parser output without sanitization (see Gotchas).

Traverse the tree — this is where the package earns its keep. Write a small recursive walker that applies rules per node type:

  • Link pass — detect external hosts, set rel/target, rewrite youtube.com/watch to embeds, strip tracking query strings.
  • Image pass — add loading="lazy" and decoding="async", rewrite relative paths to your CDN/asset URL.
  • Heading pass — collect (level, text, anchor) tuples while walking to emit a TOC array next to the rendered HTML.
  • Excerpt pass — take the first N block nodes of plain text for list pages, instead of strip_tags(Str::limit(Str::markdown(...))).

Extend the parser, not the DOM. Add GFM tables, footnotes, autolinks, or custom syntax through league/commonmark extensions and environment config, then convert. Resist post-processing HTML for things the parser can already express as nodes — you lose the structural guarantees you adopted the package for.

Render multiple formats from one parse. Because you have a tree, you can emit HTML, plain text (for search indexing or excerpt meta), and a structured payload (for JSON-LD or a client-side renderer) from the same traversal. That "parse once, render N times" pattern is the strongest argument over Str::markdown().

Test the tree, not the string. Assert on nodes (assertInstanceOf(Link::class, ...), assertEquals('nofollow', $node->getAttribute('rel'))) instead of assertStringContainsString against rendered HTML. Keep a small golden-file suite for byte-level HTML output if that output is contractual.

When to reach for it. If all you need is HTML from Markdown, Str::markdown() (Laravel 9+) is one line and already wraps league/commonmark. Use markdown-dom when you need structure: TOC generation, link rewriting, content analysis, semantic truncation, or multiple output formats.

Gotchas and Tips

The main package alone has no parser. markdown-dom is the DOM abstraction; a fresh install that "does nothing" is almost always a missing adapter (markdown-dom-league-common) or an unbound MarkdownParser interface. Install both, bind the interface, and read the adapter's constructor for config options.

Upgrade to v0.2.1 if you're on 0.2.0. v0.2.1 is a single-commit patch correcting an off-by-one error in the package's bundled custom autoloader (PR #18). No API changes. The failure mode of the bug is intermittent "class not found" errors that look like Composer/cache problems and can burn an afternoon. Run composer update prinsfrank/markdown-dom followed by composer dump-autoload. If you vendored or copied any package classes, re-check them against 0.2.1.

Sanitization is not included. Markdown permits raw HTML depending on the CommonMark html_input setting (strip, escape, or allow), and markdown-dom faithfully represents whatever the parser produces — it does not clean it. For user-generated content, set html_input => 'strip' and allow_unsafe_links => false on the CommonMark environment and run the output through a sanitizer (HTMLPurifier or a DOM whitelist) as defense in depth. Do not infer safety from the parser's name.

Pin the version. This is pre-1.0 (0.2.x), single-maintainer, MIT, and 0.x SemVer permits breaking changes in minor bumps. Pin exactly (0.2.1) rather than ^0.2 unless you're prepared for surprise breaks. Watch for a 1.0 or a stated BC policy before treating it as infrastructure.

The custom autoloader is a maturity flag. The v0.2.1 fix suggests the package still carries hand-rolled class-loading logic. Verify via composer show that you're loading it through Composer's PSR-4 autoloader, not a package-bundled autoload path, and keep an eye on future autoloader-related patches.

Performance scales with how often you parse. DOM construction and serialization are heavier in CPU and memory than string rendering. Irrelevant if you parse on write with cached HTML; relevant if you render user content on every request. Cache aggressively and prefer pre-rendered columns.

Markdown-flavor fidelity is unverified. CommonMark compliance and extension coverage (GFM tables, footnotes, strikethrough, task lists) depend on the adapter's configuration, not on markdown-dom itself. Build a golden-file test over 50–100 real documents from your own corpus and diff against your current renderer before switching a surface over.

Plan a fail-open path. Malformed or edge-case input can throw or produce a partial DOM. Wrap parsing in a try/catch that falls back to your existing renderer (e.g. Str::markdown()) so a single bad comment doesn't take down a page.

Don't regex the rendered HTML. If you find yourself needing to modify the output, add a node visitor instead — that's the whole reason to adopt a DOM-based pipeline. Regex-on-HTML is the pattern this package exists to retire.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator