jamdesk 1.1.127 → 1.1.129
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/dist/commands/broken-links.d.ts +2 -0
- package/dist/commands/broken-links.d.ts.map +1 -1
- package/dist/commands/broken-links.js +11 -2
- package/dist/commands/broken-links.js.map +1 -1
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +5 -0
- package/dist/lib/deps.js.map +1 -1
- package/package.json +2 -1
- package/vendored/app/api/markdown-export/[project]/[...slug]/route.ts +71 -1
- package/vendored/app/layout.tsx +8 -0
- package/vendored/components/AIActionsMenu.tsx +72 -3
- package/vendored/components/layout/EmbedLinkInterceptor.tsx +37 -0
- package/vendored/components/layout/LayoutWrapper.tsx +29 -1
- package/vendored/components/layout/PageColumns.tsx +10 -5
- package/vendored/components/navigation/Breadcrumb.tsx +6 -1
- package/vendored/components/navigation/SocialFooter.tsx +40 -17
- package/vendored/lib/api-spec-menu-gate.ts +49 -0
- package/vendored/lib/api-spec-offer.ts +50 -0
- package/vendored/lib/api-specs-bundle.ts +255 -0
- package/vendored/lib/api-specs-markdown-hint.ts +49 -0
- package/vendored/lib/api-specs-route.ts +45 -0
- package/vendored/lib/docs-types.ts +1 -1
- package/vendored/lib/heading-extractor.ts +34 -30
- package/vendored/lib/layout-helpers.tsx +19 -2
- package/vendored/lib/middleware-helpers.ts +29 -0
- package/vendored/lib/render-doc-page.tsx +93 -76
- package/vendored/lib/scanner-blocklist.ts +7 -0
- package/vendored/lib/static-artifacts.ts +39 -9
- package/vendored/lib/static-file-route.ts +35 -1
- package/vendored/scripts/github-slugger-regex.cjs +13 -0
- package/vendored/scripts/validate-links.cjs +136 -22
- package/vendored/themes/jam/variables.css +8 -0
- package/vendored/workspace-package-lock.json +128 -120
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
|
+
// Byte-for-byte copy of github-slugger@2's removal regex (ESM->CJS). Generated
|
|
24
|
+
// by scripts/gen-github-slugger-regex.cjs; this is the SAME regex rehype-slug
|
|
25
|
+
// uses to assign real heading ids, so #fragment validation matches the true
|
|
26
|
+
// DOM anchors. Drift-guarded by validate-links-slug.test.ts.
|
|
27
|
+
const { regex: GITHUB_SLUGGER_REGEX } = require('./github-slugger-regex.cjs');
|
|
23
28
|
|
|
24
29
|
// Parse command line arguments
|
|
25
30
|
const args = process.argv.slice(2);
|
|
@@ -208,23 +213,125 @@ function isInCodeBlock(lineNumber, ranges) {
|
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
/**
|
|
211
|
-
*
|
|
216
|
+
* Stateless slug — exact port of github-slugger@2's `slug()`:
|
|
217
|
+
* lowercase, remove the chars matched by GITHUB_SLUGGER_REGEX, then turn each
|
|
218
|
+
* remaining space into a hyphen. NO trimming or dash-collapsing (matches the lib).
|
|
219
|
+
* This is what rehype-slug assigns as the real heading `id`, so the validator
|
|
220
|
+
* checks #fragments against the true DOM ids.
|
|
221
|
+
*/
|
|
222
|
+
function githubSlug(value) {
|
|
223
|
+
if (typeof value !== 'string') return '';
|
|
224
|
+
return value.toLowerCase().replace(GITHUB_SLUGGER_REGEX, '').replace(/ /g, '-');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Generate URL-friendly slug. Must match generateSlug in heading-extractor.ts
|
|
229
|
+
* (both delegate to github-slugger).
|
|
212
230
|
*/
|
|
213
231
|
function generateSlug(text) {
|
|
214
|
-
return text
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
232
|
+
return githubSlug(text);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build a stateful slugger that mirrors github-slugger@2's class: repeated
|
|
237
|
+
* values get `-1`, `-2`, ... suffixes in call order — exactly how rehype-slug
|
|
238
|
+
* de-duplicates real heading ids within a page. One slugger per page.
|
|
239
|
+
*/
|
|
240
|
+
function createSlugger() {
|
|
241
|
+
const occurrences = Object.create(null);
|
|
242
|
+
return function slug(value) {
|
|
243
|
+
let result = githubSlug(value);
|
|
244
|
+
const originalSlug = result;
|
|
245
|
+
while (Object.prototype.hasOwnProperty.call(occurrences, result)) {
|
|
246
|
+
occurrences[originalSlug]++;
|
|
247
|
+
result = originalSlug + '-' + occurrences[originalSlug];
|
|
248
|
+
}
|
|
249
|
+
occurrences[result] = 0;
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Levenshtein edit distance (iterative two-row DP). Inlined rather than pulling
|
|
256
|
+
* a dependency: validate-links.cjs is vendored into the CLI as a self-contained
|
|
257
|
+
* script (only ./github-slugger-regex.cjs ships alongside it), so it must not
|
|
258
|
+
* add runtime requires.
|
|
259
|
+
*/
|
|
260
|
+
function levenshtein(a, b) {
|
|
261
|
+
if (a === b) return 0;
|
|
262
|
+
if (a.length === 0) return b.length;
|
|
263
|
+
if (b.length === 0) return a.length;
|
|
264
|
+
let prev = new Array(b.length + 1);
|
|
265
|
+
let curr = new Array(b.length + 1);
|
|
266
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
267
|
+
for (let i = 0; i < a.length; i++) {
|
|
268
|
+
curr[0] = i + 1;
|
|
269
|
+
for (let j = 0; j < b.length; j++) {
|
|
270
|
+
const cost = a[i] === b[j] ? 0 : 1;
|
|
271
|
+
curr[j + 1] = Math.min(curr[j] + 1, prev[j + 1] + 1, prev[j] + cost);
|
|
272
|
+
}
|
|
273
|
+
const tmp = prev;
|
|
274
|
+
prev = curr;
|
|
275
|
+
curr = tmp;
|
|
276
|
+
}
|
|
277
|
+
return prev[b.length];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Maximum edit distance for a "Did you mean #X?" suggestion. Deliberately small
|
|
282
|
+
* (precision over recall): the dominant real cases — apostrophe slips
|
|
283
|
+
* (reset-a-user-s-password → reset-a-users-password) and accent slips
|
|
284
|
+
* (pagina → página) — are distance 1. A larger cap starts suggesting
|
|
285
|
+
* real-but-wrong anchors, which is worse than no suggestion.
|
|
286
|
+
*/
|
|
287
|
+
const MAX_SUGGESTION_DISTANCE = 2;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Find the closest valid heading slug to a fragment that wasn't found, for a
|
|
291
|
+
* "Did you mean #X?" hint. Precision over recall — only returns a slug for an
|
|
292
|
+
* UNAMBIGUOUS near-miss: within MAX_SUGGESTION_DISTANCE AND strictly closer than
|
|
293
|
+
* every other candidate. So apostrophe/accent/typo slips get a suggestion, but a
|
|
294
|
+
* fragment equidistant from several similar headings (step-1/step-2/…,
|
|
295
|
+
* setup/setup-1, changelog versions) yields NO suggestion rather than an
|
|
296
|
+
* arbitrary, confidently-wrong one. Returns null when nothing qualifies.
|
|
297
|
+
*/
|
|
298
|
+
function closestSlug(fragment, slugSet) {
|
|
299
|
+
if (!fragment || !slugSet || slugSet.size === 0) return null;
|
|
300
|
+
let best = null;
|
|
301
|
+
let bestDist = Infinity;
|
|
302
|
+
let secondDist = Infinity;
|
|
303
|
+
for (const slug of slugSet) {
|
|
304
|
+
// Edit distance can't be below the length gap; skip candidates that can't
|
|
305
|
+
// beat the cap. Also bounds the Levenshtein work on very large pages, and
|
|
306
|
+
// can't hide a true tie (a distance ≤2 tie implies a length gap ≤2).
|
|
307
|
+
if (Math.abs(fragment.length - slug.length) > MAX_SUGGESTION_DISTANCE) continue;
|
|
308
|
+
const d = levenshtein(fragment, slug);
|
|
309
|
+
if (d < bestDist) {
|
|
310
|
+
secondDist = bestDist;
|
|
311
|
+
bestDist = d;
|
|
312
|
+
best = slug;
|
|
313
|
+
} else if (d < secondDist) {
|
|
314
|
+
secondDist = d;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Unambiguous near-miss: within the cap, not an exact match (≥1), and strictly
|
|
318
|
+
// closer than the runner-up (ties → no suggestion).
|
|
319
|
+
return best !== null &&
|
|
320
|
+
bestDist >= 1 &&
|
|
321
|
+
bestDist <= MAX_SUGGESTION_DISTANCE &&
|
|
322
|
+
bestDist < secondDist
|
|
323
|
+
? best
|
|
324
|
+
: null;
|
|
218
325
|
}
|
|
219
326
|
|
|
220
327
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
328
|
+
* Build the "Fragment #X not found in headings" warning message, appending a
|
|
329
|
+
* "Did you mean #Y?" hint when closestSlug found an unambiguous near-miss.
|
|
330
|
+
* Shared by both fragment-not-found sites in validateFile().
|
|
223
331
|
*/
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return count === 0 ? base : `${base}-${count + 1}`;
|
|
332
|
+
function fragmentNotFoundMessage(fragment, suggestion) {
|
|
333
|
+
return 'Fragment #' + fragment + ' not found in headings' +
|
|
334
|
+
(suggestion ? '. Did you mean #' + suggestion + '?' : '');
|
|
228
335
|
}
|
|
229
336
|
|
|
230
337
|
/**
|
|
@@ -239,7 +346,7 @@ function uniquifySlug(seen, base) {
|
|
|
239
346
|
*/
|
|
240
347
|
function extractHeadingSlugs(content) {
|
|
241
348
|
const slugs = new Set();
|
|
242
|
-
const
|
|
349
|
+
const slugger = createSlugger();
|
|
243
350
|
const lines = content.split('\n');
|
|
244
351
|
let inCodeBlock = false;
|
|
245
352
|
let fencePattern = '';
|
|
@@ -275,20 +382,20 @@ function extractHeadingSlugs(content) {
|
|
|
275
382
|
|
|
276
383
|
const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
277
384
|
if (headingMatch) {
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
385
|
+
const text = headingMatch[1].trim();
|
|
386
|
+
if (generateSlug(text)) slugs.add(slugger(text));
|
|
280
387
|
}
|
|
281
388
|
|
|
282
389
|
const updateMatch = line.match(/<Update\s+label=["']([^"']+)["']/);
|
|
283
390
|
if (updateMatch) {
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
391
|
+
const text = updateMatch[1];
|
|
392
|
+
if (generateSlug(text)) slugs.add(slugger(text));
|
|
286
393
|
}
|
|
287
394
|
|
|
288
395
|
if (inStepsBlock) {
|
|
289
396
|
for (const match of line.matchAll(STEP_TITLE_REGEX)) {
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
397
|
+
const text = match[1] ?? match[2];
|
|
398
|
+
if (generateSlug(text)) slugs.add(slugger(text));
|
|
292
399
|
}
|
|
293
400
|
}
|
|
294
401
|
|
|
@@ -387,12 +494,13 @@ function validateFile(filePath, contentDir, results, headingMap) {
|
|
|
387
494
|
if (!linkPath && fragment) {
|
|
388
495
|
const currentSlugs = headingMap.get(currentPagePath);
|
|
389
496
|
if (currentSlugs && !currentSlugs.has(fragment)) {
|
|
497
|
+
const suggestion = closestSlug(fragment, currentSlugs);
|
|
390
498
|
results.push({
|
|
391
499
|
type: 'broken_link',
|
|
392
500
|
file: relativePath,
|
|
393
501
|
line: lineNumber,
|
|
394
502
|
link: rawHref,
|
|
395
|
-
message:
|
|
503
|
+
message: fragmentNotFoundMessage(fragment, suggestion),
|
|
396
504
|
});
|
|
397
505
|
}
|
|
398
506
|
continue;
|
|
@@ -421,12 +529,13 @@ function validateFile(filePath, contentDir, results, headingMap) {
|
|
|
421
529
|
if (fragment) {
|
|
422
530
|
const targetSlugs = getTargetHeadingSlugs(targetPath, contentDir, headingMap);
|
|
423
531
|
if (targetSlugs && !targetSlugs.has(fragment)) {
|
|
532
|
+
const suggestion = closestSlug(fragment, targetSlugs);
|
|
424
533
|
results.push({
|
|
425
534
|
type: 'broken_link',
|
|
426
535
|
file: relativePath,
|
|
427
536
|
line: lineNumber,
|
|
428
537
|
link: rawHref,
|
|
429
|
-
message:
|
|
538
|
+
message: fragmentNotFoundMessage(fragment, suggestion),
|
|
430
539
|
});
|
|
431
540
|
}
|
|
432
541
|
}
|
|
@@ -552,7 +661,10 @@ if (require.main === module) {
|
|
|
552
661
|
const location = w.line ? `${w.file}:${w.line}` : w.file;
|
|
553
662
|
console.log(` ${location}`);
|
|
554
663
|
if (w.message) {
|
|
555
|
-
|
|
664
|
+
// message is a full sentence (may end in "…?"); keep the link on its
|
|
665
|
+
// own line so the output reads cleanly rather than "…mean #y?: #x".
|
|
666
|
+
console.log(` ${w.message}`);
|
|
667
|
+
console.log(` Link: ${w.link}`);
|
|
556
668
|
} else {
|
|
557
669
|
console.log(` Missing page: ${w.link}`);
|
|
558
670
|
}
|
|
@@ -566,4 +678,6 @@ if (require.main === module) {
|
|
|
566
678
|
}
|
|
567
679
|
|
|
568
680
|
// Export for programmatic use
|
|
569
|
-
|
|
681
|
+
// generateSlug + createSlugger are exported for the drift test
|
|
682
|
+
// (validate-links-slug.test.ts), which asserts they match the live github-slugger.
|
|
683
|
+
module.exports = { validateProject, generateSlug, createSlugger, closestSlug, levenshtein };
|
|
@@ -247,6 +247,14 @@ html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-d
|
|
|
247
247
|
background-color: transparent !important;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/* Embed render (widget modal): the LayoutWrapper embed branch wraps content in
|
|
251
|
+
a solid-bg div (for decoration:"none"/dark/non-jam readability). In gradient
|
|
252
|
+
mode, make that wrapper transparent so the body decoration shows through —
|
|
253
|
+
same as the chrome wrappers above. */
|
|
254
|
+
html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-decoration="grid"]) [data-embed="true"] {
|
|
255
|
+
background-color: transparent !important;
|
|
256
|
+
}
|
|
257
|
+
|
|
250
258
|
html:not(.dark) body[data-theme="jam"]:not([data-decoration="none"]):not([data-decoration="grid"]) header {
|
|
251
259
|
background-color: transparent !important;
|
|
252
260
|
background: transparent !important;
|