@ox-content/vite-plugin 2.4.0 → 2.6.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.*",
@@ -9700,6 +9944,40 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9700
9944
  border-color: color-mix(in srgb, var(--color-primary) 38%, var(--color-border));
9701
9945
  background: color-mix(in srgb, var(--color-bg-alt) 68%, var(--color-primary) 6%);
9702
9946
  }
9947
+ .content .ox-api-stats {
9948
+ display: grid;
9949
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
9950
+ gap: 0.55rem;
9951
+ margin: 1rem 0 1.35rem;
9952
+ }
9953
+ .content .ox-api-stat {
9954
+ min-width: 0;
9955
+ padding: 0.7rem 0.75rem;
9956
+ border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
9957
+ border-radius: 4px;
9958
+ background: color-mix(in srgb, var(--color-bg-alt) 72%, transparent);
9959
+ }
9960
+ .content .ox-api-stat strong {
9961
+ display: block;
9962
+ font-family: var(--font-mono);
9963
+ font-size: 1rem;
9964
+ line-height: 1.2;
9965
+ color: var(--color-text);
9966
+ }
9967
+ .content .ox-api-stat span {
9968
+ display: block;
9969
+ margin-top: 0.25rem;
9970
+ font-family: var(--font-mono);
9971
+ font-size: 0.7rem;
9972
+ font-weight: 600;
9973
+ letter-spacing: 0.04em;
9974
+ text-transform: uppercase;
9975
+ color: var(--color-text-muted);
9976
+ }
9977
+ .content .ox-api-stat--warning {
9978
+ border-color: color-mix(in srgb, #f59e0b 42%, var(--color-border));
9979
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, #f59e0b 8%);
9980
+ }
9703
9981
  .content .ox-api-entry,
9704
9982
  .content .ox-api-module {
9705
9983
  margin: 0;
@@ -9809,6 +10087,31 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9809
10087
  flex-direction: column;
9810
10088
  gap: 0.55rem;
9811
10089
  }
10090
+ .content .ox-api-entry__meta,
10091
+ .content .ox-api-module__meta {
10092
+ display: flex;
10093
+ flex-wrap: wrap;
10094
+ gap: 0.35rem;
10095
+ }
10096
+ .content .ox-api-badge {
10097
+ display: inline-flex;
10098
+ align-items: center;
10099
+ max-width: 100%;
10100
+ padding: 0.18rem 0.42rem;
10101
+ border: 1px solid color-mix(in srgb, var(--color-border) 78%, transparent);
10102
+ border-radius: 4px;
10103
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, var(--color-primary) 6%);
10104
+ color: color-mix(in srgb, var(--color-text) 74%, var(--color-text-muted));
10105
+ font-family: var(--font-mono);
10106
+ font-size: 0.68rem;
10107
+ font-weight: 600;
10108
+ line-height: 1.35;
10109
+ }
10110
+ .content .ox-api-badge--warning {
10111
+ border-color: color-mix(in srgb, #f59e0b 46%, var(--color-border));
10112
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, #f59e0b 9%);
10113
+ color: color-mix(in srgb, var(--color-text) 86%, #f59e0b);
10114
+ }
9812
10115
  .content .ox-api-entry__name,
9813
10116
  .content .ox-api-entry__signature,
9814
10117
  .content .ox-api-module__name,
@@ -9957,6 +10260,23 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9957
10260
  font-size: 0.88rem;
9958
10261
  line-height: 1.55;
9959
10262
  }
10263
+ .content .ox-api-entry__section--signature pre {
10264
+ margin: 0;
10265
+ border: 1px solid var(--color-code-frame-border);
10266
+ border-radius: 4px;
10267
+ }
10268
+ .content .ox-api-entry__example + .ox-api-entry__example {
10269
+ margin-top: 0.85rem;
10270
+ }
10271
+ .content .ox-api-entry__example-heading {
10272
+ margin-bottom: 0.45rem;
10273
+ font-family: var(--font-mono);
10274
+ font-size: 0.72rem;
10275
+ font-weight: 700;
10276
+ letter-spacing: 0.04em;
10277
+ text-transform: uppercase;
10278
+ color: var(--color-text-muted);
10279
+ }
9960
10280
  .content .ox-api-entry__section--examples pre {
9961
10281
  margin: 0;
9962
10282
  border: 1px solid var(--color-code-frame-border);