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.
Files changed (33) hide show
  1. package/dist/commands/broken-links.d.ts +2 -0
  2. package/dist/commands/broken-links.d.ts.map +1 -1
  3. package/dist/commands/broken-links.js +11 -2
  4. package/dist/commands/broken-links.js.map +1 -1
  5. package/dist/lib/deps.d.ts.map +1 -1
  6. package/dist/lib/deps.js +5 -0
  7. package/dist/lib/deps.js.map +1 -1
  8. package/package.json +2 -1
  9. package/vendored/app/api/markdown-export/[project]/[...slug]/route.ts +71 -1
  10. package/vendored/app/layout.tsx +8 -0
  11. package/vendored/components/AIActionsMenu.tsx +72 -3
  12. package/vendored/components/layout/EmbedLinkInterceptor.tsx +37 -0
  13. package/vendored/components/layout/LayoutWrapper.tsx +29 -1
  14. package/vendored/components/layout/PageColumns.tsx +10 -5
  15. package/vendored/components/navigation/Breadcrumb.tsx +6 -1
  16. package/vendored/components/navigation/SocialFooter.tsx +40 -17
  17. package/vendored/lib/api-spec-menu-gate.ts +49 -0
  18. package/vendored/lib/api-spec-offer.ts +50 -0
  19. package/vendored/lib/api-specs-bundle.ts +255 -0
  20. package/vendored/lib/api-specs-markdown-hint.ts +49 -0
  21. package/vendored/lib/api-specs-route.ts +45 -0
  22. package/vendored/lib/docs-types.ts +1 -1
  23. package/vendored/lib/heading-extractor.ts +34 -30
  24. package/vendored/lib/layout-helpers.tsx +19 -2
  25. package/vendored/lib/middleware-helpers.ts +29 -0
  26. package/vendored/lib/render-doc-page.tsx +93 -76
  27. package/vendored/lib/scanner-blocklist.ts +7 -0
  28. package/vendored/lib/static-artifacts.ts +39 -9
  29. package/vendored/lib/static-file-route.ts +35 -1
  30. package/vendored/scripts/github-slugger-regex.cjs +13 -0
  31. package/vendored/scripts/validate-links.cjs +136 -22
  32. package/vendored/themes/jam/variables.css +8 -0
  33. 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
- * Generate URL-friendly slug. Must match generateSlug in heading-extractor.ts.
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
- .toLowerCase()
216
- .replace(/[^a-z0-9]+/g, '-')
217
- .replace(/^-+|-+$/g, '');
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
- * Mirrors uniquifySlug in lib/heading-extractor.ts. First occurrence keeps the
222
- * bare slug; later collisions get -2, -3, ... suffixes in source order.
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 uniquifySlug(seen, base) {
225
- const count = seen.get(base) || 0;
226
- seen.set(base, count + 1);
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 seen = new Map();
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 base = generateSlug(headingMatch[1].trim());
279
- if (base) slugs.add(uniquifySlug(seen, base));
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 base = generateSlug(updateMatch[1]);
285
- if (base) slugs.add(uniquifySlug(seen, base));
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 base = generateSlug(match[1] ?? match[2]);
291
- if (base) slugs.add(uniquifySlug(seen, base));
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: 'Fragment #' + fragment + ' not found in headings',
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: 'Fragment #' + fragment + ' not found in headings',
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
- console.log(` ${w.message}: ${w.link}`);
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
- module.exports = { validateProject };
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;