Skip to content

Commit 452c4e4

Browse files
fix(docs): resolve metadata paths with zero-width spaces
1 parent 22f2b36 commit 452c4e4

3 files changed

Lines changed: 145 additions & 3 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {describe, expect, it} from 'vitest';
9+
import {docuHash} from '@docusaurus/utils';
10+
import {fromPartial} from '@total-typescript/shoehorn';
11+
12+
import {createContentHelpers} from '../contentHelpers';
13+
import type {DocMetadata, LoadedContent} from '@docusaurus/plugin-content-docs';
14+
15+
function createLoadedContent(sources: string[]): LoadedContent {
16+
return fromPartial<LoadedContent>({
17+
loadedVersions: [
18+
{
19+
docs: sources.map((source) =>
20+
fromPartial<DocMetadata>({
21+
source,
22+
}),
23+
),
24+
},
25+
],
26+
});
27+
}
28+
29+
describe('contentHelpers', () => {
30+
it('resolves MDX loader metadata sources with zero-width spaces', () => {
31+
const asciiHelpers = createContentHelpers();
32+
asciiHelpers.updateContent(
33+
createLoadedContent([
34+
'@site/docs/contribute/vulnerability-report/index.mdx',
35+
]),
36+
);
37+
38+
expect(
39+
docuHash(
40+
asciiHelpers.getMetadataSource(
41+
'@site/docs/contribute/vulnerability-report/index.mdx',
42+
),
43+
),
44+
).toBe('site-docs-contribute-vulnerability-report-index-mdx-59e');
45+
46+
const trailingZeroWidthSpaceHelpers = createContentHelpers();
47+
trailingZeroWidthSpaceHelpers.updateContent(
48+
createLoadedContent([
49+
'@site/docs/contribute/vulnerability-report\u200B\u200B/index.mdx',
50+
]),
51+
);
52+
53+
expect(
54+
docuHash(
55+
trailingZeroWidthSpaceHelpers.getMetadataSource(
56+
'@site/docs/contribute/vulnerability-report\u200B/index.mdx',
57+
),
58+
),
59+
).toBe('site-docs-contribute-vulnerability-report-index-mdx-b66');
60+
61+
const internalZeroWidthSpaceHelpers = createContentHelpers();
62+
internalZeroWidthSpaceHelpers.updateContent(
63+
createLoadedContent([
64+
'@site/docs/contribute/vulnerability\u200B-report/index.mdx',
65+
]),
66+
);
67+
68+
expect(
69+
docuHash(
70+
internalZeroWidthSpaceHelpers.getMetadataSource(
71+
'@site/docs/contribute/vulnerability-report/index.mdx',
72+
),
73+
),
74+
).toBe('site-docs-contribute-vulnerability-report-index-mdx-ed8');
75+
76+
const unicodeHelpers = createContentHelpers();
77+
unicodeHelpers.updateContent(
78+
createLoadedContent(['@site/docs/contribute/보안-취약점-제보/index.mdx']),
79+
);
80+
81+
expect(
82+
docuHash(
83+
unicodeHelpers.getMetadataSource(
84+
'@site/docs/contribute/보안-취약점-제보/index.mdx',
85+
),
86+
),
87+
).toBe('site-docs-contribute-보안-취약점-제보-index-mdx-6b2');
88+
});
89+
90+
it('reports ambiguous zero-width-space metadata sources clearly', () => {
91+
const helpers = createContentHelpers();
92+
93+
helpers.updateContent(
94+
createLoadedContent([
95+
'@site/docs/contribute/vulnerability-report/index.mdx',
96+
'@site/docs/contribute/vulnerability\u200B-report/index.mdx',
97+
]),
98+
);
99+
100+
expect(() =>
101+
helpers.getMetadataSource(
102+
'@site/docs/contribute/vulnerability-report/index.mdx',
103+
),
104+
).toThrow(/U\+200B ZERO WIDTH SPACE/);
105+
});
106+
});

