hono-decks 0.2.1 → 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/README.ja.md CHANGED
@@ -12,6 +12,8 @@ bunx hono-decks init
12
12
  bunx hono-decks compile
13
13
  ```
14
14
 
15
+ AI agent を使う場合は `npx @tanstack/intent@latest install` を実行してください。公開パッケージには、agent が `node_modules` から検出できるバージョン付きの hono-decks Skill が含まれます。
16
+
15
17
  `init` は次の2ファイルを作ります。既存ファイルは上書きしません。
16
18
 
17
19
  - `hono-decks.config.ts`: CLI と runtime が共有する唯一の設定
package/README.md CHANGED
@@ -14,6 +14,8 @@ bunx hono-decks init
14
14
  bunx hono-decks compile
15
15
  ~~~
16
16
 
17
+ If you use an AI agent, run `npx @tanstack/intent@latest install`. The published package includes a versioned hono-decks skill that the agent can discover from `node_modules`.
18
+
17
19
  <code>init</code> creates two files without overwriting existing files:
18
20
 
19
21
  - <code>hono-decks.config.ts</code>: the shared configuration for the CLI and runtime
package/dist/advanced.js CHANGED
@@ -514,7 +514,7 @@ function localAssetRelativePath(sourcePath) {
514
514
  return markerIndex === -1 ? normalized.split("/").at(-1) ?? normalized : normalized.slice(markerIndex + 8);
515
515
  }
516
516
  function normalizeVoidElementSpacing(html) {
517
- return html.replace(/<(img|br|hr)([^>]*)\/>/g, "<$1$2 />");
517
+ return html.replace(/<(img|br|hr)([^<>]*)\/>/g, "<$1$2 />");
518
518
  }
519
519
  //#endregion
520
520
  //#region src/renderer/control-icons.ts
@@ -1612,7 +1612,11 @@ function createDeckPaths(mountPath, slug) {
1612
1612
  function normalizeMountPath(value) {
1613
1613
  const trimmed = value.trim();
1614
1614
  if (!trimmed || trimmed === "/") return "/";
1615
- return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
1615
+ let start = 0;
1616
+ let end = trimmed.length;
1617
+ while (trimmed[start] === "/") start += 1;
1618
+ while (end > start && trimmed[end - 1] === "/") end -= 1;
1619
+ return `/${trimmed.slice(start, end)}`;
1616
1620
  }
1617
1621
  //#endregion
1618
1622
  //#region src/server/browser-export.ts
@@ -1749,7 +1753,12 @@ function recordValue(value) {
1749
1753
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
1750
1754
  }
1751
1755
  function safeFilename$1(value) {
1752
- return value.trim().replaceAll(/[^a-zA-Z0-9._-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "deck";
1756
+ const sanitized = value.trim().replaceAll(/[^a-zA-Z0-9._-]+/g, "-");
1757
+ let start = 0;
1758
+ let end = sanitized.length;
1759
+ while (sanitized[start] === "-") start += 1;
1760
+ while (end > start && sanitized[end - 1] === "-") end -= 1;
1761
+ return sanitized.slice(start, end) || "deck";
1753
1762
  }
1754
1763
  //#endregion
1755
1764
  //#region src/server/viewer-script.ts
@@ -3225,7 +3234,7 @@ async function resolveBucket(bucket, c) {
3225
3234
  return typeof bucket === "function" ? bucket(c) : bucket;
3226
3235
  }
3227
3236
  function findRoutableAsset(assets, slug, assetPath) {
3228
- const normalized = assetPath.replace(/^\/+/, "");
3237
+ const normalized = stripLeadingSlashes(assetPath);
3229
3238
  return assets.find((asset) => {
3230
3239
  if (asset.type !== "local" && asset.type !== "r2") return false;
3231
3240
  const suffix = `/${slug}/assets/${normalized}`;
@@ -3236,8 +3245,19 @@ function resolveR2Key(input, options) {
3236
3245
  if (options.key) return options.key(input);
3237
3246
  if (input.asset.r2Key) return input.asset.r2Key;
3238
3247
  const key = input.asset.sourcePath || `${input.slug}/assets/${input.assetPath}`;
3239
- const prefix = options.keyPrefix?.replace(/\/+$/, "");
3240
- return prefix ? `${prefix}/${key.replace(/^\/+/, "")}` : key.replace(/^\/+/, "");
3248
+ const prefix = options.keyPrefix ? stripTrailingSlashes(options.keyPrefix) : void 0;
3249
+ const normalizedKey = stripLeadingSlashes(key);
3250
+ return prefix ? `${prefix}/${normalizedKey}` : normalizedKey;
3251
+ }
3252
+ function stripLeadingSlashes(value) {
3253
+ let index = 0;
3254
+ while (value[index] === "/") index += 1;
3255
+ return value.slice(index);
3256
+ }
3257
+ function stripTrailingSlashes(value) {
3258
+ let end = value.length;
3259
+ while (end > 0 && value[end - 1] === "/") end -= 1;
3260
+ return value.slice(0, end);
3241
3261
  }
3242
3262
  async function responseFromR2Object(object, input, options) {
3243
3263
  const body = object.body ?? (object.arrayBuffer ? await object.arrayBuffer() : void 0);
package/dist/bin.js CHANGED
@@ -350,8 +350,8 @@ function addExternalAssetWarnings(warnings, assets) {
350
350
  }
351
351
  function collectMarkdownAssetCandidates(markdown) {
352
352
  const candidates = [];
353
- for (const match of markdown.matchAll(/^\s*(?:background|image|src|asset):\s*['"]?([^'"\n]+)['"]?\s*$/gim)) candidates.push(match[1].trim());
354
- for (const match of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) candidates.push(match[1].trim());
353
+ candidates.push(...collectDirectiveAssetCandidates(markdown));
354
+ candidates.push(...collectMarkdownImageCandidates(markdown));
355
355
  for (const match of markdown.matchAll(/\b(?:src|href|image|background)=["']([^"']+)["']/g)) candidates.push(match[1].trim());
356
356
  return candidates;
357
357
  }
@@ -361,12 +361,75 @@ function collectFrontmatterAssetCandidates(value) {
361
361
  return [];
362
362
  }
363
363
  function contentTypeForPath(path) {
364
- const pathname = path.replace(/\?.*$/, "").toLowerCase();
365
- if (/\.png(?:#.*)?$/.test(pathname)) return "image/png";
366
- if (/\.jpe?g(?:#.*)?$/.test(pathname)) return "image/jpeg";
367
- if (/\.gif(?:#.*)?$/.test(pathname)) return "image/gif";
368
- if (/\.svg(?:#.*)?$/.test(pathname)) return "image/svg+xml";
369
- if (/\.webp(?:#.*)?$/.test(pathname)) return "image/webp";
364
+ const queryIndex = path.indexOf("?");
365
+ const pathname = (queryIndex === -1 ? path : path.slice(0, queryIndex)).toLowerCase();
366
+ if (hasExtension(pathname, ".png")) return "image/png";
367
+ if (hasExtension(pathname, ".jpg") || hasExtension(pathname, ".jpeg")) return "image/jpeg";
368
+ if (hasExtension(pathname, ".gif")) return "image/gif";
369
+ if (hasExtension(pathname, ".svg")) return "image/svg+xml";
370
+ if (hasExtension(pathname, ".webp")) return "image/webp";
371
+ }
372
+ function hasExtension(path, extension) {
373
+ return path.endsWith(extension) || path.includes(`${extension}#`);
374
+ }
375
+ function collectDirectiveAssetCandidates(markdown) {
376
+ const candidates = [];
377
+ for (const sourceLine of markdown.split("\n")) {
378
+ const line = sourceLine.endsWith("\r") ? sourceLine.slice(0, -1).trim() : sourceLine.trim();
379
+ const separator = line.indexOf(":");
380
+ if (separator === -1) continue;
381
+ const key = line.slice(0, separator).toLowerCase();
382
+ if (key !== "background" && key !== "image" && key !== "src" && key !== "asset") continue;
383
+ let value = line.slice(separator + 1).trim();
384
+ if (value.startsWith("\"") || value.startsWith("'")) value = value.slice(1);
385
+ if (value.endsWith("\"") || value.endsWith("'")) value = value.slice(0, -1);
386
+ value = value.trim();
387
+ if (value && !value.includes("\"") && !value.includes("'")) candidates.push(value);
388
+ }
389
+ return candidates;
390
+ }
391
+ function collectMarkdownImageCandidates(markdown) {
392
+ const candidates = [];
393
+ let index = 0;
394
+ while (index < markdown.length - 1) {
395
+ const imageStart = markdown.indexOf("![", index);
396
+ if (imageStart === -1) break;
397
+ const labelEnd = markdown.indexOf("]", imageStart + 2);
398
+ if (labelEnd === -1) break;
399
+ if (markdown[labelEnd + 1] !== "(") {
400
+ index = labelEnd + 1;
401
+ continue;
402
+ }
403
+ let cursor = labelEnd + 2;
404
+ const destinationStart = cursor;
405
+ while (cursor < markdown.length && markdown[cursor] !== ")" && !isMarkdownWhitespace(markdown[cursor])) cursor += 1;
406
+ if (cursor === destinationStart) {
407
+ index = cursor + 1;
408
+ continue;
409
+ }
410
+ const destination = markdown.slice(destinationStart, cursor);
411
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
412
+ if (markdown[cursor] !== ")") {
413
+ const quote = markdown[cursor];
414
+ if (quote !== "\"" && quote !== "'") {
415
+ index = cursor + 1;
416
+ continue;
417
+ }
418
+ cursor += 1;
419
+ while (cursor < markdown.length && markdown[cursor] !== "\"" && markdown[cursor] !== "'") cursor += 1;
420
+ if (cursor >= markdown.length) break;
421
+ cursor += 1;
422
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
423
+ }
424
+ if (markdown[cursor] === ")") {
425
+ candidates.push(destination);
426
+ index = cursor + 1;
427
+ } else index = cursor + 1;
428
+ }
429
+ return candidates;
430
+ }
431
+ function isMarkdownWhitespace(value) {
432
+ return value === " " || value === " " || value === "\r" || value === "\n";
370
433
  }
