sourcey 3.4.11 → 3.5.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.
Files changed (102) hide show
  1. package/README.md +13 -12
  2. package/dist/cli.js +46 -4
  3. package/dist/components/changelog/ChangelogPage.d.ts +5 -0
  4. package/dist/components/changelog/ChangelogPage.d.ts.map +1 -0
  5. package/dist/components/changelog/ChangelogPage.js +8 -0
  6. package/dist/components/changelog/EntryList.d.ts +5 -0
  7. package/dist/components/changelog/EntryList.d.ts.map +1 -0
  8. package/dist/components/changelog/EntryList.js +6 -0
  9. package/dist/components/changelog/TypeBadge.d.ts +6 -0
  10. package/dist/components/changelog/TypeBadge.d.ts.map +1 -0
  11. package/dist/components/changelog/TypeBadge.js +14 -0
  12. package/dist/components/changelog/VersionCard.d.ts +6 -0
  13. package/dist/components/changelog/VersionCard.d.ts.map +1 -0
  14. package/dist/components/changelog/VersionCard.js +25 -0
  15. package/dist/components/layout/Head.d.ts.map +1 -1
  16. package/dist/components/layout/Head.js +15 -3
  17. package/dist/components/layout/Page.d.ts.map +1 -1
  18. package/dist/components/layout/Page.js +12 -6
  19. package/dist/components/openapi/Introduction.d.ts.map +1 -1
  20. package/dist/components/openapi/Introduction.js +1 -1
  21. package/dist/components/openapi/MediaTypeDetails.d.ts +11 -0
  22. package/dist/components/openapi/MediaTypeDetails.d.ts.map +1 -0
  23. package/dist/components/openapi/MediaTypeDetails.js +54 -0
  24. package/dist/components/openapi/Parameters.d.ts.map +1 -1
  25. package/dist/components/openapi/Parameters.js +6 -1
  26. package/dist/components/openapi/RequestBody.d.ts.map +1 -1
  27. package/dist/components/openapi/RequestBody.js +5 -1
  28. package/dist/components/openapi/Responses.d.ts.map +1 -1
  29. package/dist/components/openapi/Responses.js +1 -1
  30. package/dist/components/openapi/Security.d.ts.map +1 -1
  31. package/dist/components/openapi/Security.js +3 -2
  32. package/dist/components/openapi/Tags.d.ts.map +1 -1
  33. package/dist/components/openapi/Tags.js +8 -1
  34. package/dist/config.d.ts +50 -1
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +50 -4
  37. package/dist/core/changelog-normalizer.d.ts +8 -0
  38. package/dist/core/changelog-normalizer.d.ts.map +1 -0
  39. package/dist/core/changelog-normalizer.js +531 -0
  40. package/dist/core/doxygen-loader.d.ts.map +1 -1
  41. package/dist/core/doxygen-loader.js +2 -0
  42. package/dist/core/loader.js +2 -0
  43. package/dist/core/markdown-loader.d.ts +21 -0
  44. package/dist/core/markdown-loader.d.ts.map +1 -1
  45. package/dist/core/markdown-loader.js +43 -25
  46. package/dist/core/mcp-normalizer.d.ts.map +1 -1
  47. package/dist/core/navigation.d.ts +2 -2
  48. package/dist/core/navigation.d.ts.map +1 -1
  49. package/dist/core/navigation.js +5 -1
  50. package/dist/core/normalizer.d.ts.map +1 -1
  51. package/dist/core/normalizer.js +102 -4
  52. package/dist/core/parser.d.ts +2 -2
  53. package/dist/core/parser.d.ts.map +1 -1
  54. package/dist/core/parser.js +23 -16
  55. package/dist/core/search-indexer.d.ts +3 -2
  56. package/dist/core/search-indexer.d.ts.map +1 -1
  57. package/dist/core/search-indexer.js +50 -17
  58. package/dist/core/tag-utils.d.ts +5 -0
  59. package/dist/core/tag-utils.d.ts.map +1 -0
  60. package/dist/core/tag-utils.js +22 -0
  61. package/dist/core/types.d.ts +82 -4
  62. package/dist/core/types.d.ts.map +1 -1
  63. package/dist/core/types.js +1 -1
  64. package/dist/dev-server.d.ts +2 -0
  65. package/dist/dev-server.d.ts.map +1 -1
  66. package/dist/dev-server.js +81 -210
  67. package/dist/index.d.ts +6 -10
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +115 -247
  70. package/dist/init.d.ts.map +1 -1
  71. package/dist/init.js +7 -3
  72. package/dist/og/fonts/Inter-Bold.ttf +0 -0
  73. package/dist/og/fonts/Inter-Regular.ttf +0 -0
  74. package/dist/og/generate-og-image.d.ts +10 -0
  75. package/dist/og/generate-og-image.d.ts.map +1 -0
  76. package/dist/og/generate-og-image.js +198 -0
  77. package/dist/renderer/changelog-feed.d.ts +18 -0
  78. package/dist/renderer/changelog-feed.d.ts.map +1 -0
  79. package/dist/renderer/changelog-feed.js +91 -0
  80. package/dist/renderer/context.d.ts +27 -7
  81. package/dist/renderer/context.d.ts.map +1 -1
  82. package/dist/renderer/html-builder.d.ts +10 -1
  83. package/dist/renderer/html-builder.d.ts.map +1 -1
  84. package/dist/renderer/html-builder.js +58 -5
  85. package/dist/renderer/llms.d.ts.map +1 -1
  86. package/dist/renderer/llms.js +133 -24
  87. package/dist/site-assembly.d.ts +25 -0
  88. package/dist/site-assembly.d.ts.map +1 -0
  89. package/dist/site-assembly.js +467 -0
  90. package/dist/site-url.d.ts +12 -0
  91. package/dist/site-url.d.ts.map +1 -0
  92. package/dist/site-url.js +67 -0
  93. package/dist/themes/default/sourcey.css +236 -0
  94. package/dist/utils/code-samples.d.ts.map +1 -1
  95. package/dist/utils/code-samples.js +53 -12
  96. package/dist/utils/markdown.d.ts +6 -0
  97. package/dist/utils/markdown.d.ts.map +1 -1
  98. package/dist/utils/markdown.js +11 -2
  99. package/dist/vite-plugin.d.ts +2 -0
  100. package/dist/vite-plugin.d.ts.map +1 -1
  101. package/dist/vite-plugin.js +6 -2
  102. package/package.json +6 -3
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename, extname, relative } from "node:path";
3
3
  import { load as parseYaml } from "js-yaml";
4
+ import { normalizeChangelog } from "./changelog-normalizer.js";
4
5
  import { htmlId } from "../utils/html-id.js";
5
6
  import { renderIcon } from "../utils/icons.js";
6
7
  import { renderCodeBlock, renderMarkdown, renderMarkdownInline, extractHeadings, } from "../utils/markdown.js";
@@ -215,31 +216,6 @@ function buildDirectiveAttrList(entries) {
215
216
  .map(([key, value]) => `${key}="${escapeDirectiveAttr(value)}"`);
216
217
  return attrs.length > 0 ? `{${attrs.join(" ")}}` : "";
217
218
  }
218
- function indentBlock(text, prefix) {
219
- const lines = text.split("\n");
220
- const result = [];
221
- let fence = null;
222
- for (const line of lines) {
223
- if (fence) {
224
- // Inside a fenced code block — don't indent
225
- result.push(line);
226
- if (closesFence(line, fence))
227
- fence = null;
228
- }
229
- else {
230
- const nextFence = isFenceStart(line);
231
- if (nextFence) {
232
- // Fence opener — don't indent so markdown recognises it
233
- fence = nextFence;
234
- result.push(line);
235
- }
236
- else {
237
- result.push(`${prefix}${line}`);
238
- }
239
- }
240
- }
241
- return result.join("\n");
242
- }
243
219
  function isWhitespaceOnlyText(node) {
244
220
  return node.kind === "text" && node.value.trim() === "";
245
221
  }
