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

Commit

Permalink
feat(html pipe): add support for anchors on headings
Browse files Browse the repository at this point in the history
Fixes #26
  • Loading branch information
ramboz authored and trieloff committed Apr 16, 2019
1 parent a925708 commit f52f768
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"callsites": "^3.0.0",
"clone": "^2.1.2",
"fs-extra": "^7.0.0",
"github-slugger": "^1.2.1",
"hast-to-hyperscript": "^6.0.0",
"hast-util-to-html": "^5.0.0",
"hyperscript": "^2.0.2",
Expand Down
64 changes: 64 additions & 0 deletions src/utils/heading-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
const fallback = require('mdast-util-to-hast/lib/handlers/heading');
const GithubSlugger = require('github-slugger');

/**
* Utility class injects heading identifiers during the MDAST to VDOM transformation.
*/
class HeadingHandler {
/**
* Initializes the handler
*/
constructor() {
// scoping the slugger instance to the current transform operation
// so that heading uniqueness is guaranteed for each transformation separately
this.slugger = new GithubSlugger();
}

/**
* Gets the text content for the specified heading.
* @param {UnistParent~Heading} heading The heading node
* @returns {string} The text content for the heading
*/
static getTextContent(heading) {
return heading.children
.filter(el => el.type === 'text')
.map(el => el.value)
.join('').trim();
}

/**
* Reset the heading counter
*/
reset() {
this.slugger.reset();
}

/**
* Returns the handler function
*/
handler() {
return (h, node) => {
// Prepare the heading id
const headingIdentifier = this.slugger.slug(HeadingHandler.getTextContent(node));

// Inject the id after transformation
const n = Object.assign({}, node);
const el = fallback(h, n);
el.properties.id = headingIdentifier;
return el;
};
}
}

module.exports = HeadingHandler;
12 changes: 12 additions & 0 deletions src/utils/mdast-to-vdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const hast2html = require('hast-util-to-html');
const unified = require('unified');
const parse = require('rehype-parse');
const { JSDOM } = require('jsdom');
const HeadingHandler = require('./heading-handler');
const image = require('./image-handler');
const embed = require('./embed-handler');
const link = require('./link-handler');
Expand Down Expand Up @@ -60,6 +61,9 @@ class VDOMTransformer {
this._handlers[type] = (cb, node, parent) => VDOMTransformer.handle(cb, node, parent, that);
return true;
});

this._headingHandler = new HeadingHandler(options);
this.match('heading', this._headingHandler.handler());
this.match('image', image(options));
this.match('embed', embed(options));
this.match('link', link(options));
Expand Down Expand Up @@ -215,6 +219,14 @@ class VDOMTransformer {
// create a JSDOM object with the hast surrounded by the provided tag
return new JSDOM(`<${tag}>${VDOMTransformer.toHTML(this._root, this._handlers)}</${tag}>`).window.document.body.firstChild;
}

/**
* Resets the transformer to avoid leakages between sequential transformations
*/
reset() {
// Reset the heading handler so that id uniqueness is guarateed and reset
this._headingHandler.reset();
}
}

module.exports = VDOMTransformer;
64 changes: 64 additions & 0 deletions test/fixtures/heading-ids.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"type": "root",
"children": [{
"children": [
{
"type": "text",
"value": "Foo"
}
],
"depth": 1,
"type": "heading"
},
{
"children": [
{
"type": "text",
"value": "Bar"
}
],
"depth": 2,
"type": "heading"
},
{
"children": [
{
"type": "text",
"value": "Baz"
}
],
"depth": 3,
"type": "heading"
},
{
"children": [
{
"type": "text",
"value": "Qux"
}
],
"depth": 2,
"type": "heading"
},
{
"children": [
{
"type": "text",
"value": "Bar"
}
],
"depth": 3,
"type": "heading"
},
{
"children": [
{
"type": "text",
"value": "Bar-1"
}
],
"depth": 4,
"type": "heading"
}
]
}
11 changes: 11 additions & 0 deletions test/fixtures/heading-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Foo

