PHPackages                             amondar-libs/php-markdown - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. amondar-libs/php-markdown

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

amondar-libs/php-markdown
=========================

Zero dependency markdown builder for PHP.

2.0.0(2mo ago)1586↓45.7%1MITPHPPHP ^8.2

Since Nov 2Pushed 2mo agoCompare

[ Source](https://github.com/amondar-libs/php-markdown)[ Packagist](https://packagist.org/packages/amondar-libs/php-markdown)[ RSS](/packages/amondar-libs-php-markdown/feed)WikiDiscussions master Synced 1mo ago

READMEChangelogDependencies (8)Versions (12)Used By (1)

PHP Markdown Builder
====================

[](#php-markdown-builder)

Zero-dependency, fluent Markdown builder for PHP 8.2+.

- Tiny API, no runtime dependencies
- Fluent chaining to compose documents
- Supports headings, paragraphs, ordered and unordered lists (with simple nesting), quotes, fenced code blocks, links, images, and raw markdown
- Customizable newline and indentation characters
- Around 9-10ms to render 10k markdown lines

Installation
------------

[](#installation)

Install via Composer:

```
composer require amondar-libs/php-markdown
```

Requirements:

- PHP 8.2+

Quick start
-----------

[](#quick-start)

```
use Amondar\Markdown\Markdown;
use Amondar\Markdown\MarkdownHeading;

$md = Markdown::make()
    ->heading('Hello World')
    ->line('This is a paragraph.')
    ->link('https://example.com', 'Example')
    ->toString();

// Result:
// # Hello World
//
// This is a paragraph.
//
// [Example](https://example.com)
```

API and examples
----------------

[](#api-and-examples)

Below, most examples mirror the test suite to ensure accuracy.

### Create an instance

[](#create-an-instance)

```
$md = Amondar\Markdown\Markdown::make();
```

You can convert to a string using either `toString()` or implicit casting via `__toString()`.

```
(string) $md;      // calls __toString()
$md->toString();   // explicit
```

### Headings

[](#headings)

```
use Amondar\Markdown\Markdown;
use Amondar\Markdown\MarkdownHeading;

Markdown::make()->heading('Test Heading')->toString();
// "# Test Heading"

Markdown::make()->heading('Test Heading', MarkdownHeading::H2)->toString();
// "## Test Heading"

Markdown::make()->heading('Test Heading', MarkdownHeading::H3)->toString();
// "### Test Heading"
```

### Paragraph line (with optional prefix)

[](#paragraph-line-with-optional-prefix)

```
Markdown::make()->line('Test Line')->toString();
// "Test Line"

Markdown::make()->line('Test Line', '> ')->toString();
// "> Test Line"
```

Notes:

- Empty strings and nulls are ignored by the builder (no output for that call).

### Ordered (numeric) lists

[](#ordered-numeric-lists)

Simple list:

```
$list = [
    'Item 1',
    'Item 2',
    'Item 3',
];

Markdown::make()->numericList($list)->toString();
/*
1. Item 1
2. Item 2
3. Item 3
*/
```

Keyed + value description (common pattern):

```
$list = [
    '**Item 1**' => 'Description',
    'Item 2',
    'Item 3',
];

Markdown::make()->numericList($list)->toString();
/*
1. **Item 1** - Description
2. Item 2
3. Item 3
*/
```

Nested style (category with description + sub-items):

```
$complex = [
    '**Category 1**' => [
        'Description',
        'Sub-item 1',
        'Sub-item 2',
    ],
    '**Category 2**' => [
        'Description',
        'Sub-item 1',
        'Sub-item 2',
    ],
];

Markdown::make()->numericList($complex)->toString();
/*
1. **Category 1** - Description
   - Sub-item 1
   - Sub-item 2
2. **Category 2** - Description
   - Sub-item 1
   - Sub-item 2
*/
```

> You can use `Description` as empty string or NULL to omit the description.

### Unordered (bullet) lists

[](#unordered-bullet-lists)

Simple list:

```
$list = [
    'Item 1',
    'Item 2',
    'Item 3',
];

Markdown::make()->list($list)->toString();
/*
- Item 1
- Item 2
- Item 3
*/
```

Keyed + value description:

```
$list = [
    '**Item 1**' => 'Description',
    'Item 2',
    'Item 3',
];

Markdown::make()->list($list)->toString();
/*
- **Item 1** - Description
- Item 2
- Item 3
*/
```

Nested style:

```
$complex = [
    '**Category 1**' => [
        'Description',
        'Sub-item 1',
        'Sub-item 2',
    ],
    'Category 2' => [
        'Description',
        'Sub-item 1',
        'Sub-item 2',
    ],
];

Markdown::make()->list($complex)->toString();
/*
- **Category 1** - Description
   - Sub-item 1
   - Sub-item 2
- Category 2 - Description
   - Sub-item 1
   - Sub-item 2
*/
```

> You can use `Description` as empty string or NULL to omit the description.

### Quotes

[](#quotes)

```
Markdown::make()->quote('Test Quote')->toString();
// "> Test Quote"

Markdown::make()->quote([
    'Quote Line 1',
    'Quote Line 2',
    'Quote Line 3',
])->toString();
/*
> Quote Line 1
> Quote Line 2
> Quote Line 3
*/
```

### Fenced code blocks

[](#fenced-code-blocks)

```
$code = toString();

/*
    ```php
    function test() {
        return true;
    }
    ```
*/

Markdown::make()->block('SELECT * FROM users;', 'sql')->toString();

/*
    ```sql
    SELECT * FROM users;
    ```
*/
```

### Links

[](#links)

```
Markdown::make()->link('https://example.com')->toString();
// "[https://example.com](https://example.com)"

Markdown::make()->link('https://example.com', 'Example')->toString();
// "[Example](https://example.com)"
```

### Images

[](#images)

```
Markdown::make()->image('https://example.com/image.png')->toString();
// "![](https://example.com/image.png)"

Markdown::make()->image(
    'https://example.com/image.png',
    'Title Text',
    'Alt title'
)->toString();
// "![Title Text](https://example.com/image.png \"Alt title\")"
```

### Tables

[](#tables)

```
$result = Markdown::make()->table(
        ['Header 1', 'Header 2', 'Header 3'],
        [
            ['Value 1', 'Value 2', 'Value 3'],
            ['Value 4', 'Value 5', 'Value 6'],
        ],
    )->toString();

/*
| Header 1 | Header 2 | Header 3 |
| --- | --- | --- |
| Value 1 | Value 2 | Value 3 |
| Value 4 | Value 5 | Value 6 |
*/
```

### Raw markdown passthrough

[](#raw-markdown-passthrough)

```
Markdown::make()->raw('#### Raw content')->toString();
// "#### Raw content"
```

> You can also put a `Markdown` class instance as "raw" content.

### Method chaining

[](#method-chaining)

```
$result = Markdown::make()
    ->heading('Title')
    ->line('Content')
    ->link('https://example.com', 'Example')
    ->toString();

/*
# Title

Content

[Example](https://example.com)
*/
```

You can also suppress Markdown output to stop automatic adding additional new line after each "block":

```
$result = Markdown::make()
        ->startSuppressing()
        ->heading('Title')
        ->line('Content')
        ->break()
        ->link('https://example.com', 'Example')
        ->endSuppressing()
        ->heading('Title 2')
        ->line('Content 2')
        ->toString();

/*
# Title
Content

[Example](https://example.com)

# Title 2

Content 2
*/

//This usage is the same as above
$result = Markdown::make()
        ->suppress(
            fn($markdown) => $markdown->heading('Title')
                                      ->line('Content')
                                      ->break()
                                      ->link('https://example.com', 'Example')
        )
        ->heading('Title 2')
        ->line('Content 2')
        ->toString();
```

### Conditioning

[](#conditioning)

You can apply class conditional extension:

```
$result = Markdown::make(escaper: Escaper::make(['.']))
        ->heading('Title')
        ->line('Content')
        ->link('https://example.com', 'Example')
        ->when(false, fn(Markdown $markdown) => $markdown->line('This line should not be added.'))
        ->toString();

/*
# Title

Content

[Example](https://example.com)
*/

$result = Markdown::make(escaper: Escaper::make(['.']))
        ->heading('Title')
        ->line('Content')
        ->link('https://example.com', 'Example')
        ->when(true, fn(Markdown $markdown) => $markdown->line('This line should be added.'))
        ->toString();

/*
# Title

Content

[Example](https://example.com)

This line should be added\.
*/

$result = Markdown::make(escaper: Escaper::make(['.']))
        ->heading('Title')
        ->line('Content')
        ->link('https://example.com', 'Example')
        ->when(
            fn() => 'This line should be added too.',
            fn(Markdown $markdown, $line) => $markdown->suppress(
                fn($m) => $m->break()->line('This line should not be added.')->line($line)
            )
        )
        ->toString();

/*
# Title

Content

[Example](https://example.com)

This line should not be added\.
This line should be added too\.
*/
```

### Markdown V2 autoescape

[](#markdown-v2-autoescape)

When you are using the package in Markdown V2 mode (for example in `Telegram` messages), you can use the autoescaping feature. Just pass an array of characters, that should be escaped in your text during Markdown building:

```
// complex list with nested items
$complexList = [
    '**Category 1.**' => [
        'Description.',
        'Sub-item 1',
        'Sub-item 2',
    ],
    'Category 2' => [
        'Description',
        'Sub-item 1',
        'Sub-item 2.',
    ],
];

$result = Markdown::make(escaper: \Amondar\Markdown\Escaper::makeForV2())->list($complexList)->toString();

/*
- **Category 1\.** \- Description\.
   - Sub\-item 1
   - Sub\-item 2
- Category 2 \- Description
   - Sub\-item 1
   - Sub\-item 2\.
*/
```

> As you can see, it is still possible to use Markdown syntax in your text, but all characters that are in the list will be escaped. If you want to use *Markdown* syntax characters as *none-Markdown*, you should escape them by yourself.

### Bindings (`{{?}}` placeholders)

[](#bindings--placeholders)

When using the escaper, you often have **static template text** mixed with **dynamic user data**. Bindings let you keep the template unescaped while safely escaping only the dynamic parts. Use `{{?}}` as a placeholder and pass the replacement values via the `bindings` parameter:

```
use Amondar\Markdown\Markdown;
use Amondar\Markdown\Escaper;

// Simple line with bindings
$result = Markdown::make(escaper: Escaper::makeForV2())
    ->line(
        'Test Line with bindings: {{?}}, {{?}}, {{?}}...',
        bindings: ['A_D', 'D_A']
    )
    ->toString();

// "Test Line with bindings: A\_D, D\_A, \{\{?\}\}..."
// Note: unused placeholders are escaped as literal text.
```

Bindings are supported in most builder methods: `heading`, `line`, `paragraph`, `list`, `numericList`, `quote`, `link`, and `table`.

#### Heading with bindings

[](#heading-with-bindings)

```
$result = Markdown::make(escaper: Escaper::makeForV2())
    ->heading(
        'Report for _{{?}}_',
        bindings: ['user.name_test']
    )
    ->toString();

// "# Report for _user\.name\_test_"
```

#### Nested list with bindings

[](#nested-list-with-bindings)

For lists with nested items, bindings are passed as a nested array — one entry per top-level item, and within each entry one sub-array per string in that item (description + sub-items):

```
$complexList = [
    '**Category 1**' => [
        'Description for {{?}}',
        'Sub-item {{?}} and {{?}}',
        'Sub-item {{?}}',
    ],
    '**Category 2**' => [
        'Description for {{?}} and {{?}}',
        'Sub-item {{?}}',
        'Sub-item {{?}}',
    ],
];

$result = Markdown::make(escaper: Escaper::makeForV2())->numericList($complexList, [
    [
        ['Me'],
        [1],
        [2],
    ],
    [
        ['Me', 'You'],
        [2, 3],
        [4],
    ],
])->toString();

/*
1\. **Category 1** \- Description for Me
   - Sub-item 1 and \{\{?\}\}
   - Sub-item 2
2\. **Category 2** \- Description for Me and You
   - Sub-item 2
   - Sub-item 4
*/
```

#### Real-world example: Telegram notification with bindings

[](#real-world-example-telegram-notification-with-bindings)

```
use Amondar\Markdown\Markdown;
use Amondar\Markdown\Escaper;
use Amondar\Markdown\MarkdownHeading;

// Imagine building a Telegram order confirmation message
// where product names and prices come from user/database input.
$orderItems = [
    ['name' => 'USB-C Cable (2m)', 'qty' => 2, 'price' => 9.99],
    ['name' => 'Wireless Mouse [Bluetooth]', 'qty' => 1, 'price' => 24.50],
];
$customerName = 'John_Doe';
$orderId = '#ORD-2024.001';

$md = Markdown::make(suppressed: true, escaper: Escaper::makeForV2())
        ->heading('Order Confirmation', MarkdownHeading::H3, bindings: [])
        ->break()
        ->line('**Customer:** {{?}}', bindings: [$customerName])
        ->line('**Order:** {{?}}', bindings: [$orderId])
        ->break()
        ->raw('**Items:**')
        ->list(
            array_map(fn($item) => '{{?}} × {{?}} — ${{?}}', $orderItems),
            array_map(fn($item) => [$item['name'], $item['qty'], $item['price']], $orderItems),
        )
        ->break()
        ->line('Thank you for your order!');

echo $md;

/*
### Order Confirmation

**Customer:** John\_Doe
**Order:** \#ORD\-2024\.001

**Items:**
- USB\-C Cable \(2m\) × 2 — $9\.99
- Wireless Mouse \[Bluetooth\] × 1 — $24\.5

Thank you for your order\!
*/
```

Customization
-------------

[](#customization)

You can customize the builder by passing custom newline and indentation characters as well as suppressing automatic newlines:

```
use Amondar\Markdown\Markdown;

// force LF newlines
// 4-space indentation
Markdown::make(nl: "\n", tab: "    ", suppressed: false);
```

Behavior and notes
------------------

[](#behavior-and-notes)

- Null or empty values are ignored. For example, `->line(null)` or `->heading('')` produce no output.
- `toString()` trims trailing newlines and extra spaces from the final output.
- `__toString()` proxies to `toString()` so you can echo the builder directly: `echo Markdown::make()->line('Hi');`.
- List rendering supports a simple two-level pattern: a keyed item may have a description (first element) and optional sub-items which are rendered as indented bullets. That can be helpful in documentation generation.

License
-------

[](#license)

MIT License. See [LICENSE.md](LICENSE.md).

###  Health Score

45

—

FairBetter than 93% of packages

Maintenance88

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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

Every ~13 days

Recently: every ~33 days

Total

11

Last Release

61d ago

Major Versions

1.3.5 → 2.0.02026-03-19

### Community

Maintainers

![](https://www.gravatar.com/avatar/00b2e99bd1ba72674cf4370c0ef6e8efabebdd257036e5ed14020b5b487772b5?d=identicon)[amondar-libs](/maintainers/amondar-libs)

---

Top Contributors

[![amondar-libs](https://avatars.githubusercontent.com/u/3830450?v=4)](https://github.com/amondar-libs "amondar-libs (15 commits)")

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

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

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

###  Alternatives

[ghunti/highcharts-php

A php wrapper for highcharts and highstock javascript libraries

3772.3M5](/packages/ghunti-highcharts-php)[kra8/laravel-snowflake

Snowflake for Laravel and Lumen.

188402.3k6](/packages/kra8-laravel-snowflake)[mageplaza/magento-2-blog-extension

Magento 2 Blog extension

123708.2k5](/packages/mageplaza-magento-2-blog-extension)[werneckbh/laravel-qr-code

QR Code Generator for PHP wrapper for Laravel

90587.8k](/packages/werneckbh-laravel-qr-code)[leth/ip-address

IPv4 and IPv6 address and subnet classes with awesome utility functions.

62716.6k3](/packages/leth-ip-address)[samdark/hydrator

Allows to extract data from an object or create a new object based on data for the purpose of persisting state. Works with private and protected properties.

11476.9k](/packages/samdark-hydrator)

PHPackages © 2026

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