@ox-content/vite-plugin 2.5.0 → 2.7.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/dist/index.cjs CHANGED
@@ -7377,6 +7377,32 @@ export const ${exportName}: NavItem[] = ${JSON.stringify(navItems, null, 2)} as
7377
7377
  */
7378
7378
  const DOCS_MANIFEST_FILE = ".ox-content-docs-manifest.json";
7379
7379
  const DOCS_DATA_FILE = "docs.json";
7380
+ const DOC_KIND_ORDER = [
7381
+ "function",
7382
+ "class",
7383
+ "interface",
7384
+ "type",
7385
+ "variable",
7386
+ "module"
7387
+ ];
7388
+ const DOC_KIND_PLURAL = {
7389
+ function: "functions",
7390
+ class: "classes",
7391
+ interface: "interfaces",
7392
+ type: "types",
7393
+ variable: "variables",
7394
+ module: "modules"
7395
+ };
7396
+ const DEFAULT_DOCS_INCLUDE = [
7397
+ "**/*.ts",
7398
+ "**/*.tsx",
7399
+ "**/*.js",
7400
+ "**/*.jsx",
7401
+ "**/*.mts",
7402
+ "**/*.mjs",
7403
+ "**/*.cts",
7404
+ "**/*.cjs"
7405
+ ];
7380
7406
  function escapeHtml$3(str) {
7381
7407
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
7382
7408
  }
@@ -7385,7 +7411,7 @@ function entryAnchor(name) {
7385
7411
  }
7386
7412
  function cleanSummaryText(text, maxLength = 120) {
7387
7413
  if (!text) return "";
7388
- const collapsed = text.replace(/\s+/g, " ").trim();
7414
+ const collapsed = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\s+/g, " ").trim();
7389
7415
  if (collapsed.length <= maxLength) return collapsed;
7390
7416
  return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
7391
7417
  }
