@bfra.me/doc-sync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -0
- package/lib/chunk-6NKAJT2M.js +1233 -0
- package/lib/chunk-DR6UG237.js +1027 -0
- package/lib/chunk-G5KKGJYO.js +1560 -0
- package/lib/chunk-ROLA7SBB.js +12 -0
- package/lib/cli/index.d.ts +1 -0
- package/lib/cli/index.js +397 -0
- package/lib/generators/index.d.ts +170 -0
- package/lib/generators/index.js +76 -0
- package/lib/index.d.ts +141 -0
- package/lib/index.js +118 -0
- package/lib/parsers/index.d.ts +264 -0
- package/lib/parsers/index.js +113 -0
- package/lib/types.d.ts +388 -0
- package/lib/types.js +7 -0
- package/package.json +99 -0
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/sync.ts +146 -0
- package/src/cli/commands/validate.ts +151 -0
- package/src/cli/commands/watch.ts +74 -0
- package/src/cli/index.ts +71 -0
- package/src/cli/types.ts +19 -0
- package/src/cli/ui.ts +123 -0
- package/src/generators/api-reference-generator.ts +268 -0
- package/src/generators/code-example-formatter.ts +313 -0
- package/src/generators/component-mapper.ts +383 -0
- package/src/generators/content-merger.ts +295 -0
- package/src/generators/frontmatter-generator.ts +277 -0
- package/src/generators/index.ts +56 -0
- package/src/generators/mdx-generator.ts +289 -0
- package/src/index.ts +131 -0
- package/src/orchestrator/index.ts +21 -0
- package/src/orchestrator/package-scanner.ts +276 -0
- package/src/orchestrator/sync-orchestrator.ts +382 -0
- package/src/orchestrator/validation-pipeline.ts +328 -0
- package/src/parsers/export-analyzer.ts +335 -0
- package/src/parsers/guards.ts +350 -0
- package/src/parsers/index.ts +82 -0
- package/src/parsers/jsdoc-extractor.ts +313 -0
- package/src/parsers/package-info.ts +267 -0
- package/src/parsers/readme-parser.ts +334 -0
- package/src/parsers/typescript-parser.ts +299 -0
- package/src/types.ts +423 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/safe-patterns.ts +280 -0
- package/src/utils/sanitization.ts +164 -0
- package/src/watcher/change-detector.ts +138 -0
- package/src/watcher/debouncer.ts +168 -0
- package/src/watcher/file-watcher.ts +164 -0
- package/src/watcher/index.ts +27 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/generators/code-example-formatter - Syntax-highlighted code blocks from JSDoc examples
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {ExportedFunction, ExportedType, PackageAPI} from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for code example formatting
|
|
9
|
+
*/
|
|
10
|
+
export interface CodeExampleOptions {
|
|
11
|
+
/** Default language for code blocks */
|
|
12
|
+
readonly defaultLanguage?: string
|
|
13
|
+
/** Whether to add function name as title */
|
|
14
|
+
readonly includeTitle?: boolean
|
|
15
|
+
/** Maximum number of examples per function */
|
|
16
|
+
readonly maxExamplesPerFunction?: number
|
|
17
|
+
/** Whether to wrap examples in collapsible sections */
|
|
18
|
+
readonly collapsible?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default options for code example formatting
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_OPTIONS: Required<CodeExampleOptions> = {
|
|
25
|
+
defaultLanguage: 'typescript',
|
|
26
|
+
includeTitle: true,
|
|
27
|
+
maxExamplesPerFunction: 3,
|
|
28
|
+
collapsible: false,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatCodeExamples(api: PackageAPI, options: CodeExampleOptions = {}): string {
|
|
32
|
+
const mergedOptions = {...DEFAULT_OPTIONS, ...options}
|
|
33
|
+
const examples: string[] = []
|
|
34
|
+
|
|
35
|
+
for (const fn of api.functions) {
|
|
36
|
+
const fnExamples = formatFunctionExamples(fn, mergedOptions)
|
|
37
|
+
if (fnExamples.length > 0) {
|
|
38
|
+
examples.push(fnExamples)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const type of api.types) {
|
|
43
|
+
const typeExamples = formatTypeExamples(type, mergedOptions)
|
|
44
|
+
if (typeExamples.length > 0) {
|
|
45
|
+
examples.push(typeExamples)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return examples.join('\n\n')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatFunctionExamples(
|
|
53
|
+
fn: ExportedFunction,
|
|
54
|
+
options: CodeExampleOptions = {},
|
|
55
|
+
): string {
|
|
56
|
+
const mergedOptions = {...DEFAULT_OPTIONS, ...options}
|
|
57
|
+
|
|
58
|
+
if (fn.jsdoc?.examples === undefined || fn.jsdoc.examples.length === 0) {
|
|
59
|
+
return ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const exampleCount = Math.min(fn.jsdoc.examples.length, mergedOptions.maxExamplesPerFunction)
|
|
63
|
+
const examples = fn.jsdoc.examples.slice(0, exampleCount)
|
|
64
|
+
|
|
65
|
+
const sections: string[] = []
|
|
66
|
+
|
|
67
|
+
if (mergedOptions.includeTitle) {
|
|
68
|
+
sections.push(`### ${fn.name}`)
|
|
69
|
+
sections.push('')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [index, example] of examples.entries()) {
|
|
73
|
+
const formattedExample = formatCodeBlock(example, mergedOptions.defaultLanguage)
|
|
74
|
+
|
|
75
|
+
if (mergedOptions.collapsible && examples.length > 1) {
|
|
76
|
+
sections.push(`<details>`)
|
|
77
|
+
sections.push(`<summary>Example ${index + 1}</summary>`)
|
|
78
|
+
sections.push('')
|
|
79
|
+
sections.push(formattedExample)
|
|
80
|
+
sections.push('')
|
|
81
|
+
sections.push(`</details>`)
|
|
82
|
+
} else {
|
|
83
|
+
sections.push(formattedExample)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (index < examples.length - 1) {
|
|
87
|
+
sections.push('')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return sections.join('\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatTypeExamples(type: ExportedType, options: CodeExampleOptions = {}): string {
|
|
95
|
+
const mergedOptions = {...DEFAULT_OPTIONS, ...options}
|
|
96
|
+
|
|
97
|
+
if (type.jsdoc?.examples === undefined || type.jsdoc.examples.length === 0) {
|
|
98
|
+
return ''
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const exampleCount = Math.min(type.jsdoc.examples.length, mergedOptions.maxExamplesPerFunction)
|
|
102
|
+
const examples = type.jsdoc.examples.slice(0, exampleCount)
|
|
103
|
+
|
|
104
|
+
const sections: string[] = []
|
|
105
|
+
|
|
106
|
+
if (mergedOptions.includeTitle) {
|
|
107
|
+
sections.push(`### ${type.name}`)
|
|
108
|
+
sections.push('')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const [index, example] of examples.entries()) {
|
|
112
|
+
const formattedExample = formatCodeBlock(example, mergedOptions.defaultLanguage)
|
|
113
|
+
|
|
114
|
+
if (mergedOptions.collapsible && examples.length > 1) {
|
|
115
|
+
sections.push(`<details>`)
|
|
116
|
+
sections.push(`<summary>Example ${index + 1}</summary>`)
|
|
117
|
+
sections.push('')
|
|
118
|
+
sections.push(formattedExample)
|
|
119
|
+
sections.push('')
|
|
120
|
+
sections.push(`</details>`)
|
|
121
|
+
} else {
|
|
122
|
+
sections.push(formattedExample)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (index < examples.length - 1) {
|
|
126
|
+
sections.push('')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return sections.join('\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatCodeBlock(code: string, language = 'typescript'): string {
|
|
134
|
+
const cleanedCode = cleanCodeExample(code)
|
|
135
|
+
const detectedLanguage = detectLanguage(cleanedCode) ?? language
|
|
136
|
+
|
|
137
|
+
return `\`\`\`${detectedLanguage}\n${cleanedCode}\n\`\`\``
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function cleanCodeExample(code: string): string {
|
|
141
|
+
let cleaned = code.trim()
|
|
142
|
+
|
|
143
|
+
if (cleaned.startsWith('```')) {
|
|
144
|
+
const lines = cleaned.split('\n')
|
|
145
|
+
lines.shift()
|
|
146
|
+
if (lines.at(-1)?.trim() === '```') {
|
|
147
|
+
lines.pop()
|
|
148
|
+
}
|
|
149
|
+
cleaned = lines.join('\n')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
cleaned = cleaned
|
|
153
|
+
.split('\n')
|
|
154
|
+
.map(line => {
|
|
155
|
+
if (line.startsWith(' * ')) {
|
|
156
|
+
return line.slice(3)
|
|
157
|
+
}
|
|
158
|
+
if (line.startsWith('* ')) {
|
|
159
|
+
return line.slice(2)
|
|
160
|
+
}
|
|
161
|
+
return line
|
|
162
|
+
})
|
|
163
|
+
.join('\n')
|
|
164
|
+
|
|
165
|
+
const lines = cleaned.split('\n')
|
|
166
|
+
const nonEmptyLines = lines.filter(line => line.trim().length > 0)
|
|
167
|
+
|
|
168
|
+
if (nonEmptyLines.length === 0) {
|
|
169
|
+
return cleaned
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const minIndent = Math.min(
|
|
173
|
+
...nonEmptyLines.map(line => {
|
|
174
|
+
const match = /^(\s*)/.exec(line)
|
|
175
|
+
return match?.[1]?.length ?? 0
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if (minIndent > 0) {
|
|
180
|
+
cleaned = lines.map(line => line.slice(minIndent)).join('\n')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return cleaned.trim()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function detectLanguage(code: string): string | undefined {
|
|
187
|
+
if (code.includes('import ') || code.includes('export ') || code.includes(': ')) {
|
|
188
|
+
if (code.includes('<') && code.includes('/>')) {
|
|
189
|
+
return 'tsx'
|
|
190
|
+
}
|
|
191
|
+
return 'typescript'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (code.includes('const ') || code.includes('let ') || code.includes('function ')) {
|
|
195
|
+
if (code.includes('<') && code.includes('/>')) {
|
|
196
|
+
return 'jsx'
|
|
197
|
+
}
|
|
198
|
+
return 'javascript'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (code.startsWith('{') || code.startsWith('[')) {
|
|
202
|
+
return 'json'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (code.includes('$ ') || code.includes('npm ') || code.includes('pnpm ')) {
|
|
206
|
+
return 'bash'
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return undefined
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function formatUsageExample(fn: ExportedFunction): string {
|
|
213
|
+
const params = fn.parameters.map(p => {
|
|
214
|
+
if (p.optional && p.defaultValue !== undefined) {
|
|
215
|
+
return `${p.name} = ${p.defaultValue}`
|
|
216
|
+
}
|
|
217
|
+
if (p.optional) {
|
|
218
|
+
return `${p.name}?`
|
|
219
|
+
}
|
|
220
|
+
return p.name
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const call = `${fn.name}(${params.join(', ')})`
|
|
224
|
+
|
|
225
|
+
if (fn.isAsync) {
|
|
226
|
+
return `const result = await ${call}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (fn.returnType !== 'void') {
|
|
230
|
+
return `const result = ${call}`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return call
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function groupExamplesByCategory(
|
|
237
|
+
api: PackageAPI,
|
|
238
|
+
): Map<string, {functions: ExportedFunction[]; types: ExportedType[]}> {
|
|
239
|
+
const categories = new Map<string, {functions: ExportedFunction[]; types: ExportedType[]}>()
|
|
240
|
+
|
|
241
|
+
for (const fn of api.functions) {
|
|
242
|
+
if (fn.jsdoc?.examples === undefined || fn.jsdoc.examples.length === 0) {
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const category = inferCategory(fn.name)
|
|
247
|
+
const existing = categories.get(category) ?? {functions: [], types: []}
|
|
248
|
+
existing.functions.push(fn)
|
|
249
|
+
categories.set(category, existing)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const type of api.types) {
|
|
253
|
+
if (type.jsdoc?.examples === undefined || type.jsdoc.examples.length === 0) {
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const category = inferCategory(type.name)
|
|
258
|
+
const existing = categories.get(category) ?? {functions: [], types: []}
|
|
259
|
+
existing.types.push(type)
|
|
260
|
+
categories.set(category, existing)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return categories
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function inferCategory(name: string): string {
|
|
267
|
+
const prefixes = ['create', 'get', 'set', 'is', 'has', 'parse', 'format', 'validate', 'build']
|
|
268
|
+
|
|
269
|
+
for (const prefix of prefixes) {
|
|
270
|
+
if (name.toLowerCase().startsWith(prefix)) {
|
|
271
|
+
return capitalizeFirst(prefix)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return 'General'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function capitalizeFirst(str: string): string {
|
|
279
|
+
if (str.length === 0) return str
|
|
280
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function formatGroupedExamples(api: PackageAPI, options: CodeExampleOptions = {}): string {
|
|
284
|
+
const categories = groupExamplesByCategory(api)
|
|
285
|
+
const sections: string[] = []
|
|
286
|
+
|
|
287
|
+
for (const [category, items] of categories) {
|
|
288
|
+
sections.push(`### ${category}`)
|
|
289
|
+
sections.push('')
|
|
290
|
+
|
|
291
|
+
for (const fn of items.functions) {
|
|
292
|
+
const examples = formatFunctionExamples(fn, {...options, includeTitle: false})
|
|
293
|
+
if (examples.length > 0) {
|
|
294
|
+
sections.push(`#### \`${fn.name}\``)
|
|
295
|
+
sections.push('')
|
|
296
|
+
sections.push(examples)
|
|
297
|
+
sections.push('')
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const type of items.types) {
|
|
302
|
+
const examples = formatTypeExamples(type, {...options, includeTitle: false})
|
|
303
|
+
if (examples.length > 0) {
|
|
304
|
+
sections.push(`#### \`${type.name}\``)
|
|
305
|
+
sections.push('')
|
|
306
|
+
sections.push(examples)
|
|
307
|
+
sections.push('')
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return sections.join('\n').trim()
|
|
313
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/generators/component-mapper - Map content sections to Starlight components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {PackageInfo, ReadmeContent, ReadmeSection} from '../types'
|
|
6
|
+
|
|
7
|
+
import {sanitizeAttribute} from '../utils/sanitization'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for Starlight component mapping
|
|
11
|
+
*/
|
|
12
|
+
export interface ComponentMapperConfig {
|
|
13
|
+
/** Section titles that should use CardGrid for features */
|
|
14
|
+
readonly featureSections?: readonly string[]
|
|
15
|
+
/** Section titles that should use Tabs for installation */
|
|
16
|
+
readonly tabSections?: readonly string[]
|
|
17
|
+
/** Section titles to exclude from output */
|
|
18
|
+
readonly excludeSections?: readonly string[]
|
|
19
|
+
/** Custom component mappings by section title */
|
|
20
|
+
readonly customMappings?: Record<string, SectionMapper>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A function that maps section content to MDX output
|
|
25
|
+
*/
|
|
26
|
+
export type SectionMapper = (section: ReadmeSection, info: PackageInfo) => string
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default configuration for component mapping
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_CONFIG: Required<Omit<ComponentMapperConfig, 'customMappings'>> = {
|
|
32
|
+
featureSections: ['features', 'highlights', 'key features'],
|
|
33
|
+
tabSections: ['installation', 'getting started', 'setup'],
|
|
34
|
+
excludeSections: ['license', 'contributing', 'contributors', 'changelog'],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function mapToStarlightComponents(
|
|
38
|
+
readme: ReadmeContent,
|
|
39
|
+
packageInfo: PackageInfo,
|
|
40
|
+
config: ComponentMapperConfig = {},
|
|
41
|
+
): string {
|
|
42
|
+
const mergedConfig = {...DEFAULT_CONFIG, ...config}
|
|
43
|
+
const sections: string[] = []
|
|
44
|
+
|
|
45
|
+
if (readme.preamble !== undefined && readme.preamble.trim().length > 0) {
|
|
46
|
+
sections.push(readme.preamble)
|
|
47
|
+
sections.push('')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const section of readme.sections) {
|
|
51
|
+
const output = mapSection(section, packageInfo, mergedConfig)
|
|
52
|
+
if (output.length > 0) {
|
|
53
|
+
sections.push(output)
|
|
54
|
+
sections.push('')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return sections.join('\n').trim()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mapSection(
|
|
62
|
+
section: ReadmeSection,
|
|
63
|
+
packageInfo: PackageInfo,
|
|
64
|
+
config: Required<Omit<ComponentMapperConfig, 'customMappings'>> &
|
|
65
|
+
Pick<ComponentMapperConfig, 'customMappings'>,
|
|
66
|
+
): string {
|
|
67
|
+
const normalizedHeading = section.heading.toLowerCase().trim()
|
|
68
|
+
|
|
69
|
+
if (isExcludedSection(normalizedHeading, config.excludeSections, packageInfo)) {
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (config.customMappings?.[normalizedHeading] !== undefined) {
|
|
74
|
+
return config.customMappings[normalizedHeading](section, packageInfo)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isFeatureSection(normalizedHeading, config.featureSections)) {
|
|
78
|
+
return mapFeatureSection(section)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isInstallationSection(normalizedHeading, config.tabSections)) {
|
|
82
|
+
return mapInstallationSection(section, packageInfo)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return mapDefaultSection(section, packageInfo, config)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isExcludedSection(
|
|
89
|
+
heading: string,
|
|
90
|
+
excludeSections: readonly string[],
|
|
91
|
+
packageInfo: PackageInfo,
|
|
92
|
+
): boolean {
|
|
93
|
+
if (excludeSections.some(excluded => heading.includes(excluded.toLowerCase()))) {
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (packageInfo.docsConfig?.excludeSections !== undefined) {
|
|
98
|
+
return packageInfo.docsConfig.excludeSections.some(excluded =>
|
|
99
|
+
heading.includes(excluded.toLowerCase()),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isFeatureSection(heading: string, featureSections: readonly string[]): boolean {
|
|
107
|
+
return featureSections.some(feature => heading.includes(feature.toLowerCase()))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isInstallationSection(heading: string, tabSections: readonly string[]): boolean {
|
|
111
|
+
return tabSections.some(tab => heading.includes(tab.toLowerCase()))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mapFeatureSection(section: ReadmeSection): string {
|
|
115
|
+
const lines: string[] = []
|
|
116
|
+
|
|
117
|
+
lines.push(`## ${section.heading}`)
|
|
118
|
+
lines.push('')
|
|
119
|
+
|
|
120
|
+
const features = extractFeatureItems(section.content)
|
|
121
|
+
|
|
122
|
+
if (features.length > 0) {
|
|
123
|
+
lines.push('<CardGrid>')
|
|
124
|
+
for (const feature of features) {
|
|
125
|
+
const icon = inferFeatureIcon(feature.title, feature.emoji)
|
|
126
|
+
lines.push(` <Card title="${sanitizeAttribute(feature.title)}" icon="${icon}">`)
|
|
127
|
+
lines.push(` ${feature.description}`)
|
|
128
|
+
lines.push(' </Card>')
|
|
129
|
+
}
|
|
130
|
+
lines.push('</CardGrid>')
|
|
131
|
+
} else {
|
|
132
|
+
lines.push(section.content)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return lines.join('\n')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface FeatureItem {
|
|
139
|
+
readonly title: string
|
|
140
|
+
readonly description: string
|
|
141
|
+
readonly emoji?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const EMOJI_TO_ICON_MAP: Record<string, string> = {
|
|
145
|
+
'📝': 'document',
|
|
146
|
+
'📖': 'document',
|
|
147
|
+
'🔄': 'seti:refresh',
|
|
148
|
+
'👁️': 'eye-open',
|
|
149
|
+
'✨': 'star',
|
|
150
|
+
'🛡️': 'shield',
|
|
151
|
+
'🎨': 'seti:settings',
|
|
152
|
+
'🔒': 'lock',
|
|
153
|
+
} as const
|
|
154
|
+
|
|
155
|
+
function extractFeatureItems(content: string): FeatureItem[] {
|
|
156
|
+
const features: FeatureItem[] = []
|
|
157
|
+
|
|
158
|
+
// Uses specific character classes to prevent catastrophic backtracking
|
|
159
|
+
// Pattern breakdown:
|
|
160
|
+
// ^[-*] - list marker at start of line
|
|
161
|
+
// ([^*\r\n]*) - prefix (no asterisks or newlines)
|
|
162
|
+
// \*\*([^*\r\n]+)\*\* - bold text (must contain non-asterisk chars)
|
|
163
|
+
// [ ]?[:—–-][ ]? - separator with optional spaces
|
|
164
|
+
// ([^\r\n]+) - description (rest of line, no newlines)
|
|
165
|
+
const boldListPattern = /^[-*] ([^*\r\n]*)\*\*([^*\r\n]+)\*\* ?[:—–-] ?([^\r\n]+)$/gm
|
|
166
|
+
|
|
167
|
+
for (const match of content.matchAll(boldListPattern)) {
|
|
168
|
+
if (match[2] !== undefined && match[3] !== undefined) {
|
|
169
|
+
const prefix = match[1] ?? ''
|
|
170
|
+
const emoji = extractEmoji(prefix)
|
|
171
|
+
|
|
172
|
+
features.push({
|
|
173
|
+
title: match[2].trim(),
|
|
174
|
+
description: match[3].trim(),
|
|
175
|
+
emoji,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (features.length === 0) {
|
|
181
|
+
const dashSeparatedPattern = /^[-*] ([^—–\n]+)[—–] (.+)$/gm
|
|
182
|
+
|
|
183
|
+
for (const match of content.matchAll(dashSeparatedPattern)) {
|
|
184
|
+
if (match[1] !== undefined && match[2] !== undefined) {
|
|
185
|
+
const rawTitle = match[1].trim()
|
|
186
|
+
const description = match[2].trim()
|
|
187
|
+
const emoji = extractEmoji(rawTitle)
|
|
188
|
+
const title = rawTitle.replace(
|
|
189
|
+
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+\s*/u,
|
|
190
|
+
'',
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if (title.length > 0 && description.length > 0 && description.length < 300) {
|
|
194
|
+
features.push({title, description, emoji})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Tertiary fallback pattern: any bold text followed by description
|
|
201
|
+
if (features.length === 0) {
|
|
202
|
+
const boldPattern = /\*\*([^*]+)\*\*/g
|
|
203
|
+
const boldMatches = [...content.matchAll(boldPattern)]
|
|
204
|
+
|
|
205
|
+
for (const match of boldMatches) {
|
|
206
|
+
if (match[1] !== undefined) {
|
|
207
|
+
const title = match[1].trim()
|
|
208
|
+
const afterMatch = content.slice((match.index ?? 0) + match[0].length)
|
|
209
|
+
const description = afterMatch.split(/\n\n|\*\*/)[0]?.trim() ?? ''
|
|
210
|
+
|
|
211
|
+
if (description.length > 0 && description.length < 200) {
|
|
212
|
+
features.push({title, description})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return features
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractEmoji(text: string): string | undefined {
|
|
222
|
+
const emojiMatch = text.match(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+/u)
|
|
223
|
+
return emojiMatch?.[0]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function inferFeatureIcon(title: string, emoji?: string): string {
|
|
227
|
+
if (emoji !== undefined && EMOJI_TO_ICON_MAP[emoji] !== undefined) {
|
|
228
|
+
return EMOJI_TO_ICON_MAP[emoji]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const lowerTitle = title.toLowerCase()
|
|
232
|
+
|
|
233
|
+
const keywordIconMap: Record<string, string> = {
|
|
234
|
+
typescript: 'approve-check',
|
|
235
|
+
type: 'approve-check',
|
|
236
|
+
test: 'approve-check',
|
|
237
|
+
fast: 'rocket',
|
|
238
|
+
speed: 'rocket',
|
|
239
|
+
performance: 'rocket',
|
|
240
|
+
tree: 'seti:plan',
|
|
241
|
+
modular: 'puzzle',
|
|
242
|
+
plugin: 'puzzle',
|
|
243
|
+
extensible: 'puzzle',
|
|
244
|
+
document: 'document',
|
|
245
|
+
doc: 'document',
|
|
246
|
+
config: 'setting',
|
|
247
|
+
setting: 'setting',
|
|
248
|
+
star: 'star',
|
|
249
|
+
feature: 'star',
|
|
250
|
+
safe: 'approve-check',
|
|
251
|
+
secure: 'approve-check',
|
|
252
|
+
error: 'warning',
|
|
253
|
+
async: 'rocket',
|
|
254
|
+
result: 'approve-check',
|
|
255
|
+
function: 'puzzle',
|
|
256
|
+
watch: 'eye-open',
|
|
257
|
+
incremental: 'seti:refresh',
|
|
258
|
+
preservation: 'shield',
|
|
259
|
+
mdx: 'document',
|
|
260
|
+
parsing: 'document',
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const [keyword, icon] of Object.entries(keywordIconMap)) {
|
|
264
|
+
if (lowerTitle.includes(keyword)) {
|
|
265
|
+
return icon
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return 'star'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function mapInstallationSection(section: ReadmeSection, packageInfo: PackageInfo): string {
|
|
273
|
+
const lines: string[] = []
|
|
274
|
+
|
|
275
|
+
lines.push(`## ${section.heading}`)
|
|
276
|
+
lines.push('')
|
|
277
|
+
|
|
278
|
+
// Use word boundaries to avoid matching 'npm' within 'pnpm'
|
|
279
|
+
const hasPnpm = /\bpnpm\b/.test(section.content)
|
|
280
|
+
const hasNpm = /\bnpm\b/.test(section.content)
|
|
281
|
+
const hasYarn = /\byarn\b/.test(section.content)
|
|
282
|
+
const packageManagerCount = [hasPnpm, hasNpm, hasYarn].filter(Boolean).length
|
|
283
|
+
|
|
284
|
+
const hasExistingTabs = section.content.includes('```bash')
|
|
285
|
+
const hasMultiplePackageManagers = packageManagerCount >= 2
|
|
286
|
+
|
|
287
|
+
if (hasExistingTabs && hasMultiplePackageManagers) {
|
|
288
|
+
lines.push(section.content)
|
|
289
|
+
} else {
|
|
290
|
+
lines.push(generateInstallTabs(packageInfo.name))
|
|
291
|
+
|
|
292
|
+
const contentWithoutInstall = removeInstallCommand(section.content)
|
|
293
|
+
if (contentWithoutInstall.trim().length > 0) {
|
|
294
|
+
lines.push('')
|
|
295
|
+
lines.push(contentWithoutInstall)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return lines.join('\n')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function generateInstallTabs(packageName: string): string {
|
|
303
|
+
return `<Tabs>
|
|
304
|
+
<TabItem label="pnpm">
|
|
305
|
+
\`\`\`bash
|
|
306
|
+
pnpm add ${packageName}
|
|
307
|
+
\`\`\`
|
|
308
|
+
</TabItem>
|
|
309
|
+
<TabItem label="npm">
|
|
310
|
+
\`\`\`bash
|
|
311
|
+
npm install ${packageName}
|
|
312
|
+
\`\`\`
|
|
313
|
+
</TabItem>
|
|
314
|
+
<TabItem label="yarn">
|
|
315
|
+
\`\`\`bash
|
|
316
|
+
yarn add ${packageName}
|
|
317
|
+
\`\`\`
|
|
318
|
+
</TabItem>
|
|
319
|
+
</Tabs>`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function removeInstallCommand(content: string): string {
|
|
323
|
+
return content
|
|
324
|
+
.replaceAll(/```(?:bash|sh)\n(?:npm|pnpm|yarn)\s+(?:install|add|i)\s+\S+\n```/g, '')
|
|
325
|
+
.trim()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function mapDefaultSection(
|
|
329
|
+
section: ReadmeSection,
|
|
330
|
+
packageInfo: PackageInfo,
|
|
331
|
+
config: Required<Omit<ComponentMapperConfig, 'customMappings'>> &
|
|
332
|
+
Pick<ComponentMapperConfig, 'customMappings'>,
|
|
333
|
+
): string {
|
|
334
|
+
const lines: string[] = []
|
|
335
|
+
|
|
336
|
+
const headingLevel = '#'.repeat(Math.min(section.level + 1, 6))
|
|
337
|
+
lines.push(`${headingLevel} ${section.heading}`)
|
|
338
|
+
lines.push('')
|
|
339
|
+
lines.push(section.content)
|
|
340
|
+
|
|
341
|
+
for (const child of section.children) {
|
|
342
|
+
const childOutput = mapSection(child, packageInfo, config)
|
|
343
|
+
if (childOutput.length > 0) {
|
|
344
|
+
lines.push('')
|
|
345
|
+
lines.push(childOutput)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return lines.join('\n')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function createBadge(
|
|
353
|
+
text: string,
|
|
354
|
+
variant: 'note' | 'tip' | 'caution' | 'danger' | 'success' | 'default' = 'note',
|
|
355
|
+
): string {
|
|
356
|
+
return `<Badge text="${sanitizeAttribute(text)}" variant="${variant}" />`
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function createCard(title: string, content: string, icon?: string): string {
|
|
360
|
+
const iconAttr = icon === undefined ? '' : ` icon="${icon}"`
|
|
361
|
+
return `<Card title="${sanitizeAttribute(title)}"${iconAttr}>
|
|
362
|
+
${content}
|
|
363
|
+
</Card>`
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function createCardGrid(cards: {title: string; content: string; icon?: string}[]): string {
|
|
367
|
+
const cardElements = cards.map(card => createCard(card.title, card.content, card.icon))
|
|
368
|
+
return `<CardGrid>
|
|
369
|
+
${cardElements.map(c => ` ${c}`).join('\n')}
|
|
370
|
+
</CardGrid>`
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function createTabs(items: {label: string; content: string}[]): string {
|
|
374
|
+
const tabItems = items.map(
|
|
375
|
+
item => ` <TabItem label="${sanitizeAttribute(item.label)}">
|
|
376
|
+
${item.content}
|
|
377
|
+
</TabItem>`,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return `<Tabs>
|
|
381
|
+
${tabItems.join('\n')}
|
|
382
|
+
</Tabs>`
|
|
383
|
+
}
|