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

Commit f52f768

Browse files
ramboztrieloff
authored andcommitted
feat(html pipe): add support for anchors on headings
Fixes #26
1 parent a925708 commit f52f768

File tree

7 files changed

+207
-5
lines changed

7 files changed

+207
-5
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"callsites": "^3.0.0",
5454
"clone": "^2.1.2",
5555
"fs-extra": "^7.0.0",
56+
"github-slugger": "^1.2.1",
5657
"hast-to-hyperscript": "^6.0.0",
5758
"hast-util-to-html": "^5.0.0",
5859
"hyperscript": "^2.0.2",

src/utils/heading-handler.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2019 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
const fallback = require('mdast-util-to-hast/lib/handlers/heading');
13+
const GithubSlugger = require('github-slugger');
14+
15+
/**
16+
* Utility class injects heading identifiers during the MDAST to VDOM transformation.
17+
*/
18+
class HeadingHandler {
19+
/**
20+
* Initializes the handler
21+
*/
22+
constructor() {
23+
// scoping the slugger instance to the current transform operation
24+
// so that heading uniqueness is guaranteed for each transformation separately
25+
this.slugger = new GithubSlugger();
26+
}
27+
28+
/**
29+
* Gets the text content for the specified heading.
30+
* @param {UnistParent~Heading} heading The heading node
31+
* @returns {string} The text content for the heading
32+
*/
33+
static getTextContent(heading) {
34+
return heading.children
35+
.filter(el => el.type === 'text')
36+
.map(el => el.value)
37+
.join('').trim();
38+
}
39+
40+
/**
41+
* Reset the heading counter
42+
*/
43+
reset() {
44+
this.slugger.reset();
45+
}
46+
47+
/**
48+
* Returns the handler function
49+
*/
50+
handler() {
51+
return (h, node) => {
52+
// Prepare the heading id
53+
const headingIdentifier = this.slugger.slug(HeadingHandler.getTextContent(node));
54+
55+
// Inject the id after transformation
56+
const n = Object.assign({}, node);
57+
const el = fallback(h, n);
58+
el.properties.id = headingIdentifier;
59+
return el;
60+
};
61+
}
62+
}
63+
64+
module.exports = HeadingHandler;

src/utils/mdast-to-vdom.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const hast2html = require('hast-util-to-html');
1818
const unified = require('unified');
1919
const parse = require('rehype-parse');
2020
const { JSDOM } = require('jsdom');
21+
const HeadingHandler = require('./heading-handler');
2122
const image = require('./image-handler');
2223
const embed = require('./embed-handler');
2324
const link = require('./link-handler');
@@ -60,6 +61,9 @@ class VDOMTransformer {
6061
this._handlers[type] = (cb, node, parent) => VDOMTransformer.handle(cb, node, parent, that);
6162
return true;
6263
});
64+
65+
this._headingHandler = new HeadingHandler(options);
66+
this.match('heading', this._headingHandler.handler());
6367
this.match('image', image(options));
6468
this.match('embed', embed(options));
6569
this.match('link', link(options));
@@ -215,6 +219,14 @@ class VDOMTransformer {
215219
// create a JSDOM object with the hast surrounded by the provided tag
216220
return new JSDOM(`<${tag}>${VDOMTransformer.toHTML(this._root, this._handlers)}</${tag}>`).window.document.body.firstChild;
217221
}
222+
223+
/**
224+
* Resets the transformer to avoid leakages between sequential transformations
225+
*/
226+
reset() {
227+
// Reset the heading handler so that id uniqueness is guarateed and reset
228+
this._headingHandler.reset();
229+
}
218230
}
219231

220232
module.exports = VDOMTransformer;

test/fixtures/heading-ids.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"type": "root",
3+
"children": [{
4+
"children": [
5+
{
6+
"type": "text",
7+
"value": "Foo"
8+
}
9+
],
10+
"depth": 1,
11+
"type": "heading"
12+
},
13+
{
14+
"children": [
15+
{
16+
"type": "text",
17+
"value": "Bar"
18+
}
19+
],
20+
"depth": 2,
21+
"type": "heading"
22+
},
23+
{
24+
"children": [
25+
{
26+
"type": "text",
27+
"value": "Baz"
28+
}
29+
],
30+
"depth": 3,
31+
"type": "heading"
32+
},
33+
{
34+
"children": [
35+
{
36+
"type": "text",
37+
"value": "Qux"
38+
}
39+
],
40+
"depth": 2,
41+
"type": "heading"
42+
},
43+
{
44+
"children": [
45+
{
46+
"type": "text",
47+
"value": "Bar"
48+
}
49+
],
50+
"depth": 3,
51+
"type": "heading"
52+
},
53+
{
54+
"children": [
55+
{
56+
"type": "text",
57+
"value": "Bar-1"
58+
}
59+
],
60+
"depth": 4,
61+
"type": "heading"
62+
}
63+
]
64+
}

