@xyd-js/content 0.1.0-build.171
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/CHANGELOG.md +2174 -0
- package/ISSUES.md +1 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/TODO.md +2 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +1625 -0
- package/dist/index.js.map +1 -0
- package/dist/md.d.ts +72 -0
- package/dist/md.js +23508 -0
- package/dist/md.js.map +1 -0
- package/dist/mdToc-NBBxMJ4l.d.ts +12 -0
- package/dist/vite.d.ts +1066 -0
- package/dist/vite.js +20156 -0
- package/dist/vite.js.map +1 -0
- package/package.json +67 -0
- package/packages/md/index.ts +25 -0
- package/packages/md/plugins/component-directives/index.ts +3 -0
- package/packages/md/plugins/component-directives/mdComponentDirective.ts +577 -0
- package/packages/md/plugins/component-directives/types.ts +1 -0
- package/packages/md/plugins/component-directives/utils.ts +27 -0
- package/packages/md/plugins/composer/__fixtures__/1.single-example/input.md +7 -0
- package/packages/md/plugins/composer/__fixtures__/1.single-example/output.json +63 -0
- package/packages/md/plugins/composer/__fixtures__/2.single-example-with-name/input.md +7 -0
- package/packages/md/plugins/composer/__fixtures__/2.single-example-with-name/output.json +63 -0
- package/packages/md/plugins/composer/__fixtures__/3.multiple-examples/input.md +15 -0
- package/packages/md/plugins/composer/__fixtures__/3.multiple-examples/output.json +122 -0
- package/packages/md/plugins/composer/__fixtures__/4.example-groups/input.md +23 -0
- package/packages/md/plugins/composer/__fixtures__/4.example-groups/output.json +184 -0
- package/packages/md/plugins/composer/__tests__/mdComposer.test.ts +41 -0
- package/packages/md/plugins/composer/__tests__/testHelpers.ts +48 -0
- package/packages/md/plugins/composer/index.ts +1 -0
- package/packages/md/plugins/composer/mdComposer.ts +146 -0
- package/packages/md/plugins/developer-writing/index.ts +3 -0
- package/packages/md/plugins/developer-writing/mdCodeRehype.ts +81 -0
- package/packages/md/plugins/functions/__fixtures__/external.ts +4 -0
- package/packages/md/plugins/functions/__fixtures__/test-include.md +31 -0
- package/packages/md/plugins/functions/__fixtures__/test.js +11 -0
- package/packages/md/plugins/functions/__fixtures__/test.py +9 -0
- package/packages/md/plugins/functions/__fixtures__/test.ts +18 -0
- package/packages/md/plugins/functions/__tests__/mdFunctionImportCode.test.ts +314 -0
- package/packages/md/plugins/functions/__tests__/mdFunctionInclude.test.ts +44 -0
- package/packages/md/plugins/functions/__tests__/parseFunctionCall.test.ts +70 -0
- package/packages/md/plugins/functions/__tests__/testHelpers.ts +95 -0
- package/packages/md/plugins/functions/index.ts +15 -0
- package/packages/md/plugins/functions/mdFunctionChangelog.ts +135 -0
- package/packages/md/plugins/functions/mdFunctionImportCode.ts +92 -0
- package/packages/md/plugins/functions/mdFunctionInclude.ts +119 -0
- package/packages/md/plugins/functions/mdFunctionUniform.ts +79 -0
- package/packages/md/plugins/functions/types.ts +9 -0
- package/packages/md/plugins/functions/uniformProcessor.ts +349 -0
- package/packages/md/plugins/functions/utils.ts +457 -0
- package/packages/md/plugins/index.ts +125 -0
- package/packages/md/plugins/mdCode.ts +16 -0
- package/packages/md/plugins/mdHeadingId.ts +47 -0
- package/packages/md/plugins/mdImage.test.ts +59 -0
- package/packages/md/plugins/mdImage.ts +55 -0
- package/packages/md/plugins/mdImageRehype.ts +13 -0
- package/packages/md/plugins/mdPage.ts +35 -0
- package/packages/md/plugins/mdThemeSettings.ts +34 -0
- package/packages/md/plugins/mdToc.ts +229 -0
- package/packages/md/plugins/meta/index.ts +1 -0
- package/packages/md/plugins/meta/mdMeta.ts +198 -0
- package/packages/md/plugins/output-variables/__fixtures__/1.simple/input.md +22 -0
- package/packages/md/plugins/output-variables/__fixtures__/1.simple/output.json +191 -0
- package/packages/md/plugins/output-variables/__fixtures__/2.multiple-vars/input.md +21 -0
- package/packages/md/plugins/output-variables/__fixtures__/2.multiple-vars/output.json +127 -0
- package/packages/md/plugins/output-variables/__tests__/index.test.ts +28 -0
- package/packages/md/plugins/output-variables/__tests__/testHelpers.ts +36 -0
- package/packages/md/plugins/output-variables/index.ts +1 -0
- package/packages/md/plugins/output-variables/lib/const.ts +4 -0
- package/packages/md/plugins/output-variables/lib/factoryAttributes.ts +350 -0
- package/packages/md/plugins/output-variables/lib/factoryLabel.ts +135 -0
- package/packages/md/plugins/output-variables/lib/factoryName.ts +59 -0
- package/packages/md/plugins/output-variables/lib/index.ts +21 -0
- package/packages/md/plugins/output-variables/lib/outputVarsContainer.ts +328 -0
- package/packages/md/plugins/output-variables/lib/util.ts +494 -0
- package/packages/md/plugins/output-variables/remarkOutputVars.ts +22 -0
- package/packages/md/plugins/recmaOverrideComponents.ts +74 -0
- package/packages/md/plugins/rehypeHeading.ts +58 -0
- package/packages/md/plugins/types.ts +15 -0
- package/packages/md/plugins/utils/componentLike.ts +76 -0
- package/packages/md/plugins/utils/index.ts +2 -0
- package/packages/md/plugins/utils/injectCodeMeta.ts +59 -0
- package/packages/md/plugins/utils/mdParameters.test.ts +114 -0
- package/packages/md/plugins/utils/mdParameters.ts +249 -0
- package/packages/md/plugins/utils/mdastTypes.ts +42 -0
- package/packages/md/search/index.ts +257 -0
- package/packages/md/search/types.ts +36 -0
- package/packages/vite/index.ts +20 -0
- package/src/fs.ts +81 -0
- package/src/index.ts +7 -0
- package/src/navigation.ts +147 -0
- package/src/types.ts +8 -0
- package/tsconfig.json +49 -0
- package/tsup.config.ts +32 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {VFile} from 'vfile';
|
|
4
|
+
|
|
5
|
+
import {Settings} from '@xyd-js/core';
|
|
6
|
+
import {mdParameters} from "../utils/mdParameters";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Common options for all function plugins
|
|
10
|
+
*/
|
|
11
|
+
export interface FunctionOptions {
|
|
12
|
+
resolveFrom?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a function call with arguments
|
|
17
|
+
* @param node The AST node to parse
|
|
18
|
+
* @param functionName The name of the function to look for
|
|
19
|
+
*/
|
|
20
|
+
export function parseFunctionCall<args = any>(node: any, functionName: string): [string, args] | null {
|
|
21
|
+
function resp(args: any) {
|
|
22
|
+
const response: any[] = []
|
|
23
|
+
|
|
24
|
+
response.push(args[0])
|
|
25
|
+
let jsonArgs = undefined;
|
|
26
|
+
let argsString = args[1]
|
|
27
|
+
if (argsString) {
|
|
28
|
+
try {
|
|
29
|
+
argsString = argsString
|
|
30
|
+
.replace(/'/g, '"')
|
|
31
|
+
.replace(/(\w+):/g, '"$1":');
|
|
32
|
+
|
|
33
|
+
jsonArgs = JSON.parse(argsString);
|
|
34
|
+
response.push(jsonArgs);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return response as [string, any];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check for the simple case with a single text node
|
|
43
|
+
if (node.children && node.children.length === 1 && node.children[0].type === 'text') {
|
|
44
|
+
const textNode = node.children[0];
|
|
45
|
+
|
|
46
|
+
// Check for Markdown attribute syntax for the given function name
|
|
47
|
+
const mdAttrMatch = textNode.value.match(new RegExp(`^@?${functionName}\\[(.*)\\]\\s+(.*)$`));
|
|
48
|
+
if (mdAttrMatch) {
|
|
49
|
+
const attrsString = mdAttrMatch[1];
|
|
50
|
+
let pathString = mdAttrMatch[2].trim();
|
|
51
|
+
if ((pathString.startsWith('"') && pathString.endsWith('"')) || (pathString.startsWith("'") && pathString.endsWith("'"))) {
|
|
52
|
+
pathString = pathString.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const {attributes} = mdParameters(`[${attrsString}]`)
|
|
56
|
+
return [pathString, { __mdAttrs: attributes } as any];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Accept @ prefix for function name
|
|
60
|
+
const fnRegex = new RegExp(`^@?${functionName}`);
|
|
61
|
+
if (!fnRegex.test(textNode.value)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for parentheses syntax with multiple arguments
|
|
66
|
+
const parenthesesMatch = textNode.value.match(new RegExp(`^@?${functionName}\\((.*)\\)$`));
|
|
67
|
+
if (parenthesesMatch) {
|
|
68
|
+
const argsText = parenthesesMatch[1];
|
|
69
|
+
const args = argsText.split(',').map((arg: string) => arg.trim().replace(/^['\"]|['\"]$/g, ''));
|
|
70
|
+
return resp(args)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for the original syntax
|
|
74
|
+
const originalMatch = textNode.value.match(new RegExp(`^@?${functionName}\\s+['\"](.*)['\"]$`));
|
|
75
|
+
if (originalMatch) {
|
|
76
|
+
return resp([originalMatch[1], originalMatch[2]]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for the complex case with multiple nodes
|
|
81
|
+
if (!node || !node.children || node.children.length < 3) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if the first node contains the function part
|
|
86
|
+
const firstNode = node.children[0];
|
|
87
|
+
const middleNode = node.children[1];
|
|
88
|
+
const lastNode = node.children[2];
|
|
89
|
+
|
|
90
|
+
if (firstNode.type === 'text' &&
|
|
91
|
+
firstNode.value.startsWith(`${functionName} "`) &&
|
|
92
|
+
middleNode.type === 'link' &&
|
|
93
|
+
lastNode.type === 'text' &&
|
|
94
|
+
lastNode.value === '"') {
|
|
95
|
+
|
|
96
|
+
// We found a split command, extract the URL from the link node
|
|
97
|
+
const url = middleNode.url;
|
|
98
|
+
|
|
99
|
+
return resp([url]) // Create a match array with the URL in position 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for parentheses syntax with multiple arguments
|
|
103
|
+
if (firstNode.type === 'text' &&
|
|
104
|
+
firstNode.value.startsWith(`${functionName}(`) &&
|
|
105
|
+
lastNode.type === 'text' &&
|
|
106
|
+
lastNode.value === ')') {
|
|
107
|
+
|
|
108
|
+
// Extract the arguments from the middle node
|
|
109
|
+
if (middleNode.type === 'text') {
|
|
110
|
+
// Simple case: all arguments in a single text node
|
|
111
|
+
const argsText = middleNode.value;
|
|
112
|
+
// Split by comma and trim each argument
|
|
113
|
+
const args = argsText.split(',').map((arg: string) => arg.trim().replace(/^["']|["']$/g, ''));
|
|
114
|
+
// Return the first argument as the path
|
|
115
|
+
return resp(args);
|
|
116
|
+
} else if (middleNode.type === 'link') {
|
|
117
|
+
// Case with a link node (for URLs)
|
|
118
|
+
const url = middleNode.url;
|
|
119
|
+
return resp([url]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse an import path to extract file path, regions, and line ranges
|
|
128
|
+
*/
|
|
129
|
+
export function parseImportPath(importPath: string): {
|
|
130
|
+
filePath: string;
|
|
131
|
+
regions: Region[];
|
|
132
|
+
lineRanges: LineRange[]
|
|
133
|
+
} {
|
|
134
|
+
// Initialize result
|
|
135
|
+
const result = {
|
|
136
|
+
filePath: importPath,
|
|
137
|
+
regions: [] as Region[],
|
|
138
|
+
lineRanges: [] as LineRange[]
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// First, handle line ranges in the main path (not in regions)
|
|
142
|
+
// Only match line ranges that contain numbers
|
|
143
|
+
const mainLineRangeMatch = result.filePath.match(/\{([0-9,\s:-]+)\}/);
|
|
144
|
+
if (mainLineRangeMatch) {
|
|
145
|
+
const lineRangeStr = mainLineRangeMatch[1];
|
|
146
|
+
result.filePath = result.filePath.replace(/\{[0-9,\s:-]+\}/, '');
|
|
147
|
+
|
|
148
|
+
// Parse line ranges like "1,2-4, 8:, :10"
|
|
149
|
+
const rangeParts = lineRangeStr.split(',').map(part => part.trim());
|
|
150
|
+
|
|
151
|
+
for (const part of rangeParts) {
|
|
152
|
+
if (part.includes('-')) {
|
|
153
|
+
// Range like "2-4"
|
|
154
|
+
const [start, end] = part.split('-').map(num => parseInt(num, 10));
|
|
155
|
+
result.lineRanges.push({start, end});
|
|
156
|
+
} else if (part.endsWith(':')) {
|
|
157
|
+
// Range like "8:"
|
|
158
|
+
const start = parseInt(part.replace(':', ''), 10);
|
|
159
|
+
result.lineRanges.push({start});
|
|
160
|
+
} else if (part.startsWith(':')) {
|
|
161
|
+
// Range like ":10"
|
|
162
|
+
const end = parseInt(part.replace(':', ''), 10);
|
|
163
|
+
result.lineRanges.push({end});
|
|
164
|
+
} else {
|
|
165
|
+
// Single line like "1"
|
|
166
|
+
const line = parseInt(part, 10);
|
|
167
|
+
result.lineRanges.push({start: line, end: line});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Then, handle regions
|
|
173
|
+
const hashIndex = result.filePath.indexOf('#');
|
|
174
|
+
if (hashIndex !== -1) {
|
|
175
|
+
// Split the path at the hash
|
|
176
|
+
const basePath = result.filePath.substring(0, hashIndex);
|
|
177
|
+
const regionPart = result.filePath.substring(hashIndex + 1);
|
|
178
|
+
|
|
179
|
+
// Update the file path
|
|
180
|
+
result.filePath = basePath;
|
|
181
|
+
|
|
182
|
+
// Check if the region part contains a numeric line range
|
|
183
|
+
const regionLineRangeMatch = regionPart.match(/\{([0-9,\s:-]+)\}/);
|
|
184
|
+
|
|
185
|
+
if (regionLineRangeMatch) {
|
|
186
|
+
// If there are numeric line ranges in the region part, extract them
|
|
187
|
+
const regionLineRangeStr = regionLineRangeMatch[1];
|
|
188
|
+
const regionName = regionPart.replace(/\{[0-9,\s:-]+\}/, '').trim();
|
|
189
|
+
|
|
190
|
+
// Parse the line ranges
|
|
191
|
+
const rangeParts = regionLineRangeStr.split(',').map(part => part.trim());
|
|
192
|
+
const regionLineRanges: LineRange[] = [];
|
|
193
|
+
|
|
194
|
+
for (const part of rangeParts) {
|
|
195
|
+
if (part.includes('-')) {
|
|
196
|
+
// Range like "2-4"
|
|
197
|
+
const [start, end] = part.split('-').map(num => parseInt(num, 10));
|
|
198
|
+
regionLineRanges.push({start, end});
|
|
199
|
+
} else if (part.endsWith(':')) {
|
|
200
|
+
// Range like "8:"
|
|
201
|
+
const start = parseInt(part.replace(':', ''), 10);
|
|
202
|
+
regionLineRanges.push({start});
|
|
203
|
+
} else if (part.startsWith(':')) {
|
|
204
|
+
// Range like ":10"
|
|
205
|
+
const end = parseInt(part.replace(':', ''), 10);
|
|
206
|
+
regionLineRanges.push({end});
|
|
207
|
+
} else {
|
|
208
|
+
// Single line like "1"
|
|
209
|
+
const line = parseInt(part, 10);
|
|
210
|
+
regionLineRanges.push({start: line, end: line});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add the region with its line ranges
|
|
215
|
+
result.regions.push({name: regionName, lineRanges: regionLineRanges});
|
|
216
|
+
} else {
|
|
217
|
+
// If there are no numeric line ranges in the region part, check if it contains commas
|
|
218
|
+
if (regionPart.includes(',')) {
|
|
219
|
+
// Split by comma for multiple regions
|
|
220
|
+
const regionNames = regionPart.split(',');
|
|
221
|
+
for (const name of regionNames) {
|
|
222
|
+
result.regions.push({name: name.trim()});
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Single region
|
|
226
|
+
result.regions.push({name: regionPart.trim()});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Process content based on regions and line ranges
|
|
236
|
+
*/
|
|
237
|
+
export function processContent(content: string, regions: Region[], lineRanges: LineRange[]): string {
|
|
238
|
+
const lines = content.split('\n');
|
|
239
|
+
|
|
240
|
+
// If no regions or line ranges specified, return the original content
|
|
241
|
+
if (regions.length === 0 && lineRanges.length === 0) {
|
|
242
|
+
return content;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Process regions if present
|
|
246
|
+
if (regions.length > 0) {
|
|
247
|
+
const regionLines: string[] = [];
|
|
248
|
+
|
|
249
|
+
for (const region of regions) {
|
|
250
|
+
const regionStart = lines.findIndex(line => line.includes(`#region ${region.name}`));
|
|
251
|
+
const regionEnd = lines.findIndex(line => line.includes(`#endregion ${region.name}`));
|
|
252
|
+
|
|
253
|
+
if (regionStart !== -1 && regionEnd !== -1) {
|
|
254
|
+
// Only include the content between region markers, not the markers themselves
|
|
255
|
+
for (let i = regionStart + 1; i < regionEnd; i++) {
|
|
256
|
+
regionLines.push(lines[i]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If we found regions, return only the region content
|
|
262
|
+
if (regionLines.length > 0) {
|
|
263
|
+
return regionLines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Process line ranges if present
|
|
268
|
+
if (lineRanges.length > 0) {
|
|
269
|
+
// Create a set of line numbers to include
|
|
270
|
+
const lineSet = new Set<number>();
|
|
271
|
+
|
|
272
|
+
for (const range of lineRanges) {
|
|
273
|
+
const start = range.start || 1;
|
|
274
|
+
const end = range.end || lines.length;
|
|
275
|
+
|
|
276
|
+
// Adjust for 0-based indexing
|
|
277
|
+
const startIndex = Math.max(0, start - 1);
|
|
278
|
+
const endIndex = Math.min(lines.length, end);
|
|
279
|
+
|
|
280
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
281
|
+
lineSet.add(i);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Filter lines based on the set
|
|
286
|
+
const selectedLines = lines.filter((_, index) => lineSet.has(index));
|
|
287
|
+
return selectedLines.join('\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return content;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Map common extensions to languages
|
|
294
|
+
const languageMap: Record<string, string> = {
|
|
295
|
+
'js': 'javascript',
|
|
296
|
+
'jsx': 'jsx',
|
|
297
|
+
'ts': 'typescript',
|
|
298
|
+
'tsx': 'tsx',
|
|
299
|
+
'py': 'python',
|
|
300
|
+
'rb': 'ruby',
|
|
301
|
+
'java': 'java',
|
|
302
|
+
'c': 'c',
|
|
303
|
+
'cpp': 'cpp',
|
|
304
|
+
'cs': 'csharp',
|
|
305
|
+
'go': 'go',
|
|
306
|
+
'rs': 'rust',
|
|
307
|
+
'php': 'php',
|
|
308
|
+
'swift': 'swift',
|
|
309
|
+
'kt': 'kotlin',
|
|
310
|
+
'scala': 'scala',
|
|
311
|
+
'html': 'html',
|
|
312
|
+
'css': 'css',
|
|
313
|
+
'scss': 'scss',
|
|
314
|
+
'less': 'less',
|
|
315
|
+
'json': 'json',
|
|
316
|
+
'xml': 'xml',
|
|
317
|
+
'yaml': 'yaml',
|
|
318
|
+
'yml': 'yaml',
|
|
319
|
+
'md': 'markdown',
|
|
320
|
+
'mdx': 'mdx',
|
|
321
|
+
'sh': 'bash',
|
|
322
|
+
'bash': 'bash',
|
|
323
|
+
'sql': 'sql',
|
|
324
|
+
'graphql': 'graphql',
|
|
325
|
+
'vue': 'vue',
|
|
326
|
+
'svelte': 'svelte',
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Detect language from file extension
|
|
331
|
+
*/
|
|
332
|
+
export function detectLanguage(filePath: string): string {
|
|
333
|
+
const extension = path.extname(filePath).toLowerCase().replace('.', '');
|
|
334
|
+
|
|
335
|
+
return languageMap[extension] || path.extname(filePath).split('.').pop() || '';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Read a local file
|
|
340
|
+
*/
|
|
341
|
+
export function readLocalFile(filePath: string, baseDir: string): string {
|
|
342
|
+
filePath = parseIfLocalHomePath(filePath);
|
|
343
|
+
|
|
344
|
+
const fullPath = path.resolve(baseDir, filePath);
|
|
345
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseIfLocalHomePath(filePath: string): string {
|
|
349
|
+
// Handle "~/" prefix by replacing it with the current working directory
|
|
350
|
+
|
|
351
|
+
if (filePath.startsWith('~/')) {
|
|
352
|
+
filePath = filePath.replace('~/', process.cwd() + '/');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return filePath;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Fetch file content from a URL
|
|
360
|
+
*/
|
|
361
|
+
export async function fetchFileContent(url: string): Promise<string> {
|
|
362
|
+
const response = await fetch(url);
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
365
|
+
}
|
|
366
|
+
return response.text();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Interface for line range
|
|
371
|
+
*/
|
|
372
|
+
export interface LineRange {
|
|
373
|
+
start?: number;
|
|
374
|
+
end?: number;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Interface for region
|
|
379
|
+
*/
|
|
380
|
+
export interface Region {
|
|
381
|
+
name: string;
|
|
382
|
+
lineRanges?: LineRange[];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
export async function downloadContent(
|
|
387
|
+
filePath: string,
|
|
388
|
+
file: VFile,
|
|
389
|
+
resolveFrom?: string,
|
|
390
|
+
) {
|
|
391
|
+
const isExternal = filePath.startsWith('http://') || filePath.startsWith('https://');
|
|
392
|
+
|
|
393
|
+
let content: string;
|
|
394
|
+
|
|
395
|
+
if (isExternal) {
|
|
396
|
+
// Fetch external content
|
|
397
|
+
content = await fetchFileContent(filePath);
|
|
398
|
+
} else {
|
|
399
|
+
const baseDir = resolveFrom || (file.dirname || process.cwd());
|
|
400
|
+
content = readLocalFile(filePath, baseDir);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return content;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function functionMatch(value: string, functionName: string): boolean {
|
|
407
|
+
return value.startsWith(functionName); // TODO: better function matching like args etc
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Resolves a path alias if the given path is an alias defined in the settings.
|
|
412
|
+
* @param inputPath The path to resolve
|
|
413
|
+
* @param settings The settings object containing path aliases
|
|
414
|
+
* @param baseDir Optional base directory to resolve the final path from
|
|
415
|
+
* @returns The resolved path or the original path if no alias is found
|
|
416
|
+
*/
|
|
417
|
+
export function resolvePathAlias(inputPath: string, settings?: Settings, currentFile?: VFile, cwd: string = process.cwd()): string {
|
|
418
|
+
const baseDir = path.join(cwd, currentFile?.dirname || "");
|
|
419
|
+
|
|
420
|
+
if (!settings?.engine?.paths) {
|
|
421
|
+
return parseIfLocalHomePath(inputPath)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Find the longest matching alias
|
|
425
|
+
let resolvedPath = inputPath;
|
|
426
|
+
let longestMatch = '';
|
|
427
|
+
|
|
428
|
+
for (const [alias, aliasPaths] of Object.entries(settings.engine.paths)) {
|
|
429
|
+
// Convert alias pattern to regex, replacing * with .*
|
|
430
|
+
const aliasPattern = alias.replace(/\*/g, '.*');
|
|
431
|
+
const aliasRegex = new RegExp(`^${aliasPattern}`);
|
|
432
|
+
|
|
433
|
+
if (aliasRegex.test(inputPath) && alias.length > longestMatch.length) {
|
|
434
|
+
longestMatch = alias;
|
|
435
|
+
// Replace the alias with the path (handle both string and array)
|
|
436
|
+
const aliasPath = Array.isArray(aliasPaths) ? aliasPaths[0] : aliasPaths;
|
|
437
|
+
// Extract the part after the alias pattern
|
|
438
|
+
const aliasPrefix = alias.replace('*', '');
|
|
439
|
+
const matchedPart = inputPath.substring(aliasPrefix.length);
|
|
440
|
+
// Remove leading slash if present
|
|
441
|
+
const cleanMatchedPart = matchedPart.startsWith('/') ? matchedPart.slice(1) : matchedPart;
|
|
442
|
+
// If aliasPath contains *, replace it with the matched part, otherwise append the matched part
|
|
443
|
+
if (aliasPath.includes('*')) {
|
|
444
|
+
resolvedPath = aliasPath.replace(/\*/g, cleanMatchedPart);
|
|
445
|
+
} else {
|
|
446
|
+
resolvedPath = path.join(aliasPath, cleanMatchedPart);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// If we found a match and have a base directory, resolve the path relative to it
|
|
452
|
+
if (longestMatch) {
|
|
453
|
+
resolvedPath = path.resolve(cwd, resolvedPath);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return resolvedPath;
|
|
457
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
2
|
+
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkDirective from 'remark-directive'
|
|
5
|
+
import rehypeRaw from 'rehype-raw';
|
|
6
|
+
import remarkMath from 'remark-math'
|
|
7
|
+
import rehypeKatex from 'rehype-katex'
|
|
8
|
+
|
|
9
|
+
import { Settings } from "@xyd-js/core";
|
|
10
|
+
|
|
11
|
+
import { remarkMdxToc, RemarkMdxTocOptions } from "./mdToc";
|
|
12
|
+
import { remarkInjectCodeMeta } from "./mdCode";
|
|
13
|
+
import { extractThemeSettings } from "./mdThemeSettings";
|
|
14
|
+
import { extractPage } from "./mdPage";
|
|
15
|
+
import { mdHeadingId } from "./mdHeadingId";
|
|
16
|
+
import { rehypeHeading } from "./rehypeHeading";
|
|
17
|
+
import { mdComponentDirective } from "./component-directives";
|
|
18
|
+
import { mdFunctionChangelog, mdFunctionImportCode, mdFunctionUniform, mdFunctionInclude } from "./functions"
|
|
19
|
+
import { mdCodeRehype } from "./developer-writing";
|
|
20
|
+
import { mdMeta } from "./meta";
|
|
21
|
+
import { mdComposer } from "./composer/mdComposer";
|
|
22
|
+
import { outputVars } from "./output-variables";
|
|
23
|
+
import { mdImage } from "./mdImage";
|
|
24
|
+
import { mdImageRehype } from "./mdImageRehype";
|
|
25
|
+
|
|
26
|
+
import { recmaOverrideComponents } from "./recmaOverrideComponents";
|
|
27
|
+
|
|
28
|
+
export function defaultRemarkPlugins(
|
|
29
|
+
toc: RemarkMdxTocOptions,
|
|
30
|
+
settings?: Settings
|
|
31
|
+
) {
|
|
32
|
+
return [
|
|
33
|
+
...thirdPartyRemarkPlugins(),
|
|
34
|
+
...remarkPlugins(toc, settings),
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function thirdPartyRemarkPlugins() {
|
|
39
|
+
return [
|
|
40
|
+
remarkFrontmatter,
|
|
41
|
+
remarkMdxFrontmatter,
|
|
42
|
+
remarkGfm,
|
|
43
|
+
remarkDirective,
|
|
44
|
+
remarkMath,
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function remarkPlugins(
|
|
49
|
+
toc: RemarkMdxTocOptions,
|
|
50
|
+
settings?: Settings
|
|
51
|
+
) {
|
|
52
|
+
return [
|
|
53
|
+
mdHeadingId,
|
|
54
|
+
remarkInjectCodeMeta,
|
|
55
|
+
mdImage,
|
|
56
|
+
remarkMdxToc(toc),
|
|
57
|
+
extractThemeSettings, // TODO: to delet ?
|
|
58
|
+
extractPage, // TODO: to delete ?
|
|
59
|
+
outputVars,
|
|
60
|
+
mdComponentDirective(settings),
|
|
61
|
+
mdComposer(settings),
|
|
62
|
+
...remarkFunctionPlugins(settings),
|
|
63
|
+
mdMeta(settings),
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function includeRemarkPlugins(settings?: Settings) {
|
|
68
|
+
return [
|
|
69
|
+
remarkGfm,
|
|
70
|
+
remarkDirective,
|
|
71
|
+
|
|
72
|
+
mdHeadingId,
|
|
73
|
+
remarkInjectCodeMeta,
|
|
74
|
+
outputVars,
|
|
75
|
+
mdComponentDirective(settings),
|
|
76
|
+
...remarkFunctionPlugins(settings),
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function remarkFunctionPlugins(settings?: Settings) {
|
|
81
|
+
return [
|
|
82
|
+
mdFunctionImportCode(settings),
|
|
83
|
+
mdFunctionUniform(settings),
|
|
84
|
+
mdFunctionInclude(settings),
|
|
85
|
+
mdFunctionChangelog(settings),
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let rehypeMermaid
|
|
90
|
+
async function getMermaidPlugin() {
|
|
91
|
+
if (!rehypeMermaid) {
|
|
92
|
+
rehypeMermaid = (await import('rehype-mermaid')).default
|
|
93
|
+
}
|
|
94
|
+
return rehypeMermaid
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function thirdPartyRehypePlugins(settings?: Settings) {
|
|
98
|
+
const plugins = [
|
|
99
|
+
[rehypeRaw, {
|
|
100
|
+
passThrough: ['mdxjsEsm', 'mdxJsxFlowElement', 'mdxJsxTextElement'],
|
|
101
|
+
}] as any,
|
|
102
|
+
rehypeKatex,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if (settings?.integrations?.diagrams) {
|
|
106
|
+
plugins.push(await getMermaidPlugin())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return plugins
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function defaultRehypePlugins(settings?: Settings) {
|
|
113
|
+
return [
|
|
114
|
+
...(await thirdPartyRehypePlugins(settings)),
|
|
115
|
+
rehypeHeading,
|
|
116
|
+
mdImageRehype,
|
|
117
|
+
mdCodeRehype(settings),
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function defaultRecmaPlugins(settings?: Settings) {
|
|
122
|
+
return [
|
|
123
|
+
recmaOverrideComponents,
|
|
124
|
+
]
|
|
125
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { visit } from "unist-util-visit";
|
|
2
|
+
import { injectCodeMeta } from "./utils/injectCodeMeta";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This plugin injects the code meta into the code node's data
|
|
6
|
+
* so that it can be used in the code block component
|
|
7
|
+
*/
|
|
8
|
+
export function remarkInjectCodeMeta() {
|
|
9
|
+
return (tree: any) => {
|
|
10
|
+
console.time('plugin:remarkInjectCodeMeta');
|
|
11
|
+
visit(tree, 'code', (node) => {
|
|
12
|
+
injectCodeMeta(node, node.meta);
|
|
13
|
+
});
|
|
14
|
+
console.timeEnd('plugin:remarkInjectCodeMeta');
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
import GithubSlugger from 'github-slugger';
|
|
3
|
+
import type { Plugin } from 'unified';
|
|
4
|
+
import type { Root, Heading } from 'mdast';
|
|
5
|
+
|
|
6
|
+
import {mdParameters} from "./utils/mdParameters"
|
|
7
|
+
|
|
8
|
+
interface HeadingData {
|
|
9
|
+
hProperties?: {
|
|
10
|
+
id?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const mdHeadingId: Plugin<[], Root> = () => {
|
|
16
|
+
let slugger = new GithubSlugger();
|
|
17
|
+
|
|
18
|
+
return (tree) => {
|
|
19
|
+
visit(tree, 'heading', (node: Heading & { data?: HeadingData }) => {
|
|
20
|
+
if (!node.data) {
|
|
21
|
+
node.data = {
|
|
22
|
+
hProperties: {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (node.depth === 1) {
|
|
27
|
+
slugger.reset();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create a slug from the heading text
|
|
31
|
+
const text = node.children
|
|
32
|
+
.map((child) => ('value' in child ? child.value : ''))
|
|
33
|
+
.join('');
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const {props, sanitizedText} = mdParameters(text)
|
|
37
|
+
|
|
38
|
+
const id = props.id || slugger.slug(sanitizedText);
|
|
39
|
+
|
|
40
|
+
// Add the id to the heading's data
|
|
41
|
+
node.data.hProperties = {
|
|
42
|
+
...node.data.hProperties,
|
|
43
|
+
id,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {unified} from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import {visit} from 'unist-util-visit';
|
|
4
|
+
import {describe, it, expect} from 'vitest';
|
|
5
|
+
|
|
6
|
+
import {mdImage} from './mdImage';
|
|
7
|
+
|
|
8
|
+
describe('mdImage', () => {
|
|
9
|
+
it('should parse caption attribute from image and attach to hProperties', async () => {
|
|
10
|
+
const md = '{caption="Hello world"}';
|
|
11
|
+
const processor = unified()
|
|
12
|
+
.use(remarkParse)
|
|
13
|
+
.use(mdImage);
|
|
14
|
+
const tree = processor.parse(md);
|
|
15
|
+
await processor.run(tree);
|
|
16
|
+
let found = false;
|
|
17
|
+
visit(tree, 'image', (node: any) => {
|
|
18
|
+
found = true;
|
|
19
|
+
expect(node.url).toBe('/img.png');
|
|
20
|
+
expect(node.alt).toBe('Alt text');
|
|
21
|
+
expect(node.data).toBeDefined();
|
|
22
|
+
expect(node.data.hProperties).toBeDefined();
|
|
23
|
+
expect(node.data.hProperties.caption).toBe('Hello world');
|
|
24
|
+
});
|
|
25
|
+
expect(found).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should work with multiple attributes', async () => {
|
|
29
|
+
const md = '{caption="Hello world" style="border:1px solid red"}';
|
|
30
|
+
const processor = unified()
|
|
31
|
+
.use(remarkParse)
|
|
32
|
+
.use(mdImage);
|
|
33
|
+
const tree = processor.parse(md);
|
|
34
|
+
await processor.run(tree);
|
|
35
|
+
let found = false;
|
|
36
|
+
visit(tree, 'image', (node: any) => {
|
|
37
|
+
found = true;
|
|
38
|
+
expect(node.data.hProperties.caption).toBe('Hello world');
|
|
39
|
+
expect(node.data.hProperties.style).toBe('border:1px solid red');
|
|
40
|
+
});
|
|
41
|
+
expect(found).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should not fail if no attributes are present', async () => {
|
|
45
|
+
const md = '';
|
|
46
|
+
const processor = unified()
|
|
47
|
+
.use(remarkParse)
|
|
48
|
+
.use(mdImage);
|
|
49
|
+
const tree = processor.parse(md);
|
|
50
|
+
await processor.run(tree);
|
|
51
|
+
let found = false;
|
|
52
|
+
visit(tree, 'image', (node: any) => {
|
|
53
|
+
found = true;
|
|
54
|
+
expect(node.alt).toBe('Alt text');
|
|
55
|
+
expect(node.data?.hProperties?.caption).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
expect(found).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|