mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-02 10:52:35 +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",
|
||||
"mdast-util-to-string": "^1.0.7",
|
||||
"remark-emoji": "^2.0.2",
|
||||
"remark-slug": "^5.1.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"unist-util-visit": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"remark": "^11.0.2",
|
||||
"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": {
|
||||
"node": ">=10.9.0"
|
||||
|
|
|
@ -9,9 +9,9 @@ const {getOptions} = require('loader-utils');
|
|||
const {readFile} = require('fs-extra');
|
||||
const mdx = require('@mdx-js/mdx');
|
||||
const emoji = require('remark-emoji');
|
||||
const slug = require('remark-slug');
|
||||
const matter = require('gray-matter');
|
||||
const stringifyObject = require('stringify-object');
|
||||
const slug = require('./remark/slug');
|
||||
const rightToc = require('./remark/rightToc');
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
|
|
|
@ -62,7 +62,7 @@ exports[`non text phrasing content 1`] = `
|
|||
},
|
||||
{
|
||||
value: '<i>HTML</i>',
|
||||
id: 'ihtmli',
|
||||
id: 'html',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
|
|
|
@ -10,11 +10,13 @@ import remark from 'remark';
|
|||
import mdx from 'remark-mdx';
|
||||
import vfile from 'to-vfile';
|
||||
import plugin from '../index';
|
||||
import slug from '../../slug/index';
|
||||
|
||||
const processFixture = async (name, options) => {
|
||||
const path = join(__dirname, 'fixtures', `${name}.md`);
|
||||
const file = await vfile.read(path);
|
||||
const result = await remark()
|
||||
.use(slug)
|
||||
.use(mdx)
|
||||
.use(plugin, options)
|
||||
.process(file);
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
const toString = require('mdast-util-to-string');
|
||||
const visit = require('unist-util-visit');
|
||||
const escapeHtml = require('escape-html');
|
||||
const slugs = require('github-slugger')();
|
||||
|
||||
// https://github.com/syntax-tree/mdast#heading
|
||||
function toValue(node) {
|
||||
|
@ -40,19 +39,18 @@ function search(node) {
|
|||
let current = -1;
|
||||
let currentDepth = 0;
|
||||
|
||||
slugs.reset();
|
||||
|
||||
const onHeading = (child, index, parent) => {
|
||||
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) {
|
||||
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) {
|
||||
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