@@ -7523,6 +7549,86 @@ function renderDetailsControlsHtml(targetSelector) {
7523
7549
  <button type="button" class="ox-api-controls__button" data-ox-api-toggle="collapse">Close all</button>
7524
7550
  </div>`;
7525
7551
  }
7552
+ function createEmptyEntryStats() {
7553
+ return {
7554
+ entries: 0,
7555
+ byKind: {},
7556
+ params: 0,
7557
+ returns: 0,
7558
+ examples: 0,
7559
+ deprecated: 0
7560
+ };
7561
+ }
7562
+ function summarizeEntries(entries) {
7563
+ const stats = createEmptyEntryStats();
7564
+ for (const entry of entries) {
7565
+ stats.entries++;
7566
+ stats.byKind[entry.kind] = (stats.byKind[entry.kind] ?? 0) + 1;
7567
+ stats.params += entry.params?.length ?? 0;
7568
+ stats.returns += entry.returns ? 1 : 0;
7569
+ stats.examples += entry.examples?.length ?? 0;
7570
+ stats.deprecated += entry.tags?.deprecated !== void 0 ? 1 : 0;
7571
+ }
7572
+ return stats;
7573
+ }
7574
+ function buildDocsSummary(docs) {
7575
+ const stats = summarizeEntries(docs.flatMap((doc) => doc.entries));
7576
+ const byKind = {};
7577
+ for (const kind of DOC_KIND_ORDER) {
7578
+ const count = stats.byKind[kind];
7579
+ if (count) byKind[kind] = count;
7580
+ }
7581
+ return {
7582
+ modules: docs.length,
7583
+ entries: stats.entries,
7584
+ byKind,
7585
+ params: stats.params,
7586
+ returns: stats.returns,
7587
+ examples: stats.examples,
7588
+ deprecated: stats.deprecated
7589
+ };
7590
+ }
7591
+ function renderStatsHtml(stats, moduleCount) {
7592
+ const items = [];
7593
+ if (moduleCount !== void 0) items.push({
7594
+ label: "modules",
7595
+ value: moduleCount
7596
+ });
7597
+ items.push({
7598
+ label: "symbols",
7599
+ value: stats.entries
7600
+ });
7601
+ for (const kind of DOC_KIND_ORDER) {
7602
+ const count = stats.byKind[kind];
7603
+ if (count) items.push({
7604
+ label: DOC_KIND_PLURAL[kind],
7605
+ value: count
7606
+ });
7607
+ }
7608
+ if (stats.params) items.push({
7609
+ label: "parameters",
7610
+ value: stats.params
7611
+ });
7612
+ if (stats.returns) items.push({
7613
+ label: "returns",
7614
+ value: stats.returns
7615
+ });
7616
+ if (stats.examples) items.push({
7617
+ label: "examples",
7618
+ value: stats.examples
7619
+ });
7620
+ if (stats.deprecated) items.push({
7621
+ label: "deprecated",
7622
+ value: stats.deprecated,
7623
+ tone: "warning"
7624
+ });
7625
+ return `<div class="ox-api-stats" aria-label="API reference summary">
7626
+ ${items.map((item) => `<span class="ox-api-stat${item.tone ? ` ox-api-stat--${item.tone}` : ""}">
7627
+ <strong>${item.value}</strong>
7628
+ <span>${escapeHtml$3(item.label)}</span>
7629
+ </span>`).join("\n")}
7630
+ </div>`;
7631
+ }
7526
7632
  function normalizeDocFilePath(filePath) {
7527
7633
  const normalized = filePath.replace(/\\/g, "/");
7528
7634
  return normalized.match(/(?:^|\/)((?:npm|packages|crates|src)\/.+)$/)?.[1] ?? normalized.replace(/^\/+/, "");
@@ -7531,6 +7637,7 @@ function buildDocsData(docs) {
7531
7637
  return {
7532
7638
  version: 1,
7533
7639
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7640
+ summary: buildDocsSummary(docs),
7534
7641
  modules: docs.map((doc) => ({
7535
7642
  ...doc,
7536
7643
  file: normalizeDocFilePath(doc.file),
@@ -7541,6 +7648,86 @@ function buildDocsData(docs) {
7541
7648
  }))
7542
7649
  };
7543
7650
  }
7651
+ function consumeJSDocType(value) {
7652
+ const trimmed = value.trimStart();
7653
+ if (!trimmed.startsWith("{")) return { rest: trimmed };
7654
+ let depth = 0;
7655
+ for (let index = 0; index < trimmed.length; index++) {
7656
+ const char = trimmed[index];
7657
+ if (char === "{") depth++;
7658
+ else if (char === "}") {
7659
+ depth--;
7660
+ if (depth === 0) return {
7661
+ type: trimmed.slice(1, index).trim() || void 0,
7662
+ rest: trimmed.slice(index + 1).trimStart()
7663
+ };
7664
+ }
7665
+ }
7666
+ return { rest: trimmed };
7667
+ }
7668
+ function cleanTagDescription(value) {
7669
+ return value.trim().replace(/^-\s*/, "").trim();
7670
+ }
7671
+ function splitTagNameAndDescription(value) {
7672
+ const trimmed = value.trimStart();
7673
+ if (trimmed.startsWith("[")) {
7674
+ const closeIndex = trimmed.indexOf("]");
7675
+ if (closeIndex >= 0) return {
7676
+ name: trimmed.slice(0, closeIndex + 1),
7677
+ description: trimmed.slice(closeIndex + 1).trimStart()
7678
+ };
7679
+ }
7680
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/u.exec(trimmed);
7681
+ return {
7682
+ name: match?.[1] ?? "",
7683
+ description: match?.[2] ?? ""
7684
+ };
7685
+ }
7686
+ function parseParamTagValue(value) {
7687
+ const { type, rest } = consumeJSDocType(value);
7688
+ const { name: rawName, description } = splitTagNameAndDescription(rest);
7689
+ let name = rawName.trim();
7690
+ if (!name) return null;
7691
+ let optional = false;
7692
+ let defaultValue;
7693
+ const optionalMatch = /^\[(.*)\]$/u.exec(name);
7694
+ if (optionalMatch) {
7695
+ optional = true;
7696
+ const [innerName, innerDefault] = optionalMatch[1].split(/=(.*)/su);
7697
+ name = innerName.trim();
7698
+ defaultValue = innerDefault?.trim() || void 0;
7699
+ }
7700
+ if (!name) return null;
7701
+ return {
7702
+ name,
7703
+ type: type || "unknown",
7704
+ description: cleanTagDescription(description),
7705
+ optional: optional || void 0,
7706
+ default: defaultValue
7707
+ };
7708
+ }
7709
+ function parseReturnsTagValue(value) {
7710
+ const { type, rest } = consumeJSDocType(value);
7711
+ return {
7712
+ type: type || "unknown",
7713
+ description: cleanTagDescription(rest)
7714
+ };
7715
+ }
7716
+ function normalizeReturnType(value) {
7717
+ const parsed = parseReturnsTagValue(value);
7718
+ return parsed.type === "unknown" ? value : parsed.type;
7719
+ }
7720
+ function mergeParam(params, next) {
7721
+ const existing = params.find((param) => param.name === next.name);
7722
+ if (!existing) {
7723
+ params.push(next);
7724
+ return;
7725
+ }
7726
+ if (next.type && (existing.type === "unknown" || next.type !== "unknown")) existing.type = next.type;
7727
+ if (next.description) existing.description = next.description;
7728
+ if (next.optional) existing.optional = true;
7729
+ if (next.default) existing.default = next.default;
7730
+ }
7544
7731
  /**
7545
7732
  * Extracts JSDoc documentation from source files in specified directories.
7546
7733
  *
@@ -7552,7 +7739,7 @@ function buildDocsData(docs) {
7552
7739
  *
7553
7740
  * 1. **File Discovery**: Recursively walks directories, applying filters
7554
7741
  * 2. **File Reading**: Loads each matching file's content
7555
- * 3. **JSDoc Extraction**: Parses JSDoc comments using regex patterns
7742
+ * 3. **JSDoc Extraction**: Parses JSDoc comments using the native parser
7556
7743
  * 4. **Declaration Matching**: Pairs JSDoc comments with source declarations
7557
7744
  * 5. **Result Collection**: Aggregates extracted documentation by file
7558
7745
  *
@@ -7692,32 +7879,29 @@ function parseNapiDocItem(item) {
7692
7879
  currentExample = "";
7693
7880
  inExample = false;
7694
7881
  }
7695
- const tagMatch = /@(\w+)\s*(?:\{([^}]*)\})?(.*)/.exec(lineText);
7882
+ const tagMatch = /^@(\S+)\s*([\s\S]*)$/u.exec(lineText);
7696
7883
  if (tagMatch) {
7697
- const [, tagName, tagType, tagRest] = tagMatch;
7884
+ const [, tagName, tagValue = ""] = tagMatch;
7698
7885
  switch (tagName) {
7699
7886
  case "param":
7700
- const paramMatch = /(\w+)\s*-?\s*(.*)/.exec(tagRest.trim());
7701
- if (paramMatch) params.push({
7702
- name: paramMatch[1],
7703
- type: tagType || "unknown",
7704
- description: paramMatch[2]
7705
- });
7887
+ case "arg":
7888
+ case "argument": {
7889
+ const param = parseParamTagValue(tagValue);
7890
+ if (param) mergeParam(params, param);
7706
7891
  break;
7892
+ }
7707
7893
  case "returns":
7708
7894
  case "return":
7709
- returns = {
7710
- type: tagType || "unknown",
7711
- description: tagRest.trim()
7712
- };
7895
+ returns = parseReturnsTagValue(tagValue);
7713
7896
  break;
7714
7897
  case "example":
7715
7898
  inExample = true;
7899
+ currentExample = tagValue.trim() ? `${tagValue.trim()}\n` : "";
7716
7900
  break;
7717
7901
  case "private":
7718
7902
  isPrivate = true;
7719
7903
  break;
7720
- default: tags[tagName] = tagRest.trim();
7904
+ default: tags[tagName] = tagValue.trim();
7721
7905
  }
7722
7906
  }
7723
7907
  } else if (inExample) currentExample += rawLine + "\n";
@@ -7725,32 +7909,41 @@ function parseNapiDocItem(item) {
7725
7909
  else description += "\n" + lineText;
7726
7910
  }
7727
7911
  if (inExample && currentExample) examples.push(currentExample.trim());
7728
- if (params.length === 0 && item.params.length > 0) params.push(...item.params.map((param) => ({
7729
- name: param.name,
7730
- type: param.typeAnnotation ?? "unknown",
7731
- description: param.description ?? "",
7732
- optional: param.optional || void 0,
7733
- default: param.defaultValue
7734
- })));
7735
- else if (item.params.length > 0) {
7736
- const paramMap = new Map(item.params.map((param) => [param.name, param]));
7737
- for (const param of params) {
7738
- const rustParam = paramMap.get(param.name);
7739
- if (!rustParam) continue;
7740
- if (param.type === "unknown" && rustParam.typeAnnotation) param.type = rustParam.typeAnnotation;
7741
- if (!param.description && rustParam.description) param.description = rustParam.description;
7742
- if (param.optional === void 0 && rustParam.optional) param.optional = true;
7743
- if (!param.default && rustParam.defaultValue) param.default = rustParam.defaultValue;
7744
- }
7912
+ for (const param of item.params) {
7913
+ if (params.length > 0 && param.name === "param" && !param.typeAnnotation && !param.description && !param.defaultValue) continue;
7914
+ mergeParam(params, {
7915
+ name: param.name,
7916
+ type: param.typeAnnotation ?? "unknown",
7917
+ description: param.description ?? "",
7918
+ optional: param.optional || void 0,
7919
+ default: param.defaultValue
7920
+ });
7745
7921
  }
7746
7922
  if (!returns && item.returnType) returns = {
7747
- type: item.returnType,
7923
+ type: normalizeReturnType(item.returnType),
7748
7924
  description: ""
7749
7925
  };
7750
- else if (returns && returns.type === "unknown" && item.returnType) returns.type = item.returnType;
7926
+ else if (returns && item.returnType) returns.type = normalizeReturnType(item.returnType);
7751
7927
  if (!description) description = item.doc ?? "";
7752
7928
  for (const tag of item.tags) {
7753
- if (tag.tag === "param" || tag.tag === "returns" || tag.tag === "return" || tag.tag === "example") continue;
7929
+ if (tag.tag === "param" || tag.tag === "arg" || tag.tag === "argument" || tag.tag === "returns" || tag.tag === "return") {
7930
+ if (tag.tag === "param" || tag.tag === "arg" || tag.tag === "argument") {
7931
+ const param = parseParamTagValue(tag.value);
7932
+ if (param) mergeParam(params, param);
7933
+ } else {
7934
+ const parsedReturns = parseReturnsTagValue(tag.value);
7935
+ if (!returns) returns = parsedReturns;
7936
+ else {
7937
+ returns.type = returns.type === "unknown" ? parsedReturns.type : returns.type;
7938
+ returns.description ||= parsedReturns.description;
7939
+ }
7940
+ }
7941
+ continue;
7942
+ }
7943
+ if (tag.tag === "example") {
7944
+ if (tag.value && !examples.includes(tag.value)) examples.push(tag.value);
7945
+ continue;
7946
+ }
7754
7947
  if (tag.tag === "private") {
7755
7948
  isPrivate = true;
7756
7949
  continue;
@@ -7837,6 +8030,7 @@ function generateFileMarkdown(doc, options, currentFileName, symbolMap) {
7837
8030
  }
7838
8031
  md += `> ${doc.entries.length} documented symbol${doc.entries.length === 1 ? "" : "s"}. `;
7839
8032
  md += "Read the signatures first, then expand each item for parameters, return types, and examples.\n\n";
8033
+ md += renderStatsHtml(summarizeEntries(doc.entries)) + "\n\n";
7840
8034
  md += "## Reference\n\n";
7841
8035
  if (doc.entries.length > 1) md += renderDetailsControlsHtml(".ox-api-entry") + "\n\n";
7842
8036
  for (const entry of doc.entries) md += generateEntryMarkdown(entry, options, currentFileName, symbolMap);
@@ -7856,6 +8050,42 @@ function formatKindLabel(kind) {
7856
8050
  default: return kind;
7857
8051
  }
7858
8052
  }
8053
+ function formatCountLabel(count, singular, plural = `${singular}s`) {
8054
+ return `${count} ${count === 1 ? singular : plural}`;
8055
+ }
8056
+ function getEntryBadges(entry) {
8057
+ const badges = [];
8058
+ if (entry.tags?.deprecated !== void 0) badges.push({
8059
+ label: "deprecated",
8060
+ tone: "warning"
8061
+ });
8062
+ if (entry.params?.length) badges.push({ label: formatCountLabel(entry.params.length, "param") });
8063
+ if (entry.returns) badges.push({ label: `returns ${entry.returns.type}` });
8064
+ if (entry.examples?.length) badges.push({ label: formatCountLabel(entry.examples.length, "example") });
8065
+ if (entry.tags?.since) badges.push({ label: `since ${entry.tags.since}` });
8066
+ if (entry.private) badges.push({
8067
+ label: "private",
8068
+ tone: "warning"
8069
+ });
8070
+ return badges;
8071
+ }
8072
+ function renderEntryBadgesHtml(entry, className) {
8073
+ const badges = getEntryBadges(entry);
8074
+ if (badges.length === 0) return "";
8075
+ return `<span class="${className}">${badges.map((badge) => `<span class="ox-api-badge${badge.tone ? ` ox-api-badge--${badge.tone}` : ""}">${escapeHtml$3(badge.label)}</span>`).join("")}</span>`;
8076
+ }
8077
+ function parseExampleBlock(example) {
8078
+ const trimmed = example.trim();
8079
+ const fenceMatch = /^```([\w-]+)?[^\n]*\n([\s\S]*?)\n?```$/u.exec(trimmed);
8080
+ if (!fenceMatch) return {
8081
+ code: trimmed,
8082
+ language: "ts"
8083
+ };
8084
+ return {
8085
+ code: fenceMatch[2],
8086
+ language: fenceMatch[1] || "ts"
8087
+ };
8088
+ }
7859
8089
  function renderOverviewLine(entry, href) {
7860
8090
  const signature = normalizeSignature(entry.signature);
7861
8091
  const summary = cleanSummaryText(entry.description, 88);
@@ -7867,8 +8097,9 @@ function renderOverviewLine(entry, href) {
7867
8097
  function renderOverviewHtmlItem(entry, href) {
7868
8098
  const signature = normalizeSignature(entry.signature);
7869
8099
  const summary = cleanSummaryText(entry.description, 88);
8100
+ const meta = renderEntryBadgesHtml(entry, "ox-api-module__meta");
7870
8101
  const heading = signature ? `<a href="${escapeHtml$3(href)}" class="ox-api-module__link">${renderHighlightedInlineCodeHtml(signature, "ox-api-module__signature ox-api-module__signature--highlighted")}</a>` : `<a href="${escapeHtml$3(href)}" class="ox-api-module__link"><code class="ox-api-module__name">${escapeHtml$3(entry.name)}</code></a>`;
7871
- return `<li><span class="ox-api-module__kind">${escapeHtml$3(formatKindLabel(entry.kind))}</span><div class="ox-api-module__item">${heading}${summary ? `<span class="ox-api-module__summary">${renderInlineHtml(summary)}</span>` : ""}</div></li>`;
8102
+ return `<li><span class="ox-api-module__kind">${escapeHtml$3(formatKindLabel(entry.kind))}</span><div class="ox-api-module__item">${heading}${summary ? `<span class="ox-api-module__summary">${renderInlineHtml(summary)}</span>` : ""}${meta}</div></li>`;
7872
8103
  }
7873
8104
  function renderParamsListHtml(params) {
7874
8105
  return `<div class="ox-api-entry__section ox-api-entry__section--params">
@@ -7900,6 +8131,10 @@ function generateEntryMarkdown(entry, options, currentFileName, symbolMap) {
7900
8131
  const sourceHref = options?.githubUrl ? generateSourceHref(entry.file, options.githubUrl, entry.line, entry.endLine) : void 0;
7901
8132
  let body = "";
7902
8133
  if (processedDescription) body += renderMarkdownBlocksHtml(processedDescription) + "\n";
8134
+ if (entry.signature) body += `<div class="ox-api-entry__section ox-api-entry__section--signature">
8135
+ <h4>Signature</h4>
8136
+ ${renderCodeBlockHtml(entry.signature, "typescript")}
8137
+ </div>\n`;
7903
8138
  if (sourceHref) body += `<p class="ox-api-entry__source"><a href="${escapeHtml$3(sourceHref)}">View source</a></p>\n`;
7904
8139
  if (entry.params && entry.params.length > 0) body += renderParamsListHtml(entry.params) + "\n";
7905
8140
  if (entry.returns) body += `<div class="ox-api-entry__section ox-api-entry__section--returns">
@@ -7910,13 +8145,19 @@ function generateEntryMarkdown(entry, options, currentFileName, symbolMap) {
7910
8145
  </div>
7911
8146
  </div>\n`;
7912
8147
  if (entry.examples && entry.examples.length > 0) {
7913
- const examplesHtml = entry.examples.map((example) => example.replace(/^```\w*\n?/, "").replace(/\n?```$/, "")).map((example) => renderCodeBlockHtml(example, "ts")).join("\n");
8148
+ const examplesHtml = entry.examples.map((example, index) => {
8149
+ const parsed = parseExampleBlock(example);
8150
+ return `<div class="ox-api-entry__example">
8151
+ <div class="ox-api-entry__example-heading">Example ${index + 1}</div>
8152
+ ${renderCodeBlockHtml(parsed.code, parsed.language)}
8153
+ </div>`;
8154
+ }).join("\n");
7914
8155
  body += `<div class="ox-api-entry__section ox-api-entry__section--examples">\n<h4>Examples</h4>\n${examplesHtml}\n</div>\n`;
7915
8156
  }
7916
8157
  if (entry.tags && Object.keys(entry.tags).length > 0) body += renderTagListHtml(entry.tags) + "\n";
7917
8158
  const summaryDescription = cleanSummaryText(processedDescription, summarySignature ? 80 : 120);
7918
8159
  const summaryHeading = summarySignature ? renderHighlightedInlineCodeHtml(summarySignature, "ox-api-entry__signature ox-api-entry__signature--highlighted") : `<code class="ox-api-entry__name">${escapeHtml$3(entry.name)}</code>`;
7919
- const summaryParts = [`<span class="ox-api-entry__kind">${escapeHtml$3(formatKindLabel(entry.kind))}</span>`, `<span class="ox-api-entry__summary-main">${summaryHeading}${summaryDescription ? `<span class="ox-api-entry__description">${renderInlineHtml(summaryDescription)}</span>` : ""}</span>`];
8160
+ const summaryParts = [`<span class="ox-api-entry__kind">${escapeHtml$3(formatKindLabel(entry.kind))}</span>`, `<span class="ox-api-entry__summary-main">${summaryHeading}${summaryDescription ? `<span class="ox-api-entry__description">${renderInlineHtml(summaryDescription)}</span>` : ""}${renderEntryBadgesHtml(entry, "ox-api-entry__meta")}</span>`];
7920
8161
  return `<details id="${entryAnchor(entry.name)}" class="ox-api-entry">
7921
8162
  <summary>${summaryParts.join("")}</summary>
7922
8163
  <div class="ox-api-entry__body">
@@ -7930,6 +8171,7 @@ function generateIndex(docs, docToFile) {
7930
8171
  let md = "# API Documentation\n\n";
7931
8172
  md += "Generated by [Ox Content](https://github.com/ubugeeei/ox-content)\n\n";
7932
8173
  md += "> Use search scopes like `@api transform` to limit results to the generated API reference.\n\n";
8174
+ md += renderStatsHtml(summarizeEntries(docs.flatMap((doc) => doc.entries)), docs.length) + "\n\n";
7933
8175
  md += "## Modules\n\n";
7934
8176
  if (docs.length > 1) md += renderDetailsControlsHtml(".ox-api-module") + "\n\n";
7935
8177
  for (const doc of docs) {
@@ -7959,6 +8201,7 @@ function generateCategoryMarkdown(kind, entries, options, symbolMap) {
7959
8201
  const categoryFileName = `${kind}s`;
7960
8202
  let md = `# ${kind.charAt(0).toUpperCase() + kind.slice(1)}s\n\n`;
7961
8203
  md += `> ${entries.length} documented ${kind}${entries.length === 1 ? "" : "s"} collected across modules.\n\n`;
8204
+ md += renderStatsHtml(summarizeEntries(entries)) + "\n\n";
7962
8205
  md += "## Overview\n\n";
7963
8206
  for (const entry of entries) md += renderOverviewLine(entry, `#${entryAnchor(entry.name)}`);
7964
8207
  md += "\n## Reference\n\n";
@@ -7969,6 +8212,7 @@ function generateCategoryMarkdown(kind, entries, options, symbolMap) {
7969
8212
  function generateCategoryIndex(byKind) {
7970
8213
  let md = "# API Documentation\n\n";
7971
8214
  md += "Generated by [Ox Content](https://github.com/ubugeeei/ox-content)\n\n";
8215
+ md += renderStatsHtml(summarizeEntries([...byKind.values()].flatMap((entries) => entries))) + "\n\n";
7972
8216
  for (const [kind, entries] of [...byKind.entries()].sort(([a], [b]) => compareStrings(a, b))) {
7973
8217
  const kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1) + "s";
7974
8218
  md += `## [${kindTitle}](./${kind}s.md)\n\n`;
@@ -8080,7 +8324,7 @@ function resolveDocsOptions(options) {
8080
8324
  enabled: opts.enabled ?? true,
8081
8325
  src: opts.src ?? ["./src"],
8082
8326
  out: opts.out ?? "docs/api",
8083
- include: opts.include ?? ["**/*.ts", "**/*.tsx"],
8327
+ include: opts.include ?? DEFAULT_DOCS_INCLUDE,
8084
8328
  exclude: opts.exclude ?? [
8085
8329
  "**/*.test.*",
8086
8330
  "**/*.spec.*",
@@ -9128,6 +9372,7 @@ function resolveTheme(config) {
9128
9372
  header: merged.header ?? defaultTheme.header,
9129
9373
  footer: merged.footer ?? defaultTheme.footer,
9130
9374
  socialLinks: merged.socialLinks ?? defaultTheme.socialLinks,
9375
+ sidebar: merged.sidebar ?? [],
9131
9376
  embed: merged.embed ?? {},
9132
9377
  css: merged.css ?? "",
9133
9378
  js: merged.js ?? ""
@@ -9137,6 +9382,7 @@ function resolveTheme(config) {
9137
9382
  * Converts resolved theme to the format expected by Rust NAPI.
9138
9383
  */
9139
9384
  function themeToNapi(theme) {
9385
+ const socialLinks = socialLinksToNapi(theme.socialLinks);
9140
9386
  return {
9141
9387
  colors: theme.colors.primary ? {
9142
9388
  primary: theme.colors.primary,
@@ -9182,16 +9428,30 @@ function themeToNapi(theme) {
9182
9428
  message: theme.footer.message,
9183
9429
  copyright: theme.footer.copyright
9184
9430
  } : void 0,
9185
- socialLinks: theme.socialLinks.github || theme.socialLinks.twitter || theme.socialLinks.discord ? {
9186
- github: theme.socialLinks.github,
9187
- twitter: theme.socialLinks.twitter,
9188
- discord: theme.socialLinks.discord
9189
- } : void 0,
9431
+ socialLinks,
9190
9432
  embed: Object.keys(theme.embed).length > 0 ? theme.embed : void 0,
9191
9433
  css: theme.css || void 0,
9192
9434
  js: theme.js || void 0
9193
9435
  };
9194
9436
  }
9437
+ function socialLinksToNapi(links) {
9438
+ if (Array.isArray(links)) {
9439
+ const items = links.map((item) => {
9440
+ return {
9441
+ icon: typeof item.icon === "string" ? item.icon : void 0,
9442
+ iconSvg: typeof item.icon === "object" ? item.icon.svg : void 0,
9443
+ link: item.link,
9444
+ ariaLabel: item.ariaLabel
9445
+ };
9446
+ });
9447
+ return items.length > 0 ? { links: items } : void 0;
9448
+ }
9449
+ return links.github || links.twitter || links.discord ? {
9450
+ github: links.github,
9451
+ twitter: links.twitter,
9452
+ discord: links.discord
9453
+ } : void 0;
9454
+ }
9195
9455
  //#endregion
9196
9456
  //#region src/ssg.ts
9197
9457
  /**
@@ -9700,6 +9960,40 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9700
9960
  border-color: color-mix(in srgb, var(--color-primary) 38%, var(--color-border));
9701
9961
  background: color-mix(in srgb, var(--color-bg-alt) 68%, var(--color-primary) 6%);
9702
9962
  }
9963
+ .content .ox-api-stats {
9964
+ display: grid;
9965
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
9966
+ gap: 0.55rem;
9967
+ margin: 1rem 0 1.35rem;
9968
+ }
9969
+ .content .ox-api-stat {
9970
+ min-width: 0;
9971
+ padding: 0.7rem 0.75rem;
9972
+ border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
9973
+ border-radius: 4px;
9974
+ background: color-mix(in srgb, var(--color-bg-alt) 72%, transparent);
9975
+ }
9976
+ .content .ox-api-stat strong {
9977
+ display: block;
9978
+ font-family: var(--font-mono);
9979
+ font-size: 1rem;
9980
+ line-height: 1.2;
9981
+ color: var(--color-text);
9982
+ }
9983
+ .content .ox-api-stat span {
9984
+ display: block;
9985
+ margin-top: 0.25rem;
9986
+ font-family: var(--font-mono);
9987
+ font-size: 0.7rem;
9988
+ font-weight: 600;
9989
+ letter-spacing: 0.04em;
9990
+ text-transform: uppercase;
9991
+ color: var(--color-text-muted);
9992
+ }
9993
+ .content .ox-api-stat--warning {
9994
+ border-color: color-mix(in srgb, #f59e0b 42%, var(--color-border));
9995
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, #f59e0b 8%);
9996
+ }
9703
9997
  .content .ox-api-entry,
9704
9998
  .content .ox-api-module {
9705
9999
  margin: 0;
@@ -9809,6 +10103,31 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9809
10103
  flex-direction: column;
9810
10104
  gap: 0.55rem;
9811
10105
  }
10106
+ .content .ox-api-entry__meta,
10107
+ .content .ox-api-module__meta {
10108
+ display: flex;
10109
+ flex-wrap: wrap;
10110
+ gap: 0.35rem;
10111
+ }
10112
+ .content .ox-api-badge {
10113
+ display: inline-flex;
10114
+ align-items: center;
10115
+ max-width: 100%;
10116
+ padding: 0.18rem 0.42rem;
10117
+ border: 1px solid color-mix(in srgb, var(--color-border) 78%, transparent);
10118
+ border-radius: 4px;
10119
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, var(--color-primary) 6%);
10120
+ color: color-mix(in srgb, var(--color-text) 74%, var(--color-text-muted));
10121
+ font-family: var(--font-mono);
10122
+ font-size: 0.68rem;
10123
+ font-weight: 600;
10124
+ line-height: 1.35;
10125
+ }
10126
+ .content .ox-api-badge--warning {
10127
+ border-color: color-mix(in srgb, #f59e0b 46%, var(--color-border));
10128
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, #f59e0b 9%);
10129
+ color: color-mix(in srgb, var(--color-text) 86%, #f59e0b);
10130
+ }
9812
10131
  .content .ox-api-entry__name,
9813
10132
  .content .ox-api-entry__signature,
9814
10133
  .content .ox-api-module__name,
@@ -9957,6 +10276,23 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9957
10276
  font-size: 0.88rem;
9958
10277
  line-height: 1.55;
9959
10278
  }
10279
+ .content .ox-api-entry__section--signature pre {
10280
+ margin: 0;
10281
+ border: 1px solid var(--color-code-frame-border);
10282
+ border-radius: 4px;
10283
+ }
10284
+ .content .ox-api-entry__example + .ox-api-entry__example {
10285
+ margin-top: 0.85rem;
10286
+ }
10287
+ .content .ox-api-entry__example-heading {
10288
+ margin-bottom: 0.45rem;
10289
+ font-family: var(--font-mono);
10290
+ font-size: 0.72rem;
10291
+ font-weight: 700;
10292
+ letter-spacing: 0.04em;
10293
+ text-transform: uppercase;
10294
+ color: var(--color-text-muted);
10295
+ }
9960
10296
  .content .ox-api-entry__section--examples pre {
9961
10297
  margin: 0;
9962
10298
  border: 1px solid var(--color-code-frame-border);
@@ -10471,7 +10807,8 @@ function resolveSsgOptions(ssg) {
10471
10807
  extension: ".html",
10472
10808
  clean: false,
10473
10809
  bare: false,
10474
- generateOgImage: false
10810
+ generateOgImage: false,
10811
+ lastUpdated: false
10475
10812
  };
10476
10813
  if (ssg === true || ssg === void 0) return {
10477
10814
  enabled: true,
@@ -10479,6 +10816,7 @@ function resolveSsgOptions(ssg) {
10479
10816
  clean: false,
10480
10817
  bare: false,
10481
10818
  generateOgImage: false,
10819
+ lastUpdated: false,
10482
10820
  theme: resolveTheme(void 0)
10483
10821
  };
10484
10822
  return {
@@ -10489,6 +10827,7 @@ function resolveSsgOptions(ssg) {
10489
10827
  siteName: ssg.siteName,
10490
10828
  ogImage: ssg.ogImage,
10491
10829
  generateOgImage: ssg.generateOgImage ?? false,
10830
+ lastUpdated: ssg.lastUpdated ?? false,
10492
10831
  siteUrl: ssg.siteUrl,
10493
10832
  theme: resolveTheme(ssg.theme)
10494
10833
  };
@@ -10542,13 +10881,17 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
10542
10881
  text: entry.text,
10543
10882
  slug: entry.slug
10544
10883
  }));
10884
+ const toRustNavItem = (item) => ({
10885
+ title: item.title,
10886
+ path: item.path,
10887
+ href: item.href,
10888
+ children: item.children?.map(toRustNavItem),
10889
+ collapsed: item.collapsed
10890
+ });
10545
10891
  const navGroupsForRust = navGroups.map((group) => ({
10546
10892
  title: group.title,
10547
- items: group.items.map((item) => ({
10548
- title: item.title,
10549
- path: item.path,
10550
- href: item.href
10551
- }))
10893
+ collapsed: group.collapsed,
10894
+ items: group.items.map(toRustNavItem)
10552
10895
  }));
10553
10896
  const themeForRust = theme ? themeToNapi(theme) : void 0;
10554
10897
  const entryPageForRust = pageData.entryPage ? {
@@ -10587,6 +10930,7 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
10587
10930
  description: pageData.description,
10588
10931
  content: pageData.content,
10589
10932
  toc: tocForRust,
10933
+ lastUpdated: pageData.lastUpdated,
10590
10934
  path: pageData.path,
10591
10935
  entryPage: entryPageForRust
10592
10936
  }, navGroupsForRust, {
@@ -10874,6 +11218,63 @@ function buildNavItems(markdownFiles, srcDir, base, extension) {
10874
11218
  });
10875
11219
  return result;
10876
11220
  }
11221
+ function isSafeSidebarLink(link) {
11222
+ const trimmed = link.trim();
11223
+ if (trimmed.startsWith("//")) return false;
11224
+ return !/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || /^(https?:|mailto:)/i.test(trimmed);
11225
+ }
11226
+ function sidebarPath(link) {
11227
+ if (!link || !isSafeSidebarLink(link)) return "";
11228
+ if (/^(https?:|mailto:|#)/i.test(link.trim())) return "";
11229
+ const bare = link.trim().split("#", 1)[0].split("?", 1)[0].replace(/^\/+/, "").replace(/\/$/, "").replace(/\.(md|markdown)$/i, "");
11230
+ if (!bare || bare === "index") return "/";
11231
+ return bare.replace(/\/index$/, "");
11232
+ }
11233
+ function sidebarHref(link, base, extension) {
11234
+ if (!link) return "#";
11235
+ const trimmed = link.trim();
11236
+ if (!isSafeSidebarLink(trimmed)) return "#";
11237
+ if (/^(https?:|mailto:|#)/i.test(trimmed)) return trimmed;
11238
+ const hash = trimmed.includes("#") ? `#${trimmed.split("#").slice(1).join("#")}` : "";
11239
+ const withoutExt = trimmed.split("#", 1)[0].replace(/^\/+/, "").replace(/\/$/, "").replace(/\.(md|markdown)$/i, "");
11240
+ return `${base}${!withoutExt || withoutExt === "index" ? "index" : `${withoutExt.replace(/\/index$/, "")}/index`}${extension}${hash}`;
11241
+ }
11242
+ /**
11243
+ * Builds navigation items from an explicit theme sidebar tree.
11244
+ */
11245
+ function buildThemeNavItems(sidebar, base, extension) {
11246
+ const toNavItem = (item) => {
11247
+ const navItem = {
11248
+ title: item.text ?? item.link ?? "Untitled",
11249
+ path: sidebarPath(item.link),
11250
+ href: sidebarHref(item.link, base, extension)
11251
+ };
11252
+ if (item.items?.length) navItem.children = item.items.map(toNavItem);
11253
+ if (item.collapsed !== void 0) navItem.collapsed = item.collapsed;
11254
+ return navItem;
11255
+ };
11256
+ const groups = [];
11257
+ let looseItems = [];
11258
+ const flushLooseItems = () => {
11259
+ if (looseItems.length > 0) {
11260
+ groups.push({
11261
+ title: "Guide",
11262
+ items: looseItems
11263
+ });
11264
+ looseItems = [];
11265
+ }
11266
+ };
11267
+ for (const item of sidebar) if (item.items?.length && !item.link) {
11268
+ flushLooseItems();
11269
+ groups.push({
11270
+ title: item.text ?? "Guide",
11271
+ items: item.items.map(toNavItem),
11272
+ collapsed: item.collapsed
11273
+ });
11274
+ } else looseItems.push(toNavItem(item));
11275
+ flushLooseItems();
11276
+ return groups;
11277
+ }
10877
11278
  /**
10878
11279
  * Builds all markdown files to static HTML.
10879
11280
  */
@@ -10896,7 +11297,7 @@ async function buildSsg(options, root) {
10896
11297
  });
10897
11298
  } catch {}
10898
11299
  const markdownFiles = await collectMarkdownFiles$1(srcDir);
10899
- const navItems = buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension);
11300
+ const navItems = ssgOptions.theme?.sidebar.length ? buildThemeNavItems(ssgOptions.theme.sidebar, base, ssgOptions.extension) : buildNavItems(markdownFiles, srcDir, base, ssgOptions.extension);
10900
11301
  let siteName = ssgOptions.siteName ?? "Documentation";
10901
11302
  if (!ssgOptions.siteName) try {
10902
11303
  const pkgPath = path.join(root, "package.json");
@@ -10908,6 +11309,7 @@ async function buildSsg(options, root) {
10908
11309
  const ogImageUrlMap = /* @__PURE__ */ new Map();
10909
11310
  const shouldGenerateOgImages = (options.ogImage || ssgOptions.generateOgImage) && !ssgOptions.bare;
10910
11311
  const pageResults = [];
11312
+ const napi = ssgOptions.lastUpdated ? await require_mermaid.importNapiModule() : void 0;
10911
11313
  for (const inputPath of markdownFiles) try {
10912
11314
  const result = await transformMarkdown(await fs_promises.readFile(inputPath, "utf-8"), inputPath, options, {
10913
11315
  convertMdLinks: true,
@@ -10935,6 +11337,7 @@ async function buildSsg(options, root) {
10935
11337
  transformedHtml,
10936
11338
  title,
10937
11339
  description,
11340
+ lastUpdated: napi?.getGitLastUpdated(inputPath, root) ?? void 0,
10938
11341
  frontmatter: result.frontmatter,
10939
11342
  toc: result.toc
10940
11343
  });
@@ -10980,7 +11383,7 @@ async function buildSsg(options, root) {
10980
11383
  ogImageUrlMap.clear();
10981
11384
  }
10982
11385
  for (const pageResult of pageResults) try {
10983
- const { inputPath, transformedHtml, title, description, frontmatter, toc } = pageResult;
11386
+ const { inputPath, transformedHtml, title, description, lastUpdated, frontmatter, toc } = pageResult;
10984
11387
  let pageOgImage = ssgOptions.ogImage;
10985
11388
  if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
10986
11389
  let entryPage;
@@ -10995,6 +11398,7 @@ async function buildSsg(options, root) {
10995
11398
  description,
10996
11399
  content: transformedHtml,
10997
11400
  toc,
11401
+ lastUpdated,
10998
11402
  frontmatter,
10999
11403
  path: getUrlPath$1(inputPath, srcDir),
11000
11404
  href: getHref(inputPath, srcDir, base, ssgOptions.extension),
@@ -12903,6 +13307,7 @@ function renderPage(page, options) {
12903
13307
  description: page.description,
12904
13308
  html: page.html,
12905
13309
  toc: page.toc,
13310
+ lastUpdated: page.lastUpdated,
12906
13311
  path: page.path,
12907
13312
  url: page.url,
12908
13313
  frontmatter: page.frontmatter,
@@ -12917,6 +13322,7 @@ function renderPage(page, options) {
12917
13322
  description: p.description,
12918
13323
  html: p.html,
12919
13324
  toc: p.toc,
13325
+ lastUpdated: p.lastUpdated,
12920
13326
  path: p.path,
12921
13327
  url: p.url,
12922
13328
  frontmatter: p.frontmatter,