feat(v1): add 'slugPreprocessor' config option to allow users customize the hash links (#3124)

* fix(v1): remove HTML content from hash links

* fix one more test

* rewrite changes as new feature to prevent breaking changes
This commit is contained in:
Bartosz Kaszubowski 2020-07-28 14:17:28 +02:00 committed by GitHub
parent d1a27efe8c
commit aa7430e168
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 50 additions and 11 deletions

View file

@ -318,6 +318,10 @@ Set this to `true` if you want to enable the scroll to top button at the bottom
Optional options configuration for the scroll to top button. You do not need to use this, even if you set `scrollToTop` to `true`; it just provides you more configuration control of the button. You can find more options [here](https://github.com/vfeskov/vanilla-back-to-top/blob/v7.1.14/OPTIONS.md). By default, we set the zIndex option to 100. Optional options configuration for the scroll to top button. You do not need to use this, even if you set `scrollToTop` to `true`; it just provides you more configuration control of the button. You can find more options [here](https://github.com/vfeskov/vanilla-back-to-top/blob/v7.1.14/OPTIONS.md). By default, we set the zIndex option to 100.
#### `slugPreprocessor` [function]
Define the slug preprocessor function if you want to customize the text used for generating the hash links. Function provides the base string as the first argument and must always return a string.
#### `stylesheets` [array] #### `stylesheets` [array]
An array of CSS sources to load. The values can be either strings or plain objects of attribute-value maps. The link tag will be inserted in the HTML head. An array of CSS sources to load. The values can be either strings or plain objects of attribute-value maps. The link tag will be inserted in the HTML head.
@ -463,6 +467,9 @@ const siteConfig = {
scrollToTopOptions: { scrollToTopOptions: {
zIndex: 100, zIndex: 100,
}, },
// Remove the HTML tags and HTML tags content before generating the slug
slugPreprocessor: (slugBase) =>
slugBase.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''),
}; };
module.exports = siteConfig; module.exports = siteConfig;

View file

