diff --git a/docs/api-site-config.md b/docs/api-site-config.md index e7b8c3b20315..68accd3eb423 100644 --- a/docs/api-site-config.md +++ b/docs/api-site-config.md @@ -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. +#### `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] 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: { zIndex: 100, }, + // Remove the HTML tags and HTML tags content before generating the slug + slugPreprocessor: (slugBase) => + slugBase.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''), }; module.exports = siteConfig; diff --git a/packages/docusaurus-1.x/lib/core/__tests__/toc.test.js b/packages/docusaurus-1.x/lib/core/__tests__/toc.test.js index 926b68e0a43b..7a5b95e07a71 100644 --- a/packages/docusaurus-1.x/lib/core/__tests__/toc.test.js +++ b/packages/docusaurus-1.x/lib/core/__tests__/toc.test.js @@ -42,7 +42,7 @@ describe('getTOC', () => { test('html tag in source', () => { const headings = getTOC(`## Foo`, 'h2', []); - expect(headings[0].hashLink).toEqual('foo'); + expect(headings[0].hashLink).toEqual('a-namefooa-foo'); expect(headings[0].rawContent).toEqual(` Foo`); expect(headings[0].content).toEqual(` Foo`); }); @@ -50,7 +50,7 @@ describe('getTOC', () => { test('transform markdown syntax to html syntax', () => { const headings = getTOC(`## _Foo_`, 'h2', []); - expect(headings[0].hashLink).toEqual('_foo_'); + expect(headings[0].hashLink).toEqual('a-namefooa-_foo_'); expect(headings[0].rawContent).toEqual(` _Foo_`); expect(headings[0].content).toEqual(` Foo`); @@ -69,6 +69,16 @@ describe('getTOC', () => { expect(headings[0].rawContent).toEqual(`function1 [array]`); expect(headings[0].content).toEqual(`function1 [array]`); }); + + test('test slugPreprocessor', () => { + const headings = getTOC(`## Foo`, 'h2', [], (s) => + s.replace(/foo/gi, 'bar'), + ); + + expect(headings[0].hashLink).toEqual('a-namebara-bar'); + expect(headings[0].rawContent).toEqual(` Foo`); + expect(headings[0].content).toEqual(` Foo`); + }); }); describe('insertTOC', () => { diff --git a/packages/docusaurus-1.x/lib/core/anchors.js b/packages/docusaurus-1.x/lib/core/anchors.js index 229981619638..ef0b028983eb 100644 --- a/packages/docusaurus-1.x/lib/core/anchors.js +++ b/packages/docusaurus-1.x/lib/core/anchors.js @@ -11,7 +11,7 @@ const toSlug = require('./toSlug'); /** * The anchors plugin adds GFM-style anchors to headings. */ -function anchors(md) { +function anchors(md, slugPreprocessor) { const originalRender = md.renderer.rules.heading_open; md.renderer.rules.heading_open = function (tokens, idx, options, env) { @@ -22,10 +22,11 @@ function anchors(md) { const textToken = tokens[idx + 1]; if (textToken.content) { - const anchor = toSlug( - textToken.content.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''), - slugger, - ); + const slugBase = + slugPreprocessor && typeof slugPreprocessor === 'function' + ? slugPreprocessor(textToken.content) + : textToken.content; + const anchor = toSlug(slugBase, slugger); return ``; } diff --git a/packages/docusaurus-1.x/lib/core/nav/OnPageNav.js b/packages/docusaurus-1.x/lib/core/nav/OnPageNav.js index d3562b55a2e4..75fe4ac2889e 100644 --- a/packages/docusaurus-1.x/lib/core/nav/OnPageNav.js +++ b/packages/docusaurus-1.x/lib/core/nav/OnPageNav.js @@ -37,8 +37,18 @@ class OnPageNav extends React.Component { render() { const customTags = siteConfig.onPageNavHeadings; const headings = customTags - ? getTOC(this.props.rawContent, customTags.topLevel, customTags.sub) - : getTOC(this.props.rawContent); + ? getTOC( + this.props.rawContent, + customTags.topLevel, + customTags.sub, + siteConfig.slugPreprocessor, + ) + : getTOC( + this.props.rawContent, + undefined, + undefined, + siteConfig.slugPreprocessor, + ); return ; } diff --git a/packages/docusaurus-1.x/lib/core/renderMarkdown.js b/packages/docusaurus-1.x/lib/core/renderMarkdown.js index 6727685956f1..47863c47e23d 100644 --- a/packages/docusaurus-1.x/lib/core/renderMarkdown.js +++ b/packages/docusaurus-1.x/lib/core/renderMarkdown.js @@ -102,7 +102,7 @@ class MarkdownRenderer { const md = new Markdown(markdownOptions); // Register anchors plugin - md.use(anchors); + md.use(anchors, siteConfig.slugPreprocessor); // Linkify md.use(linkify); diff --git a/packages/docusaurus-1.x/lib/core/toc.js b/packages/docusaurus-1.x/lib/core/toc.js index f11efe256c6e..9354f1420336 100644 --- a/packages/docusaurus-1.x/lib/core/toc.js +++ b/packages/docusaurus-1.x/lib/core/toc.js @@ -19,7 +19,12 @@ const tocRegex = new RegExp('', 'i'); * 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 headingLevels = [].concat(headingTags).map(tagToLevel); const subHeadingLevels = subHeadingTags @@ -38,10 +43,11 @@ function getTOC(content, headingTags = 'h2', subHeadingTags = 'h3') { headings.forEach((heading) => { const rawContent = heading.content; const rendered = md.renderInline(rawContent); - const hashLink = toSlug( - rawContent.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''), - slugger, - ); + const slugBase = + slugPreprocessor && typeof slugPreprocessor === 'function' + ? slugPreprocessor(rawContent) + : rawContent; + const hashLink = toSlug(slugBase, slugger); if (!allowedHeadingLevels.includes(heading.lvl)) { return; } @@ -63,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 // contents inserted -function insertTOC(rawContent) { +function insertTOC(rawContent, slugPreprocessor = undefined) { if (!rawContent || !tocRegex.test(rawContent)) { return rawContent; } const filterRe = /^`[^`]*`/; - const headers = getTOC(rawContent, 'h3', null); + const headers = getTOC(rawContent, 'h3', null, slugPreprocessor); const tableOfContents = headers .filter((header) => filterRe.test(header.rawContent)) .map((header) => ` - [${header.rawContent}](#${header.hashLink})`) diff --git a/packages/docusaurus-1.x/lib/server/docs.js b/packages/docusaurus-1.x/lib/server/docs.js index b7145113f25f..7b054d362844 100644 --- a/packages/docusaurus-1.x/lib/server/docs.js +++ b/packages/docusaurus-1.x/lib/server/docs.js @@ -104,7 +104,7 @@ function mdToHtmlify(oldContent, mdToHtml, metadata, siteConfig) { function getMarkup(rawContent, mdToHtml, metadata, siteConfig) { // 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 content = mdToHtmlify(content, mdToHtml, metadata, siteConfig);