@bfra.me/doc-sync 0.1.0 → 0.1.2

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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @bfra.me/doc-sync/utils/safe-patterns - Safe regex patterns and utilities for MDX/HTML parsing
3
+ * All patterns are designed to prevent ReDoS attacks
4
+ */
5
+ /**
6
+ * Create safe heading pattern for specific level
7
+ * Uses explicit character class instead of greedy `.+` to prevent ReDoS
8
+ *
9
+ * @param level - Heading level (1-6)
10
+ * @returns Safe regex pattern for the heading level
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const h2Pattern = createHeadingPattern(2)
15
+ * const matches = content.match(h2Pattern)
16
+ * ```
17
+ */
18
+ declare function createHeadingPattern(level: number): RegExp;
19
+ /**
20
+ * Check if content contains a specific JSX component
21
+ * Uses a safe pattern that avoids catastrophic backtracking
22
+ *
23
+ * @param content - The MDX/HTML content to search
24
+ * @param componentName - Name of the component to find (e.g., 'Card', 'Badge')
25
+ * @returns True if the component is found
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const hasCard = hasComponent(content, 'Card')
30
+ * const hasCardGrid = hasComponent(content, 'CardGrid')
31
+ * ```
32
+ */
33
+ declare function hasComponent(content: string, componentName: string): boolean;
34
+ /**
35
+ * Extract code blocks and inline code from markdown content using unified/remark (safe, no regex)
36
+ * This approach uses AST parsing instead of regex to avoid ReDoS vulnerabilities
37
+ *
38
+ * @param content - The markdown content to parse
39
+ * @returns Array of code block strings (fenced blocks with backticks, inline code with backticks)
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const blocks = extractCodeBlocks(content)
44
+ * for (const block of blocks) {
45
+ * console.log(block)
46
+ * }
47
+ * ```
48
+ */
49
+ declare function extractCodeBlocks(content: string): readonly string[];
50
+ /**
51
+ * Parse JSX tags from content using a safe, non-backtracking approach.
52
+ * Uses a state machine instead of regex to prevent ReDoS.
53
+ *
54
+ * @param content - The MDX/HTML content to parse
55
+ * @returns Array of matched JSX tags with their positions
56
+ */
57
+ declare function parseJSXTags(content: string): readonly {
58
+ tag: string;
59
+ index: number;
60
+ isClosing: boolean;
61
+ isSelfClosing: boolean;
62
+ }[];
63
+ /**
64
+ * Find empty markdown links in content using safe parsing.
65
+ * Uses indexOf-based scanning instead of regex to prevent ReDoS.
66
+ *
67
+ * @param content - The markdown content to check
68
+ * @returns Array of positions where empty links were found
69
+ */
70
+ declare function findEmptyMarkdownLinks(content: string): readonly number[];
71
+
72
+ /**
73
+ * @bfra.me/doc-sync/utils/sanitization - Sanitization utilities for MDX content
74
+ * Provides comprehensive XSS prevention for user-generated content
75
+ */
76
+ /**
77
+ * Sanitize HTML content for MDX context
78
+ * Escapes all HTML entities and JSX curly braces to prevent XSS
79
+ *
80
+ * @param content - The content to sanitize
81
+ * @returns Sanitized content safe for MDX rendering
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const safe = sanitizeForMDX('<script>alert("xss")</script>')
86
+ * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
87
+ * ```
88
+ */
89
+ declare function sanitizeForMDX(content: string): string;
90
+ /**
91
+ * Sanitize value for use in HTML/JSX attribute
92
+ * Uses escape-html library for proper attribute encoding
93
+ *
94
+ * @param value - The attribute value to sanitize
95
+ * @returns Sanitized value safe for attribute context
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * const safe = sanitizeAttribute('value" onload="alert(1)')
100
+ * // Returns: 'value&quot; onload=&quot;alert(1)'
101
+ * ```
102
+ */
103
+ declare function sanitizeAttribute(value: string): string;
104
+ /**
105
+ * JSX attribute parsed from a tag
106
+ */
107
+ interface JSXAttribute {
108
+ readonly name: string;
109
+ readonly value: string | null;
110
+ }
111
+ /**
112
+ * Parse JSX tag attributes safely without using complex regex
113
+ * Uses a simple state machine approach to avoid ReDoS vulnerabilities
114
+ *
115
+ * @param tag - The complete JSX tag string (e.g., '<Badge text="hello" />')
116
+ * @returns Array of parsed attributes
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const attrs = parseJSXAttributes('<Card title="Hello" icon="star" />')
121
+ * // Returns: [{name: 'title', value: 'Hello'}, {name: 'icon', value: 'star'}]
122
+ * ```
123
+ */
124
+ declare function parseJSXAttributes(tag: string): readonly JSXAttribute[];
125
+ /**
126
+ * Sanitize a complete JSX tag including all attributes
127
+ * Parses the tag and escapes all attribute values to prevent XSS
128
+ *
129
+ * @param tag - The complete JSX tag string
130
+ * @returns Sanitized JSX tag safe for rendering
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * const safe = sanitizeJSXTag('<Badge text="v1.0.0" onclick="alert(1)" />')
135
+ * // Returns: '<Badge text="v1.0.0" onclick="alert(1)" />' (with escaped values)
136
+ * ```
137
+ */
138
+ declare function sanitizeJSXTag(tag: string): string;
139
+
140
+ export { createHeadingPattern, extractCodeBlocks, findEmptyMarkdownLinks, hasComponent, parseJSXAttributes, parseJSXTags, sanitizeAttribute, sanitizeForMDX, sanitizeJSXTag };
@@ -0,0 +1,24 @@
1
+ import "../chunk-DRBRT57F.js";
2
+ import {
3
+ createHeadingPattern,
4
+ extractCodeBlocks,
5
+ findEmptyMarkdownLinks,
6
+ hasComponent,
7
+ parseJSXAttributes,
8
+ parseJSXTags,
9
+ sanitizeAttribute,
10
+ sanitizeForMDX,
11
+ sanitizeJSXTag
12
+ } from "../chunk-GZ2MP3VN.js";
13
+ export {
14
+ createHeadingPattern,
15
+ extractCodeBlocks,
16
+ findEmptyMarkdownLinks,
17
+ hasComponent,
18
+ parseJSXAttributes,
19
+ parseJSXTags,
20
+ sanitizeAttribute,
21
+ sanitizeForMDX,
22
+ sanitizeJSXTag
23
+ };
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,62 @@
1
+ import { FileChangeEvent, PackageInfo } from '../types.js';
2
+ import '@bfra.me/es/result';
3
+ import 'zod';
4
+
5
+ interface DocWatcherOptions {
6
+ readonly rootDir?: string;
7
+ readonly debounceMs?: number;
8
+ readonly additionalIgnore?: readonly string[];
9
+ readonly usePolling?: boolean;
10
+ }
11
+ type DocChangeHandler = (events: readonly FileChangeEvent[]) => void | Promise<void>;
12
+ interface DocFileWatcher {
13
+ readonly start: () => Promise<void>;
14
+ readonly close: () => Promise<void>;
15
+ readonly onChanges: (handler: DocChangeHandler) => () => void;
16
+ readonly getWatchedPaths: () => readonly string[];
17
+ }
18
+ declare function createDocWatcher(options?: DocWatcherOptions): DocFileWatcher;
19
+ type FileCategory = 'readme' | 'source' | 'package-json' | 'unknown';
20
+ declare function categorizeFile(filePath: string): FileCategory;
21
+ declare function groupChangesByPackage(events: readonly FileChangeEvent[]): Map<string, FileChangeEvent[]>;
22
+ declare function filterDocumentationChanges(events: readonly FileChangeEvent[]): FileChangeEvent[];
23
+
24
+ interface DocChangeDetectorOptions {
25
+ readonly algorithm?: 'sha256' | 'md5';
26
+ }
27
+ interface PackageChangeAnalysis {
28
+ readonly packageName: string;
29
+ readonly needsRegeneration: boolean;
30
+ readonly changedCategories: readonly FileCategory[];
31
+ readonly changedFiles: readonly string[];
32
+ }
33
+ interface DocChangeDetector {
34
+ readonly hasChanged: (filePath: string) => Promise<boolean>;
35
+ readonly record: (filePath: string) => Promise<void>;
36
+ readonly recordPackage: (pkg: PackageInfo, files: readonly string[]) => Promise<void>;
37
+ readonly clear: (filePath: string) => void;
38
+ readonly clearAll: () => void;
39
+ readonly analyzeChanges: (events: readonly FileChangeEvent[]) => Promise<PackageChangeAnalysis[]>;
40
+ }
41
+ declare function createDocChangeDetector(options?: DocChangeDetectorOptions): DocChangeDetector;
42
+ type RegenerationScope = 'full' | 'api-only' | 'readme-only' | 'metadata-only' | 'none';
43
+ declare function determineRegenerationScope(changedCategories: readonly FileCategory[]): RegenerationScope;
44
+ declare function hasAnyFileChanged(detector: DocChangeDetector, files: readonly string[]): Promise<boolean>;
45
+
46
+ interface DocDebouncerOptions {
47
+ readonly debounceMs?: number;
48
+ readonly maxWaitMs?: number;
49
+ }
50
+ type BatchChangeHandler = (events: readonly FileChangeEvent[]) => void | Promise<void>;
51
+ interface DocDebouncer {
52
+ readonly add: (event: FileChangeEvent) => void;
53
+ readonly addAll: (events: readonly FileChangeEvent[]) => void;
54
+ readonly flush: () => void;
55
+ readonly cancel: () => void;
56
+ readonly getPendingCount: () => number;
57
+ }
58
+ declare function createDocDebouncer(handler: BatchChangeHandler, options?: DocDebouncerOptions): DocDebouncer;
59
+ declare function deduplicateEvents(events: readonly FileChangeEvent[]): FileChangeEvent[];
60
+ declare function consolidateEvents(events: readonly FileChangeEvent[]): FileChangeEvent[];
61
+
62
+ export { type BatchChangeHandler, type DocChangeDetector, type DocChangeDetectorOptions, type DocChangeHandler, type DocDebouncer, type DocDebouncerOptions, type DocFileWatcher, type DocWatcherOptions, type FileCategory, type PackageChangeAnalysis, type RegenerationScope, categorizeFile, consolidateEvents, createDocChangeDetector, createDocDebouncer, createDocWatcher, deduplicateEvents, determineRegenerationScope, filterDocumentationChanges, groupChangesByPackage, hasAnyFileChanged };
@@ -0,0 +1,25 @@
1
+ import {
2
+ categorizeFile,
3
+ consolidateEvents,
4
+ createDocChangeDetector,
5
+ createDocDebouncer,
6
+ createDocWatcher,
7
+ deduplicateEvents,
8
+ determineRegenerationScope,
9
+ filterDocumentationChanges,
10
+ groupChangesByPackage,
11
+ hasAnyFileChanged
12
+ } from "../chunk-45NROJIG.js";
13
+ export {
14
+ categorizeFile,
15
+ consolidateEvents,
16
+ createDocChangeDetector,
17
+ createDocDebouncer,
18
+ createDocWatcher,
19
+ deduplicateEvents,
20
+ determineRegenerationScope,
21
+ filterDocumentationChanges,
22
+ groupChangesByPackage,
23
+ hasAnyFileChanged
24
+ };
25
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bfra.me/doc-sync",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Intelligent documentation synchronization engine for automatic Astro Starlight site updates",
5
5
  "keywords": [
6
6
  "astro",
@@ -35,6 +35,11 @@
35
35
  "source": "./src/generators/index.ts",
36
36
  "import": "./lib/generators/index.js"
37
37
  },
