docusaurus-plugin-llms 0.2.2 → 0.3.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/lib/processor.js CHANGED
@@ -61,15 +61,39 @@ async function processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix = 'doc
61
61
  if (data.draft === true) {
62
62
  return null;
63
63
  }
64
+ // Validate and clean empty frontmatter fields
65
+ // Empty strings should be treated as undefined to allow fallback logic
66
+ if (data.title !== undefined && !(0, utils_1.isNonEmptyString)(data.title)) {
67
+ utils_1.logger.warn(`Empty title in frontmatter for ${filePath}. Using fallback.`);
68
+ data.title = undefined;
69
+ }
70
+ if (data.description !== undefined && !(0, utils_1.isNonEmptyString)(data.description)) {
71
+ data.description = undefined;
72
+ }
73
+ if (data.slug !== undefined && !(0, utils_1.isNonEmptyString)(data.slug)) {
74
+ data.slug = undefined;
75
+ }
76
+ if (data.id !== undefined && !(0, utils_1.isNonEmptyString)(data.id)) {
77
+ data.id = undefined;
78
+ }
64
79
  // Resolve partial imports before processing
65
80
  const resolvedContent = await (0, utils_1.resolvePartialImports)(markdownContent, filePath);
66
81
  const relativePath = path.relative(baseDir, filePath);
67
82
  // Convert to URL path format (replace backslashes with forward slashes on Windows)
68
- const normalizedPath = relativePath.replace(/\\/g, '/');
83
+ const normalizedPath = (0, utils_1.normalizePath)(relativePath);
69
84
  let fullUrl;
