@clidey/dory 0.30.0 → 0.33.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.
Files changed (70) hide show
  1. package/bin/dist/dory.js +44 -20
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +54 -0
  4. package/package.json +9 -4
  5. package/src/app.css +5 -0
  6. package/src/app.tsx +13 -0
  7. package/src/components/dropdown.tsx +53 -0
  8. package/src/components/hooks.tsx +36 -0
  9. package/src/components/loading.tsx +21 -0
  10. package/src/components/mdx-provider.tsx +13 -0
  11. package/src/components/navigation.tsx +58 -0
  12. package/src/components/notification.tsx +13 -0
  13. package/src/components/routes.tsx +92 -0
  14. package/src/components/search-index.ts +43 -0
  15. package/src/components/search.tsx +448 -0
  16. package/src/components/store.ts +216 -0
  17. package/src/config/mdx.js +12 -0
  18. package/src/config/mdx.ts +12 -0
  19. package/src/embed/loader.ts +200 -0
  20. package/src/embed/types.ts +54 -0
  21. package/src/embed/widget-iframe.ts +302 -0
  22. package/src/main.tsx +22 -0
  23. package/src/mdx/accordion.tsx +41 -0
  24. package/src/mdx/api-playground.tsx +659 -0
  25. package/src/mdx/api.tsx +354 -0
  26. package/src/mdx/async-api.tsx +353 -0
  27. package/src/mdx/callout.tsx +64 -0
  28. package/src/mdx/card.tsx +54 -0
  29. package/src/mdx/code.tsx +289 -0
  30. package/src/mdx/expandable.tsx +36 -0
  31. package/src/mdx/fence.tsx +74 -0
  32. package/src/mdx/icon.tsx +73 -0
  33. package/src/mdx/latex.tsx +26 -0
  34. package/src/mdx/list.tsx +7 -0
  35. package/src/mdx/mdx.tsx +47 -0
  36. package/src/mdx/mermaid.tsx +147 -0
  37. package/src/mdx/open-api.tsx +311 -0
  38. package/src/mdx/processor.ts +174 -0
  39. package/src/mdx/source.tsx +58 -0
  40. package/src/mdx/step.tsx +51 -0
  41. package/src/mdx/table.tsx +20 -0
  42. package/src/mdx/tag.tsx +53 -0
  43. package/src/mdx/text.tsx +99 -0
  44. package/src/mdx/websocket-playground.tsx +824 -0
  45. package/src/plugins/docs-assets.ts +79 -0
  46. package/src/plugins/frontmatter-dev-server.ts +158 -0
  47. package/src/plugins/frontmatter-generator.ts +157 -0
  48. package/src/plugins/html-files-middleware.ts +108 -0
  49. package/src/plugins/html-metadata-injector.ts +76 -0
  50. package/src/plugins/llm-txt-dev-server.ts +183 -0
  51. package/src/plugins/llm-txt-generator.ts +186 -0
  52. package/src/plugins/sanitize.ts +163 -0
  53. package/src/plugins/unified-mdx-plugin.ts +35 -0
  54. package/src/sdk/index.ts +89 -0
  55. package/src/sdk/react.tsx +139 -0
  56. package/src/sdk/vue.ts +107 -0
  57. package/src/theme.scss +72 -0
  58. package/src/types/config.ts +89 -0
  59. package/src/ui/header.tsx +96 -0
  60. package/src/ui/layout.tsx +237 -0
  61. package/src/ui/mobile-navigation.tsx +72 -0
  62. package/src/ui/prev-next-link.tsx +74 -0
  63. package/src/ui/table-of-content.tsx +167 -0
  64. package/src/utils/colors.ts +54 -0
  65. package/src/utils/fonts.ts +23 -0
  66. package/src/utils/functions.ts +12 -0
  67. package/src/vite-env.d.ts +1 -0
  68. package/vite.config.embed-loader.ts +28 -0
  69. package/vite.config.embed-widget.ts +31 -0
  70. 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
+ }
@@ -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
+ }