docusaurus-plugin-docusynx 0.1.1 → 0.1.3
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/dist/assets.d.ts +1 -0
- package/dist/assets.js +34 -7
- package/dist/diagram-worker.d.ts +1 -0
- package/dist/diagram-worker.js +152 -0
- package/dist/diagrams.d.ts +4 -0
- package/dist/diagrams.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/markdown.js +3 -1
- package/dist/svg.d.ts +1 -0
- package/dist/svg.js +223 -0
- package/dist/types.d.ts +6 -2
- package/package.json +11 -3
package/dist/assets.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export declare class AssetCollector {
|
|
|
3
3
|
#private;
|
|
4
4
|
constructor(outputDirectory: string);
|
|
5
5
|
add(sourcePath: string, mimeType?: string): Promise<string>;
|
|
6
|
+
addBytes(content: string | Uint8Array, extension: string, mimeType?: string): Promise<string>;
|
|
6
7
|
values(): BundleAsset[];
|
|
7
8
|
}
|
package/dist/assets.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { assertSafeSvg } from './svg.js';
|
|
4
5
|
const MIME_BY_EXTENSION = {
|
|
5
6
|
'.gif': 'image/gif',
|
|
6
7
|
'.jpeg': 'image/jpeg',
|
|
@@ -17,21 +18,47 @@ export class AssetCollector {
|
|
|
17
18
|
}
|
|
18
19
|
async add(sourcePath, mimeType) {
|
|
19
20
|
const bytes = await readFile(sourcePath);
|
|
21
|
+
return this.addBytes(bytes, path.extname(sourcePath).toLowerCase(), mimeType);
|
|
22
|
+
}
|
|
23
|
+
async addBytes(content, extension, mimeType) {
|
|
24
|
+
if (!/^\.[a-z0-9]+$/.test(extension)) {
|
|
25
|
+
throw new Error(`invalid asset extension ${JSON.stringify(extension)}`);
|
|
26
|
+
}
|
|
27
|
+
const bytes = typeof content === 'string' ? Buffer.from(content, 'utf8') : content;
|
|
20
28
|
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
21
|
-
const extension = path.extname(sourcePath).toLowerCase();
|
|
22
29
|
const id = `sha256:${digest}`;
|
|
23
30
|
const relativePath = `assets/${digest}${extension}`;
|
|
24
|
-
|
|
31
|
+
const resolvedMimeType = mimeType ??
|
|
32
|
+
MIME_BY_EXTENSION[extension] ??
|
|
33
|
+
'application/octet-stream';
|
|
34
|
+
if (extension === '.svg' ||
|
|
35
|
+
resolvedMimeType.split(';', 1)[0]?.trim().toLowerCase() ===
|
|
36
|
+
'image/svg+xml') {
|
|
37
|
+
let svg;
|
|
38
|
+
try {
|
|
39
|
+
svg = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error('unsafe SVG content: invalid UTF-8');
|
|
43
|
+
}
|
|
44
|
+
assertSafeSvg(svg);
|
|
45
|
+
}
|
|
46
|
+
const existing = this.#assets.get(id);
|
|
47
|
+
if (existing) {
|
|
48
|
+
if (existing.path !== relativePath ||
|
|
49
|
+
existing.mimeType !== resolvedMimeType) {
|
|
50
|
+
throw new Error(`asset metadata conflicts for ${id}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
25
54
|
await mkdir(path.join(this.#outputDirectory, 'assets'), {
|
|
26
55
|
recursive: true,
|
|
27
56
|
});
|
|
28
|
-
await
|
|
57
|
+
await writeFile(path.join(this.#outputDirectory, relativePath), bytes);
|
|
29
58
|
this.#assets.set(id, {
|
|
30
59
|
id,
|
|
31
60
|
path: relativePath,
|
|
32
|
-
mimeType:
|
|
33
|
-
MIME_BY_EXTENSION[extension] ??
|
|
34
|
-
'application/octet-stream',
|
|
61
|
+
mimeType: resolvedMimeType,
|
|
35
62
|
hash: id,
|
|
36
63
|
});
|
|
37
64
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
3
|
+
import { assertSafeSvg } from './svg.js';
|
|
4
|
+
const input = workerData;
|
|
5
|
+
async function renderMermaid(source) {
|
|
6
|
+
const [{ createHTMLWindow }, { default: createDOMPurify }, { JSDOM }] = await Promise.all([
|
|
7
|
+
import('svgdom'),
|
|
8
|
+
import('dompurify'),
|
|
9
|
+
import('jsdom'),
|
|
10
|
+
]);
|
|
11
|
+
const purificationWindow = new JSDOM('').window;
|
|
12
|
+
Object.assign(createDOMPurify, createDOMPurify(purificationWindow));
|
|
13
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', {
|
|
14
|
+
configurable: true,
|
|
15
|
+
value: purificationWindow.CSSStyleSheet,
|
|
16
|
+
});
|
|
17
|
+
const window = createHTMLWindow();
|
|
18
|
+
Object.assign(globalThis, { window, document: window.document });
|
|
19
|
+
const { default: mermaid } = await import('mermaid');
|
|
20
|
+
const digest = createHash('sha256').update(source).digest('hex');
|
|
21
|
+
mermaid.initialize({
|
|
22
|
+
startOnLoad: false,
|
|
23
|
+
securityLevel: 'strict',
|
|
24
|
+
htmlLabels: false,
|
|
25
|
+
flowchart: {
|
|
26
|
+
htmlLabels: false,
|
|
27
|
+
},
|
|
28
|
+
deterministicIds: true,
|
|
29
|
+
deterministicIDSeed: digest,
|
|
30
|
+
theme: 'neutral',
|
|
31
|
+
});
|
|
32
|
+
const { svg } = await mermaid.render(`docusynx-${digest.slice(0, 16)}`, source);
|
|
33
|
+
return normalizeSvg(svg);
|
|
34
|
+
}
|
|
35
|
+
async function renderExcalidraw(source) {
|
|
36
|
+
const { JSDOM } = await import('jsdom');
|
|
37
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
|
|
38
|
+
pretendToBeVisual: true,
|
|
39
|
+
url: 'http://localhost/',
|
|
40
|
+
});
|
|
41
|
+
installDomGlobals(dom.window);
|
|
42
|
+
const scene = parseExcalidraw(source);
|
|
43
|
+
validateExcalidrawFiles(scene.files ?? {});
|
|
44
|
+
const excalidraw = (await import('@excalidraw/utils'));
|
|
45
|
+
const svg = await excalidraw.exportToSvg({
|
|
46
|
+
data: {
|
|
47
|
+
elements: scene.elements,
|
|
48
|
+
appState: {
|
|
49
|
+
exportBackground: scene.appState?.exportBackground !== false,
|
|
50
|
+
exportPadding: scene.appState?.exportPadding,
|
|
51
|
+
exportScale: 1,
|
|
52
|
+
viewBackgroundColor: scene.appState?.viewBackgroundColor ?? '#ffffff',
|
|
53
|
+
exportWithDarkMode: false,
|
|
54
|
+
exportEmbedScene: false,
|
|
55
|
+
},
|
|
56
|
+
files: (scene.files ?? {}),
|
|
57
|
+
},
|
|
58
|
+
config: { skipInliningFonts: true },
|
|
59
|
+
});
|
|
60
|
+
return normalizeSvg(svg.outerHTML);
|
|
61
|
+
}
|
|
62
|
+
function parseExcalidraw(source) {
|
|
63
|
+
let value;
|
|
64
|
+
try {
|
|
65
|
+
value = JSON.parse(source);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`invalid Excalidraw JSON: ${errorMessage(error)}`);
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== 'object' ||
|
|
71
|
+
value === null ||
|
|
72
|
+
!('type' in value) ||
|
|
73
|
+
value.type !== 'excalidraw' ||
|
|
74
|
+
!('elements' in value) ||
|
|
75
|
+
!Array.isArray(value.elements)) {
|
|
76
|
+
throw new Error('invalid Excalidraw scene');
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function validateExcalidrawFiles(files) {
|
|
81
|
+
for (const [fileID, file] of Object.entries(files)) {
|
|
82
|
+
if (typeof file !== 'object' ||
|
|
83
|
+
file === null ||
|
|
84
|
+
!('dataURL' in file) ||
|
|
85
|
+
typeof file.dataURL !== 'string' ||
|
|
86
|
+
!isSafeRasterDataURL(file.dataURL)) {
|
|
87
|
+
throw new Error(`Excalidraw file ${JSON.stringify(fileID)} must use a base64 raster data URL`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function installDomGlobals(window) {
|
|
92
|
+
const values = window;
|
|
93
|
+
for (const name of [
|
|
94
|
+
'Blob',
|
|
95
|
+
'CSSStyleDeclaration',
|
|
96
|
+
'CSSStyleSheet',
|
|
97
|
+
'DOMParser',
|
|
98
|
+
'Element',
|
|
99
|
+
'FileReader',
|
|
100
|
+
'HTMLCanvasElement',
|
|
101
|
+
'HTMLElement',
|
|
102
|
+
'HTMLImageElement',
|
|
103
|
+
'Image',
|
|
104
|
+
'Node',
|
|
105
|
+
'SVGElement',
|
|
106
|
+
'XMLSerializer',
|
|
107
|
+
'document',
|
|
108
|
+
'getComputedStyle',
|
|
109
|
+
'navigator',
|
|
110
|
+
'window',
|
|
111
|
+
]) {
|
|
112
|
+
Object.defineProperty(globalThis, name, {
|
|
113
|
+
configurable: true,
|
|
114
|
+
value: values[name],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
Object.defineProperty(globalThis, 'devicePixelRatio', {
|
|
118
|
+
configurable: true,
|
|
119
|
+
value: 1,
|
|
120
|
+
});
|
|
121
|
+
Object.defineProperty(globalThis, 'requestAnimationFrame', {
|
|
122
|
+
configurable: true,
|
|
123
|
+
value: (callback) => setTimeout(() => callback(0), 0),
|
|
124
|
+
});
|
|
125
|
+
Object.defineProperty(globalThis, 'cancelAnimationFrame', {
|
|
126
|
+
configurable: true,
|
|
127
|
+
value: clearTimeout,
|
|
128
|
+
});
|
|
129
|
+
Object.defineProperty(globalThis, 'ResizeObserver', {
|
|
130
|
+
configurable: true,
|
|
131
|
+
value: class {
|
|
132
|
+
observe() { }
|
|
133
|
+
unobserve() { }
|
|
134
|
+
disconnect() { }
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function normalizeSvg(svg) {
|
|
139
|
+
const normalized = svg.replaceAll('\r\n', '\n').trim();
|
|
140
|
+
assertSafeSvg(normalized);
|
|
141
|
+
return normalized + '\n';
|
|
142
|
+
}
|
|
143
|
+
function isSafeRasterDataURL(value) {
|
|
144
|
+
return /^data:image\/(?:avif|gif|jpeg|png|webp);base64,[a-z0-9+/]+={0,2}$/i.test(value);
|
|
145
|
+
}
|
|
146
|
+
function errorMessage(error) {
|
|
147
|
+
return error instanceof Error ? error.message : String(error);
|
|
148
|
+
}
|
|
149
|
+
const svg = input.kind === 'mermaid'
|
|
150
|
+
? await renderMermaid(input.source)
|
|
151
|
+
: await renderExcalidraw(input.source);
|
|
152
|
+
parentPort?.postMessage({ svg });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type DiagramKind = 'excalidraw' | 'mermaid';
|
|
2
|
+
export declare function renderDiagramSvg(kind: DiagramKind, source: string): Promise<string>;
|
|
3
|
+
export declare function renderMermaidSvg(source: string): Promise<string>;
|
|
4
|
+
export declare function renderExcalidrawSvg(source: string): Promise<string>;
|
package/dist/diagrams.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
const RENDER_TIMEOUT_MS = 30_000;
|
|
5
|
+
export async function renderDiagramSvg(kind, source) {
|
|
6
|
+
if (!source.trim())
|
|
7
|
+
throw new Error(`${kind} source is empty`);
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const worker = new Worker(diagramWorkerUrl(), {
|
|
10
|
+
workerData: { kind, source },
|
|
11
|
+
});
|
|
12
|
+
let settled = false;
|
|
13
|
+
const timeout = setTimeout(() => {
|
|
14
|
+
settled = true;
|
|
15
|
+
void worker.terminate();
|
|
16
|
+
reject(new Error(`${kind} renderer timed out`));
|
|
17
|
+
}, RENDER_TIMEOUT_MS);
|
|
18
|
+
worker.once('message', (message) => {
|
|
19
|
+
clearTimeout(timeout);
|
|
20
|
+
settled = true;
|
|
21
|
+
if (typeof message !== 'object' ||
|
|
22
|
+
message === null ||
|
|
23
|
+
!('svg' in message) ||
|
|
24
|
+
typeof message.svg !== 'string') {
|
|
25
|
+
reject(new Error(`${kind} renderer returned an invalid response`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolve(message.svg);
|
|
29
|
+
});
|
|
30
|
+
worker.once('error', (error) => {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
settled = true;
|
|
33
|
+
reject(error);
|
|
34
|
+
});
|
|
35
|
+
worker.once('exit', (code) => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
if (!settled && code !== 0) {
|
|
38
|
+
reject(new Error(`${kind} renderer exited with code ${code}`));
|
|
39
|
+
}
|
|
40
|
+
else if (!settled) {
|
|
41
|
+
reject(new Error(`${kind} renderer exited without an SVG`));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function diagramWorkerUrl() {
|
|
47
|
+
const adjacent = new URL('./diagram-worker.js', import.meta.url);
|
|
48
|
+
if (existsSync(fileURLToPath(adjacent)))
|
|
49
|
+
return adjacent;
|
|
50
|
+
const built = new URL('../dist/diagram-worker.js', import.meta.url);
|
|
51
|
+
if (existsSync(fileURLToPath(built)))
|
|
52
|
+
return built;
|
|
53
|
+
return adjacent;
|
|
54
|
+
}
|
|
55
|
+
export async function renderMermaidSvg(source) {
|
|
56
|
+
return renderDiagramSvg('mermaid', source);
|
|
57
|
+
}
|
|
58
|
+
export async function renderExcalidrawSvg(source) {
|
|
59
|
+
return renderDiagramSvg('excalidraw', source);
|
|
60
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default } from './plugin.js';
|
|
2
2
|
export { contentHash, canonicalJson } from './canonical.js';
|
|
3
|
+
export { renderDiagramSvg, renderExcalidrawSvg, renderMermaidSvg, } from './diagrams.js';
|
|
3
4
|
export { defineComponentHandler } from './types.js';
|
|
5
|
+
export type { DiagramKind } from './diagrams.js';
|
|
4
6
|
export type { Block, BundleAsset, BundleDocument, BundleSource, ComponentHandler, ComponentHandlerAssetInput, ComponentHandlerContext, ComponentHandlerRegistration, DocumentBundle, Inline, LinkInline, MdxAstNode, PluginOptions, TextInline, TextMark, } from './types.js';
|
package/dist/index.js
CHANGED
package/dist/markdown.js
CHANGED
|
@@ -184,7 +184,9 @@ async function transformComponent(node, context) {
|
|
|
184
184
|
siteDir: context.siteDir,
|
|
185
185
|
sourcePath: context.sourcePath,
|
|
186
186
|
readSiteFile: async (requestedPath) => readFile(resolveSitePath(context.siteDir, requestedPath), 'utf8'),
|
|
187
|
-
addAsset: async (input) =>
|
|
187
|
+
addAsset: async (input) => 'path' in input
|
|
188
|
+
? context.assets.add(resolveSitePath(context.siteDir, input.path), input.mimeType)
|
|
189
|
+
: context.assets.addBytes(input.content, input.extension, input.mimeType),
|
|
188
190
|
transformChildren: async () => transformBlockChildren(children, context),
|
|
189
191
|
};
|
|
190
192
|
const result = await handler.transform(handlerContext);
|
package/dist/svg.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertSafeSvg(svg: string): void;
|
package/dist/svg.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
const cssTree = createRequire(import.meta.url)('css-tree');
|
|
4
|
+
const cssValueAttributes = new Set([
|
|
5
|
+
'clip-path',
|
|
6
|
+
'color',
|
|
7
|
+
'cursor',
|
|
8
|
+
'fill',
|
|
9
|
+
'filter',
|
|
10
|
+
'flood-color',
|
|
11
|
+
'lighting-color',
|
|
12
|
+
'marker-end',
|
|
13
|
+
'marker-mid',
|
|
14
|
+
'marker-start',
|
|
15
|
+
'mask',
|
|
16
|
+
'stop-color',
|
|
17
|
+
'stroke',
|
|
18
|
+
]);
|
|
19
|
+
const externalCssFunctions = new Set(['cross-fade', '-webkit-cross-fade', 'image', 'image-set', '-webkit-image-set', 'src']);
|
|
20
|
+
const unsafeSvgElements = new Set(['animate', 'animatecolor', 'animatemotion', 'animatetransform', 'foreignobject', 'script', 'set']);
|
|
21
|
+
const xmlDeclaration = /^\s*<\?xml\s+version\s*=\s*(?:"1\.0"|'1\.0')(?:\s+encoding\s*=\s*(?:"[Uu][Tt][Ff]-8"|'[Uu][Tt][Ff]-8'))?(?:\s+standalone\s*=\s*(?:"(?:yes|no)"|'(?:yes|no)'))?\s*\?>/;
|
|
22
|
+
export function assertSafeSvg(svg) {
|
|
23
|
+
if (/<!doctype\b/i.test(svg)) {
|
|
24
|
+
throw unsafe('document type declaration');
|
|
25
|
+
}
|
|
26
|
+
const declaration = svg.match(xmlDeclaration)?.[0] ?? '';
|
|
27
|
+
if (/<\?/.test(svg.slice(declaration.length))) {
|
|
28
|
+
throw unsafe('processing instruction');
|
|
29
|
+
}
|
|
30
|
+
let document;
|
|
31
|
+
try {
|
|
32
|
+
document = new JSDOM(svg, { contentType: 'image/svg+xml' }).window.document;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw unsafe(`invalid XML: ${errorMessage(error)}`);
|
|
36
|
+
}
|
|
37
|
+
if (document.doctype) {
|
|
38
|
+
throw unsafe('document type declaration');
|
|
39
|
+
}
|
|
40
|
+
for (const child of document.childNodes) {
|
|
41
|
+
if (child.nodeType === child.PROCESSING_INSTRUCTION_NODE) {
|
|
42
|
+
throw unsafe('processing instruction');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const root = document.documentElement;
|
|
46
|
+
if (root.localName.toLowerCase() !== 'svg' || (root.namespaceURI && root.namespaceURI !== 'http://www.w3.org/2000/svg')) {
|
|
47
|
+
throw unsafe('invalid root element');
|
|
48
|
+
}
|
|
49
|
+
for (const element of document.querySelectorAll('*')) {
|
|
50
|
+
const elementName = element.localName.toLowerCase();
|
|
51
|
+
if (unsafeSvgElements.has(elementName)) {
|
|
52
|
+
throw unsafe(elementName);
|
|
53
|
+
}
|
|
54
|
+
if (elementName === 'style') {
|
|
55
|
+
validateCss(element.textContent ?? '', 'stylesheet');
|
|
56
|
+
}
|
|
57
|
+
for (const attribute of element.attributes) {
|
|
58
|
+
const name = attribute.localName.toLowerCase();
|
|
59
|
+
if (name === 'base' && attribute.namespaceURI === 'http://www.w3.org/XML/1998/namespace') {
|
|
60
|
+
throw unsafe('xml:base attribute');
|
|
61
|
+
}
|
|
62
|
+
if (name.startsWith('on')) {
|
|
63
|
+
throw unsafe('event handler');
|
|
64
|
+
}
|
|
65
|
+
if (name === 'href' || name === 'src') {
|
|
66
|
+
validateReference(elementName, attribute.value);
|
|
67
|
+
}
|
|
68
|
+
else if (name === 'style') {
|
|
69
|
+
validateCss(attribute.value, 'declarationList');
|
|
70
|
+
}
|
|
71
|
+
else if (cssValueAttributes.has(name)) {
|
|
72
|
+
validateCss(attribute.value, 'value');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function validateReference(elementName, reference) {
|
|
78
|
+
const navigation = elementName === 'a' && /^(?:https?:|mailto:|#)/i.test(reference);
|
|
79
|
+
if (!navigation && !reference.startsWith('#') && !isSafeRasterDataUrl(reference)) {
|
|
80
|
+
throw unsafe('external reference');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function validateCss(source, context) {
|
|
84
|
+
const canonical = canonicalizeCss(source);
|
|
85
|
+
assertBalancedCss(canonical);
|
|
86
|
+
let ast;
|
|
87
|
+
try {
|
|
88
|
+
ast = cssTree.parse(canonical, {
|
|
89
|
+
context,
|
|
90
|
+
onParseError(error) {
|
|
91
|
+
throw error;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw unsafe(`invalid CSS: ${errorMessage(error)}`);
|
|
97
|
+
}
|
|
98
|
+
cssTree.walk(ast, (node) => {
|
|
99
|
+
const name = typeof node.name === 'string' ? node.name.toLowerCase() : undefined;
|
|
100
|
+
if (node.type === 'Atrule' && name === 'import') {
|
|
101
|
+
throw unsafe('CSS import');
|
|
102
|
+
}
|
|
103
|
+
if (node.type === 'Url') {
|
|
104
|
+
if (typeof node.value !== 'string' || !node.value.startsWith('#')) {
|
|
105
|
+
throw unsafe('CSS external reference');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (node.type === 'Function' && name && externalCssFunctions.has(name)) {
|
|
109
|
+
throw unsafe('CSS external resource function');
|
|
110
|
+
}
|
|
111
|
+
if (node.type === 'Raw' && /(?:@import\b|\burl\s*\()/i.test(node.value ?? '')) {
|
|
112
|
+
throw unsafe('CSS external reference');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function assertBalancedCss(source) {
|
|
117
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
118
|
+
const closing = new Set(Object.values(pairs));
|
|
119
|
+
const stack = [];
|
|
120
|
+
let quote = '';
|
|
121
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
122
|
+
const character = source[index];
|
|
123
|
+
if (quote) {
|
|
124
|
+
if (character === '\\') {
|
|
125
|
+
index += 1;
|
|
126
|
+
}
|
|
127
|
+
else if (character === quote) {
|
|
128
|
+
quote = '';
|
|
129
|
+
}
|
|
130
|
+
else if (/[\n\r\f]/.test(character)) {
|
|
131
|
+
throw unsafe('invalid CSS string');
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (character === '"' || character === "'") {
|
|
136
|
+
quote = character;
|
|
137
|
+
}
|
|
138
|
+
else if (pairs[character]) {
|
|
139
|
+
stack.push(character);
|
|
140
|
+
}
|
|
141
|
+
else if (closing.has(character)) {
|
|
142
|
+
const open = stack.pop();
|
|
143
|
+
if (!open || pairs[open] !== character) {
|
|
144
|
+
throw unsafe('unbalanced CSS delimiter');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (quote || stack.length > 0) {
|
|
149
|
+
throw unsafe('unbalanced CSS delimiter');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function canonicalizeCss(source) {
|
|
153
|
+
let output = '';
|
|
154
|
+
let quote = '';
|
|
155
|
+
for (let index = 0; index < source.length;) {
|
|
156
|
+
const character = source[index];
|
|
157
|
+
if (!quote && character === '/' && source[index + 1] === '*') {
|
|
158
|
+
const end = source.indexOf('*/', index + 2);
|
|
159
|
+
if (end < 0) {
|
|
160
|
+
throw unsafe('invalid CSS comment');
|
|
161
|
+
}
|
|
162
|
+
index = end + 2;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (character === '\\') {
|
|
166
|
+
const decoded = decodeCssEscape(source, index);
|
|
167
|
+
if (quote && (decoded.value === quote || decoded.value === '\\')) {
|
|
168
|
+
output += '\\';
|
|
169
|
+
}
|
|
170
|
+
output += decoded.value;
|
|
171
|
+
index = decoded.next;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
output += character;
|
|
175
|
+
index += 1;
|
|
176
|
+
if ((character === '"' || character === "'") && (!quote || quote === character)) {
|
|
177
|
+
quote = quote ? '' : character;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (quote) {
|
|
181
|
+
throw unsafe('invalid CSS string');
|
|
182
|
+
}
|
|
183
|
+
if (output.includes('/*') || output.includes('*/')) {
|
|
184
|
+
throw unsafe('invalid CSS comment');
|
|
185
|
+
}
|
|
186
|
+
return output;
|
|
187
|
+
}
|
|
188
|
+
function decodeCssEscape(source, start) {
|
|
189
|
+
let index = start + 1;
|
|
190
|
+
if (index >= source.length || /[\n\r\f]/.test(source[index])) {
|
|
191
|
+
throw unsafe('invalid CSS escape');
|
|
192
|
+
}
|
|
193
|
+
let hex = '';
|
|
194
|
+
while (index < source.length && hex.length < 6 && /[0-9a-f]/i.test(source[index])) {
|
|
195
|
+
hex += source[index];
|
|
196
|
+
index += 1;
|
|
197
|
+
}
|
|
198
|
+
if (!hex) {
|
|
199
|
+
const codePoint = source.codePointAt(index);
|
|
200
|
+
return { value: String.fromCodePoint(codePoint), next: index + (codePoint > 0xffff ? 2 : 1) };
|
|
201
|
+
}
|
|
202
|
+
if (index < source.length && /[\t\n\f\r ]/.test(source[index])) {
|
|
203
|
+
if (source[index] === '\r' && source[index + 1] === '\n')
|
|
204
|
+
index += 1;
|
|
205
|
+
index += 1;
|
|
206
|
+
}
|
|
207
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
208
|
+
return {
|
|
209
|
+
value: codePoint === 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
210
|
+
? '\ufffd'
|
|
211
|
+
: String.fromCodePoint(codePoint),
|
|
212
|
+
next: index,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function isSafeRasterDataUrl(value) {
|
|
216
|
+
return /^data:image\/(?:avif|gif|jpeg|png|webp);base64,[a-z0-9+/]+={0,2}$/i.test(value);
|
|
217
|
+
}
|
|
218
|
+
function unsafe(detail) {
|
|
219
|
+
return new Error(`unsafe SVG content: ${detail}`);
|
|
220
|
+
}
|
|
221
|
+
function errorMessage(error) {
|
|
222
|
+
return error instanceof Error ? error.message : String(error);
|
|
223
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -98,10 +98,14 @@ export interface DocumentBundle {
|
|
|
98
98
|
assets: BundleAsset[];
|
|
99
99
|
hash: string;
|
|
100
100
|
}
|
|
101
|
-
export
|
|
101
|
+
export type ComponentHandlerAssetInput = {
|
|
102
102
|
path: string;
|
|
103
103
|
mimeType?: string;
|
|
104
|
-
}
|
|
104
|
+
} | {
|
|
105
|
+
content: string | Uint8Array;
|
|
106
|
+
extension: '.svg';
|
|
107
|
+
mimeType: 'image/svg+xml';
|
|
108
|
+
};
|
|
105
109
|
export interface ComponentHandlerContext {
|
|
106
110
|
readonly node: MdxAstNode;
|
|
107
111
|
readonly props: Readonly<Record<string, unknown>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docusaurus-plugin-docusynx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Export a deterministic Docusaurus document bundle for docusynx",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"README.md"
|
|
36
36
|
],
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
38
|
+
"node": ">=22.13"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@docusaurus/core": "^3.10.0"
|
|
@@ -50,25 +50,33 @@
|
|
|
50
50
|
"registry": "https://registry.npmjs.org/"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@excalidraw/utils": "0.1.5",
|
|
53
54
|
"cheerio": "1.0.0",
|
|
55
|
+
"css-tree": "2.3.1",
|
|
54
56
|
"domhandler": "5.0.3",
|
|
57
|
+
"dompurify": "3.4.15",
|
|
55
58
|
"gray-matter": "4.0.3",
|
|
59
|
+
"jsdom": "26.1.0",
|
|
56
60
|
"mdast-util-to-string": "4.0.0",
|
|
61
|
+
"mermaid": "11.17.2",
|
|
57
62
|
"remark-directive": "4.0.0",
|
|
58
63
|
"remark-frontmatter": "5.0.0",
|
|
59
64
|
"remark-gfm": "4.0.1",
|
|
60
65
|
"remark-mdx": "3.1.1",
|
|
61
66
|
"remark-parse": "11.0.0",
|
|
67
|
+
"svgdom": "0.1.29",
|
|
62
68
|
"unified": "11.0.5"
|
|
63
69
|
},
|
|
64
70
|
"devDependencies": {
|
|
71
|
+
"@types/jsdom": "21.1.7",
|
|
65
72
|
"@types/node": "22.15.3",
|
|
73
|
+
"@types/svgdom": "0.1.2",
|
|
66
74
|
"typescript": "5.8.3",
|
|
67
75
|
"vitest": "3.1.2"
|
|
68
76
|
},
|
|
69
77
|
"scripts": {
|
|
70
78
|
"build": "tsc -p tsconfig.build.json",
|
|
71
|
-
"test": "vitest run",
|
|
79
|
+
"test": "pnpm build && vitest run",
|
|
72
80
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
73
81
|
}
|
|
74
82
|
}
|