## Bar

### Baz

## Qux

### Bar

#### Bar-1
27 changes: 23 additions & 4 deletions test/testHTMLFromMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('Testing Markdown conversion', () => {
Hello World
`, `
<h1>Hello</h1>
<h1 id="hello">Hello</h1>
<pre><code>Hello World\n</code></pre>
`);
await assertMd(
Expand All @@ -151,6 +151,25 @@ describe('Testing Markdown conversion', () => {
);
});

it('Quote with markdown', async () => {
await assertMd(`
# Foo
bar
> # Foo
>
> bar
`, `
<h1 id="foo">Foo</h1>
<p>bar</p>
<blockquote>
<h1 id="foo-1">Foo</h1>
<p>bar</p>
</blockquote>
`);
});

it('Link references', async () => {
await assertMd(`
Hello [World]
Expand All @@ -166,7 +185,7 @@ describe('Testing Markdown conversion', () => {
Hello World [link](<foobar)
`, `
<h1>Foo</h1>
<h1 id="foo">Foo</h1>
<p>Hello World [link](&lt;foobar)</p>
`);
});
Expand All @@ -177,7 +196,7 @@ describe('Testing Markdown conversion', () => {
Hello World [link](foo bar)
`, `
<h1>Foo</h1>
<h1 id="foo">Foo</h1>
<p>Hello World [link](foo bar)</p>
`);
});
Expand All @@ -188,7 +207,7 @@ describe('Testing Markdown conversion', () => {
Hello World [link](λ)
`, `
<h1>Foo</h1>
<h1 id="foo">Foo</h1>
<p>Hello World <a href="%CE%BB">link</a></p>
`);
});
Expand Down
33 changes: 32 additions & 1 deletion test/testMdastToVDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const assertTransformerYieldsDocument = (transformer, expected) => {
new JSDOM(expected).window.document,
);

// Reset the transformer between the 2 calls to avoid leakages in the tests
transformer.reset();

assertEquivalentNode(
transformer.getNode('section'),
new JSDOM(`<section>${expected}</section>`).window.document.body.firstChild,
Expand All @@ -48,7 +51,35 @@ describe('Test MDAST to VDOM Transformation', () => {
const mdast = fs.readJSONSync(path.resolve(__dirname, 'fixtures', 'simple.json'));
assertTransformerYieldsDocument(
new VDOM(mdast, action.secrets),
'<h1>Hello World</h1>',
'<h1 id="hello-world">Hello World</h1>',
);
});

it('Headings MDAST Conversion', () => {
const mdast = fs.readJSONSync(path.resolve(__dirname, 'fixtures', 'heading-ids.json'));
assertTransformerYieldsDocument(
new VDOM(mdast, action.secrets), `
<h1 id="foo">Foo</h1>
<h2 id="bar">Bar</h2>
<h3 id="baz">Baz</h1>
<h2 id="qux">Qux</h2>
<h3 id="bar-1">Bar</h3>
<h4 id="bar-1-1">Bar-1</h4>`,
);
});

it('Sections MDAST Conversion', () => {
const mdast = fs.readJSONSync(path.resolve(__dirname, 'fixtures', 'headings.json'));
assertTransformerYieldsDocument(
new VDOM(mdast, action.secrets), `
<h1 id="heading-1-double-underline">Heading 1 (double-underline)</h1>
<h2 id="heading-2-single-underline">Heading 2 (single-underline)</h2>
<h1 id="heading-1">Heading 1</h1>
<h2 id="heading-2">Heading 2</h2>
<h3 id="heading-3">Heading 3</h3>
<h4 id="heading-4">Heading 4</h4>
<h5 id="heading-5">Heading 5</h5>
<h6 id="heading-6">Heading 6</h6>`,
);
});

Expand Down

0 comments on commit f52f768

Please sign in to comment.