@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/generators/content-merger - Preserve manual sections using sentinel markers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {Result} from '@bfra.me/es/result'
|
|
6
|
+
import type {SyncError} from '../types'
|
|
7
|
+
|
|
8
|
+
import {err, ok} from '@bfra.me/es/result'
|
|
9
|
+
import {SENTINEL_MARKERS} from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A section of content extracted from MDX
|
|
13
|
+
*/
|
|
14
|
+
export interface ContentSection {
|
|
15
|
+
/** Section type */
|
|
16
|
+
readonly type: 'auto' | 'manual' | 'unchanged'
|
|
17
|
+
/** Section content */
|
|
18
|
+
readonly content: string
|
|
19
|
+
/** Start position in source */
|
|
20
|
+
readonly startIndex: number
|
|
21
|
+
/** End position in source */
|
|
22
|
+
readonly endIndex: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Merge options
|
|
27
|
+
*/
|
|
28
|
+
export interface MergeOptions {
|
|
29
|
+
/** Strategy for handling conflicts */
|
|
30
|
+
readonly conflictStrategy?: 'keep-manual' | 'keep-auto' | 'merge'
|
|
31
|
+
/** Preserve empty manual sections */
|
|
32
|
+
readonly preserveEmptyManual?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of a merge operation
|
|
37
|
+
*/
|
|
38
|
+
export interface MergeResult {
|
|
39
|
+
/** Merged content */
|
|
40
|
+
readonly content: string
|
|
41
|
+
/** Number of manual sections preserved */
|
|
42
|
+
readonly preservedCount: number
|
|
43
|
+
/** Number of auto-generated sections updated */
|
|
44
|
+
readonly updatedCount: number
|
|
45
|
+
/** Whether content changed */
|
|
46
|
+
readonly hasChanges: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function mergeContent(
|
|
50
|
+
existingContent: string,
|
|
51
|
+
newContent: string,
|
|
52
|
+
options: MergeOptions = {},
|
|
53
|
+
): Result<MergeResult, SyncError> {
|
|
54
|
+
const {conflictStrategy = 'keep-manual', preserveEmptyManual = true} = options
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const existingManualSections = extractManualSections(existingContent)
|
|
58
|
+
const newAutoSections = extractAutoSections(newContent)
|
|
59
|
+
|
|
60
|
+
if (existingManualSections.length === 0 && newAutoSections.length === 0) {
|
|
61
|
+
const hasChanges = existingContent.trim() !== newContent.trim()
|
|
62
|
+
return ok({
|
|
63
|
+
content: newContent,
|
|
64
|
+
preservedCount: 0,
|
|
65
|
+
updatedCount: hasChanges ? 1 : 0,
|
|
66
|
+
hasChanges,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let merged = newContent
|
|
71
|
+
let preservedCount = 0
|
|
72
|
+
|
|
73
|
+
for (const manual of existingManualSections) {
|
|
74
|
+
if (!preserveEmptyManual && manual.content.trim().length === 0) {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const manualMarker = `${SENTINEL_MARKERS.MANUAL_START}\n${manual.content}\n${SENTINEL_MARKERS.MANUAL_END}`
|
|
79
|
+
|
|
80
|
+
if (!merged.includes(SENTINEL_MARKERS.MANUAL_START)) {
|
|
81
|
+
const autoEndIndex = merged.indexOf(SENTINEL_MARKERS.AUTO_END)
|
|
82
|
+
if (autoEndIndex === -1) {
|
|
83
|
+
merged = `${merged}\n\n${manualMarker}`
|
|
84
|
+
} else {
|
|
85
|
+
merged = `${merged.slice(0, autoEndIndex + SENTINEL_MARKERS.AUTO_END.length)}\n\n${manualMarker}\n${merged.slice(autoEndIndex + SENTINEL_MARKERS.AUTO_END.length)}`
|
|
86
|
+
}
|
|
87
|
+
} else if (conflictStrategy === 'keep-manual') {
|
|
88
|
+
const existingManualInNew = extractManualSections(merged)
|
|
89
|
+
if (existingManualInNew.length > 0) {
|
|
90
|
+
const firstManualInNew = existingManualInNew[0]
|
|
91
|
+
if (firstManualInNew !== undefined) {
|
|
92
|
+
merged = replaceSection(merged, firstManualInNew, manual.content)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
preservedCount++
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hasChanges = existingContent.trim() !== merged.trim()
|
|
101
|
+
|
|
102
|
+
return ok({
|
|
103
|
+
content: merged,
|
|
104
|
+
preservedCount,
|
|
105
|
+
updatedCount: newAutoSections.length,
|
|
106
|
+
hasChanges,
|
|
107
|
+
})
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return err({
|
|
110
|
+
code: 'GENERATION_ERROR',
|
|
111
|
+
message: `Failed to merge content: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
cause: error,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function extractManualSections(content: string): ContentSection[] {
|
|
118
|
+
const sections: ContentSection[] = []
|
|
119
|
+
let searchStart = 0
|
|
120
|
+
|
|
121
|
+
while (searchStart < content.length) {
|
|
122
|
+
const startIndex = content.indexOf(SENTINEL_MARKERS.MANUAL_START, searchStart)
|
|
123
|
+
if (startIndex === -1) break
|
|
124
|
+
|
|
125
|
+
const endIndex = content.indexOf(SENTINEL_MARKERS.MANUAL_END, startIndex)
|
|
126
|
+
if (endIndex === -1) break
|
|
127
|
+
|
|
128
|
+
const contentStart = startIndex + SENTINEL_MARKERS.MANUAL_START.length
|
|
129
|
+
const sectionContent = content.slice(contentStart, endIndex).trim()
|
|
130
|
+
|
|
131
|
+
sections.push({
|
|
132
|
+
type: 'manual',
|
|
133
|
+
content: sectionContent,
|
|
134
|
+
startIndex,
|
|
135
|
+
endIndex: endIndex + SENTINEL_MARKERS.MANUAL_END.length,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
searchStart = endIndex + SENTINEL_MARKERS.MANUAL_END.length
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return sections
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function extractAutoSections(content: string): ContentSection[] {
|
|
145
|
+
const sections: ContentSection[] = []
|
|
146
|
+
let searchStart = 0
|
|
147
|
+
|
|
148
|
+
while (searchStart < content.length) {
|
|
149
|
+
const startIndex = content.indexOf(SENTINEL_MARKERS.AUTO_START, searchStart)
|
|
150
|
+
if (startIndex === -1) break
|
|
151
|
+
|
|
152
|
+
const endIndex = content.indexOf(SENTINEL_MARKERS.AUTO_END, startIndex)
|
|
153
|
+
if (endIndex === -1) break
|
|
154
|
+
|
|
155
|
+
const contentStart = startIndex + SENTINEL_MARKERS.AUTO_START.length
|
|
156
|
+
const sectionContent = content.slice(contentStart, endIndex).trim()
|
|
157
|
+
|
|
158
|
+
sections.push({
|
|
159
|
+
type: 'auto',
|
|
160
|
+
content: sectionContent,
|
|
161
|
+
startIndex,
|
|
162
|
+
endIndex: endIndex + SENTINEL_MARKERS.AUTO_END.length,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
searchStart = endIndex + SENTINEL_MARKERS.AUTO_END.length
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return sections
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function replaceSection(content: string, section: ContentSection, newContent: string): string {
|
|
172
|
+
const marker =
|
|
173
|
+
section.type === 'manual' ? SENTINEL_MARKERS.MANUAL_START : SENTINEL_MARKERS.AUTO_START
|
|
174
|
+
const endMarker =
|
|
175
|
+
section.type === 'manual' ? SENTINEL_MARKERS.MANUAL_END : SENTINEL_MARKERS.AUTO_END
|
|
176
|
+
|
|
177
|
+
const before = content.slice(0, section.startIndex)
|
|
178
|
+
const after = content.slice(section.endIndex)
|
|
179
|
+
|
|
180
|
+
return `${before}${marker}\n${newContent}\n${endMarker}${after}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function hasManualContent(content: string): boolean {
|
|
184
|
+
return content.includes(SENTINEL_MARKERS.MANUAL_START)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function hasAutoContent(content: string): boolean {
|
|
188
|
+
return content.includes(SENTINEL_MARKERS.AUTO_START)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function wrapManualSection(content: string): string {
|
|
192
|
+
return `${SENTINEL_MARKERS.MANUAL_START}\n${content}\n${SENTINEL_MARKERS.MANUAL_END}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function wrapAutoSection(content: string): string {
|
|
196
|
+
return `${SENTINEL_MARKERS.AUTO_START}\n${content}\n${SENTINEL_MARKERS.AUTO_END}`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function stripSentinelMarkers(content: string): string {
|
|
200
|
+
return content
|
|
201
|
+
.replaceAll(SENTINEL_MARKERS.AUTO_START, '')
|
|
202
|
+
.replaceAll(SENTINEL_MARKERS.AUTO_END, '')
|
|
203
|
+
.replaceAll(SENTINEL_MARKERS.MANUAL_START, '')
|
|
204
|
+
.replaceAll(SENTINEL_MARKERS.MANUAL_END, '')
|
|
205
|
+
.replaceAll(/\n{3,}/g, '\n\n')
|
|
206
|
+
.trim()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function validateMarkerPairing(content: string): Result<true, SyncError> {
|
|
210
|
+
const autoStartCount = countOccurrences(content, SENTINEL_MARKERS.AUTO_START)
|
|
211
|
+
const autoEndCount = countOccurrences(content, SENTINEL_MARKERS.AUTO_END)
|
|
212
|
+
const manualStartCount = countOccurrences(content, SENTINEL_MARKERS.MANUAL_START)
|
|
213
|
+
const manualEndCount = countOccurrences(content, SENTINEL_MARKERS.MANUAL_END)
|
|
214
|
+
|
|
215
|
+
if (autoStartCount !== autoEndCount) {
|
|
216
|
+
return err({
|
|
217
|
+
code: 'VALIDATION_ERROR',
|
|
218
|
+
message: `Mismatched AUTO-GENERATED markers: ${autoStartCount} starts, ${autoEndCount} ends`,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (manualStartCount !== manualEndCount) {
|
|
223
|
+
return err({
|
|
224
|
+
code: 'VALIDATION_ERROR',
|
|
225
|
+
message: `Mismatched MANUAL-CONTENT markers: ${manualStartCount} starts, ${manualEndCount} ends`,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const autoNesting = checkNesting(content, SENTINEL_MARKERS.AUTO_START, SENTINEL_MARKERS.AUTO_END)
|
|
230
|
+
if (!autoNesting) {
|
|
231
|
+
return err({
|
|
232
|
+
code: 'VALIDATION_ERROR',
|
|
233
|
+
message: 'AUTO-GENERATED markers are improperly nested',
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const manualNesting = checkNesting(
|
|
238
|
+
content,
|
|
239
|
+
SENTINEL_MARKERS.MANUAL_START,
|
|
240
|
+
SENTINEL_MARKERS.MANUAL_END,
|
|
241
|
+
)
|
|
242
|
+
if (!manualNesting) {
|
|
243
|
+
return err({
|
|
244
|
+
code: 'VALIDATION_ERROR',
|
|
245
|
+
message: 'MANUAL-CONTENT markers are improperly nested',
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return ok(true)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function countOccurrences(content: string, substring: string): number {
|
|
253
|
+
let count = 0
|
|
254
|
+
let position = content.indexOf(substring, 0)
|
|
255
|
+
|
|
256
|
+
while (position !== -1) {
|
|
257
|
+
count++
|
|
258
|
+
position = content.indexOf(substring, position + substring.length)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return count
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function checkNesting(content: string, startMarker: string, endMarker: string): boolean {
|
|
265
|
+
let depth = 0
|
|
266
|
+
let position = 0
|
|
267
|
+
|
|
268
|
+
while (position < content.length) {
|
|
269
|
+
const startPos = content.indexOf(startMarker, position)
|
|
270
|
+
const endPos = content.indexOf(endMarker, position)
|
|
271
|
+
|
|
272
|
+
if (startPos === -1 && endPos === -1) break
|
|
273
|
+
|
|
274
|
+
if (startPos !== -1 && (endPos === -1 || startPos < endPos)) {
|
|
275
|
+
depth++
|
|
276
|
+
position = startPos + startMarker.length
|
|
277
|
+
} else {
|
|
278
|
+
depth--
|
|
279
|
+
if (depth < 0) return false
|
|
280
|
+
position = endPos + endMarker.length
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return depth === 0
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function createDiffSummary(oldContent: string, newContent: string): string {
|
|
288
|
+
const oldLines = oldContent.split('\n')
|
|
289
|
+
const newLines = newContent.split('\n')
|
|
290
|
+
|
|
291
|
+
const added = newLines.filter(line => !oldLines.includes(line)).length
|
|
292
|
+
const removed = oldLines.filter(line => !newLines.includes(line)).length
|
|
293
|
+
|
|
294
|
+
return `${added} lines added, ${removed} lines removed`
|
|
295
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/generators/frontmatter-generator - Starlight-compatible frontmatter generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {MDXFrontmatter, PackageInfo, ReadmeContent} from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mutable frontmatter for internal construction before freezing
|
|
9
|
+
*/
|
|
10
|
+
interface MutableFrontmatter {
|
|
11
|
+
title: string
|
|
12
|
+
description?: string
|
|
13
|
+
sidebar?: MDXFrontmatter['sidebar']
|
|
14
|
+
tableOfContents?: MDXFrontmatter['tableOfContents']
|
|
15
|
+
template?: MDXFrontmatter['template']
|
|
16
|
+
hero?: MDXFrontmatter['hero']
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateFrontmatter(
|
|
20
|
+
packageInfo: PackageInfo,
|
|
21
|
+
readme: ReadmeContent | undefined,
|
|
22
|
+
overrides?: Partial<MDXFrontmatter>,
|
|
23
|
+
): MDXFrontmatter {
|
|
24
|
+
const title = resolveTitle(packageInfo, readme, overrides?.title)
|
|
25
|
+
const description = resolveDescription(packageInfo, readme, overrides?.description)
|
|
26
|
+
|
|
27
|
+
const frontmatter: MutableFrontmatter = {
|
|
28
|
+
title,
|
|
29
|
+
...(description !== undefined && {description}),
|
|
30
|
+
...(overrides?.sidebar !== undefined && {sidebar: overrides.sidebar}),
|
|
31
|
+
...(overrides?.tableOfContents !== undefined && {tableOfContents: overrides.tableOfContents}),
|
|
32
|
+
...(overrides?.template !== undefined && {template: overrides.template}),
|
|
33
|
+
...(overrides?.hero !== undefined && {hero: overrides.hero}),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (packageInfo.docsConfig?.sidebar !== undefined) {
|
|
37
|
+
frontmatter.sidebar = {
|
|
38
|
+
...packageInfo.docsConfig.sidebar,
|
|
39
|
+
...frontmatter.sidebar,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (packageInfo.docsConfig?.frontmatter !== undefined) {
|
|
44
|
+
const customFrontmatter = packageInfo.docsConfig.frontmatter
|
|
45
|
+
return {...customFrontmatter, ...frontmatter} as MDXFrontmatter
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return frontmatter as MDXFrontmatter
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveTitle(
|
|
52
|
+
packageInfo: PackageInfo,
|
|
53
|
+
readme: ReadmeContent | undefined,
|
|
54
|
+
override?: string,
|
|
55
|
+
): string {
|
|
56
|
+
if (override !== undefined) {
|
|
57
|
+
return override
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (packageInfo.docsConfig?.title !== undefined) {
|
|
61
|
+
return packageInfo.docsConfig.title
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (readme?.title !== undefined) {
|
|
65
|
+
return readme.title
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return packageInfo.name
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveDescription(
|
|
72
|
+
packageInfo: PackageInfo,
|
|
73
|
+
readme: ReadmeContent | undefined,
|
|
74
|
+
override?: string,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
if (override !== undefined) {
|
|
77
|
+
return override
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (packageInfo.docsConfig?.description !== undefined) {
|
|
81
|
+
return packageInfo.docsConfig.description
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (packageInfo.description !== undefined) {
|
|
85
|
+
return packageInfo.description
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (readme?.preamble !== undefined) {
|
|
89
|
+
return extractFirstSentence(readme.preamble)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractFirstSentence(text: string): string {
|
|
96
|
+
const cleaned = text.trim().replaceAll(/\s+/g, ' ')
|
|
97
|
+
|
|
98
|
+
const sentenceEnd = cleaned.search(/[.!?](?:\s|$)/)
|
|
99
|
+
if (sentenceEnd > 0) {
|
|
100
|
+
return cleaned.slice(0, sentenceEnd + 1)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (cleaned.length > 160) {
|
|
104
|
+
const truncated = cleaned.slice(0, 157)
|
|
105
|
+
const lastSpace = truncated.lastIndexOf(' ')
|
|
106
|
+
if (lastSpace > 100) {
|
|
107
|
+
return `${truncated.slice(0, lastSpace)}...`
|
|
108
|
+
}
|
|
109
|
+
return `${truncated}...`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return cleaned
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function stringifyFrontmatter(frontmatter: MDXFrontmatter): string {
|
|
116
|
+
const lines: string[] = []
|
|
117
|
+
|
|
118
|
+
lines.push(`title: ${yamlString(frontmatter.title)}`)
|
|
119
|
+
|
|
120
|
+
if (frontmatter.description !== undefined) {
|
|
121
|
+
lines.push(`description: ${yamlString(frontmatter.description)}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (frontmatter.sidebar !== undefined) {
|
|
125
|
+
lines.push('sidebar:')
|
|
126
|
+
const sidebar = frontmatter.sidebar
|
|
127
|
+
|
|
128
|
+
if (sidebar.label !== undefined) {
|
|
129
|
+
lines.push(` label: ${yamlString(sidebar.label)}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (sidebar.order !== undefined) {
|
|
133
|
+
lines.push(` order: ${sidebar.order}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (sidebar.hidden !== undefined) {
|
|
137
|
+
lines.push(` hidden: ${sidebar.hidden}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (sidebar.badge !== undefined) {
|
|
141
|
+
if (typeof sidebar.badge === 'string') {
|
|
142
|
+
lines.push(` badge: ${yamlString(sidebar.badge)}`)
|
|
143
|
+
} else {
|
|
144
|
+
lines.push(' badge:')
|
|
145
|
+
lines.push(` text: ${yamlString(sidebar.badge.text)}`)
|
|
146
|
+
if (sidebar.badge.variant !== undefined) {
|
|
147
|
+
lines.push(` variant: ${sidebar.badge.variant}`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (frontmatter.tableOfContents !== undefined) {
|
|
154
|
+
if (typeof frontmatter.tableOfContents === 'boolean') {
|
|
155
|
+
lines.push(`tableOfContents: ${frontmatter.tableOfContents}`)
|
|
156
|
+
} else {
|
|
157
|
+
lines.push('tableOfContents:')
|
|
158
|
+
if (frontmatter.tableOfContents.minHeadingLevel !== undefined) {
|
|
159
|
+
lines.push(` minHeadingLevel: ${frontmatter.tableOfContents.minHeadingLevel}`)
|
|
160
|
+
}
|
|
161
|
+
if (frontmatter.tableOfContents.maxHeadingLevel !== undefined) {
|
|
162
|
+
lines.push(` maxHeadingLevel: ${frontmatter.tableOfContents.maxHeadingLevel}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (frontmatter.template !== undefined) {
|
|
168
|
+
lines.push(`template: ${frontmatter.template}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (frontmatter.hero !== undefined) {
|
|
172
|
+
lines.push('hero:')
|
|
173
|
+
const hero = frontmatter.hero
|
|
174
|
+
|
|
175
|
+
if (hero.title !== undefined) {
|
|
176
|
+
lines.push(` title: ${yamlString(hero.title)}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (hero.tagline !== undefined) {
|
|
180
|
+
lines.push(` tagline: ${yamlString(hero.tagline)}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (hero.image !== undefined) {
|
|
184
|
+
lines.push(' image:')
|
|
185
|
+
lines.push(` src: ${yamlString(hero.image.src)}`)
|
|
186
|
+
lines.push(` alt: ${yamlString(hero.image.alt)}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (hero.actions !== undefined && hero.actions.length > 0) {
|
|
190
|
+
lines.push(' actions:')
|
|
191
|
+
for (const action of hero.actions) {
|
|
192
|
+
lines.push(` - text: ${yamlString(action.text)}`)
|
|
193
|
+
lines.push(` link: ${yamlString(action.link)}`)
|
|
194
|
+
if (action.icon !== undefined) {
|
|
195
|
+
lines.push(` icon: ${yamlString(action.icon)}`)
|
|
196
|
+
}
|
|
197
|
+
if (action.variant !== undefined) {
|
|
198
|
+
lines.push(` variant: ${action.variant}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return lines.join('\n')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function yamlString(value: string): string {
|
|
208
|
+
if (needsQuoting(value)) {
|
|
209
|
+
return `'${value.replaceAll("'", "''")}'`
|
|
210
|
+
}
|
|
211
|
+
return value
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function needsQuoting(value: string): boolean {
|
|
215
|
+
if (value.length === 0) {
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (value.startsWith(' ') || value.endsWith(' ')) {
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const specialChars = /[:#{}[\]&*?|<>=!%@`"',]/
|
|
224
|
+
if (specialChars.test(value)) {
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const reserved = ['true', 'false', 'null', 'yes', 'no', 'on', 'off']
|
|
229
|
+
if (reserved.includes(value.toLowerCase())) {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Mutable frontmatter for internal parsing
|
|
242
|
+
*/
|
|
243
|
+
interface MutableParsedFrontmatter {
|
|
244
|
+
title?: string
|
|
245
|
+
description?: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function parseFrontmatter(yaml: string): Partial<MDXFrontmatter> {
|
|
249
|
+
const result: MutableParsedFrontmatter = {}
|
|
250
|
+
const lines = yaml.split('\n')
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
const trimmedLine = line.trim()
|
|
254
|
+
|
|
255
|
+
if (trimmedLine.startsWith('title:')) {
|
|
256
|
+
const value = trimmedLine.slice(6).trim()
|
|
257
|
+
result.title = stripQuotes(value)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (trimmedLine.startsWith('description:')) {
|
|
261
|
+
const value = trimmedLine.slice(12).trim()
|
|
262
|
+
result.description = stripQuotes(value)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return result as Partial<MDXFrontmatter>
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function stripQuotes(value: string): string {
|
|
270
|
+
if (
|
|
271
|
+
(value.startsWith("'") && value.endsWith("'")) ||
|
|
272
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
273
|
+
) {
|
|
274
|
+
return value.slice(1, -1)
|
|
275
|
+
}
|
|
276
|
+
return value
|
|
277
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bfra.me/doc-sync/generators - Unified export of all generator modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
generateAPICompact,
|
|
7
|
+
generateAPIReference,
|
|
8
|
+
generateCategoryReference,
|
|
9
|
+
} from './api-reference-generator'
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
cleanCodeExample,
|
|
13
|
+
detectLanguage,
|
|
14
|
+
formatCodeBlock,
|
|
15
|
+
formatCodeExamples,
|
|
16
|
+
formatFunctionExamples,
|
|
17
|
+
formatGroupedExamples,
|
|
18
|
+
formatTypeExamples,
|
|
19
|
+
formatUsageExample,
|
|
20
|
+
groupExamplesByCategory,
|
|
21
|
+
} from './code-example-formatter'
|
|
22
|
+
export type {CodeExampleOptions} from './code-example-formatter'
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
createBadge,
|
|
26
|
+
createCard,
|
|
27
|
+
createCardGrid,
|
|
28
|
+
createTabs,
|
|
29
|
+
generateInstallTabs,
|
|
30
|
+
mapToStarlightComponents,
|
|
31
|
+
} from './component-mapper'
|
|
32
|
+
export type {ComponentMapperConfig, SectionMapper} from './component-mapper'
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
createDiffSummary,
|
|
36
|
+
extractAutoSections,
|
|
37
|
+
extractManualSections,
|
|
38
|
+
hasAutoContent,
|
|
39
|
+
hasManualContent,
|
|
40
|
+
mergeContent,
|
|
41
|
+
stripSentinelMarkers,
|
|
42
|
+
validateMarkerPairing,
|
|
43
|
+
wrapAutoSection,
|
|
44
|
+
wrapManualSection,
|
|
45
|
+
} from './content-merger'
|
|
46
|
+
export type {ContentSection, MergeOptions, MergeResult} from './content-merger'
|
|
47
|
+
|
|
48
|
+
export {generateFrontmatter, parseFrontmatter, stringifyFrontmatter} from './frontmatter-generator'
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
generateMDXDocument,
|
|
52
|
+
sanitizeContent,
|
|
53
|
+
sanitizeTextContent,
|
|
54
|
+
validateMDXSyntax,
|
|
55
|
+
} from './mdx-generator'
|
|
56
|
+
export type {MDXGeneratorOptions} from './mdx-generator'
|