Skip to content

Commit

Permalink
Merge pull request #1401 from styfle/slugger2
Browse files Browse the repository at this point in the history
Fix duplicate heading id
  • Loading branch information
joshbruce committed Dec 31, 2018
2 parents 6dff94d + 632ac5d commit dfbc9a1
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 23 deletions.
41 changes: 38 additions & 3 deletions lib/marked.js
Expand Up @@ -953,13 +953,13 @@ Renderer.prototype.html = function(html) {
return html;
};

Renderer.prototype.heading = function(text, level, raw) {
Renderer.prototype.heading = function(text, level, raw, slugger) {
if (this.options.headerIds) {
return '<h'
+ level
+ ' id="'
+ this.options.headerPrefix
+ raw.toLowerCase().replace(/[^\w]+/g, '-')
+ slugger.slug(raw)
+ '">'
+ text
+ '</h'
Expand Down Expand Up @@ -1108,6 +1108,7 @@ function Parser(options) {
this.options.renderer = this.options.renderer || new Renderer();
this.renderer = this.options.renderer;
this.renderer.options = this.options;
this.slugger = new Slugger();
}

/**
Expand Down Expand Up @@ -1186,7 +1187,8 @@ Parser.prototype.tok = function() {
return this.renderer.heading(
this.inline.output(this.token.text),
this.token.depth,
unescape(this.inlineText.output(this.token.text)));
unescape(this.inlineText.output(this.token.text)),
this.slugger);
}
case 'code': {
return this.renderer.code(this.token.text,
Expand Down Expand Up @@ -1283,6 +1285,37 @@ Parser.prototype.tok = function() {
}
};

/**
* Slugger generates header id
*/

function Slugger () {
this.seen = {};
}

/**
* Convert string to unique id
*/

Slugger.prototype.slug = function (value) {
var slug = value
.toLowerCase()
.trim()
.replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '')
.replace(/\s/g, '-');

if (this.seen.hasOwnProperty(slug)) {
var originalSlug = slug;
do {
this.seen[originalSlug]++;
slug = originalSlug + '-' + this.seen[originalSlug];
} while (this.seen.hasOwnProperty(slug));
}
this.seen[slug] = 0;

return slug;
};

/**
* Helpers
*/
Expand Down Expand Up @@ -1617,6 +1650,8 @@ marked.lexer = Lexer.lex;
marked.InlineLexer = InlineLexer;
marked.inlineLexer = InlineLexer.output;

marked.Slugger = Slugger;

marked.parse = marked;

if (typeof module !== 'undefined' && typeof exports === 'object') {
Expand Down
8 changes: 4 additions & 4 deletions test/new/cm_blockquotes.html
Expand Up @@ -11,7 +11,7 @@ <h3 id="example-192">Example 192</h3>
<p>The spaces after the <code>&gt;</code> characters can be omitted:</p>

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

<blockquote>
<h1 id="foo">Foo</h1>
<h1 id="baz">Baz</h1>
<p>bar
baz</p>
</blockquote>
Expand All @@ -30,7 +30,7 @@ <h3 id="example-194">Example 194</h3>

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

<pre><code>&gt; # Foo
<pre><code>&gt; # Qux
&gt; bar
&gt; baz</code></pre>

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

<blockquote>
<h1 id="foo">Foo</h1>
<h1 id="quux">Quux</h1>
<p>bar
baz</p>
</blockquote>
Expand Down
8 changes: 4 additions & 4 deletions test/new/cm_blockquotes.md
Expand Up @@ -8,31 +8,31 @@

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

># Foo
># Bar
>bar
> baz
### Example 193

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

> # Foo
> # Baz
> bar
> baz
### Example 194

Four spaces gives us a code block:

> # Foo
> # Qux
> bar
> baz

### Example 195

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

> # Foo
> # Quux
> bar
baz

Expand Down
2 changes: 1 addition & 1 deletion test/new/toplevel_paragraphs.html
Expand Up @@ -12,7 +12,7 @@
<h1 id="how-are-you">how are you</h1>

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

<p>paragraph before blockquote</p>
<blockquote><p>text for blockquote</p></blockquote>
Expand Down
2 changes: 1 addition & 1 deletion test/new/toplevel_paragraphs.md
Expand Up @@ -17,7 +17,7 @@ paragraph before head with hash
# how are you

paragraph before head with equals
how are you
how are you again
===========

paragraph before blockquote
Expand Down
16 changes: 8 additions & 8 deletions test/original/markdown_documentation_basics.html
@@ -1,4 +1,4 @@
<h1>Markdown: Basics</h1>
<h1 id="markdown-basics">Markdown: Basics</h1>

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

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

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

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

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

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

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

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

<h2>Lists</h2>
<h2 id="lists">Lists</h2>

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

<h3>Links</h3>
<h3 id="links">Links</h3>

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

<h3>Images</h3>
<h3 id="images">Images</h3>

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

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

<h3>Code</h3>
<h3 id="code">Code</h3>

<p>In a regular paragraph, you can create code span by wrapping text in
backtick quotes. Any ampersands (<code>&amp;</code>) and angle brackets (<code>&lt;</code> or
Expand Down
50 changes: 48 additions & 2 deletions test/unit/marked-spec.js
Expand Up @@ -2,8 +2,9 @@ var marked = require('../../lib/marked.js');

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

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

describe('Test slugger functionality', function() {
it('should use lowercase slug', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('Test')).toBe('test');
});

it('should be unique to avoid collisions 1280', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('test')).toBe('test');
expect(slugger.slug('test')).toBe('test-1');
expect(slugger.slug('test')).toBe('test-2');
});

it('should be unique when slug ends with number', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('test 1')).toBe('test-1');
expect(slugger.slug('test')).toBe('test');
expect(slugger.slug('test')).toBe('test-2');
});

it('should be unique when slug ends with hyphen number', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('foo')).toBe('foo');
expect(slugger.slug('foo')).toBe('foo-1');
expect(slugger.slug('foo 1')).toBe('foo-1-1');
expect(slugger.slug('foo-1')).toBe('foo-1-2');
expect(slugger.slug('foo')).toBe('foo-2');
});

it('should allow non-latin chars', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('привет')).toBe('привет');
});

it('should remove ampersands 857', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('This & That Section')).toBe('this--that-section');
});

it('should remove periods', function() {
var slugger = new marked.Slugger();
expect(slugger.slug('file.txt')).toBe('filetxt');
});
});

describe('Test paragraph token type', function () {
it('should use the "paragraph" type on top level', function () {
const md = 'A Paragraph.\n\n> A blockquote\n\n- list item\n';
Expand Down

0 comments on commit dfbc9a1

Please sign in to comment.