70
- if (resolvedUrl) {
85
+ if ((0, utils_1.isNonEmptyString)(resolvedUrl)) {
71
86
  // Use the actual resolved URL from Docusaurus if provided
72
- fullUrl = new URL(resolvedUrl, siteUrl).toString();
87
+ try {
88
+ fullUrl = new URL(resolvedUrl, siteUrl).toString();
89
+ }
90
+ catch (error) {
91
+ utils_1.logger.warn(`Invalid URL construction: ${resolvedUrl} with base ${siteUrl}. Using fallback.`);
92
+ // Fallback to string concatenation with proper path joining
93
+ const baseUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
94
+ const urlPath = resolvedUrl.startsWith('/') ? resolvedUrl : `/${resolvedUrl}`;
95
+ fullUrl = baseUrl + urlPath;
96
+ }
73
97
  }
74
98
  else {
75
99
  // Fallback to the old path construction method
@@ -95,15 +119,49 @@ async function processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix = 'doc
95
119
  if (pathPrefix && pathTransformation?.ignorePaths?.includes(pathPrefix)) {
96
120
  transformedPathPrefix = '';
97
121
  }
98
- // Generate full URL with transformed path and path prefix
99
- fullUrl = new URL(`${transformedPathPrefix ? `${transformedPathPrefix}/` : ''}${transformedLinkPath}`, siteUrl).toString();
122
+ // Ensure path segments are URL-safe with sophisticated encoding detection
123
+ const encodedLinkPath = transformedLinkPath.split('/').map(segment => {
124
+ // Check if segment contains characters that need encoding
125
+ // Unreserved characters (per RFC 3986): A-Z a-z 0-9 - . _ ~
126
+ if (!/[^A-Za-z0-9\-._~]/.test(segment)) {
127
+ // Segment only contains unreserved characters, no encoding needed
128
+ return segment;
129
+ }
130
+ try {
131
+ // Try to decode - if it changes, it was already encoded
132
+ const decoded = decodeURIComponent(segment);
133
+ if (decoded !== segment) {
134
+ // Was already encoded, return as-is
135
+ return segment;
136
+ }
137
+ // Not encoded, encode it
138
+ return encodeURIComponent(segment);
139
+ }
140
+ catch {
141
+ // Malformed encoding, re-encode
142
+ return encodeURIComponent(segment);
143
+ }
144
+ }).join('/');
145
+ // Construct URL by encoding path components, then combine with site URL
146
+ // We don't use URL constructor for the full path because it decodes some characters
147
+ const pathPart = transformedPathPrefix ? `${transformedPathPrefix}/${encodedLinkPath}` : encodedLinkPath;
148
+ try {
149
+ const baseUrl = new URL(siteUrl);
150
+ fullUrl = `${baseUrl.origin}/${pathPart}`;
151
+ }
152
+ catch (error) {
153
+ utils_1.logger.warn(`Invalid siteUrl: ${siteUrl}. Using fallback.`);
154
+ // Fallback to string concatenation with proper path joining
155
+ const baseUrl = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
156
+ fullUrl = `${baseUrl}/${pathPart}`;
157
+ }
100
158
  }
101
159
  // Extract title
102
160
  const title = (0, utils_1.extractTitle)(data, resolvedContent, filePath);
103
161
  // Get description from frontmatter or first paragraph
104
162
  let description = '';
105
163
  // First priority: Use frontmatter description if available
106
- if (data.description) {
164
+ if ((0, utils_1.isNonEmptyString)(data.description)) {
107
165
  description = data.description;
108
166
  }
109
167
  else {
@@ -127,13 +185,13 @@ async function processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix = 'doc
127
185
  }
128
186
  // Only remove heading markers at the beginning of descriptions or lines
129
187
  // This preserves # characters that are part of the content
130
- if (description) {
188
+ if ((0, utils_1.isNonEmptyString)(description)) {
131
189
  // Original approach had issues with hashtags inside content
132
190
  // Fix: Only remove # symbols at the beginning of lines or description
133
191
  // that are followed by a space (actual heading markers)
134
192
  description = description.replace(/^(#+)\s+/gm, '');
135
193
  // Special handling for description frontmatter with heading markers
136
- if (data.description && data.description.startsWith('#')) {
194
+ if ((0, utils_1.isNonEmptyString)(data.description) && data.description.startsWith('#')) {
137
195
  // If the description in frontmatter starts with a heading marker,
138
196
  // we should preserve it in the extracted description
139
197
  description = description.replace(/^#+\s+/, '');
@@ -142,15 +200,15 @@ async function processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix = 'doc
142
200
  // We don't want to treat hashtags in the middle of content as headings
143
201
  // Validate that the description doesn't contain markdown headings
144
202
  if (description.match(/^#+\s+/m)) {
145
- console.warn(`Warning: Description for "${title}" may still contain heading markers`);
203
+ utils_1.logger.warn(`Warning: Description for "${title}" may still contain heading markers`);
146
204
  }
147
205
  // Warn if the description contains HTML tags
148
206
  if (/<[^>]+>/g.test(description)) {
149
- console.warn(`Warning: Description for "${title}" contains HTML tags`);
207
+ utils_1.logger.warn(`Warning: Description for "${title}" contains HTML tags`);
150
208
  }
151
209
  // Warn if the description is very long
152
210
  if (description.length > 500) {
153
- console.warn(`Warning: Description for "${title}" is very long (${description.length} characters)`);
211
+ utils_1.logger.warn(`Warning: Description for "${title}" is very long (${description.length} characters)`);
154
212
  }
155
213
  }
156
214
  // Clean and process content (now with partials already resolved)
@@ -164,6 +222,110 @@ async function processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix = 'doc
164
222
  frontMatter: data,
165
223
  };
166
224
  }
225
+ /**
226
+ * Remove numbered prefixes from path segments (e.g., "01-intro" -> "intro")
227
+ */
228
+ function removeNumberedPrefixes(path) {
229
+ return path.split('/').map(segment => {
230
+ // Remove numbered prefixes like "01-", "1-", "001-" from each segment
231
+ return segment.replace(/^\d+-/, '');
232
+ }).join('/');
233
+ }
234
+ /**
235
+ * Try to find a route in the route map from a list of possible paths
236
+ */
237
+ function findRouteInMap(routeMap, possiblePaths) {
238
+ for (const possiblePath of possiblePaths) {
239
+ const route = routeMap.get(possiblePath) || routeMap.get(possiblePath + '/');
240
+ if (route) {
241
+ return route;
242
+ }
243
+ }
244
+ return undefined;
245
+ }
246
+ /**
247
+ * Try exact match for route resolution
248
+ */
249
+ function tryExactRouteMatch(routeMap, relativePath, pathPrefix) {
250
+ const possiblePaths = [
251
+ `/${pathPrefix}/${relativePath}`,
252
+ `/${relativePath}`,
253
+ ];
254
+ return findRouteInMap(routeMap, possiblePaths);
255
+ }
256
+ /**
257
+ * Try route resolution with numbered prefix removal
258
+ */
259
+ function tryNumberedPrefixResolution(routeMap, relativePath, pathPrefix) {
260
+ const cleanPath = removeNumberedPrefixes(relativePath);
261
+ // Try basic cleaned path
262
+ const basicPaths = [`/${pathPrefix}/${cleanPath}`, `/${cleanPath}`];
263
+ const basicMatch = findRouteInMap(routeMap, basicPaths);
264
+ if (basicMatch) {
265
+ return basicMatch;
266
+ }
267
+ // Try nested folder structures with numbered prefixes at different levels
268
+ const segments = relativePath.split('/');
269
+ if (segments.length > 1) {
270
+ for (let i = 0; i < segments.length; i++) {
271
+ const modifiedSegments = [...segments];
272
+ modifiedSegments[i] = modifiedSegments[i].replace(/^\d+-/, '');
273
+ const modifiedPath = modifiedSegments.join('/');
274
+ const pathsToTry = [`/${pathPrefix}/${modifiedPath}`, `/${modifiedPath}`];
275
+ const match = findRouteInMap(routeMap, pathsToTry);
276
+ if (match) {
277
+ return match;
278
+ }
279
+ }
280
+ }
281
+ return undefined;
282
+ }
283
+ /**
284
+ * Try finding best match using routes paths array
285
+ */
286
+ function tryRoutesPathsMatch(routesPaths, relativePath, pathPrefix) {
287
+ const cleanPath = removeNumberedPrefixes(relativePath);
288
+ const normalizedCleanPath = cleanPath.toLowerCase();
289
+ return routesPaths.find(routePath => {
290
+ const normalizedRoute = routePath.toLowerCase();
291
+ return normalizedRoute.endsWith(`/${normalizedCleanPath}`) ||
292
+ normalizedRoute === `/${pathPrefix}/${normalizedCleanPath}` ||
293
+ normalizedRoute === `/${normalizedCleanPath}`;
294
+ });
295
+ }
296
+ /**
297
+ * Resolve the URL for a document using Docusaurus routes
298
+ * @param filePath - Full path to the file
299
+ * @param baseDir - Base directory (typically siteDir)
300
+ * @param pathPrefix - Path prefix ('docs' or 'blog')
301
+ * @param context - Plugin context with route map
302
+ * @returns Resolved URL or undefined if not found
303
+ */
304
+ function resolveDocumentUrl(filePath, baseDir, pathPrefix, context) {
305
+ // Early return if no route map available
306
+ if (!context.routeMap) {
307
+ return undefined;
308
+ }
309
+ // Convert file path to a potential route path
310
+ const relativePath = (0, utils_1.normalizePath)(path.relative(baseDir, filePath))
311
+ .replace(/\.mdx?$/, '')
312
+ .replace(/\/index$/, '');
313
+ // Try exact match first (respects Docusaurus's resolved routes)
314
+ const exactMatch = tryExactRouteMatch(context.routeMap, relativePath, pathPrefix);
315
+ if (exactMatch) {
316
+ return exactMatch;
317
+ }
318
+ // Try numbered prefix removal as fallback
319
+ const prefixMatch = tryNumberedPrefixResolution(context.routeMap, relativePath, pathPrefix);
320
+ if (prefixMatch) {
321
+ return prefixMatch;
322
+ }
323
+ // Try to find the best match using the routesPaths array
324
+ if (context.routesPaths) {
325
+ return tryRoutesPathsMatch(context.routesPaths, relativePath, pathPrefix);
326
+ }
327
+ return undefined;
328
+ }
167
329
  /**
168
330
  * Process files based on include patterns, ignore patterns, and ordering
169
331
  * @param context - Plugin context
@@ -174,21 +336,44 @@ async function processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix = 'doc
174
336
  * @param includeUnmatched - Whether to include unmatched files
175
337
  * @returns Processed files
176
338
  */
339
+ /**
340
+ * Helper function to check if a file matches a pattern
341
+ * Tries matching against multiple path variants for better usability
342
+ */
343
+ function matchesPattern(file, pattern, siteDir, docsDir) {
344
+ const minimatchOptions = { matchBase: true };
345
+ // Get site-relative path (e.g., "docs/quickstart/file.md")
346
+ const siteRelativePath = (0, utils_1.normalizePath)(path.relative(siteDir, file));
347
+ // Get docs-relative path (e.g., "quickstart/file.md")
348
+ // Normalize both paths to handle different path separators and resolve any .. or .
349
+ const docsBaseDir = path.resolve(path.join(siteDir, docsDir));
350
+ const resolvedFile = path.resolve(file);
351
+ const docsRelativePath = resolvedFile.startsWith(docsBaseDir)
352
+ ? (0, utils_1.normalizePath)(path.relative(docsBaseDir, resolvedFile))
353
+ : null;
354
+ // Try matching against site-relative path
355
+ if ((0, minimatch_1.minimatch)(siteRelativePath, pattern, minimatchOptions)) {
356
+ return true;
357
+ }
358
+ // Try matching against docs-relative path if available
359
+ if (docsRelativePath && (0, minimatch_1.minimatch)(docsRelativePath, pattern, minimatchOptions)) {
360
+ return true;
361
+ }
362
+ return false;
363
+ }
177
364
  async function processFilesWithPatterns(context, allFiles, includePatterns = [], ignorePatterns = [], orderPatterns = [], includeUnmatched = false) {
178
365
  const { siteDir, siteUrl, docsDir } = context;
179
366
  // Filter files based on include patterns
180
367
  let filteredFiles = allFiles;
181
368
  if (includePatterns.length > 0) {
182
369
  filteredFiles = allFiles.filter(file => {
183
- const relativePath = path.relative(siteDir, file);
184
- return includePatterns.some(pattern => (0, minimatch_1.minimatch)(relativePath, pattern, { matchBase: true }));
370
+ return includePatterns.some(pattern => matchesPattern(file, pattern, siteDir, docsDir));
185
371
  });
186
372
  }
187
373
  // Apply ignore patterns
188
374
  if (ignorePatterns.length > 0) {
189
375
  filteredFiles = filteredFiles.filter(file => {
190
- const relativePath = path.relative(siteDir, file);
191
- return !ignorePatterns.some(pattern => (0, minimatch_1.minimatch)(relativePath, pattern, { matchBase: true }));
376
+ return !ignorePatterns.some(pattern => matchesPattern(file, pattern, siteDir, docsDir));
192
377
  });
193
378
  }
194
379
  // Order files according to orderPatterns
@@ -198,8 +383,7 @@ async function processFilesWithPatterns(context, allFiles, includePatterns = [],
198
383
  // Process files according to orderPatterns
199
384
  for (const pattern of orderPatterns) {
200
385
  const matchingFiles = filteredFiles.filter(file => {
201
- const relativePath = path.relative(siteDir, file);
202
- return (0, minimatch_1.minimatch)(relativePath, pattern, { matchBase: true }) && !matchedFiles.has(file);
386
+ return matchesPattern(file, pattern, siteDir, docsDir) && !matchedFiles.has(file);
203
387
  });
204
388
  for (const file of matchingFiles) {
205
389
  filesToProcess.push(file);
@@ -215,9 +399,8 @@ async function processFilesWithPatterns(context, allFiles, includePatterns = [],
215
399
  else {
216
400
  filesToProcess = filteredFiles;
217
401
  }
218
- // Process each file to generate DocInfo
219
- const processedDocs = [];
220
- for (const filePath of filesToProcess) {
402
+ // Process files in parallel using Promise.allSettled
403
+ const results = await Promise.allSettled(filesToProcess.map(async (filePath) => {
221
404
  try {
222
405
  // Determine if this is a blog or docs file
223
406
  const isBlogFile = filePath.includes(path.join(siteDir, 'blog'));
@@ -225,73 +408,27 @@ async function processFilesWithPatterns(context, allFiles, includePatterns = [],
225
408
  const baseDir = siteDir;
226
409
  const pathPrefix = isBlogFile ? 'blog' : 'docs';
227
410
  // Try to find the resolved URL for this file from the route map
228
- let resolvedUrl;
229
- if (context.routeMap) {
230
- // Convert file path to a potential route path
231
- const relativePath = path.relative(baseDir, filePath)
232
- .replace(/\\/g, '/')
411
+ const resolvedUrl = resolveDocumentUrl(filePath, baseDir, pathPrefix, context);
412
+ // Log when we successfully resolve a URL using Docusaurus routes
413
+ if (resolvedUrl && context.routeMap) {
414
+ const relativePath = (0, utils_1.normalizePath)(path.relative(baseDir, filePath))
233
415
  .replace(/\.mdx?$/, '')
234
416
  .replace(/\/index$/, '');
235
- // Function to remove numbered prefixes from path segments
236
- const removeNumberedPrefixes = (path) => {
237
- return path.split('/').map(segment => {
238
- // Remove numbered prefixes like "01-", "1-", "001-" from each segment
239
- return segment.replace(/^\d+-/, '');
240
- }).join('/');
241
- };
242
- // Check various possible route patterns
243
- const cleanPath = removeNumberedPrefixes(relativePath);
244
- const possiblePaths = [
245
- `/${pathPrefix}/${cleanPath}`,
246
- `/${cleanPath}`,
247
- `/${pathPrefix}/${relativePath}`, // Try with original path
248
- `/${relativePath}`, // Try without prefix
249
- ];
250
- // Also handle nested folder structures with numbered prefixes
251
- const segments = relativePath.split('/');
252
- if (segments.length > 1) {
253
- // Try removing numbered prefixes from different levels
254
- for (let i = 0; i < segments.length; i++) {
255
- const modifiedSegments = [...segments];
256
- modifiedSegments[i] = modifiedSegments[i].replace(/^\d+-/, '');
257
- const modifiedPath = modifiedSegments.join('/');
258
- possiblePaths.push(`/${pathPrefix}/${modifiedPath}`);
259
- possiblePaths.push(`/${modifiedPath}`);
260
- }
261
- }
262
- // Try to find a match in the route map
263
- for (const possiblePath of possiblePaths) {
264
- if (context.routeMap.has(possiblePath)) {
265
- resolvedUrl = context.routeMap.get(possiblePath);
266
- break;
267
- }
268
- }
269
- // If still not found, try to find the best match using the routesPaths array
270
- if (!resolvedUrl && context.routesPaths) {
271
- const normalizedCleanPath = cleanPath.toLowerCase();
272
- const matchingRoute = context.routesPaths.find(routePath => {
273
- const normalizedRoute = routePath.toLowerCase();
274
- return normalizedRoute.endsWith(`/${normalizedCleanPath}`) ||
275
- normalizedRoute === `/${pathPrefix}/${normalizedCleanPath}` ||
276
- normalizedRoute === `/${normalizedCleanPath}`;
277
- });
278
- if (matchingRoute) {
279
- resolvedUrl = matchingRoute;
280
- }
281
- }
282
- // Log when we successfully resolve a URL using Docusaurus routes
283
- if (resolvedUrl && resolvedUrl !== `/${pathPrefix}/${relativePath}`) {
284
- console.log(`Resolved URL for ${path.basename(filePath)}: ${resolvedUrl} (was: /${pathPrefix}/${relativePath})`);
417
+ if (resolvedUrl !== `/${pathPrefix}/${relativePath}`) {
418
+ utils_1.logger.verbose(`Resolved URL for ${path.basename(filePath)}: ${resolvedUrl} (was: /${pathPrefix}/${relativePath})`);
285
419
  }
286
420
  }
287
421
  const docInfo = await processMarkdownFile(filePath, baseDir, siteUrl, pathPrefix, context.options.pathTransformation, context.options.excludeImports || false, context.options.removeDuplicateHeadings || false, resolvedUrl);
288
- if (docInfo !== null) {
289
- processedDocs.push(docInfo);
290
- }
422
+ return docInfo;
291
423
  }
292
424
  catch (err) {
293
- console.warn(`Error processing ${filePath}: ${err.message}`);
425
+ utils_1.logger.warn(`Error processing ${filePath}: ${(0, utils_1.getErrorMessage)(err)}`);
426
+ return null;
294
427
  }
295
- }
428
+ }));
429
+ // Filter successful results and non-null DocInfo objects
430
+ const processedDocs = results
431
+ .filter((r) => r.status === 'fulfilled' && r.value !== null)
432
+ .map(r => r.value);
296
433
  return processedDocs;
297
434
  }
package/lib/types.d.ts CHANGED
@@ -87,6 +87,16 @@ export interface PluginOptions {
87
87
  rootContent?: string;
88
88
  /** Custom content to include at the root level of llms-full.txt (after title/description, before content sections) */
89
89
  fullRootContent?: string;
90
+ /** Whether to preserve directory structure in generated markdown files (default: true) */
91
+ preserveDirectoryStructure?: boolean;
92
+ /** Batch size for processing large document sets to prevent memory issues (default: 100) */
93
+ processingBatchSize?: number;
94
+ /** Logging level for plugin output (default: 'normal'). Options: 'quiet', 'normal', 'verbose' */
95
+ logLevel?: 'quiet' | 'normal' | 'verbose';
96
+ /** Whether to warn about files that are ignored (no extension or unsupported extension) (default: false) */
97
+ warnOnIgnoredFiles?: boolean;
98
+ /** Index signature for Docusaurus plugin compatibility */
99
+ [key: string]: unknown;
90
100
  }
91
101
  /**
92
102
  * Plugin context with processed options
package/lib/utils.d.ts CHANGED
@@ -2,6 +2,129 @@
2
2
  * Utility functions for the docusaurus-plugin-llms plugin
3
3
  */
4
4
  import { PluginOptions } from './types';
5
+ /**
6
+ * Null/Undefined Handling Guidelines:
7
+ *
8
+ * 1. For required parameters: Throw early if null/undefined
9
+ * 2. For optional parameters: Use optional chaining `value?.property`
10
+ * 3. For explicit null checks: Use `!== null` and `!== undefined` or the isDefined type guard
11
+ * 4. For string validation: Use isNonEmptyString() type guard
12
+ * 5. For truthy checks on booleans: Use explicit comparison or Boolean(value)
13
+ *
14
+ * Avoid: `if (value)` when value could be 0, '', or false legitimately
15
+ * Use: Type guards for consistent, type-safe checks
16
+ */
17
+ /**
18
+ * Type guard to check if a value is defined (not null or undefined)
19
+ * @param value - Value to check
20
+ * @returns True if value is not null or undefined
21
+ */
22
+ export declare function isDefined<T>(value: T | null | undefined): value is T;
23
+ /**
24
+ * Type guard to check if a value is a non-empty string
25
+ * @param value - Value to check
26
+ * @returns True if value is a string with at least one non-whitespace character
27
+ */
28
+ export declare function isNonEmptyString(value: unknown): value is string;
29
+ /**
30
+ * Type guard to check if a value is a non-empty array
31
+ * @param value - Value to check
32
+ * @returns True if value is an array with at least one element
33
+ */
34
+ export declare function isNonEmptyArray<T>(value: unknown): value is T[];
35
+ /**
36
+ * Safely extract an error message from an unknown error value
37
+ * @param error - The error value (can be Error, string, or any other type)
38
+ * @returns A string representation of the error
39
+ */
40
+ export declare function getErrorMessage(error: unknown): string;
41
+ /**
42
+ * Extract stack trace from unknown error types
43
+ * @param error - The error value (can be Error or any other type)
44
+ * @returns Stack trace if available, undefined otherwise
45
+ */
46
+ export declare function getErrorStack(error: unknown): string | undefined;
47
+ /**
48
+ * Custom error class for validation errors
49
+ */
50
+ export declare class ValidationError extends Error {
51
+ constructor(message: string);
52
+ }
53
+ /**
54
+ * Validates that a value is not null or undefined
55
+ * @param value - The value to validate
56
+ * @param paramName - The parameter name for error messages
57
+ * @returns The validated value
58
+ * @throws ValidationError if the value is null or undefined
59
+ */
60
+ export declare function validateRequired<T>(value: T | null | undefined, paramName: string): T;
61
+ /**
62
+ * Validates that a value is a string and optionally checks its properties
63
+ * @param value - The value to validate
64
+ * @param paramName - The parameter name for error messages
65
+ * @param options - Validation options for min/max length and pattern
66
+ * @returns The validated string
67
+ * @throws ValidationError if validation fails
68
+ */
69
+ export declare function validateString(value: unknown, paramName: string, options?: {
70
+ minLength?: number;
71
+ maxLength?: number;
72
+ pattern?: RegExp;
73
+ }): string;
74
+ /**
75
+ * Validates that a value is an array and optionally validates elements
76
+ * @param value - The value to validate
77
+ * @param paramName - The parameter name for error messages
78
+ * @param elementValidator - Optional function to validate each element
79
+ * @returns The validated array
80
+ * @throws ValidationError if validation fails
81
+ */
82
+ export declare function validateArray<T>(value: unknown, paramName: string, elementValidator?: (item: unknown) => boolean): T[];
83
+ /**
84
+ * Logging level enumeration
85
+ */
86
+ export declare enum LogLevel {
87
+ QUIET = 0,
88
+ NORMAL = 1,
89
+ VERBOSE = 2
90
+ }
91
+ /**
92
+ * Set the logging level for the plugin
93
+ * @param level - The logging level to use
94
+ */
95
+ export declare function setLogLevel(level: LogLevel): void;
96
+ /**
97
+ * Logger utility for consistent logging across the plugin
98
+ */
99
+ export declare const logger: {
100
+ error: (message: string) => void;
101
+ warn: (message: string) => void;
102
+ info: (message: string) => void;
103
+ verbose: (message: string) => void;
104
+ };
105
+ /**
106
+ * Normalizes a file path by converting all backslashes to forward slashes.
107
+ * This ensures consistent path handling across Windows and Unix systems.
108
+ *
109
+ * @param filePath - The file path to normalize
110
+ * @returns The normalized path with forward slashes
111
+ * @throws ValidationError if filePath is not a string
112
+ */
113
+ export declare function normalizePath(filePath: string): string;
114
+ /**
115
+ * Validates that a file path does not exceed the platform-specific maximum length
116
+ * @param filePath - The file path to validate
117
+ * @returns True if the path is within limits, false otherwise
118
+ */
119
+ export declare function validatePathLength(filePath: string): boolean;
120
+ /**
121
+ * Shortens a file path by creating a hash-based filename if the path is too long
122
+ * @param fullPath - The full file path that may be too long
123
+ * @param outputDir - The output directory base path
124
+ * @param relativePath - The relative path from the output directory
125
+ * @returns A shortened path if necessary, or the original path if it's within limits
126
+ */
127
+ export declare function shortenPathIfNeeded(fullPath: string, outputDir: string, relativePath: string): string;
5
128
  /**
6
129
  * Write content to a file
7
130
  * @param filePath - Path to write the file to
@@ -11,25 +134,30 @@ export declare function writeFile(filePath: string, data: string): Promise<void>
11
134
  /**
12
135
  * Read content from a file
13
136
  * @param filePath - Path of the file to read
14
- * @returns Content of the file
137
+ * @returns Content of the file with BOM removed if present
15
138
  */
16
139
  export declare function readFile(filePath: string): Promise<string>;
17
140
  /**
18
141
  * Check if a file should be ignored based on glob patterns
142
+ * Matches against both site-relative and docs-relative paths
19
143
  * @param filePath - Path to the file
20
- * @param baseDir - Base directory for relative paths
144
+ * @param baseDir - Base directory (site root) for relative paths
21
145
  * @param ignorePatterns - Glob patterns for files to ignore
146
+ * @param docsDir - Docs directory name (e.g., 'docs')
22
147
  * @returns Whether the file should be ignored
23
148
  */
24
- export declare function shouldIgnoreFile(filePath: string, baseDir: string, ignorePatterns: string[]): boolean;
149
+ export declare function shouldIgnoreFile(filePath: string, baseDir: string, ignorePatterns: string[], docsDir?: string): boolean;
25
150
  /**
26
151
  * Recursively reads all Markdown files in a directory
27
152
  * @param dir - Directory to scan
28
- * @param baseDir - Base directory for relative paths
153
+ * @param baseDir - Base directory (site root) for relative paths
29
154
  * @param ignorePatterns - Glob patterns for files to ignore
155
+ * @param docsDir - Docs directory name (e.g., 'docs')
156
+ * @param warnOnIgnoredFiles - Whether to warn about ignored files
157
+ * @param visitedPaths - Set of already visited real paths to detect symlink loops (internal use)
30
158
  * @returns Array of file paths
31
159
  */
32
- export declare function readMarkdownFiles(dir: string, baseDir: string, ignorePatterns?: string[]): Promise<string[]>;
160
+ export declare function readMarkdownFiles(dir: string, baseDir: string, ignorePatterns?: string[], docsDir?: string, warnOnIgnoredFiles?: boolean, visitedPaths?: Set<string>): Promise<string[]>;
33
161
  /**
34
162
  * Extract title from content or use the filename
35
163
  * @param data - Frontmatter data
@@ -42,9 +170,10 @@ export declare function extractTitle(data: any, content: string, filePath: strin
42
170
  * Resolve and inline partial imports in markdown content
43
171
  * @param content - The markdown content with import statements
44
172
  * @param filePath - The path of the file containing the imports
173
+ * @param importChain - Set of file paths in the current import chain (for circular dependency detection)
45
174
  * @returns Content with partials resolved
46
175
  */
47
- export declare function resolvePartialImports(content: string, filePath: string): Promise<string>;
176
+ export declare function resolvePartialImports(content: string, filePath: string, importChain?: Set<string>): Promise<string>;
48
177
  /**
49
178
  * Clean markdown content for LLM consumption
50
179
  * @param content - Raw markdown content
@@ -65,14 +194,19 @@ export declare function applyPathTransformations(urlPath: string, pathTransforma
65
194
  * @param input - Input string (typically a title)
66
195
  * @param fallback - Fallback string if input becomes empty after sanitization
67
196
  * @returns Sanitized filename (without extension)
197
+ * @throws ValidationError if input or fallback are not strings
68
198
  */
69
- export declare function sanitizeForFilename(input: string, fallback?: string): string;
199
+ export declare function sanitizeForFilename(input: string, fallback?: string, options?: {
200
+ preserveUnicode?: boolean;
201
+ preserveCase?: boolean;
202
+ }): string;
70
203
  /**
71
204
  * Ensure a unique identifier from a set of used identifiers
72
205
  * @param baseIdentifier - Base identifier to make unique
73
206
  * @param usedIdentifiers - Set of already used identifiers
74
207
  * @param suffix - Suffix pattern (default: number in parentheses)
75
208
  * @returns Unique identifier
209
+ * @throws ValidationError if baseIdentifier is not a string or usedIdentifiers is not a Set
76
210
  */
77
211
  export declare function ensureUniqueIdentifier(baseIdentifier: string, usedIdentifiers: Set<string>, suffix?: (counter: number, base: string) => string): string;
78
212
  /**