38
+ "./orchestrator": {
39
+ "types": "./lib/orchestrator/index.d.ts",
40
+ "source": "./src/orchestrator/index.ts",
41
+ "import": "./lib/orchestrator/index.js"
42
+ },
38
43
  "./parsers": {
39
44
  "types": "./lib/parsers/index.d.ts",
40
45
  "source": "./src/parsers/index.ts",
@@ -45,6 +50,16 @@
45
50
  "source": "./src/types.ts",
46
51
  "import": "./lib/types.js"
47
52
  },
53
+ "./utils": {
54
+ "types": "./lib/utils/index.d.ts",
55
+ "source": "./src/utils/index.ts",
56
+ "import": "./lib/utils/index.js"
57
+ },
58
+ "./watcher": {
59
+ "types": "./lib/watcher/index.d.ts",
60
+ "source": "./src/watcher/index.ts",
61
+ "import": "./lib/watcher/index.js"
62
+ },
48
63
  "./package.json": "./package.json"
49
64
  },
50
65
  "main": "./lib/index.js",
@@ -65,7 +80,7 @@
65
80
  "fast-glob": "3.3.3",
66
81
  "remark-mdx": "3.1.1",
67
82
  "remark-parse": "11.0.0",
68
- "ts-morph": "26.0.0",
83
+ "ts-morph": "27.0.2",
69
84
  "unified": "11.0.5",
