tempest/markdown
A fast, extensible server-side Markdown parser for PHP that uses a simple lexer instead of regex. Built-in support for code highlighting, tables, divs, responsive images, and frontmatter, with benchmarks well ahead of commonmark and Parsedown Extra.
Architecture fit. tempest/markdown is a framework-agnostic, near-zero-dependency PHP library — not a Laravel package. There is no service provider, facade, config file, publishable assets, or Blade directive. Consumption is new Markdown() → parse() → a result object exposing html and frontmatter. That neutrality is an asset (it can live in a domain service, an Artisan command, a queue worker, or a CI static-site step without dragging framework coupling along) and a cost (every Laravel-shaped convenience — singleton binding, Blade directive, config, HTML caching — is ~30–50 lines we write and own). Treat the adapter as a known line item, not a surprise.
Integration feasibility. High. One Composer dependency, no external services, no Node toolchain, no build step. The surface area is a single class plus a result object, which makes a wrapper-and-swap strategy cheap. The main feasibility questions are not "can we call it" but "does its output match what our templates, CSS, and downstream consumers expect."
Technical risk.
maxNestingDepth. Good that the knobs exist; the implication is that this class of bug has occurred. Markdown → HTML with untrusted input is an XSS surface regardless of parser, so budget an HTML sanitizer and our own adversarial test corpus as part of the integration, not as a later hardening task.parse() result shape and frontmatter semantics could shift. Pin versions and treat upgrades as reviewed changes.league/commonmark on the Tempest docs corpus) is vendor-run on a vendor corpus. Directionally credible given the lexer-vs-regex design, but must be re-measured on our content, our PHP version, and our runtime (FPM vs queue worker vs serverless).Key questions.
Stack fit. Laravel (10/11/12) on modern PHP — verify the package's PHP constraint against our runtime before anything else; the 1.0.0 changelog includes a "Fix PHP version" entry, so confirm from composer.json rather than the README. Because the package pulls in almost nothing, dependency-conflict risk with the existing composer.lock is low. It does not integrate with Blade, Eloquent, queues, or caching on its own; all of that is our adapter's job.
Migration path.
MarkdownRenderer interface (render(string $markdown): ParsedDocument exposing html and frontmatter). Ship two implementations: incumbent (league/commonmark or current renderer) and TempestMarkdownRenderer. Nothing in application code should reference Tempest\Markdown\Markdown directly.javascript: schemes) through the new renderer and through the sanitizer. No passing the gate without sanitizer coverage.maxNestingDepth limits. Compare against the incumbent on identical hardware.Compatibility considerations. Frontmatter is parsed by the package, not by a separate YAML step — validate and default every key our templates read, because malformed frontmatter will surface as template errors rather than parse errors. Manual header IDs are supported (1.2.0), which matters for anchor stability and deep links. Custom syntax (mentions, callouts, embeds) is addable via the lexer's rule set, but every custom rule we add is an upstream-divergence we now maintain.
Sequencing. One sprint: adapter + shadow diff + security corpus + benchmark, ending in a written go/no-go. Do not start with user-generated content. Pin the exact version in composer.json and commit the lock file at every step so rollback is a one-line revert.
Maintenance. We own the adapter, the sanitizer configuration, the caching layer, and the upgrade cadence. Upstream is a small team shipping frequently and fixing real issues — healthy, but not a contract. Assign a named owner and a monthly release-review ritual; with a pinned version and a parity test in CI, upgrades are low-drama.
Support. No vendor, no SLA, no paid tier. Support is GitHub issues, and escalation realistically means "we write the fix and open the PR." Budget a small contingency (a few engineer-days per quarter) for upstream contributions and for keeping our fork-ability intact (i.e., keeping our changes in the adapter, not patched into vendor/).
Scaling. Parsing is CPU-bound and near-linear in input size; the memory advantage is the more operationally interesting number because it improves density on queue workers, container memory limits, and serverless PHP runtimes. For high-traffic public pages, do not parse per request — cache rendered HTML keyed by content hash and renderer version, and invalidate on content change. For moderate traffic, per-request rendering at ~10ms for a docs-sized corpus is defensible. Always set maxNestingDepth and an input-size cap on any endpoint accepting user content.
Failure modes and mitigations.
maxNestingDepth, request size limits, timeouts, and rate limiting on any user-facing render endpoint.Ramp-up. Low for a PHP developer: half a day to read the docs and render a file, two to three days to build the adapter, shadow diff, and test scaffolding. The real learning curve is not the API — it is edge-case behavior and the taxonomy of acceptable vs. unacceptable output differences, which is exactly what the spike is designed to surface. Document our custom rules and our diff-triage decisions in the repo so the next engineer inherits judgment, not archaeology.
Recommendation. Fund a one-sprint pilot with a written exit criterion, gated on the parity diff, sanitizer coverage, and measured performance in our runtime. Treat this as a high-upside, low-switching-cost adoption — not a platform bet.
How can I help you explore Laravel packages today?