@xyd-js/content 0.0.0-build

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 (97) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/ISSUES.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +3 -0
  5. package/TODO.md +2 -0
  6. package/dist/index.d.ts +28 -0
  7. package/dist/index.js +1625 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/md.d.ts +72 -0
  10. package/dist/md.js +23508 -0
  11. package/dist/md.js.map +1 -0
  12. package/dist/mdToc-NBBxMJ4l.d.ts +12 -0
  13. package/dist/vite.d.ts +1066 -0
  14. package/dist/vite.js +20156 -0
  15. package/dist/vite.js.map +1 -0
  16. package/package.json +67 -0
  17. package/packages/md/index.ts +25 -0
  18. package/packages/md/plugins/component-directives/index.ts +3 -0
  19. package/packages/md/plugins/component-directives/mdComponentDirective.ts +577 -0
  20. package/packages/md/plugins/component-directives/types.ts +1 -0
  21. package/packages/md/plugins/component-directives/utils.ts +27 -0
  22. package/packages/md/plugins/composer/__fixtures__/1.single-example/input.md +7 -0
  23. package/packages/md/plugins/composer/__fixtures__/1.single-example/output.json +63 -0
  24. package/packages/md/plugins/composer/__fixtures__/2.single-example-with-name/input.md +7 -0
  25. package/packages/md/plugins/composer/__fixtures__/2.single-example-with-name/output.json +63 -0
  26. package/packages/md/plugins/composer/__fixtures__/3.multiple-examples/input.md +15 -0
  27. package/packages/md/plugins/composer/__fixtures__/3.multiple-examples/output.json +122 -0
  28. package/packages/md/plugins/composer/__fixtures__/4.example-groups/input.md +23 -0
  29. package/packages/md/plugins/composer/__fixtures__/4.example-groups/output.json +184 -0
  30. package/packages/md/plugins/composer/__tests__/mdComposer.test.ts +41 -0
  31. package/packages/md/plugins/composer/__tests__/testHelpers.ts +48 -0
  32. package/packages/md/plugins/composer/index.ts +1 -0
  33. package/packages/md/plugins/composer/mdComposer.ts +146 -0
  34. package/packages/md/plugins/developer-writing/index.ts +3 -0
  35. package/packages/md/plugins/developer-writing/mdCodeRehype.ts +81 -0
  36. package/packages/md/plugins/functions/__fixtures__/external.ts +4 -0
  37. package/packages/md/plugins/functions/__fixtures__/test-include.md +31 -0
  38. package/packages/md/plugins/functions/__fixtures__/test.js +11 -0
  39. package/packages/md/plugins/functions/__fixtures__/test.py +9 -0
  40. package/packages/md/plugins/functions/__fixtures__/test.ts +18 -0
  41. package/packages/md/plugins/functions/__tests__/mdFunctionImportCode.test.ts +314 -0
  42. package/packages/md/plugins/functions/__tests__/mdFunctionInclude.test.ts +44 -0
  43. package/packages/md/plugins/functions/__tests__/parseFunctionCall.test.ts +70 -0
  44. package/packages/md/plugins/functions/__tests__/testHelpers.ts +95 -0
  45. package/packages/md/plugins/functions/index.ts +15 -0
  46. package/packages/md/plugins/functions/mdFunctionChangelog.ts +135 -0
  47. package/packages/md/plugins/functions/mdFunctionImportCode.ts +92 -0
  48. package/packages/md/plugins/functions/mdFunctionInclude.ts +119 -0
  49. package/packages/md/plugins/functions/mdFunctionUniform.ts +79 -0
  50. package/packages/md/plugins/functions/types.ts +9 -0
  51. package/packages/md/plugins/functions/uniformProcessor.ts +349 -0
  52. package/packages/md/plugins/functions/utils.ts +457 -0
  53. package/packages/md/plugins/index.ts +125 -0
  54. package/packages/md/plugins/mdCode.ts +16 -0
  55. package/packages/md/plugins/mdHeadingId.ts +47 -0
  56. package/packages/md/plugins/mdImage.test.ts +59 -0
  57. package/packages/md/plugins/mdImage.ts +55 -0
  58. package/packages/md/plugins/mdImageRehype.ts +13 -0
  59. package/packages/md/plugins/mdPage.ts +35 -0
  60. package/packages/md/plugins/mdThemeSettings.ts +34 -0
  61. package/packages/md/plugins/mdToc.ts +229 -0
  62. package/packages/md/plugins/meta/index.ts +1 -0
  63. package/packages/md/plugins/meta/mdMeta.ts +198 -0
  64. package/packages/md/plugins/output-variables/__fixtures__/1.simple/input.md +22 -0
  65. package/packages/md/plugins/output-variables/__fixtures__/1.simple/output.json +191 -0
  66. package/packages/md/plugins/output-variables/__fixtures__/2.multiple-vars/input.md +21 -0
  67. package/packages/md/plugins/output-variables/__fixtures__/2.multiple-vars/output.json +127 -0
  68. package/packages/md/plugins/output-variables/__tests__/index.test.ts +28 -0
  69. package/packages/md/plugins/output-variables/__tests__/testHelpers.ts +36 -0
  70. package/packages/md/plugins/output-variables/index.ts +1 -0
  71. package/packages/md/plugins/output-variables/lib/const.ts +4 -0
  72. package/packages/md/plugins/output-variables/lib/factoryAttributes.ts +350 -0
  73. package/packages/md/plugins/output-variables/lib/factoryLabel.ts +135 -0
  74. package/packages/md/plugins/output-variables/lib/factoryName.ts +59 -0
  75. package/packages/md/plugins/output-variables/lib/index.ts +21 -0
  76. package/packages/md/plugins/output-variables/lib/outputVarsContainer.ts +328 -0
  77. package/packages/md/plugins/output-variables/lib/util.ts +494 -0
  78. package/packages/md/plugins/output-variables/remarkOutputVars.ts +22 -0
  79. package/packages/md/plugins/recmaOverrideComponents.ts +74 -0
  80. package/packages/md/plugins/rehypeHeading.ts +58 -0
  81. package/packages/md/plugins/types.ts +15 -0
  82. package/packages/md/plugins/utils/componentLike.ts +76 -0
  83. package/packages/md/plugins/utils/index.ts +2 -0
  84. package/packages/md/plugins/utils/injectCodeMeta.ts +59 -0
  85. package/packages/md/plugins/utils/mdParameters.test.ts +114 -0
  86. package/packages/md/plugins/utils/mdParameters.ts +249 -0
  87. package/packages/md/plugins/utils/mdastTypes.ts +42 -0
  88. package/packages/md/search/index.ts +257 -0
  89. package/packages/md/search/types.ts +36 -0
  90. package/packages/vite/index.ts +20 -0
  91. package/src/fs.ts +81 -0
  92. package/src/index.ts +7 -0
  93. package/src/navigation.ts +147 -0
  94. package/src/types.ts +8 -0
  95. package/tsconfig.json +49 -0
  96. package/tsup.config.ts +32 -0
  97. 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 = '![Alt text](/img.png){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 = '![Alt text](/img.png){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 = '![Alt text](/img.png)';
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
+ });