@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.mjs CHANGED
@@ -7402,6 +7402,32 @@ export const ${exportName}: NavItem[] = ${JSON.stringify(navItems, null, 2)} as
7402
7402
  */
7403
7403
  const DOCS_MANIFEST_FILE = ".ox-content-docs-manifest.json";
7404
7404
  const DOCS_DATA_FILE = "docs.json";
7405
+ const DOC_KIND_ORDER = [
7406
+ "function",
7407
+ "class",
7408
+ "interface",
7409
+ "type",
7410
+ "variable",
7411
+ "module"
7412
+ ];
7413
+ const DOC_KIND_PLURAL = {
7414
+ function: "functions",
7415
+ class: "classes",
7416
+ interface: "interfaces",
7417
+ type: "types",
7418
+ variable: "variables",
7419
+ module: "modules"
7420
+ };
7421
+ const DEFAULT_DOCS_INCLUDE = [
7422
+ "**/*.ts",
7423
+ "**/*.tsx",
7424
+ "**/*.js",
7425
+ "**/*.jsx",
7426
+ "**/*.mts",
7427
+ "**/*.mjs",
7428
+ "**/*.cts",
7429
+ "**/*.cjs"
7430
+ ];
7405
7431
  function escapeHtml$3(str) {
7406
7432
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
7407
7433
  }
@@ -7410,7 +7436,7 @@ function entryAnchor(name) {
7410
7436
  }
7411
7437
  function cleanSummaryText(text, maxLength = 120) {
7412
7438
  if (!text) return "";
7413
- const collapsed = text.replace(/\s+/g, " ").trim();
7439
+ const collapsed = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\s+/g, " ").trim();
7414
7440
  if (collapsed.length <= maxLength) return collapsed;
7415
7441
  return `${collapsed.slice(0, maxLength - 1).trimEnd()}…`;
7416
7442
  }
