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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AssetCollector } from './assets.js';
|
|
2
|
+
import type { Block, ComponentHandler, ComponentHandlerRegistration } from './types.js';
|
|
3
|
+
export declare function loadComponentHandlers(registrations: ComponentHandlerRegistration[]): Promise<Map<string, ComponentHandler>>;
|
|
4
|
+
export declare function markdownToBlocks(input: {
|
|
5
|
+
source: string;
|
|
6
|
+
siteDir: string;
|
|
7
|
+
sourcePath: string;
|
|
8
|
+
assets: AssetCollector;
|
|
9
|
+
handlers: Map<string, ComponentHandler>;
|
|
10
|
+
strict: boolean;
|
|
11
|
+
}): Promise<Block[]>;
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { toString } from 'mdast-util-to-string';
|
|
4
|
+
import remarkDirective from 'remark-directive';
|
|
5
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import remarkMdx from 'remark-mdx';
|
|
8
|
+
import remarkParse from 'remark-parse';
|
|
9
|
+
import { unified } from 'unified';
|
|
10
|
+
export async function loadComponentHandlers(registrations) {
|
|
11
|
+
const handlers = new Map();
|
|
12
|
+
for (const registration of registrations) {
|
|
13
|
+
const key = handlerKey(registration.importSource, registration.exportName);
|
|
14
|
+
if (handlers.has(key)) {
|
|
15
|
+
throw new Error(`duplicate component handler for ${key}`);
|
|
16
|
+
}
|
|
17
|
+
if (typeof registration.handler === 'string') {
|
|
18
|
+
const imported = (await import(pathToImportUrl(registration.handler)));
|
|
19
|
+
const handler = imported.default?.transform
|
|
20
|
+
? imported.default
|
|
21
|
+
: imported.transform
|
|
22
|
+
? { transform: imported.transform }
|
|
23
|
+
: undefined;
|
|
24
|
+
if (!handler) {
|
|
25
|
+
throw new Error(`component handler ${registration.handler} has no transform export`);
|
|
26
|
+
}
|
|
27
|
+
handlers.set(key, handler);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
handlers.set(key, registration.handler);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return handlers;
|
|
34
|
+
}
|
|
35
|
+
function pathToImportUrl(value) {
|
|
36
|
+
if (value.startsWith('file:'))
|
|
37
|
+
return value;
|
|
38
|
+
return new URL(`file://${path.resolve(value)}`).href;
|
|
39
|
+
}
|
|
40
|
+
export async function markdownToBlocks(input) {
|
|
41
|
+
const processor = unified()
|
|
42
|
+
.use(remarkParse)
|
|
43
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
44
|
+
.use(remarkGfm)
|
|
45
|
+
.use(remarkDirective)
|
|
46
|
+
.use(remarkMdx);
|
|
47
|
+
const root = processor.parse(input.source);
|
|
48
|
+
const context = {
|
|
49
|
+
...input,
|
|
50
|
+
imports: parseImports(input.source),
|
|
51
|
+
};
|
|
52
|
+
return transformBlockChildren(root.children, context);
|
|
53
|
+
}
|
|
54
|
+
async function transformBlockChildren(nodes, context) {
|
|
55
|
+
const blocks = [];
|
|
56
|
+
for (const node of nodes) {
|
|
57
|
+
blocks.push(...(await transformBlock(node, context)));
|
|
58
|
+
}
|
|
59
|
+
return blocks;
|
|
60
|
+
}
|
|
61
|
+
async function transformBlock(node, context) {
|
|
62
|
+
switch (node.type) {
|
|
63
|
+
case 'yaml':
|
|
64
|
+
case 'toml':
|
|
65
|
+
case 'mdxjsEsm':
|
|
66
|
+
return [];
|
|
67
|
+
case 'paragraph': {
|
|
68
|
+
const children = node.children ?? [];
|
|
69
|
+
if (children.length === 1 && children[0]?.type === 'image') {
|
|
70
|
+
return [await transformImage(children[0], context)];
|
|
71
|
+
}
|
|
72
|
+
return [{ type: 'paragraph', inlines: transformInlines(children) }];
|
|
73
|
+
}
|
|
74
|
+
case 'heading':
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
type: 'heading',
|
|
78
|
+
level: node.depth ?? 1,
|
|
79
|
+
inlines: transformInlines(node.children ?? []),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
case 'code': {
|
|
83
|
+
if (node.lang === 'mermaid')
|
|
84
|
+
return [{ type: 'mermaid', value: node.value ?? '' }];
|
|
85
|
+
const titleMatch = node.meta?.match(/(?:^|\s)title=(?:"([^"]+)"|'([^']+)'|(\S+))/);
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
type: 'code',
|
|
89
|
+
...(node.lang ? { language: node.lang } : {}),
|
|
90
|
+
...(titleMatch
|
|
91
|
+
? {
|
|
92
|
+
title: titleMatch[1] ??
|
|
93
|
+
titleMatch[2] ??
|
|
94
|
+
titleMatch[3],
|
|
95
|
+
}
|
|
96
|
+
: {}),
|
|
97
|
+
value: node.value ?? '',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
case 'list': {
|
|
102
|
+
const items = [];
|
|
103
|
+
for (const item of node.children ?? []) {
|
|
104
|
+
items.push(await transformBlockChildren(item.children ?? [], context));
|
|
105
|
+
}
|
|
106
|
+
return [{ type: 'list', ordered: node.ordered === true, items }];
|
|
107
|
+
}
|
|
108
|
+
case 'table': {
|
|
109
|
+
const rows = (node.children ?? []).map((row) => (row.children ?? []).map((cell) => transformInlines(cell.children ?? [])));
|
|
110
|
+
return [
|
|
111
|
+
{ type: 'table', header: rows[0] ?? [], rows: rows.slice(1) },
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
case 'blockquote':
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
type: 'admonition',
|
|
118
|
+
kind: 'quote',
|
|
119
|
+
blocks: await transformBlockChildren(node.children ?? [], context),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
case 'containerDirective':
|
|
123
|
+
case 'leafDirective': {
|
|
124
|
+
const title = directiveTitle(node);
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
type: 'admonition',
|
|
128
|
+
kind: node.name ?? 'note',
|
|
129
|
+
...(title ? { title } : {}),
|
|
130
|
+
blocks: await transformBlockChildren(node.children ?? [], context),
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
case 'thematicBreak':
|
|
135
|
+
return [{ type: 'thematicBreak' }];
|
|
136
|
+
case 'mdxJsxFlowElement':
|
|
137
|
+
return transformComponent(node, context);
|
|
138
|
+
case 'mdxFlowExpression':
|
|
139
|
+
if (!node.value?.trim() ||
|
|
140
|
+
/^\/\*[\s\S]*\*\/$/.test(node.value.trim()))
|
|
141
|
+
return [];
|
|
142
|
+
break;
|
|
143
|
+
case 'html':
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
type: 'extension',
|
|
147
|
+
name: 'html',
|
|
148
|
+
data: { value: node.value ?? '' },
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
default:
|
|
152
|
+
if (node.children)
|
|
153
|
+
return transformBlockChildren(node.children, context);
|
|
154
|
+
}
|
|
155
|
+
if (context.strict) {
|
|
156
|
+
throw new Error(`${context.sourcePath}: unsupported Markdown node ${node.type}`);
|
|
157
|
+
}
|
|
158
|
+
return [
|
|
159
|
+
{ type: 'extension', name: node.type, data: { value: toString(node) } },
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
async function transformComponent(node, context) {
|
|
163
|
+
const name = node.name;
|
|
164
|
+
if (!name)
|
|
165
|
+
throw new Error(`${context.sourcePath}: an MDX component has no name`);
|
|
166
|
+
if (/^[a-z]/.test(name)) {
|
|
167
|
+
return transformBlockChildren(node.children ?? [], context);
|
|
168
|
+
}
|
|
169
|
+
const imported = context.imports.get(name);
|
|
170
|
+
if (!imported) {
|
|
171
|
+
throw new Error(`${context.sourcePath}: component ${name} has no static import`);
|
|
172
|
+
}
|
|
173
|
+
const key = handlerKey(imported.importSource, imported.exportName);
|
|
174
|
+
const handler = context.handlers.get(key);
|
|
175
|
+
if (!handler) {
|
|
176
|
+
throw new Error(`${context.sourcePath}: unknown MDX component ${name} imported as ${imported.exportName} from ${imported.importSource}`);
|
|
177
|
+
}
|
|
178
|
+
const children = node.children ?? [];
|
|
179
|
+
const props = parseAttributes(node.attributes ?? []);
|
|
180
|
+
const handlerContext = {
|
|
181
|
+
node,
|
|
182
|
+
props,
|
|
183
|
+
children,
|
|
184
|
+
siteDir: context.siteDir,
|
|
185
|
+
sourcePath: context.sourcePath,
|
|
186
|
+
readSiteFile: async (requestedPath) => readFile(resolveSitePath(context.siteDir, requestedPath), 'utf8'),
|
|
187
|
+
addAsset: async (input) => context.assets.add(resolveSitePath(context.siteDir, input.path), input.mimeType),
|
|
188
|
+
transformChildren: async () => transformBlockChildren(children, context),
|
|
189
|
+
};
|
|
190
|
+
const result = await handler.transform(handlerContext);
|
|
191
|
+
return Array.isArray(result) ? result : [result];
|
|
192
|
+
}
|
|
193
|
+
function transformInlines(nodes, marks = []) {
|
|
194
|
+
const inlines = [];
|
|
195
|
+
for (const node of nodes) {
|
|
196
|
+
switch (node.type) {
|
|
197
|
+
case 'text':
|
|
198
|
+
inlines.push({
|
|
199
|
+
type: 'text',
|
|
200
|
+
value: node.value ?? '',
|
|
201
|
+
...(marks.length ? { marks: [...marks].sort() } : {}),
|
|
202
|
+
});
|
|
203
|
+
break;
|
|
204
|
+
case 'inlineCode':
|
|
205
|
+
inlines.push({
|
|
206
|
+
type: 'text',
|
|
207
|
+
value: node.value ?? '',
|
|
208
|
+
marks: [...marks, 'code'].sort(),
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
case 'strong':
|
|
212
|
+
inlines.push(...transformInlines(node.children ?? [], [...marks, 'bold']));
|
|
213
|
+
break;
|
|
214
|
+
case 'emphasis':
|
|
215
|
+
inlines.push(...transformInlines(node.children ?? [], [
|
|
216
|
+
...marks,
|
|
217
|
+
'italic',
|
|
218
|
+
]));
|
|
219
|
+
break;
|
|
220
|
+
case 'delete':
|
|
221
|
+
inlines.push(...transformInlines(node.children ?? [], [
|
|
222
|
+
...marks,
|
|
223
|
+
'strikethrough',
|
|
224
|
+
]));
|
|
225
|
+
break;
|
|
226
|
+
case 'link':
|
|
227
|
+
inlines.push({
|
|
228
|
+
type: 'link',
|
|
229
|
+
target: { kind: 'url', value: node.url ?? '' },
|
|
230
|
+
children: transformInlines(node.children ?? [], marks),
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
case 'break':
|
|
234
|
+
inlines.push({ type: 'text', value: '\n' });
|
|
235
|
+
break;
|
|
236
|
+
case 'mdxTextExpression':
|
|
237
|
+
if (node.value?.trim())
|
|
238
|
+
inlines.push({ type: 'text', value: `{${node.value}}` });
|
|
239
|
+
break;
|
|
240
|
+
default:
|
|
241
|
+
if (node.children)
|
|
242
|
+
inlines.push(...transformInlines(node.children, marks));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return mergeAdjacentText(inlines);
|
|
246
|
+
}
|
|
247
|
+
async function transformImage(node, context) {
|
|
248
|
+
const url = node.url ?? '';
|
|
249
|
+
if (/^(?:https?:)?\/\//.test(url) || url.startsWith('data:')) {
|
|
250
|
+
return {
|
|
251
|
+
type: 'extension',
|
|
252
|
+
name: 'remoteImage',
|
|
253
|
+
data: { url, alt: node.alt, title: node.title },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const sourceDirectory = path.dirname(path.resolve(context.siteDir, context.sourcePath));
|
|
257
|
+
const absolutePath = url.startsWith('/')
|
|
258
|
+
? path.join(context.siteDir, 'static', url.replace(/^\/+/, ''))
|
|
259
|
+
: path.resolve(sourceDirectory, url);
|
|
260
|
+
const assetId = await context.assets.add(absolutePath);
|
|
261
|
+
return {
|
|
262
|
+
type: 'image',
|
|
263
|
+
assetId,
|
|
264
|
+
...(node.alt ? { alt: node.alt } : {}),
|
|
265
|
+
...(node.title ? { title: node.title } : {}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function parseImports(source) {
|
|
269
|
+
const imports = new Map();
|
|
270
|
+
const expression = /import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"];?/g;
|
|
271
|
+
for (const match of source.matchAll(expression)) {
|
|
272
|
+
const clause = match[1]?.trim();
|
|
273
|
+
const importSource = match[2];
|
|
274
|
+
if (!clause || !importSource || clause.startsWith('type '))
|
|
275
|
+
continue;
|
|
276
|
+
const defaultMatch = clause.match(/^([A-Za-z_$][\w$]*)/);
|
|
277
|
+
if (defaultMatch?.[1])
|
|
278
|
+
imports.set(defaultMatch[1], {
|
|
279
|
+
importSource,
|
|
280
|
+
exportName: 'default',
|
|
281
|
+
});
|
|
282
|
+
const namedMatch = clause.match(/\{([\s\S]*?)\}/);
|
|
283
|
+
for (const entry of namedMatch?.[1]?.split(',') ?? []) {
|
|
284
|
+
const [exported, local] = entry.trim().split(/\s+as\s+/);
|
|
285
|
+
if (exported)
|
|
286
|
+
imports.set(local ?? exported, {
|
|
287
|
+
importSource,
|
|
288
|
+
exportName: exported,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return imports;
|
|
293
|
+
}
|
|
294
|
+
function parseAttributes(attributes) {
|
|
295
|
+
return Object.fromEntries((attributes ?? [])
|
|
296
|
+
.filter((attribute) => attribute.name)
|
|
297
|
+
.map((attribute) => [attribute.name, attribute.value ?? true]));
|
|
298
|
+
}
|
|
299
|
+
function directiveTitle(node) {
|
|
300
|
+
const first = node.children?.[0];
|
|
301
|
+
if (first?.type === 'paragraph') {
|
|
302
|
+
const value = toString(first).trim();
|
|
303
|
+
if (value.startsWith('[') && value.endsWith(']'))
|
|
304
|
+
return value.slice(1, -1);
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
function mergeAdjacentText(inlines) {
|
|
309
|
+
const result = [];
|
|
310
|
+
for (const inline of inlines) {
|
|
311
|
+
const previous = result.at(-1);
|
|
312
|
+
if (inline.type === 'text' &&
|
|
313
|
+
previous?.type === 'text' &&
|
|
314
|
+
JSON.stringify(inline.marks ?? []) ===
|
|
315
|
+
JSON.stringify(previous.marks ?? [])) {
|
|
316
|
+
previous.value += inline.value;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
result.push(inline);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
function handlerKey(importSource, exportName) {
|
|
325
|
+
return `${importSource}#${exportName}`;
|
|
326
|
+
}
|
|
327
|
+
function resolveSitePath(siteDir, requestedPath) {
|
|
328
|
+
const resolved = path.resolve(siteDir, requestedPath.replace(/^@site\//, ''));
|
|
329
|
+
const relative = path.relative(path.resolve(siteDir), resolved);
|
|
330
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
331
|
+
throw new Error(`path escapes the Docusaurus site: ${requestedPath}`);
|
|
332
|
+
}
|
|
333
|
+
return resolved;
|
|
334
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PluginOptions } from './types.js';
|
|
2
|
+
interface DocusaurusContext {
|
|
3
|
+
siteDir: string;
|
|
4
|
+
siteConfig: {
|
|
5
|
+
title?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
interface AllContentLoadedArgs {
|
|
11
|
+
allContent: unknown;
|
|
12
|
+
}
|
|
13
|
+
interface PostBuildArgs {
|
|
14
|
+
outDir: string;
|
|
15
|
+
routesPaths?: string[];
|
|
16
|
+
siteConfig: DocusaurusContext['siteConfig'];
|
|
17
|
+
}
|
|
18
|
+
export interface DocusaurusPluginInstance {
|
|
19
|
+
name: string;
|
|
20
|
+
allContentLoaded(args: AllContentLoadedArgs): void;
|
|
21
|
+
postBuild(args: PostBuildArgs): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export default function docusynxPlugin(context: DocusaurusContext, options?: PluginOptions): DocusaurusPluginInstance;
|
|
24
|
+
export {};
|