mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
Ensure anchor links are unique per document (#574)
This commit is contained in:
parent
2a83959ac1
commit
9c98142fea
12 changed files with 440 additions and 43 deletions
20
lib/core/__tests__/__fixtures__/getTOC.md
Normal file
20
lib/core/__tests__/__fixtures__/getTOC.md
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
## foo
|
||||||
|
### foo
|
||||||
|
### foo 1
|
||||||
|
## foo 1
|
||||||
|
## foo 2
|
||||||
|
### foo
|
||||||
|
#### 4th level headings
|
||||||
|
All 4th level headings should not be shown by default
|
||||||
|
|
||||||
|
## bar
|
||||||
|
### bar
|
||||||
|
#### bar
|
||||||
|
4th level heading should be ignored by default, but is should be always taken
|
||||||
|
into account, when generating slugs
|
||||||
|
### `bar`
|
||||||
|
#### `bar`
|
||||||
|
## bar
|
||||||
|
### bar
|
||||||
|
#### bar
|
||||||
|
## bar
|
5
lib/core/__tests__/__snapshots__/anchors.tests.js.snap
Normal file
5
lib/core/__tests__/__snapshots__/anchors.tests.js.snap
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Anchors rendering 1`] = `"<h1><a class=\\"anchor\\" aria-hidden=\\"true\\" id=\\"hello-world\\"></a><a href=\\"#hello-world\\" aria-hidden=\\"true\\" class=\\"hash-link\\" ><svg aria-hidden=\\"true\\" height=\\"16\\" version=\\"1.1\\" viewBox=\\"0 0 16 16\\" width=\\"16\\"><path fill-rule=\\"evenodd\\" d=\\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\\"></path></svg></a>"`;
|
||||||
|
|
||||||
|
exports[`Anchors rendering 2`] = `"<h2><a class=\\"anchor\\" aria-hidden=\\"true\\" id=\\"hello-small-world\\"></a><a href=\\"#hello-small-world\\" aria-hidden=\\"true\\" class=\\"hash-link\\" ><svg aria-hidden=\\"true\\" height=\\"16\\" version=\\"1.1\\" viewBox=\\"0 0 16 16\\" width=\\"16\\"><path fill-rule=\\"evenodd\\" d=\\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\\"></path></svg></a>"`;
|
187
lib/core/__tests__/__snapshots__/getTOC.tests.js.snap
Normal file
187
lib/core/__tests__/__snapshots__/getTOC.tests.js.snap
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`with custom heading levels 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo",
|
||||||
|
"hashLink": "foo-1",
|
||||||
|
"rawContent": "foo",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo 1",
|
||||||
|
"hashLink": "foo-1-1",
|
||||||
|
"rawContent": "foo 1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "foo",
|
||||||
|
"hashLink": "foo",
|
||||||
|
"rawContent": "foo",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo 1",
|
||||||
|
"hashLink": "foo-1-2",
|
||||||
|
"rawContent": "foo 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo",
|
||||||
|
"hashLink": "foo-3",
|
||||||
|
"rawContent": "foo",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "4th level headings",
|
||||||
|
"hashLink": "4th-level-headings",
|
||||||
|
"rawContent": "4th level headings",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "foo 2",
|
||||||
|
"hashLink": "foo-2",
|
||||||
|
"rawContent": "foo 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-1",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-2",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "<code>bar</code>",
|
||||||
|
"hashLink": "bar-3",
|
||||||
|
"rawContent": "\`bar\`",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "<code>bar</code>",
|
||||||
|
"hashLink": "bar-4",
|
||||||
|
"rawContent": "\`bar\`",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-6",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-7",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-5",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-8",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`with defaults 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo",
|
||||||
|
"hashLink": "foo-1",
|
||||||
|
"rawContent": "foo",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo 1",
|
||||||
|
"hashLink": "foo-1-1",
|
||||||
|
"rawContent": "foo 1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "foo",
|
||||||
|
"hashLink": "foo",
|
||||||
|
"rawContent": "foo",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo 1",
|
||||||
|
"hashLink": "foo-1-2",
|
||||||
|
"rawContent": "foo 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "foo",
|
||||||
|
"hashLink": "foo-3",
|
||||||
|
"rawContent": "foo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "foo 2",
|
||||||
|
"hashLink": "foo-2",
|
||||||
|
"rawContent": "foo 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-1",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "<code>bar</code>",
|
||||||
|
"hashLink": "bar-3",
|
||||||
|
"rawContent": "\`bar\`",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-6",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-5",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"children": Array [],
|
||||||
|
"content": "bar",
|
||||||
|
"hashLink": "bar-8",
|
||||||
|
"rawContent": "bar",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
104
lib/core/__tests__/anchors.tests.js
Normal file
104
lib/core/__tests__/anchors.tests.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
const anchors = require('../anchors');
|
||||||
|
|
||||||
|
const md = {
|
||||||
|
renderer: {
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
anchors(md);
|
||||||
|
|
||||||
|
const render = md.renderer.rules.heading_open;
|
||||||
|
|
||||||
|
test('Anchors rendering', () => {
|
||||||
|
expect(
|
||||||
|
render([{hLevel: 1}, {content: 'Hello world'}], 0, {}, {})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
render([{hLevel: 2}, {content: 'Hello small world'}], 0, {}, {})
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Each anchor is unique across rendered document', () => {
|
||||||
|
const tokens = [
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading 1'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading 1'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading 2'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading'},
|
||||||
|
];
|
||||||
|
const options = {};
|
||||||
|
const env = {};
|
||||||
|
|
||||||
|
expect(render(tokens, 0, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 2, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading-1"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 4, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading-1-1"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 6, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading-1-2"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 8, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading-2"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 10, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading-3"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Each anchor is unique across rendered document. Case 2', () => {
|
||||||
|
const tokens = [
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'foo'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'foo 1'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'foo'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'foo 1'},
|
||||||
|
];
|
||||||
|
const options = {};
|
||||||
|
const env = {};
|
||||||
|
|
||||||
|
expect(render(tokens, 0, options, env)).toContain('id="foo"');
|
||||||
|
expect(render(tokens, 2, options, env)).toContain('id="foo-1"');
|
||||||
|
expect(render(tokens, 4, options, env)).toContain('id="foo-2"');
|
||||||
|
expect(render(tokens, 6, options, env)).toContain('id="foo-1-1"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Anchor index resets on each render', () => {
|
||||||
|
const tokens = [
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading'},
|
||||||
|
{hLevel: 1},
|
||||||
|
{content: 'Almost unique heading'},
|
||||||
|
];
|
||||||
|
const options = {};
|
||||||
|
const env = {};
|
||||||
|
const env2 = {};
|
||||||
|
|
||||||
|
expect(render(tokens, 0, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 2, options, env)).toContain(
|
||||||
|
'id="almost-unique-heading-1"'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(render(tokens, 0, options, env2)).toContain(
|
||||||
|
'id="almost-unique-heading"'
|
||||||
|
);
|
||||||
|
expect(render(tokens, 2, options, env2)).toContain(
|
||||||
|
'id="almost-unique-heading-1"'
|
||||||
|
);
|
||||||
|
});
|
26
lib/core/__tests__/getTOC.tests.js
Normal file
26
lib/core/__tests__/getTOC.tests.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
const path = require('path');
|
||||||
|
const readFileSync = require('fs').readFileSync;
|
||||||
|
const getTOC = require('../getTOC');
|
||||||
|
|
||||||
|
const mdContents = readFileSync(
|
||||||
|
path.join(__dirname, '__fixtures__', 'getTOC.md'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
test('with defaults', () => {
|
||||||
|
const headings = getTOC(mdContents);
|
||||||
|
const headingsJson = JSON.stringify(headings);
|
||||||
|
|
||||||
|
expect(headings).toMatchSnapshot();
|
||||||
|
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
|
||||||
|
expect(headingsJson).not.toContain('4th level headings');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with custom heading levels', () => {
|
||||||
|
const headings = getTOC(mdContents, 'h2', ['h3', 'h4']);
|
||||||
|
const headingsJson = JSON.stringify(headings);
|
||||||
|
|
||||||
|
expect(headings).toMatchSnapshot();
|
||||||
|
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
|
||||||
|
expect(headingsJson).toContain('4th level headings');
|
||||||
|
});
|
|
@ -12,3 +12,18 @@ const toSlug = require('../toSlug');
|
||||||
expect(toSlug(input)).toBe(output);
|
expect(toSlug(input)).toBe(output);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('unique slugs if `context` argument passed', () => {
|
||||||
|
[
|
||||||
|
['foo', 'foo'],
|
||||||
|
['foo', 'foo-1'],
|
||||||
|
['foo 1', 'foo-1-1'],
|
||||||
|
['foo 1', 'foo-1-2'],
|
||||||
|
['foo 2', 'foo-2'],
|
||||||
|
['foo', 'foo-3'],
|
||||||
|
].reduce((context, [input, output]) => {
|
||||||
|
expect(toSlug(input, context)).toBe(output);
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
29
lib/core/anchors.js
Normal file
29
lib/core/anchors.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
const toSlug = require('./toSlug.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The anchors plugin adds GFM-style anchors to headings.
|
||||||
|
*/
|
||||||
|
function anchors(md) {
|
||||||
|
md.renderer.rules.heading_open = function(tokens, idx, options, env) {
|
||||||
|
const textToken = tokens[idx + 1];
|
||||||
|
const anchor = toSlug(textToken.content, env);
|
||||||
|
|
||||||
|
return (
|
||||||
|
'<h' +
|
||||||
|
tokens[idx].hLevel +
|
||||||
|
'><a class="anchor" aria-hidden="true" id="' +
|
||||||
|
anchor +
|
||||||
|
'"></a><a href="#' +
|
||||||
|
anchor +
|
||||||
|
'" aria-hidden="true" class="hash-link" ><svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = anchors;
|
|
@ -23,20 +23,29 @@ module.exports = (content, headingTags = 'h2', subHeadingTags = 'h3') => {
|
||||||
const subHeadingLevels = subHeadingTags
|
const subHeadingLevels = subHeadingTags
|
||||||
? [].concat(subHeadingTags).map(tagToLevel)
|
? [].concat(subHeadingTags).map(tagToLevel)
|
||||||
: [];
|
: [];
|
||||||
|
const allowedHeadingLevels = headingLevels.concat(subHeadingLevels);
|
||||||
|
|
||||||
const md = new Remarkable();
|
const md = new Remarkable();
|
||||||
const headings = mdToc(content, {
|
const headings = mdToc(content).json;
|
||||||
filter: function(str, ele) {
|
|
||||||
return headingLevels.concat(subHeadingLevels).includes(ele.lvl);
|
|
||||||
},
|
|
||||||
}).json;
|
|
||||||
|
|
||||||
const toc = [];
|
const toc = [];
|
||||||
|
const context = {};
|
||||||
let current;
|
let current;
|
||||||
|
|
||||||
headings.forEach(heading => {
|
headings.forEach(heading => {
|
||||||
|
// we need always generate slugs to ensure, that we will have consistent
|
||||||
|
// slug indexes for headings with the same names
|
||||||
|
const hashLink = toSlug(heading.content, context);
|
||||||
|
|
||||||
|
if (!allowedHeadingLevels.includes(heading.lvl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawContent = mdToc.titleize(heading.content);
|
||||||
const entry = {
|
const entry = {
|
||||||
hashLink: toSlug(heading.content),
|
hashLink,
|
||||||
content: md.renderInline(mdToc.titleize(heading.content)),
|
rawContent,
|
||||||
|
content: md.renderInline(rawContent),
|
||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,28 +7,10 @@
|
||||||
|
|
||||||
const hljs = require('highlight.js');
|
const hljs = require('highlight.js');
|
||||||
const Markdown = require('remarkable');
|
const Markdown = require('remarkable');
|
||||||
const toSlug = require('./toSlug.js');
|
const anchors = require('./anchors.js');
|
||||||
|
|
||||||
const CWD = process.cwd();
|
const CWD = process.cwd();
|
||||||
|
|
||||||
/**
|
|
||||||
* The anchors plugin adds GFM-style anchors to headings.
|
|
||||||
*/
|
|
||||||
function anchors(md) {
|
|
||||||
md.renderer.rules.heading_open = function(tokens, idx /*, options, env */) {
|
|
||||||
const textToken = tokens[idx + 1];
|
|
||||||
return (
|
|
||||||
'<h' +
|
|
||||||
tokens[idx].hLevel +
|
|
||||||
'><a class="anchor" aria-hidden="true" id="' +
|
|
||||||
toSlug(textToken.content) +
|
|
||||||
'"></a><a href="#' +
|
|
||||||
toSlug(textToken.content) +
|
|
||||||
'" aria-hidden="true" class="hash-link" ><svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class MarkdownRenderer {
|
class MarkdownRenderer {
|
||||||
constructor() {
|
constructor() {
|
||||||
const siteConfig = require(CWD + '/siteConfig.js');
|
const siteConfig = require(CWD + '/siteConfig.js');
|
||||||
|
|
|
@ -18,7 +18,16 @@ const exceptAlphanum = new RegExp(
|
||||||
'g'
|
'g'
|
||||||
);
|
);
|
||||||
|
|
||||||
module.exports = string => {
|
/**
|
||||||
|
* Converts a string to a slug, that can be used in heading anchors
|
||||||
|
*
|
||||||
|
* @param {string} string
|
||||||
|
* @param {Object} [context={}] - an optional context to track used slugs and
|
||||||
|
* ensure that new slug will be unique
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
module.exports = (string, context = {}) => {
|
||||||
// var accents = "àáäâèéëêìíïîòóöôùúüûñç";
|
// var accents = "àáäâèéëêìíïîòóöôùúüûñç";
|
||||||
const accents =
|
const accents =
|
||||||
'\u00e0\u00e1\u00e4\u00e2\u00e8' +
|
'\u00e0\u00e1\u00e4\u00e2\u00e8' +
|
||||||
|
@ -50,5 +59,22 @@ module.exports = string => {
|
||||||
slug += '-';
|
slug += '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!context.slugStats) {
|
||||||
|
context.slugStats = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof context.slugStats[slug] === 'number') {
|
||||||
|
// search for an index, that will not clash with an existing headings
|
||||||
|
while (
|
||||||
|
typeof context.slugStats[slug + '-' + ++context.slugStats[slug]] ===
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
slug += '-' + context.slugStats[slug];
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are tracking both original anchors and suffixed to avoid future name
|
||||||
|
// clashing with headings with numbers e.g. `#Foo 1` may clash with the second `#Foo`
|
||||||
|
context.slugStats[slug] = 0;
|
||||||
|
|
||||||
return slug;
|
return slug;
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ async function execute() {
|
||||||
const readMetadata = require('./readMetadata.js');
|
const readMetadata = require('./readMetadata.js');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const color = require('color');
|
const color = require('color');
|
||||||
const toSlug = require('../core/toSlug.js');
|
const getTOC = require('../core/getTOC.js');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const mkdirp = require('mkdirp');
|
const mkdirp = require('mkdirp');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
|
@ -42,15 +42,12 @@ async function execute() {
|
||||||
// takes the content of a doc article and returns the content with a table of
|
// takes the content of a doc article and returns the content with a table of
|
||||||
// contents inserted
|
// contents inserted
|
||||||
const insertTableOfContents = rawContent => {
|
const insertTableOfContents = rawContent => {
|
||||||
const regexp = /\n###\s+(`.*`.*)\n/g;
|
const filterRe = /^`[^`]*`/;
|
||||||
let match;
|
const headers = getTOC(rawContent, 'h3', null);
|
||||||
const headers = [];
|
|
||||||
while ((match = regexp.exec(rawContent))) {
|
|
||||||
headers.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableOfContents = headers
|
const tableOfContents = headers
|
||||||
.map(header => ` - [${header}](#${toSlug(header)})`)
|
.filter(header => filterRe.test(header.rawContent))
|
||||||
|
.map(header => ` - [${header.rawContent}](#${header.hashLink})`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents);
|
return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents);
|
||||||
|
|
|
@ -17,7 +17,7 @@ function execute(port) {
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const color = require('color');
|
const color = require('color');
|
||||||
const toSlug = require('../core/toSlug');
|
const getTOC = require('../core/getTOC');
|
||||||
const mkdirp = require('mkdirp');
|
const mkdirp = require('mkdirp');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
@ -91,15 +91,12 @@ function execute(port) {
|
||||||
const TABLE_OF_CONTENTS_TOKEN = '<AUTOGENERATED_TABLE_OF_CONTENTS>';
|
const TABLE_OF_CONTENTS_TOKEN = '<AUTOGENERATED_TABLE_OF_CONTENTS>';
|
||||||
|
|
||||||
const insertTableOfContents = rawContent => {
|
const insertTableOfContents = rawContent => {
|
||||||
const regexp = /\n###\s+(`.*`.*)\n/g;
|
const filterRe = /^`[^`]*`/;
|
||||||
let match;
|
const headers = getTOC(rawContent, 'h3', null);
|
||||||
const headers = [];
|
|
||||||
while ((match = regexp.exec(rawContent))) {
|
|
||||||
headers.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableOfContents = headers
|
const tableOfContents = headers
|
||||||
.map(header => ` - [${header}](#${toSlug(header)})`)
|
.filter(header => filterRe.test(header.rawContent))
|
||||||
|
.map(header => ` - [${header.rawContent}](#${header.hashLink})`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents);
|
return rawContent.replace(TABLE_OF_CONTENTS_TOKEN, tableOfContents);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue