Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit e23be05

Browse files
committed
Adding ids to headers
1 parent 8f21d74 commit e23be05

File tree

7 files changed

+118
-1
lines changed

7 files changed

+118
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ The currently supported block extension syntaxes are:
3131

3232
* `const FencedCodeBlockSyntax()` - Code blocks familiar to Pandoc and PHP
3333
Markdown Extra users.
34+
* `const HeaderWithIdSyntax()` - ATX-style headers have generated IDs, for link
35+
anchors (akin to Pandoc's [`auto_identifiers`](http://pandoc.org/README.html#extension-auto_identifiers)).
36+
* `const SetextHeaderWithIdSyntax()` - Setext-style headers have generated IDs
37+
for link anchors (akin to Pandoc's [`auto_identifiers`](http://pandoc.org/README.html#extension-auto_identifiers)).
3438

3539
For example:
3640

lib/src/ast.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Element implements Node {
1717
final String tag;
1818
final List<Node> children;
1919
final Map<String, String> attributes;
20+
String generatedId;
2021

2122
Element(this.tag, this.children) : attributes = <String, String>{};
2223

lib/src/block_parser.dart

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ class BlockParser {
111111
}
112112

113113
abstract class BlockSyntax {
114-
115114
const BlockSyntax();
116115

117116
/// Gets the regex used to identify the beginning of this block, if any.
@@ -144,6 +143,20 @@ abstract class BlockSyntax {
144143
if (parser.isDone) return true;
145144
return parser.blockSyntaxes.any((s) => s.canParse(parser) && s.canEndBlock);
146145
}
146+
147+
/// Generate a valid HTML anchor from the inner text of [element].
148+
static String generateAnchorHash(Element element) =>
149+
_concatenatedText(element)
150+
.toLowerCase()
151+
.replaceAll(new RegExp(r'^[^a-z]+'), '')
152+
.replaceAll(new RegExp(r'[^a-z0-9 _-]'), '')
153+
.replaceAll(new RegExp(r'\s'), '-')
154+
.replaceAll(new RegExp(r'-$'), '');
155+
156+
/// Concatenate the text fonud in all the children of [element].
157+
static String _concatenatedText(Element element) => element.children
158+
.map((child) => (child is Text) ? child.text : _concatenatedText(child))
159+
.join('');
147160
}
148161

149162
class EmptyBlockSyntax extends BlockSyntax {
@@ -181,6 +194,18 @@ class SetextHeaderSyntax extends BlockSyntax {
181194
}
182195
}
183196

197+
/// Parses setext-style headers, and adds generated IDs to the generated
198+
/// elements.
199+
class SetextHeaderWithIdSyntax extends SetextHeaderSyntax {
200+
const SetextHeaderWithIdSyntax();
201+
202+
Node parse(BlockParser parser) {
203+
var element = super.parse(parser);
204+
element.generatedId = BlockSyntax.generateAnchorHash(element);
205+
return element;
206+
}
207+
}
208+
184209
/// Parses atx-style headers: `## Header ##`.
185210
class HeaderSyntax extends BlockSyntax {
186211
RegExp get pattern => _headerPattern;
@@ -196,6 +221,17 @@ class HeaderSyntax extends BlockSyntax {
196221
}
197222
}
198223

224+
/// Parses atx-style headers, and adds generated IDs to the generated elements.
225+
class HeaderWithIdSyntax extends HeaderSyntax {
226+
const HeaderWithIdSyntax();
227+
228+
Node parse(BlockParser parser) {
229+
var element = super.parse(parser);
230+
element.generatedId = BlockSyntax.generateAnchorHash(element);
231+
return element;
232+
}
233+
}
234+
199235
/// Parses email-style blockquotes: `> quote`.
200236
class BlockquoteSyntax extends BlockSyntax {
201237
RegExp get pattern => _blockquotePattern;

lib/src/html_renderer.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
library markdown.src.html_renderer;
66

7+
import 'dart:collection';
8+
79
import 'ast.dart';
810
import 'document.dart';
911
import 'extension_set.dart';
@@ -40,11 +42,13 @@ class HtmlRenderer implements NodeVisitor {
4042
static final _blockTags = new RegExp('blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre');
4143

4244
StringBuffer buffer;
45+
LinkedHashSet<String> uniqueIds;
4346

4447
HtmlRenderer();
4548

4649
String render(List<Node> nodes) {
4750
buffer = new StringBuffer();
51+
uniqueIds = new LinkedHashSet<String>();
4852

4953
for (final node in nodes) node.accept(this);
5054

@@ -71,6 +75,11 @@ class HtmlRenderer implements NodeVisitor {
7175
buffer.write(' $name="${element.attributes[name]}"');
7276
}
7377

78+
// attach header anchor ids generated from text
79+
if (element.generatedId != null) {
80+
buffer.write(' id="${uniquifyId(element.generatedId)}"');
81+
}
82+
7483
if (element.isEmpty) {
7584
// Empty element like <hr/>.
7685
buffer.write(' />');
@@ -84,4 +93,20 @@ class HtmlRenderer implements NodeVisitor {
8493
void visitElementAfter(Element element) {
8594
buffer.write('</${element.tag}>');
8695
}
96+
97+
/// Uniquify an id generated from text
98+
String uniquifyId(String id) {
99+
if (!uniqueIds.contains(id)) {
100+
uniqueIds.add(id);
101+
return id;
102+
}
103+
104+
int suffix = 2;
105+
String suffixedId = '$id-$suffix';
106+
while (uniqueIds.contains(suffixedId)) {
107+
suffixedId = '$id-${suffix++}';
108+
}
109+
uniqueIds.add(suffixedId);
110+
return suffixedId;
111+
}
87112
}

test/extensions/headers_with_ids.unit

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
>>> simple header
2+
# header
3+
4+
<<<
5+
<h1 id="header">header</h1>
6+
>>> header that starts with garbage
7+
## 2. header again
8+
9+
<<<
10+
<h2 id="header-again">2. header again</h2>
11+
>>> header with inline syntaxes
12+
### headers **rock** `etc.`
13+
14+
<<<
15+
<h3 id="headers-rock-etc">headers <strong>rock</strong> <code>etc.</code></h3>
16+
>>> non-unique headers
17+
# header
18+
19+
## header
20+
21+
<<<
22+
<h1 id="header">header</h1>
23+
<h2 id="header-2">header</h2>
24+
>>> header starts with inline syntax
25+
# *headers* etc.
26+
<<<
27+
<h1 id="headers-etc"><em>headers</em> etc.</h1>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
>>> h1
2+
text
3+
===
4+
5+
<<<
6+
<h1 id="text">text</h1>
7+
>>> h2
8+
text
9+
---
10+
11+
<<<
12+
<h2 id="text">text</h2>
13+
>>> header with inline syntax
14+
header *emphasised*
15+
===
16+
17+
<<<
18+
<h1 id="header-emphasised">header <em>emphasised</em></h1>

test/markdown_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ nyan''', '''<p>~=[,,_,,]:3</p>
105105

106106
testFile('extensions/inline_html.unit',
107107
inlineSyntaxes: [new InlineHtmlSyntax()]);
108+
109+
testFile('extensions/headers_with_ids.unit',
110+
blockSyntaxes: [const HeaderWithIdSyntax()]);
111+
112+
testFile('extensions/setext_headers_with_ids.unit',
113+
blockSyntaxes: [const SetextHeaderWithIdSyntax()]);
108114
}

0 commit comments

Comments
 (0)