Normalize Headings Extension
This extension rewrites the heading levels in your document so that the resulting HTML is valid and sits wherever you need it to.
It always repairs headings which skip a level - an <h1> followed by an <h3> is invalid HTML, and
becomes an <h1> followed by an <h2>. Documents whose headings are already valid are left alone.
It can also constrain headings to a configured range of levels. For example, if you configure the
allowed levels as 2 through 4:
#headings (<h1>) will be converted to<h2>#####and######headings (<h5>/<h6>) will be converted to<h4>
And it can optionally rebase each document so that its headings always begin at that lowest level, regardless of which level the author started at.
Installation
This extension is bundled with league/commonmark. This library can be installed via Composer:
composer require league/commonmark
See the installation section for more details.
Usage
This extension can be added to any new Environment:
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\NormalizeHeadings\NormalizeHeadingsExtension;
use League\CommonMark\MarkdownConverter;
// Extension defaults are shown below
// If you're happy with the defaults, feel free to remove them from this array
$config = [
'normalize_headings' => [
'min_level' => 1,
'max_level' => 6,
'rebase_to_min_level' => false,
],
];
// Configure the Environment with all the CommonMark parsers/renderers
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
// Add this extension
$environment->addExtension(new NormalizeHeadingsExtension());
// Instantiate the converter engine and start converting some Markdown!
$converter = new MarkdownConverter($environment);
echo $converter->convert("# Heading 1\n\n## Heading 2");
Repairing Skipped Levels
The W3C validator reports an error when a heading descends by more than one
level at a time - an <h1> may be followed by an <h2>, but not by an <h3>. (Ascending by any amount
is fine.)
This extension always rewrites headings so that each one descends at most one level below the heading it’s nested under. A heading’s new level is based on how deeply it’s nested, so headings which were siblings in the original document remain siblings:
# Header 1
#### Header 4a
#### Header 4b
# Header 1a
### Header 3
produces <h1>, <h2>, <h2>, <h1>, <h2>.
Documents whose headings are already valid come out unchanged.
Configuration
This extension can be configured by providing a normalize_headings array with three nested options.
min_level and max_level
These two settings control the allowed heading range. By default, all six levels (1 to 6) are allowed.
The min_level value must be less than or equal to max_level.
When a heading falls outside the configured range, it is converted to the nearest boundary:
- below
min_level->min_level - above
max_level->max_level
Setting min_level and max_level to the same value will therefore flatten every heading to that level.
rebase_to_min_level
By default, top-level headings keep the level they were written at, so a document which deliberately
starts at <h3> stays there and only its skipped levels are repaired. This respects an author who starts
at <h2> because the surrounding page template already provides the <h1>.
When rebase_to_min_level is enabled, top-level headings are moved to min_level instead, so that every
document begins at a known level regardless of how it was written. Consider a document which starts at
<h3>:
### Introduction
##### A Nested Heading
### Conclusion
With the default min_level of 1:
rebase_to_min_level |
Output |
|---|---|
false (default) |
<h3>, <h4>, <h3> |
true |
<h1>, <h2>, <h1> |
This works in both directions: with a min_level of 2, a document starting at <h1> would be moved
down so that its top-level headings become <h2>.