mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 18:27:56 +02:00
fix(mdx-loader): refactor and fix heading to toc html value serialization (#11004)
* refactor with iso behavior * Add unit tests * change behavior for <img> tags
This commit is contained in:
parent
1d4d17da18
commit
e88f1aaf96
3 changed files with 207 additions and 76 deletions
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {toHeadingHTMLValue} from '../utils';
|
||||||
|
import type {Heading} from 'mdast';
|
||||||
|
|
||||||
|
describe('toHeadingHTMLValue', () => {
|
||||||
|
async function convert(heading: Heading): Promise<string> {
|
||||||
|
const {toString} = await import('mdast-util-to-string');
|
||||||
|
return toHeadingHTMLValue(heading, toString);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('converts a simple heading', async () => {
|
||||||
|
const heading: Heading = {
|
||||||
|
type: 'heading',
|
||||||
|
depth: 2,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
value: 'Some heading text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(convert(heading)).resolves.toMatchInlineSnapshot(
|
||||||
|
`"Some heading text"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts a heading with b tag', async () => {
|
||||||
|
const heading: Heading = {
|
||||||
|
type: 'heading',
|
||||||
|
depth: 2,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'mdxJsxTextElement',
|
||||||
|
name: 'b',
|
||||||
|
attributes: [],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
value: 'Some title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(convert(heading)).resolves.toMatchInlineSnapshot(
|
||||||
|
`"<b>Some title</b>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts a heading with span tag + className', async () => {
|
||||||
|
const heading: Heading = {
|
||||||
|
type: 'heading',
|
||||||
|
depth: 2,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'mdxJsxTextElement',
|
||||||
|
name: 'span',
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: 'mdxJsxAttribute',
|
||||||
|
name: 'className',
|
||||||
|
value: 'my-class',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
value: 'Some title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(convert(heading)).resolves.toMatchInlineSnapshot(
|
||||||
|
`"<span class="my-class">Some title</span>"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts a heading - remove img tag', async () => {
|
||||||
|
const heading: Heading = {
|
||||||
|
type: 'heading',
|
||||||
|
depth: 2,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'mdxJsxTextElement',
|
||||||
|
name: 'img',
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: 'mdxJsxAttribute',
|
||||||
|
name: 'src',
|
||||||
|
value: '/img/slash-introducing.svg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'mdxJsxAttribute',
|
||||||
|
name: 'height',
|
||||||
|
value: '32',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'mdxJsxAttribute',
|
||||||
|
name: 'alt',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
value: ' Some title',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(convert(heading)).resolves.toMatchInlineSnapshot(
|
||||||
|
`"Some title"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,9 +5,13 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {toValue} from '../utils';
|
import escapeHtml from 'escape-html';
|
||||||
import type {Node} from 'unist';
|
import type {Node, Parent} from 'unist';
|
||||||
import type {MdxjsEsm} from 'mdast-util-mdx';
|
import type {
|
||||||
|
MdxjsEsm,
|
||||||
|
MdxJsxAttribute,
|
||||||
|
MdxJsxTextElement,
|
||||||
|
} from 'mdast-util-mdx';
|
||||||
import type {TOCHeading, TOCItem, TOCItems, TOCSlice} from './types';
|
import type {TOCHeading, TOCItem, TOCItems, TOCSlice} from './types';
|
||||||
import type {
|
import type {
|
||||||
Program,
|
Program,
|
||||||
|
@ -15,6 +19,7 @@ import type {
|
||||||
ImportDeclaration,
|
ImportDeclaration,
|
||||||
ImportSpecifier,
|
ImportSpecifier,
|
||||||
} from 'estree';
|
} from 'estree';
|
||||||
|
import type {Heading, PhrasingContent} from 'mdast';
|
||||||
|
|
||||||
export function getImportDeclarations(program: Program): ImportDeclaration[] {
|
export function getImportDeclarations(program: Program): ImportDeclaration[] {
|
||||||
return program.body.filter(
|
return program.body.filter(
|
||||||
|
@ -118,7 +123,7 @@ export async function createTOCExportNodeAST({
|
||||||
const {toString} = await import('mdast-util-to-string');
|
const {toString} = await import('mdast-util-to-string');
|
||||||
const {valueToEstree} = await import('estree-util-value-to-estree');
|
const {valueToEstree} = await import('estree-util-value-to-estree');
|
||||||
const value: TOCItem = {
|
const value: TOCItem = {
|
||||||
value: toValue(heading, toString),
|
value: toHeadingHTMLValue(heading, toString),
|
||||||
id: heading.data!.id!,
|
id: heading.data!.id!,
|
||||||
level: heading.depth,
|
level: heading.depth,
|
||||||
};
|
};
|
||||||
|
@ -172,3 +177,73 @@ export async function createTOCExportNodeAST({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyChildren(
|
||||||
|
node: Parent,
|
||||||
|
toString: (param: unknown) => string, // TODO temporary, due to ESM
|
||||||
|
): string {
|
||||||
|
return (node.children as PhrasingContent[])
|
||||||
|
.map((item) => toHeadingHTMLValue(item, toString))
|
||||||
|
.join('')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO This is really a workaround, and not super reliable
|
||||||
|
// For now we only support serializing tagName, className and content
|
||||||
|
// Can we implement the TOC with real JSX nodes instead of html strings later?
|
||||||
|
function mdxJsxTextElementToHtml(
|
||||||
|
element: MdxJsxTextElement,
|
||||||
|
toString: (param: unknown) => string, // TODO temporary, due to ESM
|
||||||
|
): string {
|
||||||
|
const tag = element.name;
|
||||||
|
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/11003#issuecomment-2733925363
|
||||||
|
if (tag === 'img') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = element.attributes.filter(
|
||||||
|
(child): child is MdxJsxAttribute => child.type === 'mdxJsxAttribute',
|
||||||
|
);
|
||||||
|
|
||||||
|
const classAttribute =
|
||||||
|
attributes.find((attr) => attr.name === 'className') ??
|
||||||
|
attributes.find((attr) => attr.name === 'class');
|
||||||
|
|
||||||
|
const classAttributeString = classAttribute
|
||||||
|
? `class="${escapeHtml(String(classAttribute.value))}"`
|
||||||
|
: ``;
|
||||||
|
|
||||||
|
const allAttributes = classAttributeString ? ` ${classAttributeString}` : '';
|
||||||
|
|
||||||
|
const content = stringifyChildren(element, toString);
|
||||||
|
|
||||||
|
return `<${tag}${allAttributes}>${content}</${tag}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toHeadingHTMLValue(
|
||||||
|
node: PhrasingContent | Heading | MdxJsxTextElement,
|
||||||
|
toString: (param: unknown) => string, // TODO temporary, due to ESM
|
||||||
|
): string {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'mdxJsxTextElement': {
|
||||||
|
return mdxJsxTextElementToHtml(node as MdxJsxTextElement, toString);
|
||||||
|
}
|
||||||
|
case 'text':
|
||||||
|
return escapeHtml(node.value);
|
||||||
|
case 'heading':
|
||||||
|
return stringifyChildren(node, toString);
|
||||||
|
case 'inlineCode':
|
||||||
|
return `<code>${escapeHtml(node.value)}</code>`;
|
||||||
|
case 'emphasis':
|
||||||
|
return `<em>${stringifyChildren(node, toString)}</em>`;
|
||||||
|
case 'strong':
|
||||||
|
return `<strong>${stringifyChildren(node, toString)}</strong>`;
|
||||||
|
case 'delete':
|
||||||
|
return `<del>${stringifyChildren(node, toString)}</del>`;
|
||||||
|
case 'link':
|
||||||
|
return stringifyChildren(node, toString);
|
||||||
|
default:
|
||||||
|
return toString(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,14 +5,8 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import escapeHtml from 'escape-html';
|
import type {Node} from 'unist';
|
||||||
import type {Parent, Node} from 'unist';
|
import type {MdxJsxAttributeValueExpression} from 'mdast-util-mdx';
|
||||||
import type {PhrasingContent, Heading} from 'mdast';
|
|
||||||
import type {
|
|
||||||
MdxJsxAttribute,
|
|
||||||
MdxJsxAttributeValueExpression,
|
|
||||||
MdxJsxTextElement,
|
|
||||||
} from 'mdast-util-mdx';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util to transform one node type to another node type
|
* Util to transform one node type to another node type
|
||||||
|
@ -35,70 +29,6 @@ export function transformNode<NewNode extends Node>(
|
||||||
return node as NewNode;
|
return node as NewNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyContent(
|
|
||||||
node: Parent,
|
|
||||||
toString: (param: unknown) => string, // TODO weird but works
|
|
||||||
): string {
|
|
||||||
return (node.children as PhrasingContent[])
|
|
||||||
.map((item) => toValue(item, toString))
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO This is really a workaround, and not super reliable
|
|
||||||
// For now we only support serializing tagName, className and content
|
|
||||||
// Can we implement the TOC with real JSX nodes instead of html strings later?
|
|
||||||
function mdxJsxTextElementToHtml(
|
|
||||||
element: MdxJsxTextElement,
|
|
||||||
toString: (param: unknown) => string, // TODO weird but works
|
|
||||||
): string {
|
|
||||||
const tag = element.name;
|
|
||||||
|
|
||||||
const attributes = element.attributes.filter(
|
|
||||||
(child): child is MdxJsxAttribute => child.type === 'mdxJsxAttribute',
|
|
||||||
);
|
|
||||||
|
|
||||||
const classAttribute =
|
|
||||||
attributes.find((attr) => attr.name === 'className') ??
|
|
||||||
attributes.find((attr) => attr.name === 'class');
|
|
||||||
|
|
||||||
const classAttributeString = classAttribute
|
|
||||||
? `class="${escapeHtml(String(classAttribute.value))}"`
|
|
||||||
: ``;
|
|
||||||
|
|
||||||
const allAttributes = classAttributeString ? ` ${classAttributeString}` : '';
|
|
||||||
|
|
||||||
const content = stringifyContent(element, toString);
|
|
||||||
|
|
||||||
return `<${tag}${allAttributes}>${content}</${tag}>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toValue(
|
|
||||||
node: PhrasingContent | Heading | MdxJsxTextElement,
|
|
||||||
toString: (param: unknown) => string, // TODO weird but works
|
|
||||||
): string {
|
|
||||||
switch (node.type) {
|
|
||||||
case 'mdxJsxTextElement': {
|
|
||||||
return mdxJsxTextElementToHtml(node as MdxJsxTextElement, toString);
|
|
||||||
}
|
|
||||||
case 'text':
|
|
||||||
return escapeHtml(node.value);
|
|
||||||
case 'heading':
|
|
||||||
return stringifyContent(node, toString);
|
|
||||||
case 'inlineCode':
|
|
||||||
return `<code>${escapeHtml(node.value)}</code>`;
|
|
||||||
case 'emphasis':
|
|
||||||
return `<em>${stringifyContent(node, toString)}</em>`;
|
|
||||||
case 'strong':
|
|
||||||
return `<strong>${stringifyContent(node, toString)}</strong>`;
|
|
||||||
case 'delete':
|
|
||||||
return `<del>${stringifyContent(node, toString)}</del>`;
|
|
||||||
case 'link':
|
|
||||||
return stringifyContent(node, toString);
|
|
||||||
default:
|
|
||||||
return toString(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assetRequireAttributeValue(
|
export function assetRequireAttributeValue(
|
||||||
requireString: string,
|
requireString: string,
|
||||||
hash: string,
|
hash: string,
|
||||||
|
|
Loading…
Add table
Reference in a new issue