@ -69,6 +69,16 @@ describe('getTOC', () => {
expect(headings[0].rawContent).toEqual(`function1 [array<string>]`); expect(headings[0].rawContent).toEqual(`function1 [array<string>]`);
expect(headings[0].content).toEqual(`function1 [array<string>]`); expect(headings[0].content).toEqual(`function1 [array<string>]`);
}); });
test('test slugPreprocessor', () => {
const headings = getTOC(`## <a name="foo"></a> Foo`, 'h2', [], (s) =>
s.replace(/foo/gi, 'bar'),
);
expect(headings[0].hashLink).toEqual('a-namebara-bar');
expect(headings[0].rawContent).toEqual(`<a name="foo"></a> Foo`);
expect(headings[0].content).toEqual(`<a name="foo"></a> Foo`);
});
}); });
describe('insertTOC', () => { describe('insertTOC', () => {

View file

@ -11,7 +11,7 @@ const toSlug = require('./toSlug');
/** /**
* The anchors plugin adds GFM-style anchors to headings. * The anchors plugin adds GFM-style anchors to headings.
*/ */
function anchors(md) { function anchors(md, slugPreprocessor) {
const originalRender = md.renderer.rules.heading_open; const originalRender = md.renderer.rules.heading_open;
md.renderer.rules.heading_open = function (tokens, idx, options, env) { md.renderer.rules.heading_open = function (tokens, idx, options, env) {
@ -22,7 +22,11 @@ function anchors(md) {
const textToken = tokens[idx + 1]; const textToken = tokens[idx + 1];
if (textToken.content) { if (textToken.content) {
const anchor = toSlug(textToken.content, slugger); const slugBase =
slugPreprocessor && typeof slugPreprocessor === 'function'
? slugPreprocessor(textToken.content)
: textToken.content;
const anchor = toSlug(slugBase, slugger);
return `<h${tokens[idx].hLevel}><a class="anchor" aria-hidden="true" id="${anchor}"></a><a href="#${anchor}" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" 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>`; return `<h${tokens[idx].hLevel}><a class="anchor" aria-hidden="true" id="${anchor}"></a><a href="#${anchor}" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" 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>`;
} }

View file

@ -37,8 +37,18 @@ class OnPageNav extends React.Component {
render() { render() {
const customTags = siteConfig.onPageNavHeadings; const customTags = siteConfig.onPageNavHeadings;
const headings = customTags const headings = customTags
? getTOC(this.props.rawContent, customTags.topLevel, customTags.sub) ? getTOC(
: getTOC(this.props.rawContent); this.props.rawContent,
customTags.topLevel,
customTags.sub,
siteConfig.slugPreprocessor,
)
: getTOC(
this.props.rawContent,
undefined,
undefined,
siteConfig.slugPreprocessor,
);
return <Headings headings={headings} />; return <Headings headings={headings} />;
} }

View file

@ -102,7 +102,7 @@ class MarkdownRenderer {
const md = new Markdown(markdownOptions); const md = new Markdown(markdownOptions);
// Register anchors plugin // Register anchors plugin
md.use(anchors); md.use(anchors, siteConfig.slugPreprocessor);
// Linkify // Linkify
md.use(linkify); md.use(linkify);

View file

@ -19,7 +19,12 @@ const tocRegex = new RegExp('<AUTOGENERATED_TABLE_OF_CONTENTS>', 'i');
* Array of heading objects with `hashLink`, `content` and `children` fields * Array of heading objects with `hashLink`, `content` and `children` fields
* *
*/ */
function getTOC(content, headingTags = 'h2', subHeadingTags = 'h3') { function getTOC(
content,
headingTags = 'h2',
subHeadingTags = 'h3',
slugPreprocessor = undefined,
) {
const tagToLevel = (tag) => Number(tag.slice(1)); const tagToLevel = (tag) => Number(tag.slice(1));
const headingLevels = [].concat(headingTags).map(tagToLevel); const headingLevels = [].concat(headingTags).map(tagToLevel);
const subHeadingLevels = subHeadingTags const subHeadingLevels = subHeadingTags
@ -38,8 +43,11 @@ function getTOC(content, headingTags = 'h2', subHeadingTags = 'h3') {
headings.forEach((heading) => { headings.forEach((heading) => {
const rawContent = heading.content; const rawContent = heading.content;
const rendered = md.renderInline(rawContent); const rendered = md.renderInline(rawContent);
const slugBase =
const hashLink = toSlug(rawContent, slugger); slugPreprocessor && typeof slugPreprocessor === 'function'
? slugPreprocessor(rawContent)
: rawContent;
const hashLink = toSlug(slugBase, slugger);
if (!allowedHeadingLevels.includes(heading.lvl)) { if (!allowedHeadingLevels.includes(heading.lvl)) {
return; return;
} }
@ -61,12 +69,12 @@ function getTOC(content, headingTags = 'h2', subHeadingTags = 'h3') {
// 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
function insertTOC(rawContent) { function insertTOC(rawContent, slugPreprocessor = undefined) {
if (!rawContent || !tocRegex.test(rawContent)) { if (!rawContent || !tocRegex.test(rawContent)) {
return rawContent; return rawContent;
} }
const filterRe = /^`[^`]*`/; const filterRe = /^`[^`]*`/;
const headers = getTOC(rawContent, 'h3', null); const headers = getTOC(rawContent, 'h3', null, slugPreprocessor);
const tableOfContents = headers const tableOfContents = headers
.filter((header) => filterRe.test(header.rawContent)) .filter((header) => filterRe.test(header.rawContent))
.map((header) => ` - [${header.rawContent}](#${header.hashLink})`) .map((header) => ` - [${header.rawContent}](#${header.hashLink})`)

View file

@ -104,7 +104,7 @@ function mdToHtmlify(oldContent, mdToHtml, metadata, siteConfig) {
function getMarkup(rawContent, mdToHtml, metadata, siteConfig) { function getMarkup(rawContent, mdToHtml, metadata, siteConfig) {
// generate table of contents // generate table of contents
let content = insertTOC(rawContent); let content = insertTOC(rawContent, siteConfig.slugPreprocessor);
// replace any links to markdown files to their website html links // replace any links to markdown files to their website html links
content = mdToHtmlify(content, mdToHtml, metadata, siteConfig); content = mdToHtmlify(content, mdToHtml, metadata, siteConfig);