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

Commit dab8258

Browse files
committed
Merge pull request #31 from srawlins/add-ids-to-headers
Adding ids to headers
2 parents 8f21d74 + 1d66820 commit dab8258

File tree

8 files changed

+124
-2
lines changed

8 files changed

+124
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* Inline HTML syntax support; This is also considered an extension (#18).
99
* The text `[foo] (bar)` now parses as an inline link (#53).
1010
* The text `[foo]()` now renders as an inline link.
11+
* Header identifier support in the HeaderWithIdSyntax and
12+
SetextHeaderWithIdSyntax extensions.
1113

1214
## 0.8.0
1315

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,17 @@ specifying an Array of extension syntaxes in the `blockSyntaxes` or
2525
The currently supported inline extension syntaxes are:
2626

2727
* `new InlineHtmlSyntax()` - approximately CommonMark's
28-
[definition](http://spec.commonmark.org/0.22/#raw-html) of "Raw HTML".
28+
[definition][commonmark-raw-html] of "Raw HTML".
2929

3030
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`][pandoc-auto_identifiers]).
36+
* `const SetextHeaderWithIdSyntax()` - Setext-style headers have generated IDs
37+
for link anchors (akin to Pandoc's
38+
[`auto_identifiers`][pandoc-auto_identifiers]).
3439

3540
For example:
3641

@@ -75,3 +80,5 @@ void main() {
7580

7681
[Perl Markdown]: http://daringfireball.net/projects/markdown/
7782
[CommonMark]: http://commonmark.org/
83+
[commonMark-raw-html]: http://spec.commonmark.org/0.22/#raw-html
84+
[pandoc-auto_identifiers]: http://pandoc.org/README.html#extension-auto_identifiers

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+
/// Generates a valid HTML anchor from the inner text of [element].
148+
static String generateAnchorHash(Element element) =>
149+
_concatenatedText(element)
150+
.toLowerCase()
151+
.trim()
152+
.replaceFirst(new RegExp(r'^[^a-z]+'), '')
153+
.replaceAll(new RegExp(r'[^a-z0-9 _-]'), '')
154+
.replaceAll(new RegExp(r'\s'), '-');
155+
156+
/// Concatenates the text found 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+
Set<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+
/// Uniquifies 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)