mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-04 03:42:34 +02:00
fix(v2): remove HTML from heading slug (#2426)
* fix(v2): remove HTML from heading slug * Fix tests for rightToc
This commit is contained in:
parent
c50df3003c
commit
9cf3c66917
7 changed files with 302 additions and 11 deletions
|
@ -19,14 +19,15 @@
|
||||||
"loader-utils": "^1.2.3",
|
"loader-utils": "^1.2.3",
|
||||||
"mdast-util-to-string": "^1.0.7",
|
"mdast-util-to-string": "^1.0.7",
|
||||||
"remark-emoji": "^2.0.2",
|
"remark-emoji": "^2.0.2",
|
||||||
"remark-slug": "^5.1.2",
|
|
||||||
"stringify-object": "^3.3.0",
|
"stringify-object": "^3.3.0",
|
||||||
"unist-util-visit": "^2.0.1"
|
"unist-util-visit": "^2.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"remark": "^11.0.2",
|
"remark": "^11.0.2",
|
||||||
"remark-mdx": "^1.5.1",
|
"remark-mdx": "^1.5.1",
|
||||||
"to-vfile": "^6.0.0"
|
"to-vfile": "^6.0.0",
|
||||||
|
"unist-builder": "^2.0.3",
|
||||||
|
"unist-util-remove-position": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.9.0"
|
"node": ">=10.9.0"
|
||||||
|
|
|
@ -9,9 +9,9 @@ const {getOptions} = require('loader-utils');
|
||||||
const {readFile} = require('fs-extra');
|
const {readFile} = require('fs-extra');
|
||||||
const mdx = require('@mdx-js/mdx');
|
const mdx = require('@mdx-js/mdx');
|
||||||
const emoji = require('remark-emoji');
|
const emoji = require('remark-emoji');
|
||||||
const slug = require('remark-slug');
|
|
||||||
const matter = require('gray-matter');
|
const matter = require('gray-matter');
|
||||||
const stringifyObject = require('stringify-object');
|
const stringifyObject = require('stringify-object');
|
||||||
|
const slug = require('./remark/slug');
|
||||||
const rightToc = require('./remark/rightToc');
|
const rightToc = require('./remark/rightToc');
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
|
|
|
@ -62,7 +62,7 @@ exports[`non text phrasing content 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<i>HTML</i>',
|
value: '<i>HTML</i>',
|
||||||
id: 'ihtmli',
|
id: 'html',
|
||||||
children: []
|
children: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,11 +10,13 @@ import remark from 'remark';
|
||||||
import mdx from 'remark-mdx';
|
import mdx from 'remark-mdx';
|
||||||
import vfile from 'to-vfile';
|
import vfile from 'to-vfile';
|
||||||
import plugin from '../index';
|
import plugin from '../index';
|
||||||
|
import slug from '../../slug/index';
|
||||||
|
|
||||||
const processFixture = async (name, options) => {
|
const processFixture = async (name, options) => {
|
||||||
const path = join(__dirname, 'fixtures', `${name}.md`);
|
const path = join(__dirname, 'fixtures', `${name}.md`);
|
||||||
const file = await vfile.read(path);
|
const file = await vfile.read(path);
|
||||||
const result = await remark()
|
const result = await remark()
|
||||||
|
.use(slug)
|
||||||
.use(mdx)
|
.use(mdx)
|
||||||
.use(plugin, options)
|
.use(plugin, options)
|
||||||
.process(file);
|
.process(file);
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
const toString = require('mdast-util-to-string');
|
const toString = require('mdast-util-to-string');
|
||||||
const visit = require('unist-util-visit');
|
const visit = require('unist-util-visit');
|
||||||
const escapeHtml = require('escape-html');
|
const escapeHtml = require('escape-html');
|
||||||
const slugs = require('github-slugger')();
|
|
||||||
|
|
||||||
// https://github.com/syntax-tree/mdast#heading
|
// https://github.com/syntax-tree/mdast#heading
|
||||||
function toValue(node) {
|
function toValue(node) {
|
||||||
|
@ -40,19 +39,18 @@ function search(node) {
|
||||||
let current = -1;
|
let current = -1;
|
||||||
let currentDepth = 0;
|
let currentDepth = 0;
|
||||||
|
|
||||||
slugs.reset();
|
|
||||||
|
|
||||||
const onHeading = (child, index, parent) => {
|
const onHeading = (child, index, parent) => {
|
||||||
const value = toString(child);
|
const value = toString(child);
|
||||||
const id =
|
|
||||||
child.data && child.data.hProperties && child.data.hProperties.id;
|
|
||||||
const slug = slugs.slug(id || value);
|
|
||||||
|
|
||||||
if (parent !== node || !value || child.depth > 3 || child.depth < 2) {
|
if (parent !== node || !value || child.depth > 3 || child.depth < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = {value: toValue(child), id: slug, children: []};
|
const entry = {
|
||||||
|
value: toValue(child),
|
||||||
|
id: child.data.id,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
if (!headings.length || currentDepth >= child.depth) {
|
if (!headings.length || currentDepth >= child.depth) {
|
||||||
headings.push(entry);
|
headings.push(entry);
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Based on remark-slug (https://github.com/remarkjs/remark-slug) */
|
||||||
|
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
|
||||||
|
import remark from 'remark';
|
||||||
|
import u from 'unist-builder';
|
||||||
|
import removePosition from 'unist-util-remove-position';
|
||||||
|
import slug from '../index';
|
||||||
|
|
||||||
|
function process(doc, plugins = []) {
|
||||||
|
const processor = remark().use({plugins: [...plugins, slug]});
|
||||||
|
return removePosition(processor.runSync(processor.parse(doc)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function heading(label, id) {
|
||||||
|
return u(
|
||||||
|
'heading',
|
||||||
|
{depth: 2, data: {id, hProperties: {id}}},
|
||||||
|
label ? [u('text', label)] : [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('slug plugin', () => {
|
||||||
|
test('should patch `id`s and `data.hProperties.id', () => {
|
||||||
|
const result = process('# Normal\n\n## Table of Contents\n\n# Baz\n');
|
||||||
|
const expected = u('root', [
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{depth: 1, data: {hProperties: {id: 'normal'}, id: 'normal'}},
|
||||||
|
[u('text', 'Normal')],
|
||||||
|
),
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 2,
|
||||||
|
data: {
|
||||||
|
hProperties: {id: 'table-of-contents'},
|
||||||
|
id: 'table-of-contents',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[u('text', 'Table of Contents')],
|
||||||
|
),
|
||||||
|
u('heading', {depth: 1, data: {hProperties: {id: 'baz'}, id: 'baz'}}, [
|
||||||
|
u('text', 'Baz'),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not overwrite `data` on headings', () => {
|
||||||
|
const result = process('# Normal\n', [
|
||||||
|
function() {
|
||||||
|
function transform(tree) {
|
||||||
|
tree.children[0].data = {foo: 'bar'};
|
||||||
|
}
|
||||||
|
return transform;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const expected = u('root', [
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 1,
|
||||||
|
data: {foo: 'bar', hProperties: {id: 'normal'}, id: 'normal'},
|
||||||
|
},
|
||||||
|
[u('text', 'Normal')],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not overwrite `data.hProperties` on headings', () => {
|
||||||
|
const result = process('# Normal\n', [
|
||||||
|
function() {
|
||||||
|
function transform(tree) {
|
||||||
|
tree.children[0].data = {hProperties: {className: ['foo']}};
|
||||||
|
}
|
||||||
|
return transform;
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const expected = u('root', [
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 1,
|
||||||
|
data: {hProperties: {className: ['foo'], id: 'normal'}, id: 'normal'},
|
||||||
|
},
|
||||||
|
[u('text', 'Normal')],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate `id`s and `hProperties.id`s, based on `hProperties.id` if they exist', () => {
|
||||||
|
const result = process(
|
||||||
|
[
|
||||||
|
'## Something',
|
||||||
|
'## Something here',
|
||||||
|
'## Something there',
|
||||||
|
'## Something also',
|
||||||
|
].join('\n\n'),
|
||||||
|
[
|
||||||
|
function() {
|
||||||
|
function transform(tree) {
|
||||||
|
tree.children[1].data = {hProperties: {id: 'here'}};
|
||||||
|
tree.children[3].data = {hProperties: {id: 'something'}};
|
||||||
|
}
|
||||||
|
return transform;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const expected = u('root', [
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 2,
|
||||||
|
data: {hProperties: {id: 'something'}, id: 'something'},
|
||||||
|
},
|
||||||
|
[u('text', 'Something')],
|
||||||
|
),
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 2,
|
||||||
|
data: {hProperties: {id: 'here'}, id: 'here'},
|
||||||
|
},
|
||||||
|
[u('text', 'Something here')],
|
||||||
|
),
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 2,
|
||||||
|
data: {hProperties: {id: 'something-there'}, id: 'something-there'},
|
||||||
|
},
|
||||||
|
[u('text', 'Something there')],
|
||||||
|
),
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 2,
|
||||||
|
data: {hProperties: {id: 'something-1'}, id: 'something-1'},
|
||||||
|
},
|
||||||
|
[u('text', 'Something also')],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create GitHub slugs', () => {
|
||||||
|
const result = process(
|
||||||
|
[
|
||||||
|
'## I ♥ unicode',
|
||||||
|
'',
|
||||||
|
'## Dash-dash',
|
||||||
|
'',
|
||||||
|
'## en–dash',
|
||||||
|
'',
|
||||||
|
'## em–dash',
|
||||||
|
'',
|
||||||
|
'## 😄 unicode emoji',
|
||||||
|
'',
|
||||||
|
'## 😄-😄 unicode emoji',
|
||||||
|
'',
|
||||||
|
'## 😄_😄 unicode emoji',
|
||||||
|
'',
|
||||||
|
'##',
|
||||||
|
'',
|
||||||
|
'## ',
|
||||||
|
'',
|
||||||
|
'## Initial spaces',
|
||||||
|
'',
|
||||||
|
'## Final spaces ',
|
||||||
|
'',
|
||||||
|
'## Duplicate',
|
||||||
|
'',
|
||||||
|
'## Duplicate',
|
||||||
|
'',
|
||||||
|
'## :ok: No underscore',
|
||||||
|
'',
|
||||||
|
'## :ok_hand: Single',
|
||||||
|
'',
|
||||||
|
'## :ok_hand::hatched_chick: Two in a row with no spaces',
|
||||||
|
'',
|
||||||
|
'## :ok_hand: :hatched_chick: Two in a row',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
const expected = u('root', [
|
||||||
|
heading('I ♥ unicode', 'i--unicode'),
|
||||||
|
heading('Dash-dash', 'dash-dash'),
|
||||||
|
heading('en–dash', 'endash'),
|
||||||
|
heading('em–dash', 'emdash'),
|
||||||
|
heading('😄 unicode emoji', '-unicode-emoji'),
|
||||||
|
heading('😄-😄 unicode emoji', '--unicode-emoji'),
|
||||||
|
heading('😄_😄 unicode emoji', '_-unicode-emoji'),
|
||||||
|
heading(null, ''),
|
||||||
|
heading(null, '-1'),
|
||||||
|
heading('Initial spaces', 'initial-spaces'),
|
||||||
|
heading('Final spaces', 'final-spaces'),
|
||||||
|
heading('Duplicate', 'duplicate'),
|
||||||
|
heading('Duplicate', 'duplicate-1'),
|
||||||
|
heading(':ok: No underscore', 'ok-no-underscore'),
|
||||||
|
heading(':ok_hand: Single', 'ok_hand-single'),
|
||||||
|
heading(
|
||||||
|
':ok_hand::hatched_chick: Two in a row with no spaces',
|
||||||
|
'ok_handhatched_chick-two-in-a-row-with-no-spaces',
|
||||||
|
),
|
||||||
|
heading(
|
||||||
|
':ok_hand: :hatched_chick: Two in a row',
|
||||||
|
'ok_hand-hatched_chick-two-in-a-row',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should generate slug from only text contents of headings if they contains HTML tags', () => {
|
||||||
|
const result = process('# <span class="normal-header">Normal</span>\n');
|
||||||
|
const expected = u('root', [
|
||||||
|
u(
|
||||||
|
'heading',
|
||||||
|
{
|
||||||
|
depth: 1,
|
||||||
|
data: {hProperties: {id: 'normal'}, id: 'normal'},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
u('html', '<span class="normal-header">'),
|
||||||
|
u('text', 'Normal'),
|
||||||
|
u('html', '</span>'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
43
packages/docusaurus-mdx-loader/src/remark/slug/index.js
Normal file
43
packages/docusaurus-mdx-loader/src/remark/slug/index.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Based on remark-slug (https://github.com/remarkjs/remark-slug) */
|
||||||
|
|
||||||
|
const visit = require('unist-util-visit');
|
||||||
|
const toString = require('mdast-util-to-string');
|
||||||
|
const slugs = require('github-slugger')();
|
||||||
|
|
||||||
|
function slug() {
|
||||||
|
const transformer = ast => {
|
||||||
|
slugs.reset();
|
||||||
|
|
||||||
|
function visitor(headingNode) {
|
||||||
|
const data = headingNode.data || (headingNode.data = {}); // eslint-disable-line
|
||||||
|
const properties = data.hProperties || (data.hProperties = {});
|
||||||
|
let {id} = properties;
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
id = slugs.slug(id, true);
|
||||||
|
} else {
|
||||||
|
const headingTextNodes =
|
||||||
|
headingNode.children.find(
|
||||||
|
({type}) => !['html', 'jsx'].includes(type),
|
||||||
|
) || headingNode;
|
||||||
|
id = slugs.slug(toString(headingTextNodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
data.id = id;
|
||||||
|
properties.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(ast, 'heading', visitor);
|
||||||
|
};
|
||||||
|
|
||||||
|
return transformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = slug;
|
Loading…
Add table
Add a link
Reference in a new issue