Skip to content

Commit

Permalink
feat(v1): add 'slugPreprocessor' config option to allow users customi…
Browse files Browse the repository at this point in the history
…ze 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
  • Loading branch information
Simek committed Jul 28, 2020
1 parent d1a27ef commit aa7430e
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 11 deletions.
7 changes: 7 additions & 0 deletions docs/api-site-config.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions packages/docusaurus-1.x/lib/core/__tests__/toc.test.js
Expand Up @@ -69,6 +69,16 @@ describe('getTOC', () => {
expect(headings[0].rawContent).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', () => {
Expand Down
8 changes: 6 additions & 2 deletions packages/docusaurus-1.x/lib/core/anchors.js
Expand Up @@ -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) {
Expand All @@ -22,7 +22,11 @@ function anchors(md) {
const textToken = tokens[idx + 1];

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>`;
}

Expand Down
14 changes: 12 additions & 2 deletions packages/docusaurus-1.x/lib/core/nav/OnPageNav.js
Expand Up @@ -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 <Headings headings={headings} />;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-1.x/lib/core/renderMarkdown.js
Expand Up @@ -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);
Expand Down
18 changes: 13 additions & 5 deletions packages/docusaurus-1.x/lib/core/toc.js
Expand Up @@ -19,7 +19,12 @@ const tocRegex = new RegExp('<AUTOGENERATED_TABLE_OF_CONTENTS>', '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
Expand All @@ -38,8 +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, slugger);
const slugBase =
slugPreprocessor && typeof slugPreprocessor === 'function'
? slugPreprocessor(rawContent)
: rawContent;
const hashLink = toSlug(slugBase, slugger);
if (!allowedHeadingLevels.includes(heading.lvl)) {
return;
}
Expand All @@ -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
// 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})`)
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-1.x/lib/server/docs.js
Expand Up @@ -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);
Expand Down

0 comments on commit aa7430e

Please sign in to comment.