test/fixtures/heading-ids.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Foo
2+
3+
## Bar
4+
5+
### Baz
6+
7+
## Qux
8+
9+
### Bar
10+
11+
#### Bar-1

test/testHTMLFromMarkdown.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('Testing Markdown conversion', () => {
138138
139139
Hello World
140140
`, `
141-
<h1>Hello</h1>
141+
<h1 id="hello">Hello</h1>
142142
<pre><code>Hello World\n</code></pre>
143143
`);
144144
await assertMd(
@@ -151,6 +151,25 @@ describe('Testing Markdown conversion', () => {
151151
);
152152
});
153153

154+
it('Quote with markdown', async () => {
155+
await assertMd(`
156+
# Foo
157+
158+
bar
159+
160+
> # Foo
161+
>
162+
> bar
163+
`, `
164+
<h1 id="foo">Foo</h1>
165+
<p>bar</p>
166+
<blockquote>
167+
<h1 id="foo-1">Foo</h1>
168+
<p>bar</p>
169+
</blockquote>
170+
`);
171+
});
172+
154173
it('Link references', async () => {
155174
await assertMd(`
156175
Hello [World]
@@ -166,7 +185,7 @@ describe('Testing Markdown conversion', () => {
166185
167186
Hello World [link](<foobar)
168187
`, `
169-
<h1>Foo</h1>
188+
<h1 id="foo">Foo</h1>
170189
<p>Hello World [link](&lt;foobar)</p>
171190
`);
172191
});
@@ -177,7 +196,7 @@ describe('Testing Markdown conversion', () => {
177196
178197
Hello World [link](foo bar)
179198
`, `
180-
<h1>Foo</h1>
199+
<h1 id="foo">Foo</h1>
181200
<p>Hello World [link](foo bar)</p>
182201
`);
183202
});
@@ -188,7 +207,7 @@ describe('Testing Markdown conversion', () => {
188207
189208
Hello World [link](λ)
190209
`, `
191-
<h1>Foo</h1>
210+
<h1 id="foo">Foo</h1>
192211
<p>Hello World <a href="%CE%BB">link</a></p>
193212
`);
194213
});

test/testMdastToVDOM.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const assertTransformerYieldsDocument = (transformer, expected) => {
3333
new JSDOM(expected).window.document,
3434
);
3535

36+
// Reset the transformer between the 2 calls to avoid leakages in the tests
37+
transformer.reset();
38+
3639
assertEquivalentNode(
3740
transformer.getNode('section'),
3841
new JSDOM(`<section>${expected}</section>`).window.document.body.firstChild,
@@ -48,7 +51,35 @@ describe('Test MDAST to VDOM Transformation', () => {
4851
const mdast = fs.readJSONSync(path.resolve(__dirname, 'fixtures', 'simple.json'));
4952
assertTransformerYieldsDocument(
5053
new VDOM(mdast, action.secrets),
51-
'<h1>Hello World</h1>',
54+
'<h1 id="hello-world">Hello World</h1>',
55+
);
56+
});
57+
58+
it('Headings MDAST Conversion', () => {
59+
const mdast = fs.readJSONSync(path.resolve(__dirname, 'fixtures', 'heading-ids.json'));
60+
assertTransformerYieldsDocument(
61+
new VDOM(mdast, action.secrets), `
62+
<h1 id="foo">Foo</h1>
63+
<h2 id="bar">Bar</h2>
64+
<h3 id="baz">Baz</h1>
65+
<h2 id="qux">Qux</h2>
66+
<h3 id="bar-1">Bar</h3>
67+
<h4 id="bar-1-1">Bar-1</h4>`,
68+
);
69+
});
70+
71+
it('Sections MDAST Conversion', () => {
72+
const mdast = fs.readJSONSync(path.resolve(__dirname, 'fixtures', 'headings.json'));
73+
assertTransformerYieldsDocument(
74+
new VDOM(mdast, action.secrets), `
75+
<h1 id="heading-1-double-underline">Heading 1 (double-underline)</h1>
76+
<h2 id="heading-2-single-underline">Heading 2 (single-underline)</h2>
77+
<h1 id="heading-1">Heading 1</h1>
78+
<h2 id="heading-2">Heading 2</h2>
79+
<h3 id="heading-3">Heading 3</h3>
80+
<h4 id="heading-4">Heading 4</h4>
81+
<h5 id="heading-5">Heading 5</h5>
82+
<h6 id="heading-6">Heading 6</h6>`,
5283
);
5384
});
5485

0 commit comments

Comments
 (0)