@clidey/dory 0.30.0 → 0.31.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/bin/dist/dory.js +1 -8
- package/bin/dist/dory.js.map +1 -1
- package/index.html +54 -0
- package/package.json +12 -4
- package/src/app.css +5 -0
- package/src/app.tsx +13 -0
- package/src/components/dropdown.tsx +53 -0
- package/src/components/hooks.tsx +36 -0
- package/src/components/loading.tsx +21 -0
- package/src/components/mdx-provider.tsx +13 -0
- package/src/components/navigation.tsx +58 -0
- package/src/components/notification.tsx +13 -0
- package/src/components/routes.tsx +92 -0
- package/src/components/search-index.ts +43 -0
- package/src/components/search.tsx +448 -0
- package/src/components/store.ts +216 -0
- package/src/config/mdx.js +12 -0
- package/src/config/mdx.ts +12 -0
- package/src/embed/loader.ts +200 -0
- package/src/embed/types.ts +54 -0
- package/src/embed/widget-iframe.ts +302 -0
- package/src/main.tsx +22 -0
- package/src/mdx/accordion.tsx +41 -0
- package/src/mdx/api-playground.tsx +659 -0
- package/src/mdx/api.tsx +354 -0
- package/src/mdx/async-api.tsx +353 -0
- package/src/mdx/callout.tsx +64 -0
- package/src/mdx/card.tsx +54 -0
- package/src/mdx/code.tsx +289 -0
- package/src/mdx/expandable.tsx +36 -0
- package/src/mdx/fence.tsx +74 -0
- package/src/mdx/icon.tsx +73 -0
- package/src/mdx/latex.tsx +26 -0
- package/src/mdx/list.tsx +7 -0
- package/src/mdx/mdx.tsx +47 -0
- package/src/mdx/mermaid.tsx +147 -0
- package/src/mdx/open-api.tsx +311 -0
- package/src/mdx/processor.ts +174 -0
- package/src/mdx/source.tsx +58 -0
- package/src/mdx/step.tsx +51 -0
- package/src/mdx/table.tsx +20 -0
- package/src/mdx/tag.tsx +53 -0
- package/src/mdx/text.tsx +99 -0
- package/src/mdx/websocket-playground.tsx +824 -0
- package/src/plugins/docs-assets.ts +79 -0
- package/src/plugins/frontmatter-dev-server.ts +158 -0
- package/src/plugins/frontmatter-generator.ts +157 -0
- package/src/plugins/html-files-middleware.ts +108 -0
- package/src/plugins/html-metadata-injector.ts +76 -0
- package/src/plugins/llm-txt-dev-server.ts +183 -0
- package/src/plugins/llm-txt-generator.ts +186 -0
- package/src/plugins/sanitize.ts +163 -0
- package/src/plugins/unified-mdx-plugin.ts +35 -0
- package/src/sdk/index.ts +89 -0
- package/src/sdk/react.tsx +139 -0
- package/src/sdk/vue.ts +107 -0
- package/src/theme.scss +72 -0
- package/src/types/config.ts +89 -0
- package/src/ui/header.tsx +96 -0
- package/src/ui/layout.tsx +237 -0
- package/src/ui/mobile-navigation.tsx +72 -0
- package/src/ui/prev-next-link.tsx +74 -0
- package/src/ui/table-of-content.tsx +167 -0
- package/src/utils/colors.ts +54 -0
- package/src/utils/fonts.ts +23 -0
- package/src/utils/functions.ts +12 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.bin.json +25 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.embed-loader.ts +28 -0
- package/vite.config.embed-widget.ts +31 -0
- package/vite.config.ts +45 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
6
|
+
function getNavigationOrder(docsDir: string): string[] {
|
|
7
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
8
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
9
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
10
|
+
const order: string[] = [];
|
|
11
|
+
|
|
12
|
+
function walkPages(pages: any[]) {
|
|
13
|
+
for (const page of pages) {
|
|
14
|
+
if (typeof page === 'string') {
|
|
15
|
+
order.push(page);
|
|
16
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
17
|
+
walkPages(page.pages);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
23
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
24
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
25
|
+
for (const group of tab.groups) {
|
|
26
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
27
|
+
walkPages(group.pages);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return order;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Function to parse frontmatter from MDX content
|
|
37
|
+
function parseFrontmatter(content: string) {
|
|
38
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
39
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
40
|
+
|
|
41
|
+
const frontmatter = frontmatterMatch[1];
|
|
42
|
+
const data: Record<string, string> = {};
|
|
43
|
+
|
|
44
|
+
frontmatter.split('\n').forEach(line => {
|
|
45
|
+
const [key, ...valueParts] = line.split(':');
|
|
46
|
+
if (key && valueParts.length) {
|
|
47
|
+
const value = valueParts.join(':').trim();
|
|
48
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
56
|
+
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
57
|
+
const files: Record<string, string> = {};
|
|
58
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
+
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const fullPath = path.join(dir, item.name);
|
|
62
|
+
if (item.isDirectory()) {
|
|
63
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
64
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
65
|
+
// Key: relative path without .mdx, with forward slashes
|
|
66
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
67
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
68
|
+
files[rel] = fullPath;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Function to clean MDX content for LLM consumption
|
|
76
|
+
function cleanMdxContent(content: string): string {
|
|
77
|
+
// Remove JSX components but keep their content
|
|
78
|
+
let cleaned = content
|
|
79
|
+
// Remove JSX opening/closing tags but keep content
|
|
80
|
+
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
81
|
+
.replace(/<\/\w+>/g, '')
|
|
82
|
+
// Remove code block language specifiers
|
|
83
|
+
.replace(/```(\w+)/g, '```')
|
|
84
|
+
// Remove HTML comments
|
|
85
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
86
|
+
// Remove excessive whitespace
|
|
87
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
88
|
+
// Remove leading/trailing whitespace
|
|
89
|
+
.trim();
|
|
90
|
+
|
|
91
|
+
return cleaned;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Function to generate LLM text content in dory.json order, then append any extra files
|
|
95
|
+
function generateLlmContent(docsDir: string): string {
|
|
96
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
97
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
98
|
+
const usedKeys = new Set<string>();
|
|
99
|
+
let aggregatedContent = '';
|
|
100
|
+
|
|
101
|
+
// Output files in navigation order, in order as in dory.json
|
|
102
|
+
for (const navKey of navOrder) {
|
|
103
|
+
// Try both with and without "index"
|
|
104
|
+
let fileKey = navKey;
|
|
105
|
+
if (mdxFilesMap[fileKey]) {
|
|
106
|
+
usedKeys.add(fileKey);
|
|
107
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
108
|
+
fileKey = `${fileKey}/index`;
|
|
109
|
+
usedKeys.add(fileKey);
|
|
110
|
+
} else {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const filePath = mdxFilesMap[fileKey];
|
|
114
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
115
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
116
|
+
const cleanedContent = cleanMdxContent(content);
|
|
117
|
+
|
|
118
|
+
// Add file header
|
|
119
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
120
|
+
|
|
121
|
+
// Add title if available
|
|
122
|
+
if (data.title) {
|
|
123
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add description if available
|
|
127
|
+
if (data.description) {
|
|
128
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add cleaned content
|
|
132
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
133
|
+
aggregatedContent += '---\n\n';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Output any extra files not in navigation order, sorted
|
|
137
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
138
|
+
for (const fileKey of extraKeys) {
|
|
139
|
+
const filePath = mdxFilesMap[fileKey];
|
|
140
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
141
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
142
|
+
const cleanedContent = cleanMdxContent(content);
|
|
143
|
+
|
|
144
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
145
|
+
if (data.title) {
|
|
146
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
147
|
+
}
|
|
148
|
+
if (data.description) {
|
|
149
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
150
|
+
}
|
|
151
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
152
|
+
aggregatedContent += '---\n\n';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Remove leading newlines for the very first file, so the first file is at the very top
|
|
156
|
+
return aggregatedContent.replace(/^\n+/, '').trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function llmTxtDevServer(): Plugin {
|
|
160
|
+
return {
|
|
161
|
+
name: 'llm-txt-dev-server',
|
|
162
|
+
configureServer(server) {
|
|
163
|
+
server.middlewares.use('/llms.txt', (req, res, next) => {
|
|
164
|
+
if (req.method !== 'GET') {
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
170
|
+
const llmContent = generateLlmContent(docsDir);
|
|
171
|
+
|
|
172
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
173
|
+
res.setHeader('Content-Length', Buffer.byteLength(llmContent));
|
|
174
|
+
res.end(llmContent);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Error generating LLM text:', error);
|
|
177
|
+
res.statusCode = 500;
|
|
178
|
+
res.end('Internal Server Error');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
interface LlmTxtGeneratorOptions {
|
|
6
|
+
docsDir?: string;
|
|
7
|
+
outputPath?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Read dory.json to get navigation order (flat list, in order)
|
|
11
|
+
function getNavigationOrder(docsDir: string): string[] {
|
|
12
|
+
const doryJsonPath = path.join(docsDir, 'dory.json');
|
|
13
|
+
if (!fs.existsSync(doryJsonPath)) return [];
|
|
14
|
+
const doryJson = JSON.parse(fs.readFileSync(doryJsonPath, 'utf-8'));
|
|
15
|
+
const order: string[] = [];
|
|
16
|
+
|
|
17
|
+
function walkPages(pages: any[]) {
|
|
18
|
+
for (const page of pages) {
|
|
19
|
+
if (typeof page === 'string') {
|
|
20
|
+
order.push(page);
|
|
21
|
+
} else if (typeof page === 'object' && page.pages) {
|
|
22
|
+
walkPages(page.pages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (doryJson.navigation && Array.isArray(doryJson.navigation.tabs)) {
|
|
28
|
+
for (const tab of doryJson.navigation.tabs) {
|
|
29
|
+
if (tab.groups && Array.isArray(tab.groups)) {
|
|
30
|
+
for (const group of tab.groups) {
|
|
31
|
+
if (group.pages && Array.isArray(group.pages)) {
|
|
32
|
+
walkPages(group.pages);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return order;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function to parse frontmatter from MDX content
|
|
42
|
+
function parseFrontmatter(content: string) {
|
|
43
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
44
|
+
if (!frontmatterMatch) return { data: {}, content };
|
|
45
|
+
|
|
46
|
+
const frontmatter = frontmatterMatch[1];
|
|
47
|
+
const data: Record<string, string> = {};
|
|
48
|
+
|
|
49
|
+
frontmatter.split('\n').forEach(line => {
|
|
50
|
+
const [key, ...valueParts] = line.split(':');
|
|
51
|
+
if (key && valueParts.length) {
|
|
52
|
+
const value = valueParts.join(':').trim();
|
|
53
|
+
data[key.trim()] = value.replace(/^["']|["']$/g, '');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { data, content: content.slice(frontmatterMatch[0].length) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Function to recursively find all MDX files and map by relative path (without .mdx)
|
|
61
|
+
function findMdxFilesMap(dir: string, baseDir: string): Record<string, string> {
|
|
62
|
+
const files: Record<string, string> = {};
|
|
63
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
64
|
+
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
const fullPath = path.join(dir, item.name);
|
|
67
|
+
if (item.isDirectory()) {
|
|
68
|
+
Object.assign(files, findMdxFilesMap(fullPath, baseDir));
|
|
69
|
+
} else if (item.isFile() && item.name.endsWith('.mdx')) {
|
|
70
|
+
// Key: relative path without .mdx, with forward slashes
|
|
71
|
+
let rel = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
72
|
+
if (rel.endsWith('.mdx')) rel = rel.slice(0, -4);
|
|
73
|
+
files[rel] = fullPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Function to clean MDX content for LLM consumption
|
|
81
|
+
function cleanMdxContent(content: string): string {
|
|
82
|
+
// Remove JSX components but keep their content
|
|
83
|
+
let cleaned = content
|
|
84
|
+
// Remove JSX opening/closing tags but keep content
|
|
85
|
+
.replace(/<(\w+)([^>]*?)>/g, '')
|
|
86
|
+
.replace(/<\/\w+>/g, '')
|
|
87
|
+
// Remove code block language specifiers
|
|
88
|
+
.replace(/```(\w+)/g, '```')
|
|
89
|
+
// Remove HTML comments
|
|
90
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
91
|
+
// Remove excessive whitespace
|
|
92
|
+
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
93
|
+
// Remove leading/trailing whitespace
|
|
94
|
+
.trim();
|
|
95
|
+
|
|
96
|
+
return cleaned;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Function to generate LLM text content in dory.json order, then append any extra files
|
|
100
|
+
function generateLlmContent(docsDir: string): string {
|
|
101
|
+
const navOrder = getNavigationOrder(docsDir);
|
|
102
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
103
|
+
const usedKeys = new Set<string>();
|
|
104
|
+
let aggregatedContent = '';
|
|
105
|
+
|
|
106
|
+
// Output files in navigation order, in order as in dory.json
|
|
107
|
+
for (const navKey of navOrder) {
|
|
108
|
+
// Try both with and without "index"
|
|
109
|
+
let fileKey = navKey;
|
|
110
|
+
if (mdxFilesMap[fileKey]) {
|
|
111
|
+
usedKeys.add(fileKey);
|
|
112
|
+
} else if (mdxFilesMap[`${fileKey}/index`]) {
|
|
113
|
+
fileKey = `${fileKey}/index`;
|
|
114
|
+
usedKeys.add(fileKey);
|
|
115
|
+
} else {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const filePath = mdxFilesMap[fileKey];
|
|
119
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
120
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
121
|
+
const cleanedContent = cleanMdxContent(content);
|
|
122
|
+
|
|
123
|
+
// Add file header
|
|
124
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
125
|
+
|
|
126
|
+
// Add title if available
|
|
127
|
+
if (data.title) {
|
|
128
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Add description if available
|
|
132
|
+
if (data.description) {
|
|
133
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add cleaned content
|
|
137
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
138
|
+
aggregatedContent += '---\n\n';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Output any extra files not in navigation order, sorted
|
|
142
|
+
const extraKeys = Object.keys(mdxFilesMap).filter(k => !usedKeys.has(k)).sort();
|
|
143
|
+
for (const fileKey of extraKeys) {
|
|
144
|
+
const filePath = mdxFilesMap[fileKey];
|
|
145
|
+
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
146
|
+
const { data, content } = parseFrontmatter(rawContent);
|
|
147
|
+
const cleanedContent = cleanMdxContent(content);
|
|
148
|
+
|
|
149
|
+
aggregatedContent += `\n# ${fileKey}.mdx\n`;
|
|
150
|
+
if (data.title) {
|
|
151
|
+
aggregatedContent += `## ${data.title}\n\n`;
|
|
152
|
+
}
|
|
153
|
+
if (data.description) {
|
|
154
|
+
aggregatedContent += `${data.description}\n\n`;
|
|
155
|
+
}
|
|
156
|
+
aggregatedContent += cleanedContent + '\n\n';
|
|
157
|
+
aggregatedContent += '---\n\n';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Remove leading newlines for the very first file, so the first file is at the very top
|
|
161
|
+
return aggregatedContent.replace(/^\n+/, '').trim();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function llmTxtGenerator(options: LlmTxtGeneratorOptions = {}): Plugin {
|
|
165
|
+
return {
|
|
166
|
+
name: 'llm-txt-generator',
|
|
167
|
+
generateBundle() {
|
|
168
|
+
const docsDir = options.docsDir || path.join(process.cwd(), 'docs');
|
|
169
|
+
const aggregatedContent = generateLlmContent(docsDir);
|
|
170
|
+
|
|
171
|
+
this.emitFile({
|
|
172
|
+
type: 'asset',
|
|
173
|
+
fileName: 'llms.txt',
|
|
174
|
+
source: aggregatedContent
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// For logging, count the number of MDX files processed
|
|
178
|
+
const mdxFilesMap = findMdxFilesMap(docsDir, docsDir);
|
|
179
|
+
const mdxFilesCount = Object.keys(mdxFilesMap).length;
|
|
180
|
+
|
|
181
|
+
console.log(`✅ Generated LLM text file`);
|
|
182
|
+
console.log(`📄 Processed ${mdxFilesCount} MDX files`);
|
|
183
|
+
console.log(`📝 Total content size: ${aggregatedContent.length} characters`);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
// This plugin replaces mdxTextExpression nodes with inlineCode for safety.
|
|
4
|
+
export const remarkSafeVars = () => {
|
|
5
|
+
return (tree: any) => {
|
|
6
|
+
visit(tree, 'mdxTextExpression', (node: any, index: number | undefined, parent: any) => {
|
|
7
|
+
if (parent && typeof index === 'number') {
|
|
8
|
+
parent.children[index] = {
|
|
9
|
+
type: 'inlineCode',
|
|
10
|
+
value: node.value
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const KNOWN_COMPONENTS = [
|
|
18
|
+
// Text components
|
|
19
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'Row', 'Col', 'a', 'img',
|
|
20
|
+
// Code components
|
|
21
|
+
'code', 'CodeGroup', 'pre',
|
|
22
|
+
// UI components
|
|
23
|
+
'Accordion', 'AccordionGroup', 'Icon', 'Card', 'CardGroup',
|
|
24
|
+
'Callout', 'Note', 'Warning', 'Info', 'Tip', 'Check',
|
|
25
|
+
'Latex', 'API', 'Expandable', 'ResponseField', 'Properties', 'Property',
|
|
26
|
+
'Steps', 'Step', 'table', 'th', 'td', 'ul', 'ol', 'li',
|
|
27
|
+
'APIPlayground', 'WebSocketPlayground', 'AsyncAPI', 'Source',
|
|
28
|
+
// Common HTML elements that should be preserved
|
|
29
|
+
'div', 'span', 'br', 'hr', 'strong', 'em', 'b', 'i', 'u', 's',
|
|
30
|
+
'blockquote', 'nav', 'section', 'article', 'header', 'footer', 'main',
|
|
31
|
+
'aside', 'figure', 'figcaption', 'details', 'summary', 'mark', 'small',
|
|
32
|
+
'del', 'ins', 'sub', 'sup', 'kbd', 'samp', 'var', 'time', 'abbr',
|
|
33
|
+
'address', 'cite', 'q', 'dfn'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Helper: returns true if the given code string is inside a code block (```)
|
|
37
|
+
function isInsideCodeBlock(code: string, idx: number): boolean {
|
|
38
|
+
// Count the number of ``` before idx
|
|
39
|
+
const before = code.slice(0, idx);
|
|
40
|
+
const codeBlockMatches = before.match(/```/g);
|
|
41
|
+
// Odd number of ``` means inside a code block
|
|
42
|
+
return codeBlockMatches ? codeBlockMatches.length % 2 === 1 : false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Helper: checks if a known component tag has a matching closing tag
|
|
46
|
+
function hasMatchingClosingTag(code: string, tagName: string, openingTagOffset: number): boolean {
|
|
47
|
+
// Simple approach: look for the next occurrence of either an opening or closing tag of the same type
|
|
48
|
+
const afterOpening = code.slice(openingTagOffset + `<${tagName}>`.length);
|
|
49
|
+
|
|
50
|
+
// Find the first occurrence of either opening or closing tag
|
|
51
|
+
const nextOpeningMatch = afterOpening.match(new RegExp(`<${tagName}>`, 'i'));
|
|
52
|
+
const nextClosingMatch = afterOpening.match(new RegExp(`</${tagName}>`, 'i'));
|
|
53
|
+
|
|
54
|
+
// If no closing tag found at all
|
|
55
|
+
if (!nextClosingMatch) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const nextOpeningIndex = nextOpeningMatch ? nextOpeningMatch.index! : Infinity;
|
|
60
|
+
const nextClosingIndex = nextClosingMatch ? nextClosingMatch.index! : Infinity;
|
|
61
|
+
|
|
62
|
+
// If the closing tag comes before any other opening tag, this tag is properly closed
|
|
63
|
+
if (nextClosingIndex < nextOpeningIndex) {
|
|
64
|
+
// Check if the closing tag is inside a code block
|
|
65
|
+
const closingTagOffset = openingTagOffset + `<${tagName}>`.length + nextClosingIndex;
|
|
66
|
+
if (isInsideCodeBlock(code, closingTagOffset)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return false; // Another opening tag comes before the closing tag, so this one is not properly closed
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Allow any <[a-z][a-z0-9]*> tag inside code blocks to be left as-is (not parsed as JSX).
|
|
76
|
+
export function preprocessMdxTags() {
|
|
77
|
+
return {
|
|
78
|
+
name: 'preprocess-mdx-tags',
|
|
79
|
+
enforce: 'pre' as const,
|
|
80
|
+
transform(code: string, id: string) {
|
|
81
|
+
if (!id.endsWith('.mdx')) return;
|
|
82
|
+
|
|
83
|
+
// Remove leading whitespace from code fence blocks
|
|
84
|
+
let processed = code.replace(/^(\s+)(```\w*)/gm, '$2');
|
|
85
|
+
|
|
86
|
+
// Handle self-closing tags first (like <VideoPlayer />)
|
|
87
|
+
processed = processed.replace(/<([A-Za-z][A-Za-z0-9]*)[^>]*\/>/g, (match, tag, offset) => {
|
|
88
|
+
// If inside a code block, do not touch
|
|
89
|
+
if (isInsideCodeBlock(processed, offset)) {
|
|
90
|
+
return match;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If unknown component, treat as text
|
|
94
|
+
if (!KNOWN_COMPONENTS.includes(tag)) {
|
|
95
|
+
return `\`${match}\``;
|
|
96
|
+
}
|
|
97
|
+
return match;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Only replace unrecognized <someTag> with `"<someTag>"` if NOT inside a code block
|
|
101
|
+
// This regex finds all <tag> occurrences (fixed to handle uppercase)
|
|
102
|
+
processed = processed.replace(/<([A-Za-z][A-Za-z0-9]*)>/g, (match, tag, offset) => {
|
|
103
|
+
// If inside a code block, do not touch
|
|
104
|
+
if (isInsideCodeBlock(processed, offset)) {
|
|
105
|
+
return match;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If known component, check if it has a closing tag
|
|
109
|
+
if (KNOWN_COMPONENTS.includes(tag)) {
|
|
110
|
+
// If it doesn't have a matching closing tag, treat it as text
|
|
111
|
+
if (!hasMatchingClosingTag(processed, tag, offset)) {
|
|
112
|
+
return `\`<${tag}>\``;
|
|
113
|
+
}
|
|
114
|
+
// Otherwise, leave as-is for proper JSX parsing
|
|
115
|
+
return match;
|
|
116
|
+
}
|
|
117
|
+
// For unknown components, always treat as text (wrap in backticks)
|
|
118
|
+
// If already inside backticks, do not wrap again
|
|
119
|
+
const before = processed.slice(0, offset);
|
|
120
|
+
const backtickMatches = before.match(/`+/g);
|
|
121
|
+
const insideBackticks = backtickMatches ? backtickMatches.reduce((acc, s) => acc + s.length, 0) % 2 === 1 : false;
|
|
122
|
+
if (insideBackticks) {
|
|
123
|
+
return match;
|
|
124
|
+
}
|
|
125
|
+
return `\`<${tag}>\``;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// This regex finds all <tag> and </tag> occurrences (fixed to handle uppercase)
|
|
129
|
+
processed = processed.replace(/<\/?([A-Za-z][A-Za-z0-9]*)>/g, (match, tag, offset) => {
|
|
130
|
+
// If inside a code block, do not touch
|
|
131
|
+
if (isInsideCodeBlock(processed, offset)) {
|
|
132
|
+
return match;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If known component, check if it has a closing tag (only for opening tags)
|
|
136
|
+
if (KNOWN_COMPONENTS.includes(tag)) {
|
|
137
|
+
// Only check for matching closing tag for opening tags
|
|
138
|
+
if (!match.startsWith('</')) {
|
|
139
|
+
if (!hasMatchingClosingTag(processed, tag, offset)) {
|
|
140
|
+
return `\`${match}\``;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Otherwise, leave as-is for proper JSX parsing
|
|
144
|
+
return match;
|
|
145
|
+
}
|
|
146
|
+
// For unknown components, always treat as text (wrap in backticks)
|
|
147
|
+
// If already inside backticks, do not wrap again
|
|
148
|
+
const before = processed.slice(0, offset);
|
|
149
|
+
const backtickMatches = before.match(/`+/g);
|
|
150
|
+
const insideBackticks = backtickMatches ? backtickMatches.reduce((acc, s) => acc + s.length, 0) % 2 === 1 : false;
|
|
151
|
+
if (insideBackticks) {
|
|
152
|
+
return match;
|
|
153
|
+
}
|
|
154
|
+
return `\`${match}\``;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
code: processed,
|
|
159
|
+
map: null,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { preprocessMdxContent } from '../mdx/processor';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unified MDX preprocessing plugin for Vite.
|
|
6
|
+
* This plugin ensures that ALL MDX preprocessing goes through the same pipeline
|
|
7
|
+
* as the CLI verify:content command.
|
|
8
|
+
*
|
|
9
|
+
* This is the SINGLE SOURCE OF TRUTH for MDX preprocessing.
|
|
10
|
+
* Any changes to MDX processing should be made in src/mdx/processor.ts
|
|
11
|
+
*/
|
|
12
|
+
export function unifiedMdxPreprocessor(): Plugin {
|
|
13
|
+
return {
|
|
14
|
+
name: 'unified-mdx-preprocessor',
|
|
15
|
+
enforce: 'pre', // Must run before the MDX plugin
|
|
16
|
+
async transform(code: string, id: string) {
|
|
17
|
+
if (!id.endsWith('.mdx')) return;
|
|
18
|
+
|
|
19
|
+
// Process through the unified preprocessing pipeline
|
|
20
|
+
const result = await preprocessMdxContent(code, id);
|
|
21
|
+
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
// Throw the error to stop the build
|
|
24
|
+
throw result.error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Return the preprocessed content
|
|
28
|
+
// The actual MDX compilation will be handled by @mdx-js/rollup plugin
|
|
29
|
+
return {
|
|
30
|
+
code: result.processedContent || code,
|
|
31
|
+
map: null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dory Embedded Documentation SDK
|
|
3
|
+
* Export all framework integrations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export React SDK
|
|
7
|
+
export { useDoryDocs as useReactDoryDocs, DoryDocsTrigger, DoryDocsProvider } from './react';
|
|
8
|
+
|
|
9
|
+
// Re-export Vue SDK
|
|
10
|
+
export { useDoryDocs as useVueDoryDocs } from './vue';
|
|
11
|
+
|
|
12
|
+
// Export types
|
|
13
|
+
export type { DoryEmbedConfig } from '../embed/types';
|
|
14
|
+
|
|
15
|
+
// Export vanilla JS API reference
|
|
16
|
+
export interface DoryDocsAPI {
|
|
17
|
+
init(config: Partial<any>): DoryDocsAPI;
|
|
18
|
+
open(path?: string): Promise<void>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
toggle(): Promise<void>;
|
|
21
|
+
navigate(path: string): Promise<void>;
|
|
22
|
+
setTheme(theme: 'light' | 'dark'): Promise<void>;
|
|
23
|
+
isOpen(): boolean;
|
|
24
|
+
destroy(): Promise<void>;
|
|
25
|
+
on(event: string, handler: Function): DoryDocsAPI;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if DoryDocs is loaded
|
|
30
|
+
*/
|
|
31
|
+
export function isDoryDocsLoaded(): boolean {
|
|
32
|
+
return typeof window !== 'undefined' && !!window.DoryDocs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wait for DoryDocs to be loaded
|
|
37
|
+
*/
|
|
38
|
+
export function waitForDoryDocs(timeout = 5000): Promise<boolean> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
if (isDoryDocsLoaded()) {
|
|
41
|
+
resolve(true);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const timeoutId = setTimeout(() => {
|
|
46
|
+
window.removeEventListener('dory:ready', handleReady);
|
|
47
|
+
resolve(false);
|
|
48
|
+
}, timeout);
|
|
49
|
+
|
|
50
|
+
const handleReady = () => {
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
resolve(true);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
window.addEventListener('dory:ready', handleReady, { once: true });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load Dory embed script dynamically
|
|
61
|
+
*/
|
|
62
|
+
export function loadDoryScript(docsUrl: string): Promise<void> {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
if (isDoryDocsLoaded()) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const script = document.createElement('script');
|
|
70
|
+
script.src = `${docsUrl}/embed.js`;
|
|
71
|
+
script.async = true;
|
|
72
|
+
|
|
73
|
+
script.onload = () => {
|
|
74
|
+
waitForDoryDocs().then((loaded) => {
|
|
75
|
+
if (loaded) {
|
|
76
|
+
resolve();
|
|
77
|
+
} else {
|
|
78
|
+
reject(new Error('DoryDocs failed to initialize'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
script.onerror = () => {
|
|
84
|
+
reject(new Error('Failed to load DoryDocs script'));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
document.head.appendChild(script);
|
|
88
|
+
});
|
|
89
|
+
}
|