fix(v2): remove HTML from heading slug (#2426)

* fix(v2): remove HTML from heading slug

* Fix tests for rightToc
This commit is contained in:
Alexey Pyltsyn 2020-03-21 10:22:55 +03:00 committed by GitHub
parent c50df3003c
commit 9cf3c66917
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 302 additions and 11 deletions

View file

@ -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"

View file

@ -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 = {

View file

@ -62,7 +62,7 @@ exports[`non text phrasing content 1`] = `
},
{
value: '<i>HTML</i>',
id: 'ihtmli',
id: 'html',
children: []
},
{

View file

@ -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);

View 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);

View file

@ -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',
'',
'## endash',
'',
'## emdash',
'',
'## 😄 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('endash', 'endash'),
heading('emdash', '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);
});
});

View 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;