docusaurus-plugin-docusynx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/assets.d.ts +7 -0
- package/dist/assets.js +46 -0
- package/dist/canonical.d.ts +2 -0
- package/dist/canonical.js +21 -0
- package/dist/discover.d.ts +24 -0
- package/dist/discover.js +335 -0
- package/dist/html.d.ts +22 -0
- package/dist/html.js +329 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/markdown.d.ts +11 -0
- package/dist/markdown.js +334 -0
- package/dist/plugin.d.ts +24 -0
- package/dist/plugin.js +355 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.js +3 -0
- package/package.json +74 -0
- package/schema/document-bundle.schema.json +234 -0
package/dist/discover.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
export async function discoverDocuments(input) {
|
|
5
|
+
const documents = new Map();
|
|
6
|
+
let nextOrder = 0;
|
|
7
|
+
for (const value of objectValuesDeep(input.allContent)) {
|
|
8
|
+
const versions = arrayProperty(value, 'loadedVersions');
|
|
9
|
+
if (versions) {
|
|
10
|
+
for (const versionValue of versions) {
|
|
11
|
+
const version = asRecord(versionValue);
|
|
12
|
+
if (!version)
|
|
13
|
+
continue;
|
|
14
|
+
const versionName = stringProperty(version, 'versionName') ??
|
|
15
|
+
stringProperty(version, 'name') ??
|
|
16
|
+
'current';
|
|
17
|
+
const prefix = versionName === 'current' ? '' : `${versionName}:`;
|
|
18
|
+
const docs = arrayProperty(version, 'docs') ?? [];
|
|
19
|
+
for (const docValue of docs) {
|
|
20
|
+
const candidate = await candidateFromMetadata(docValue, input.siteDir, prefix, nextOrder++);
|
|
21
|
+
if (candidate)
|
|
22
|
+
documents.set(candidate.id, candidate);
|
|
23
|
+
}
|
|
24
|
+
applySidebars(version.sidebars, documents, prefix, () => nextOrder++);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const pages = arrayProperty(value, 'loadedPages');
|
|
28
|
+
if (pages) {
|
|
29
|
+
for (const pageValue of pages) {
|
|
30
|
+
const candidate = await candidateFromMetadata(pageValue, input.siteDir, 'page:', nextOrder++);
|
|
31
|
+
if (candidate) {
|
|
32
|
+
// Pages can define local JSX components that have no stable import key.
|
|
33
|
+
// Their rendered <main> content is the deterministic public contract.
|
|
34
|
+
candidate.forceRendered = true;
|
|
35
|
+
documents.set(candidate.id, candidate);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
reconcileGeneratedIndexRoutes(documents, input.routePaths);
|
|
41
|
+
const routeSet = new Set([...documents.values()].map((document) => normalizeRoute(document.route)));
|
|
42
|
+
for (const route of [...input.routePaths].sort()) {
|
|
43
|
+
const normalizedRoute = normalizeRoute(route);
|
|
44
|
+
const exclusions = [
|
|
45
|
+
'/404.html',
|
|
46
|
+
'/404/',
|
|
47
|
+
...(input.options.excludeRoutePatterns ?? []),
|
|
48
|
+
];
|
|
49
|
+
if (matchesAny(normalizedRoute, exclusions) ||
|
|
50
|
+
matchesAny(route, exclusions))
|
|
51
|
+
continue;
|
|
52
|
+
if (routeSet.has(normalizedRoute))
|
|
53
|
+
continue;
|
|
54
|
+
if (!(await renderedRouteExists(input.outDir, normalizedRoute)))
|
|
55
|
+
continue;
|
|
56
|
+
const id = normalizedRoute === '/'
|
|
57
|
+
? 'page:index'
|
|
58
|
+
: `route:${normalizedRoute.replace(/^\/|\/$/g, '')}`;
|
|
59
|
+
documents.set(id, {
|
|
60
|
+
id,
|
|
61
|
+
title: normalizedRoute === '/'
|
|
62
|
+
? 'Home'
|
|
63
|
+
: titleFromRoute(normalizedRoute),
|
|
64
|
+
route: normalizedRoute,
|
|
65
|
+
sourcePath: `.docusaurus/routes${normalizedRoute}index.html`,
|
|
66
|
+
order: nextOrder++,
|
|
67
|
+
forceRendered: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
for (const document of documents.values()) {
|
|
71
|
+
if (matchesAny(document.route, input.options.renderedRoutePatterns ?? []))
|
|
72
|
+
document.forceRendered = true;
|
|
73
|
+
}
|
|
74
|
+
return [...documents.values()].sort(compareCandidate);
|
|
75
|
+
}
|
|
76
|
+
async function candidateFromMetadata(value, siteDir, idPrefix, order) {
|
|
77
|
+
const outer = asRecord(value);
|
|
78
|
+
if (!outer)
|
|
79
|
+
return undefined;
|
|
80
|
+
const metadata = asRecord(outer.metadata) ?? outer;
|
|
81
|
+
const route = stringProperty(metadata, 'permalink') ??
|
|
82
|
+
stringProperty(metadata, 'route');
|
|
83
|
+
const rawSource = stringProperty(metadata, 'source') ??
|
|
84
|
+
stringProperty(metadata, 'sourcePath');
|
|
85
|
+
if (!route || !rawSource)
|
|
86
|
+
return undefined;
|
|
87
|
+
const absolute = resolveSourcePath(siteDir, rawSource);
|
|
88
|
+
let frontMatter = asRecord(metadata.frontMatter) ?? {};
|
|
89
|
+
try {
|
|
90
|
+
const parsed = matter(await readFile(absolute, 'utf8'));
|
|
91
|
+
frontMatter = { ...parsed.data, ...frontMatter };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// The rendered route remains available when a generated source is outside the site tree.
|
|
95
|
+
}
|
|
96
|
+
const routeId = normalizeRoute(route) === '/'
|
|
97
|
+
? 'index'
|
|
98
|
+
: normalizeRoute(route).replace(/^\/|\/$/g, '');
|
|
99
|
+
const rawId = stringProperty(frontMatter, 'docusynx_id') ??
|
|
100
|
+
stringProperty(metadata, 'id') ??
|
|
101
|
+
routeId;
|
|
102
|
+
const sourceOverride = stringProperty(frontMatter, 'source_path') ??
|
|
103
|
+
stringProperty(frontMatter, 'docusynx_source_path');
|
|
104
|
+
return {
|
|
105
|
+
id: `${idPrefix}${rawId}`,
|
|
106
|
+
title: stringProperty(metadata, 'title') ??
|
|
107
|
+
stringProperty(frontMatter, 'title') ??
|
|
108
|
+
titleFromRoute(route),
|
|
109
|
+
route: normalizeRoute(route),
|
|
110
|
+
sourcePath: sourceOverride ?? normalizeSourcePath(siteDir, absolute, rawId),
|
|
111
|
+
sourcePathIsRepositoryRelative: sourceOverride !== undefined,
|
|
112
|
+
sourceAbsolutePath: absolute,
|
|
113
|
+
order,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function applySidebars(sidebarsValue, documents, prefix, nextOrder) {
|
|
117
|
+
const sidebars = asRecord(sidebarsValue);
|
|
118
|
+
if (!sidebars)
|
|
119
|
+
return;
|
|
120
|
+
for (const sidebarName of Object.keys(sidebars).sort()) {
|
|
121
|
+
const items = Array.isArray(sidebars[sidebarName])
|
|
122
|
+
? sidebars[sidebarName]
|
|
123
|
+
: [];
|
|
124
|
+
walkSidebar(items, undefined, [sidebarName], documents, prefix, nextOrder);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function walkSidebar(items, parentId, ancestry, documents, prefix, nextOrder) {
|
|
128
|
+
items.forEach((itemValue, index) => {
|
|
129
|
+
const item = asRecord(itemValue);
|
|
130
|
+
if (!item)
|
|
131
|
+
return;
|
|
132
|
+
const type = stringProperty(item, 'type');
|
|
133
|
+
if (type === 'doc') {
|
|
134
|
+
const id = `${prefix}${stringProperty(item, 'id') ?? ''}`;
|
|
135
|
+
if (id === parentId)
|
|
136
|
+
return;
|
|
137
|
+
const document = documents.get(id);
|
|
138
|
+
if (document) {
|
|
139
|
+
document.parentId = parentId;
|
|
140
|
+
document.order = index;
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (type !== 'category')
|
|
145
|
+
return;
|
|
146
|
+
const label = stringProperty(item, 'label') ?? 'Category';
|
|
147
|
+
const link = asRecord(item.link);
|
|
148
|
+
const explicitLinkedId = stringProperty(link ?? {}, 'type') === 'doc'
|
|
149
|
+
? `${prefix}${stringProperty(link ?? {}, 'id') ?? ''}`
|
|
150
|
+
: undefined;
|
|
151
|
+
const linkedId = explicitLinkedId ??
|
|
152
|
+
indexDocumentId(item.items, documents, prefix);
|
|
153
|
+
const id = linkedId && documents.has(linkedId)
|
|
154
|
+
? linkedId
|
|
155
|
+
: `${prefix}category:${slug([...ancestry, label].join('/'))}`;
|
|
156
|
+
if (!documents.has(id)) {
|
|
157
|
+
const route = stringProperty(link ?? {}, 'slug') ??
|
|
158
|
+
`/.docusynx/category/${slug([...ancestry, label].join('/'))}/`;
|
|
159
|
+
const generatedIndexRoute = stringProperty(link ?? {}, 'type') === 'generated-index'
|
|
160
|
+
? normalizeRoute(route)
|
|
161
|
+
: undefined;
|
|
162
|
+
documents.set(id, {
|
|
163
|
+
id,
|
|
164
|
+
title: label,
|
|
165
|
+
route: normalizeRoute(route),
|
|
166
|
+
sourcePath: `.docusynx/categories/${slug([...ancestry, label].join('/'))}`,
|
|
167
|
+
parentId,
|
|
168
|
+
order: nextOrder(),
|
|
169
|
+
syntheticBlocks: [
|
|
170
|
+
{
|
|
171
|
+
type: 'heading',
|
|
172
|
+
level: 1,
|
|
173
|
+
inlines: [{ type: 'text', value: label }],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
...(generatedIndexRoute ? { generatedIndexRoute } : {}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const categoryDocument = documents.get(id);
|
|
181
|
+
if (categoryDocument) {
|
|
182
|
+
categoryDocument.parentId = parentId;
|
|
183
|
+
categoryDocument.order = index;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
walkSidebar(Array.isArray(item.items) ? item.items : [], id, [...ancestry, label], documents, prefix, nextOrder);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function indexDocumentId(itemsValue, documents, prefix) {
|
|
190
|
+
if (!Array.isArray(itemsValue))
|
|
191
|
+
return undefined;
|
|
192
|
+
for (const itemValue of itemsValue) {
|
|
193
|
+
const item = asRecord(itemValue);
|
|
194
|
+
if (!item || stringProperty(item, 'type') !== 'doc')
|
|
195
|
+
continue;
|
|
196
|
+
const rawId = stringProperty(item, 'id') ?? '';
|
|
197
|
+
const basename = rawId.split('/').at(-1);
|
|
198
|
+
if (basename !== 'index' && basename !== 'overview')
|
|
199
|
+
continue;
|
|
200
|
+
const id = `${prefix}${rawId}`;
|
|
201
|
+
if (documents.has(id))
|
|
202
|
+
return id;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
function reconcileGeneratedIndexRoutes(documents, routePaths) {
|
|
207
|
+
const normalizedRoutes = [...new Set(routePaths.map(normalizeRoute))].sort();
|
|
208
|
+
for (const document of documents.values()) {
|
|
209
|
+
if (!document.generatedIndexRoute)
|
|
210
|
+
continue;
|
|
211
|
+
const suffix = document.generatedIndexRoute;
|
|
212
|
+
const matches = normalizedRoutes.filter((route) => route === suffix || route.endsWith(suffix));
|
|
213
|
+
if (matches.length > 1) {
|
|
214
|
+
throw new Error(`generated-index category ${document.id} matches multiple rendered routes: ${matches.join(', ')}`);
|
|
215
|
+
}
|
|
216
|
+
const renderedRoute = matches[0];
|
|
217
|
+
if (!renderedRoute)
|
|
218
|
+
continue;
|
|
219
|
+
document.route = renderedRoute;
|
|
220
|
+
document.sourcePath = `.docusaurus/routes${renderedRoute}index.html`;
|
|
221
|
+
document.sourcePathIsRepositoryRelative = false;
|
|
222
|
+
document.syntheticBlocks = undefined;
|
|
223
|
+
document.forceRendered = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export async function readRenderedRoute(outDir, route) {
|
|
227
|
+
for (const candidate of renderedRouteCandidates(outDir, route)) {
|
|
228
|
+
try {
|
|
229
|
+
return await readFile(candidate, 'utf8');
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Try the next Docusaurus trailing-slash form.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
throw new Error(`no rendered HTML found for route ${route}`);
|
|
236
|
+
}
|
|
237
|
+
async function renderedRouteExists(outDir, route) {
|
|
238
|
+
for (const candidate of renderedRouteCandidates(outDir, route)) {
|
|
239
|
+
try {
|
|
240
|
+
await access(candidate);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Try the next form.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
function renderedRouteCandidates(outDir, route) {
|
|
250
|
+
const relative = normalizeRoute(route).replace(/^\/+|\/+$/g, '');
|
|
251
|
+
if (!relative)
|
|
252
|
+
return [path.join(outDir, 'index.html')];
|
|
253
|
+
return [
|
|
254
|
+
path.join(outDir, relative, 'index.html'),
|
|
255
|
+
path.join(outDir, `${relative}.html`),
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
function objectValuesDeep(value) {
|
|
259
|
+
const results = [];
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
const walk = (entry) => {
|
|
262
|
+
if (entry === null || typeof entry !== 'object' || seen.has(entry))
|
|
263
|
+
return;
|
|
264
|
+
seen.add(entry);
|
|
265
|
+
if (Array.isArray(entry)) {
|
|
266
|
+
entry.forEach(walk);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const record = entry;
|
|
270
|
+
results.push(record);
|
|
271
|
+
Object.values(record).forEach(walk);
|
|
272
|
+
};
|
|
273
|
+
walk(value);
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
276
|
+
function arrayProperty(record, key) {
|
|
277
|
+
return Array.isArray(record[key]) ? record[key] : undefined;
|
|
278
|
+
}
|
|
279
|
+
function asRecord(value) {
|
|
280
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
281
|
+
? value
|
|
282
|
+
: undefined;
|
|
283
|
+
}
|
|
284
|
+
function stringProperty(record, key) {
|
|
285
|
+
return typeof record[key] === 'string' ? record[key] : undefined;
|
|
286
|
+
}
|
|
287
|
+
function resolveSourcePath(siteDir, source) {
|
|
288
|
+
if (path.isAbsolute(source))
|
|
289
|
+
return source;
|
|
290
|
+
return path.resolve(siteDir, source.replace(/^@site\//, ''));
|
|
291
|
+
}
|
|
292
|
+
function normalizeSourcePath(siteDir, source, documentId) {
|
|
293
|
+
const relative = path.relative(siteDir, source);
|
|
294
|
+
return relative.startsWith('..') || path.isAbsolute(relative)
|
|
295
|
+
? `.generated/${slug(documentId)}/${path.basename(source)}`
|
|
296
|
+
: relative.replaceAll(path.sep, '/');
|
|
297
|
+
}
|
|
298
|
+
export function normalizeRoute(route) {
|
|
299
|
+
const pathname = route.split(/[?#]/)[0] || '/';
|
|
300
|
+
return `/${pathname.replace(/^\/+|\/+$/g, '')}${pathname === '/' ? '' : '/'}`;
|
|
301
|
+
}
|
|
302
|
+
function titleFromRoute(route) {
|
|
303
|
+
const segment = route
|
|
304
|
+
.replace(/^\/+|\/+$/g, '')
|
|
305
|
+
.split('/')
|
|
306
|
+
.at(-1) || 'Home';
|
|
307
|
+
return segment
|
|
308
|
+
.replace(/[-_]+/g, ' ')
|
|
309
|
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
310
|
+
}
|
|
311
|
+
function slug(value) {
|
|
312
|
+
return value
|
|
313
|
+
.toLowerCase()
|
|
314
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
315
|
+
.replace(/^-|-$/g, '');
|
|
316
|
+
}
|
|
317
|
+
function matchesAny(value, patterns) {
|
|
318
|
+
return patterns.some((pattern) => matchesRoutePattern(value, pattern));
|
|
319
|
+
}
|
|
320
|
+
export function matchesRoutePattern(route, pattern) {
|
|
321
|
+
const expression = pattern
|
|
322
|
+
.split('**')
|
|
323
|
+
.map((part) => part.split('*').map(escapeExpression).join('[^/]*'))
|
|
324
|
+
.join('.*');
|
|
325
|
+
return new RegExp(`^${expression}$`).test(route);
|
|
326
|
+
}
|
|
327
|
+
function escapeExpression(value) {
|
|
328
|
+
return value.replace(/[|\\{}()[\]^$+?.-]/g, '\\$&');
|
|
329
|
+
}
|
|
330
|
+
function compareCandidate(left, right) {
|
|
331
|
+
return left.order - right.order || compareStrings(left.id, right.id);
|
|
332
|
+
}
|
|
333
|
+
function compareStrings(left, right) {
|
|
334
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
335
|
+
}
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AssetCollector } from './assets.js';
|
|
2
|
+
import type { Block } from './types.js';
|
|
3
|
+
export type RenderedLinkSemantic = {
|
|
4
|
+
href: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function renderedHtmlToBlocks(input: {
|
|
8
|
+
html: string;
|
|
9
|
+
outDir: string;
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
assets: AssetCollector;
|
|
12
|
+
selectors?: string[];
|
|
13
|
+
route: string;
|
|
14
|
+
strict?: boolean;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
title?: string;
|
|
17
|
+
blocks: Block[];
|
|
18
|
+
}>;
|
|
19
|
+
export declare function renderedHtmlLinkSemantics(input: {
|
|
20
|
+
html: string;
|
|
21
|
+
selectors?: string[];
|
|
22
|
+
}): RenderedLinkSemantic[];
|
package/dist/html.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { load } from 'cheerio';
|
|
3
|
+
export async function renderedHtmlToBlocks(input) {
|
|
4
|
+
const { $, root } = renderedContent(input.html, input.selectors);
|
|
5
|
+
const title = root.find('h1').first().text().trim() ||
|
|
6
|
+
$('title').text().split('|')[0]?.trim();
|
|
7
|
+
const blocks = await transformElements($, root.children().toArray(), {
|
|
8
|
+
...input,
|
|
9
|
+
strict: input.strict !== false,
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
...(title ? { title } : {}),
|
|
13
|
+
blocks,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function renderedHtmlLinkSemantics(input) {
|
|
17
|
+
const { $, root } = renderedContent(input.html, input.selectors);
|
|
18
|
+
const semantics = [];
|
|
19
|
+
for (const element of root.find('a[href]').toArray()) {
|
|
20
|
+
const anchor = $(element);
|
|
21
|
+
const href = anchor.attr('href') ?? '';
|
|
22
|
+
semantics.push({ href, text: normalizeVisibleText(anchor.text()) });
|
|
23
|
+
}
|
|
24
|
+
return semantics;
|
|
25
|
+
}
|
|
26
|
+
function renderedContent(html, selectors) {
|
|
27
|
+
const $ = load(html);
|
|
28
|
+
const contentSelectors = selectors ?? [
|
|
29
|
+
'main article .theme-doc-markdown',
|
|
30
|
+
'main article',
|
|
31
|
+
'main',
|
|
32
|
+
];
|
|
33
|
+
let root = $('body');
|
|
34
|
+
for (const selector of contentSelectors) {
|
|
35
|
+
const candidate = $(selector).first();
|
|
36
|
+
if (candidate.length) {
|
|
37
|
+
root = candidate;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
root.find('nav, footer, script, style, .theme-doc-toc-mobile, .theme-doc-toc-desktop, .pagination-nav, .breadcrumbs').remove();
|
|
42
|
+
return { $, root };
|
|
43
|
+
}
|
|
44
|
+
async function transformElements($, nodes, context) {
|
|
45
|
+
const blocks = [];
|
|
46
|
+
for (const node of nodes) {
|
|
47
|
+
if (node.type === 'text') {
|
|
48
|
+
const value = $(node).text().trim();
|
|
49
|
+
if (value)
|
|
50
|
+
blocks.push({
|
|
51
|
+
type: 'paragraph',
|
|
52
|
+
inlines: [{ type: 'text', value }],
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (node.type !== 'tag')
|
|
57
|
+
continue;
|
|
58
|
+
const element = node;
|
|
59
|
+
const name = element.name.toLowerCase();
|
|
60
|
+
if (name === 'a') {
|
|
61
|
+
const href = $(element).attr('href');
|
|
62
|
+
const children = await transformElements($, element.children, context);
|
|
63
|
+
rejectMeaningfulEmptyAnchor($, element, href ?? '', children.length, context);
|
|
64
|
+
blocks.push(...(href
|
|
65
|
+
? applyLinkToBlocks(children, href, context)
|
|
66
|
+
: children));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (/^h[1-6]$/.test(name)) {
|
|
70
|
+
blocks.push({
|
|
71
|
+
type: 'heading',
|
|
72
|
+
level: Number(name.slice(1)),
|
|
73
|
+
inlines: transformInlines($, element.children, context),
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (name === 'p') {
|
|
78
|
+
const images = $(element).children('img');
|
|
79
|
+
if (images.length === 1 && $(element).text().trim() === '') {
|
|
80
|
+
blocks.push(await transformImage($, images.get(0), context));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
blocks.push({
|
|
84
|
+
type: 'paragraph',
|
|
85
|
+
inlines: transformInlines($, element.children, context),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (name === 'pre') {
|
|
91
|
+
const code = $(element).find('code').first();
|
|
92
|
+
const language = classLanguage(code.attr('class'));
|
|
93
|
+
const value = code.length
|
|
94
|
+
? code.text().replace(/\n$/, '')
|
|
95
|
+
: $(element).text().replace(/\n$/, '');
|
|
96
|
+
blocks.push(language === 'mermaid'
|
|
97
|
+
? { type: 'mermaid', value }
|
|
98
|
+
: {
|
|
99
|
+
type: 'code',
|
|
100
|
+
...(language ? { language } : {}),
|
|
101
|
+
value,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (name === 'ul' || name === 'ol') {
|
|
106
|
+
const items = [];
|
|
107
|
+
for (const item of $(element).children('li').toArray()) {
|
|
108
|
+
const childBlocks = await transformElements($, item.children, context);
|
|
109
|
+
items.push(childBlocks.length
|
|
110
|
+
? childBlocks
|
|
111
|
+
: [
|
|
112
|
+
{
|
|
113
|
+
type: 'paragraph',
|
|
114
|
+
inlines: transformInlines($, item.children, context),
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
blocks.push({ type: 'list', ordered: name === 'ol', items });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (name === 'table') {
|
|
122
|
+
const rows = $(element)
|
|
123
|
+
.find('tr')
|
|
124
|
+
.toArray()
|
|
125
|
+
.map((row) => $(row)
|
|
126
|
+
.children('th,td')
|
|
127
|
+
.toArray()
|
|
128
|
+
.map((cell) => transformInlines($, cell.children, context)));
|
|
129
|
+
blocks.push({
|
|
130
|
+
type: 'table',
|
|
131
|
+
header: rows[0] ?? [],
|
|
132
|
+
rows: rows.slice(1),
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (name === 'img') {
|
|
137
|
+
blocks.push(await transformImage($, element, context));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (name === 'hr') {
|
|
141
|
+
blocks.push({ type: 'thematicBreak' });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const className = $(element).attr('class') ?? '';
|
|
145
|
+
if (name === 'blockquote' || className.split(/\s+/).includes('alert')) {
|
|
146
|
+
const alertKind = className.match(/alert--([\w-]+)/)?.[1];
|
|
147
|
+
blocks.push({
|
|
148
|
+
type: 'admonition',
|
|
149
|
+
kind: name === 'blockquote' ? 'quote' : (alertKind ?? 'note'),
|
|
150
|
+
blocks: await transformElements($, element.children, context),
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
blocks.push(...(await transformElements($, element.children, context)));
|
|
155
|
+
}
|
|
156
|
+
return blocks.filter((block) => block.type !== 'paragraph' || block.inlines.length > 0);
|
|
157
|
+
}
|
|
158
|
+
function applyLinkToBlocks(blocks, href, context) {
|
|
159
|
+
return blocks.map((block) => applyLinkToBlock(block, href, context));
|
|
160
|
+
}
|
|
161
|
+
function applyLinkToBlock(block, href, context) {
|
|
162
|
+
switch (block.type) {
|
|
163
|
+
case 'paragraph':
|
|
164
|
+
case 'heading':
|
|
165
|
+
if (containsLink(block.inlines))
|
|
166
|
+
return unsupportedLinkedBlock(block, href, context, 'nested link');
|
|
167
|
+
return {
|
|
168
|
+
...block,
|
|
169
|
+
inlines: block.inlines.length === 0
|
|
170
|
+
? block.inlines
|
|
171
|
+
: [
|
|
172
|
+
{
|
|
173
|
+
type: 'link',
|
|
174
|
+
target: { kind: 'url', value: href },
|
|
175
|
+
children: block.inlines,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
case 'list':
|
|
180
|
+
return {
|
|
181
|
+
...block,
|
|
182
|
+
items: block.items.map((item) => applyLinkToBlocks(item, href, context)),
|
|
183
|
+
};
|
|
184
|
+
case 'admonition':
|
|
185
|
+
return {
|
|
186
|
+
...block,
|
|
187
|
+
blocks: applyLinkToBlocks(block.blocks, href, context),
|
|
188
|
+
};
|
|
189
|
+
case 'table':
|
|
190
|
+
case 'code':
|
|
191
|
+
case 'mermaid':
|
|
192
|
+
case 'image':
|
|
193
|
+
case 'thematicBreak':
|
|
194
|
+
case 'extension':
|
|
195
|
+
return unsupportedLinkedBlock(block, href, context, 'unsupported block semantics');
|
|
196
|
+
default:
|
|
197
|
+
return assertNever(block);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function unsupportedLinkedBlock(block, href, context, reason) {
|
|
201
|
+
rejectUnsupportedLink(context, href, reason, block.type);
|
|
202
|
+
return block;
|
|
203
|
+
}
|
|
204
|
+
function rejectUnsupportedLink(context, href, reason, blockType) {
|
|
205
|
+
if (!context.strict)
|
|
206
|
+
return;
|
|
207
|
+
throw new Error(`cannot preserve block link on route ${context.route} for href ${href}: ${reason} for block type ${blockType}`);
|
|
208
|
+
}
|
|
209
|
+
function containsLink(inlines) {
|
|
210
|
+
return inlines.some((inline) => inline.type === 'link');
|
|
211
|
+
}
|
|
212
|
+
function assertNever(value) {
|
|
213
|
+
throw new Error(`unsupported rendered block: ${JSON.stringify(value)}`);
|
|
214
|
+
}
|
|
215
|
+
function transformInlines($, nodes, context, marks = []) {
|
|
216
|
+
const result = [];
|
|
217
|
+
for (const node of nodes) {
|
|
218
|
+
if (node.type === 'text') {
|
|
219
|
+
result.push({
|
|
220
|
+
type: 'text',
|
|
221
|
+
value: $(node).text(),
|
|
222
|
+
...(marks.length ? { marks: [...marks].sort() } : {}),
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (node.type !== 'tag')
|
|
227
|
+
continue;
|
|
228
|
+
const element = node;
|
|
229
|
+
const name = element.name.toLowerCase();
|
|
230
|
+
if (name === 'strong' || name === 'b')
|
|
231
|
+
result.push(...transformInlines($, element.children, context, [
|
|
232
|
+
...marks,
|
|
233
|
+
'bold',
|
|
234
|
+
]));
|
|
235
|
+
else if (name === 'em' || name === 'i')
|
|
236
|
+
result.push(...transformInlines($, element.children, context, [
|
|
237
|
+
...marks,
|
|
238
|
+
'italic',
|
|
239
|
+
]));
|
|
240
|
+
else if (name === 'del' || name === 's')
|
|
241
|
+
result.push(...transformInlines($, element.children, context, [
|
|
242
|
+
...marks,
|
|
243
|
+
'strikethrough',
|
|
244
|
+
]));
|
|
245
|
+
else if (name === 'code')
|
|
246
|
+
result.push(...transformInlines($, element.children, context, [
|
|
247
|
+
...marks,
|
|
248
|
+
'code',
|
|
249
|
+
]));
|
|
250
|
+
else if (name === 'a') {
|
|
251
|
+
const href = $(element).attr('href') ?? '';
|
|
252
|
+
if ($(element).find('img').length > 0)
|
|
253
|
+
rejectUnsupportedLink(context, href, 'unsupported block semantics', 'image');
|
|
254
|
+
const children = transformInlines($, element.children, context, marks);
|
|
255
|
+
rejectMeaningfulEmptyAnchor($, element, href, children.length, context);
|
|
256
|
+
result.push({
|
|
257
|
+
type: 'link',
|
|
258
|
+
target: { kind: 'url', value: href },
|
|
259
|
+
children,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else if (name === 'br')
|
|
263
|
+
result.push({ type: 'text', value: '\n' });
|
|
264
|
+
else
|
|
265
|
+
result.push(...transformInlines($, element.children, context, marks));
|
|
266
|
+
}
|
|
267
|
+
return mergeText(result);
|
|
268
|
+
}
|
|
269
|
+
function rejectMeaningfulEmptyAnchor($, element, href, outputLength, context) {
|
|
270
|
+
if (outputLength > 0)
|
|
271
|
+
return;
|
|
272
|
+
const anchor = $(element);
|
|
273
|
+
const hasAccessibleText = Boolean(anchor.attr('aria-label')?.trim()) ||
|
|
274
|
+
Boolean(anchor.attr('title')?.trim()) ||
|
|
275
|
+
anchor.find('[aria-label], [title], [alt]').length > 0;
|
|
276
|
+
const hasMeaningfulDescendant = element.children.some((child) => child.type === 'tag' || $(child).text().trim().length > 0);
|
|
277
|
+
if (!hasAccessibleText && !hasMeaningfulDescendant)
|
|
278
|
+
return;
|
|
279
|
+
rejectUnsupportedLink(context, href, 'unsupported inline semantics', 'extension');
|
|
280
|
+
}
|
|
281
|
+
function normalizeVisibleText(value) {
|
|
282
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
283
|
+
}
|
|
284
|
+
async function transformImage($, element, context) {
|
|
285
|
+
const source = $(element).attr('src') ?? '';
|
|
286
|
+
if (/^(?:https?:)?\/\//.test(source) || source.startsWith('data:')) {
|
|
287
|
+
return {
|
|
288
|
+
type: 'extension',
|
|
289
|
+
name: 'remoteImage',
|
|
290
|
+
data: { url: source, alt: $(element).attr('alt') },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const withoutQuery = source.split(/[?#]/)[0] ?? source;
|
|
294
|
+
const basePrefix = `/${context.baseUrl.replace(/^\/+|\/+$/g, '')}/`.replace('//', '/');
|
|
295
|
+
const relative = withoutQuery.startsWith(basePrefix)
|
|
296
|
+
? withoutQuery.slice(basePrefix.length)
|
|
297
|
+
: withoutQuery.replace(/^\/+/, '');
|
|
298
|
+
const absolute = path.resolve(context.outDir, relative);
|
|
299
|
+
const outputRelative = path.relative(path.resolve(context.outDir), absolute);
|
|
300
|
+
if (outputRelative.startsWith('..') || path.isAbsolute(outputRelative)) {
|
|
301
|
+
throw new Error(`rendered asset escapes the Docusaurus output directory: ${source}`);
|
|
302
|
+
}
|
|
303
|
+
const assetId = await context.assets.add(absolute);
|
|
304
|
+
return {
|
|
305
|
+
type: 'image',
|
|
306
|
+
assetId,
|
|
307
|
+
...($(element).attr('alt') ? { alt: $(element).attr('alt') } : {}),
|
|
308
|
+
...($(element).attr('title')
|
|
309
|
+
? { title: $(element).attr('title') }
|
|
310
|
+
: {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function classLanguage(className) {
|
|
314
|
+
return className?.match(/(?:^|\s)language-([\w-]+)/)?.[1];
|
|
315
|
+
}
|
|
316
|
+
function mergeText(inlines) {
|
|
317
|
+
const result = [];
|
|
318
|
+
for (const inline of inlines) {
|
|
319
|
+
const previous = result.at(-1);
|
|
320
|
+
if (inline.type === 'text' &&
|
|
321
|
+
previous?.type === 'text' &&
|
|
322
|
+
JSON.stringify(inline.marks ?? []) ===
|
|
323
|
+
JSON.stringify(previous.marks ?? []))
|
|
324
|
+
previous.value += inline.value;
|
|
325
|
+
else
|
|
326
|
+
result.push(inline);
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default } from './plugin.js';
|
|
2
|
+
export { contentHash, canonicalJson } from './canonical.js';
|
|
3
|
+
export { defineComponentHandler } from './types.js';
|
|
4
|
+
export type { Block, BundleAsset, BundleDocument, BundleSource, ComponentHandler, ComponentHandlerAssetInput, ComponentHandlerContext, ComponentHandlerRegistration, DocumentBundle, Inline, LinkInline, MdxAstNode, PluginOptions, TextInline, TextMark, } from './types.js';
|
package/dist/index.js
ADDED