sourcey 3.6.0 → 3.6.2

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 (86) hide show
  1. package/README.md +5 -2
  2. package/dist/adapters/index.d.ts +3 -2
  3. package/dist/adapters/index.d.ts.map +1 -1
  4. package/dist/adapters/index.js +35 -3
  5. package/dist/adapters/types.d.ts +8 -2
  6. package/dist/adapters/types.d.ts.map +1 -1
  7. package/dist/cli.js +28 -34
  8. package/dist/client/search.js +181 -119
  9. package/dist/client/tabs.js +23 -0
  10. package/dist/components/layout/Head.d.ts.map +1 -1
  11. package/dist/components/layout/Head.js +7 -6
  12. package/dist/components/openapi/Introduction.d.ts.map +1 -1
  13. package/dist/components/openapi/Introduction.js +16 -1
  14. package/dist/components/openapi/Security.d.ts.map +1 -1
  15. package/dist/components/openapi/Security.js +18 -2
  16. package/dist/config.d.ts +70 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +23 -7
  19. package/dist/core/api-rendering.d.ts +47 -0
  20. package/dist/core/api-rendering.d.ts.map +1 -0
  21. package/dist/core/api-rendering.js +76 -0
  22. package/dist/core/doxygen-loader.d.ts +1 -1
  23. package/dist/core/doxygen-loader.d.ts.map +1 -1
  24. package/dist/core/doxygen-loader.js +37 -19
  25. package/dist/core/godoc-loader.d.ts.map +1 -1
  26. package/dist/core/godoc-loader.js +17 -6
  27. package/dist/core/markdown-loader.d.ts.map +1 -1
  28. package/dist/core/markdown-loader.js +8 -5
  29. package/dist/core/mcp-normalizer.d.ts.map +1 -1
  30. package/dist/core/mcp-normalizer.js +48 -34
  31. package/dist/core/parser.d.ts.map +1 -1
  32. package/dist/core/parser.js +75 -4
  33. package/dist/core/rustdoc-introspector.d.ts +29 -0
  34. package/dist/core/rustdoc-introspector.d.ts.map +1 -0
  35. package/dist/core/rustdoc-introspector.js +236 -0
  36. package/dist/core/rustdoc-loader.d.ts +25 -0
  37. package/dist/core/rustdoc-loader.d.ts.map +1 -0
  38. package/dist/core/rustdoc-loader.js +472 -0
  39. package/dist/core/rustdoc-render.d.ts +82 -0
  40. package/dist/core/rustdoc-render.d.ts.map +1 -0
  41. package/dist/core/rustdoc-render.js +808 -0
  42. package/dist/core/rustdoc-types.d.ts +314 -0
  43. package/dist/core/rustdoc-types.d.ts.map +1 -0
  44. package/dist/core/rustdoc-types.js +18 -0
  45. package/dist/core/search-indexer.js +1 -1
  46. package/dist/core/sourcey-rustdoc/Cargo.lock +705 -0
  47. package/dist/core/sourcey-rustdoc/Cargo.toml +38 -0
  48. package/dist/core/sourcey-rustdoc/README.md +59 -0
  49. package/dist/core/sourcey-rustdoc/src/diagnostics.rs +42 -0
  50. package/dist/core/sourcey-rustdoc/src/doctest.rs +268 -0
  51. package/dist/core/sourcey-rustdoc/src/extract.rs +492 -0
  52. package/dist/core/sourcey-rustdoc/src/lib.rs +58 -0
  53. package/dist/core/sourcey-rustdoc/src/links.rs +126 -0
  54. package/dist/core/sourcey-rustdoc/src/main.rs +256 -0
  55. package/dist/core/sourcey-rustdoc/src/signature.rs +415 -0
  56. package/dist/core/sourcey-rustdoc/src/spec.rs +390 -0
  57. package/dist/core/sourcey-rustdoc/tests/format_version.rs +45 -0
  58. package/dist/dev-server.d.ts.map +1 -1
  59. package/dist/dev-server.js +50 -1
  60. package/dist/index.d.ts +4 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +22 -13
  63. package/dist/init.js +2 -2
  64. package/dist/renderer/changelog-feed.d.ts.map +1 -1
  65. package/dist/renderer/changelog-feed.js +27 -19
  66. package/dist/renderer/context.d.ts +1 -0
  67. package/dist/renderer/context.d.ts.map +1 -1
  68. package/dist/renderer/html-builder.d.ts.map +1 -1
  69. package/dist/renderer/html-builder.js +4 -3
  70. package/dist/renderer/llms.js +57 -2
  71. package/dist/site-assembly.d.ts +5 -0
  72. package/dist/site-assembly.d.ts.map +1 -1
  73. package/dist/site-assembly.js +57 -2
  74. package/dist/themes/default/sourcey.css +300 -0
  75. package/dist/utils/code-samples.d.ts +1 -0
  76. package/dist/utils/code-samples.d.ts.map +1 -1
  77. package/dist/utils/code-samples.js +30 -22
  78. package/dist/utils/html.d.ts +10 -0
  79. package/dist/utils/html.d.ts.map +1 -0
  80. package/dist/utils/html.js +152 -0
  81. package/dist/utils/http.d.ts +0 -2
  82. package/dist/utils/http.d.ts.map +1 -1
  83. package/dist/utils/http.js +19 -34
  84. package/dist/utils/markdown.d.ts.map +1 -1
  85. package/dist/utils/markdown.js +102 -37
  86. package/package.json +10 -5
