@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.
- package/lib/chunk-45NROJIG.js +327 -0
- package/lib/{chunk-G5KKGJYO.js → chunk-CUBMCGAY.js} +22 -261
- package/lib/chunk-DRBRT57F.js +1 -0
- package/lib/{chunk-DR6UG237.js → chunk-DTXB5PMR.js} +21 -335
- package/lib/chunk-GZ2MP3VN.js +261 -0
- package/lib/chunk-SQSYXPIF.js +1 -0
- package/lib/{chunk-6NKAJT2M.js → chunk-VHUUC45J.js} +35 -3
- package/lib/cli/index.js +5 -3
- package/lib/generators/index.d.ts +1 -3
- package/lib/generators/index.js +2 -1
- package/lib/index.d.ts +7 -139
- package/lib/index.js +146 -13
- package/lib/orchestrator/index.d.ts +82 -0
- package/lib/orchestrator/index.js +27 -0
- package/lib/parsers/index.js +1 -1
- package/lib/utils/index.d.ts +140 -0
- package/lib/utils/index.js +24 -0
- package/lib/watcher/index.d.ts +62 -0
- package/lib/watcher/index.js +25 -0
- package/package.json +17 -2
- package/src/generators/component-mapper.ts +11 -1
- package/src/generators/mdx-generator.ts +18 -17
- package/src/index.ts +82 -0
- package/src/parsers/readme-parser.ts +55 -2
- package/src/utils/safe-patterns.ts +6 -2
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createHeadingPattern,
|
|
3
|
-
extractCodeBlocks,
|
|
4
|
-
findEmptyMarkdownLinks,
|
|
5
2
|
generateMDXDocument,
|
|
6
|
-
hasComponent,
|
|
7
3
|
mergeContent,
|
|
8
4
|
validateMDXSyntax
|
|
9
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CUBMCGAY.js";
|
|
10
6
|
import {
|
|
11
7
|
analyzePublicAPI,
|
|
12
8
|
parsePackageComplete,
|
|
13
9
|
parseReadmeFile
|
|
14
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-VHUUC45J.js";
|
|
11
|
+
import {
|
|
12
|
+
createHeadingPattern,
|
|
13
|
+
extractCodeBlocks,
|
|
14
|
+
findEmptyMarkdownLinks,
|
|
15
|
+
hasComponent
|
|
16
|
+
} from "./chunk-GZ2MP3VN.js";
|
|
17
|
+
import {
|
|
18
|
+
createDocDebouncer,
|
|
19
|
+
createDocWatcher,
|
|
20
|
+
determineRegenerationScope,
|
|
21
|
+
groupChangesByPackage
|
|
22
|
+
} from "./chunk-45NROJIG.js";
|
|
15
23
|
|
|
16
24
|
// src/orchestrator/package-scanner.ts
|
|
17
25
|
import fs from "fs/promises";
|
|
@@ -197,318 +205,6 @@ function getPackageScope(packageName) {
|
|
|
197
205
|
return void 0;
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
// src/watcher/file-watcher.ts
|
|
201
|
-
import path2 from "path";
|
|
202
|
-
import process from "process";
|
|
203
|
-
import { createFileWatcher as createBaseWatcher } from "@bfra.me/es/watcher";
|
|
204
|
-
var DEFAULT_WATCH_PATTERNS = [
|
|
205
|
-
"packages/*/README.md",
|
|
206
|
-
"packages/*/readme.md",
|
|
207
|
-
"packages/*/package.json",
|
|
208
|
-
"packages/*/src/**/*.ts",
|
|
209
|
-
"packages/*/src/**/*.tsx"
|
|
210
|
-
];
|
|
211
|
-
var DEFAULT_IGNORE_PATTERNS = [
|
|
212
|
-
"**/node_modules/**",
|
|
213
|
-
"**/lib/**",
|
|
214
|
-
"**/dist/**",
|
|
215
|
-
"**/.git/**",
|
|
216
|
-
"**/coverage/**",
|
|
217
|
-
"**/*.test.ts",
|
|
218
|
-
"**/*.spec.ts",
|
|
219
|
-
"**/__tests__/**",
|
|
220
|
-
"**/__mocks__/**"
|
|
221
|
-
];
|
|
222
|
-
function extractPackageName(filePath, root) {
|
|
223
|
-
const relativePath = path2.relative(root, filePath);
|
|
224
|
-
const parts = relativePath.split(path2.sep);
|
|
225
|
-
if (parts[0] === "packages" && parts.length > 1) {
|
|
226
|
-
return parts[1];
|
|
227
|
-
}
|
|
228
|
-
return void 0;
|
|
229
|
-
}
|
|
230
|
-
function createDocWatcher(options = {}) {
|
|
231
|
-
const {
|
|
232
|
-
rootDir = process.cwd(),
|
|
233
|
-
debounceMs = 300,
|
|
234
|
-
additionalIgnore = [],
|
|
235
|
-
usePolling = false
|
|
236
|
-
} = options;
|
|
237
|
-
const watchPaths = DEFAULT_WATCH_PATTERNS.map((pattern) => path2.join(rootDir, pattern));
|
|
238
|
-
const ignoredPatterns = [...DEFAULT_IGNORE_PATTERNS, ...additionalIgnore];
|
|
239
|
-
const watcherOptions = {
|
|
240
|
-
debounceMs,
|
|
241
|
-
ignored: ignoredPatterns,
|
|
242
|
-
usePolling
|
|
243
|
-
};
|
|
244
|
-
const baseWatcher = createBaseWatcher(watchPaths, watcherOptions);
|
|
245
|
-
const handlers = /* @__PURE__ */ new Set();
|
|
246
|
-
function transformToDocEvents(changes) {
|
|
247
|
-
return changes.map((change) => ({
|
|
248
|
-
type: change.type,
|
|
249
|
-
path: change.path,
|
|
250
|
-
packageName: extractPackageName(change.path, rootDir),
|
|
251
|
-
timestamp: new Date(change.timestamp)
|
|
252
|
-
}));
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
async start() {
|
|
256
|
-
baseWatcher.on("change", (event) => {
|
|
257
|
-
const docEvents = transformToDocEvents(event.changes);
|
|
258
|
-
for (const handler of handlers) {
|
|
259
|
-
Promise.resolve(handler(docEvents)).catch((error) => {
|
|
260
|
-
console.error("[doc-sync] Error in change handler:", error);
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
await baseWatcher.start();
|
|
265
|
-
},
|
|
266
|
-
async close() {
|
|
267
|
-
await baseWatcher.close();
|
|
268
|
-
handlers.clear();
|
|
269
|
-
},
|
|
270
|
-
onChanges(handler) {
|
|
271
|
-
handlers.add(handler);
|
|
272
|
-
return () => {
|
|
273
|
-
handlers.delete(handler);
|
|
274
|
-
};
|
|
275
|
-
},
|
|
276
|
-
getWatchedPaths() {
|
|
277
|
-
return watchPaths;
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function categorizeFile(filePath) {
|
|
282
|
-
const basename = path2.basename(filePath).toLowerCase();
|
|
283
|
-
if (basename === "readme.md" || basename === "readme") {
|
|
284
|
-
return "readme";
|
|
285
|
-
}
|
|
286
|
-
if (basename === "package.json") {
|
|
287
|
-
return "package-json";
|
|
288
|
-
}
|
|
289
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
290
|
-
if (ext === ".ts" || ext === ".tsx") {
|
|
291
|
-
return "source";
|
|
292
|
-
}
|
|
293
|
-
return "unknown";
|
|
294
|
-
}
|
|
295
|
-
function groupChangesByPackage(events) {
|
|
296
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
297
|
-
for (const event of events) {
|
|
298
|
-
const pkg = event.packageName ?? "__unknown__";
|
|
299
|
-
const existing = grouped.get(pkg);
|
|
300
|
-
if (existing === void 0) {
|
|
301
|
-
grouped.set(pkg, [event]);
|
|
302
|
-
} else {
|
|
303
|
-
existing.push(event);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return grouped;
|
|
307
|
-
}
|
|
308
|
-
function filterDocumentationChanges(events) {
|
|
309
|
-
return events.filter((event) => {
|
|
310
|
-
const category = categorizeFile(event.path);
|
|
311
|
-
return category !== "unknown";
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// src/watcher/change-detector.ts
|
|
316
|
-
import { createChangeDetector as createBaseDetector } from "@bfra.me/es/watcher";
|
|
317
|
-
function createDocChangeDetector(options = {}) {
|
|
318
|
-
const baseDetector = createBaseDetector({ algorithm: options.algorithm });
|
|
319
|
-
const packageFiles = /* @__PURE__ */ new Map();
|
|
320
|
-
return {
|
|
321
|
-
async hasChanged(filePath) {
|
|
322
|
-
return baseDetector.hasChanged(filePath);
|
|
323
|
-
},
|
|
324
|
-
async record(filePath) {
|
|
325
|
-
await baseDetector.record(filePath);
|
|
326
|
-
},
|
|
327
|
-
async recordPackage(pkg, files) {
|
|
328
|
-
const fileSet = new Set(files);
|
|
329
|
-
packageFiles.set(pkg.name, fileSet);
|
|
330
|
-
await Promise.all(files.map(async (file) => baseDetector.record(file)));
|
|
331
|
-
},
|
|
332
|
-
clear(filePath) {
|
|
333
|
-
baseDetector.clear(filePath);
|
|
334
|
-
for (const fileSet of packageFiles.values()) {
|
|
335
|
-
fileSet.delete(filePath);
|
|
336
|
-
}
|
|
337
|
-
},
|
|
338
|
-
clearAll() {
|
|
339
|
-
baseDetector.clearAll();
|
|
340
|
-
packageFiles.clear();
|
|
341
|
-
},
|
|
342
|
-
async analyzeChanges(events) {
|
|
343
|
-
const packageChanges = /* @__PURE__ */ new Map();
|
|
344
|
-
for (const event of events) {
|
|
345
|
-
const packageName = event.packageName ?? "__unknown__";
|
|
346
|
-
const category = categorizeFile(event.path);
|
|
347
|
-
let analysis = packageChanges.get(packageName);
|
|
348
|
-
if (analysis === void 0) {
|
|
349
|
-
analysis = { categories: /* @__PURE__ */ new Set(), files: [] };
|
|
350
|
-
packageChanges.set(packageName, analysis);
|
|
351
|
-
}
|
|
352
|
-
if (category !== "unknown") {
|
|
353
|
-
analysis.categories.add(category);
|
|
354
|
-
}
|
|
355
|
-
analysis.files.push(event.path);
|
|
356
|
-
}
|
|
357
|
-
const results = [];
|
|
358
|
-
for (const [packageName, analysis] of packageChanges) {
|
|
359
|
-
const changedCategories = [...analysis.categories];
|
|
360
|
-
const needsRegeneration = changedCategories.includes("readme") || changedCategories.includes("package-json") || changedCategories.includes("source");
|
|
361
|
-
results.push({
|
|
362
|
-
packageName,
|
|
363
|
-
needsRegeneration,
|
|
364
|
-
changedCategories,
|
|
365
|
-
changedFiles: analysis.files
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
return results;
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
function determineRegenerationScope(changedCategories) {
|
|
373
|
-
const hasReadme = changedCategories.includes("readme");
|
|
374
|
-
const hasSource = changedCategories.includes("source");
|
|
375
|
-
const hasPackageJson = changedCategories.includes("package-json");
|
|
376
|
-
if (hasReadme && hasSource) {
|
|
377
|
-
return "full";
|
|
378
|
-
}
|
|
379
|
-
if (hasSource) {
|
|
380
|
-
return "api-only";
|
|
381
|
-
}
|
|
382
|
-
if (hasReadme) {
|
|
383
|
-
return "readme-only";
|
|
384
|
-
}
|
|
385
|
-
if (hasPackageJson) {
|
|
386
|
-
return "metadata-only";
|
|
387
|
-
}
|
|
388
|
-
return "none";
|
|
389
|
-
}
|
|
390
|
-
async function hasAnyFileChanged(detector, files) {
|
|
391
|
-
const results = await Promise.all(files.map(async (file) => detector.hasChanged(file)));
|
|
392
|
-
return results.some((changed) => changed);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// src/watcher/debouncer.ts
|
|
396
|
-
import { createDebouncer as createBaseDebouncer } from "@bfra.me/es/watcher";
|
|
397
|
-
function createDocDebouncer(handler, options = {}) {
|
|
398
|
-
const { debounceMs = 300, maxWaitMs = 5e3 } = options;
|
|
399
|
-
let pendingEvents = [];
|
|
400
|
-
let maxWaitTimeout;
|
|
401
|
-
function processEvents(events) {
|
|
402
|
-
if (maxWaitTimeout !== void 0) {
|
|
403
|
-
clearTimeout(maxWaitTimeout);
|
|
404
|
-
maxWaitTimeout = void 0;
|
|
405
|
-
}
|
|
406
|
-
const deduplicated = deduplicateEvents(events);
|
|
407
|
-
if (deduplicated.length > 0) {
|
|
408
|
-
Promise.resolve(handler(deduplicated)).catch((error) => {
|
|
409
|
-
console.error("[doc-sync] Error in batch handler:", error);
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
const baseDebouncer = createBaseDebouncer((events) => {
|
|
414
|
-
processEvents(events);
|
|
415
|
-
pendingEvents = [];
|
|
416
|
-
}, debounceMs);
|
|
417
|
-
function startMaxWaitTimer() {
|
|
418
|
-
if (maxWaitTimeout === void 0) {
|
|
419
|
-
maxWaitTimeout = setTimeout(() => {
|
|
420
|
-
baseDebouncer.flush();
|
|
421
|
-
}, maxWaitMs);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return {
|
|
425
|
-
add(event) {
|
|
426
|
-
pendingEvents.push(event);
|
|
427
|
-
startMaxWaitTimer();
|
|
428
|
-
baseDebouncer.add(event);
|
|
429
|
-
},
|
|
430
|
-
addAll(events) {
|
|
431
|
-
for (const event of events) {
|
|
432
|
-
pendingEvents.push(event);
|
|
433
|
-
baseDebouncer.add(event);
|
|
434
|
-
}
|
|
435
|
-
if (events.length > 0) {
|
|
436
|
-
startMaxWaitTimer();
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
flush() {
|
|
440
|
-
if (maxWaitTimeout !== void 0) {
|
|
441
|
-
clearTimeout(maxWaitTimeout);
|
|
442
|
-
maxWaitTimeout = void 0;
|
|
443
|
-
}
|
|
444
|
-
baseDebouncer.flush();
|
|
445
|
-
},
|
|
446
|
-
cancel() {
|
|
447
|
-
if (maxWaitTimeout !== void 0) {
|
|
448
|
-
clearTimeout(maxWaitTimeout);
|
|
449
|
-
maxWaitTimeout = void 0;
|
|
450
|
-
}
|
|
451
|
-
pendingEvents = [];
|
|
452
|
-
baseDebouncer.cancel();
|
|
453
|
-
},
|
|
454
|
-
getPendingCount() {
|
|
455
|
-
return pendingEvents.length;
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function deduplicateEvents(events) {
|
|
460
|
-
const latestByPath = /* @__PURE__ */ new Map();
|
|
461
|
-
for (const event of events) {
|
|
462
|
-
const existing = latestByPath.get(event.path);
|
|
463
|
-
if (existing === void 0 || event.timestamp > existing.timestamp) {
|
|
464
|
-
latestByPath.set(event.path, event);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
return [...latestByPath.values()];
|
|
468
|
-
}
|
|
469
|
-
function consolidateEvents(events) {
|
|
470
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
471
|
-
for (const event of events) {
|
|
472
|
-
const existing = byPath.get(event.path);
|
|
473
|
-
if (existing === void 0) {
|
|
474
|
-
byPath.set(event.path, [event]);
|
|
475
|
-
} else {
|
|
476
|
-
existing.push(event);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
const consolidated = [];
|
|
480
|
-
for (const [, pathEvents] of byPath) {
|
|
481
|
-
const firstEvent = pathEvents[0];
|
|
482
|
-
if (firstEvent === void 0) {
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
if (pathEvents.length === 1) {
|
|
486
|
-
consolidated.push(firstEvent);
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
pathEvents.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
490
|
-
const first = pathEvents[0];
|
|
491
|
-
const last = pathEvents.at(-1);
|
|
492
|
-
if (first === void 0 || last === void 0) {
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if (first.type === "add" && last.type === "unlink") {
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (first.type === "unlink" && last.type === "add") {
|
|
499
|
-
consolidated.push({
|
|
500
|
-
type: "change",
|
|
501
|
-
path: last.path,
|
|
502
|
-
packageName: last.packageName,
|
|
503
|
-
timestamp: last.timestamp
|
|
504
|
-
});
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
consolidated.push(last);
|
|
508
|
-
}
|
|
509
|
-
return consolidated;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
208
|
// src/orchestrator/validation-pipeline.ts
|
|
513
209
|
import { err as err2, ok as ok2 } from "@bfra.me/es/result";
|
|
514
210
|
var DEFAULT_OPTIONS2 = {
|
|
@@ -722,7 +418,7 @@ function validateContentString(content, options) {
|
|
|
722
418
|
|
|
723
419
|
// src/orchestrator/sync-orchestrator.ts
|
|
724
420
|
import fs2 from "fs/promises";
|
|
725
|
-
import
|
|
421
|
+
import path2 from "path";
|
|
726
422
|
import { err as err3, ok as ok3 } from "@bfra.me/es/result";
|
|
727
423
|
function createSyncOrchestrator(options) {
|
|
728
424
|
const { config, dryRun = false, verbose = false, onProgress, onError } = options;
|
|
@@ -943,7 +639,7 @@ function createSyncOrchestrator(options) {
|
|
|
943
639
|
}
|
|
944
640
|
function getOutputPath(packageName, outputDir) {
|
|
945
641
|
const slug = createSlug(getUnscopedName2(packageName));
|
|
946
|
-
return
|
|
642
|
+
return path2.join(outputDir, `${slug}.mdx`);
|
|
947
643
|
}
|
|
948
644
|
function createSlug(name) {
|
|
949
645
|
return name.toLowerCase().replaceAll(/[^a-z0-9-]/g, "-");
|
|
@@ -958,7 +654,7 @@ function getUnscopedName2(packageName) {
|
|
|
958
654
|
return packageName;
|
|
959
655
|
}
|
|
960
656
|
async function writeFile(filePath, content) {
|
|
961
|
-
const dir =
|
|
657
|
+
const dir = path2.dirname(filePath);
|
|
962
658
|
await fs2.mkdir(dir, { recursive: true });
|
|
963
659
|
await fs2.writeFile(filePath, content, "utf-8");
|
|
964
660
|
}
|
|
@@ -992,15 +688,15 @@ function isNodeError(error) {
|
|
|
992
688
|
return error instanceof Error && "code" in error;
|
|
993
689
|
}
|
|
994
690
|
function categorizeFileChange(filePath) {
|
|
995
|
-
const basename =
|
|
691
|
+
const basename = path2.basename(filePath).toLowerCase();
|
|
996
692
|
if (basename === "readme.md" || basename === "readme") return "readme";
|
|
997
693
|
if (basename === "package.json") return "package-json";
|
|
998
694
|
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "source";
|
|
999
695
|
return "unknown";
|
|
1000
696
|
}
|
|
1001
697
|
function isValidFilePath(filePath, rootDir) {
|
|
1002
|
-
const resolvedPath =
|
|
1003
|
-
const resolvedRoot =
|
|
698
|
+
const resolvedPath = path2.resolve(filePath);
|
|
699
|
+
const resolvedRoot = path2.resolve(rootDir);
|
|
1004
700
|
return resolvedPath.startsWith(resolvedRoot);
|
|
1005
701
|
}
|
|
1006
702
|
|
|
@@ -1008,20 +704,10 @@ export {
|
|
|
1008
704
|
createPackageScanner,
|
|
1009
705
|
filterPackagesByPattern,
|
|
1010
706
|
groupPackagesByScope,
|
|
1011
|
-
createDocWatcher,
|
|
1012
|
-
categorizeFile,
|
|
1013
|
-
groupChangesByPackage,
|
|
1014
|
-
filterDocumentationChanges,
|
|
1015
|
-
createDocChangeDetector,
|
|
1016
|
-
determineRegenerationScope,
|
|
1017
|
-
hasAnyFileChanged,
|
|
1018
|
-
createDocDebouncer,
|
|
1019
|
-
deduplicateEvents,
|
|
1020
|
-
consolidateEvents,
|
|
1021
707
|
createValidationPipeline,
|
|
1022
708
|
validateDocument,
|
|
1023
709
|
validateContentString,
|
|
1024
710
|
createSyncOrchestrator,
|
|
1025
711
|
isValidFilePath
|
|
1026
712
|
};
|
|
1027
|
-
//# sourceMappingURL=chunk-
|
|
713
|
+
//# sourceMappingURL=chunk-DTXB5PMR.js.map
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// src/utils/sanitization.ts
|
|
2
|
+
import { sanitizeInput } from "@bfra.me/es/validation";
|
|
3
|
+
import escapeHtml from "escape-html";
|
|
4
|
+
function sanitizeForMDX(content) {
|
|
5
|
+
const escaped = sanitizeInput(content, { trim: false });
|
|
6
|
+
return escaped.replaceAll("{", "{").replaceAll("}", "}");
|
|
7
|
+
}
|
|
8
|
+
function sanitizeAttribute(value) {
|
|
9
|
+
return escapeHtml(value);
|
|
10
|
+
}
|
|
11
|
+
function parseJSXAttributes(tag) {
|
|
12
|
+
const attrs = [];
|
|
13
|
+
const spaceIndex = tag.indexOf(" ");
|
|
14
|
+
if (spaceIndex === -1) return attrs;
|
|
15
|
+
const closeIndex = tag.lastIndexOf(">");
|
|
16
|
+
if (closeIndex === -1) return attrs;
|
|
17
|
+
const attrRegion = tag.slice(spaceIndex + 1, closeIndex).trim();
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < attrRegion.length) {
|
|
20
|
+
while (i < attrRegion.length && /\s/.test(attrRegion.charAt(i))) i++;
|
|
21
|
+
if (i >= attrRegion.length) break;
|
|
22
|
+
let name = "";
|
|
23
|
+
while (i < attrRegion.length && /[\w-]/.test(attrRegion.charAt(i))) {
|
|
24
|
+
name += attrRegion[i];
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
if (!name) break;
|
|
28
|
+
while (i < attrRegion.length && /\s/.test(attrRegion.charAt(i))) i++;
|
|
29
|
+
if (i >= attrRegion.length || attrRegion[i] !== "=") {
|
|
30
|
+
attrs.push({ name, value: null });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
i++;
|
|
34
|
+
while (i < attrRegion.length && /\s/.test(attrRegion.charAt(i))) i++;
|
|
35
|
+
if (i >= attrRegion.length) {
|
|
36
|
+
attrs.push({ name, value: "" });
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
let value = "";
|
|
40
|
+
const quote = attrRegion[i];
|
|
41
|
+
if (quote === '"' || quote === "'") {
|
|
42
|
+
i++;
|
|
43
|
+
while (i < attrRegion.length && attrRegion[i] !== quote) {
|
|
44
|
+
value += attrRegion[i];
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
if (i < attrRegion.length) i++;
|
|
48
|
+
} else {
|
|
49
|
+
while (i < attrRegion.length && !/[\s/>]/.test(attrRegion.charAt(i))) {
|
|
50
|
+
value += attrRegion[i];
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
attrs.push({ name, value });
|
|
55
|
+
}
|
|
56
|
+
return attrs;
|
|
57
|
+
}
|
|
58
|
+
function sanitizeJSXTag(tag) {
|
|
59
|
+
const tagMatch = tag.match(/^<([A-Z][a-zA-Z0-9]*)/);
|
|
60
|
+
if (!tagMatch || typeof tagMatch[1] !== "string" || tagMatch[1].length === 0) {
|
|
61
|
+
return escapeHtml(tag);
|
|
62
|
+
}
|
|
63
|
+
const tagName = tagMatch[1];
|
|
64
|
+
const selfClosing = tag.endsWith("/>");
|
|
65
|
+
const attributes = parseJSXAttributes(tag);
|
|
66
|
+
const sanitizedAttrs = attributes.map(({ name, value }) => {
|
|
67
|
+
if (value === null) return name;
|
|
68
|
+
const escaped = escapeHtml(value);
|
|
69
|
+
return `${name}="${escaped}"`;
|
|
70
|
+
});
|
|
71
|
+
const attrString = sanitizedAttrs.length > 0 ? ` ${sanitizedAttrs.join(" ")}` : "";
|
|
72
|
+
return `<${tagName}${attrString}${selfClosing ? " />" : ">"}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/utils/safe-patterns.ts
|
|
76
|
+
import remarkParse from "remark-parse";
|
|
77
|
+
import { unified } from "unified";
|
|
78
|
+
function createHeadingPattern(level) {
|
|
79
|
+
if (level < 1 || level > 6) {
|
|
80
|
+
throw new Error("Heading level must be between 1 and 6");
|
|
81
|
+
}
|
|
82
|
+
const hashes = "#".repeat(level);
|
|
83
|
+
return new RegExp(`^${hashes} ([^\r
|
|
84
|
+
]+)$`, "gm");
|
|
85
|
+
}
|
|
86
|
+
function hasComponent(content, componentName) {
|
|
87
|
+
const escaped = componentName.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
88
|
+
const pattern = new RegExp(String.raw`<${escaped}(?:\s[^>]*)?(?:>|\/>)`);
|
|
89
|
+
return pattern.test(content);
|
|
90
|
+
}
|
|
91
|
+
function extractCodeBlocks(content) {
|
|
92
|
+
const processor = unified().use(remarkParse);
|
|
93
|
+
let tree;
|
|
94
|
+
try {
|
|
95
|
+
tree = processor.parse(content);
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const blocks = [];
|
|
100
|
+
function visit(node) {
|
|
101
|
+
if (node.type === "code") {
|
|
102
|
+
const lang = node.lang ?? "";
|
|
103
|
+
const value = node.value ?? "";
|
|
104
|
+
blocks.push(`\`\`\`${lang}
|
|
105
|
+
${value}
|
|
106
|
+
\`\`\``);
|
|
107
|
+
}
|
|
108
|
+
if (node.type === "inlineCode") {
|
|
109
|
+
const value = node.value ?? "";
|
|
110
|
+
blocks.push(`\`${value}\``);
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(node.children)) {
|
|
113
|
+
for (const child of node.children) {
|
|
114
|
+
visit(child);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
visit(tree);
|
|
119
|
+
return blocks;
|
|
120
|
+
}
|
|
121
|
+
function parseJSXTags(content) {
|
|
122
|
+
const results = [];
|
|
123
|
+
let i = 0;
|
|
124
|
+
while (i < content.length) {
|
|
125
|
+
if (content[i] !== "<") {
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const startIndex = i;
|
|
130
|
+
i++;
|
|
131
|
+
if (i >= content.length) break;
|
|
132
|
+
const isClosing = content[i] === "/";
|
|
133
|
+
if (isClosing) {
|
|
134
|
+
i++;
|
|
135
|
+
if (i >= content.length) break;
|
|
136
|
+
}
|
|
137
|
+
if (!/^[A-Z]/.test(content[i] ?? "")) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
let tagName = "";
|
|
141
|
+
while (i < content.length && /^[a-z0-9]/i.test(content[i] ?? "")) {
|
|
142
|
+
tagName += content[i];
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
if (tagName.length === 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (isClosing) {
|
|
149
|
+
if (i < content.length && content[i] === ">") {
|
|
150
|
+
results.push({
|
|
151
|
+
tag: `</${tagName}>`,
|
|
152
|
+
index: startIndex,
|
|
153
|
+
isClosing: true,
|
|
154
|
+
isSelfClosing: false
|
|
155
|
+
});
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let foundEnd = false;
|
|
162
|
+
let isSelfClosing = false;
|
|
163
|
+
while (i < content.length && !foundEnd) {
|
|
164
|
+
const char = content[i];
|
|
165
|
+
if ((char === '"' || char === "'") && depth === 0) {
|
|
166
|
+
const quote = char;
|
|
167
|
+
i++;
|
|
168
|
+
while (i < content.length && content[i] !== quote) {
|
|
169
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
170
|
+
i += 2;
|
|
171
|
+
} else {
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (i < content.length) i++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (char === "{") {
|
|
179
|
+
depth++;
|
|
180
|
+
i++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === "}") {
|
|
184
|
+
depth = Math.max(0, depth - 1);
|
|
185
|
+
i++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (depth === 0) {
|
|
189
|
+
if (char === "/" && i + 1 < content.length && content[i + 1] === ">") {
|
|
190
|
+
isSelfClosing = true;
|
|
191
|
+
foundEnd = true;
|
|
192
|
+
i += 2;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (char === ">") {
|
|
196
|
+
foundEnd = true;
|
|
197
|
+
i++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
if (foundEnd) {
|
|
204
|
+
const fullTag = content.slice(startIndex, i);
|
|
205
|
+
results.push({ tag: fullTag, index: startIndex, isClosing: false, isSelfClosing });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
function findEmptyMarkdownLinks(content) {
|
|
211
|
+
const positions = [];
|
|
212
|
+
let pos = 0;
|
|
213
|
+
while (pos < content.length) {
|
|
214
|
+
const openBracket = content.indexOf("[", pos);
|
|
215
|
+
if (openBracket === -1) break;
|
|
216
|
+
let bracketDepth = 1;
|
|
217
|
+
let closeBracket = openBracket + 1;
|
|
218
|
+
while (closeBracket < content.length && bracketDepth > 0) {
|
|
219
|
+
if (content[closeBracket] === "[") {
|
|
220
|
+
bracketDepth++;
|
|
221
|
+
} else if (content[closeBracket] === "]") {
|
|
222
|
+
bracketDepth--;
|
|
223
|
+
}
|
|
224
|
+
closeBracket++;
|
|
225
|
+
}
|
|
226
|
+
if (bracketDepth !== 0) {
|
|
227
|
+
pos = openBracket + 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
closeBracket--;
|
|
231
|
+
if (closeBracket + 1 < content.length && content[closeBracket + 1] === "(") {
|
|
232
|
+
let parenPos = closeBracket + 2;
|
|
233
|
+
let isEmptyOrWhitespace = true;
|
|
234
|
+
while (parenPos < content.length && content[parenPos] !== ")") {
|
|
235
|
+
if (!/^\s/.test(content[parenPos] ?? "")) {
|
|
236
|
+
isEmptyOrWhitespace = false;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
parenPos++;
|
|
240
|
+
}
|
|
241
|
+
if (isEmptyOrWhitespace && parenPos < content.length && content[parenPos] === ")") {
|
|
242
|
+
positions.push(openBracket);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
pos = closeBracket + 1;
|
|
246
|
+
}
|
|
247
|
+
return positions;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export {
|
|
251
|
+
sanitizeForMDX,
|
|
252
|
+
sanitizeAttribute,
|
|
253
|
+
parseJSXAttributes,
|
|
254
|
+
sanitizeJSXTag,
|
|
255
|
+
createHeadingPattern,
|
|
256
|
+
hasComponent,
|
|
257
|
+
extractCodeBlocks,
|
|
258
|
+
parseJSXTags,
|
|
259
|
+
findEmptyMarkdownLinks
|
|
260
|
+
};
|
|
261
|
+
//# sourceMappingURL=chunk-GZ2MP3VN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-SQSYXPIF.js.map
|