From b052efd674bf8a4487b703dbaeaefb7be3bfd0c1 Mon Sep 17 00:00:00 2001
From: Colin O'Dell
Date: Sat, 22 Jan 2022 09:15:35 -0500
Subject: [PATCH 01/36] Create 2.3 docs
---
docs/2.3/basic-usage.md | 101 +++++++
docs/2.3/changelog.md | 22 ++
docs/2.3/configuration.md | 93 ++++++
.../2.3/customization/abstract-syntax-tree.md | 256 +++++++++++++++++
docs/2.3/customization/block-parsing.md | 155 ++++++++++
docs/2.3/customization/configuration.md | 105 +++++++
docs/2.3/customization/cursor.md | 57 ++++
.../2.3/customization/delimiter-processing.md | 102 +++++++
docs/2.3/customization/environment.md | 109 +++++++
docs/2.3/customization/event-dispatcher.md | 177 ++++++++++++
docs/2.3/customization/extensions.md | 43 +++
docs/2.3/customization/inline-parsing.md | 179 ++++++++++++
docs/2.3/customization/overview.md | 74 +++++
docs/2.3/customization/rendering.md | 143 ++++++++++
docs/2.3/customization/slug-normalizer.md | 103 +++++++
docs/2.3/extensions/attributes.md | 90 ++++++
docs/2.3/extensions/autolinks.md | 53 ++++
docs/2.3/extensions/commonmark.md | 53 ++++
docs/2.3/extensions/default-attributes.md | 121 ++++++++
docs/2.3/extensions/description-lists.md | 76 +++++
docs/2.3/extensions/disallowed-raw-html.md | 76 +++++
docs/2.3/extensions/external-links.md | 157 ++++++++++
docs/2.3/extensions/footnotes.md | 160 +++++++++++
docs/2.3/extensions/front-matter.md | 150 ++++++++++
.../extensions/github-flavored-markdown.md | 67 +++++
docs/2.3/extensions/heading-permalinks.md | 222 +++++++++++++++
docs/2.3/extensions/inlines-only.md | 42 +++
docs/2.3/extensions/mentions.md | 269 ++++++++++++++++++
docs/2.3/extensions/overview.md | 116 ++++++++
docs/2.3/extensions/smart-punctuation.md | 63 ++++
docs/2.3/extensions/strikethrough.md | 46 +++
docs/2.3/extensions/table-of-contents.md | 197 +++++++++++++
docs/2.3/extensions/tables.md | 126 ++++++++
docs/2.3/extensions/task-lists.md | 55 ++++
docs/2.3/index.md | 44 +++
docs/2.3/installation.md | 19 ++
docs/2.3/security.md | 92 ++++++
docs/2.3/support.md | 23 ++
docs/2.3/upgrading.md | 7 +
docs/2.3/xml.md | 48 ++++
docs/_data/menu.yml | 44 +++
docs/_data/project.yml | 3 +
42 files changed, 4138 insertions(+)
create mode 100644 docs/2.3/basic-usage.md
create mode 100644 docs/2.3/changelog.md
create mode 100644 docs/2.3/configuration.md
create mode 100644 docs/2.3/customization/abstract-syntax-tree.md
create mode 100644 docs/2.3/customization/block-parsing.md
create mode 100644 docs/2.3/customization/configuration.md
create mode 100644 docs/2.3/customization/cursor.md
create mode 100644 docs/2.3/customization/delimiter-processing.md
create mode 100644 docs/2.3/customization/environment.md
create mode 100644 docs/2.3/customization/event-dispatcher.md
create mode 100644 docs/2.3/customization/extensions.md
create mode 100644 docs/2.3/customization/inline-parsing.md
create mode 100644 docs/2.3/customization/overview.md
create mode 100644 docs/2.3/customization/rendering.md
create mode 100644 docs/2.3/customization/slug-normalizer.md
create mode 100644 docs/2.3/extensions/attributes.md
create mode 100644 docs/2.3/extensions/autolinks.md
create mode 100644 docs/2.3/extensions/commonmark.md
create mode 100644 docs/2.3/extensions/default-attributes.md
create mode 100644 docs/2.3/extensions/description-lists.md
create mode 100644 docs/2.3/extensions/disallowed-raw-html.md
create mode 100644 docs/2.3/extensions/external-links.md
create mode 100644 docs/2.3/extensions/footnotes.md
create mode 100644 docs/2.3/extensions/front-matter.md
create mode 100644 docs/2.3/extensions/github-flavored-markdown.md
create mode 100644 docs/2.3/extensions/heading-permalinks.md
create mode 100644 docs/2.3/extensions/inlines-only.md
create mode 100644 docs/2.3/extensions/mentions.md
create mode 100644 docs/2.3/extensions/overview.md
create mode 100644 docs/2.3/extensions/smart-punctuation.md
create mode 100644 docs/2.3/extensions/strikethrough.md
create mode 100644 docs/2.3/extensions/table-of-contents.md
create mode 100644 docs/2.3/extensions/tables.md
create mode 100644 docs/2.3/extensions/task-lists.md
create mode 100644 docs/2.3/index.md
create mode 100644 docs/2.3/installation.md
create mode 100644 docs/2.3/security.md
create mode 100644 docs/2.3/support.md
create mode 100644 docs/2.3/upgrading.md
create mode 100644 docs/2.3/xml.md
diff --git a/docs/2.3/basic-usage.md b/docs/2.3/basic-usage.md
new file mode 100644
index 0000000000..1163802ded
--- /dev/null
+++ b/docs/2.3/basic-usage.md
@@ -0,0 +1,101 @@
+---
+layout: default
+title: Basic Usage
+description: Basic usage of the CommonMark parser
+---
+
+# Basic Usage
+
+
+**Important:** See the [security](/2.3/security/) section for important details on avoiding security misconfigurations.
+
+The `CommonMarkConverter` class provides a simple wrapper for converting Markdown to HTML:
+
+```php
+require __DIR__ . '/vendor/autoload.php';
+
+use League\CommonMark\CommonMarkConverter;
+
+$converter = new CommonMarkConverter();
+echo $converter->convert('# Hello World!');
+
+// Hello World!
+```
+
+Or if you want GitHub-Flavored Markdown:
+
+```php
+require __DIR__ . '/vendor/autoload.php';
+
+use League\CommonMark\GithubFlavoredMarkdownConverter;
+
+$converter = new GithubFlavoredMarkdownConverter();
+echo $converter->convert('# Hello World!');
+
+// Hello World!
+```
+
+## Using Extensions
+
+The `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` shown above automatically configure [the environment](/2.3/customization/environment/) for you, but if you want to use [additional extensions](/2.3/customization/extensions/) you'll need to avoid those classes and use the generic `MarkdownConverter` class instead to customize [the environment](/2.3/customization/environment/) with whatever extensions you wish to use:
+
+```php
+require __DIR__ . '/vendor/autoload.php';
+
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\InlinesOnly\InlinesOnlyExtension;
+use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
+use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
+use League\CommonMark\MarkdownConverter;
+
+$environment = new Environment();
+
+$environment->addExtension(new InlinesOnlyExtension());
+$environment->addExtension(new SmartPunctExtension());
+$environment->addExtension(new StrikethroughExtension());
+
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('**Hello World!**');
+
+// Hello World!
+```
+
+## Configuration
+
+If you're using the `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` class you can pass configuration options directly into their constructor:
+
+```php
+use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\GithubFlavoredMarkdownConverter;
+
+$converter = new CommonMarkConverter($config);
+// or
+$converter = new GithubFlavoredMarkdownConverter($config);
+```
+
+Otherwise, if you’re using `MarkdownConverter` to customize the extensions in your parser, pass the configuration into the `Environment`'s constructor instead:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\InlinesOnly\InlinesOnlyExtension;
+use League\CommonMark\MarkdownConverter;
+
+// Here's where we set the configuration array:
+$environment = new Environment($config);
+
+// TODO: Add any/all the extensions you wish; for example:
+$environment->addExtension(new InlinesOnlyExtension());
+
+// Go forth and convert you some Markdown!
+$converter = new MarkdownConverter($environment);
+```
+
+See the [configuration section](/2.3/configuration/) for more information on the available configuration options.
+
+## Supported Character Encodings
+
+Please note that only UTF-8 and ASCII encodings are supported. If your Markdown uses a different encoding please convert it to UTF-8 before running it through this library.
+
+## Return Value
+
+The `convert()` method actually returns an instance of `League\CommonMark\Output\RenderedContentInterface`. You can cast this (implicitly, as shown above, or explicitly) to a `string` or call `getContent()` to get the final HTML output.
diff --git a/docs/2.3/changelog.md b/docs/2.3/changelog.md
new file mode 100644
index 0000000000..bd8998792b
--- /dev/null
+++ b/docs/2.3/changelog.md
@@ -0,0 +1,22 @@
+---
+layout: default
+title: Changelog
+description: Important changes made in recent releases
+---
+
+# Changelog
+
+All notable changes made in `2.x` releases are shown below. See the [full list of releases](/releases) for the complete changelog.
+
+{% assign releases = site.github.releases | where_exp: "r", "r.name >= '2.3'" | where_exp: "r", "r.name < '3.0'" %}
+
+{% for release in releases %}
+
+## [{{ release.name }}]({{ release.html_url }}) - {{ release.published_at | date: "%Y-%m-%d" }}
+
+{{ release.body | markdownify }}
+{% endfor %}
+
+## Older Versions
+
+Please see the [full list of releases](/releases) for the complete changelog.
diff --git a/docs/2.3/configuration.md b/docs/2.3/configuration.md
new file mode 100644
index 0000000000..479c54d957
--- /dev/null
+++ b/docs/2.3/configuration.md
@@ -0,0 +1,93 @@
+---
+layout: default
+title: Configuration
+---
+
+# Configuration
+
+Many aspects of this library's behavior can be tweaked using configuration options.
+
+You can provide an array of configuration options to the `Environment` or converter classes when creating them:
+
+```php
+$config = [
+ 'renderer' => [
+ 'block_separator' => "\n",
+ 'inner_separator' => "\n",
+ 'soft_break' => "\n",
+ ],
+ 'commonmark' => [
+ 'enable_em' => true,
+ 'enable_strong' => true,
+ 'use_asterisk' => true,
+ 'use_underscore' => true,
+ 'unordered_list_markers' => ['-', '*', '+'],
+ ],
+ 'html_input' => 'escape',
+ 'allow_unsafe_links' => false,
+ 'max_nesting_level' => PHP_INT_MAX,
+ 'slug_normalizer' => [
+ 'max_length' => 255,
+ ],
+];
+```
+
+If you're using the basic `CommonMarkConverter` or `GithubFlavoredMarkdown` classes, simply pass the configuration array into the constructor:
+
+```php
+use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\GithubFlavoredMarkdownConverter;
+
+$converter = new CommonMarkConverter($config);
+// or
+$converter = new GithubFlavoredMarkdownConverter($config);
+```
+
+Otherwise, if you're using `MarkdownConverter` to customize the extensions in your parser, pass the configuration into the [Environment](/2.3/customization/environment/)'s constructor instead:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\InlinesOnly\InlinesOnlyExtension;
+use League\CommonMark\MarkdownConverter;
+
+// Here's where we set the configuration array:
+$environment = new Environment($config);
+
+// TODO: Add any/all the extensions you wish; for example:
+$environment->addExtension(new InlinesOnlyExtension());
+
+// Go forth and convert you some Markdown!
+$converter = new MarkdownConverter($environment);
+```
+
+Here's a list of the core configuration options available:
+
+- `renderer` - Array of options for rendering HTML
+ - `block_separator` - String to use for separating renderer block elements
+ - `inner_separator` - String to use for separating inner block contents
+ - `soft_break` - String to use for rendering soft breaks
+- `html_input` - How to handle HTML input. Set this option to one of the following strings:
+ - `strip` - Strip all HTML (equivalent to `'safe' => true`)
+ - `allow` - Allow all HTML input as-is (default value; equivalent to `'safe' => false)
+ - `escape` - Escape all HTML
+- `allow_unsafe_links` - Remove risky link and image URLs by setting this to `false` (default: `true`)
+- `max_nesting_level` - The maximum nesting level for blocks (default: `PHP_INT_MAX`). Setting this to a positive integer can help protect against long parse times and/or segfaults if blocks are too deeply-nested.
+- `slug_normalizer` - Array of options for configuring how URL-safe slugs are created; see [the slug normalizer docs](/2.3/customization/slug-normalizer/#configuration) for more details
+ - `instance` - An alternative normalizer to use (defaults to the included `SlugNormalizer`)
+ - `max_length` - Limits the size of generated slugs (defaults to 255 characters)
+ - `unique` - Controls whether slugs should be unique per `'document'` (default) or per `'environment'`; can be disabled with `false`
+
+Additional configuration options are available for most of the [available extensions](/2.3/customization/extensions/) - refer to their individual documentation for more details. For example, the CommonMark core extension offers these additional options:
+
+- `commonmark` - Array of options for configuring the CommonMark core extension:
+ - `enable_em` - Disable `` parsing by setting to `false`; enable with `true` (default: `true`)
+ - `enable_strong` - Disable `` parsing by setting to `false`; enable with `true` (default: `true`)
+ - `use_asterisk` - Disable parsing of `*` for emphasis by setting to `false`; enable with `true` (default: `true`)
+ - `use_underscore` - Disable parsing of `_` for emphasis by setting to `false`; enable with `true` (default: `true`)
+ - `unordered_list_markers` - Array of characters that can be used to indicate a bulleted list (default: `["-", "*", "+"]`)
+
+## Environment
+
+The configuration is ultimately passed to (and managed via) the `Environment`. If you're creating your own `Environment`, simply pass your config array into its constructor instead.
+
+[Learn more about customizing the Environment](/2.3/customization/environment/)
diff --git a/docs/2.3/customization/abstract-syntax-tree.md b/docs/2.3/customization/abstract-syntax-tree.md
new file mode 100644
index 0000000000..595e10016f
--- /dev/null
+++ b/docs/2.3/customization/abstract-syntax-tree.md
@@ -0,0 +1,256 @@
+---
+layout: default
+title: Abstract Syntax Tree
+description: Using the Abstract Syntax Tree (AST) to manipulate the parsed content
+---
+
+# Abstract Syntax Tree
+
+This library uses a doubly-linked list Abstract Syntax Tree (AST) to represent the parsed block and inline elements. All such elements extend from the `Node` class.
+
+## `Document`
+
+The root node of the AST will always be a `Document` object. You can obtain this node a few different ways:
+
+- By calling the `parse()` method on the `MarkdownParser`
+- By calling the `getDocument()` method on either the `DocumentPreParsedEvent` or `DocumentParsedEvent` [see the (Event Dispatcher documentation](/2.3/customization/event-dispatcher/))
+
+## Visualization
+
+Even with an interactive debugger it can be tricky to view an entire tree at once. Consider using the [`XmlRenderer`](/2.3/xml/) to provide a simple text-based representation of the AST for debugging purposes.
+
+## Node Traversal
+
+There are four different ways to traverse/iterate the Nodes within the AST:
+
+| Method | Pros | Cons |
+| --- | --- | --- |
+| Manual Traversal | Best way to access/check direct relatives of nodes | Not useful for iteration |
+| Iterating the Tree | Fast and efficient | Possible unexpected behavior when adding/removing sibling nodes while iterating |
+| Walking the Tree | Full control over iteration | Up to twice as slow as iteration; adding/removing nodes while iterating can lead to weird behaviors |
+| Querying Nodes | Easier to write and understand; no weird behaviors | Not memory efficient |
+
+Each is described in more detail below
+
+### Manual Traversal
+
+The following methods can be used to manually traverse from one `Node` to any of its direct relatives:
+
+- `previous()`
+- `next()`
+- `parent()`
+- `firstChild()`
+- `lastChild()`
+- `children()`
+
+This is best suited for situations when you need to know information about those relatives.
+
+### Iterating the Tree
+
+If you'd like to iterate through all the nodes, use the `iterator()` method to obtain an iterator that will loop through each node in the tree (using pre-order traversal):
+
+```php
+foreach ($document->iterator() as $node) {
+ echo 'Current node: ' . get_class($node) . "\n";
+}
+```
+
+Given an AST like this (XML representation):
+
+```xml
+
+
+ Hello World!
+
+
+ This is an example of
+
+ CommonMark
+
+ .
+
+
+```
+
+The code above will output:
+
+```text
+Current node: League\CommonMark\Node\Block\Document
+Current node: League\CommonMark\Extension\CommonMark\Node\Block\Heading
+Current node: League\CommonMark\Node\Inline\Text
+Current node: League\CommonMark\Node\Block\Paragraph
+Current node: League\CommonMark\Node\Inline\Text
+Current node: League\CommonMark\Extension\CommonMark\Node\Inline\Strong
+Current node: League\CommonMark\Node\Inline\Text
+Current node: League\CommonMark\Node\Inline\Text
+```
+
+This iterator doesn't use recursion, so you won't blow the stack when working with deeply-nested nodes. It's also very CPU and memory-efficient.
+
+Be careful when modifying nodes while iterating the tree as some of those changes may affect the current iteration process, especially for sibling nodes that come after the current one. For example, if you remove the current node's `next()` sibling, the next loop of that iteration will still include the removed sibling even though it was successfully removed from the AST. Similarly, any new siblings that are added won't be visited on the next loop.
+
+### Walking the Tree
+
+If you'd like to walk through all the nodes, visiting each one as you enter and leave it, use the `walker()` method to obtain an instance of `NodeWalker`. This also uses pre-order traversal but emitting `NodeWalkerEvent`s along the way:
+
+```php
+use League\CommonMark\Node\NodeWalker;
+
+/** @var NodeWalker $walker */
+$walker = $document->walker();
+while ($event = $walker->next()) {
+ echo 'Now ' . ($event->isEntering() ? 'entering' : 'leaving') . ' a ' . get_class($event->getNode()) . ' node' . "\n";
+}
+```
+
+Using the same example AST in the previous section, this code will output:
+
+```text
+Now entering a League\CommonMark\Node\Block\Document node
+Now entering a League\CommonMark\Extension\CommonMark\Node\Block\Heading node
+Now entering a League\CommonMark\Node\Inline\Text node
+Now leaving a League\CommonMark\Extension\CommonMark\Node\Block\Heading node
+Now entering a League\CommonMark\Node\Block\Paragraph node
+Now entering a League\CommonMark\Node\Inline\Text node
+Now entering a League\CommonMark\Extension\CommonMark\Node\Inline\Strong node
+Now entering a League\CommonMark\Node\Inline\Text node
+Now leaving a League\CommonMark\Extension\CommonMark\Node\Inline\Strong node
+Now entering a League\CommonMark\Node\Inline\Text node
+Now leaving a League\CommonMark\Node\Block\Paragraph node
+Now leaving a League\CommonMark\Node\Block\Document node
+```
+
+This approach offers many of the same benefits as the simple iteration shown in the previous section such as memory efficiency and no recursion. The key differences come from how you enter and leave nodes:
+
+1. Iteration can potentially take twice as long - not ideal for performance
+2. Provides you with more control over exactly when an action is taken on a node which is sometimes needed for certain AST manipulations
+3. Also provides a `resumeAt()` method to override where it should iterate next
+
+But like with the iterator, be careful when adding/removing nodes while walking the tree, as there are even more subtle cases where the walker could even lose track of where it was, which may result in some nodes being visited multiple times or not at all.
+
+### Querying Nodes
+
+If you're trying to locate certain nodes to perform actions on them, querying the nodes from the AST might be easier to implement. This can be done with the `Query` class:
+
+```php
+use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote;
+use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
+use League\CommonMark\Node\Block\Paragraph;
+use League\CommonMark\Node\Query;
+
+// Find all paragraphs and blockquotes that contain links
+$matchingNodes = (new Query())
+ ->where(Query::type(Paragraph::class))
+ ->orWhere(Query::type(BlockQuote::class))
+ ->andWhere(Query::hasChild(Query::type(Link::class)))
+ ->findAll($document);
+
+foreach ($matchingNodes as $node) {
+ // TODO: Do something with them
+}
+```
+
+Each condition passed into `where()`, `orWhere()`, or `andWhere()` must be a callable "filter" that accepts a `Node` and returns `true` or `false`. We provide several methods that can help create these filters for you:
+
+| Method | Description |
+| --- | --- |
+| `Query::type(string $class)` | Creates a filter that matches nodes with the given class name |
+| `Query::hasChild()` | Creates a filter that matches nodes which contain at least one child |
+| `Query::hasChild(callable $condition)` | Creates a filter that matches nodes which contain at least one child that matches the inner `$condition` |
+| `Query::hasParent()` | Creates a filter that matches nodes which have a parent |
+| `Query::hasParent(callable $condition)` | Creates a filter that matches nodes which have a parent that matches the inner `$condition` |
+
+You can of course create your own custom filters/conditions using an anonymous function or by implementing `ExpressionInterface`:
+
+```php
+use League\CommonMark\Node\Node;
+use League\CommonMark\Node\Query;
+use League\CommonMark\Node\Query\ExpressionInterface;
+
+class ChildCountGreaterThan implements ExpressionInterface
+{
+ private $count;
+
+ public function __construct(int $count)
+ {
+ $this->count = $count;
+ }
+
+ public function __invoke(Node $node) : bool{
+ return count($node->children()) > $this->count;
+ }
+}
+
+$query = (new Query())
+ ->where(function (Node $node): bool { return $node->data->has('attributes/class'); })
+ ->andWhere(new ChildCountGreaterThan(3));
+```
+
+## Modification
+
+The following methods can be used to modify the AST:
+
+- `insertAfter(Node $sibling)`
+- `insertBefore(Node $sibling)`
+- `replaceWith(Node $replacement)`
+- `detach()`
+- `appendChild(Node $child)`
+- `prependChild(Node $child)`
+- `detachChildren()`
+- `replaceChildren(Node[] $children)`
+
+## `DocumentParsedEvent`
+
+The best way to access and manipulate the AST is by adding an [event listener](/2.3/customization/event-dispatcher/) for the `DocumentParsedEvent`.
+
+## Data Storage
+
+Each `Node` has a property called `data` which is a `Data` (array-like) object. This can be used to store any arbitrary data you'd like on the node:
+
+```php
+use League\CommonMark\Node\Inline\Text;
+
+$text1 = new Text('Hello, world!');
+$text1->data->set('language', 'English');
+$text1->data->set('is_good_translation', true);
+
+$text2 = new Text('Bonjour monde!');
+$text2->data->set('language', 'French');
+$text2->data->set('is_good_translation', false);
+
+foreach ([$text1, $text2] as $text) {
+ if ($text->data->get('is_good_translation')) {
+ sprintf('In %s we would say: "%s"', $text->data->get('language'), $text->getLiteral());
+ } else {
+ sprintf('I think they would say "%s" in %s, but I\'m not sure.', $text->getLiteral(), $text->data->get('language'));
+ }
+}
+```
+
+You can also access deeply-nested paths using `/` or `.` as delimiters:
+
+```php
+use League\CommonMark\Node\Inline\Text;
+
+$text = new Text('Hello, world!');
+$text->data->set('info', ['language' => 'English', 'is_good_translation' => true]);
+
+var_dump($text->data->get('info/language'));
+var_dump($text->data->get('info.is_good_translation'));
+
+$text->data->set('info/is_example', true);
+```
+
+### HTML Attributes
+
+The `data` property comes pre-instantiated with a single data element called `attributes` which is used to store any HTML attributes that need to be rendered. For example:
+
+```php
+use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
+
+$link = new Link('https://twitter.com/colinodell', '@colinodell');
+$link->data->append('attributes/class', 'social-link');
+$link->data->append('attributes/class', 'twitter');
+$link->data->set('attributes/target', '_blank');
+$link->data->set('attributes/rel', 'noopener');
+```
diff --git a/docs/2.3/customization/block-parsing.md b/docs/2.3/customization/block-parsing.md
new file mode 100644
index 0000000000..4defec5d4c
--- /dev/null
+++ b/docs/2.3/customization/block-parsing.md
@@ -0,0 +1,155 @@
+---
+layout: default
+title: Block Parsing
+description: How to parse block-level elements
+---
+
+# Block Parsing
+
+At a high level, block parsing is a two-step process:
+
+ 1. Using a `BlockStartParserInterface` to identify if/where a block start exists on the given line
+ 2. Using a `BlockContinueParserInterface` to perform additional processing of the identified block
+
+So to implement a custom block parser you will actually need to implement both of these classes.
+
+## `BlockStartParserInterface`
+
+Instances of this interface have a single `tryStart()` method:
+
+```php
+/**
+ * Check whether we should handle the block at the current position
+ *
+ * @param Cursor $cursor
+ * @param MarkdownParserStateInterface $parserState
+ *
+ * @return BlockStart|null
+ */
+public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart;
+```
+
+Given a [`Cursor`](/2.3/customization/cursor/) at the current position, plus some extra information about the state of the parser, this method is responsible for determining whether a particular type of block seems to exist at the given position. You don't actually parse the block here - that's the job of a `BlockContinueParserInterface`. Your only job here is to return whether or not a particular type of block does exist here, and if so which block parser should parse it.
+
+If you find that you **cannot** parse the given block, you should `return BlockStart::none();` from this function.
+
+However, if the Markdown at the current position does indeed seem to be the type of block you're looking for, you should return a `BlockStart` instance using the following static constructor pattern:
+
+```php
+use League\CommonMark\Parser\Block\BlockStart;
+
+return BlockStart::of(new MyCustomParser())->at($cursor);
+```
+
+Unlike in 1.x, the `Cursor` state is no longer shared between parsers. You must therefore explicitly provide the `BlockStart` object with a copy of your cursor at the correct, post-parsing position.
+
+**NOTE:** If your custom block starts with a [letter character](http://unicode.org/reports/tr18/#General_Category_Property) you'll need to [add your parser to the environment](/2.3/customization/environment/#addblockstartparser) with a priority of `250` or higher. This is due to a performance optimization where such lines are usually skipped.
+
+## `BlockContinueParserInterface`
+
+The previous interface only helps the engine identify where a block starts. Additional information about the block, as well as the ability to parse additional lines of input, is all handled by the `BlockContinueParserInterface`.
+
+This interface has several methods, so it's usually easier to extend from `AbstractBlockContinueParser` instead, which sets most of the methods to use typical defaults you can override as needed.
+
+### `getBlock()`
+
+```php
+public function getBlock(): AbstractBlock;
+```
+
+Each instance of a `BlockContinueParserInterface` is associated with a new block that is being parsed. This method here returns that block.
+
+### `isContainer()`
+
+```php
+public function isContainer(): bool;
+```
+
+This method returns whether or not the block is a "container" capable of containing other blocks as children.
+
+### `canContain()`
+
+```php
+public function canContain(AbstractBlock $childBlock): bool;
+```
+
+This method returns whether the current block being parsed can contain the given child block.
+
+### `canHaveLazyContinuationLines()`
+
+```php
+public function canHaveLazyContinuationLines(): bool;
+```
+
+This method returns whether or not this parser should also receive subsequent lines of Markdown input. This is primarily used when a block can span multiple lines, like code blocks do.
+
+### `addLine()`
+
+```php
+public function addLine(string $line): void;
+```
+
+If `canHaveLazyContinuationLines()` returned `true`, this method will be called with the additional lines of content.
+
+### `tryContinue()`
+
+```php
+public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue;
+```
+
+### `closeBlock()`
+
+This method allows you to try and parse an additional line of Markdown.
+
+```php
+public function closeBlock(): void;
+```
+
+This method is called when the block is done being parsed. Any final adjustments to the block should be made at this time.
+
+### `parseInlines()`
+
+```php
+public function parseInlines(InlineParserEngineInterface $inlineParser): void;
+```
+
+This method is called when the engine is ready to parse any inline child elements.
+
+**Note:** For performance reasons, this method is not part of `BlockContinueParserInterface`. If your block may contain inlines, you should make sure that your "continue parser" also implements `BlockContinueParserWithInlinesInterface`.
+
+## Tips
+
+Here are some additional tips to consider when writing your own custom parsers:
+
+### Combining both into one file
+
+Although parsing requires two classes, you can use the anonymous class feature of PHP to combine both into a single file! Here's an example:
+
+```php
+use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
+use League\CommonMark\Parser\Block\BlockStartParserInterface;
+
+final class MyCustomBlockParser extends AbstractBlockContinueParser
+{
+ // TODO: implement your continuation parsing methods here
+
+ public static function createBlockStartParser(): BlockStartParserInterface
+ {
+ return new class implements BlockStartParserInterface
+ {
+ // TODO: implement the tryStart() method here
+ };
+ }
+}
+
+```
+
+### Performance
+
+The `BlockStartParserInterface::tryStart()` and `BlockContinueParserInterface::tryContinue()` methods may be called hundreds or thousands of times during execution. For best performance, have your methods return as early as possible, and make sure your code is highly optimized.
+
+## Block Elements
+
+In addition to creating a block parser, you may also want to have it return a custom "block element" - this is a class that extends from `AbstractBlock` and represents that particular block within the AST.
+
+If your block contains literal strings/text within the block (and not as part of a child block), you should have your custom block type also `implement StringContainerInterface`.
diff --git a/docs/2.3/customization/configuration.md b/docs/2.3/customization/configuration.md
new file mode 100644
index 0000000000..e7cbe1cbf3
--- /dev/null
+++ b/docs/2.3/customization/configuration.md
@@ -0,0 +1,105 @@
+---
+layout: default
+title: Configuration
+description: Defining configuration schemas and accessing user-provided configuration options within your custom extensions
+---
+
+# Configuration Schemas and Values
+
+Version 2.0 introduced a new robust system for defining configuration schemas and accessing them within custom extensions.
+
+## Configuration Schemas
+
+Unlike in 1.x, all configuration options must have a defined schema. This defines which options are available, what types of values they accept, whether any are required, and any default values you wish to define if the user doesn't provide any.
+
+These custom options can be defined from within your [custom extension](/2.3/customization/extensions/) by implementing the `ConfigurableExtensionInterface`:
+
+```php
+use League\Config\ConfigurationBuilderInterface;
+use League\CommonMark\Extension\ConfigurableExtensionInterface;
+use Nette\Schema\Expect;
+
+final class MyCustomExtension implements ConfigurableExtensionInterface
+{
+ public function configureSchema(ConfigurationBuilderInterface $builder): void
+ {
+ $builder->addSchema('my_extension', Expect::structure([
+ 'enable_some_feature' => Expect::bool()->default(true),
+ 'html_class' => Expect::string()->default('my-custom-extension'),
+ 'align' => Expect::anyOf('left', 'center', 'right')->default('left'),
+ 'favorite_number' => Expect::int()->min(1)->max(100)->default(42),
+ ]));
+ }
+
+ public function register(EnvironmentBuilderInterface $environment): void
+ {
+ // TODO: Implement register() method
+ }
+}
+```
+
+See the [league/config documentation](https://config.thephpleague.com/1.0/schemas/) for more examples of how to define custom configuration schemas.
+
+Note that you only need to implement `ConfigurableExtensionInterface` if you plan to define new configuration options - you don't need this if you're only reading existing options.
+
+## Reading Configuration Values
+
+Okay, so your extension has defined the different options that are available, but now you want to start using them within your custom extension. There are a few ways you can access the values:
+
+### During Extension Registration
+
+Perhaps your extension needs to decide whether/how to register certain parsers/renderers/etc based on the user-provided configuration values - in that case, you can read the value from the `$environment` - for example:
+
+```php
+use League\Config\ConfigurationBuilderInterface;
+use League\CommonMark\Environment\EnvironmentBuilderInterface;
+use League\CommonMark\Extension\ConfigurableExtensionInterface;
+
+final class MyCustomExtension implements ConfigurableExtensionInterface
+{
+ public function configureSchema(ConfigurationBuilderInterface $builder): void
+ {
+ // (see code example above)
+ }
+
+ public function register(EnvironmentBuilderInterface $environment): void
+ {
+ if ($environment->getConfiguration()->get('my_extension/enable_some_feature')) {
+ $environment->addBlockStartParser(new MyCustomParser());
+ $environment->addRenderer(MyCustomBlockType::class, new MyCustomRenderer());
+ }
+ }
+}
+```
+
+### Within Parsers/Renderers/Listeners
+
+Perhaps you want to reference those configuration values from within a custom parser, renderer, event listener, or something else. This can easily by done by having that class also implement `ConfigurationAwareInterface`. This interface signals to the `Environment` that your class needs a copy of the final configuration so it can read it later:
+
+```php
+use League\CommonMark\Node\Node;
+use League\CommonMark\Renderer\ChildNodeRendererInterface;
+use League\CommonMark\Renderer\NodeRendererInterface;
+use League\Config\ConfigurationAwareInterface;
+use League\Config\ConfigurationInterface;
+
+final class MyCustomRenderer implements NodeRendererInterface, ConfigurationAwareInterface
+{
+ /**
+ * @var ConfigurationInterface
+ */
+ private $config;
+
+ public function setConfiguration(ConfigurationInterface $configuration): void
+ {
+ $this->config = $configuration;
+ }
+
+ public function render(Node $node, ChildNodeRendererInterface $childRenderer)
+ {
+ return 'My favorite number is ' . $this->config->get('my_extension/favorite_number');
+ }
+}
+```
+
+You can access any configuration value from here, not just the ones you might have defined yourself.
diff --git a/docs/2.3/customization/cursor.md b/docs/2.3/customization/cursor.md
new file mode 100644
index 0000000000..01e1a016ee
--- /dev/null
+++ b/docs/2.3/customization/cursor.md
@@ -0,0 +1,57 @@
+---
+layout: default
+title: Cursor
+description: Using the Cursor object to parse Markdown content
+---
+
+# Cursor
+
+A `Cursor` is essentially a fancy string wrapper that remembers your current position as you parse it. It contains a set of highly-optimized methods making it easy to parse characters, match regular expressions, and more.
+
+## Supported Encodings
+
+As of now, only UTF-8 (and, by extension, ASCII) encoding is supported.
+
+## Usage
+
+Instantiating a new `Cursor` is as simple as:
+
+```php
+use League\CommonMark\Parser\Cursor;
+
+$cursor = new Cursor('Hello World!');
+```
+
+Or, if you're creating a custom [block parser](/2.3/customization/block-parsing/) or [inline parser](/2.3/customization/inline-parsing/), a pre-configured `Cursor` will be provided to you with (with the `Cursor` already set to the current `position` trying to be parsed).
+
+## Methods
+
+You can then call any of the following methods to parse the string within that `Cursor`:
+
+| Method | Purpose |
+| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| `getPosition()` | Returns the current position/index of the `Cursor` within the string |
+| `getColumn()` | Returns the current column (used when handling tabbed indentation) |
+| `getIndent()` | Returns the current amount of indentation |
+| `isIndented()` | Returns whether the cursor is indented to `INDENT_LEVEL` |
+| `getCharacter(int $index)` | Returns the character at the given absolute position |
+| `getCurrentCharacter()` | Returns the character at the current position |
+| `peek()` | Returns the next character without changing the current `position` of the cursor |
+| `peek(int $offset)` | Returns the character `$offset` chars away without changing the current `position` of the cursor |
+| `getNextNonSpacePosition()` | Returns the position of the next character which is not a space or tab |
+| `getNextNonSpaceCharacter()` | Returns the next character which isn't a space (or tab) |
+| `advance()` | Moves the cursor forward by 1 character |
+| `advanceBy(int $characters)` | Moves the cursor forward by `$characters` characters |
+| `advanceBy(int $characters, true)` | Moves the cursor forward by `$characters` characters, handling tabs as columns |
+| `advanceBySpaceOrTab()` | Advances forward one character (and returns `true`) if it's a space or tab; returns false otherwise |
+| `advanceToNextNonSpaceOrTab()` | Advances forward past all spaces and tabs found, returning the number of such characters found |
+| `advanceToNextNonSpaceOrNewline()` | Advances forward past all spaces and newlines found, returning the number of such characters found |
+| `advanceToEnd()` | Advances the position to the very end of the string, returning the number of such characters passed |
+| `match(string $regex)` | Attempts to match the given `$regex`; returns `null` if matching fails, otherwise it advances past and returns the matched text |
+| `getPreviousText()` | Returns the text that was just advanced through during the last `advance__()` or `match()` operation |
+| `getRemainder()` | Returns the contents of the string from the current position through the end of the string |
+| `isBlank()` | Returns whether the remainder is blank (we're at the end or only space characters remain) |
+| `isAtEnd()` | Returns whether the cursor has reached the end of the string |
+| `saveState()` | Encapsulates the current state of the cursor into an `array` in case you need to `restoreState()` later |
+| `restoreState($state)` | Pass the result of `saveState()` back into here to restore the original state of the `Cursor` |
+| `getLine()` | Returns the entire string (not taking the position into account) |
diff --git a/docs/2.3/customization/delimiter-processing.md b/docs/2.3/customization/delimiter-processing.md
new file mode 100644
index 0000000000..ec2c354da7
--- /dev/null
+++ b/docs/2.3/customization/delimiter-processing.md
@@ -0,0 +1,102 @@
+---
+layout: default
+title: Delimiter Processing
+description: Processing CommonMark delimiter runs with a custom processor
+---
+
+# Delimiter Processing
+
+Delimiter processors allow you to implement [delimiter runs](https://spec.commonmark.org/0.29/#delimiter-run) the same way the core library implements emphasis.
+
+Delimiter runs are a special type of inline:
+
+- They are denoted by "wrapping" text with one or more characters before **and** after those inner contents
+- They can contain other delimiter runs or inlines inside of them
+
+```markdown
+This is an example of **emphasis**. Note how the text is *wrapped* with the same character(s) before and after.
+```
+
+When implementing something with these characteristics you should consider leveraging delimiter runs; otherwise, a basic [inline parser](/2.3/inline-parsing/) should be sufficient.
+
+## Delimiter Priority
+
+Delimiter processors have a lower priority than inline parsers - if an [inline parser](/2.3/inline-parsing/) successfully handles the same special character you're interested in then your delimiter processor will not be called.
+
+## Implementing Standalone Delimiter Processors
+
+Implement the `DelimiterProcessorInterface` and add it to your environment:
+
+```php
+$environment->addDelimiterProcessor(new MyCustomDelimiterProcessor());
+```
+
+### `getOpeningCharacter()` and `getClosingCharacter()`
+
+These two methods tell the engine which characters are used to delineate your custom syntax. Generally these will be the same, such as when using `*emphasis*`, but they can be different; for example, maybe you want to use `{this syntax}`. Simply tell the engine which characters you'd like to use.
+
+### `getMinimumLength()`
+
+This method tells the engine the minimum number of characters needed to match or "activate" your processor. For example, if you want to match {% raw %}`{{example}}`{% endraw %} and not `{example}`, set this to `2`.
+
+### `getDelimiterUse()`
+
+```php
+public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int;
+```
+
+This method is used to tell the engine how many characters from the matching delimiters should be consumed. For simple processors you'll likely return `1` (or whatever your minimum length is). In more advanced cases, you can examine the opening and closing delimiters and perform additional logic to determine whether they should be fully or partially consumed. You can also return `0` if you'd like.
+
+### `process()`
+
+```php
+public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void;
+```
+
+This is where the magic happens. Once the engine determines it can use the delimiter it found (by looking at all the other methods above) it'll call this method. Your job is to take everything between the `$opener` and `$closer` and wrap that in whatever custom inline element you'd like. Here's a basic example of wrapping the inner contents inside a new `Emphasis` element:
+
+```php
+use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
+
+// Create the outer element
+$emphasis = new Emphasis();
+
+// Add everything between $opener and $closer (exclusive) to the new outer element
+$tmp = $opener->next();
+while ($tmp !== null && $tmp !== $closer) {
+ $next = $tmp->next();
+ $emphasis->appendChild($tmp);
+ $tmp = $next;
+}
+
+// Place the outer element into the AST
+$opener->insertAfter($emphasis);
+```
+
+Note that `$opener` and `$closer` will be automatically removed for you after this function returns - no need to do that yourself.
+
+## Combining Inline Parsers with Delimiter Processors
+
+Basic delimiter processors, as covered above, do not require any custom inline parsers - they'll "just work". But in some rare cases you may want to pair it with a custom [inline parser](/2.3/customization/inline-parsing/): the inline parser will identify the delimiter, adding an entry to the delimiter stack for the processor to process later. Note that this is an advanced use case and you probably don't need this. But if you do then read on.
+
+### Inline Parsers and the Delimiter Stack
+
+As your identifies potential delimiter-based inlines, it should create a new `AbstractStringContainer` node (either `Text` or something custom) with the inner contents and also push a new `DelimiterInterface` onto the `DelimiterStack`:
+
+```php
+use League\CommonMark\Delimiter\Delimiter;
+use League\CommonMark\Node\Inline\Text;
+
+$node = new Text($cursor->getPreviousText(), [
+ 'delim' => true,
+]);
+$inlineContext->getContainer()->appendChild($node);
+
+// Add entry to stack to this opener
+$delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
+$inlineContext->getDelimiterStack()->push($delimiter);
+```
+
+This basically tells the engine that text was found which _might_ be emphasis, but due to the delimiter run rules we can't make that determination just yet. That final determination is later on by a "delimiter processor".
+
+Your implementation of the delimiter processor won't look any different in this approach - you'll still need to implement all of the same methods especially `process()`. The difference is that **you've identified where the delimiter is, instead of relying on the engine to do this for you.**
diff --git a/docs/2.3/customization/environment.md b/docs/2.3/customization/environment.md
new file mode 100644
index 0000000000..9b30d14a7a
--- /dev/null
+++ b/docs/2.3/customization/environment.md
@@ -0,0 +1,109 @@
+---
+layout: default
+title: The Environment
+description: Configuring the CommonMark environment with custom options and added functionality
+---
+
+# The Environment
+
+The `Environment` contains all of the parsers, renderers, configurations, etc. that the library uses during the conversion process. You therefore must register all extensions, parsers, renderers, etc. with the `Environment` so that the library is aware of them.
+
+An empty `Environment` can be obtained like this:
+
+```php
+use League\CommonMark\Environment\Environment;
+
+$config = [];
+$environment = new Environment($config);
+```
+
+You can customize the `Environment` using any of the methods below (from the `EnvironmentBuilderInterface` interface).
+
+Once your `Environment` is configured with whatever configuration and extensions you want, you can instantiate a `MarkdownConverter` and start converting MD to HTML:
+
+```php
+use League\CommonMark\MarkdownConverter;
+
+// Using $environment from the previous code sample
+$converter = new MarkdownConverter($environment);
+
+echo $converter->convert('# Hello World!');
+```
+
+## addExtension()
+
+```php
+public function addExtension(ExtensionInterface $extension);
+```
+
+Registers the given [extension](/2.3/customization/extensions/) with the environment. For example, if you want core CommonMark functionality plus footnote support:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\Footnote\FootnoteExtension;
+
+$config = [];
+$environment = new Environment($config);
+
+$environment->addExtension(new CommonMarkCoreExtension());
+$environment->addExtension(new FootnoteExtension());
+```
+
+## addBlockStartParser()
+
+```php
+public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0);
+```
+
+Registers the given `BlockStartParserInterface` with the environment with the given priority (a higher number will be executed earlier).
+
+See [Block Parsing](/2.3/customization/block-parsing/) for details.
+
+## addInlineParser()
+
+```php
+public function addInlineParser(InlineParserInterface $parser, int $priority = 0);
+```
+
+Registers the given `InlineParserInterface` with the environment with the given priority (a higher number will be executed earlier).
+
+See [Inline Parsing](/2.3/customization/inline-parsing/) for details.
+
+## addDelimiterProcessor()
+
+```php
+public function addDelimiterProcessor(DelimiterProcessorInterface $processor);
+```
+
+Registers the given `DelimiterProcessorInterface` with the environment.
+
+See [Inline Parsing](/2.3/customization/delimiter-processing/) for details.
+
+## addRenderer()
+
+```php
+public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0);
+```
+
+Registers a `NodeRendererInterface` to handle a specific type of AST node (`$nodeClass`) with the given priority (a higher number will be executed earlier).
+
+See [Rendering](/2.3/customization/rendering/) for details.
+
+## addEventListener()
+
+```php
+public function addEventListener(string $eventClass, callable $listener, int $priority = 0);
+```
+
+Registers the given event listener with the environment.
+
+See [Event Dispatcher](/2.3/customization/event-dispatcher/) for details.
+
+## Priority
+
+Several of these methods allows you to specify a numeric `$priority`. In cases where multiple things are registered, the internal engine will attempt to use the higher-priority ones first, falling back to lower priority ones if the first one(s) were unable to handle things.
+
+## Accessing the Environment and Configuration within parsers/renderers/etc
+
+If your custom parser/renderer/listener/etc. implements either `EnvironmentAwareInterface` or `ConfigurationAwareInterface` we'll automatically inject the environment or configuration into them once the environment has been fully initialized. This will provide your code with access to the finalized information it may need.
diff --git a/docs/2.3/customization/event-dispatcher.md b/docs/2.3/customization/event-dispatcher.md
new file mode 100644
index 0000000000..78915e40e2
--- /dev/null
+++ b/docs/2.3/customization/event-dispatcher.md
@@ -0,0 +1,177 @@
+---
+layout: default
+title: Event Dispatcher
+description: How to leverage the event dispatcher to hook into the library
+---
+
+# Event Dispatcher
+
+This library includes basic, [PSR-14](https://www.php-fig.org/psr/psr-14/)-compliant event dispatcher functionality. This makes it possible to add hook points throughout the library and third-party extensions which other code can listen for and execute code.
+
+## Event Class
+
+Any [PSR-14 compliant event](https://www.php-fig.org/psr/psr-14/#events) can be used, though we also provide an `AbstractEvent` class you can use to easily create your own events:
+
+```php
+use League\CommonMark\Event\AbstractEvent;
+
+class MyCustomEvent extends AbstractEvent {}
+```
+
+An event can have any number of methods on it which return useful information the listeners can use or modify.
+
+## Registering Listeners
+
+Listeners can be registered with the `Environment` using the `addEventListener()` method:
+
+```php
+public function addEventListener(string $eventClass, callable $listener, int $priority = 0)
+```
+
+The parameters for this method are:
+
+1. The fully-qualified name of the event class you wish to observe
+2. Any [PHP callable](https://www.php.net/manual/en/language.types.callable.php) to execute when that type of event is dispatched
+3. An optional priority (defaults to `0`)
+
+For example:
+
+```php
+// Telling the environment which method to call:
+$customListener = new MyCustomListener();
+$environment->addEventListener(MyCustomEvent::class, [$customListener, 'onDocumentParsed']);
+
+// Or if MyCustomerListener has an __invoke() method:
+$environment->addEventListener(MyCustomEvent::class, new MyCustomListener(), 10);
+
+// Or use any other type of callable you wish!
+$environment->addEventListener(MyCustomEvent::class, function (MyCustomEvent $event) {
+ // TODO: Stuff
+}, 10);
+```
+
+## Dispatching Events
+
+Events can be dispatched via the `$environment->dispatch()` method which takes a single argument - the event object to dispatch:
+
+```php
+$environment->dispatch(new MyCustomEvent());
+```
+
+Listeners will be called in order of priority (higher priorities will be called first). If multiple listeners have the same priority, they'll be called in the order in which they were registered. If you'd like your listener to prevent other subsequent events from running, simply call `$event->stopPropagation()`.
+
+Listeners may call any method on the event to get more information about the event, make changes to event data, etc.
+
+## List of Available Events
+
+This library supports the following default events which you can register listeners for:
+
+### `League\CommonMark\Event\DocumentPreParsedEvent`
+
+This event is dispatched just before any processing is done. It can be used to pre-populate reference map of a document or manipulate the Markdown contents before any processing is performed.
+
+### `League\CommonMark\Event\DocumentParsedEvent`
+
+This event is dispatched once all other processing is done. This offers extensions the opportunity to inspect and modify the [Abstract Syntax Tree](/2.3/customization/abstract-syntax-tree/) prior to rendering.
+
+### `League\CommonMark\Event\DocumentPreRenderEvent`
+
+This event is dispatched by the renderer just before rendering begins. Like with `DocumentParsedEvent`, this offers extensions the opportunity to inspect and modify the [Abstract Syntax Tree](/2.3/customization/abstract-syntax-tree/) prior to rendering, but with the added knowledge of which format is being rendered to (e.g. `html`).
+
+### `League\CommonMark\Event\DocumentRenderedEvent`
+
+This event is dispatched once the rendering step has been completed, just before the output is returned. The final output can be adjusted at this point or additional metadata can be attached to the return object.
+
+## Bring Your Own PSR-14 Event Dispatcher
+
+Although this library provides PSR-14 compliant event dispatching out-of-the-box, you may want to use your own PSR-14 event dispatcher instead. This is possible as long as that third-party library both:
+
+ 1. Implements the PSR-14 `EventDispatcherInterface`; and,
+ 2. Allows you to register additional `ListenerProviderInterface` instances with that dispatcher library
+
+Not all libraries support this so please check carefully! Assuming yours does, delegating all the event behavior to that library can be done with two steps:
+
+First, call the `setEventDispatcher()` method on the `Environment` to register that other implementation. With that done, any calls to `Environment::dispatch()` will be passed through to that other dispatcher. But we still need to let that dispatcher know about the events registered by CommonMark extensions, otherwise nothing will happen when events are dispatched.
+
+Because the `Environment` implements PSR-14's `ListenerProviderInterface` you'll also need to pass the configured `Environment` object to your event dispatcher so that it becomes aware of those available events.
+
+## Example
+
+Here's an example of a listener which uses the `DocumentParsedEvent` to add an `external-link` class to external URLs:
+
+```php
+use League\CommonMark\Environment\EnvironmentInterface;
+use League\CommonMark\Event\DocumentParsedEvent;
+use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
+
+class ExternalLinkProcessor
+{
+ private $environment;
+
+ public function __construct(EnvironmentInterface $environment)
+ {
+ $this->environment = $environment;
+ }
+
+ public function onDocumentParsed(DocumentParsedEvent $event): void
+ {
+ $document = $event->getDocument();
+ $walker = $document->walker();
+ while ($event = $walker->next()) {
+ $node = $event->getNode();
+
+ // Only stop at Link nodes when we first encounter them
+ if (!($node instanceof Link) || !$event->isEntering()) {
+ continue;
+ }
+
+ $url = $node->getUrl();
+ if ($this->isUrlExternal($url)) {
+ $node->data->append('attributes/class', 'external-link');
+ }
+ }
+ }
+
+ private function isUrlExternal(string $url): bool
+ {
+ // Only look at http and https URLs
+ if (!preg_match('/^https?:\/\//', $url)) {
+ return false;
+ }
+
+ $host = parse_url($url, PHP_URL_HOST);
+
+ return $host != $this->environment->getConfiguration()->get('host');
+ }
+}
+```
+
+And here's how you'd use it:
+
+```php
+use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Event\DocumentParsedEvent;
+
+$env = new Environment();
+
+$listener = new ExternalLinkProcessor($env);
+$env->addEventListener(DocumentParsedEvent::class, [$listener, 'onDocumentParsed']);
+
+$converter = new CommonMarkConverter(['host' => 'commonmark.thephpleague.com'], $env);
+
+$input = 'My two favorite sites are and ';
+
+echo $converter->convert($input);
+```
+
+Output (formatted for readability):
+
+```html
+
+ My two favorite sites are
+ https://google.com
+ and
+ https://commonmark.thephpleague.com
+
+```
diff --git a/docs/2.3/customization/extensions.md b/docs/2.3/customization/extensions.md
new file mode 100644
index 0000000000..9c15c0b3ea
--- /dev/null
+++ b/docs/2.3/customization/extensions.md
@@ -0,0 +1,43 @@
+---
+layout: default
+title: Extensions
+description: Creating custom extensions to add new syntax and other custom functionality
+---
+
+# Extensions
+
+Extensions provide a way to group related parsers, renderers, etc. together with pre-defined priorities, configuration settings, etc. They are perfect for distributing your customizations as reusable, open-source packages that others can plug into their own projects!
+
+To create an extension, simply create a new class implementing `ExtensionInterface`. This has a single method where you're given a `ConfigurableEnvironmentInterface` to register whatever things you need to. For example:
+
+```php
+use League\CommonMark\Extension\ExtensionInterface;
+use League\CommonMark\Environment\ConfigurableEnvironmentInterface;
+
+final class EmojiExtension implements ExtensionInterface
+{
+ public function register(ConfigurableEnvironmentInterface $environment): void
+ {
+ $environment
+ // TODO: Create the EmojiParser, Emoji, and EmojiRenderer classes
+ ->addInlineParser(new EmojiParser(), 20)
+ ->addInlineRenderer(Emoji::class, new EmojiRenderer(), 0)
+ ;
+ }
+}
+```
+
+To hook up your new extension to the `Environment`, simply do this:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\MarkdownConverter;
+
+$environment = new Environment();
+$environment->addExtension(new CommonMarkCoreExtension());
+$environment->addExtension(new EmojiExtension());
+
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('Hello! :wave:');
+```
diff --git a/docs/2.3/customization/inline-parsing.md b/docs/2.3/customization/inline-parsing.md
new file mode 100644
index 0000000000..6f23a1a9a7
--- /dev/null
+++ b/docs/2.3/customization/inline-parsing.md
@@ -0,0 +1,179 @@
+---
+layout: default
+title: Inline Parsing
+description: Parsing inline elements with a custom parser
+---
+
+# Inline Parsing
+
+There are two ways to implement custom inline syntax:
+
+- Inline Parsers (covered here)
+- [Delimiter Processors](/2.3/customization/delimiter-processing/)
+
+The difference between normal inlines and delimiter-run-based inlines is subtle but important to understand. In a nutshell, delimiter-run-based inlines:
+
+- Are denoted by "wrapping" text with one or more characters before **and** after those inner contents
+- Can contain other delimiter runs or inlines inside of them
+
+An example of this would be emphasis:
+
+```markdown
+This is an example of **emphasis**. Note how the text is *wrapped* with the same character(s) before and after.
+```
+
+If your syntax looks like that, consider using a [delimiter processor](/2.3/customization/delimiter-processing/) instead. Otherwise, an inline parser is your best bet.
+
+## Implementing Inline Parsers
+
+Inline parsers should implement `InlineParserInterface` and the following two methods:
+
+### getMatchDefinition()
+
+This method should return an instance of `InlineParserMatch` which defines the text the parser is looking for. Examples of this might be something like:
+
+```php
+use League\CommonMark\Parser\Inline\InlineParserMatch;
+
+InlineParserMatch::string('@'); // Match any '@' characters found in the text
+InlineParserMatch::string('foo'); // Match the text 'foo' (case insensitive)
+
+InlineParserMatch::oneOf('@', '!'); // Match either character
+InlineParserMatch::oneOf('http://', 'https://'); // Match either string
+
+InlineParserMatch::regex('\d+'); // Match the regular expression (omit the regex delimiters and any flags)
+```
+
+Once a match is found, the `parse()` method below may be called.
+
+### parse()
+
+This method will be called if both conditions are met:
+
+1. The engine has found at a matching string in the current line; and,
+2. No other inline parsers with a [higher priority](/2.3/customization/environment/#addinlineparser) have successfully parsed the text at this point in the line
+
+#### Parameters
+
+- `InlineParserContext $inlineContext` - Encapsulates the current state of the inline parser - see more information below.
+
+##### InlineParserContext
+
+This class has several useful methods:
+
+- `getContainer()` - Returns the current container block the inline text was found in. You'll almost always call `$inlineContext->getContainer()->appendChild(...)` to add the parsed inline text inside that block.
+- `getReferenceMap()` - Returns the document's reference map
+- `getCursor()` - Returns the current [`Cursor`](/2.3/customization/cursor/) used to parse the current line. (Note that the cursor will be positioned **before** the matched text, so you must advance it yourself if you determine it's a valid match)
+- `getDelimiterStack()` - Returns the current delimiter stack. Only used in advanced use cases.
+- `getFullMatch()` - Returns the full string that matched you `InlineParserMatch` definition
+- `getFullMatchLength()` - Returns the length of the full match - useful for advancing the cursor
+- `getSubMatches()` - If your `InlineParserMatch` used a regular expression with capture groups, this will return the text matches by those groups.
+- `getMatches()` - Returns an array where index `0` is the "full match", plus any sub-matches. It basically simulates `preg_match()`'s behavior.
+
+#### Return value
+
+`parse()` should return `false` if it's unable to handle the text at the current position for any reason. Other parsers will then have a chance to try parsing that text. If all registered parsers return false, the text will be added as plain text.
+
+Returning `true` tells the engine that you've successfully parsed the character (and related ones after it). It is your responsibility to:
+
+1. Advance the cursor to the end of the parsed/matched text
+2. Add the parsed inline to the container (`$inlineContext->getContainer()->appendChild(...)`)
+
+## Inline Parser Examples
+
+### Example 1 - Twitter Handles
+
+Let's say you wanted to autolink Twitter handles without using the link syntax. This could be accomplished by registering a new inline parser to handle the `@` character:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
+use League\CommonMark\Parser\Inline\InlineParserInterface;
+use League\CommonMark\Parser\Inline\InlineParserMatch;
+use League\CommonMark\Parser\InlineParserContext;
+
+class TwitterHandleParser implements InlineParserInterface
+{
+ public function getMatchDefinition(): InlineParserMatch
+ {
+ return InlineParserMatch::regex('@([A-Za-z0-9_]{1,15}(?!\w))');
+ }
+
+ public function parse(InlineParserContext $inlineContext): bool
+ {
+ $cursor = $inlineContext->getCursor();
+ // The @ symbol must not have any other characters immediately prior
+ $previousChar = $cursor->peek(-1);
+ if ($previousChar !== null && $previousChar !== ' ') {
+ // peek() doesn't modify the cursor, so no need to restore state first
+ return false;
+ }
+
+ // This seems to be a valid match
+ // Advance the cursor to the end of the match
+ $cursor->advanceBy($inlineContext->getFullMatchLength());
+
+ // Grab the Twitter handle
+ [$handle] = $inlineContext->getSubMatches();
+ $profileUrl = 'https://twitter.com/' . $handle;
+ $inlineContext->getContainer()->appendChild(new Link($profileUrl, '@' . $handle));
+ return true;
+ }
+}
+
+// And here's how to hook it up:
+
+$environment = new Environment();
+$environment->addExtension(new CommonMarkCoreExtension());
+$environment->addInlineParser(new TwitterHandleParser());
+```
+
+### Example 2 - Emoticons
+
+Let's say you want to automatically convert smilies (or "frownies") to emoticon images. This is incredibly easy with an inline parser:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
+use League\CommonMark\Parser\Inline\InlineParserInterface;
+use League\CommonMark\Parser\Inline\InlineParserMatch;
+use League\CommonMark\Parser\InlineParserContext;
+
+class SmilieParser implements InlineParserInterface
+{
+ public function getMatchDefinition(): InlineParserMatch
+ {
+ return InlineParserMatch::oneOf(':)', ':(');
+ }
+
+ public function parse(InlineParserContext $inlineContext): bool
+ {
+ $cursor = $inlineContext->getCursor();
+
+ // Advance the cursor past the 2 matched chars since we're able to parse them successfully
+ $cursor->advanceBy(2);
+
+ // Add the corresponding image
+ if ($inlineContext->getFullMatch() === ':)') {
+ $inlineContext->getContainer()->appendChild(new Image('/img/happy.png'));
+ } elseif ($inlineContext->getFullMatch() === ':(') {
+ $inlineContext->getContainer()->appendChild(new Image('/img/sad.png'));
+ }
+
+ return true;
+ }
+}
+
+$environment = new Environment();
+$environment->addExtension(new CommonMarkCoreExtension());
+$environment->addInlineParser(new SmilieParserParser());
+```
+
+## Tips
+
+- For best performance:
+ - Avoid using overly-complex regular expressions in `getMatchDefinition()` - use the simplest regex you can and have `parse()` do the heavier validation
+ - Have your `parse()` method `return false` **as soon as possible**.
+- You can `peek()` without modifying the cursor state. This makes it useful for validating nearby characters as it's quick and you can bail without needed to restore state.
+- You can look at (and modify) any part of the AST if needed (via `$inlineContext->getContainer()`).
diff --git a/docs/2.3/customization/overview.md b/docs/2.3/customization/overview.md
new file mode 100644
index 0000000000..77e5d41cab
--- /dev/null
+++ b/docs/2.3/customization/overview.md
@@ -0,0 +1,74 @@
+---
+layout: default
+title: Customization Overview
+description: An overview of the powerful customization features
+---
+
+# Customization Overview
+
+Ready to go beyond the basics of converting Markdown to HTML? This page describes some of the more advanced things you can customize this library to do.
+
+## Parsing and Rendering
+
+The actual process of converting Markdown to HTML has several steps:
+
+ 1. Create an [`Environment`](/2.3/customization/environment/), adding whichever extensions/parser/renders/configuration you need
+ 2. Instantiate a `MarkdownParser` and `HtmlRenderer` using that `Environment`
+ 3. Use the `MarkdownParser` to parse the Markdown input into an [Abstract Syntax Tree](/2.3/customization/abstract-syntax-tree/) (aka an "AST")
+ 4. Use the `HtmlRenderer` to convert the [AST `Document`](/2.3/customization/abstract-syntax-tree/#document) into HTML
+
+The `MarkdownConverter` class handles all of this for you, but you can execute that process yourself if you wish:
+
+```php
+use League\CommonMark\Parser\MarkdownParser;
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Renderer\HtmlRenderer;
+
+$environment = new Environment([
+ 'html_input' => 'strip',
+]);
+$environment->addExtension(new CommonMarkCoreExtension());
+
+$parser = new MarkdownParser($environment);
+$htmlRenderer = new HtmlRenderer($environment);
+
+$markdown = '# Hello World!';
+
+$document = $parser->parse($markdown);
+echo $htmlRenderer->renderDocument($document);
+
+// Hello World!
+```
+
+Feel free to swap out different components or add your own steps in between. However, the best way to customize this library is to [create your own extensions](/2.3/customization/extensions/) which hook into the parsing and rendering steps - continue reading to see which kinds of extension points are available to you.
+
+## Add Custom Syntax with Parsers
+
+Parsers examine the Markdown input and produce an abstract syntax tree (AST) of the document's structure.
+This resulting AST contains both blocks (structural elements like paragraphs, lists, headers, etc) and inlines (words, spaces, links, emphasis, etc).
+
+There are two main types of parsers:
+
+- [Block parsers](/2.3/customization/block-parsing/)
+- [Inline parsers](/2.3/customization/inline-parsing/)
+
+The parsing approach is identical for both types - examine text at the current position (via the [`Cursor`](/2.3/customization/cursor/)) and determine if you can handle it;
+if so, create the corresponding AST element,
+otherwise you abort and the engine will try other parsers. If no parser succeeds then the current text is treated as plain text.
+
+Simple delimiter-based inlines (like emphasis, strikethrough, etc.) can be parsed without needing a dedicated inline parser by leveraging the new [Delimiter Processing](/2.3/customization/delimiter-processing/) functionality.
+
+## AST manipulation
+
+Once the [Abstract Syntax Tree](/2.3/customization/abstract-syntax-tree/) is parsed, you are free to access/manipulate it as needed before it's passed into the rendering engine.
+
+## Customize HTML Output with Custom Renderers
+
+[Renderers](/2.3/customization/rendering/) convert the parsed blocks/inlines from the AST representation into HTML. When registering these with the environment, you must tell it which block/inline classes it should handle. This allows you to essentially "swap out" built-in renderers with your own.
+
+## Examples
+
+Some examples of what's possible:
+
+- [Parse Twitter handles into profile links](/2.3/customization/inline-parsing#example-1---twitter-handles)
+- [Convert smilies into emoticon images](/2.3/customization/inline-parsing#example-2---emoticons)
diff --git a/docs/2.3/customization/rendering.md b/docs/2.3/customization/rendering.md
new file mode 100644
index 0000000000..3c48a49355
--- /dev/null
+++ b/docs/2.3/customization/rendering.md
@@ -0,0 +1,143 @@
+---
+layout: default
+title: Rendering
+description: How to customize the rendering of block and inline elements
+---
+
+# Custom Rendering
+
+Renderers are responsible for converting the parsed AST elements into their HTML representation.
+
+All block renderers should implement `NodeRendererInterface` and its `render()` method. Note that in v2.0, both
+block renderers and inline renderers share the same interface and method:
+
+## render()
+
+```php
+public function render(Node $node, ChildNodeRendererInterface $childRenderer);
+```
+
+The `HtmlRenderer` will call this method during the rendering process whenever a supported element is encountered.
+
+If your renderer can only handle certain block types, be sure to verify that you've been passed the correct type.
+
+### Parameters
+
+- `Node $node` - The encountered block or inline element that needs to be rendered
+- `ChildNodeRendererInterface $childRenderer` - If the given $node has children, use this to render those child elements
+
+### Return value
+
+The method must return the final HTML representation of the node and its contents, including any children. This can be an `HtmlElement` object (preferred; castable to a string), a string of raw HTML, or `null` if it could not render (and perhaps another renderer should give it a try).
+
+If you choose to return an HTML `string` you are responsible for handling any escaping that may be necessary.
+
+#### `HtmlElement`
+
+Instead of manually building the HTML output yourself, you can leverage the `HtmlElement` to generate that for you. For example:
+
+```php
+use League\CommonMark\Util\HtmlElement;
+
+$link = new HtmlElement('a', ['href' => 'https://github.com'], 'GitHub');
+$img = new HtmlElement('img', ['src' => 'logo.jpg'], '', true);
+```
+
+## Designating Renderers
+
+When registering your renderer, you must tell the `Environment` which node element class your renderer should handle. For example:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
+
+$environment = new Environment();
+$environment->addExtension(new CommonMarkCoreExtension());
+
+// First param - the node class type that should use our renderer
+// Second param - instance of the renderer
+$environment->addRenderer(FencedCode::class, new MyCustomCodeRenderer());
+```
+
+A single renderer could even be used for multiple types:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
+use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
+
+$environment = new Environment();
+$environment->addExtension(new CommonMarkCoreExtension());
+
+$myRenderer = new MyCustomCodeRenderer();
+
+$environment->addRenderer(FencedCode::class, $myRenderer, 10);
+$environment->addRenderer(IndentedCode::class, $myRenderer, 20);
+```
+
+Multiple renderers can be added per element type - when this happens, we use the result from the highest-priority renderer that returns a non-`null` result.
+
+## Example
+
+Here's a custom renderer which renders thematic breaks as text (instead of `
`):
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
+use League\CommonMark\Node\Node;
+use League\CommonMark\Renderer\ChildNodeRendererInterface;
+use League\CommonMark\Renderer\NodeRendererInterface;
+use League\CommonMark\Util\HtmlElement;
+
+class TextDividerRenderer implements NodeRendererInterface
+{
+ public function render(Node $node, ChildNodeRendererInterface $childRenderer)
+ {
+ return new HtmlElement('pre', ['class' => 'divider'], '==============================');
+ }
+}
+
+$environment = new Environment();
+$environment->addExtension(new CommonMarkCoreExtension());
+$environment->addRenderer(ThematicBreak::class, new TextDividerRenderer());
+```
+
+Note that thematic breaks should not contain children, which is why the `$childRenderer` is unused in this example. Otherwise we'd have to call code like this and return the result as part of the rendered HTML we're generating here: `$innerHtml = $childRenderer->renderNodes($node->children());`
+
+## Tips
+
+- Return an `HtmlElement` if possible. This makes it easier to extend and modify the results later.
+- Don't forget to render any child elements that your node might contain!
+
+## XML Rendering
+
+The [XML renderer](/2.3/xml/) will automatically attempt to convert any AST nodes to XML by inspecting the name of the block/inline node and its attributes. You can instead control the XML element name and attributes by making your renderer implement `XmlNodeRendererInterface`:
+
+```php
+use League\CommonMark\Node\Node;
+use League\CommonMark\Renderer\ChildNodeRendererInterface;
+use League\CommonMark\Renderer\NodeRendererInterface;
+use League\CommonMark\Util\HtmlElement;
+use League\CommonMark\Xml\XmlNodeRendererInterface;
+
+class TextDividerRenderer implements NodeRendererInterface, XmlNodeRendererInterface
+{
+ public function render(Node $node, ChildNodeRendererInterface $childRenderer)
+ {
+ return new HtmlElement('pre', ['class' => 'divider'], '==============================');
+ }
+
+ public function getXmlTagName(Node $node): string
+ {
+ return 'text_divider';
+ }
+
+ public function getXmlAttributes(Node $node): array
+ {
+ return ['character' => '='];
+ }
+}
+```
diff --git a/docs/2.3/customization/slug-normalizer.md b/docs/2.3/customization/slug-normalizer.md
new file mode 100644
index 0000000000..d268301b9b
--- /dev/null
+++ b/docs/2.3/customization/slug-normalizer.md
@@ -0,0 +1,103 @@
+---
+layout: default
+title: Slug Normalizer
+description: Using the Slug Normalizer to produce unique, URL-safe text strings
+---
+
+# Slug Normalizer
+
+"Slugs" are strings used within `href`, `name`, and `id` HTML attributes to identify particular elements within a document.
+
+Some extensions (like the `HeadingPermalinkExtension`) need the ability to convert user-provided text into these URL-safe slugs while also ensuring that these are unique throughout the generated HTML. The `Environment` provides a pre-built normalizer you can use for this purpose.
+
+## Usage
+
+You can obtain a reference to the built-in slug normalizer by calling `$environment->getSlugNormalizer()`;
+
+To use this within your extension, have your parser/renderer/whatever implement `EnvironmentAwareInterface` and then implement the corresponding `setEnvironment` method like this:
+
+```php
+
+use League\CommonMark\Environment\EnvironmentInterface;
+use League\CommonMark\Environment\EnvironmentAwareInterface;
+
+class MyCustomParserOrRenderer implements EnvironmentAwareInterface
+{
+ private $slugNormalizer;
+
+ public function setEnvironment(EnvironmentInterface $environment): void
+ {
+ $this->slugNormalizer = $environment->getSlugNormalizer();
+ }
+}
+```
+
+You can then call `$this->slugNormalizer->normalize($text)` as needed.
+
+## Configuration
+
+The `slug_normalizer` configuration section allows you to adjust the following options:
+
+### `instance`
+
+You can change the string that is used as the "slug" by setting the `instance` option to any class that implements `TextNormalizerInterface`.
+We provide a simple `SlugNormalizer` by default, but you may want to plug in a different library or create your own normalizer instead.
+
+For example, if you'd like each slug to be an MD5 hash, you could create a class like this:
+
+```php
+use League\CommonMark\Normalizer\TextNormalizerInterface;
+
+final class MD5Normalizer implements TextNormalizerInterface
+{
+ public function normalize(string $text, $context = null): string
+ {
+ return md5($text);
+ }
+}
+```
+
+And then configure it like this:
+
+```php
+$config = [
+ 'slug_normalizer' => [
+ // ... other options here ...
+ 'instance' => new MD5Normalizer(),
+ ],
+];
+```
+
+Or you could use [PHP's anonymous class feature](https://www.php.net/manual/en/language.oop5.anonymous.php) to define the generator's behavior without creating a new class file:
+
+```php
+$config = [
+ 'slug_normalizer' => [
+ // ... other options here ...
+ 'instance' => new class implements TextNormalizerInterface {
+ public function normalize(string $text, $context = null): string
+ {
+ // TODO: Implement your code here
+ }
+ },
+ ],
+];
+```
+
+### `max_length`
+
+This can be configured to limit the length of that slug to prevent overly-long values. By default, that limit is `255` characters. You may set this to any positive integer, or `0` for no limit.
+
+(Note that generated slugs might be slightly longer than this "limit" if the `unique` option is enabled and the slug generator detects a duplicate slug and needs to add a suffix to make it unique.)
+
+### `unique`
+
+This options controls whether slugs should be unique. Possible values include:
+
+- `'document'` (string; **default**) - Ensures slugs are unique within a single document
+- `'environment'` (string) - Ensures slugs are unique across multiple documents - see below
+- `false` (boolean) - Disables unique slug generation
+
+You might have a use case where you're converting several different Markdown documents on the same page and so you'd like to ensure that none of those documents use conflicting slugs. In that case, you should set the `scope` option to `'environment'` to ensure that a single instance of a `MarkdownConverter` (which uses a single `Environment`) will never produce the same slug twice during its lifetime (which usually lasts the entire duration of a single HTTP request).
+
+If you need complete control over how unique slugs are generated, make your `'instance'` implement `UniqueSlugNormalizerInterface`; otherwise, we'll simply append incremental numbers to slugs to ensure they are unique.
diff --git a/docs/2.3/extensions/attributes.md b/docs/2.3/extensions/attributes.md
new file mode 100644
index 0000000000..63b39bec87
--- /dev/null
+++ b/docs/2.3/extensions/attributes.md
@@ -0,0 +1,90 @@
+---
+layout: default
+title: Attributes Extension
+description: The AttributesExtension allows HTML attributes to be added from within the document.
+---
+
+# Attributes
+
+The `AttributesExtension` allows HTML attributes to be added from within the document.
+
+## Attribute Syntax
+
+The basic syntax was inspired by [Kramdown](http://kramdown.gettalong.org/syntax.html#attribute-list-definitions)'s Attribute Lists feature.
+
+You can assign any attribute to a block-level element. Just directly prepend or follow the block with a block inline attribute list.
+That consists of a left curly brace, optionally followed by a colon, the attribute definitions and a right curly brace:
+
+```markdown
+> A nice blockquote
+{: title="Blockquote title"}
+```
+
+This results in the following output:
+
+```html
+
+A nice blockquote
+
+```
+
+CSS-selector-style declarations can be used to set the `id` and `class` attributes:
+
+```markdown
+{#id .class}
+## Header
+```
+
+Output:
+
+```html
+Header
+```
+
+As with a block-level element you can assign any attribute to a span-level elements using a span inline attribute list,
+that has the same syntax and must immediately follow the span-level element:
+
+```markdown
+This is *red*{style="color: red"}.
+```
+
+Output:
+
+```html
+This is red.
+```
+
+## Installation
+
+This extension is bundled with `league/commonmark`. This library can be installed via Composer:
+
+```bash
+composer require league/commonmark
+```
+
+See the [installation](/2.3/installation/) section for more details.
+
+## Usage
+
+Configure your `Environment` as usual and simply add the `AttributesExtension`:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\Attributes\AttributesExtension;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\MarkdownConverter;
+
+// Define your configuration, if needed
+$config = [];
+
+// Configure the Environment with all the CommonMark parsers/renderers
+$environment = new Environment($config);
+$environment->addExtension(new CommonMarkCoreExtension());
+
+// Add this extension
+$environment->addExtension(new AttributesExtension());
+
+// Instantiate the converter engine and start converting some Markdown!
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('# Hello World!');
+```
diff --git a/docs/2.3/extensions/autolinks.md b/docs/2.3/extensions/autolinks.md
new file mode 100644
index 0000000000..6ffda434f5
--- /dev/null
+++ b/docs/2.3/extensions/autolinks.md
@@ -0,0 +1,53 @@
+---
+layout: default
+title: Autolink Extension
+description: The Autolink extension automatically converts URLs in plain text to clickable links
+---
+
+# Autolink Extension
+
+_(Note: this extension is included by default within [the GFM extension](/2.3/extensions/github-flavored-markdown/))_
+
+The `AutolinkExtension` adds [GFM-style autolinking][link-gfm-spec-autolinking]. It automatically links URLs and email addresses even when the CommonMark `<...>` autolink syntax is not used.
+
+## Installation
+
+This extension is bundled with `league/commonmark`. This library can be installed via Composer:
+
+```bash
+composer require league/commonmark
+```
+
+See the [installation](/2.3/installation/) section for more details.
+
+## Usage
+
+Configure your `Environment` as usual and simply add the `AutolinkExtension` provided by this package:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\Autolink\AutolinkExtension;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\MarkdownConverter;
+
+// Define your configuration, if needed
+$config = [];
+
+// Configure the Environment with all the CommonMark parsers/renderers
+$environment = new Environment($config);
+$environment->addExtension(new CommonMarkCoreExtension());
+
+// Add this extension
+$environment->addExtension(new AutolinkExtension());
+
+// Instantiate the converter engine and start converting some Markdown!
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('I successfully installed the https://github.com/thephpleague/commonmark project with the Autolink extension!');
+```
+
+## `@mention`-style Autolinking
+
+As of v1.5, [mention autolinking is now handled by a Mention Parser outside of this extension](/2.3/extensions/mention/).
+
+[link-league-commonmark]: https://github.com/thephpleague/commonmark
+[link-gfm-spec-autolinking]: https://github.github.com/gfm/#autolinks-extension-
diff --git a/docs/2.3/extensions/commonmark.md b/docs/2.3/extensions/commonmark.md
new file mode 100644
index 0000000000..aa442f745c
--- /dev/null
+++ b/docs/2.3/extensions/commonmark.md
@@ -0,0 +1,53 @@
+---
+layout: default
+title: CommonMark Core Extension
+description: The CommonMarkCoreExtension class includes all core Markdown syntax
+---
+
+# CommonMark Core Extension
+
+The `CommonMarkCoreExtension` class contains all of the core Markdown syntax - things like parsing headers, code blocks, links, image, etc.
+
+## Installation
+
+This extension is bundled with `league/commonmark`. This library can be installed via Composer:
+
+```bash
+composer require league/commonmark
+```
+
+See the [installation](/2.3/installation/) section for more details.
+
+## Included by Default
+
+This extension is automatically installed for you (behind-the-scenes) whenever you instantiate the parser using the `CommonMarkConverter` class:
+
+```php
+use League\CommonMark\CommonMarkConverter;
+
+$converter = new CommonMarkConverter();
+echo $converter->convert('# Hello World!');
+```
+
+## Manual Usage
+
+If you ever create a `new Environment()` from scratch, you'll probably want to include the `CommonMarkCoreExtension()` so you get all the standard Markdown syntax included:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\MarkdownConverter;
+
+// Define your configuration, if needed
+$config = [];
+
+// Create a new Environment with the core extension
+$environment = new Environment($config);
+$environment->addExtension(new CommonMarkCoreExtension());
+
+// Instantiate the converter engine and start converting some Markdown!
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('# Hello World!');
+```
+
+Alternatively, if you don't want all of the core Markdown syntax, avoid using `CommonMarkCoreExtension`. You can always add just the individual parsers, renderers, etc. you actually want with the [`Environment`](/2.3/customization/environment/). (This is actually how the [Inlines Only Extension](/2.3/extensions/inlines-only/) works - it only includes a subset of things that `CommonMarkCoreExtension` does!)
diff --git a/docs/2.3/extensions/default-attributes.md b/docs/2.3/extensions/default-attributes.md
new file mode 100644
index 0000000000..a46cbddb17
--- /dev/null
+++ b/docs/2.3/extensions/default-attributes.md
@@ -0,0 +1,121 @@
+---
+layout: default
+title: Default Attributes Extension
+description: The DefaultAttributesExtension allows you to apply default HTML classes and other attributes using configuration options.
+---
+
+# Default Attributes
+
+The `DefaultAttributesExtension` allows you to apply default HTML classes and other attributes using configuration options.
+
+It works by applying the attributes to the nodes during the [`DocumentParsedEvent` event](/2.3/customization/abstract-syntax-tree/#documentparsedevent) - right after the nodes are parsed but before they are rendered.
+(As a result, it's possible that renderers may add other attributes - the goal of this extension is only to provide custom defaults.)
+
+## Installation
+
+This extension is bundled with `league/commonmark`. This library can be installed via Composer:
+
+```bash
+composer require league/commonmark
+```
+
+See the [installation](/2.3/installation/) section for more details.
+
+## Usage
+
+Configure your `Environment` as usual and simply add the `DefaultAttributesExtension`:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
+use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
+use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
+use League\CommonMark\Extension\Table\Table;
+use League\CommonMark\MarkdownConverter;
+use League\CommonMark\Node\Block\Paragraph;
+
+// Define your configuration, if needed
+// Extension defaults are shown below
+// If you're happy with the defaults, feel free to remove them from this array
+$config = [
+ 'default_attributes' => [
+ Heading::class => [
+ 'class' => static function (Heading $node) {
+ if ($node->getLevel() === 1) {
+ return 'title-main';
+ } else {
+ return null;
+ }
+ },
+ ],
+ Table::class => [
+ 'class' => 'table',
+ ],
+ Paragraph::class => [
+ 'class' => ['text-center', 'font-comic-sans'],
+ ],
+ Link::class => [
+ 'class' => 'btn btn-link',
+ 'target' => '_blank',
+ ],
+ ],
+];
+
+// Configure the Environment with all the CommonMark parsers/renderers
+$environment = new Environment($config);
+$environment->addExtension(new CommonMarkCoreExtension());
+
+// Add the extension
+$environment->addExtension(new DefaultAttributesExtension());
+
+// Instantiate the converter engine and start converting some Markdown!
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('# Hello World!');
+```
+
+## Configuration
+
+This extension can be configured by providing a `default_attributes` array. Each key in the array should be a FQCN for the node class you wish to apply the default attribute to, and the values should be a map of attribute names to attribute values.
+
+Attribute values may be any of the following types:
+
+- `string`
+- `string[]`
+- `bool`
+- `callable` (parameter is the `Node`, return value may be `string|string[]|bool`)
+
+## Examples
+
+Here's an example that will apply Bootstrap 4 classes and attributes:
+
+```php
+$config = [
+ 'default_attributes' => [
+ Table::class => [
+ 'class' => ['table', 'table-responsive'],
+ ],
+ BlockQuote::class => [
+ 'class' => 'blockquote',
+ ],
+ ],
+];
+```
+
+Here's a more complex example that uses a `callable` to add a class only if the paragraph immediately follows an `` heading:
+
+```php
+$config = [
+ 'default_attributes' => [
+ Paragraph::class => [
+ 'class' => static function (Paragraph $paragraph) {
+ if ($paragraph->previous() instanceof Heading && $paragraph->previous()->getLevel() === 1) {
+ return 'lead';
+ }
+
+ return null;
+ },
+ ],
+ ],
+];
+```
diff --git a/docs/2.3/extensions/description-lists.md b/docs/2.3/extensions/description-lists.md
new file mode 100644
index 0000000000..66d63cebdf
--- /dev/null
+++ b/docs/2.3/extensions/description-lists.md
@@ -0,0 +1,76 @@
+---
+layout: default
+title: Description List Extension
+description: The Description List extension adds support for Markdown Extra-style lists
+---
+
+# Description List Extension
+
+The `DescriptionListExtension` adds [Markdown Extra-style description lists][link-markdown-extra-dl] to facilitate the creation of ``, `- `, and `
- ` HTML using Markdown.
+
+## Installation
+
+This extension is bundled with `league/commonmark`. This library can be installed via Composer:
+
+```bash
+composer require league/commonmark
+```
+
+See the [installation](/2.3/installation/) section for more details.
+
+## Usage
+
+Configure your `Environment` as usual and simply add the `DescriptionListExtension` provided by this package:
+
+```php
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\DescriptionList\DescriptionListExtension;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\MarkdownConverter;
+
+// Define your configuration, if needed
+$config = [];
+
+// Configure the Environment with all the CommonMark parsers/renderers
+$environment = new Environment($config);
+$environment->addExtension(new CommonMarkCoreExtension());
+
+// Add this extension
+$environment->addExtension(new DescriptionListExtension());
+
+// Instantiate the converter engine and start converting some Markdown!
+$converter = new MarkdownConverter($environment);
+echo $converter->convert('Some markdown goes here');
+```
+
+## Syntax
+
+The syntax is based directly on the rules and logic implemented by the [Markdown Extra library][link-markdown-extra-dl]. Here are some examples of sample Markdown input and HTML output demonstrating the syntax:
+
+```md
+Apple
+: Pomaceous fruit of plants of the genus Malus in
+ the family Rosaceae.
+: An American computer company.
+
+Orange
+: The fruit of an evergreen tree of the genus Citrus.
+```
+
+```html
+
+ - Apple
+ - Pomaceous fruit of plants of the genus Malus in
+ the family Rosaceae.
+ - An American computer company.
+
+ - Orange
+ - The fruit of an evergreen tree of the genus Citrus.
+
+```
+
+See the [Markdown Extra documentation][link-markdown-extra-dl] or [our own spec][link-commonmark-description-list-spec] for additional examples.
+
+[link-league-commonmark]: https://github.com/thephpleague/commonmark
+[link-markdown-extra-dl]: https://michelf.ca/projects/php-markdown/extra/#def-list
+[link-commonmark-description-list-spec]: https://github.com/thephpleague/commonmark/blob/2.0/tests/functional/Extension/DescriptionList/spec.txt
diff --git a/docs/2.3/extensions/disallowed-raw-html.md b/docs/2.3/extensions/disallowed-raw-html.md
new file mode 100644
index 0000000000..ffdd73c6f3
--- /dev/null
+++ b/docs/2.3/extensions/disallowed-raw-html.md
@@ -0,0 +1,76 @@
+---
+layout: default
+title: Disallowed Raw HTML Extension
+description: The DisallowedRawHtmlExtension automatically escapes certain HTML tags when rendering raw HTML
+---
+
+# Disallowed Raw HTML Extension
+
+_(Note: this extension is included by default within [the GFM extension](/2.3/extensions/github-flavored-markdown/))_
+
+The `DisallowedRawHtmlExtension` automatically escapes certain HTML tags when rendering raw HTML, such as:
+
+- ``
+- `