@@ -0,0 +1,808 @@
1
+ /**
2
+ * Rustdoc-grade rendering for the sourcey rustdoc adapter.
3
+ *
4
+ * Renders sourcey-native output (the same look as the godoc/doxygen/openapi
5
+ * adapters), not a copy of rustdoc's own DOM. Items render inline as sourcey
6
+ * sections; the only collapsible is the trait-implementor list. Code blocks go
7
+ * through sourcey's shared Shiki pipeline. Anchors still follow rustdoc's
8
+ * scheme (#method.<name>, #impl-Trait-for-Type%3CK%3E) so inbound deep-links
9
+ * from a rustdoc URL keep working.
10
+ */
11
+ import { renderMarkdown, renderCodeBlock } from "../utils/markdown.js";
12
+ import { apiImplToggle, apiItemInfoRow, apiSectionAnchor, apiStabilityCallout, apiSymbolLink, escapeAttr, escapeHtml, } from "./api-rendering.js";
13
+ import { RUSTDOC_DIAGNOSTIC_CODES } from "./rustdoc-types.js";
14
+ // ---------------------------------------------------------------------------
15
+ // Anchor encoding
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Encode a parametric impl anchor in rustdoc's URL-encoded form, e.g.
19
+ * impl-Clone-for-HashMap%3CK,+V,+S,+A%3E
20
+ */
21
+ export function encodeImplAnchor(traitName, forType) {
22
+ return `impl-${encodeGenericSegment(traitName)}-for-${encodeGenericSegment(forType)}`;
23
+ }
24
+ /** Encode a single segment for anchor-ID use: <,>,space and special chars escaped rustdoc-style. */
25
+ export function encodeGenericSegment(value) {
26
+ return value.replace(/</g, "%3C").replace(/>/g, "%3E").replace(/, /g, ",+").replace(/ /g, "+");
27
+ }
28
+ export function itemAnchor(item) {
29
+ switch (item.inner.kind) {
30
+ case "function":
31
+ return `fn.${slug(item.name)}`;
32
+ case "struct":
33
+ return `struct.${slug(item.name)}`;
34
+ case "enum":
35
+ return `enum.${slug(item.name)}`;
36
+ case "variant":
37
+ return `variant.${slug(item.name)}`;
38
+ case "union":
39
+ return `union.${slug(item.name)}`;
40
+ case "trait":
41
+ return `trait.${slug(item.name)}`;
42
+ case "trait_alias":
43
+ return `traitalias.${slug(item.name)}`;
44
+ case "type_alias":
45
+ return `type.${slug(item.name)}`;
46
+ case "constant":
47
+ return `constant.${slug(item.name)}`;
48
+ case "static":
49
+ return `static.${slug(item.name)}`;
50
+ case "macro":
51
+ return `macro.${slug(item.name)}`;
52
+ case "proc_macro":
53
+ return `macro.${slug(item.name)}`;
54
+ case "assoc_type":
55
+ return `associatedtype.${slug(item.name)}`;
56
+ case "assoc_const":
57
+ return `associatedconstant.${slug(item.name)}`;
58
+ case "struct_field":
59
+ return `structfield.${slug(item.name)}`;
60
+ case "use":
61
+ return `reexport.${slug(useBaseName(item.inner))}`;
62
+ case "module":
63
+ return `mod.${slug(item.name)}`;
64
+ case "impl":
65
+ return implAnchor(item);
66
+ case "primitive":
67
+ return `primitive.${slug(item.name)}`;
68
+ case "extern_type":
69
+ return `externtype.${slug(item.name)}`;
70
+ }
71
+ }
72
+ function implAnchor(item) {
73
+ if (item.inner.kind !== "impl")
74
+ return `impl-${slug(item.name)}`;
75
+ const traitDisplay = item.inner.trait_path?.display ?? "";
76
+ const forDisplay = item.inner.for_type.display;
77
+ if (traitDisplay)
78
+ return encodeImplAnchor(traitDisplay, forDisplay);
79
+ return `impl-${encodeGenericSegment(forDisplay)}`;
80
+ }
81
+ function slug(name) {
82
+ if (!name)
83
+ return "";
84
+ return name;
85
+ }
86
+ /** Base name of a re-export: the imported name, or the last path segment. */
87
+ function useBaseName(inner) {
88
+ return inner.name || (inner.source.split("::").filter(Boolean).pop() ?? inner.source);
89
+ }
90
+ /**
91
+ * Heading/anchor display name for an item. Re-exports (`use`) carry their name
92
+ * on `inner`, not the top-level `item.name` which rustdoc leaves null, so we
93
+ * derive it from the imported name or the re-exported path (with `::*` for globs).
94
+ */
95
+ export function itemDisplayName(item) {
96
+ if (item.inner.kind === "use") {
97
+ const base = useBaseName(item.inner);
98
+ return item.inner.is_glob ? `${base}::*` : base;
99
+ }
100
+ return item.name ?? "";
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Signature formatter
104
+ // ---------------------------------------------------------------------------
105
+ const SIGNATURE_INLINE_LIMIT = 100;
106
+ /** Multi-line signature renderer with rustdoc break rules. */
107
+ export function renderSignature(signature, ctx) {
108
+ const inline = renderInlineSignature(signature, ctx);
109
+ if (signature.display.length <= SIGNATURE_INLINE_LIMIT) {
110
+ return `<span class="code-header rust-signature">${inline}</span>`;
111
+ }
112
+ return `<span class="code-header rust-signature">${renderMultilineSignature(signature, ctx)}</span>`;
113
+ }
114
+ function renderInlineSignature(signature, ctx) {
115
+ const parts = [];
116
+ for (const token of signature.tokens) {
117
+ parts.push(renderToken(token, ctx));
118
+ }
119
+ return parts.join("");
120
+ }
121
+ function renderMultilineSignature(signature, ctx) {
122
+ // Heuristic: break each input onto its own line; place `where` if present on
123
+ // its own line. Keeps in sync with rustdoc's visual cadence without
124
+ // requiring an AST.
125
+ let head = "";
126
+ const inputs = [];
127
+ let output = "";
128
+ let seenOpenParen = false;
129
+ let seenCloseParen = false;
130
+ let currentInput = "";
131
+ for (const token of signature.tokens) {
132
+ const rendered = renderToken(token, ctx);
133
+ if (!seenOpenParen) {
134
+ head += rendered;
135
+ if (token.kind === "punct" && token.text === "(")
136
+ seenOpenParen = true;
137
+ continue;
138
+ }
139
+ if (!seenCloseParen) {
140
+ if (token.kind === "punct" && token.text === ")") {
141
+ if (currentInput.trim().length > 0)
142
+ inputs.push(currentInput);
143
+ currentInput = "";
144
+ seenCloseParen = true;
145
+ output = rendered;
146
+ continue;
147
+ }
148
+ if (token.kind === "punct" && token.text === "," && currentInput.trim().length > 0) {
149
+ inputs.push(currentInput.trimEnd());
150
+ currentInput = "";
151
+ continue;
152
+ }
153
+ currentInput += rendered;
154
+ continue;
155
+ }
156
+ output += rendered;
157
+ }
158
+ const inputsBlock = inputs
159
+ .map((line, idx) => ` ${line.trimStart()}${idx === inputs.length - 1 ? "" : ","}`)
160
+ .join("\n");
161
+ return `${head}\n${inputsBlock}\n${output}`;
162
+ }
163
+ function renderToken(token, ctx) {
164
+ switch (token.kind) {
165
+ case "keyword":
166
+ return `<span class="kw">${escapeHtml(token.text)}</span>`;
167
+ case "punct":
168
+ return escapeHtml(token.text);
169
+ case "generic":
170
+ return `<span class="generic">${escapeHtml(token.text)}</span>`;
171
+ case "lifetime":
172
+ return `<span class="lifetime">'${escapeHtml(token.text)}</span>`;
173
+ case "type": {
174
+ const target = token.target;
175
+ if (target) {
176
+ const href = typePathHref(target, ctx);
177
+ if (href) {
178
+ return apiSymbolLink({
179
+ kind: typeLinkClass(target),
180
+ href,
181
+ text: token.text,
182
+ title: target.path.join("::"),
183
+ });
184
+ }
185
+ }
186
+ return `<span class="ident">${escapeHtml(token.text)}</span>`;
187
+ }
188
+ case "whitespace":
189
+ return " ";
190
+ case "newline":
191
+ return "\n";
192
+ }
193
+ }
194
+ function typeLinkClass(target) {
195
+ return target.external ? "rust-extern" : "rust-symbol";
196
+ }
197
+ function typePathHref(target, ctx) {
198
+ if (!target.external) {
199
+ if (target.path.length === 0)
200
+ return null;
201
+ const name = target.path[target.path.length - 1];
202
+ // Find the actual item in the snapshot so we can use ITS path (which
203
+ // tells us which module page it lives on), not the inbound target.path
204
+ // which may be a re-export shortcut.
205
+ const candidate = [...ctx.itemsById.values()].find((i) => i.name === name && i.path.join("::") === target.path.join("::")) ?? [...ctx.itemsById.values()].find((i) => i.name === name);
206
+ if (candidate) {
207
+ const crateName = candidate.path[0] ?? ctx.crate.name;
208
+ const modulePath = candidate.path.slice(1, -1);
209
+ const anchorFragment = itemAnchor(candidate);
210
+ return tabHref(ctx.tabSlug, pageSlugFor(crateName, modulePath)) + `#${anchorFragment}`;
211
+ }
212
+ // Last resort: guess from the inbound path.
213
+ const crateName = target.path[0] === ctx.crate.name ? ctx.crate.name : target.path[0];
214
+ const modulePath = target.path.slice(1, -1);
215
+ return (tabHref(ctx.tabSlug, pageSlugFor(crateName, modulePath)) + `#${encodeURIComponent(name)}`);
216
+ }
217
+ if (target.html_root_url) {
218
+ return joinUrl(target.html_root_url, target.path.join("/"));
219
+ }
220
+ return joinUrl("https://docs.rs", `${target.path[0] ?? ""}/latest/${target.path.join("/")}`);
221
+ }
222
+ function tabHref(tabSlug, slug) {
223
+ if (!slug)
224
+ return `${tabSlug}/`;
225
+ return `${tabSlug}/${slug}.html`;
226
+ }
227
+ function pageSlugFor(crateName, modulePath) {
228
+ const base = `pkg-${slugifyId(crateName)}`;
229
+ if (modulePath.length === 0)
230
+ return base;
231
+ return `${base}-${modulePath.map(slugifyId).join("-")}`;
232
+ }
233
+ function slugifyId(value) {
234
+ return value
235
+ .toLowerCase()
236
+ .replace(/[^a-z0-9]+/g, "-")
237
+ .replace(/^-+|-+$/g, "");
238
+ }
239
+ function joinUrl(base, suffix) {
240
+ const trimmedBase = base.replace(/\/+$/, "");
241
+ const trimmedSuffix = suffix.replace(/^\/+/, "");
242
+ return `${trimmedBase}/${trimmedSuffix}`;
243
+ }
244
+ // ---------------------------------------------------------------------------
245
+ // Intra-doc link resolver
246
+ // ---------------------------------------------------------------------------
247
+ /**
248
+ * Rewrite markdown intra-doc links using the rustdoc `Item.links` map.
249
+ *
250
+ * `Item.links` is a label→item-id map. We look up each link target in the
251
+ * markdown, substitute either an internal sourcey URL (for items in this
252
+ * crate) or a docs.rs / doc.rust-lang.org URL (for cross-crate items),
253
+ * leaving plain prose untouched. Unresolved labels render as plain text
254
+ * with `title="unresolved intra-doc link"`.
255
+ */
256
+ export function resolveIntraDocLinks(markdown, links, ctx) {
257
+ const unresolved = [];
258
+ // Match both `[label]` and ``[`label`]`` forms used by rustdoc intra-doc links.
259
+ // Match `[label]` or ``[`label`]`` but NOT `[label](url)` (regular markdown
260
+ // inline links keep their target URL).
261
+ const replaced = markdown.replace(/\[(`?)([^`\]\n]+)\1\](?:\[\])?(?!\()/g, (_match, _tick, label) => {
262
+ const target = lookupLinkTarget(links, label);
263
+ const display = `<code>${escapeHtml(label)}</code>`;
264
+ if (!target) {
265
+ unresolved.push(label);
266
+ return `<code title="unresolved intra-doc link">${escapeHtml(label)}</code>`;
267
+ }
268
+ if (target.kind === "internal") {
269
+ const item = ctx.itemsById.get(target.id);
270
+ if (!item) {
271
+ unresolved.push(label);
272
+ return `<code title="unresolved intra-doc link">${escapeHtml(label)}</code>`;
273
+ }
274
+ const crateName = item.path[0] ?? ctx.crate.name;
275
+ const modulePath = item.path.slice(1, -1);
276
+ const href = tabHref(ctx.tabSlug, pageSlugFor(crateName, modulePath)) + `#${itemAnchor(item)}`;
277
+ return `<a class="rust-symbol" href="${escapeAttr(href)}">${display}</a>`;
278
+ }
279
+ const externalHref = externalLinkHref(target);
280
+ return `<a class="rust-extern" href="${escapeAttr(externalHref)}" rel="noopener" target="_blank">${display}</a>`;
281
+ });
282
+ return { html: replaced, unresolved };
283
+ }
284
+ function lookupLinkTarget(links, label) {
285
+ // rustdoc's Item.links keys can include backticks, full paths, or aliases.
286
+ // Try common variants before giving up.
287
+ return links[label] ?? links[`\`${label}\``] ?? links[label.replace(/^`(.+)`$/, "$1")];
288
+ }
289
+ function externalLinkHref(target) {
290
+ const isStdLike = ["std", "core", "alloc", "proc_macro"].includes(target.crate_name);
291
+ if (isStdLike) {
292
+ return `https://doc.rust-lang.org/stable/${target.path.join("/")}`;
293
+ }
294
+ if (target.html_root_url) {
295
+ const trimmed = target.html_root_url.replace(/\/+$/, "");
296
+ return `${trimmed}/${target.path.join("/")}`;
297
+ }
298
+ return `https://docs.rs/${target.crate_name}/latest/${target.path.join("/")}`;
299
+ }
300
+ // ---------------------------------------------------------------------------
301
+ // Stability + item-info row helpers
302
+ // ---------------------------------------------------------------------------
303
+ export function renderStabilityCallouts(item) {
304
+ const parts = [];
305
+ if (item.deprecation) {
306
+ parts.push(apiStabilityCallout({
307
+ kind: "deprecated",
308
+ since: item.deprecation.since,
309
+ reason: item.deprecation.note,
310
+ }));
311
+ }
312
+ const stability = item.stability;
313
+ if (stability?.level === "unstable") {
314
+ parts.push(apiStabilityCallout({
315
+ kind: "unstable",
316
+ featureName: stability.feature,
317
+ issueId: stability.issue,
318
+ }));
319
+ }
320
+ for (const featureName of item.feature_gates) {
321
+ parts.push(apiStabilityCallout({ kind: "portability", featureName }));
322
+ }
323
+ if (hasAttr(item, "non_exhaustive")) {
324
+ parts.push(apiStabilityCallout({ kind: "non_exhaustive" }));
325
+ }
326
+ const mustUseReason = extractMustUseReason(item);
327
+ if (mustUseReason !== null) {
328
+ parts.push(apiStabilityCallout({
329
+ kind: "must_use",
330
+ reason: mustUseReason === "" ? undefined : mustUseReason,
331
+ }));
332
+ }
333
+ return parts.join("\n");
334
+ }
335
+ function hasAttr(item, marker) {
336
+ return item.attrs_structured.some((a) => a.includes(marker));
337
+ }
338
+ function extractMustUseReason(item) {
339
+ for (const attr of item.attrs_structured) {
340
+ if (attr.startsWith("MustUse"))
341
+ return "";
342
+ if (attr.startsWith('#[must_use = "')) {
343
+ const m = attr.match(/^#\[must_use = "([^"]*)"\]/);
344
+ if (m)
345
+ return m[1];
346
+ return "";
347
+ }
348
+ if (attr === "#[must_use]")
349
+ return "";
350
+ }
351
+ return null;
352
+ }
353
+ export function renderItemInfoRow(item, ctx) {
354
+ const since = item.stability?.level === "stable" ? item.stability.since : null;
355
+ const sourceHref = renderSourceHref(item, ctx);
356
+ return apiItemInfoRow({ since: since ?? null, sourceHref });
357
+ }
358
+ export function renderSourceHref(item, ctx) {
359
+ if (!item.source)
360
+ return null;
361
+ const { repo, editBranch, editBasePath } = ctx.sourceLinks;
362
+ if (!repo || !editBranch)
363
+ return null;
364
+ const base = repo.replace(/\/+$/, "");
365
+ const basePath = editBasePath ? `${editBasePath.replace(/^\/+|\/+$/g, "")}/` : "";
366
+ return `${base}/blob/${editBranch}/${basePath}${item.source.file}#L${item.source.line_start}-L${item.source.line_end}`;
367
+ }
368
+ // ---------------------------------------------------------------------------
369
+ // Item rendering
370
+ // ---------------------------------------------------------------------------
371
+ export function renderItemHtml(item, ctx) {
372
+ const anchor = itemAnchor(item);
373
+ const callouts = renderStabilityCallouts(item);
374
+ const infoRow = renderItemInfoRow(item, ctx);
375
+ const signatureHtml = renderItemSignature(item, ctx);
376
+ const docs = renderItemDocs(item, ctx);
377
+ const doctests = item.doctests.map((dt, idx) => renderDoctestBlock(item, dt, idx)).join("\n");
378
+ const impls = renderItemImpls(item, ctx);
379
+ return [
380
+ `<section class="rust-item rust-${item.inner.kind} api-item">`,
381
+ infoRow,
382
+ apiSectionAnchor({
383
+ level: 4,
384
+ id: anchor,
385
+ text: itemDisplayName(item) || anchor,
386
+ className: "code-header rust-item-header",
387
+ }),
388
+ signatureHtml,
389
+ callouts,
390
+ docs,
391
+ doctests,
392
+ impls,
393
+ `</section>`,
394
+ ]
395
+ .filter(Boolean)
396
+ .join("\n");
397
+ }
398
+ function renderItemImpls(item, ctx) {
399
+ const traitOwnMembers = renderTraitOwnMembers(item, ctx);
400
+ const implIds = collectImplIds(item);
401
+ if (implIds.length === 0 && !traitOwnMembers)
402
+ return "";
403
+ // Inherent impls render inline (they carry the type's own API). Trait impls
404
+ // can be numerous and are mostly boilerplate, so they collapse behind one
405
+ // styled toggle each, default closed.
406
+ const inherent = [];
407
+ const traitImpls = [];
408
+ const traitRows = [];
409
+ for (const implId of implIds) {
410
+ const impl = ctx.itemsById.get(implId);
411
+ if (!impl || impl.inner.kind !== "impl")
412
+ continue;
413
+ const traitName = impl.inner.trait_path?.display;
414
+ const forName = impl.inner.for_type.display;
415
+ const header = traitName ? `impl ${traitName} for ${forName}` : `impl ${forName}`;
416
+ const implAnchorId = itemAnchor(impl);
417
+ const providedMethods = new Set(impl.inner.provided_trait_methods);
418
+ const methodSections = impl.inner.items
419
+ .map((mid) => ctx.itemsById.get(mid))
420
+ .filter((m) => Boolean(m))
421
+ .map((m) => {
422
+ const isRequired = item.inner.kind === "trait" &&
423
+ m.inner.kind === "function" &&
424
+ !providedMethods.has(m.name ?? "");
425
+ return renderImplMember(m, ctx, isRequired);
426
+ })
427
+ .filter(Boolean)
428
+ .join("\n");
429
+ // No <code> child: `.code-header` gives the mono font, and a bare <code>
430
+ // inside the prose container would render with backtick pseudo-elements.
431
+ const implHeader = `<h3 class="code-header rust-impl-header" id="${escapeAttr(implAnchorId)}">${escapeHtml(header)}</h3>`;
432
+ if (traitName) {
433
+ if (methodSections) {
434
+ // Substantive trait impls collapse behind a chevron.
435
+ traitImpls.push(apiImplToggle({ open: false, summary: implHeader, body: methodSections }));
436
+ }
437
+ else {
438
+ // Marker / auto / blanket traits (Send, Sync, From, …) carry no methods.
439
+ // Render them as a quiet, non-expandable row rather than an empty
440
+ // collapsible that opens onto nothing.
441
+ traitRows.push(`<div class="rust-impl-row">${implHeader}</div>`);
442
+ }
443
+ }
444
+ else if (methodSections) {
445
+ inherent.push(implHeader + methodSections);
446
+ }
447
+ }
448
+ const parts = [];
449
+ if (traitOwnMembers)
450
+ parts.push(traitOwnMembers);
451
+ if (inherent.length > 0) {
452
+ parts.push(apiSectionAnchor({
453
+ level: 3,
454
+ id: `impls-for-${itemAnchor(item)}`,
455
+ text: "Implementations",
456
+ className: "rust-impls-header",
457
+ }));
458
+ parts.push(inherent.join("\n"));
459
+ }
460
+ if (traitImpls.length > 0 || traitRows.length > 0) {
461
+ parts.push(apiSectionAnchor({
462
+ level: 3,
463
+ id: `trait-impls-for-${itemAnchor(item)}`,
464
+ text: "Trait Implementations",
465
+ className: "rust-impls-header",
466
+ }));
467
+ if (traitImpls.length > 0)
468
+ parts.push(traitImpls.join("\n"));
469
+ if (traitRows.length > 0)
470
+ parts.push(`<div class="rust-impl-rows">${traitRows.join("\n")}</div>`);
471
+ }
472
+ if (parts.length === 0)
473
+ return "";
474
+ return parts.join("\n");
475
+ }
476
+ function renderTraitOwnMembers(item, ctx) {
477
+ if (item.inner.kind !== "trait")
478
+ return "";
479
+ const members = item.inner.items
480
+ .map((mid) => ctx.itemsById.get(mid))
481
+ .filter((m) => Boolean(m));
482
+ if (members.length === 0)
483
+ return "";
484
+ const required = [];
485
+ const provided = [];
486
+ const assocTypes = [];
487
+ const assocConsts = [];
488
+ for (const m of members) {
489
+ if (m.inner.kind === "function") {
490
+ // Functions without bodies on a trait are required; with bodies are provided.
491
+ (m.inner.has_body ? provided : required).push(m);
492
+ }
493
+ else if (m.inner.kind === "assoc_type") {
494
+ assocTypes.push(m);
495
+ }
496
+ else if (m.inner.kind === "assoc_const") {
497
+ assocConsts.push(m);
498
+ }
499
+ }
500
+ const parts = [];
501
+ if (assocTypes.length > 0) {
502
+ parts.push(apiSectionAnchor({
503
+ level: 3,
504
+ id: "required-associated-types",
505
+ text: "Required Associated Types",
506
+ }));
507
+ parts.push(assocTypes.map((m) => renderImplMember(m, ctx, false)).join("\n"));
508
+ }
509
+ if (assocConsts.length > 0) {
510
+ parts.push(apiSectionAnchor({
511
+ level: 3,
512
+ id: "required-associated-consts",
513
+ text: "Required Associated Constants",
514
+ }));
515
+ parts.push(assocConsts.map((m) => renderImplMember(m, ctx, false)).join("\n"));
516
+ }
517
+ if (required.length > 0) {
518
+ parts.push(apiSectionAnchor({ level: 3, id: "required-methods", text: "Required Methods" }));
519
+ parts.push(required.map((m) => renderImplMember(m, ctx, true)).join("\n"));
520
+ }
521
+ if (provided.length > 0) {
522
+ parts.push(apiSectionAnchor({ level: 3, id: "provided-methods", text: "Provided Methods" }));
523
+ parts.push(provided.map((m) => renderImplMember(m, ctx, false)).join("\n"));
524
+ }
525
+ return parts.join("\n");
526
+ }
527
+ function implMemberAnchor(member, isRequiredTraitMethod) {
528
+ if (member.inner.kind === "function") {
529
+ const prefix = isRequiredTraitMethod ? "tymethod" : "method";
530
+ return `${prefix}.${member.name ?? ""}`;
531
+ }
532
+ if (member.inner.kind === "assoc_type")
533
+ return `associatedtype.${member.name ?? ""}`;
534
+ if (member.inner.kind === "assoc_const")
535
+ return `associatedconstant.${member.name ?? ""}`;
536
+ return itemAnchor(member);
537
+ }
538
+ function collectImplIds(item) {
539
+ switch (item.inner.kind) {
540
+ case "struct":
541
+ case "enum":
542
+ case "union":
543
+ return item.inner.impls;
544
+ case "trait":
545
+ // Implementors of the trait are rendered separately below. Trait's own
546
+ // members are handled by renderTraitOwnMembers().
547
+ return item.inner.implementations;
548
+ default:
549
+ return [];
550
+ }
551
+ }
552
+ function renderImplMember(member, ctx, isRequiredTraitMethod = false) {
553
+ const anchor = implMemberAnchor(member, isRequiredTraitMethod);
554
+ const sigHtml = renderItemSignature(member, ctx);
555
+ const callouts = renderStabilityCallouts(member);
556
+ const docs = renderItemDocs(member, ctx);
557
+ const doctests = member.doctests.map((dt, idx) => renderDoctestBlock(member, dt, idx)).join("\n");
558
+ const infoRow = renderItemInfoRow(member, ctx);
559
+ const body = [
560
+ infoRow,
561
+ `<h4 class="code-header rust-method-header" id="${escapeAttr(anchor)}">${escapeHtml(member.name ?? "")}</h4>`,
562
+ sigHtml,
563
+ callouts,
564
+ docs,
565
+ doctests,
566
+ ]
567
+ .filter(Boolean)
568
+ .join("\n");
569
+ if (!body)
570
+ return "";
571
+ return `<section class="rust-member api-member">${body}</section>`;
572
+ }
573
+ function renderItemDocs(item, ctx) {
574
+ if (!item.docs_markdown)
575
+ return "";
576
+ const links = item.links ?? {};
577
+ const { html, unresolved } = resolveIntraDocLinks(item.docs_markdown, links, ctx);
578
+ for (const label of unresolved) {
579
+ ctx.diagnostics.push({
580
+ severity: "info",
581
+ code: RUSTDOC_DIAGNOSTIC_CODES.INTRA_DOC_LINK_UNRESOLVED,
582
+ message: `Intra-doc link "${label}" in ${item.path.join("::")} could not be resolved.`,
583
+ crate_name: ctx.crate.name,
584
+ file: item.source?.file ?? null,
585
+ line: item.source?.line_start ?? null,
586
+ });
587
+ }
588
+ return `<div class="docblock rust-doc">${renderMarkdown(html)}</div>`;
589
+ }
590
+ function renderItemSignature(item, ctx) {
591
+ if (item.inner.kind === "function") {
592
+ return renderSignature(item.inner.signature, ctx);
593
+ }
594
+ const fallback = fallbackSignatureText(item);
595
+ if (!fallback)
596
+ return "";
597
+ // A <div>, not <pre><code>: inside the page's prose container a bare <pre>
598
+ // picks up the dark fenced-code-block treatment and a <code> gets backtick
599
+ // pseudo-elements. `.code-header` already supplies the mono font + wrapping.
600
+ return `<div class="code-header rust-signature rust-signature-fallback">${escapeHtml(fallback)}</div>`;
601
+ }
602
+ function fallbackSignatureText(item) {
603
+ const path = item.path.join("::");
604
+ switch (item.inner.kind) {
605
+ case "struct":
606
+ return `pub struct ${path} { /* ... */ }`;
607
+ case "enum":
608
+ return `pub enum ${path} { /* ... */ }`;
609
+ case "trait":
610
+ return `pub trait ${path} { /* ... */ }`;
611
+ case "trait_alias":
612
+ return `pub trait ${path};`;
613
+ case "type_alias":
614
+ return `pub type ${path} = ${item.inner.aliased_type.display};`;
615
+ case "constant":
616
+ return `pub const ${path}: ${item.inner.type_display} = ${item.inner.expr};`;
617
+ case "static":
618
+ return `pub${item.inner.is_mutable ? " mut" : ""} static ${path}: ${item.inner.type_display};`;
619
+ case "macro":
620
+ return `macro_rules! ${item.name ?? ""}`;
621
+ case "proc_macro":
622
+ return `#[${item.inner.macro_kind === "derive" ? "derive" : "proc_macro"}] ${item.name ?? ""}`;
623
+ case "use": {
624
+ const { source, name, is_glob } = item.inner;
625
+ if (is_glob)
626
+ return `pub use ${source}::*;`;
627
+ const lastSegment = source.split("::").filter(Boolean).pop();
628
+ return name && name !== lastSegment ? `pub use ${source} as ${name};` : `pub use ${source};`;
629
+ }
630
+ default:
631
+ return null;
632
+ }
633
+ }
634
+ // ---------------------------------------------------------------------------
635
+ // Doctest blocks
636
+ // ---------------------------------------------------------------------------
637
+ const RUST_EDITION_DEFAULT = "2024";
638
+ export function renderDoctestBlock(parent, dt, idx) {
639
+ const anchor = `doctest-${itemAnchor(parent)}-${idx}`;
640
+ const isRust = dt.lang.toLowerCase() === "rust" || dt.lang === "";
641
+ const lang = isRust ? "rust" : dt.lang || "text";
642
+ const badges = renderDoctestBadges(dt);
643
+ const hasHidden = dt.display_code !== dt.executable_code;
644
+ const edition = pickEdition(dt.fence_attributes);
645
+ const playgroundHref = isRust && !dt.fence_attributes.includes("ignore")
646
+ ? `https://play.rust-lang.org/?code=${encodeURIComponent(dt.executable_code)}&edition=${edition}`
647
+ : null;
648
+ const runButton = playgroundHref
649
+ ? `<a class="test-arrow rust-doctest-run" href="${escapeAttr(playgroundHref)}" target="_blank" rel="noopener">Run</a>`
650
+ : "";
651
+ const hiddenToggle = hasHidden
652
+ ? `<button class="rust-doctest-toggle-hidden" type="button" data-target="#${anchor}-full" data-display="#${anchor}-code">Show hidden lines</button>`
653
+ : "";
654
+ const controls = runButton || hiddenToggle
655
+ ? `<div class="rust-doctest-controls">${runButton}${hiddenToggle}</div>`
656
+ : "";
657
+ return [
658
+ `<div class="rust-doctest api-doctest" id="${anchor}">`,
659
+ badges,
660
+ `<div class="rust-doctest-code" id="${anchor}-code">${renderCodeBlock(dt.display_code, lang)}</div>`,
661
+ hasHidden
662
+ ? `<div class="rust-doctest-code rust-doctest-full" id="${anchor}-full" hidden>${renderCodeBlock(dt.executable_code, lang)}</div>`
663
+ : "",
664
+ controls,
665
+ `</div>`,
666
+ ]
667
+ .filter(Boolean)
668
+ .join("\n");
669
+ }
670
+ export function renderDoctestBadges(dt) {
671
+ if (dt.fence_attributes.length === 0)
672
+ return "";
673
+ const badges = dt.fence_attributes
674
+ .map((a) => `<span class="rust-doctest-badge rust-doctest-badge-${escapeAttr(a.replace(/[^a-zA-Z0-9_-]/g, "_"))}" title="${escapeAttr(badgeTitle(a))}">${escapeHtml(a)}</span>`)
675
+ .join(" ");
676
+ return `<div class="rust-doctest-badges">${badges}</div>`;
677
+ }
678
+ function badgeTitle(attr) {
679
+ switch (attr) {
680
+ case "ignore":
681
+ return "Excluded from testing.";
682
+ case "no_run":
683
+ return "Compiled but not executed.";
684
+ case "should_panic":
685
+ return "Test expected to panic.";
686
+ case "compile_fail":
687
+ return "Test expected to fail to compile.";
688
+ case "edition2018":
689
+ case "edition2021":
690
+ case "edition2024":
691
+ return `Compiled with ${attr.replace("edition", "Rust edition ")}.`;
692
+ case "standalone_crate":
693
+ return "Compiled as a standalone crate.";
694
+ default:
695
+ return attr;
696
+ }
697
+ }
698
+ function pickEdition(attrs) {
699
+ for (const a of attrs) {
700
+ if (a === "edition2018")
701
+ return "2018";
702
+ if (a === "edition2021")
703
+ return "2021";
704
+ if (a === "edition2024")
705
+ return "2024";
706
+ }
707
+ return RUST_EDITION_DEFAULT;
708
+ }
709
+ export function renderModulePage(module, items, ctx) {
710
+ const parts = [];
711
+ if (module.docs_markdown) {
712
+ parts.push(`<div class="docblock rust-doc">${renderMarkdown(module.docs_markdown)}</div>`);
713
+ }
714
+ const grouped = groupItemsForRendering(items);
715
+ const sidebar = [];
716
+ for (const [group, members] of grouped) {
717
+ if (members.length === 0)
718
+ continue;
719
+ parts.push(apiSectionAnchor({ level: 2, id: groupAnchor(group), text: group }));
720
+ for (const m of members) {
721
+ const html = renderItemHtml(m, ctx);
722
+ if (!html)
723
+ continue;
724
+ parts.push(html);
725
+ }
726
+ sidebar.push({
727
+ label: group,
728
+ items: members
729
+ .map((m) => ({ text: itemDisplayName(m), anchor: itemAnchor(m) }))
730
+ .filter((entry) => entry.text)
731
+ .sort((a, b) => a.text.localeCompare(b.text)),
732
+ });
733
+ }
734
+ return {
735
+ html: parts.join("\n"),
736
+ sidebarSections: sidebar,
737
+ };
738
+ }
739
+ const GROUP_ORDER = [
740
+ "Modules",
741
+ "Macros",
742
+ "Structs",
743
+ "Enums",
744
+ "Unions",
745
+ "Traits",
746
+ "Trait Aliases",
747
+ "Type Aliases",
748
+ "Constants",
749
+ "Statics",
750
+ "Functions",
751
+ "Re-exports",
752
+ "Implementations",
753
+ ];
754
+ function groupItemsForRendering(items) {
755
+ const out = new Map();
756
+ for (const g of GROUP_ORDER)
757
+ out.set(g, []);
758
+ for (const item of items) {
759
+ const group = groupForItem(item);
760
+ if (group)
761
+ out.get(group).push(item);
762
+ }
763
+ return out;
764
+ }
765
+ function groupForItem(item) {
766
+ switch (item.inner.kind) {
767
+ case "module":
768
+ return "Modules";
769
+ case "macro":
770
+ case "proc_macro":
771
+ return "Macros";
772
+ case "struct":
773
+ return "Structs";
774
+ case "enum":
775
+ return "Enums";
776
+ case "union":
777
+ return "Unions";
778
+ case "trait":
779
+ return "Traits";
780
+ case "trait_alias":
781
+ return "Trait Aliases";
782
+ case "type_alias":
783
+ return "Type Aliases";
784
+ case "constant":
785
+ return "Constants";
786
+ case "static":
787
+ return "Statics";
788
+ case "function":
789
+ return "Functions";
790
+ case "use":
791
+ return "Re-exports";
792
+ case "impl":
793
+ return "Implementations";
794
+ default:
795
+ return null;
796
+ }
797
+ }
798
+ function groupAnchor(group) {
799
+ return group.toLowerCase().replace(/[^a-z0-9]+/g, "-");
800
+ }
801
+ // Re-export for unit tests.
802
+ export const __internals = {
803
+ encodeImplAnchor,
804
+ encodeGenericSegment,
805
+ itemAnchor,
806
+ renderDoctestBadges,
807
+ pickEdition,
808
+ };