Extensions provide a clean way to bundle related customizations together. Each extension can register inline patterns, block patterns, and render event listeners.
| Extension | Description |
|---|---|
| AutolinkExtension | Auto-links bare URLs and email addresses |
| DefaultAttributesExtension | Adds default attributes to elements by type |
| ExternalLinksExtension | Adds target="_blank" and rel attributes to external links |
| HeadingPermalinksExtension | Adds clickable anchor links to headings |
| MentionsExtension | Converts @username patterns to profile links |
| SemanticSpanExtension | Converts span attributes to semantic HTML elements (<kbd>, <dfn>, <abbr>) |
| SmartQuotesExtension | Configures locale-specific smart quote characters |
| TableOfContentsExtension | Generates a table of contents from headings |
| WikilinksExtension | Converts [[Page Name]] patterns to wiki-style links |
use Djot\DjotConverter;
use Djot\Extension\ExternalLinksExtension;
use Djot\Extension\MentionsExtension;
$converter = new DjotConverter();
// Chain multiple extensions
$converter
->addExtension(new ExternalLinksExtension())
->addExtension(new MentionsExtension());
$html = $converter->convert($djot);Extensions are applied in registration order. Generally, order doesn't matter, but there are some considerations:
- AutolinkExtension should be registered before ExternalLinksExtension if you want auto-linked URLs to also get external link attributes
- TableOfContentsExtension should be registered before HeadingPermalinksExtension if you want clean heading text in the TOC (without permalink symbols)
Adds target="_blank" and rel="noopener noreferrer" to external links (http/https URLs).
use Djot\Extension\ExternalLinksExtension;
// Default: all external links open in new tab
$converter->addExtension(new ExternalLinksExtension());
// Exclude your own domains
$converter->addExtension(new ExternalLinksExtension(
internalHosts: ['example.com', 'www.example.com'],
));
// Custom attributes
$converter->addExtension(new ExternalLinksExtension(
target: '_blank',
rel: 'noopener',
nofollow: true, // Adds 'nofollow' to rel
));Input:
Visit [Example](https://example.com) or [Home](/home).
Output:
<p>Visit <a href="https://example.com" target="_blank" rel="noopener noreferrer">Example</a> or <a href="/home">Home</a>.</p>Adds clickable permalink anchors to headings, useful for documentation sites.
use Djot\Extension\HeadingPermalinksExtension;
// Default: pilcrow symbol after heading
$converter->addExtension(new HeadingPermalinksExtension());
// Custom configuration
$converter->addExtension(new HeadingPermalinksExtension(
symbol: '#', // Link text
position: 'before', // 'before' or 'after'
cssClass: 'anchor', // CSS class for the link
ariaLabel: 'Link to section',
levels: [2, 3], // Only h2 and h3
));Input:
## Getting Started
Output:
<section id="Getting-Started">
<h2>Getting Started <span class="permalink-wrapper"><a href="#Getting-Started" class="permalink" aria-label="Permalink">¶</a></span></h2>
</section>Converts @username patterns into user profile links.
use Djot\Extension\MentionsExtension;
// Default: /users/view/{username}
$converter->addExtension(new MentionsExtension());
// Custom URL template
$converter->addExtension(new MentionsExtension(
urlTemplate: '/profile/{username}',
cssClass: 'user-mention',
));Input:
Thanks @johndoe for the help!
Output:
<p>Thanks <a href="/users/view/johndoe" data-username="johndoe" class="mention">@johndoe</a> for the help!</p>Converts spans with semantic attributes (kbd, dfn, abbr) into proper HTML5 semantic elements. This is useful for marking up keyboard shortcuts, definitions, and abbreviations.
use Djot\Extension\SemanticSpanExtension;
$converter->addExtension(new SemanticSpanExtension());Supported attributes:
| Attribute | HTML Element | Usage |
|---|---|---|
kbd |
<kbd> |
Keyboard input/shortcuts |
dfn |
<dfn> |
Definition of a term |
abbr |
<abbr> |
Abbreviation with title |
Keyboard shortcuts:
Press [Ctrl+C]{kbd} to copy and [Ctrl+V]{kbd} to paste.
<p>Press <kbd>Ctrl+C</kbd> to copy and <kbd>Ctrl+V</kbd> to paste.</p>Definitions:
A [variable]{dfn} is a named storage location.
The [API]{dfn="Application Programming Interface"} provides access to the system.
<p>A <dfn>variable</dfn> is a named storage location.</p>
<p>The <dfn title="Application Programming Interface">API</dfn> provides access to the system.</p>Abbreviations:
The [HTML]{abbr="HyperText Markup Language"} standard defines web content structure.
<p>The <abbr title="HyperText Markup Language">HTML</abbr> standard defines web content structure.</p>Combining attributes:
Attributes can be combined. The nesting order is: dfn wraps kbd wraps abbr.
[CSS]{dfn abbr="Cascading Style Sheets"}
<dfn><abbr title="Cascading Style Sheets">CSS</abbr></dfn>Preserving other attributes:
Other attributes (classes, IDs) are preserved in an outer span:
[Ctrl+S]{kbd .shortcut #save-shortcut}
<span class="shortcut" id="save-shortcut"><kbd>Ctrl+S</kbd></span>Note: This extension provides manual control via attributes. For automatic abbreviation expansion (define once, apply everywhere), use the built-in abbreviation definition syntax instead:
*[HTML]: HyperText Markup Language
The HTML specification defines...
Configures locale-specific smart quote characters. By default, the parser produces English-style typographic quotes ("…" '…'). This extension lets you change them per locale while keeping apostrophes as ' (U+2019) regardless of locale.
use Djot\Extension\SmartQuotesExtension;
// German: „…" ‚…'
$converter->addExtension(new SmartQuotesExtension(locale: 'de'));
// French: «…» ‹…›
$converter->addExtension(new SmartQuotesExtension(locale: 'fr'));
// Swiss German: «…» ‹…›
$converter->addExtension(new SmartQuotesExtension(locale: 'de-CH'));
// Explicit characters (override any locale)
$converter->addExtension(new SmartQuotesExtension(
openDoubleQuote: "\u{00AB}",
closeDoubleQuote: "\u{00BB}",
openSingleQuote: "\u{2039}",
closeSingleQuote: "\u{203A}",
));
// Mix: locale with partial overrides
$converter->addExtension(new SmartQuotesExtension(
locale: 'de',
openDoubleQuote: "\u{00AB}", // Override just double quotes
closeDoubleQuote: "\u{00BB}",
));Input (with locale: 'de'):
"Hallo," sagte sie. 'Es ist ein schöner Tag.'
Er antwortete: "Ich glaub's nicht."
Output:
<p>„Hallo," sagte sie. ‚Es ist ein schöner Tag.'</p>
<p>Er antwortete: „Ich glaub's nicht."</p>Note that the apostrophe in glaub's stays as ' (U+2019) — apostrophes are language-independent.
Supported locales: en, de, de-CH, fr, pl, ru, ja, zh, sv, da, fi, cs, hu, it, es, pt, nl, nb, nn, uk
Locale resolution: exact match → language-only fallback (e.g., de-AT → de) → English defaults. Underscore format is also accepted (e.g., fr_FR → fr).
Static helpers:
SmartQuotesExtension::getSupportedLocales(); // ['en', 'de', 'de-CH', ...]
SmartQuotesExtension::isLocaleSupported('de'); // true
SmartQuotesExtension::isLocaleSupported('de-AT'); // true (falls back to 'de')
SmartQuotesExtension::isLocaleSupported('xx'); // falseExtracts headings and generates a table of contents. The TOC is available after convert() is called.
use Djot\Extension\TableOfContentsExtension;
$tocExtension = new TableOfContentsExtension();
$converter->addExtension($tocExtension);
$html = $converter->convert($djot);
// Get TOC as HTML
$tocHtml = $tocExtension->getTocHtml();
// Or get raw data for custom rendering
$tocData = $tocExtension->getToc();
// Returns: [['level' => 1, 'text' => 'Intro', 'id' => 'Intro'], ...]Configuration:
$tocExtension = new TableOfContentsExtension(
minLevel: 2, // Start from h2
maxLevel: 4, // Up to h4
listType: 'ol', // 'ul' or 'ol'
cssClass: 'toc', // CSS class for nav element
position: 'top', // 'top', 'bottom', or null for manual placement
separator: '<hr>', // Optional HTML between TOC and content
);Auto-insertion:
// TOC automatically inserted at top of output
$converter->addExtension(new TableOfContentsExtension(position: 'top'));
$html = $converter->convert($djot); // TOC is included in $html
// Or at the bottom
$converter->addExtension(new TableOfContentsExtension(position: 'bottom'));
// With separator
$converter->addExtension(new TableOfContentsExtension(
position: 'top',
separator: '<hr>',
));
// Default: manual placement (position: null)
$tocExtension = new TableOfContentsExtension();
$converter->addExtension($tocExtension);
$html = $converter->convert($djot);
$toc = $tocExtension->getTocHtml(); // Place wherever you wantExample TOC output:
<nav class="toc">
<ul>
<li><a href="#Introduction">Introduction</a></li>
<li><a href="#Getting-Started">Getting Started</a>
<ul>
<li><a href="#Installation">Installation</a></li>
<li><a href="#Configuration">Configuration</a></li>
</ul>
</li>
</ul>
</nav>Helper methods:
$tocExtension->hasToc(); // bool - true if any headings found
$tocExtension->clear(); // Reset for reuse with another documentAutomatically converts bare URLs and email addresses into clickable links.
use Djot\Extension\AutolinkExtension;
// Default: http, https, and mailto
$converter->addExtension(new AutolinkExtension());
// Only https
$converter->addExtension(new AutolinkExtension(
allowedSchemes: ['https'],
));
// Disable email auto-linking
$converter->addExtension(new AutolinkExtension(
allowedSchemes: ['https', 'http'],
));Input:
Visit https://example.com or email user@example.com for help.
Output:
<p>Visit <a href="https://example.com">https://example.com</a> or email <a href="mailto:user@example.com">user@example.com</a> for help.</p>Adds default attributes to elements by type. Useful for adding CSS classes, lazy loading, or other common attributes.
use Djot\Extension\DefaultAttributesExtension;
$converter->addExtension(new DefaultAttributesExtension([
'image' => ['loading' => 'lazy', 'decoding' => 'async'],
'table' => ['class' => 'table table-striped'],
'link' => ['class' => 'link'],
'code_block' => ['class' => 'highlight'],
]));Behavior:
- Default attributes are only applied if the element doesn't already have that attribute
- Classes are merged (both default and existing classes are kept)
Supported element types (use snake_case):
| Block Elements | Inline Elements |
|---|---|
| paragraph | link |
| heading | image |
| code_block | emphasis |
| block_quote | strong |
| list | code |
| list_item | span |
| table | subscript |
| table_cell | superscript |
| div | footnote |
| thematic_break | footnote_ref |
Common use cases:
// Lazy loading images
$converter->addExtension(new DefaultAttributesExtension([
'image' => ['loading' => 'lazy'],
]));
// Bootstrap tables
$converter->addExtension(new DefaultAttributesExtension([
'table' => ['class' => 'table table-bordered'],
]));
// Tailwind prose styling
$converter->addExtension(new DefaultAttributesExtension([
'paragraph' => ['class' => 'mb-4'],
'heading' => ['class' => 'font-bold'],
'block_quote' => ['class' => 'border-l-4 pl-4 italic'],
]));Converts [[Page Name]] patterns into wiki-style links, commonly used in wiki systems and note-taking apps like Obsidian, Notion, and MediaWiki.
Note: This syntax is not yet part of the official djot spec. See jgm/djot#26 for the upstream discussion.
use Djot\Extension\WikilinksExtension;
// Default: creates URL-safe slugs
$converter->addExtension(new WikilinksExtension());
// Custom URL generator
$converter->addExtension(new WikilinksExtension(
urlGenerator: fn (string $page) => '/wiki/' . strtolower(str_replace(' ', '_', $page)) . '.html',
));
// Open in new window
$converter->addExtension(new WikilinksExtension(
newWindow: true,
));
// Custom CSS class
$converter->addExtension(new WikilinksExtension(
cssClass: 'wiki-link internal',
));Supported syntax:
| Syntax | Description | Output |
|---|---|---|
[[Page]] |
Basic link | <a href="page">Page</a> |
[[Page Name]] |
Spaces in name | <a href="page-name">Page Name</a> |
[[page|Display Text]] |
Custom display text | <a href="page">Display Text</a> |
[[page#section]] |
Link with anchor | <a href="page#section">page</a> |
[[page#section|Link]] |
Anchor with display text | <a href="page#section">Link</a> |
[[folder/page]] |
Path support | <a href="folder/page">folder/page</a> |
Input:
See [[Tigers]] for more info, or check [[Big Cats|the cats page]].
Jump to [[Getting Started#installation]] for setup instructions.
Output:
<p>See <a href="tigers" class="wikilink" data-wikilink="Tigers">Tigers</a> for more info,
or check <a href="big-cats" class="wikilink" data-wikilink="Big Cats">the cats page</a>.</p>
<p>Jump to <a href="getting-started#installation" class="wikilink" data-wikilink="Getting Started">installation</a> for setup instructions.</p>Configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
urlGenerator |
Closure |
Slugify | Function that converts page name to URL |
cssClass |
string |
'wikilink' |
CSS class(es) for the link |
newWindow |
bool |
false |
Open links in new tab |
Common configurations:
// Obsidian-style (preserve paths, encode for URLs)
$converter->addExtension(new WikilinksExtension(
urlGenerator: fn (string $page) => '/notes/' . rawurlencode($page) . '.md',
));
// MediaWiki-style (underscores instead of hyphens)
$converter->addExtension(new WikilinksExtension(
urlGenerator: fn (string $page) => '/wiki/' . str_replace(' ', '_', $page),
));
// Static site generator (lowercase with .html extension)
$converter->addExtension(new WikilinksExtension(
urlGenerator: fn (string $page) => '/' . strtolower(str_replace(' ', '-', $page)) . '.html',
));JavaScript integration:
Each wikilink includes a data-wikilink attribute with the original page name, useful for client-side handling:
document.querySelectorAll('a[data-wikilink]').forEach(link => {
const pageName = link.dataset.wikilink;
// Check if page exists, add special styling, etc.
});Implement ExtensionInterface to create your own extensions:
use Djot\DjotConverter;
use Djot\Extension\ExtensionInterface;
use Djot\Event\RenderEvent;
use Djot\Node\Inline\Link;
use Djot\Node\Inline\Text;
class HashtagExtension implements ExtensionInterface
{
public function __construct(
protected string $baseUrl = '/tags/',
) {
}
public function register(DjotConverter $converter): void
{
// Add inline pattern for #hashtag syntax
$converter->getParser()->getInlineParser()->addInlinePattern(
'/#([a-zA-Z][a-zA-Z0-9_]*)/',
function (string $match, array $groups): Link {
$tag = $groups[1];
$url = $this->baseUrl . rawurlencode(strtolower($tag));
$link = new Link($url);
$link->addClass('hashtag');
$link->appendChild(new Text('#' . $tag));
return $link;
},
);
}
}
// Usage
$converter->addExtension(new HashtagExtension(baseUrl: '/tags/'));Here's a complete example using all extensions:
use Djot\DjotConverter;
use Djot\Extension\AutolinkExtension;
use Djot\Extension\ExternalLinksExtension;
use Djot\Extension\HeadingPermalinksExtension;
use Djot\Extension\MentionsExtension;
use Djot\Extension\TableOfContentsExtension;
use Djot\Extension\WikilinksExtension;
$converter = new DjotConverter();
$tocExtension = new TableOfContentsExtension(minLevel: 2);
// Register extensions (order matters for some combinations)
$converter
->addExtension(new AutolinkExtension()) // First: create links from URLs
->addExtension(new ExternalLinksExtension()) // Then: add attributes to external links
->addExtension(new MentionsExtension())
->addExtension(new WikilinksExtension()) // Wiki-style links
->addExtension($tocExtension) // TOC before permalinks for clean text
->addExtension(new HeadingPermalinksExtension());
$djot = <<<'DJOT'
# Welcome
Thanks @admin for setting this up! See [[Getting Started]] below.
## Getting Started
Visit https://example.com for documentation.
## Configuration
Contact support@example.com for help. Also check [[Advanced Config|advanced settings]].
DJOT;
$html = $converter->convert($djot);
$toc = $tocExtension->getTocHtml();
echo $toc;
echo $html;