371
434
  function assetRefType(value) {
372
435
  if (/^https?:\/\//i.test(value)) return "remote";
@@ -1720,7 +1783,7 @@ function absoluteUrl(value, base) {
1720
1783
  }
1721
1784
  }
1722
1785
  function decodeHtmlEntities(value) {
1723
- return value.replaceAll("&amp;", "&").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1786
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
1724
1787
  }
1725
1788
  function isHttpUrl(value) {
1726
1789
  try {
package/dist/cli.js CHANGED
@@ -349,8 +349,8 @@ function addExternalAssetWarnings(warnings, assets) {
349
349
  }
350
350
  function collectMarkdownAssetCandidates(markdown) {
351
351
  const candidates = [];
352
- for (const match of markdown.matchAll(/^\s*(?:background|image|src|asset):\s*['"]?([^'"\n]+)['"]?\s*$/gim)) candidates.push(match[1].trim());
353
- for (const match of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) candidates.push(match[1].trim());
352
+ candidates.push(...collectDirectiveAssetCandidates(markdown));
353
+ candidates.push(...collectMarkdownImageCandidates(markdown));
354
354
  for (const match of markdown.matchAll(/\b(?:src|href|image|background)=["']([^"']+)["']/g)) candidates.push(match[1].trim());
355
355
  return candidates;
356
356
  }
@@ -360,12 +360,75 @@ function collectFrontmatterAssetCandidates(value) {
360
360
  return [];
361
361
  }
362
362
  function contentTypeForPath(path) {
363
- const pathname = path.replace(/\?.*$/, "").toLowerCase();
364
- if (/\.png(?:#.*)?$/.test(pathname)) return "image/png";
365
- if (/\.jpe?g(?:#.*)?$/.test(pathname)) return "image/jpeg";
366
- if (/\.gif(?:#.*)?$/.test(pathname)) return "image/gif";
367
- if (/\.svg(?:#.*)?$/.test(pathname)) return "image/svg+xml";
368
- if (/\.webp(?:#.*)?$/.test(pathname)) return "image/webp";
363
+ const queryIndex = path.indexOf("?");
364
+ const pathname = (queryIndex === -1 ? path : path.slice(0, queryIndex)).toLowerCase();
365
+ if (hasExtension(pathname, ".png")) return "image/png";
366
+ if (hasExtension(pathname, ".jpg") || hasExtension(pathname, ".jpeg")) return "image/jpeg";
367
+ if (hasExtension(pathname, ".gif")) return "image/gif";
368
+ if (hasExtension(pathname, ".svg")) return "image/svg+xml";
369
+ if (hasExtension(pathname, ".webp")) return "image/webp";
370
+ }
371
+ function hasExtension(path, extension) {
372
+ return path.endsWith(extension) || path.includes(`${extension}#`);
373
+ }
374
+ function collectDirectiveAssetCandidates(markdown) {
375
+ const candidates = [];
376
+ for (const sourceLine of markdown.split("\n")) {
377
+ const line = sourceLine.endsWith("\r") ? sourceLine.slice(0, -1).trim() : sourceLine.trim();
378
+ const separator = line.indexOf(":");
379
+ if (separator === -1) continue;
380
+ const key = line.slice(0, separator).toLowerCase();
381
+ if (key !== "background" && key !== "image" && key !== "src" && key !== "asset") continue;
382
+ let value = line.slice(separator + 1).trim();
383
+ if (value.startsWith("\"") || value.startsWith("'")) value = value.slice(1);
384
+ if (value.endsWith("\"") || value.endsWith("'")) value = value.slice(0, -1);
385
+ value = value.trim();
386
+ if (value && !value.includes("\"") && !value.includes("'")) candidates.push(value);
387
+ }
388
+ return candidates;
389
+ }
390
+ function collectMarkdownImageCandidates(markdown) {
391
+ const candidates = [];
392
+ let index = 0;
393
+ while (index < markdown.length - 1) {
394
+ const imageStart = markdown.indexOf("![", index);
395
+ if (imageStart === -1) break;
396
+ const labelEnd = markdown.indexOf("]", imageStart + 2);
397
+ if (labelEnd === -1) break;
398
+ if (markdown[labelEnd + 1] !== "(") {
399
+ index = labelEnd + 1;
400
+ continue;
401
+ }
402
+ let cursor = labelEnd + 2;
403
+ const destinationStart = cursor;
404
+ while (cursor < markdown.length && markdown[cursor] !== ")" && !isMarkdownWhitespace(markdown[cursor])) cursor += 1;
405
+ if (cursor === destinationStart) {
406
+ index = cursor + 1;
407
+ continue;
408
+ }
409
+ const destination = markdown.slice(destinationStart, cursor);
410
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
411
+ if (markdown[cursor] !== ")") {
412
+ const quote = markdown[cursor];
413
+ if (quote !== "\"" && quote !== "'") {
414
+ index = cursor + 1;
415
+ continue;
416
+ }
417
+ cursor += 1;
418
+ while (cursor < markdown.length && markdown[cursor] !== "\"" && markdown[cursor] !== "'") cursor += 1;
419
+ if (cursor >= markdown.length) break;
420
+ cursor += 1;
421
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
422
+ }
423
+ if (markdown[cursor] === ")") {
424
+ candidates.push(destination);
425
+ index = cursor + 1;
426
+ } else index = cursor + 1;
427
+ }
428
+ return candidates;
429
+ }
430
+ function isMarkdownWhitespace(value) {
431
+ return value === " " || value === " " || value === "\r" || value === "\n";
369
432
  }
370
433
  function assetRefType(value) {
371
434
  if (/^https?:\/\//i.test(value)) return "remote";
@@ -1719,7 +1782,7 @@ function absoluteUrl(value, base) {
1719
1782
  }
1720
1783
  }
1721
1784
  function decodeHtmlEntities(value) {
1722
- return value.replaceAll("&amp;", "&").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1785
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
1723
1786
  }
1724
1787
  function isHttpUrl(value) {
1725
1788
  try {
package/dist/mod.js CHANGED
@@ -431,7 +431,11 @@ function createDeckPaths(mountPath, slug) {
431
431
  function normalizeMountPath(value) {
432
432
  const trimmed = value.trim();
433
433
  if (!trimmed || trimmed === "/") return "/";
434
- return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
434
+ let start = 0;
435
+ let end = trimmed.length;
436
+ while (trimmed[start] === "/") start += 1;
437
+ while (end > start && trimmed[end - 1] === "/") end -= 1;
438
+ return `/${trimmed.slice(start, end)}`;
435
439
  }
436
440
  //#endregion
437
441
  //#region src/server/viewer-script.ts
@@ -1068,7 +1072,7 @@ async function resolveBucket(bucket, c) {
1068
1072
  return typeof bucket === "function" ? bucket(c) : bucket;
1069
1073
  }
1070
1074
  function findRoutableAsset(assets, slug, assetPath) {
1071
- const normalized = assetPath.replace(/^\/+/, "");
1075
+ const normalized = stripLeadingSlashes(assetPath);
1072
1076
  return assets.find((asset) => {
1073
1077
  if (asset.type !== "local" && asset.type !== "r2") return false;
1074
1078
  const suffix = `/${slug}/assets/${normalized}`;
@@ -1079,8 +1083,19 @@ function resolveR2Key(input, options) {
1079
1083
  if (options.key) return options.key(input);
1080
1084
  if (input.asset.r2Key) return input.asset.r2Key;
1081
1085
  const key = input.asset.sourcePath || `${input.slug}/assets/${input.assetPath}`;
1082
- const prefix = options.keyPrefix?.replace(/\/+$/, "");
1083
- return prefix ? `${prefix}/${key.replace(/^\/+/, "")}` : key.replace(/^\/+/, "");
1086
+ const prefix = options.keyPrefix ? stripTrailingSlashes(options.keyPrefix) : void 0;
1087
+ const normalizedKey = stripLeadingSlashes(key);
1088
+ return prefix ? `${prefix}/${normalizedKey}` : normalizedKey;
1089
+ }
1090
+ function stripLeadingSlashes(value) {
1091
+ let index = 0;
1092
+ while (value[index] === "/") index += 1;
1093
+ return value.slice(index);
1094
+ }
1095
+ function stripTrailingSlashes(value) {
1096
+ let end = value.length;
1097
+ while (end > 0 && value[end - 1] === "/") end -= 1;
1098
+ return value.slice(0, end);
1084
1099
  }
1085
1100
  async function responseFromR2Object(object, input, options) {
1086
1101
  const body = object.body ?? (object.arrayBuffer ? await object.arrayBuffer() : void 0);
package/dist/node.js CHANGED
@@ -363,8 +363,8 @@ function addExternalAssetWarnings(warnings, assets) {
363
363
  }
364
364
  function collectMarkdownAssetCandidates(markdown) {
365
365
  const candidates = [];
366
- for (const match of markdown.matchAll(/^\s*(?:background|image|src|asset):\s*['"]?([^'"\n]+)['"]?\s*$/gim)) candidates.push(match[1].trim());
367
- for (const match of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) candidates.push(match[1].trim());
366
+ candidates.push(...collectDirectiveAssetCandidates(markdown));
367
+ candidates.push(...collectMarkdownImageCandidates(markdown));
368
368
  for (const match of markdown.matchAll(/\b(?:src|href|image|background)=["']([^"']+)["']/g)) candidates.push(match[1].trim());
369
369
  return candidates;
370
370
  }
@@ -379,12 +379,75 @@ function isLocalRelativeAssetCandidate(value) {
379
379
  return /^[^/:?#]+\.(?:png|jpe?g|gif|svg|webp)(?:[?#].*)?$/i.test(value);
380
380
  }
381
381
  function contentTypeForPath(path) {
382
- const pathname = path.replace(/\?.*$/, "").toLowerCase();
383
- if (/\.png(?:#.*)?$/.test(pathname)) return "image/png";
384
- if (/\.jpe?g(?:#.*)?$/.test(pathname)) return "image/jpeg";
385
- if (/\.gif(?:#.*)?$/.test(pathname)) return "image/gif";
386
- if (/\.svg(?:#.*)?$/.test(pathname)) return "image/svg+xml";
387
- if (/\.webp(?:#.*)?$/.test(pathname)) return "image/webp";
382
+ const queryIndex = path.indexOf("?");
383
+ const pathname = (queryIndex === -1 ? path : path.slice(0, queryIndex)).toLowerCase();
384
+ if (hasExtension(pathname, ".png")) return "image/png";
385
+ if (hasExtension(pathname, ".jpg") || hasExtension(pathname, ".jpeg")) return "image/jpeg";
386
+ if (hasExtension(pathname, ".gif")) return "image/gif";
387
+ if (hasExtension(pathname, ".svg")) return "image/svg+xml";
388
+ if (hasExtension(pathname, ".webp")) return "image/webp";
389
+ }
390
+ function hasExtension(path, extension) {
391
+ return path.endsWith(extension) || path.includes(`${extension}#`);
392
+ }
393
+ function collectDirectiveAssetCandidates(markdown) {
394
+ const candidates = [];
395
+ for (const sourceLine of markdown.split("\n")) {
396
+ const line = sourceLine.endsWith("\r") ? sourceLine.slice(0, -1).trim() : sourceLine.trim();
397
+ const separator = line.indexOf(":");
398
+ if (separator === -1) continue;
399
+ const key = line.slice(0, separator).toLowerCase();
400
+ if (key !== "background" && key !== "image" && key !== "src" && key !== "asset") continue;
401
+ let value = line.slice(separator + 1).trim();
402
+ if (value.startsWith("\"") || value.startsWith("'")) value = value.slice(1);
403
+ if (value.endsWith("\"") || value.endsWith("'")) value = value.slice(0, -1);
404
+ value = value.trim();
405
+ if (value && !value.includes("\"") && !value.includes("'")) candidates.push(value);
406
+ }
407
+ return candidates;
408
+ }
409
+ function collectMarkdownImageCandidates(markdown) {
410
+ const candidates = [];
411
+ let index = 0;
412
+ while (index < markdown.length - 1) {
413
+ const imageStart = markdown.indexOf("![", index);
414
+ if (imageStart === -1) break;
415
+ const labelEnd = markdown.indexOf("]", imageStart + 2);
416
+ if (labelEnd === -1) break;
417
+ if (markdown[labelEnd + 1] !== "(") {
418
+ index = labelEnd + 1;
419
+ continue;
420
+ }
421
+ let cursor = labelEnd + 2;
422
+ const destinationStart = cursor;
423
+ while (cursor < markdown.length && markdown[cursor] !== ")" && !isMarkdownWhitespace(markdown[cursor])) cursor += 1;
424
+ if (cursor === destinationStart) {
425
+ index = cursor + 1;
426
+ continue;
427
+ }
428
+ const destination = markdown.slice(destinationStart, cursor);
429
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
430
+ if (markdown[cursor] !== ")") {
431
+ const quote = markdown[cursor];
432
+ if (quote !== "\"" && quote !== "'") {
433
+ index = cursor + 1;
434
+ continue;
435
+ }
436
+ cursor += 1;
437
+ while (cursor < markdown.length && markdown[cursor] !== "\"" && markdown[cursor] !== "'") cursor += 1;
438
+ if (cursor >= markdown.length) break;
439
+ cursor += 1;
440
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
441
+ }
442
+ if (markdown[cursor] === ")") {
443
+ candidates.push(destination);
444
+ index = cursor + 1;
445
+ } else index = cursor + 1;
446
+ }
447
+ return candidates;
448
+ }
449
+ function isMarkdownWhitespace(value) {
450
+ return value === " " || value === " " || value === "\r" || value === "\n";
388
451
  }
389
452
  function assetRefType(value) {
390
453
  if (/^https?:\/\//i.test(value)) return "remote";
@@ -2066,7 +2129,7 @@ function absoluteUrl(value, base) {
2066
2129
  }
2067
2130
  }
2068
2131
  function decodeHtmlEntities(value) {
2069
- return value.replaceAll("&amp;", "&").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
2132
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
2070
2133
  }
2071
2134
  function isHttpUrl(value) {
2072
2135
  try {
@@ -2874,7 +2937,7 @@ function localAssetRelativePath(sourcePath) {
2874
2937
  return markerIndex === -1 ? normalized.split("/").at(-1) ?? normalized : normalized.slice(markerIndex + 8);
2875
2938
  }
2876
2939
  function normalizeVoidElementSpacing(html) {
2877
- return html.replace(/<(img|br|hr)([^>]*)\/>/g, "<$1$2 />");
2940
+ return html.replace(/<(img|br|hr)([^<>]*)\/>/g, "<$1$2 />");
2878
2941
  }
2879
2942
  //#endregion
2880
2943
  //#region src/renderer/control-icons.ts
@@ -3909,7 +3972,11 @@ function createDeckPaths(mountPath, slug) {
3909
3972
  function normalizeMountPath(value) {
3910
3973
  const trimmed = value.trim();
3911
3974
  if (!trimmed || trimmed === "/") return "/";
3912
- return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
3975
+ let start = 0;
3976
+ let end = trimmed.length;
3977
+ while (trimmed[start] === "/") start += 1;
3978
+ while (end > start && trimmed[end - 1] === "/") end -= 1;
3979
+ return `/${trimmed.slice(start, end)}`;
3913
3980
  }
3914
3981
  //#endregion
3915
3982
  //#region src/server/browser-export.ts
@@ -4046,7 +4113,12 @@ function recordValue(value) {
4046
4113
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
4047
4114
  }
4048
4115
  function safeFilename$1(value) {
4049
- return value.trim().replaceAll(/[^a-zA-Z0-9._-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "deck";
4116
+ const sanitized = value.trim().replaceAll(/[^a-zA-Z0-9._-]+/g, "-");
4117
+ let start = 0;
4118
+ let end = sanitized.length;
4119
+ while (sanitized[start] === "-") start += 1;
4120
+ while (end > start && sanitized[end - 1] === "-") end -= 1;
4121
+ return sanitized.slice(start, end) || "deck";
4050
4122
  }
4051
4123
  //#endregion
4052
4124
  //#region src/server/viewer-script.ts
package/dist/vite.js CHANGED
@@ -349,8 +349,8 @@ function addExternalAssetWarnings(warnings, assets) {
349
349
  }
350
350
  function collectMarkdownAssetCandidates(markdown) {
351
351
  const candidates = [];
352
- for (const match of markdown.matchAll(/^\s*(?:background|image|src|asset):\s*['"]?([^'"\n]+)['"]?\s*$/gim)) candidates.push(match[1].trim());
353
- for (const match of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) candidates.push(match[1].trim());
352
+ candidates.push(...collectDirectiveAssetCandidates(markdown));
353
+ candidates.push(...collectMarkdownImageCandidates(markdown));
354
354
  for (const match of markdown.matchAll(/\b(?:src|href|image|background)=["']([^"']+)["']/g)) candidates.push(match[1].trim());
355
355
  return candidates;
356
356
  }
@@ -360,12 +360,75 @@ function collectFrontmatterAssetCandidates(value) {
360
360
  return [];
361
361
  }
362
362
  function contentTypeForPath(path) {
363
- const pathname = path.replace(/\?.*$/, "").toLowerCase();
364
- if (/\.png(?:#.*)?$/.test(pathname)) return "image/png";
365
- if (/\.jpe?g(?:#.*)?$/.test(pathname)) return "image/jpeg";
366
- if (/\.gif(?:#.*)?$/.test(pathname)) return "image/gif";
367
- if (/\.svg(?:#.*)?$/.test(pathname)) return "image/svg+xml";
368
- if (/\.webp(?:#.*)?$/.test(pathname)) return "image/webp";
363
+ const queryIndex = path.indexOf("?");
364
+ const pathname = (queryIndex === -1 ? path : path.slice(0, queryIndex)).toLowerCase();
365
+ if (hasExtension(pathname, ".png")) return "image/png";
366
+ if (hasExtension(pathname, ".jpg") || hasExtension(pathname, ".jpeg")) return "image/jpeg";
367
+ if (hasExtension(pathname, ".gif")) return "image/gif";
368
+ if (hasExtension(pathname, ".svg")) return "image/svg+xml";
369
+ if (hasExtension(pathname, ".webp")) return "image/webp";
370
+ }
371
+ function hasExtension(path, extension) {
372
+ return path.endsWith(extension) || path.includes(`${extension}#`);
373
+ }
374
+ function collectDirectiveAssetCandidates(markdown) {
375
+ const candidates = [];
376
+ for (const sourceLine of markdown.split("\n")) {
377
+ const line = sourceLine.endsWith("\r") ? sourceLine.slice(0, -1).trim() : sourceLine.trim();
378
+ const separator = line.indexOf(":");
379
+ if (separator === -1) continue;
380
+ const key = line.slice(0, separator).toLowerCase();
381
+ if (key !== "background" && key !== "image" && key !== "src" && key !== "asset") continue;
382
+ let value = line.slice(separator + 1).trim();
383
+ if (value.startsWith("\"") || value.startsWith("'")) value = value.slice(1);
384
+ if (value.endsWith("\"") || value.endsWith("'")) value = value.slice(0, -1);
385
+ value = value.trim();
386
+ if (value && !value.includes("\"") && !value.includes("'")) candidates.push(value);
387
+ }
388
+ return candidates;
389
+ }
390
+ function collectMarkdownImageCandidates(markdown) {
391
+ const candidates = [];
392
+ let index = 0;
393
+ while (index < markdown.length - 1) {
394
+ const imageStart = markdown.indexOf("![", index);
395
+ if (imageStart === -1) break;
396
+ const labelEnd = markdown.indexOf("]", imageStart + 2);
397
+ if (labelEnd === -1) break;
398
+ if (markdown[labelEnd + 1] !== "(") {
399
+ index = labelEnd + 1;
400
+ continue;
401
+ }
402
+ let cursor = labelEnd + 2;
403
+ const destinationStart = cursor;
404
+ while (cursor < markdown.length && markdown[cursor] !== ")" && !isMarkdownWhitespace(markdown[cursor])) cursor += 1;
405
+ if (cursor === destinationStart) {
406
+ index = cursor + 1;
407
+ continue;
408
+ }
409
+ const destination = markdown.slice(destinationStart, cursor);
410
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
411
+ if (markdown[cursor] !== ")") {
412
+ const quote = markdown[cursor];
413
+ if (quote !== "\"" && quote !== "'") {
414
+ index = cursor + 1;
415
+ continue;
416
+ }
417
+ cursor += 1;
418
+ while (cursor < markdown.length && markdown[cursor] !== "\"" && markdown[cursor] !== "'") cursor += 1;
419
+ if (cursor >= markdown.length) break;
420
+ cursor += 1;
421
+ while (cursor < markdown.length && isMarkdownWhitespace(markdown[cursor])) cursor += 1;
422
+ }
423
+ if (markdown[cursor] === ")") {
424
+ candidates.push(destination);
425
+ index = cursor + 1;
426
+ } else index = cursor + 1;
427
+ }
428
+ return candidates;
429
+ }
430
+ function isMarkdownWhitespace(value) {
431
+ return value === " " || value === " " || value === "\r" || value === "\n";
369
432
  }
370
433
  function assetRefType(value) {
371
434
  if (/^https?:\/\//i.test(value)) return "remote";
@@ -1719,7 +1782,7 @@ function absoluteUrl(value, base) {
1719
1782
  }
1720
1783
  }
1721
1784
  function decodeHtmlEntities(value) {
1722
- return value.replaceAll("&amp;", "&").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1785
+ return value.replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
1723
1786
  }
1724
1787
  function isHttpUrl(value) {
1725
1788
  try {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "hono-decks",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Slide decks for Hono and Cloudflare Workers",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
7
7
  "hono",
8
8
  "mdx",
9
- "slides"
9
+ "slides",
10
+ "tanstack-intent"
10
11
  ],
11
12
  "homepage": "https://github.com/ts-76/hono-decks#readme",
12
13
  "bugs": {
@@ -25,7 +26,8 @@
25
26
  "files": [
26
27
  "dist",
27
28
  "README.md",
28
- "README.ja.md"
29
+ "README.ja.md",
30
+ "skills"
29
31
  ],
30
32
  "type": "module",
31
33
  "sideEffects": false,
@@ -62,9 +64,11 @@
62
64
  "build": "tsdown",
63
65
  "prepack": "bun run build",
64
66
  "decks:compile": "bun run build && node dist/bin.js compile",
67
+ "skills:validate": "intent validate skills",
68
+ "skills:stale": "intent stale .",
65
69
  "typecheck": "bun run build && tsc --noEmit && tsc --noEmit -p tsconfig.worker.json",
66
70
  "test": "bun run build && vp test run",
67
- "check": "bun run typecheck && bun run test"
71
+ "check": "bun run typecheck && bun run test && bun run skills:validate"
68
72
  },
69
73
  "dependencies": {
70
74
  "@mdx-js/mdx": "^3.1.1",
@@ -77,6 +81,7 @@
77
81
  "unified": "^11.0.5"
78
82
  },
79
83
  "devDependencies": {
84
+ "@tanstack/intent": "^0.3.6",
80
85
  "hono": "^4.12.30",
81
86
  "vite": "catalog:",
82
87
  "vite-plus": "catalog:",