Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix duplicate heading id #1401

Merged
merged 3 commits into from Dec 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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