PHPackages                             birdcar/markdown-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. birdcar/markdown-php

ActiveLibrary[Templating &amp; Views](/categories/templating)

birdcar/markdown-php
====================

Birdcar Flavored Markdown (BFM) — league/commonmark extension suite

v0.1.0(1mo ago)051MITPHPPHP ^8.2CI passing

Since May 29Pushed 1mo agoCompare

[ Source](https://github.com/birdcar/markdown-php)[ Packagist](https://packagist.org/packages/birdcar/markdown-php)[ RSS](/packages/birdcar-markdown-php/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (4)Versions (3)Used By (1)

birdcar/markdown-php
====================

[](#birdcarmarkdown-php)

A [league/commonmark](https://commonmark.thephpleague.com) extension suite for **Birdcar Flavored Markdown** (BFM) — a superset of CommonMark and GFM that adds YAML front-matter, directive blocks, extended task lists, task modifiers, mentions, hashtags, metadata extraction, and document merging.

See the [BFM spec](https://github.com/birdcar/markdown-spec) for the full syntax definition.

Packages
--------

[](#packages)

This is a monorepo containing the core library and framework integration packages:

PackageDescriptionInstall`birdcar/markdown-php`Core library (this repo root)`composer require birdcar/markdown-php`[`birdcar/markdown-laravel`](packages/laravel/)Laravel service provider, `Str::bfm()` macro, `@bfmStyles` directive`composer require birdcar/markdown-laravel`[`birdcar/markdown-filament`](packages/filament/)Filament v4 `BfmEditor`, `BfmTextColumn`, `BfmTextEntry``composer require birdcar/markdown-filament`Requirements
------------

[](#requirements)

- PHP 8.2+
- league/commonmark ^2.7
- symfony/yaml ^8.0

Quick Start
-----------

[](#quick-start)

### Core library (any PHP project)

[](#core-library-any-php-project)

```
use Birdcar\Markdown\Environment\BfmEnvironmentFactory;
use League\CommonMark\MarkdownConverter;

$environment = BfmEnvironmentFactory::create();
$converter = new MarkdownConverter($environment);

echo $converter->convert(] Call dentist //due:2025-03-01');

// Inline rendering (no wrapping  tags)
$html = Str::inlineBfm('Hey @sarah');

// Access the configured converter directly
$converter = app(\League\CommonMark\MarkdownConverter::class);
```

In Blade templates:

```
{{-- Include BFM default styles --}}
@bfmStyles

{{-- Render content --}}

    {!! Str::bfm($post->body) !!}

```

Publish the config to customize the render profile or bind resolver classes:

```
php artisan vendor:publish --tag=bfm-config
```

```
// config/bfm.php
return [
    'profile' => 'html',  // 'html', 'email', or 'plain'
    'resolvers' => [
        'mention' => \App\Markdown\UserMentionResolver::class,
        'embed'   => \App\Markdown\OEmbedResolver::class,
    ],
];
```

### Filament v4

[](#filament-v4)

```
composer require birdcar/markdown-filament
```

Drop-in replacement for Filament's `MarkdownEditor` with server-side BFM preview:

```
use Birdcar\Markdown\Filament\Forms\Components\BfmEditor;
use Birdcar\Markdown\Filament\Tables\Columns\BfmTextColumn;
use Birdcar\Markdown\Filament\Infolists\Components\BfmTextEntry;

// Form field with live BFM preview
BfmEditor::make('content')
    ->previewDebounce(300);

// Table column that renders BFM
BfmTextColumn::make('content');

// Infolist entry that renders BFM
BfmTextEntry::make('content');
```

The `BfmEditor` provides a preview toggle button that renders BFM syntax server-side via Filament v4's `callSchemaComponentMethod` — no traits or page-level configuration needed.

The Factory
-----------

[](#the-factory)

`BfmEnvironmentFactory::create()` returns a fully configured `Environment` with CommonMark, GFM (minus task lists, which BFM replaces), and all BFM extensions:

```
use Birdcar\Markdown\Environment\BfmEnvironmentFactory;
use Birdcar\Markdown\Environment\RenderProfile;

$environment = BfmEnvironmentFactory::create(
    profile: RenderProfile::Html,           // Html (default), Email, or Plain
    embedResolver: $myEmbedResolver,        // optional — implements EmbedResolverInterface
    mentionResolver: $myMentionResolver,    // optional — implements MentionResolverInterface
    includeResolver: $myIncludeResolver,    // optional — implements IncludeResolverInterface
    queryResolver: $myQueryResolver,        // optional — implements QueryResolverInterface
    config: [],                             // additional league/commonmark config
);
```

### Individual extensions

[](#individual-extensions)

Each feature is a self-contained extension:

```
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\MarkdownConverter;
use Birdcar\Markdown\Inline\Task\TaskExtension;
use Birdcar\Markdown\Inline\TaskModifier\TaskModifierExtension;

$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TaskExtension());
$environment->addExtension(new TaskModifierExtension());

$converter = new MarkdownConverter($environment);
echo $converter->convert('- [>] Call dentist //due:2025-03-01');
```

ClassDescription`FrontmatterExtension`YAML front-matter (`---` blocks)`TaskExtension``[x]`, `[>]`, `[!]`, etc. in list items`TaskModifierExtension``//due:2025-03-01`, `//hard` metadata`MentionExtension``@username` inline references`HashtagExtension``#project` inline tags`CalloutExtension``@callout`/`@endcallout` container blocks`EmbedExtension``@embed`/`@endembed` leaf blocks`DetailsExtension``@details`/`@enddetails` collapsible sections`TabsExtension``@tabs`/`@tab` tabbed content groups`FigureExtension``@figure`/`@endfigure` images with captions`AsideExtension``@aside`/`@endaside` sidebar content`MathExtension``@math`/`@endmath` LaTeX display blocks`TocExtension``@toc`/`@endtoc` auto table of contents`IncludeExtension``@include`/`@endinclude` file transclusion`QueryExtension``@query`/`@endquery` dynamic content`EndnotesExtension``@endnotes`/`@endendnotes` footnote rendering`FootnoteExtension``[^label]` references and `[^label]:` definitionsResolvers
---------

[](#resolvers)

Mentions and embeds can be resolved at render time by implementing the contract interfaces.

**MentionResolverInterface:**

```
use Birdcar\Markdown\Contracts\MentionResolverInterface;

class UserMentionResolver implements MentionResolverInterface
{
    public function resolve(string $identifier): ?array
    {
        $user = User::where('username', $identifier)->first();

        return $user ? [
            'label' => $user->display_name,
            'url' => route('profile.show', $user),
        ] : null;
    }
}
```

Without a resolver, mentions render as `@identifier`. With a resolver that returns a URL, they render as `@label`.

**EmbedResolverInterface:**

```
use Birdcar\Markdown\Contracts\EmbedResolverInterface;

class OEmbedResolver implements EmbedResolverInterface
{
    public function resolve(string $url): ?array
    {
        $response = Http::get('https://noembed.com/embed', ['url' => $url]);

        return $response->successful() ? $response->json() : null;
    }
}
```

Without a resolver, embeds render as a `` with a plain link. With a resolver that returns `html`, the resolved HTML is embedded directly.

**IncludeResolverInterface:**

Resolve file transclusion for `@include` directives:

```
use Birdcar\Markdown\Contracts\IncludeResolverInterface;

class FileIncludeResolver implements IncludeResolverInterface
{
    public function resolve(string $src, ?string $type, ?string $lines): ?string
    {
        $path = base_path($src);
        return file_exists($path) ? file_get_contents($path) : null;
    }
}
```

Without a resolver, `@include` renders as a placeholder `` with data attributes.

**QueryResolverInterface:**

Resolve dynamic content for `@query` directives:

```
use Birdcar\Markdown\Contracts\QueryResolverInterface;
use League\CommonMark\Node\Block\Document;

class TaskQueryResolver implements QueryResolverInterface
{
    public function resolve(array $params, Document $document): array
    {
        return Task::query()
            ->when($params['state'] ?? null, fn ($q, $state) => $q->where('state', $state))
            ->when($params['tag'] ?? null, fn ($q, $tag) => $q->withTag($tag))
            ->limit($params['limit'] ?? 10)
            ->get()
            ->toArray();
    }
}
```

Without a resolver, `@query` renders as a placeholder `` with data attributes.

**ComputedFieldResolverInterface:**

Extend metadata extraction with custom computed fields:

```
use Birdcar\Markdown\Contracts\ComputedFieldResolverInterface;
use League\CommonMark\Node\Block\Document;

class ReadabilityResolver implements ComputedFieldResolverInterface
{
    public function resolve(Document $document, array $frontmatter, array $builtins): array
    {
        return [
            'isLongRead' => $builtins['wordCount'] > 1000,
        ];
    }
}
```

Metadata Extraction
-------------------

[](#metadata-extraction)

Extract structured metadata from parsed documents:

```
use Birdcar\Markdown\Environment\BfmEnvironmentFactory;
use Birdcar\Markdown\Metadata\MetadataExtractor;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Parser\MarkdownParser;

$environment = BfmEnvironmentFactory::create();
$parser = new MarkdownParser($environment);

$document = $parser->parse( 'My Post', 'tags' => ['bfm']]
$meta->computed['wordCount']; // 9
$meta->computed['readingTime']; // 1
$meta->computed['tags'];      // ['bfm', 'typescript']
$meta->computed['tasks'];     // TaskCollection with ->open, ->done, ->all, etc.
$meta->computed['links'];     // [LinkReference { url, title, line }]
```

Custom computed fields via resolvers:

```
$extractor = new MetadataExtractor(
    resolvers: [new ReadabilityResolver()],
);

$meta = $extractor->extract($document);
$meta->custom['isLongRead']; // false
```

Document Merging
----------------

[](#document-merging)

Deep-merge front-matter and concatenate body content across multiple documents:

```
use Birdcar\Markdown\Merge\BfmDocument;
use Birdcar\Markdown\Merge\DocumentMerger;
use Birdcar\Markdown\Merge\MergeOptions;
use Birdcar\Markdown\Merge\MergeStrategy;

$a = new BfmDocument(frontmatter: ['tags' => ['a']], body: 'Content A');
$b = new BfmDocument(frontmatter: ['tags' => ['b'], 'title' => 'B'], body: 'Content B');

$merger = new DocumentMerger();
$merged = $merger->merge([$a, $b]);
// $merged->frontmatter = ['tags' => ['a', 'b'], 'title' => 'B']
// $merged->body = "Content A\n\nContent B"

// Configurable strategies
$merger->merge([$a, $b], new MergeOptions(strategy: MergeStrategy::FirstWins));
$merger->merge([$a, $b], new MergeOptions(strategy: MergeStrategy::Error));  // throws on scalar conflicts
$merger->merge([$a, $b], new MergeOptions(
    strategy: fn (string $key, mixed $existing, mixed $incoming) => $existing . $incoming,
));
$merger->merge([$a, $b], new MergeOptions(separator: "\n---\n"));  // custom body separator
```

Styling
-------

[](#styling)

The `@bfmStyles` Blade directive (from `birdcar/markdown-laravel`) outputs a default stylesheet covering all BFM output elements. The stylesheet uses CSS custom properties for theming and supports both `prefers-color-scheme: dark` and class-based dark mode (`.dark`).

To publish the stylesheet for customization:

```
php artisan vendor:publish --tag=bfm-assets
```

Override any variable in your own CSS:

```
:root {
  --bfm-task-priority: #b91c1c;
  --bfm-mention-bg: #fce7f3;
  --bfm-mention-text: #9d174d;
}
```

The Filament package automatically loads BFM styles into the admin panel.

Syntax Reference
----------------

[](#syntax-reference)

### YAML Front-matter

[](#yaml-front-matter)

```
---
title: My Document
tags:
  - bfm
  - markdown
author:
  name: Nick
  email: nick@birdcar.dev
---

Document content starts here.
```

Front-matter must appear at the very start of the document. The YAML content is parsed and available via `FrontmatterBlock::getParsedData()`.

### Extended Task Lists

[](#extended-task-lists)

Seven states, inspired by Bullet Journal:

```
- [ ] Open task
- [x] Completed
- [>] Scheduled for later
- [] Call dentist //due:2025-03-01
- [ ] Weekly review //every:weekly
- [o] Team retro //due:2025-02-07 //every:2-weeks
- [ ] Run backups //cron:0 9 * * 1
- [!] File taxes //due:2025-04-15 //hard
- [>] Wait for response //wait
```

### Mentions

[](#mentions)

```
Hey @sarah, can you review this? Also cc @john.doe and @dev-team.
```

### Hashtags

[](#hashtags)

```
Discussing #typescript and #react-hooks in this post.
```

Identifiers follow the pattern `[a-zA-Z][a-zA-Z0-9_-]*`. The `#` must not be preceded by an alphanumeric character. Hashtags inside code spans are not parsed.

### Directive Blocks

[](#directive-blocks)

**Callouts** (container — body is parsed as markdown):

```
@callout type=warning title="Watch Out"
This is a warning with **bold** text and [links](https://example.com).
@endcallout
```

**Embeds** (leaf — body is treated as caption text):

```
@embed https://www.youtube.com/watch?v=dQw4w9WgXcQ
A classic internet moment.
@endembed
```

**Details** (container — collapsible section):

```
@details summary="Click to expand" open
Hidden content with **markdown** support.
@enddetails
```

**Tabs** (container — tabbed content groups):

```
@tabs
@tab label="JavaScript" active
console.log('hello')
@endtab
@tab label="Python"
print('hello')
@endtab
@endtabs
```

**Figure** (container — image with caption):

```
@figure src="photo.jpg" alt="A photo" id="fig-1"
Caption text with **markdown**.
@endfigure
```

**Aside** (container — sidebar content):

```
@aside title="Fun Fact"
Something tangential but interesting.
@endaside
```

**TOC** (leaf — auto-generated table of contents):

```
@toc depth=2 ordered
@endtoc
```

**Math** (leaf — LaTeX display block):

```
@math label="eq-1"
E = mc^2
@endmath
```

**Include**, **Query**, **Endnotes** (leaf — resolver-dependent or structural):

```
@include src="./snippets/example.md" type=markdown
@endinclude

@query state=open tag=engineering limit=5
@endquery

@endnotes title="References"
@endendnotes
```

### Footnotes

[](#footnotes)

Pandoc-style footnote references and definitions:

```
Some text with a footnote[^1] and another[^note].

[^1]: First footnote content.
[^note]: Named footnote with longer content
    that continues on indented lines.
```

Footnotes are auto-numbered in order of first reference. If no `@endnotes` directive is present, the endnotes section is appended at the end of the document.

HTML Output
-----------

[](#html-output)

The extensions produce semantic, BEM-style HTML:

```

      ▷

    Call dentist
    //due:2025-03-01

@Sarah Chen

  Watch Out
  Content with markdown.

  https://example.com/video
  A great video.

```

License
-------

[](#license)

MIT

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance89

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity37

Early-stage or recently created project

 Bus Factor1

Top contributor holds 96.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

56d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/434063?v=4)[Nick Cannariato](/maintainers/birdcar)[@birdcar](https://github.com/birdcar)

---

Top Contributors

[![birdcar](https://avatars.githubusercontent.com/u/434063?v=4)](https://github.com/birdcar "birdcar (29 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/birdcar-markdown-php/health.svg)

```
[![Health](https://phpackages.com/badges/birdcar-markdown-php/health.svg)](https://phpackages.com/packages/birdcar-markdown-php)
```

###  Alternatives

[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k16](/packages/tempest-framework)[statamic/cms

The Statamic CMS Core Package

4.8k3.6M1.0k](/packages/statamic-cms)[craftcms/cms

Craft CMS

3.6k3.6M3.2k](/packages/craftcms-cms)[tightenco/jigsaw

Simple static sites with Laravel's Blade.

2.3k453.6k30](/packages/tightenco-jigsaw)[jason-munro/cypht

Lightweight Open Source webmail written in PHP and JavaScript

1.6k157.9k](/packages/jason-munro-cypht)[contao/core-bundle

Contao Open Source CMS

1231.6M2.8k](/packages/contao-core-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