packages/docusaurus-plugin-content-docs/src/contentHelpers.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,63 @@
77

88
import type {DocMetadata, LoadedContent} from '@docusaurus/plugin-content-docs';
99

10+
const ZeroWidthSpace = '\u200B';
11+
1012
function indexDocsBySource(content: LoadedContent): Map<string, DocMetadata> {
1113
const allDocs = content.loadedVersions.flatMap((v) => v.docs);
1214
return new Map(allDocs.map((doc) => [doc.source, doc]));
1315
}
1416

17+
function getMetadataSourceKey(source: string): string {
18+
return source.replaceAll(ZeroWidthSpace, '');
19+
}
20+
21+
type ContentHelpers = {
22+
updateContent: (content: LoadedContent) => void;
23+
sourceToDoc: Map<string, DocMetadata>;
24+
sourceToPermalink: Map<string, string>;
25+
getMetadataSource: (source: string) => string;
26+
};
27+
1528
// TODO this is bad, we should have a better way to do this (new lifecycle?)
1629
// The source to doc/permalink is a mutable map passed to the mdx loader
1730
// See https://github.com/facebook/docusaurus/pull/10457
1831
// See https://github.com/facebook/docusaurus/pull/10185
19-
export function createContentHelpers() {
32+
export function createContentHelpers(): ContentHelpers {
2033
const sourceToDoc = new Map<string, DocMetadata>();
2134
const sourceToPermalink = new Map<string, string>();
35+
const metadataSourceKeyToSource = new Map<string, string>();
36+
const ambiguousMetadataSourceKeys = new Set<string>();
2237

2338
// Mutable map update :/
2439
function updateContent(content: LoadedContent): void {
2540
sourceToDoc.clear();
2641
sourceToPermalink.clear();
42+
metadataSourceKeyToSource.clear();
43+
ambiguousMetadataSourceKeys.clear();
2744
indexDocsBySource(content).forEach((value, key) => {
2845
sourceToDoc.set(key, value);
2946
sourceToPermalink.set(key, value.permalink);
47+
48+
const metadataSourceKey = getMetadataSourceKey(key);
49+
const existingSource = metadataSourceKeyToSource.get(metadataSourceKey);
50+
if (existingSource && existingSource !== key) {
51+
ambiguousMetadataSourceKeys.add(metadataSourceKey);
52+
metadataSourceKeyToSource.delete(metadataSourceKey);
53+
} else if (!ambiguousMetadataSourceKeys.has(metadataSourceKey)) {
54+
metadataSourceKeyToSource.set(metadataSourceKey, key);
55+
}
3056
});
3157
}
3258

33-
return {updateContent, sourceToDoc, sourceToPermalink};
59+
function getMetadataSource(source: string): string {
60+
const metadataSourceKey = getMetadataSourceKey(source);
61+
if (ambiguousMetadataSourceKeys.has(metadataSourceKey)) {
62+
throw new Error(`Docusaurus could not safely resolve the docs metadata path for "${source}" because multiple docs paths only differ by U+200B ZERO WIDTH SPACE characters.
63+
Please rename the affected docs files or folders to remove the invisible zero-width spaces.`);
64+
}
65+
return metadataSourceKeyToSource.get(metadataSourceKey) ?? source;
66+
}
67+
68+
return {updateContent, sourceToDoc, sourceToPermalink, getMetadataSource};
3469
}

packages/docusaurus-plugin-content-docs/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ export default async function pluginContentDocs(
150150
// Note that metadataPath must be the same/in-sync as
151151
// the path from createData for each MDX.
152152
const aliasedPath = aliasedSitePath(mdxPath, siteDir);
153-
return path.join(dataDir, `${docuHash(aliasedPath)}.json`);
153+
const metadataSource = contentHelpers.getMetadataSource(aliasedPath);
154+
return path.join(dataDir, `${docuHash(metadataSource)}.json`);
154155
},
155156
// createAssets converts relative paths to require() calls
156157
createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({

0 commit comments

Comments
 (0)