@@ -953,6 +929,47 @@ function preprocessDirectives(body) {
953
929
  export async function loadMarkdownPage(filePath, slug) {
954
930
  const raw = await readFile(filePath, "utf-8");
955
931
  const { meta, body } = parseFrontmatter(raw);
932
+ return loadMarkdownPageFromBody(filePath, slug, body, meta);
933
+ }
934
+ export async function loadDocsPage(filePath, slug, options = {}) {
935
+ const raw = await readFile(filePath, "utf-8");
936
+ const { meta, body } = parseFrontmatter(raw);
937
+ if (shouldTreatAsChangelog(filePath, meta, options)) {
938
+ const sourcePath = relative(process.cwd(), filePath);
939
+ const changelog = normalizeChangelog(body, {
940
+ title: typeof meta.title === "string" ? meta.title : undefined,
941
+ description: typeof meta.description === "string" ? meta.description : undefined,
942
+ repoUrl: options.repoUrl,
943
+ });
944
+ const description = typeof meta.description === "string"
945
+ ? meta.description
946
+ : changelog.description ?? "";
947
+ return {
948
+ kind: "changelog",
949
+ title: changelog.title,
950
+ description,
951
+ slug,
952
+ headings: changelog.versions.map((version) => ({
953
+ level: 2,
954
+ text: version.version ?? "Unreleased",
955
+ id: version.id,
956
+ })),
957
+ sourcePath,
958
+ editPath: sourcePath,
959
+ changelog,
960
+ rawBody: body,
961
+ };
962
+ }
963
+ return loadMarkdownPageFromBody(filePath, slug, body, meta);
964
+ }
965
+ function shouldTreatAsChangelog(filePath, meta, options) {
966
+ if (options.changelog === false)
967
+ return false;
968
+ if (typeof meta.layout === "string" && meta.layout.toLowerCase() === "changelog")
969
+ return true;
970
+ return basename(filePath).toLowerCase() === "changelog.md";
971
+ }
972
+ function loadMarkdownPageFromBody(filePath, slug, body, meta) {
956
973
  resetDirectiveCounter();
957
974
  const componentPreprocessed = preprocessComponents(body);
958
975
  const preprocessed = preprocessDirectives(componentPreprocessed);
@@ -964,6 +981,7 @@ export async function loadMarkdownPage(filePath, slug) {
964
981
  const cleanHtml = html.replace(/^\s*<h1[^>]*>(.*?)<\/h1>\s*/i, (_m, inner) => inner.replace(/<[^>]+>/g, "").trim() === title ? "" : _m);
965
982
  const sourcePath = relative(process.cwd(), filePath);
966
983
  return {
984
+ kind: "markdown",
967
985
  title,
968
986
  description,
969
987
  slug,
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-normalizer.d.ts","sourceRoot":"","sources":["../../src/core/mcp-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAoE,MAAM,YAAY,CAAC;AAC5G,OAAO,KAAK,EACV,cAAc,EAUf,MAAM,YAAY,CAAC;AAKpB,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CA+C9D"}
1
+ {"version":3,"file":"mcp-normalizer.d.ts","sourceRoot":"","sources":["../../src/core/mcp-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAoE,MAAM,YAAY,CAAC;AAC5G,OAAO,KAAK,EACV,cAAc,EASf,MAAM,YAAY,CAAC;AAKpB,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CA+C9D"}
@@ -1,6 +1,6 @@
1
1
  import type { NormalizedSpec } from "./types.js";
2
2
  import type { ResolvedTab } from "../config.js";
3
- import type { MarkdownPage } from "./markdown-loader.js";
3
+ import type { DocsPage } from "./markdown-loader.js";
4
4
  export interface SiteNavigation {
5
5
  tabs: SiteTab[];
6
6
  activeTabSlug: string;
@@ -38,7 +38,7 @@ export declare function buildNavFromSpec(spec: NormalizedSpec, tabSlug: string):
38
38
  * Build a navigation tab from markdown pages.
39
39
  * Groups come from the config; items come from loaded page data.
40
40
  */
41
- export declare function buildNavFromPages(tab: ResolvedTab, pagesByPath: Map<string, MarkdownPage>): SiteTab;
41
+ export declare function buildNavFromPages(tab: ResolvedTab, pagesByPath: Map<string, DocsPage>): SiteTab;
42
42
  /**
43
43
  * Assemble a SiteNavigation from a list of tabs.
44
44
  * Defaults to the first tab and first page as active.
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAOzD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CA+CT;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,WAAW,EAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,GACrC,OAAO,CA6BT;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,cAAc,CASnE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,cAAc,CAEhB"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/core/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAOrD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,GACd,OAAO,CAiDT;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,WAAW,EAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,GACjC,OAAO,CA6BT;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,cAAc,CASnE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,cAAc,CAEhB"}
@@ -1,3 +1,4 @@
1
+ import { tagNavigationLabel } from "./tag-utils.js";
1
2
  import { tabPath } from "../config.js";
2
3
  import { htmlId } from "../utils/html-id.js";
3
4
  // ---------------------------------------------------------------------------
@@ -25,6 +26,7 @@ export function buildNavFromSpec(spec, tabSlug) {
25
26
  }
26
27
  groups.push({ label: "", items: introItems });
27
28
  // Tag groups with operations
29
+ const tagsByName = new Map(spec.tags.map((tag) => [tag.name, tag]));
28
30
  for (const tag of spec.tags) {
29
31
  if (tag.hidden)
30
32
  continue;
@@ -34,7 +36,9 @@ export function buildNavFromSpec(spec, tabSlug) {
34
36
  id: `operation-${htmlId(op.path)}-${htmlId(op.method)}`,
35
37
  method: op.method,
36
38
  }));
37
- groups.push({ label: tag.name, items });
39
+ if (!items.length)
40
+ continue;
41
+ groups.push({ label: tagNavigationLabel(tag, tagsByName), items });
38
42
  }
39
43
  // Models group
40
44
  const schemaNames = Object.keys(spec.schemas);
@@ -1 +1 @@
1
- {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../src/core/normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,UAAU,EACV,cAAc,EAqBf,MAAM,YAAY,CAAC;AAMpB;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,cAAc,CAoBhE"}
1
+ {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../src/core/normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,UAAU,EACV,cAAc,EAsBf,MAAM,YAAY,CAAC;AAMpB;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,cAAc,CAoBhE"}
@@ -1,5 +1,5 @@
1
1
  const HTTP_METHODS = [
2
- "get", "post", "put", "delete", "patch", "options", "head", "trace",
2
+ "get", "post", "put", "delete", "patch", "options", "head", "trace", "query",
3
3
  ];
4
4
  /**
5
5
  * Transform a fully dereferenced OpenAPI 3.x document into a NormalizedSpec.
@@ -29,6 +29,7 @@ function normalizeInfo(doc) {
29
29
  const info = doc.info ?? {};
30
30
  return {
31
31
  title: str(info.title, "Untitled API"),
32
+ summary: optStr(info.summary),
32
33
  version: str(info.version, "0.0.0"),
33
34
  description: optStr(info.description),
34
35
  termsOfService: optStr(info.termsOfService),
@@ -82,7 +83,10 @@ function normalizeTags(rawTags, operations) {
82
83
  if (name) {
83
84
  tagMap.set(name, {
84
85
  name,
86
+ summary: optStr(t.summary),
85
87
  description: optStr(t.description),
88
+ parent: optStr(t.parent),
89
+ kind: optStr(t.kind),
86
90
  externalDocs: normalizeExternalDocs(t.externalDocs),
87
91
  operations: [],
88
92
  hidden: t["x-sourcey-hide"] === true,
@@ -106,10 +110,49 @@ function normalizeTags(rawTags, operations) {
106
110
  tagMap.get(tagName).operations.push(op);
107
111
  }
108
112
  }
109
- // Filter out tags with no operations (unless they have a description)
110
- return Array.from(tagMap.values()).filter((t) => t.operations.length > 0 || t.description);
113
+ const referencedAsParent = new Set();
114
+ for (const tag of tagMap.values()) {
115
+ if (tag.parent && tagMap.has(tag.parent)) {
116
+ referencedAsParent.add(tag.parent);
117
+ }
118
+ }
119
+ // Keep tags that contribute visible metadata or structure, even without operations.
120
+ return sortTagsByHierarchy(Array.from(tagMap.values()).filter((t) => t.operations.length > 0
121
+ || !!t.summary
122
+ || !!t.description
123
+ || !!t.kind
124
+ || referencedAsParent.has(t.name)));
125
+ }
126
+ function sortTagsByHierarchy(tags) {
127
+ const tagsByName = new Map(tags.map((tag) => [tag.name, tag]));
128
+ const childrenByParent = new Map();
129
+ const roots = [];
130
+ for (const tag of tags) {
131
+ if (tag.parent && tag.parent !== tag.name && tagsByName.has(tag.parent)) {
132
+ const siblings = childrenByParent.get(tag.parent) ?? [];
133
+ siblings.push(tag);
134
+ childrenByParent.set(tag.parent, siblings);
135
+ continue;
136
+ }
137
+ roots.push(tag);
138
+ }
139
+ const sorted = [];
140
+ const visited = new Set();
141
+ const visit = (tag) => {
142
+ if (visited.has(tag.name))
143
+ return;
144
+ visited.add(tag.name);
145
+ sorted.push(tag);
146
+ for (const child of childrenByParent.get(tag.name) ?? []) {
147
+ visit(child);
148
+ }
149
+ };
150
+ for (const tag of roots)
151
+ visit(tag);
152
+ for (const tag of tags)
153
+ visit(tag);
154
+ return sorted;
111
155
  }
112
- // ── Operations ─────────────────────────────────────────────────────
113
156
  function normalizeOperations(paths, globalSecurity) {
114
157
  if (!paths)
115
158
  return [];
@@ -173,6 +216,9 @@ function normalizeParameters(params) {
173
216
  schema: p.schema
174
217
  ? normalizeSchema(p.schema)
175
218
  : undefined,
219
+ content: p.content
220
+ ? normalizeContent(p.content)
221
+ : undefined,
176
222
  example: p.example,
177
223
  examples: p.examples
178
224
  ? normalizeExamples(p.examples)
@@ -201,15 +247,57 @@ function normalizeContent(content) {
201
247
  examples: value.examples
202
248
  ? normalizeExamples(value.examples)
203
249
  : undefined,
250
+ encoding: isRecord(value.encoding)
251
+ ? normalizeEncodingMap(value.encoding)
252
+ : undefined,
253
+ prefixEncoding: Array.isArray(value.prefixEncoding)
254
+ ? value.prefixEncoding
255
+ .filter(isRecord)
256
+ .map((entry) => normalizeEncodingObject(entry))
257
+ : undefined,
258
+ itemEncoding: isRecord(value.itemEncoding)
259
+ ? normalizeEncodingObject(value.itemEncoding)
260
+ : undefined,
204
261
  };
205
262
  }
206
263
  return result;
207
264
  }
265
+ function normalizeEncodingMap(encodings) {
266
+ const result = {};
267
+ for (const [name, encoding] of Object.entries(encodings)) {
268
+ if (!isRecord(encoding))
269
+ continue;
270
+ result[name] = normalizeEncodingObject(encoding);
271
+ }
272
+ return result;
273
+ }
274
+ function normalizeEncodingObject(encoding) {
275
+ return {
276
+ contentType: optStr(encoding.contentType),
277
+ headers: isRecord(encoding.headers)
278
+ ? normalizeResponseHeaders(encoding.headers)
279
+ : undefined,
280
+ style: optStr(encoding.style),
281
+ explode: typeof encoding.explode === "boolean" ? encoding.explode : undefined,
282
+ allowReserved: typeof encoding.allowReserved === "boolean"
283
+ ? encoding.allowReserved
284
+ : undefined,
285
+ prefixEncoding: Array.isArray(encoding.prefixEncoding)
286
+ ? encoding.prefixEncoding
287
+ .filter(isRecord)
288
+ .map((entry) => normalizeEncodingObject(entry))
289
+ : undefined,
290
+ itemEncoding: isRecord(encoding.itemEncoding)
291
+ ? normalizeEncodingObject(encoding.itemEncoding)
292
+ : undefined,
293
+ };
294
+ }
208
295
  function normalizeResponses(responses) {
209
296
  if (!responses)
210
297
  return [];
211
298
  return Object.entries(responses).map(([statusCode, r]) => ({
212
299
  statusCode,
300
+ summary: optStr(r.summary),
213
301
  description: str(r.description, ""),
214
302
  content: r.content
215
303
  ? normalizeContent(r.content)
@@ -234,6 +322,9 @@ function normalizeResponseHeaders(headers) {
234
322
  schema: h.schema
235
323
  ? normalizeSchema(h.schema)
236
324
  : undefined,
325
+ content: h.content
326
+ ? normalizeContent(h.content)
327
+ : undefined,
237
328
  };
238
329
  }
239
330
  return result;
@@ -409,6 +500,8 @@ function normalizeSecuritySchemes(schemes) {
409
500
  ? normalizeOAuthFlows(raw.flows)
410
501
  : undefined,
411
502
  openIdConnectUrl: optStr(raw.openIdConnectUrl),
503
+ oauth2MetadataUrl: optStr(raw.oauth2MetadataUrl),
504
+ deprecated: raw.deprecated === true,
412
505
  };
413
506
  }
414
507
  return result;
@@ -420,6 +513,7 @@ function normalizeOAuthFlows(flows) {
420
513
  "password",
421
514
  "clientCredentials",
422
515
  "authorizationCode",
516
+ "deviceAuthorization",
423
517
  ]) {
424
518
  if (flows[flowType]) {
425
519
  result[flowType] = normalizeOAuthFlow(flows[flowType]);
@@ -430,6 +524,7 @@ function normalizeOAuthFlows(flows) {
430
524
  function normalizeOAuthFlow(flow) {
431
525
  return {
432
526
  authorizationUrl: optStr(flow.authorizationUrl),
527
+ deviceAuthorizationUrl: optStr(flow.deviceAuthorizationUrl),
433
528
  tokenUrl: optStr(flow.tokenUrl),
434
529
  refreshUrl: optStr(flow.refreshUrl),
435
530
  scopes: (flow.scopes ?? {}),
@@ -469,3 +564,6 @@ function str(value, fallback) {
469
564
  function optStr(value) {
470
565
  return typeof value === "string" ? value : undefined;
471
566
  }
567
+ function isRecord(value) {
568
+ return typeof value === "object" && value !== null;
569
+ }
@@ -3,8 +3,8 @@ import type { LoadedSpec, ParsedSpec } from "./types.js";
3
3
  * Dereference a loaded spec document.
4
4
  * All $ref pointers are resolved (local, remote, circular).
5
5
  *
6
- * For local files, dereferences from the file path directly so
7
- * relative $refs resolve correctly against the file's directory.
6
+ * Uses the document source as the base URI, but honors OpenAPI 3.2's
7
+ * `$self` field when present so relative references resolve canonically.
8
8
  */
9
9
  export declare function parseSpec(loaded: LoadedSpec): Promise<ParsedSpec>;
10
10
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/core/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAmB,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAyBvE"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/core/parser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAmB,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBvE"}
@@ -1,26 +1,16 @@
1
- import { dereference } from "@readme/openapi-parser";
1
+ import { $RefParser } from "@apidevtools/json-schema-ref-parser";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
2
3
  /**
3
4
  * Dereference a loaded spec document.
4
5
  * All $ref pointers are resolved (local, remote, circular).
5
6
  *
6
- * For local files, dereferences from the file path directly so
7
- * relative $refs resolve correctly against the file's directory.
7
+ * Uses the document source as the base URI, but honors OpenAPI 3.2's
8
+ * `$self` field when present so relative references resolve canonically.
8
9
  */
9
10
  export async function parseSpec(loaded) {
10
11
  try {
11
- const isLocalFile = !loaded.source.startsWith("http://") && !loaded.source.startsWith("https://");
12
- let dereferenced;
13
- if (isLocalFile) {
14
- // Dereference from file path so relative $refs resolve correctly
15
- dereferenced = await dereference(loaded.source, {
16
- dereference: { circular: "ignore" },
17
- });
18
- }
19
- else {
20
- dereferenced = await dereference(structuredClone(loaded.raw), {
21
- dereference: { circular: "ignore" },
22
- });
23
- }
12
+ const parser = new $RefParser();
13
+ const dereferenced = await parser.dereference(resolveDocumentUri(loaded), structuredClone(loaded.raw), { dereference: { circular: "ignore" } });
24
14
  return {
25
15
  document: dereferenced,
26
16
  source: loaded.source,
@@ -31,3 +21,20 @@ export async function parseSpec(loaded) {
31
21
  throw new Error(`Failed to parse OpenAPI spec: ${message}`);
32
22
  }
33
23
  }
24
+ function resolveDocumentUri(loaded) {
25
+ const self = typeof loaded.raw["$self"] === "string" ? loaded.raw["$self"] : undefined;
26
+ if (!self)
27
+ return loaded.source;
28
+ try {
29
+ if (loaded.source.startsWith("http://") || loaded.source.startsWith("https://")) {
30
+ return new URL(self, loaded.source).toString();
31
+ }
32
+ const resolved = new URL(self, pathToFileURL(loaded.source));
33
+ return resolved.protocol === "file:"
34
+ ? fileURLToPath(resolved)
35
+ : resolved.toString();
36
+ }
37
+ catch {
38
+ return loaded.source;
39
+ }
40
+ }
@@ -1,6 +1,7 @@
1
1
  import type { NormalizedSpec } from "./types.js";
2
- import type { MarkdownPage } from "./markdown-loader.js";
2
+ import type { DocsPage } from "./markdown-loader.js";
3
3
  import type { SiteNavigation } from "./navigation.js";
4
+ import type { PrettyUrls } from "../site-url.js";
4
5
  export interface SearchEntry {
5
6
  /** Display title */
6
7
  title: string;
@@ -23,5 +24,5 @@ export interface SearchEntry {
23
24
  * Build a search index from specs and markdown pages.
24
25
  * Returns a JSON string ready to write to disk.
25
26
  */
26
- export declare function buildSearchIndex(specs: Map<string, NormalizedSpec>, pages: Map<string, MarkdownPage[]>, navigation: SiteNavigation, assetBase?: string, featuredSlugs?: string[]): string;
27
+ export declare function buildSearchIndex(specs: Map<string, NormalizedSpec>, pages: Map<string, DocsPage[]>, navigation: SiteNavigation, assetBase?: string, featuredSlugs?: string[], prettyUrls?: PrettyUrls): string;
27
28
  //# sourceMappingURL=search-indexer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-indexer.d.ts","sourceRoot":"","sources":["../../src/core/search-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,WAAW;IAC1B,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAClC,UAAU,EAAE,cAAc,EAC1B,SAAS,SAAM,EACf,aAAa,GAAE,MAAM,EAAO,GAC3B,MAAM,CAoER"}
1
+ {"version":3,"file":"search-indexer.d.ts","sourceRoot":"","sources":["../../src/core/search-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAOjD,MAAM,WAAW,WAAW;IAC1B,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EAClC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAC9B,UAAU,EAAE,cAAc,EAC1B,SAAS,SAAM,EACf,aAAa,GAAE,MAAM,EAAO,EAC5B,UAAU,GAAE,UAAkB,GAC7B,MAAM,CAuFR"}
@@ -6,21 +6,29 @@ import { htmlId } from "../utils/html-id.js";
6
6
  * Build a search index from specs and markdown pages.
7
7
  * Returns a JSON string ready to write to disk.
8
8
  */
9
- export function buildSearchIndex(specs, pages, navigation, assetBase = "/", featuredSlugs = []) {
9
+ export function buildSearchIndex(specs, pages, navigation, assetBase = "/", featuredSlugs = [], prettyUrls = false) {
10
10
  const base = assetBase.endsWith("/") ? assetBase : assetBase + "/";
11
11
  const featuredSet = new Set(featuredSlugs);
12
12
  const entries = [];
13
+ const pageUrl = (tabSlug, slug) => {
14
+ const tabPrefix = tabSlug ? `${base}${tabSlug}/` : base;
15
+ if (prettyUrls === "strip")
16
+ return `${tabPrefix}${slug}`;
17
+ if (prettyUrls === "slash")
18
+ return `${tabPrefix}${slug}/`;
19
+ return `${tabPrefix}${slug}.html`;
20
+ };
13
21
  // Index OpenAPI specs
14
22
  for (const [tabSlug, spec] of specs) {
15
23
  const tab = navigation.tabs.find((t) => t.slug === tabSlug);
16
24
  const tabLabel = tab?.label ?? tabSlug;
17
- const basePath = `${tabSlug}/index.html`;
25
+ const tabBase = tabSlug ? `${base}${tabSlug}/` : base;
18
26
  // Operations
19
27
  for (const op of spec.operations) {
20
28
  entries.push({
21
29
  title: op.summary ?? `${op.method.toUpperCase()} ${op.path}`,
22
30
  content: op.description?.slice(0, 200) ?? "",
23
- url: `${base}${basePath}#operation-${htmlId(op.path)}-${htmlId(op.method)}`,
31
+ url: `${tabBase}#operation-${htmlId(op.path)}-${htmlId(op.method)}`,
24
32
  method: op.method,
25
33
  path: op.path,
26
34
  tab: tabLabel,
@@ -32,7 +40,7 @@ export function buildSearchIndex(specs, pages, navigation, assetBase = "/", feat
32
40
  entries.push({
33
41
  title: name,
34
42
  content: spec.schemas[name].description?.slice(0, 200) ?? "",
35
- url: `${base}${basePath}#definition-${htmlId(name)}`,
43
+ url: `${tabBase}#definition-${htmlId(name)}`,
36
44
  tab: tabLabel,
37
45
  category: "Models",
38
46
  });
@@ -43,26 +51,39 @@ export function buildSearchIndex(specs, pages, navigation, assetBase = "/", feat
43
51
  const tab = navigation.tabs.find((t) => t.slug === tabSlug);
44
52
  const tabLabel = tab?.label ?? tabSlug;
45
53
  for (const page of tabPages) {
46
- // Page itself
47
- const pageBase = tabSlug ? `${base}${tabSlug}/` : base;
48
54
  const isFeatured = featuredSet.has(page.slug);
55
+ const href = pageUrl(tabSlug, page.slug);
49
56
  entries.push({
50
57
  title: page.title,
51
- content: page.description || stripHtml(page.html).slice(0, 200),
52
- url: `${pageBase}${page.slug}.html`,
58
+ content: page.kind === "markdown"
59
+ ? page.description || stripHtml(page.html).slice(0, 200)
60
+ : page.description || summarizeChangelog(page).slice(0, 200),
61
+ url: href,
53
62
  tab: tabLabel,
54
63
  category: "Pages",
55
64
  ...(isFeatured && { featured: true }),
56
65
  });
57
- // Headings within page
58
- for (const heading of page.headings) {
59
- entries.push({
60
- title: heading.text,
61
- content: `${page.title} — ${heading.text}`,
62
- url: `${pageBase}${page.slug}.html#${heading.id}`,
63
- tab: tabLabel,
64
- category: "Sections",
65
- });
66
+ if (page.kind === "markdown") {
67
+ for (const heading of page.headings) {
68
+ entries.push({
69
+ title: heading.text,
70
+ content: `${page.title} — ${heading.text}`,
71
+ url: `${href}#${heading.id}`,
72
+ tab: tabLabel,
73
+ category: "Sections",
74
+ });
75
+ }
76
+ }
77
+ else {
78
+ for (const version of page.changelog.versions) {
79
+ entries.push({
80
+ title: version.version ?? "Unreleased",
81
+ content: summarizeVersion(version).slice(0, 200),
82
+ url: `${href}#${version.id}`,
83
+ tab: tabLabel,
84
+ category: "Releases",
85
+ });
86
+ }
66
87
  }
67
88
  }
68
89
  }
@@ -74,3 +95,15 @@ export function buildSearchIndex(specs, pages, navigation, assetBase = "/", feat
74
95
  function stripHtml(html) {
75
96
  return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
76
97
  }
98
+ function summarizeChangelog(page) {
99
+ return page.changelog.versions
100
+ .slice(0, 3)
101
+ .map(summarizeVersion)
102
+ .join(" ");
103
+ }
104
+ function summarizeVersion(version) {
105
+ if (version.summary)
106
+ return version.summary;
107
+ const entries = version.sections.flatMap((section) => section.entries.map((entry) => entry.text));
108
+ return entries.slice(0, 3).join(" ");
109
+ }
@@ -0,0 +1,5 @@
1
+ import type { NormalizedTag } from "./types.js";
2
+ export declare function tagDisplayName(tag: Pick<NormalizedTag, "name" | "summary">): string;
3
+ export declare function tagLineage(tag: Pick<NormalizedTag, "name" | "parent">, tagsByName: Map<string, NormalizedTag>): NormalizedTag[];
4
+ export declare function tagNavigationLabel(tag: NormalizedTag, tagsByName: Map<string, NormalizedTag>): string;
5
+ //# sourceMappingURL=tag-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-utils.d.ts","sourceRoot":"","sources":["../../src/core/tag-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAEnF;AAED,wBAAgB,UAAU,CACxB,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC,EAC3C,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GACrC,aAAa,EAAE,CAcjB;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,aAAa,EAClB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GACrC,MAAM,CAIR"}
@@ -0,0 +1,22 @@
1
+ export function tagDisplayName(tag) {
2
+ return tag.summary ?? tag.name;
3
+ }
4
+ export function tagLineage(tag, tagsByName) {
5
+ const lineage = [];
6
+ const seen = new Set([tag.name]);
7
+ let parentName = tag.parent;
8
+ while (parentName && !seen.has(parentName)) {
9
+ const parent = tagsByName.get(parentName);
10
+ if (!parent)
11
+ break;
12
+ lineage.unshift(parent);
13
+ seen.add(parent.name);
14
+ parentName = parent.parent;
15
+ }
16
+ return lineage;
17
+ }
18
+ export function tagNavigationLabel(tag, tagsByName) {
19
+ return [...tagLineage(tag, tagsByName), tag]
20
+ .map(tagDisplayName)
21
+ .join(" / ");
22
+ }