@xyd-js/content 0.1.0-xyd.13 → 0.1.0-xyd.16
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 +34 -0
- package/ISSUES.md +1 -0
- package/TODO.md +2 -0
- package/dist/index.d.ts +24 -5
- package/dist/index.js +1411 -21776
- package/dist/index.js.map +1 -1
- package/dist/md.d.ts +62 -7
- package/dist/md.js +18071 -15085
- package/dist/md.js.map +1 -1
- package/dist/{mdToc-CYxzibVZ.d.ts → mdToc-NBBxMJ4l.d.ts} +1 -0
- package/dist/vite.d.ts +81 -2
- package/dist/vite.js +9716 -10090
- package/dist/vite.js.map +1 -1
- package/example.txt +0 -0
- package/package.json +24 -6
- package/packages/md/index.ts +17 -8
- package/packages/md/plugins/component-directives/index.ts +3 -0
- package/packages/md/plugins/component-directives/mdComponentDirective.ts +524 -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 +78 -0
- package/packages/md/plugins/functions/__fixtures__/external.ts +4 -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 +295 -0
- package/packages/md/plugins/functions/__tests__/parseFunctionCall.test.ts +47 -0
- package/packages/md/plugins/functions/__tests__/testHelpers.ts +71 -0
- package/packages/md/plugins/functions/index.ts +11 -0
- package/packages/md/plugins/functions/mdFunctionChangelog.ts +124 -0
- package/packages/md/plugins/functions/mdFunctionImportCode.ts +83 -0
- package/packages/md/plugins/functions/mdFunctionUniform.ts +79 -0
- package/packages/md/plugins/functions/types.ts +6 -0
- package/packages/md/plugins/functions/uniformProcessor.ts +349 -0
- package/packages/md/plugins/functions/utils.ts +423 -0
- package/packages/md/plugins/index.ts +56 -11
- package/packages/md/plugins/mdCode.ts +52 -4
- package/packages/md/plugins/mdHeadingId.ts +47 -0
- package/packages/md/plugins/mdPage.ts +3 -0
- package/packages/md/plugins/mdThemeSettings.ts +4 -0
- package/packages/md/plugins/mdToc.ts +108 -17
- package/packages/md/plugins/meta/index.ts +1 -0
- package/packages/md/plugins/meta/mdMeta.ts +189 -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/rehypeHeading.ts +50 -0
- package/packages/md/plugins/types.ts +15 -0
- package/packages/md/plugins/utils/componentLike.ts +72 -0
- package/packages/md/plugins/utils/index.ts +2 -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 +251 -0
- package/packages/md/search/types.ts +36 -0
- package/packages/vite/index.ts +8 -2
- package/src/fs.ts +51 -36
- package/src/index.ts +4 -4
- package/src/navigation.ts +50 -38
- package/src/types.ts +8 -0
- package/tsconfig.json +31 -8
- package/vitest.config.ts +17 -0
- package/packages/md/plugins/mdCodeGroup.ts +0 -40
- package/packages/md/plugins/mdComponentDirective.ts +0 -141
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mdParameters } from './mdParameters';
|
|
3
|
+
|
|
4
|
+
describe('mdParameters', () => {
|
|
5
|
+
describe("attributes", () => {
|
|
6
|
+
it('1.basic', () => {
|
|
7
|
+
const result = mdParameters('Hello [class="test" id="main"] world');
|
|
8
|
+
|
|
9
|
+
expect(result.attributes).toEqual({
|
|
10
|
+
class: 'test',
|
|
11
|
+
id: 'main'
|
|
12
|
+
});
|
|
13
|
+
expect(result.props).toEqual({});
|
|
14
|
+
expect(result.sanitizedText).toBe('Hello world');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('2.markdown+links', () => {
|
|
18
|
+
const result = mdParameters('[desc="This is [xyd](https://github.com/xyd-js) - a docs framework"]');
|
|
19
|
+
|
|
20
|
+
expect(result.attributes).toEqual({
|
|
21
|
+
desc: 'This is [xyd](https://github.com/xyd-js) - a docs framework',
|
|
22
|
+
});
|
|
23
|
+
expect(result.props).toEqual({});
|
|
24
|
+
expect(result.sanitizedText).toBe('');
|
|
25
|
+
});
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("props", () => {
|
|
29
|
+
it('1.basic', () => {
|
|
30
|
+
const result = mdParameters('Hello {title="tooltip" disabled} world');
|
|
31
|
+
|
|
32
|
+
expect(result.attributes).toEqual({});
|
|
33
|
+
expect(result.props).toEqual({
|
|
34
|
+
title: 'tooltip',
|
|
35
|
+
disabled: 'true'
|
|
36
|
+
});
|
|
37
|
+
expect(result.sanitizedText).toBe('Hello world');
|
|
38
|
+
});
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("attributes + props", () => {
|
|
42
|
+
it('1.basic', () => {
|
|
43
|
+
const result = mdParameters('Hello [class="test"] {title="tooltip"} world');
|
|
44
|
+
|
|
45
|
+
expect(result.attributes).toEqual({
|
|
46
|
+
class: 'test'
|
|
47
|
+
});
|
|
48
|
+
expect(result.props).toEqual({
|
|
49
|
+
title: 'tooltip'
|
|
50
|
+
});
|
|
51
|
+
expect(result.sanitizedText).toBe('Hello world');
|
|
52
|
+
});
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("attributes + htmlMd option", () => {
|
|
56
|
+
it('1.basic', () => {
|
|
57
|
+
const result = mdParameters(
|
|
58
|
+
`[desc="Produces static files within <code>.xyd/build/client</code> which you an deploy easily."]`,
|
|
59
|
+
{
|
|
60
|
+
htmlMd: true
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
expect(result.attributes).toEqual({
|
|
64
|
+
desc: "Produces static files within `.xyd/build/client` which you an deploy easily."
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.sanitizedText).toBe('');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('2.multiple HTML tags in attribute', () => {
|
|
71
|
+
const result = mdParameters(
|
|
72
|
+
`[desc="This is <strong>bold</strong> and <em>italic</em> text with <code>inline code</code>."]`,
|
|
73
|
+
{
|
|
74
|
+
htmlMd: true
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
expect(result.attributes).toEqual({
|
|
78
|
+
desc: "This is **bold** and *italic* text with `inline code`."
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("attributes + :::component", () => {
|
|
84
|
+
it('1.basic', () => {
|
|
85
|
+
const result = mdParameters(`[desc="Choose between light/dark mode. :::button \n **xyd 0.1.0-alpha is coming soon!** \n :::"]`);
|
|
86
|
+
const expectedText = `Choose between light/dark mode.\n :::button \n **xyd 0.1.0-alpha is coming soon!**\n :::`;
|
|
87
|
+
|
|
88
|
+
expect(result.attributes).toEqual({
|
|
89
|
+
desc: expectedText
|
|
90
|
+
});
|
|
91
|
+
expect(result.sanitizedText).toBe('');
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("2", () => {
|
|
95
|
+
const result = mdParameters(`[desc=":::button\\n **xyd 0.1.0-alpha is coming soon!**\\n"]`);
|
|
96
|
+
const expectedText = `:::button\n **xyd 0.1.0-alpha is coming soon!**\n:::`;
|
|
97
|
+
|
|
98
|
+
expect(result.attributes).toEqual({
|
|
99
|
+
desc: expectedText
|
|
100
|
+
});
|
|
101
|
+
expect(result.sanitizedText).toBe('');
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("3", () => {
|
|
105
|
+
const result = mdParameters(`[desc="Choose between :::color-scheme-button\\n::: mode"]`);
|
|
106
|
+
const expectedText = `Choose between\n :::color-scheme-button\n :::\n mode`;
|
|
107
|
+
|
|
108
|
+
expect(result.attributes).toEqual({
|
|
109
|
+
desc: expectedText
|
|
110
|
+
});
|
|
111
|
+
expect(result.sanitizedText).toBe('');
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
export function mdParameters(
|
|
2
|
+
text: string,
|
|
3
|
+
options = {
|
|
4
|
+
htmlMd: false,
|
|
5
|
+
}
|
|
6
|
+
) {
|
|
7
|
+
const attributes = parseAttributes(text, options.htmlMd);
|
|
8
|
+
const props = parseProps(text, options.htmlMd);
|
|
9
|
+
let sanitizedText = removeParameters(text);
|
|
10
|
+
|
|
11
|
+
// Apply HTML to markdown transformation if requested
|
|
12
|
+
if (options?.htmlMd) {
|
|
13
|
+
sanitizedText = transformHtmlToMarkdown(sanitizedText);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
attributes,
|
|
18
|
+
props,
|
|
19
|
+
sanitizedText
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseAttributes(text: string, htmlMd: boolean = false) {
|
|
24
|
+
return parseParameters(text, '[', ']', htmlMd);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseProps(text: string, htmlMd: boolean = false) {
|
|
28
|
+
return parseParameters(text, '{', '}', htmlMd);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeParameters(value: string): string {
|
|
32
|
+
// Convert escaped newlines to actual newlines
|
|
33
|
+
value = value.replace(/\\n/g, '\n');
|
|
34
|
+
|
|
35
|
+
// Remove any HTML tags
|
|
36
|
+
value = value.replace(/<[^>]*>/g, '');
|
|
37
|
+
|
|
38
|
+
// Remove any script tags and their content
|
|
39
|
+
value = value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
|
40
|
+
|
|
41
|
+
// Remove any potentially dangerous characters, but preserve newlines, :::component syntax, and markdown
|
|
42
|
+
// Don't remove quotes, colons, or asterisks as they're needed for ::: components and markdown
|
|
43
|
+
value = value.replace(/[<>]/g, '');
|
|
44
|
+
|
|
45
|
+
// Remove any control characters except newlines (\n = \x0A)
|
|
46
|
+
value = value.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '');
|
|
47
|
+
|
|
48
|
+
// Ensure proper spacing around ::: components
|
|
49
|
+
// Add newline before ::: if there isn't one already
|
|
50
|
+
value = value.replace(/([^\n])\s*:::/g, '$1\n :::');
|
|
51
|
+
|
|
52
|
+
// Auto-close ::: components if they're not already closed
|
|
53
|
+
// Check if there's a ::: component that's not properly closed
|
|
54
|
+
if (value.includes(':::')) {
|
|
55
|
+
// Count the number of ::: occurrences
|
|
56
|
+
const colonCount = (value.match(/:::/g) || []).length;
|
|
57
|
+
|
|
58
|
+
// If we have an odd number of ::: components, we need to add a closing one
|
|
59
|
+
if (colonCount % 2 === 1) {
|
|
60
|
+
// Only add closing ::: if the string doesn't already end with it
|
|
61
|
+
if (!value.trim().endsWith(':::')) {
|
|
62
|
+
// Add newline before closing ::: if the string doesn't end with newline
|
|
63
|
+
if (!value.endsWith('\n')) {
|
|
64
|
+
value = value + '\n';
|
|
65
|
+
}
|
|
66
|
+
value = value + ':::';
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Even number of ::: components, but we need to ensure proper formatting
|
|
70
|
+
// Look for patterns like "::: text" and convert to ":::\n text"
|
|
71
|
+
// Handle both cases: "::: text" and ":::\n text"
|
|
72
|
+
value = value.replace(/:::\s+([^\n]+)/g, ':::\n $1');
|
|
73
|
+
// Also handle the case where there's already a newline but we need to ensure proper spacing
|
|
74
|
+
value = value.replace(/:::\n\s*([^\n]+)/g, ':::\n $1');
|
|
75
|
+
// Specific case: if we have ":::\n text", ensure it becomes ":::\n text"
|
|
76
|
+
value = value.replace(/:::\n([^\n]+)/g, ':::\n $1');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Trim whitespace
|
|
81
|
+
value = value.trim();
|
|
82
|
+
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function removeParameters(text: string): string {
|
|
87
|
+
// Use a stack-based approach to handle nested brackets
|
|
88
|
+
let result = '';
|
|
89
|
+
let stack = 0;
|
|
90
|
+
let inQuotes = false;
|
|
91
|
+
let quoteChar = '';
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < text.length; i++) {
|
|
94
|
+
const char = text[i];
|
|
95
|
+
|
|
96
|
+
// Handle quotes
|
|
97
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
98
|
+
inQuotes = true;
|
|
99
|
+
quoteChar = char;
|
|
100
|
+
} else if (char === quoteChar && inQuotes) {
|
|
101
|
+
inQuotes = false;
|
|
102
|
+
quoteChar = '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Only process brackets when not in quotes
|
|
106
|
+
if (!inQuotes) {
|
|
107
|
+
if (char === '[' || char === '{') {
|
|
108
|
+
stack++;
|
|
109
|
+
} else if (char === ']' || char === '}') {
|
|
110
|
+
stack--;
|
|
111
|
+
} else if (stack === 0) {
|
|
112
|
+
result += char;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Clean up multiple spaces and trim
|
|
118
|
+
return result.replace(/\s+/g, ' ').trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseParameters(
|
|
122
|
+
text: string,
|
|
123
|
+
delimiter: string,
|
|
124
|
+
closingDelimiter: string,
|
|
125
|
+
htmlMd: boolean = false
|
|
126
|
+
) {
|
|
127
|
+
const attributes: Record<string, string> = {};
|
|
128
|
+
|
|
129
|
+
// Find all parameter blocks using stack-based approach
|
|
130
|
+
const blocks = findNestedBlocks(text, delimiter, closingDelimiter);
|
|
131
|
+
|
|
132
|
+
for (const blockContent of blocks) {
|
|
133
|
+
// Then parse individual parameters within the block
|
|
134
|
+
const paramRegex = /(!)?([^=\s]+)(?:=(?:"([^"]*)"|([^\s]*)))?/g;
|
|
135
|
+
let paramMatch;
|
|
136
|
+
|
|
137
|
+
while ((paramMatch = paramRegex.exec(blockContent)) !== null) {
|
|
138
|
+
const [_, isNegated, prop, quotedValue, unquotedValue] = paramMatch;
|
|
139
|
+
const value = quotedValue !== undefined ? quotedValue : unquotedValue;
|
|
140
|
+
|
|
141
|
+
// Sanitize the property name
|
|
142
|
+
let sanitizedParam = sanitizeParameters(prop);
|
|
143
|
+
|
|
144
|
+
// Handle the value - apply HTML to markdown transformation first if requested
|
|
145
|
+
let sanitizedValue: string;
|
|
146
|
+
if (value) {
|
|
147
|
+
if (htmlMd) {
|
|
148
|
+
// Apply HTML to markdown transformation first, then sanitize
|
|
149
|
+
sanitizedValue = sanitizeParameters(transformHtmlToMarkdown(value));
|
|
150
|
+
} else {
|
|
151
|
+
sanitizedValue = sanitizeParameters(value);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
sanitizedValue = isNegated ? 'false' : 'true';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sanitizedParam.startsWith("#") && sanitizedValue === "true") {
|
|
158
|
+
sanitizedValue = sanitizedParam.replace("#", "").trim()
|
|
159
|
+
sanitizedParam = "id"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
attributes[sanitizedParam] = sanitizedValue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return attributes;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function findNestedBlocks(text: string, openDelimiter: string, closeDelimiter: string): string[] {
|
|
170
|
+
const blocks: string[] = [];
|
|
171
|
+
let stack = 0;
|
|
172
|
+
let startIndex = -1;
|
|
173
|
+
let inQuotes = false;
|
|
174
|
+
let quoteChar = '';
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < text.length; i++) {
|
|
177
|
+
const char = text[i];
|
|
178
|
+
|
|
179
|
+
// Handle quotes
|
|
180
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
181
|
+
inQuotes = true;
|
|
182
|
+
quoteChar = char;
|
|
183
|
+
} else if (char === quoteChar && inQuotes) {
|
|
184
|
+
inQuotes = false;
|
|
185
|
+
quoteChar = '';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Only process delimiters when not in quotes
|
|
189
|
+
if (!inQuotes) {
|
|
190
|
+
if (char === openDelimiter) {
|
|
191
|
+
if (stack === 0) {
|
|
192
|
+
startIndex = i + 1; // +1 to skip the opening delimiter
|
|
193
|
+
}
|
|
194
|
+
stack++;
|
|
195
|
+
} else if (char === closeDelimiter) {
|
|
196
|
+
stack--;
|
|
197
|
+
if (stack === 0 && startIndex !== -1) {
|
|
198
|
+
blocks.push(text.substring(startIndex, i));
|
|
199
|
+
startIndex = -1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return blocks;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function transformHtmlToMarkdown(text: string): string {
|
|
209
|
+
// Transform common HTML tags to markdown
|
|
210
|
+
return text
|
|
211
|
+
// <code> tags to backticks
|
|
212
|
+
.replace(/<code\b[^>]*>(.*?)<\/code>/gi, '`$1`')
|
|
213
|
+
// <strong> and <b> tags to bold
|
|
214
|
+
.replace(/<(strong|b)\b[^>]*>(.*?)<\/(strong|b)>/gi, '**$2**')
|
|
215
|
+
// <em> and <i> tags to italic
|
|
216
|
+
.replace(/<(em|i)\b[^>]*>(.*?)<\/(em|i)>/gi, '*$2*')
|
|
217
|
+
// <a> tags to markdown links
|
|
218
|
+
.replace(/<a\b[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
|
219
|
+
// <br> tags to line breaks
|
|
220
|
+
.replace(/<br\b[^>]*>/gi, '\n')
|
|
221
|
+
// <p> tags to paragraphs (with line breaks)
|
|
222
|
+
.replace(/<p\b[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
|
223
|
+
// <h1> to <h6> tags to markdown headers
|
|
224
|
+
.replace(/<h1\b[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
|
225
|
+
.replace(/<h2\b[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
|
226
|
+
.replace(/<h3\b[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
|
227
|
+
.replace(/<h4\b[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n')
|
|
228
|
+
.replace(/<h5\b[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n')
|
|
229
|
+
.replace(/<h6\b[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n')
|
|
230
|
+
// <ul> and <ol> lists
|
|
231
|
+
.replace(/<ul\b[^>]*>(.*?)<\/ul>/gis, (match, content) => {
|
|
232
|
+
return content.replace(/<li\b[^>]*>(.*?)<\/li>/gi, '- $1\n') + '\n';
|
|
233
|
+
})
|
|
234
|
+
.replace(/<ol\b[^>]*>(.*?)<\/ol>/gis, (match, content) => {
|
|
235
|
+
let counter = 1;
|
|
236
|
+
return content.replace(/<li\b[^>]*>(.*?)<\/li>/gi, () => `${counter++}. $1\n`) + '\n';
|
|
237
|
+
})
|
|
238
|
+
// <blockquote> tags
|
|
239
|
+
.replace(/<blockquote\b[^>]*>(.*?)<\/blockquote>/gis, (match, content) => {
|
|
240
|
+
return content.split('\n').map(line => `> ${line}`).join('\n') + '\n\n';
|
|
241
|
+
})
|
|
242
|
+
// <pre> tags for code blocks
|
|
243
|
+
.replace(/<pre\b[^>]*>(.*?)<\/pre>/gis, '```\n$1\n```\n\n')
|
|
244
|
+
// Remove any remaining HTML tags
|
|
245
|
+
.replace(/<[^>]*>/g, '')
|
|
246
|
+
// Clean up multiple line breaks
|
|
247
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
248
|
+
.trim();
|
|
249
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List of standard MDAST node types
|
|
3
|
+
*/
|
|
4
|
+
export const standardMdastTypes = [
|
|
5
|
+
'root',
|
|
6
|
+
'paragraph',
|
|
7
|
+
'heading',
|
|
8
|
+
'text',
|
|
9
|
+
'emphasis',
|
|
10
|
+
'strong',
|
|
11
|
+
'delete',
|
|
12
|
+
'blockquote',
|
|
13
|
+
'code',
|
|
14
|
+
'link',
|
|
15
|
+
'image',
|
|
16
|
+
'list',
|
|
17
|
+
'listItem',
|
|
18
|
+
'table',
|
|
19
|
+
'tableRow',
|
|
20
|
+
'tableCell',
|
|
21
|
+
'html',
|
|
22
|
+
'break',
|
|
23
|
+
'thematicBreak',
|
|
24
|
+
'yaml',
|
|
25
|
+
'definition',
|
|
26
|
+
'footnoteDefinition',
|
|
27
|
+
'footnoteReference',
|
|
28
|
+
'inlineCode',
|
|
29
|
+
'linkReference',
|
|
30
|
+
'imageReference',
|
|
31
|
+
'footnote',
|
|
32
|
+
'tableCaption'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a given type is a standard MDAST type
|
|
37
|
+
* @param type The node type to check
|
|
38
|
+
* @returns True if the type is a standard MDAST type, false otherwise
|
|
39
|
+
*/
|
|
40
|
+
export function isStandardMdastType(type: string): boolean {
|
|
41
|
+
return standardMdastTypes.includes(type);
|
|
42
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { unified } from 'unified'
|
|
5
|
+
import remarkParse from 'remark-parse'
|
|
6
|
+
import remarkRehype from 'remark-rehype'
|
|
7
|
+
import rehypeStringify from 'rehype-stringify'
|
|
8
|
+
import { visit } from "unist-util-visit";
|
|
9
|
+
|
|
10
|
+
import GitHubSlugger from 'github-slugger';
|
|
11
|
+
|
|
12
|
+
import type { Settings, Sidebar, SidebarRoute } from '@xyd-js/core'
|
|
13
|
+
|
|
14
|
+
import { DocSectionSchema } from './types';
|
|
15
|
+
import { markdownPlugins } from "../"
|
|
16
|
+
|
|
17
|
+
export async function mapSettingsToDocSections(xydSettings: Settings) {
|
|
18
|
+
const pages = flatPages(xydSettings.navigation?.sidebar || [], {})
|
|
19
|
+
.map(page => ({
|
|
20
|
+
name: page,
|
|
21
|
+
path: path.join(process.cwd(), page)
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
return await processSections(pages, xydSettings)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function mapContentToDocSections(
|
|
28
|
+
xydSettings: Settings,
|
|
29
|
+
route: string,
|
|
30
|
+
content: string,
|
|
31
|
+
) {
|
|
32
|
+
const sections: DocSectionSchema[] = []
|
|
33
|
+
let currentSection: DocSectionSchema | null = null
|
|
34
|
+
|
|
35
|
+
const rootSection: DocSectionSchema = {
|
|
36
|
+
// id: '',
|
|
37
|
+
pageId: '',
|
|
38
|
+
pageUrl: '',
|
|
39
|
+
pageTitle: '',
|
|
40
|
+
headingLevel: 0,
|
|
41
|
+
headingTitle: '',
|
|
42
|
+
summary: '',
|
|
43
|
+
content: '',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { remarkPlugins, rehypePlugins } = markdownPlugins({}, xydSettings)
|
|
47
|
+
|
|
48
|
+
const processor = unified()
|
|
49
|
+
.use(remarkParse)
|
|
50
|
+
.use(remarkPlugins)
|
|
51
|
+
.use(() => {
|
|
52
|
+
return (tree: any) => {
|
|
53
|
+
let currentContent = ''
|
|
54
|
+
|
|
55
|
+
visit(tree, (node) => {
|
|
56
|
+
// Skip yaml frontmatter
|
|
57
|
+
if (node.type === 'yaml') {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle headings
|
|
62
|
+
if (node.type === 'heading') {
|
|
63
|
+
const heading = flattenNode(node)
|
|
64
|
+
|
|
65
|
+
// Save previous section's content
|
|
66
|
+
if (currentSection && currentContent) {
|
|
67
|
+
currentSection.content = currentContent.trim()
|
|
68
|
+
currentContent = ''
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const slug = slugify(heading)
|
|
72
|
+
|
|
73
|
+
if (node.depth === 1) {
|
|
74
|
+
if (rootSection.pageId) {
|
|
75
|
+
console.error("Multiple h1 found")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rootSection.pageId = slug
|
|
79
|
+
rootSection.pageUrl = `/${route}`
|
|
80
|
+
rootSection.pageTitle = heading
|
|
81
|
+
rootSection.headingLevel = 1
|
|
82
|
+
rootSection.headingTitle = heading
|
|
83
|
+
sections.push(rootSection)
|
|
84
|
+
currentSection = rootSection
|
|
85
|
+
} else {
|
|
86
|
+
const section: DocSectionSchema = {
|
|
87
|
+
pageId: rootSection.pageId,
|
|
88
|
+
pageUrl: `${rootSection.pageUrl}#${slug}`, // TODO: QUERY PARAMS
|
|
89
|
+
pageTitle: rootSection.headingTitle,
|
|
90
|
+
headingLevel: node.depth,
|
|
91
|
+
headingTitle: heading,
|
|
92
|
+
content: '',
|
|
93
|
+
summary: '',
|
|
94
|
+
}
|
|
95
|
+
sections.push(section)
|
|
96
|
+
currentSection = section
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Skip if we're not in a section yet
|
|
102
|
+
if (!currentSection) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// For list items, add proper formatting
|
|
107
|
+
if (node.type === 'listItem') {
|
|
108
|
+
const itemContent = flattenNode(node)
|
|
109
|
+
currentContent += `- ${itemContent}\n`
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// For links, preserve the markdown format
|
|
114
|
+
if (node.type === 'link') {
|
|
115
|
+
const text = flattenNode(node)
|
|
116
|
+
currentContent += `[${text}](${node.url})\n`
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// For other nodes, just add their content
|
|
121
|
+
const nodeContent = flattenNode(node)
|
|
122
|
+
if (nodeContent) {
|
|
123
|
+
currentContent += nodeContent + '\n'
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Save the last section's content
|
|
128
|
+
if (currentSection && currentContent) {
|
|
129
|
+
currentSection.content = currentContent.trim()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
.use(remarkRehype)
|
|
134
|
+
.use(rehypePlugins)
|
|
135
|
+
.use(rehypeStringify)
|
|
136
|
+
|
|
137
|
+
await processor.process(content)
|
|
138
|
+
|
|
139
|
+
return sections
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function flattenNode(node: any): string {
|
|
143
|
+
if (node.value) {
|
|
144
|
+
return node.value
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (node.children) {
|
|
148
|
+
return node.children.map((child: any) => flattenNode(child)).join('')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ''
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async function processSections(pages: { name: string, path: string }[], xydSettings: Settings) {
|
|
156
|
+
return (await Promise.all(
|
|
157
|
+
pages.flatMap(async (file, i) => {
|
|
158
|
+
let filePath = ""
|
|
159
|
+
let err
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const p = file.path + ".md"
|
|
163
|
+
statSync(p)
|
|
164
|
+
filePath = p
|
|
165
|
+
} catch (error) {
|
|
166
|
+
err = error
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!filePath) {
|
|
170
|
+
try {
|
|
171
|
+
const p = file.path + ".mdx"
|
|
172
|
+
statSync(p)
|
|
173
|
+
filePath = p
|
|
174
|
+
err = null
|
|
175
|
+
} catch (error) {
|
|
176
|
+
err = error
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (err) {
|
|
181
|
+
console.error(err)
|
|
182
|
+
return []
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
186
|
+
const sections = await mapContentToDocSections(
|
|
187
|
+
xydSettings,
|
|
188
|
+
file.name,
|
|
189
|
+
content,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return sections.flat()
|
|
193
|
+
})
|
|
194
|
+
)).flat()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TODO: !!!! DRY !!!
|
|
198
|
+
function flatPages(
|
|
199
|
+
sidebar: (SidebarRoute | Sidebar | string)[],
|
|
200
|
+
groups: { [key: string]: string },
|
|
201
|
+
resp: string[] = [],
|
|
202
|
+
) {
|
|
203
|
+
sidebar.map(async side => {
|
|
204
|
+
if (typeof side === "string") {
|
|
205
|
+
resp.push(side)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if ("route" in side) {
|
|
210
|
+
side.pages?.map(item => {
|
|
211
|
+
return flatPages([item], groups, resp)
|
|
212
|
+
})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Type guard to check if it's a Sidebar
|
|
217
|
+
if ("group" in side) {
|
|
218
|
+
const groupKey = side.group || "";
|
|
219
|
+
if (groups[groupKey]) {
|
|
220
|
+
const link = groups[groupKey];
|
|
221
|
+
if (link) {
|
|
222
|
+
resp.push(link);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
side.pages?.map(async page => {
|
|
227
|
+
if (typeof page === "string") {
|
|
228
|
+
resp.push(page);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if ("virtual" in page && page.virtual) {
|
|
233
|
+
resp.push(page.virtual);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if ("pages" in page) {
|
|
238
|
+
return flatPages([page as Sidebar], groups, resp);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return resp;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function slugify(text: string) {
|
|
248
|
+
const slugger = new GitHubSlugger()
|
|
249
|
+
|
|
250
|
+
return slugger.slug(text)
|
|
251
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface DocSectionSchema { // TODO: move to @xyd-js/plugins/search ?
|
|
2
|
+
/**
|
|
3
|
+
* Id of the page e.g. "getting-started"
|
|
4
|
+
*/
|
|
5
|
+
pageId: string
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* URL of the page e.g. "/getting-started"
|
|
9
|
+
*/
|
|
10
|
+
pageUrl: string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Title of the page e.g. "Getting Started"
|
|
14
|
+
*/
|
|
15
|
+
pageTitle: string
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Level of the heading e.g. 1, 2
|
|
19
|
+
*/
|
|
20
|
+
headingLevel: number
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Title of the heading e.g. "Making a new project"
|
|
24
|
+
*/
|
|
25
|
+
headingTitle: string
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Summary of the page e.g. "This is the summary of the page"
|
|
29
|
+
*/
|
|
30
|
+
summary: string
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Content of the section
|
|
34
|
+
*/
|
|
35
|
+
content: string
|
|
36
|
+
}
|