@@ -7548,6 +7574,86 @@ function renderDetailsControlsHtml(targetSelector) {
7548
7574
  <button type="button" class="ox-api-controls__button" data-ox-api-toggle="collapse">Close all</button>
7549
7575
  </div>`;
7550
7576
  }
7577
+ function createEmptyEntryStats() {
7578
+ return {
7579
+ entries: 0,
7580
+ byKind: {},
7581
+ params: 0,
7582
+ returns: 0,
7583
+ examples: 0,
7584
+ deprecated: 0
7585
+ };
7586
+ }
7587
+ function summarizeEntries(entries) {
7588
+ const stats = createEmptyEntryStats();
7589
+ for (const entry of entries) {
7590
+ stats.entries++;
7591
+ stats.byKind[entry.kind] = (stats.byKind[entry.kind] ?? 0) + 1;
7592
+ stats.params += entry.params?.length ?? 0;
7593
+ stats.returns += entry.returns ? 1 : 0;
7594
+ stats.examples += entry.examples?.length ?? 0;
7595
+ stats.deprecated += entry.tags?.deprecated !== void 0 ? 1 : 0;
7596
+ }
7597
+ return stats;
7598
+ }
7599
+ function buildDocsSummary(docs) {
7600
+ const stats = summarizeEntries(docs.flatMap((doc) => doc.entries));
7601
+ const byKind = {};
7602
+ for (const kind of DOC_KIND_ORDER) {
7603
+ const count = stats.byKind[kind];
7604
+ if (count) byKind[kind] = count;
7605
+ }
7606
+ return {
7607
+ modules: docs.length,
7608
+ entries: stats.entries,
7609
+ byKind,
7610
+ params: stats.params,
7611
+ returns: stats.returns,
7612
+ examples: stats.examples,
7613
+ deprecated: stats.deprecated
7614
+ };
7615
+ }
7616
+ function renderStatsHtml(stats, moduleCount) {
7617
+ const items = [];
7618
+ if (moduleCount !== void 0) items.push({
7619
+ label: "modules",
7620
+ value: moduleCount
7621
+ });
7622
+ items.push({
7623
+ label: "symbols",
7624
+ value: stats.entries
7625
+ });
7626
+ for (const kind of DOC_KIND_ORDER) {
7627
+ const count = stats.byKind[kind];
7628
+ if (count) items.push({
7629
+ label: DOC_KIND_PLURAL[kind],
7630
+ value: count
7631
+ });
7632
+ }
7633
+ if (stats.params) items.push({
7634
+ label: "parameters",
7635
+ value: stats.params
7636
+ });
7637
+ if (stats.returns) items.push({
7638
+ label: "returns",
7639
+ value: stats.returns
7640
+ });
7641
+ if (stats.examples) items.push({
7642
+ label: "examples",
7643
+ value: stats.examples
7644
+ });
7645
+ if (stats.deprecated) items.push({
7646
+ label: "deprecated",
7647
+ value: stats.deprecated,
7648
+ tone: "warning"
7649
+ });
7650
+ return `<div class="ox-api-stats" aria-label="API reference summary">
7651
+ ${items.map((item) => `<span class="ox-api-stat${item.tone ? ` ox-api-stat--${item.tone}` : ""}">
7652
+ <strong>${item.value}</strong>
7653
+ <span>${escapeHtml$3(item.label)}</span>
7654
+ </span>`).join("\n")}
7655
+ </div>`;
7656
+ }
7551
7657
  function normalizeDocFilePath(filePath) {
7552
7658
  const normalized = filePath.replace(/\\/g, "/");
7553
7659
  return normalized.match(/(?:^|\/)((?:npm|packages|crates|src)\/.+)$/)?.[1] ?? normalized.replace(/^\/+/, "");
@@ -7556,6 +7662,7 @@ function buildDocsData(docs) {
7556
7662
  return {
7557
7663
  version: 1,
7558
7664
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7665
+ summary: buildDocsSummary(docs),
7559
7666
  modules: docs.map((doc) => ({
7560
7667
  ...doc,
7561
7668
  file: normalizeDocFilePath(doc.file),
@@ -7566,6 +7673,86 @@ function buildDocsData(docs) {
7566
7673
  }))
7567
7674
  };
7568
7675
  }
7676
+ function consumeJSDocType(value) {
7677
+ const trimmed = value.trimStart();
7678
+ if (!trimmed.startsWith("{")) return { rest: trimmed };
7679
+ let depth = 0;
7680
+ for (let index = 0; index < trimmed.length; index++) {
7681
+ const char = trimmed[index];
7682
+ if (char === "{") depth++;
7683
+ else if (char === "}") {
7684
+ depth--;
7685
+ if (depth === 0) return {
7686
+ type: trimmed.slice(1, index).trim() || void 0,
7687
+ rest: trimmed.slice(index + 1).trimStart()
7688
+ };
7689
+ }
7690
+ }
7691
+ return { rest: trimmed };
7692
+ }
7693
+ function cleanTagDescription(value) {
7694
+ return value.trim().replace(/^-\s*/, "").trim();
7695
+ }
7696
+ function splitTagNameAndDescription(value) {
7697
+ const trimmed = value.trimStart();
7698
+ if (trimmed.startsWith("[")) {
7699
+ const closeIndex = trimmed.indexOf("]");
7700
+ if (closeIndex >= 0) return {
7701
+ name: trimmed.slice(0, closeIndex + 1),
7702
+ description: trimmed.slice(closeIndex + 1).trimStart()
7703
+ };
7704
+ }
7705
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/u.exec(trimmed);
7706
+ return {
7707
+ name: match?.[1] ?? "",
7708
+ description: match?.[2] ?? ""
7709
+ };
7710
+ }
7711
+ function parseParamTagValue(value) {
7712
+ const { type, rest } = consumeJSDocType(value);
7713
+ const { name: rawName, description } = splitTagNameAndDescription(rest);
7714
+ let name = rawName.trim();
7715
+ if (!name) return null;
7716
+ let optional = false;
7717
+ let defaultValue;
7718
+ const optionalMatch = /^\[(.*)\]$/u.exec(name);
7719
+ if (optionalMatch) {
7720
+ optional = true;
7721
+ const [innerName, innerDefault] = optionalMatch[1].split(/=(.*)/su);
7722
+ name = innerName.trim();
7723
+ defaultValue = innerDefault?.trim() || void 0;
7724
+ }
7725
+ if (!name) return null;
7726
+ return {
7727
+ name,
7728
+ type: type || "unknown",
7729
+ description: cleanTagDescription(description),
7730
+ optional: optional || void 0,
7731
+ default: defaultValue
7732
+ };
7733
+ }
7734
+ function parseReturnsTagValue(value) {
7735
+ const { type, rest } = consumeJSDocType(value);
7736
+ return {
7737
+ type: type || "unknown",
7738
+ description: cleanTagDescription(rest)
7739
+ };
7740
+ }
7741
+ function normalizeReturnType(value) {
7742
+ const parsed = parseReturnsTagValue(value);
7743
+ return parsed.type === "unknown" ? value : parsed.type;
7744
+ }
7745
+ function mergeParam(params, next) {
7746
+ const existing = params.find((param) => param.name === next.name);
7747
+ if (!existing) {
7748
+ params.push(next);
7749
+ return;
7750
+ }
7751
+ if (next.type && (existing.type === "unknown" || next.type !== "unknown")) existing.type = next.type;
7752
+ if (next.description) existing.description = next.description;
7753
+ if (next.optional) existing.optional = true;
7754
+ if (next.default) existing.default = next.default;
7755
+ }
7569
7756
  /**
7570
7757
  * Extracts JSDoc documentation from source files in specified directories.
7571
7758
  *
@@ -7577,7 +7764,7 @@ function buildDocsData(docs) {
7577
7764
  *
7578
7765
  * 1. **File Discovery**: Recursively walks directories, applying filters
7579
7766
  * 2. **File Reading**: Loads each matching file's content
7580
- * 3. **JSDoc Extraction**: Parses JSDoc comments using regex patterns
7767
+ * 3. **JSDoc Extraction**: Parses JSDoc comments using the native parser
7581
7768
  * 4. **Declaration Matching**: Pairs JSDoc comments with source declarations
7582
7769
  * 5. **Result Collection**: Aggregates extracted documentation by file
7583
7770
  *
@@ -7717,32 +7904,29 @@ function parseNapiDocItem(item) {
7717
7904
  currentExample = "";
7718
7905
  inExample = false;
7719
7906
  }
7720
- const tagMatch = /@(\w+)\s*(?:\{([^}]*)\})?(.*)/.exec(lineText);
7907
+ const tagMatch = /^@(\S+)\s*([\s\S]*)$/u.exec(lineText);
7721
7908
  if (tagMatch) {
7722
- const [, tagName, tagType, tagRest] = tagMatch;
7909
+ const [, tagName, tagValue = ""] = tagMatch;
7723
7910
  switch (tagName) {
7724
7911
  case "param":
7725
- const paramMatch = /(\w+)\s*-?\s*(.*)/.exec(tagRest.trim());
7726
- if (paramMatch) params.push({
7727
- name: paramMatch[1],
7728
- type: tagType || "unknown",
7729
- description: paramMatch[2]
7730
- });
7912
+ case "arg":
7913
+ case "argument": {
7914
+ const param = parseParamTagValue(tagValue);
7915
+ if (param) mergeParam(params, param);
7731
7916
  break;
7917
+ }
7732
7918
  case "returns":
7733
7919
  case "return":
7734
- returns = {
7735
- type: tagType || "unknown",
7736
- description: tagRest.trim()
7737
- };
7920
+ returns = parseReturnsTagValue(tagValue);
7738
7921
  break;
7739
7922
  case "example":
7740
7923
  inExample = true;
7924
+ currentExample = tagValue.trim() ? `${tagValue.trim()}\n` : "";
7741
7925
  break;
7742
7926
  case "private":
7743
7927
  isPrivate = true;
7744
7928
  break;
7745
- default: tags[tagName] = tagRest.trim();
7929
+ default: tags[tagName] = tagValue.trim();
7746
7930
  }
7747
7931
  }
7748
7932
  } else if (inExample) currentExample += rawLine + "\n";
@@ -7750,32 +7934,41 @@ function parseNapiDocItem(item) {
7750
7934
  else description += "\n" + lineText;
7751
7935
  }
7752
7936
  if (inExample && currentExample) examples.push(currentExample.trim());
7753
- if (params.length === 0 && item.params.length > 0) params.push(...item.params.map((param) => ({
7754
- name: param.name,
7755
- type: param.typeAnnotation ?? "unknown",
7756
- description: param.description ?? "",
7757
- optional: param.optional || void 0,
7758
- default: param.defaultValue
7759
- })));
7760
- else if (item.params.length > 0) {
7761
- const paramMap = new Map(item.params.map((param) => [param.name, param]));
7762
- for (const param of params) {
7763
- const rustParam = paramMap.get(param.name);
7764
- if (!rustParam) continue;
7765
- if (param.type === "unknown" && rustParam.typeAnnotation) param.type = rustParam.typeAnnotation;
7766
- if (!param.description && rustParam.description) param.description = rustParam.description;
7767
- if (param.optional === void 0 && rustParam.optional) param.optional = true;
7768
- if (!param.default && rustParam.defaultValue) param.default = rustParam.defaultValue;
7769
- }
7937
+ for (const param of item.params) {
7938
+ if (params.length > 0 && param.name === "param" && !param.typeAnnotation && !param.description && !param.defaultValue) continue;
7939
+ mergeParam(params, {
7940
+ name: param.name,
7941
+ type: param.typeAnnotation ?? "unknown",
7942
+ description: param.description ?? "",
7943
+ optional: param.optional || void 0,
7944
+ default: param.defaultValue
7945
+ });
7770
7946
  }
7771
7947
  if (!returns && item.returnType) returns = {
7772
- type: item.returnType,
7948
+ type: normalizeReturnType(item.returnType),
7773
7949
  description: ""
7774
7950
  };
7775
- else if (returns && returns.type === "unknown" && item.returnType) returns.type = item.returnType;
7951
+ else if (returns && item.returnType) returns.type = normalizeReturnType(item.returnType);
7776
7952
  if (!description) description = item.doc ?? "";
7777
7953
  for (const tag of item.tags) {
7778
- if (tag.tag === "param" || tag.tag === "returns" || tag.tag === "return" || tag.tag === "example") continue;
7954
+ if (tag.tag === "param" || tag.tag === "arg" || tag.tag === "argument" || tag.tag === "returns" || tag.tag === "return") {
7955
+ if (tag.tag === "param" || tag.tag === "arg" || tag.tag === "argument") {
7956
+ const param = parseParamTagValue(tag.value);
7957
+ if (param) mergeParam(params, param);
7958
+ } else {
7959
+ const parsedReturns = parseReturnsTagValue(tag.value);
7960
+ if (!returns) returns = parsedReturns;
7961
+ else {
7962
+ returns.type = returns.type === "unknown" ? parsedReturns.type : returns.type;
7963
+ returns.description ||= parsedReturns.description;
7964
+ }
7965
+ }
7966
+ continue;
7967
+ }
7968
+ if (tag.tag === "example") {
7969
+ if (tag.value && !examples.includes(tag.value)) examples.push(tag.value);
7970
+ continue;
7971
+ }
7779
7972
  if (tag.tag === "private") {
7780
7973
  isPrivate = true;
7781
7974
  continue;
@@ -7862,6 +8055,7 @@ function generateFileMarkdown(doc, options, currentFileName, symbolMap) {
7862
8055
  }
7863
8056
  md += `> ${doc.entries.length} documented symbol${doc.entries.length === 1 ? "" : "s"}. `;
7864
8057
  md += "Read the signatures first, then expand each item for parameters, return types, and examples.\n\n";
8058
+ md += renderStatsHtml(summarizeEntries(doc.entries)) + "\n\n";
7865
8059
  md += "## Reference\n\n";
7866
8060
  if (doc.entries.length > 1) md += renderDetailsControlsHtml(".ox-api-entry") + "\n\n";
7867
8061
  for (const entry of doc.entries) md += generateEntryMarkdown(entry, options, currentFileName, symbolMap);
@@ -7881,6 +8075,42 @@ function formatKindLabel(kind) {
7881
8075
  default: return kind;
7882
8076
  }
7883
8077
  }
8078
+ function formatCountLabel(count, singular, plural = `${singular}s`) {
8079
+ return `${count} ${count === 1 ? singular : plural}`;
8080
+ }
8081
+ function getEntryBadges(entry) {
8082
+ const badges = [];
8083
+ if (entry.tags?.deprecated !== void 0) badges.push({
8084
+ label: "deprecated",
8085
+ tone: "warning"
8086
+ });
8087
+ if (entry.params?.length) badges.push({ label: formatCountLabel(entry.params.length, "param") });
8088
+ if (entry.returns) badges.push({ label: `returns ${entry.returns.type}` });
8089
+ if (entry.examples?.length) badges.push({ label: formatCountLabel(entry.examples.length, "example") });
8090
+ if (entry.tags?.since) badges.push({ label: `since ${entry.tags.since}` });
8091
+ if (entry.private) badges.push({
8092
+ label: "private",
8093
+ tone: "warning"
8094
+ });
8095
+ return badges;
8096
+ }
8097
+ function renderEntryBadgesHtml(entry, className) {
8098
+ const badges = getEntryBadges(entry);
8099
+ if (badges.length === 0) return "";
8100
+ 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>`;
8101
+ }
8102
+ function parseExampleBlock(example) {
8103
+ const trimmed = example.trim();
8104
+ const fenceMatch = /^```([\w-]+)?[^\n]*\n([\s\S]*?)\n?```$/u.exec(trimmed);
8105
+ if (!fenceMatch) return {
8106
+ code: trimmed,
8107
+ language: "ts"
8108
+ };
8109
+ return {
8110
+ code: fenceMatch[2],
8111
+ language: fenceMatch[1] || "ts"
8112
+ };
8113
+ }
7884
8114
  function renderOverviewLine(entry, href) {
7885
8115
  const signature = normalizeSignature(entry.signature);
7886
8116
  const summary = cleanSummaryText(entry.description, 88);
@@ -7892,8 +8122,9 @@ function renderOverviewLine(entry, href) {
7892
8122
  function renderOverviewHtmlItem(entry, href) {
7893
8123
  const signature = normalizeSignature(entry.signature);
7894
8124
  const summary = cleanSummaryText(entry.description, 88);
8125
+ const meta = renderEntryBadgesHtml(entry, "ox-api-module__meta");
7895
8126
  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>`;
7896
- 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>`;
8127
+ 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>`;
7897
8128
  }
7898
8129
  function renderParamsListHtml(params) {
7899
8130
  return `<div class="ox-api-entry__section ox-api-entry__section--params">
@@ -7925,6 +8156,10 @@ function generateEntryMarkdown(entry, options, currentFileName, symbolMap) {
7925
8156
  const sourceHref = options?.githubUrl ? generateSourceHref(entry.file, options.githubUrl, entry.line, entry.endLine) : void 0;
7926
8157
  let body = "";
7927
8158
  if (processedDescription) body += renderMarkdownBlocksHtml(processedDescription) + "\n";
8159
+ if (entry.signature) body += `<div class="ox-api-entry__section ox-api-entry__section--signature">
8160
+ <h4>Signature</h4>
8161
+ ${renderCodeBlockHtml(entry.signature, "typescript")}
8162
+ </div>\n`;
7928
8163
  if (sourceHref) body += `<p class="ox-api-entry__source"><a href="${escapeHtml$3(sourceHref)}">View source</a></p>\n`;
7929
8164
  if (entry.params && entry.params.length > 0) body += renderParamsListHtml(entry.params) + "\n";
7930
8165
  if (entry.returns) body += `<div class="ox-api-entry__section ox-api-entry__section--returns">
@@ -7935,13 +8170,19 @@ function generateEntryMarkdown(entry, options, currentFileName, symbolMap) {
7935
8170
  </div>
7936
8171
  </div>\n`;
7937
8172
  if (entry.examples && entry.examples.length > 0) {
7938
- const examplesHtml = entry.examples.map((example) => example.replace(/^```\w*\n?/, "").replace(/\n?```$/, "")).map((example) => renderCodeBlockHtml(example, "ts")).join("\n");
8173
+ const examplesHtml = entry.examples.map((example, index) => {
8174
+ const parsed = parseExampleBlock(example);
8175
+ return `<div class="ox-api-entry__example">
8176
+ <div class="ox-api-entry__example-heading">Example ${index + 1}</div>
8177
+ ${renderCodeBlockHtml(parsed.code, parsed.language)}
8178
+ </div>`;
8179
+ }).join("\n");
7939
8180
  body += `<div class="ox-api-entry__section ox-api-entry__section--examples">\n<h4>Examples</h4>\n${examplesHtml}\n</div>\n`;
7940
8181
  }
7941
8182
  if (entry.tags && Object.keys(entry.tags).length > 0) body += renderTagListHtml(entry.tags) + "\n";
7942
8183
  const summaryDescription = cleanSummaryText(processedDescription, summarySignature ? 80 : 120);
7943
8184
  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>`;
7944
- 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>`];
8185
+ 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>`];
7945
8186
  return `<details id="${entryAnchor(entry.name)}" class="ox-api-entry">
7946
8187
  <summary>${summaryParts.join("")}</summary>
7947
8188
  <div class="ox-api-entry__body">
@@ -7955,6 +8196,7 @@ function generateIndex(docs, docToFile) {
7955
8196
  let md = "# API Documentation\n\n";
7956
8197
  md += "Generated by [Ox Content](https://github.com/ubugeeei/ox-content)\n\n";
7957
8198
  md += "> Use search scopes like `@api transform` to limit results to the generated API reference.\n\n";
8199
+ md += renderStatsHtml(summarizeEntries(docs.flatMap((doc) => doc.entries)), docs.length) + "\n\n";
7958
8200
  md += "## Modules\n\n";
7959
8201
  if (docs.length > 1) md += renderDetailsControlsHtml(".ox-api-module") + "\n\n";
7960
8202
  for (const doc of docs) {
@@ -7984,6 +8226,7 @@ function generateCategoryMarkdown(kind, entries, options, symbolMap) {
7984
8226
  const categoryFileName = `${kind}s`;
7985
8227
  let md = `# ${kind.charAt(0).toUpperCase() + kind.slice(1)}s\n\n`;
7986
8228
  md += `> ${entries.length} documented ${kind}${entries.length === 1 ? "" : "s"} collected across modules.\n\n`;
8229
+ md += renderStatsHtml(summarizeEntries(entries)) + "\n\n";
7987
8230
  md += "## Overview\n\n";
7988
8231
  for (const entry of entries) md += renderOverviewLine(entry, `#${entryAnchor(entry.name)}`);
7989
8232
  md += "\n## Reference\n\n";
@@ -7994,6 +8237,7 @@ function generateCategoryMarkdown(kind, entries, options, symbolMap) {
7994
8237
  function generateCategoryIndex(byKind) {
7995
8238
  let md = "# API Documentation\n\n";
7996
8239
  md += "Generated by [Ox Content](https://github.com/ubugeeei/ox-content)\n\n";
8240
+ md += renderStatsHtml(summarizeEntries([...byKind.values()].flatMap((entries) => entries))) + "\n\n";
7997
8241
  for (const [kind, entries] of [...byKind.entries()].sort(([a], [b]) => compareStrings(a, b))) {
7998
8242
  const kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1) + "s";
7999
8243
  md += `## [${kindTitle}](./${kind}s.md)\n\n`;
@@ -8105,7 +8349,7 @@ function resolveDocsOptions(options) {
8105
8349
  enabled: opts.enabled ?? true,
8106
8350
  src: opts.src ?? ["./src"],
8107
8351
  out: opts.out ?? "docs/api",
8108
- include: opts.include ?? ["**/*.ts", "**/*.tsx"],
8352
+ include: opts.include ?? DEFAULT_DOCS_INCLUDE,
8109
8353
  exclude: opts.exclude ?? [
8110
8354
  "**/*.test.*",
8111
8355
  "**/*.spec.*",
@@ -9725,6 +9969,40 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9725
9969
  border-color: color-mix(in srgb, var(--color-primary) 38%, var(--color-border));
9726
9970
  background: color-mix(in srgb, var(--color-bg-alt) 68%, var(--color-primary) 6%);
9727
9971
  }
9972
+ .content .ox-api-stats {
9973
+ display: grid;
9974
+ grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
9975
+ gap: 0.55rem;
9976
+ margin: 1rem 0 1.35rem;
9977
+ }
9978
+ .content .ox-api-stat {
9979
+ min-width: 0;
9980
+ padding: 0.7rem 0.75rem;
9981
+ border: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
9982
+ border-radius: 4px;
9983
+ background: color-mix(in srgb, var(--color-bg-alt) 72%, transparent);
9984
+ }
9985
+ .content .ox-api-stat strong {
9986
+ display: block;
9987
+ font-family: var(--font-mono);
9988
+ font-size: 1rem;
9989
+ line-height: 1.2;
9990
+ color: var(--color-text);
9991
+ }
9992
+ .content .ox-api-stat span {
9993
+ display: block;
9994
+ margin-top: 0.25rem;
9995
+ font-family: var(--font-mono);
9996
+ font-size: 0.7rem;
9997
+ font-weight: 600;
9998
+ letter-spacing: 0.04em;
9999
+ text-transform: uppercase;
10000
+ color: var(--color-text-muted);
10001
+ }
10002
+ .content .ox-api-stat--warning {
10003
+ border-color: color-mix(in srgb, #f59e0b 42%, var(--color-border));
10004
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, #f59e0b 8%);
10005
+ }
9728
10006
  .content .ox-api-entry,
9729
10007
  .content .ox-api-module {
9730
10008
  margin: 0;
@@ -9834,6 +10112,31 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9834
10112
  flex-direction: column;
9835
10113
  gap: 0.55rem;
9836
10114
  }
10115
+ .content .ox-api-entry__meta,
10116
+ .content .ox-api-module__meta {
10117
+ display: flex;
10118
+ flex-wrap: wrap;
10119
+ gap: 0.35rem;
10120
+ }
10121
+ .content .ox-api-badge {
10122
+ display: inline-flex;
10123
+ align-items: center;
10124
+ max-width: 100%;
10125
+ padding: 0.18rem 0.42rem;
10126
+ border: 1px solid color-mix(in srgb, var(--color-border) 78%, transparent);
10127
+ border-radius: 4px;
10128
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, var(--color-primary) 6%);
10129
+ color: color-mix(in srgb, var(--color-text) 74%, var(--color-text-muted));
10130
+ font-family: var(--font-mono);
10131
+ font-size: 0.68rem;
10132
+ font-weight: 600;
10133
+ line-height: 1.35;
10134
+ }
10135
+ .content .ox-api-badge--warning {
10136
+ border-color: color-mix(in srgb, #f59e0b 46%, var(--color-border));
10137
+ background: color-mix(in srgb, var(--color-bg-alt) 78%, #f59e0b 9%);
10138
+ color: color-mix(in srgb, var(--color-text) 86%, #f59e0b);
10139
+ }
9837
10140
  .content .ox-api-entry__name,
9838
10141
  .content .ox-api-entry__signature,
9839
10142
  .content .ox-api-module__name,
@@ -9982,6 +10285,23 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
9982
10285
  font-size: 0.88rem;
9983
10286
  line-height: 1.55;
9984
10287
  }
10288
+ .content .ox-api-entry__section--signature pre {
10289
+ margin: 0;
10290
+ border: 1px solid var(--color-code-frame-border);
10291
+ border-radius: 4px;
10292
+ }
10293
+ .content .ox-api-entry__example + .ox-api-entry__example {
10294
+ margin-top: 0.85rem;
10295
+ }
10296
+ .content .ox-api-entry__example-heading {
10297
+ margin-bottom: 0.45rem;
10298
+ font-family: var(--font-mono);
10299
+ font-size: 0.72rem;
10300
+ font-weight: 700;
10301
+ letter-spacing: 0.04em;
10302
+ text-transform: uppercase;
10303
+ color: var(--color-text-muted);
10304
+ }
9985
10305
  .content .ox-api-entry__section--examples pre {
9986
10306
  margin: 0;
9987
10307
  border: 1px solid var(--color-code-frame-border);