Skip to content

Commit dfbc9a1

Browse files
authored
Merge pull request #1401 from styfle/slugger2
Fix duplicate heading id
2 parents 6dff94d + 632ac5d commit dfbc9a1

File tree

7 files changed

+104
-23
lines changed

7 files changed

+104
-23
lines changed

lib/marked.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -953,13 +953,13 @@ Renderer.prototype.html = function(html) {
953953
return html;
954954
};
955955

956-
Renderer.prototype.heading = function(text, level, raw) {
956+
Renderer.prototype.heading = function(text, level, raw, slugger) {
957957
if (this.options.headerIds) {
958958
return '<h'
959959
+ level
960960
+ ' id="'
961961
+ this.options.headerPrefix
962-
+ raw.toLowerCase().replace(/[^\w]+/g, '-')
962+
+ slugger.slug(raw)
963963
+ '">'
964964
+ text
965965
+ '</h'
@@ -1108,6 +1108,7 @@ function Parser(options) {
11081108
this.options.renderer = this.options.renderer || new Renderer();
11091109
this.renderer = this.options.renderer;
11101110
this.renderer.options = this.options;
1111+
this.slugger = new Slugger();
11111112
}
11121113

11131114
/**
@@ -1186,7 +1187,8 @@ Parser.prototype.tok = function() {
11861187
return this.renderer.heading(
11871188
this.inline.output(this.token.text),
11881189
this.token.depth,
1189-
unescape(this.inlineText.output(this.token.text)));
1190+
unescape(this.inlineText.output(this.token.text)),
1191+
this.slugger);
11901192
}
11911193
case 'code': {
11921194
return this.renderer.code(this.token.text,
@@ -1283,6 +1285,37 @@ Parser.prototype.tok = function() {
12831285
}
12841286
};
12851287

1288+
/**
1289+
* Slugger generates header id
1290+
*/
1291+
1292+
function Slugger () {
1293+
this.seen = {};
1294+
}
1295+
1296+
/**
1297+
* Convert string to unique id
1298+
*/
1299+
1300+
Slugger.prototype.slug = function (value) {
1301+
var slug = value
1302+
.toLowerCase()
1303+
.trim()
1304+
.replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '')
1305+
.replace(/\s/g, '-');
1306+
1307+
if (this.seen.hasOwnProperty(slug)) {
1308+
var originalSlug = slug;
1309+
do {
1310+
this.seen[originalSlug]++;
1311+
slug = originalSlug + '-' + this.seen[originalSlug];
1312+
} while (this.seen.hasOwnProperty(slug));
1313+
}
1314+
this.seen[slug] = 0;
1315+
1316+
return slug;
1317+
};
1318+
12861319
/**
12871320
* Helpers
12881321
*/
@@ -1617,6 +1650,8 @@ marked.lexer = Lexer.lex;
16171650
marked.InlineLexer = InlineLexer;
16181651
marked.inlineLexer = InlineLexer.output;
16191652

1653+
marked.Slugger = Slugger;
1654+
16201655
marked.parse = marked;
16211656

16221657
if (typeof module !== 'undefined' && typeof exports === 'object') {

test/new/cm_blockquotes.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ <h3 id="example-192">Example 192</h3>
1111
<p>The spaces after the <code>&gt;</code> characters can be omitted:</p>
1212

1313
<blockquote>
14-
<h1 id="foo">Foo</h1>
14+
<h1 id="bar">Bar</h1>
1515
<p>bar
1616
baz</p>
1717
</blockquote>
@@ -21,7 +21,7 @@ <h3 id="example-193">Example 193</h3>
2121
<p>The <code>&gt;</code> characters can be indented 1-3 spaces:</p>
2222

2323
<blockquote>
24-
<h1 id="foo">Foo</h1>
24+
<h1 id="baz">Baz</h1>
2525
<p>bar
2626
baz</p>
2727
</blockquote>
@@ -30,7 +30,7 @@ <h3 id="example-194">Example 194</h3>
3030

3131
<p>Four spaces gives us a code block:</p>
3232

33-
<pre><code>&gt; # Foo
33+
<pre><code>&gt; # Qux
3434
&gt; bar
3535
&gt; baz</code></pre>
3636

@@ -39,7 +39,7 @@ <h3 id="example-195">Example 195</h3>
3939
<p>The Laziness clause allows us to omit the <code>&gt;</code> before paragraph continuation text:</p>
4040

4141
<blockquote>
42-
<h1 id="foo">Foo</h1>
42+
<h1 id="quux">Quux</h1>
4343
<p>bar
4444
baz</p>
4545
</blockquote>

test/new/cm_blockquotes.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@
88

99
The spaces after the `>` characters can be omitted:
1010

11-
># Foo
11+
># Bar
1212
>bar
1313
> baz
1414
1515
### Example 193
1616

1717
The `>` characters can be indented 1-3 spaces:
1818

19-
> # Foo
19+
> # Baz
2020
> bar
2121
> baz
2222
2323
### Example 194
2424

2525
Four spaces gives us a code block:
2626

27-
> # Foo
27+
> # Qux
2828
> bar
2929
> baz
3030

3131
### Example 195
3232

3333
The Laziness clause allows us to omit the `>` before paragraph continuation text:
3434

35-
> # Foo
35+
> # Quux
3636
> bar
3737
baz
3838

test/new/toplevel_paragraphs.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<h1 id="how-are-you">how are you</h1>
1313

1414
<p>paragraph before head with equals</p>
15-
<h1 id="how-are-you">how are you</h1>
15+
<h1 id="how-are-you-again">how are you again</h1>
1616

1717
<p>paragraph before blockquote</p>
1818
<blockquote><p>text for blockquote</p></blockquote>

test/new/toplevel_paragraphs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ paragraph before head with hash
1717
# how are you
1818

1919
paragraph before head with equals
20-
how are you
20+
how are you again
2121
===========
2222

2323
paragraph before blockquote

test/original/markdown_documentation_basics.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1>Markdown: Basics</h1>
1+
<h1 id="markdown-basics">Markdown: Basics</h1>
22

33
<ul id="ProjectSubmenu">
44
<li><a href="/projects/markdown/" title="Markdown Project Page">Main</a></li>
@@ -8,7 +8,7 @@ <h1>Markdown: Basics</h1>
88
<li><a href="/projects/markdown/dingus" title="Online Markdown Web Form">Dingus</a></li>
99
</ul>
1010

11-
<h2>Getting the Gist of Markdown's Formatting Syntax</h2>
11+
<h2 id="getting-the-gist-of-markdowns-formatting-syntax">Getting the Gist of Markdown's Formatting Syntax</h2>
1212

1313
<p>This page offers a brief overview of what it's like to use Markdown.
1414
The <a href="/projects/markdown/syntax" title="Markdown Syntax">syntax page</a> provides complete, detailed documentation for
@@ -24,7 +24,7 @@ <h2>Getting the Gist of Markdown's Formatting Syntax</h2>
2424
<p><strong>Note:</strong> This document is itself written using Markdown; you
2525
can <a href="/projects/markdown/basics.text">see the source for it by adding '.text' to the URL</a>.</p>
2626

27-
<h2>Paragraphs, Headers, Blockquotes</h2>
27+
<h2 id="paragraphs-headers-blockquotes">Paragraphs, Headers, Blockquotes</h2>
2828

2929
<p>A paragraph is simply one or more consecutive lines of text, separated
3030
by one or more blank lines. (A blank line is any line that looks like a
@@ -88,7 +88,7 @@ <h2>Paragraphs, Headers, Blockquotes</h2>
8888
&lt;/blockquote&gt;
8989
</code></pre>
9090

91-
<h3>Phrase Emphasis</h3>
91+
<h3 id="phrase-emphasis">Phrase Emphasis</h3>
9292

9393
<p>Markdown uses asterisks and underscores to indicate spans of emphasis.</p>
9494

@@ -110,7 +110,7 @@ <h3>Phrase Emphasis</h3>
110110
Or, if you prefer, &lt;strong&gt;use two underscores instead&lt;/strong&gt;.&lt;/p&gt;
111111
</code></pre>
112112

113-
<h2>Lists</h2>
113+
<h2 id="lists">Lists</h2>
114114

115115
<p>Unordered (bulleted) lists use asterisks, pluses, and hyphens (<code>*</code>,
116116
<code>+</code>, and <code>-</code>) as list markers. These three markers are
@@ -181,7 +181,7 @@ <h2>Lists</h2>
181181
&lt;/ul&gt;
182182
</code></pre>
183183

184-
<h3>Links</h3>
184+
<h3 id="links">Links</h3>
185185

186186
<p>Markdown supports two styles for creating links: <em>inline</em> and
187187
<em>reference</em>. With both styles, you use square brackets to delimit the
@@ -244,7 +244,7 @@ <h3>Links</h3>
244244
&lt;a href="http://www.nytimes.com/"&gt;The New York Times&lt;/a&gt;.&lt;/p&gt;
245245
</code></pre>
246246

247-
<h3>Images</h3>
247+
<h3 id="images">Images</h3>
248248

249249
<p>Image syntax is very much like link syntax.</p>
250250

@@ -265,7 +265,7 @@ <h3>Images</h3>
265265
<pre><code>&lt;img src="/path/to/img.jpg" alt="alt text" title="Title" /&gt;
266266
</code></pre>
267267

268-
<h3>Code</h3>
268+
<h3 id="code">Code</h3>
269269

270270
<p>In a regular paragraph, you can create code span by wrapping text in
271271
backtick quotes. Any ampersands (<code>&amp;</code>) and angle brackets (<code>&lt;</code> or

test/unit/marked-spec.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ var marked = require('../../lib/marked.js');
22

33
describe('Test heading ID functionality', function() {
44
it('should add id attribute by default', function() {
5-
var renderer = new marked.Renderer(marked.defaults);
6-
var header = renderer.heading('test', 1, 'test');
5+
var renderer = new marked.Renderer();
6+
var slugger = new marked.Slugger();
7+
var header = renderer.heading('test', 1, 'test', slugger);
78
expect(header).toBe('<h1 id="test">test</h1>\n');
89
});
910

@@ -14,6 +15,51 @@ describe('Test heading ID functionality', function() {
1415
});
1516
});
1617

18+
describe('Test slugger functionality', function() {
19+
it('should use lowercase slug', function() {
20+
var slugger = new marked.Slugger();
21+
expect(slugger.slug('Test')).toBe('test');
22+
});
23+
24+
it('should be unique to avoid collisions 1280', function() {
25+
var slugger = new marked.Slugger();
26+
expect(slugger.slug('test')).toBe('test');
27+
expect(slugger.slug('test')).toBe('test-1');
28+
expect(slugger.slug('test')).toBe('test-2');
29+
});
30+
31+
it('should be unique when slug ends with number', function() {
32+
var slugger = new marked.Slugger();
33+
expect(slugger.slug('test 1')).toBe('test-1');
34+
expect(slugger.slug('test')).toBe('test');
35+
expect(slugger.slug('test')).toBe('test-2');
36+
});
37+
38+
it('should be unique when slug ends with hyphen number', function() {
39+
var slugger = new marked.Slugger();
40+
expect(slugger.slug('foo')).toBe('foo');
41+
expect(slugger.slug('foo')).toBe('foo-1');
42+
expect(slugger.slug('foo 1')).toBe('foo-1-1');
43+
expect(slugger.slug('foo-1')).toBe('foo-1-2');
44+
expect(slugger.slug('foo')).toBe('foo-2');
45+
});
46+
47+
it('should allow non-latin chars', function() {
48+
var slugger = new marked.Slugger();
49+
expect(slugger.slug('привет')).toBe('привет');
50+
});
51+
52+
it('should remove ampersands 857', function() {
53+
var slugger = new marked.Slugger();
54+
expect(slugger.slug('This & That Section')).toBe('this--that-section');
55+
});
56+
57+
it('should remove periods', function() {
58+
var slugger = new marked.Slugger();
59+
expect(slugger.slug('file.txt')).toBe('filetxt');
60+
});
61+
});
62+
1763
describe('Test paragraph token type', function () {
1864
it('should use the "paragraph" type on top level', function () {
1965
const md = 'A Paragraph.\n\n> A blockquote\n\n- list item\n';

0 commit comments

Comments
 (0)