70
85
  "zod": "4.1.13",
71
86
  "@bfra.me/es": "0.1.0"
@@ -111,6 +111,16 @@ function isInstallationSection(heading: string, tabSections: readonly string[]):
111
111
  return tabSections.some(tab => heading.includes(tab.toLowerCase()))
112
112
  }
113
113
 
114
+ /**
115
+ * Escape angle brackets in text to prevent MDX JSX tag misinterpretation
116
+ * This is applied to section content to prevent TypeScript generics like Result<T, E>
117
+ * from being interpreted as unclosed JSX tags
118
+ */
119
+ function escapeAngleBrackets(text: string): string {
120
+ // Escape all < and > to HTML entities
121
+ return text.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
122
+ }
123
+
114
124
  function mapFeatureSection(section: ReadmeSection): string {
115
125
  const lines: string[] = []
116
126
 
@@ -124,7 +134,7 @@ function mapFeatureSection(section: ReadmeSection): string {
124
134
  for (const feature of features) {
125
135
  const icon = inferFeatureIcon(feature.title, feature.emoji)
126
136
  lines.push(` <Card title="${sanitizeAttribute(feature.title)}" icon="${icon}">`)
127
- lines.push(` ${feature.description}`)
137
+ lines.push(` ${escapeAngleBrackets(feature.description)}`)
128
138
  lines.push(' </Card>')
129
139
  }
130
140
  lines.push('</CardGrid>')
@@ -169,15 +169,12 @@ function renderMDXDocument(frontmatter: MDXFrontmatter, content: string): string
169
169
  * Prevents XSS by escaping potentially dangerous content
170
170
  */
171
171
  export function sanitizeContent(content: string): string {
172
- // Use comprehensive sanitization from utils
173
172
  return sanitizeForMDX(content)
174
173
  }
175
174
 
176
175
  /**
177
176
  * Sanitizes content within MDX while preserving JSX components
178
- * Only escapes content that appears to be user-provided text
179
- * Now includes sanitization of JSX component attributes to prevent XSS
180
- * Uses safe, non-backtracking parsing to prevent ReDoS
177
+ * Sanitizes JSX component attributes to prevent XSS while leaving closing tags unchanged
181
178
  */
182
179
  export function sanitizeTextContent(content: string): string {
183
180
  const jsxTags = parseJSXTags(content)
@@ -189,12 +186,7 @@ export function sanitizeTextContent(content: string): string {
189
186
  parts.push(sanitizeContent(content.slice(lastIndex, index)))
190
187
  }
191
188
 
192
- if (isClosing) {
193
- parts.push(tag)
194
- } else {
195
- parts.push(sanitizeJSXTag(tag))
196
- }
197
-
189
+ parts.push(isClosing ? tag : sanitizeJSXTag(tag))
198
190
  lastIndex = index + tag.length
199
191
  }
200
192
 
@@ -225,23 +217,32 @@ export function validateMDXSyntax(mdx: string): Result<true, SyncError> {
225
217
  return ok(true)
226
218
  }
227
219
 
220
+ /**
221
+ * Checks if a tag name is likely a TypeScript generic parameter rather than a JSX component
222
+ * Single uppercase letters (T, E, K, V, etc.) are common generic type parameters
223
+ */
224
+ function isTypeScriptGeneric(tag: string): boolean {
225
+ const tagNameMatch = tag.match(/<\/?([A-Z][a-zA-Z0-9]*)/)
226
+ const tagName = tagNameMatch?.[1]
227
+ return tagName !== undefined && tagName.length === 1
228
+ }
229
+
228
230
  function checkForUnclosedTags(mdx: string): string[] {
229
231
  const unclosed: string[] = []
230
232
  const tagStack: string[] = []
231
233
 
232
- // Remove code blocks from content before checking for JSX tags
233
- // This prevents TypeScript generics like Result<T, E> from being
234
- // misinterpreted as unclosed JSX tags
234
+ // Remove code blocks and inline code to prevent TypeScript generics like Result<T, E>
235
+ // from being misinterpreted as JSX tags
235
236
  const codeBlocks = extractCodeBlocks(mdx)
236
- let contentWithoutCodeBlocks = mdx
237
+ let contentWithoutCode = mdx
237
238
  for (const block of codeBlocks) {
238
- // Replace code block with empty lines to preserve line numbers
239
239
  const lineCount = block.split('\n').length
240
240
  const placeholder = '\n'.repeat(lineCount)
241
- contentWithoutCodeBlocks = contentWithoutCodeBlocks.replace(block, placeholder)
241
+ contentWithoutCode = contentWithoutCode.replace(block, placeholder)
242
242
  }
243
243
 
244
- const jsxTags = parseJSXTags(contentWithoutCodeBlocks)
244
+ const allJSXTags = parseJSXTags(contentWithoutCode)
245
+ const jsxTags = allJSXTags.filter(({tag}) => !isTypeScriptGeneric(tag))
245
246
 
246
247
  for (const {tag, isClosing, isSelfClosing} of jsxTags) {
247
248
  const tagNameMatch = isClosing
package/src/index.ts CHANGED
@@ -71,6 +71,75 @@ export type {
71
71
  ValidationWarning,
72
72
  } from './orchestrator'
73
73
 
74
+ // Re-export parsers
75
+ export {
76
+ analyzePublicAPI,
77
+ analyzeTypeScriptContent,
78
+ analyzeTypeScriptFile,
79
+ assertPackageAPI,
80
+ assertPackageInfo,
81
+ assertParseError,
82
+ buildDocSlug,
83
+ createProject,
84
+ extractDocsConfig,
85
+ extractExportedFunctions,
86
+ extractExportedTypes,
87
+ extractJSDocInfo,
88
+ extractPackageAPI,
89
+ extractReExports,
90
+ findEntryPoint,
91
+ findExportedSymbols,
92
+ findReadmePath,
93
+ findSection,
94
+ flattenSections,
95
+ getExportedSymbolInfo,
96
+ getExportsByKind,
97
+ getPackageScope,
98
+ getSectionsByLevel,
99
+ getTableOfContents,
100
+ getUnscopedName,
101
+ hasJSDoc,
102
+ isDocConfigSource,
103
+ isExportedFunction,
104
+ isExportedType,
105
+ isJSDocInfo,
106
+ isJSDocParam,
107
+ isJSDocTag,
108
+ isMDXFrontmatter,
109
+ isPackageAPI,
110
+ isPackageInfo,
111
+ isParseError,
112
+ isReadmeContent,
113
+ isReadmeSection,
114
+ isReExport,
115
+ isSafeContent,
116
+ isSafeFilePath,
117
+ isSymbolExported,
118
+ isSyncError,
119
+ isValidHeadingLevel,
120
+ isValidPackageName,
121
+ isValidSemver,
122
+ parseJSDoc,
123
+ parsePackageComplete,
124
+ parsePackageJson,
125
+ parsePackageJsonContent,
126
+ parseReadme,
127
+ parseReadmeFile,
128
+ parseSourceContent,
129
+ parseSourceFile,
130
+ } from './parsers'
131
+
132
+ export type {
133
+ ExportAnalyzerOptions,
134
+ JSDocableDeclaration,
135
+ PackageInfoOptions,
136
+ PackageJsonSchema,
137
+ PublicAPIAnalysis,
138
+ ReadmeParserOptions,
139
+ ResolvedExport,
140
+ TypeScriptParserOptions,
141
+ } from './parsers'
142
+
74
143
  export type {
75
144
  CLIOptions,
76
145
  DocConfig,
@@ -102,6 +171,19 @@ export type {
102
171
 
103
172
  export {SENTINEL_MARKERS} from './types'
104
173
 
174
+ // Re-export utils
175
+ export {
176
+ createHeadingPattern,
177
+ extractCodeBlocks,
178
+ findEmptyMarkdownLinks,
179
+ hasComponent,
180
+ parseJSXAttributes,
181
+ parseJSXTags,
182
+ sanitizeAttribute,
183
+ sanitizeForMDX,
184
+ sanitizeJSXTag,
185
+ } from './utils'
186
+
105
187
  // Re-export watcher
106
188
  export {
107
189
  categorizeFile,
@@ -183,12 +183,12 @@ function extractTextFromNode(node: RootContent): string {
183
183
 
184
184
  function serializeNode(node: RootContent): string {
185
185
  if (node.type === 'paragraph') {
186
- return extractTextFromNode(node)
186
+ return serializeInlineContent(node)
187
187
  }
188
188
 
189
189
  if (node.type === 'heading') {
190
190
  const prefix = '#'.repeat(node.depth)
191
- return `${prefix} ${extractTextFromNode(node)}`
191
+ return `${prefix} ${serializeInlineContent(node)}`
192
192
  }
193
193
 
194
194
  if (node.type === 'code') {
@@ -227,6 +227,59 @@ function serializeNode(node: RootContent): string {
227
227
  return extractTextFromNode(node)
228
228
  }
229
229
 
230
+ /**
231
+ * Serialize inline content preserving markdown formatting like **bold**, *italic*, `code`, etc.
232
+ */
233
+ function serializeInlineContent(node: RootContent): string {
234
+ if ('value' in node && typeof node.value === 'string') {
235
+ return node.value
236
+ }
237
+
238
+ if (!('children' in node) || !Array.isArray(node.children)) {
239
+ return ''
240
+ }
241
+
242
+ return (node.children as RootContent[])
243
+ .map(child => {
244
+ // Handle strong (bold) text
245
+ if (child.type === 'strong') {
246
+ return `**${serializeInlineContent(child)}**`
247
+ }
248
+
249
+ // Handle emphasis (italic) text
250
+ if (child.type === 'emphasis') {
251
+ return `*${serializeInlineContent(child)}*`
252
+ }
253
+
254
+ // Handle inline code
255
+ if (child.type === 'inlineCode') {
256
+ return `\`${'value' in child ? child.value : ''}\``
257
+ }
258
+
259
+ // Handle links
260
+ if (child.type === 'link') {
261
+ const text = serializeInlineContent(child)
262
+ return `[${text}](${'url' in child ? child.url : ''})`
263
+ }
264
+
265
+ // Handle images
266
+ if (child.type === 'image') {
267
+ const alt = 'alt' in child ? child.alt : ''
268
+ const url = 'url' in child ? child.url : ''
269
+ return `![${alt}](${url})`
270
+ }
271
+
272
+ // Handle plain text
273
+ if ('value' in child && typeof child.value === 'string') {
274
+ return child.value
275
+ }
276
+
277
+ // Recursively handle other inline elements
278
+ return serializeInlineContent(child)
279
+ })
280
+ .join('')
281
+ }
282
+
230
283
  function serializeTable(node: RootContent): string {
231
284
  if (node.type !== 'table' || !('children' in node)) {
232
285
  return ''
@@ -51,11 +51,11 @@ export function hasComponent(content: string, componentName: string): boolean {
51
51
  }
52
52
 
53
53
  /**
54
- * Extract code blocks from markdown content using unified/remark (safe, no regex)
54
+ * Extract code blocks and inline code from markdown content using unified/remark (safe, no regex)
55
55
  * This approach uses AST parsing instead of regex to avoid ReDoS vulnerabilities
56
56
  *
57
57
  * @param content - The markdown content to parse
58
- * @returns Array of code block strings with their language identifiers
58
+ * @returns Array of code block strings (fenced blocks with backticks, inline code with backticks)
59
59
  *
60
60
  * @example
61
61
  * ```ts
@@ -91,6 +91,10 @@ export function extractCodeBlocks(content: string): readonly string[] {
91
91
  const value = node.value ?? ''
92
92
  blocks.push(`\`\`\`${lang}\n${value}\n\`\`\``)
93
93
  }
94
+ if (node.type === 'inlineCode') {
95
+ const value = node.value ?? ''
96
+ blocks.push(`\`${value}\``)
97
+ }
94
98
  if (Array.isArray(node.children)) {
95
99
  for (const child of node.children) {
96
100
  visit(child)