sourcey 3.5.10 → 3.6.1

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 (100) hide show
  1. package/README.md +24 -97
  2. package/dist/adapters/index.d.ts +12 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/index.js +130 -0
  5. package/dist/adapters/mkdocs.d.ts +5 -0
  6. package/dist/adapters/mkdocs.d.ts.map +1 -0
  7. package/dist/adapters/mkdocs.js +367 -0
  8. package/dist/adapters/shared.d.ts +12 -0
  9. package/dist/adapters/shared.d.ts.map +1 -0
  10. package/dist/adapters/shared.js +134 -0
  11. package/dist/adapters/types.d.ts +74 -0
  12. package/dist/adapters/types.d.ts.map +1 -0
  13. package/dist/adapters/types.js +1 -0
  14. package/dist/cli.js +28 -34
  15. package/dist/client/search.js +213 -82
  16. package/dist/client/tabs.js +23 -0
  17. package/dist/components/layout/Head.js +17 -4
  18. package/dist/components/layout/TableOfContents.d.ts.map +1 -1
  19. package/dist/components/layout/TableOfContents.js +4 -2
  20. package/dist/components/openapi/Introduction.d.ts.map +1 -1
  21. package/dist/components/openapi/Introduction.js +16 -1
  22. package/dist/components/openapi/Security.d.ts.map +1 -1
  23. package/dist/components/openapi/Security.js +18 -2
  24. package/dist/config.d.ts +101 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +95 -143
  27. package/dist/core/api-rendering.d.ts +47 -0
  28. package/dist/core/api-rendering.d.ts.map +1 -0
  29. package/dist/core/api-rendering.js +76 -0
  30. package/dist/core/doxygen-loader.d.ts.map +1 -1
  31. package/dist/core/doxygen-loader.js +36 -17
  32. package/dist/core/godoc-loader.d.ts.map +1 -1
  33. package/dist/core/godoc-loader.js +17 -6
  34. package/dist/core/markdown-loader.d.ts +14 -0
  35. package/dist/core/markdown-loader.d.ts.map +1 -1
  36. package/dist/core/markdown-loader.js +70 -26
  37. package/dist/core/mcp-normalizer.d.ts.map +1 -1
  38. package/dist/core/mcp-normalizer.js +48 -34
  39. package/dist/core/navigation.d.ts.map +1 -1
  40. package/dist/core/navigation.js +8 -7
  41. package/dist/core/parser.d.ts.map +1 -1
  42. package/dist/core/parser.js +75 -4
  43. package/dist/core/rustdoc-introspector.d.ts +29 -0
  44. package/dist/core/rustdoc-introspector.d.ts.map +1 -0
  45. package/dist/core/rustdoc-introspector.js +236 -0
  46. package/dist/core/rustdoc-loader.d.ts +25 -0
  47. package/dist/core/rustdoc-loader.d.ts.map +1 -0
  48. package/dist/core/rustdoc-loader.js +469 -0
  49. package/dist/core/rustdoc-render.d.ts +76 -0
  50. package/dist/core/rustdoc-render.d.ts.map +1 -0
  51. package/dist/core/rustdoc-render.js +785 -0
  52. package/dist/core/rustdoc-types.d.ts +314 -0
  53. package/dist/core/rustdoc-types.d.ts.map +1 -0
  54. package/dist/core/rustdoc-types.js +18 -0
  55. package/dist/core/search-indexer.d.ts +10 -0
  56. package/dist/core/search-indexer.d.ts.map +1 -1
  57. package/dist/core/search-indexer.js +6 -1
  58. package/dist/core/sourcey-rustdoc/Cargo.lock +705 -0
  59. package/dist/core/sourcey-rustdoc/Cargo.toml +38 -0
  60. package/dist/core/sourcey-rustdoc/README.md +59 -0
  61. package/dist/core/sourcey-rustdoc/src/diagnostics.rs +42 -0
  62. package/dist/core/sourcey-rustdoc/src/doctest.rs +268 -0
  63. package/dist/core/sourcey-rustdoc/src/extract.rs +492 -0
  64. package/dist/core/sourcey-rustdoc/src/lib.rs +58 -0
  65. package/dist/core/sourcey-rustdoc/src/links.rs +126 -0
  66. package/dist/core/sourcey-rustdoc/src/main.rs +256 -0
  67. package/dist/core/sourcey-rustdoc/src/signature.rs +415 -0
  68. package/dist/core/sourcey-rustdoc/src/spec.rs +390 -0
  69. package/dist/core/sourcey-rustdoc/tests/format_version.rs +45 -0
  70. package/dist/dev-server.d.ts.map +1 -1
  71. package/dist/dev-server.js +109 -33
  72. package/dist/index.d.ts +4 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +26 -14
  75. package/dist/init.d.ts.map +1 -1
  76. package/dist/init.js +21 -16
  77. package/dist/renderer/changelog-feed.d.ts.map +1 -1
  78. package/dist/renderer/changelog-feed.js +27 -19
  79. package/dist/renderer/html-builder.d.ts.map +1 -1
  80. package/dist/renderer/html-builder.js +4 -3
  81. package/dist/renderer/llms.js +57 -2
  82. package/dist/site-assembly.d.ts +5 -0
  83. package/dist/site-assembly.d.ts.map +1 -1
  84. package/dist/site-assembly.js +161 -25
  85. package/dist/themes/default/sourcey.css +300 -0
  86. package/dist/utils/code-samples.d.ts +1 -0
  87. package/dist/utils/code-samples.d.ts.map +1 -1
  88. package/dist/utils/code-samples.js +30 -22
  89. package/dist/utils/html.d.ts +10 -0
  90. package/dist/utils/html.d.ts.map +1 -0
  91. package/dist/utils/html.js +152 -0
  92. package/dist/utils/http.d.ts +0 -2
  93. package/dist/utils/http.d.ts.map +1 -1
  94. package/dist/utils/http.js +19 -34
  95. package/dist/utils/markdown.d.ts.map +1 -1
  96. package/dist/utils/markdown.js +115 -39
  97. package/dist/vite-plugin.d.ts +2 -0
  98. package/dist/vite-plugin.d.ts.map +1 -1
  99. package/dist/vite-plugin.js +71 -1
  100. package/package.json +11 -5
@@ -0,0 +1,492 @@
1
+ use std::collections::{BTreeMap, HashMap, HashSet};
2
+
3
+ use rustdoc_types::{Crate, Id, Item as RdItem, ItemEnum, StructKind, VariantKind, Visibility as RdVisibility};
4
+
5
+ use crate::diagnostics::{self, codes};
6
+ use crate::doctest::extract_doctests;
7
+ use crate::links::{format_id, LinkContext};
8
+ use crate::signature::{lower_generics, lower_signature, render_type};
9
+ use crate::spec::{
10
+ AssocConstItem, AssocTypeItem, ConstantItem, CrateSpec, Deprecation, Diagnostic, EnumItem,
11
+ FunctionItem, ImplItem, Item, ItemId, ItemInner, LinkTarget, MacroItem, ModuleSpec,
12
+ ProcMacroItem, ProcMacroKindKind, SourceLocation, Stability, StabilityLevel, StaticItem,
13
+ StructFieldItem, StructItem, StructKindKind, TraitAliasItem, TraitItem, TypeAliasItem,
14
+ TypePath, UnionItem, UseItem, VariantItem, VariantKindKind, Visibility,
15
+ };
16
+
17
+ pub struct ExtractOptions {
18
+ pub include_private: bool,
19
+ pub include_hidden: bool,
20
+ pub crate_name_hint: String,
21
+ }
22
+
23
+ pub struct ExtractResult {
24
+ pub crate_spec: CrateSpec,
25
+ pub diagnostics: Vec<Diagnostic>,
26
+ }
27
+
28
+ pub fn extract_crate(krate: &Crate, opts: &ExtractOptions) -> ExtractResult {
29
+ let links = LinkContext::new(krate);
30
+ let mut diagnostics: Vec<Diagnostic> = Vec::new();
31
+ let mut items: BTreeMap<String, Item> = BTreeMap::new();
32
+ let mut modules: Vec<ModuleSpec> = Vec::new();
33
+ let mut emitted: HashSet<ItemId> = HashSet::new();
34
+
35
+ for (id, rd_item) in &krate.index {
36
+ if !should_emit(rd_item, opts) {
37
+ continue;
38
+ }
39
+ let item_id = ItemId(format_id(id));
40
+ if !emitted.insert(item_id.clone()) {
41
+ continue;
42
+ }
43
+ match &rd_item.inner {
44
+ ItemEnum::Module(m) => {
45
+ modules.push(lower_module(id, rd_item, m, krate));
46
+ // Also surface modules as items so per-module sidebar entries,
47
+ // search categories, and parent-module pages can link to
48
+ // their children.
49
+ if let Some(item) = lower_item(id, rd_item, krate, &links, &mut diagnostics) {
50
+ items.insert(item.id.0.clone(), item);
51
+ }
52
+ }
53
+ _ => {
54
+ if let Some(item) = lower_item(id, rd_item, krate, &links, &mut diagnostics) {
55
+ items.insert(item.id.0.clone(), item);
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ let root_module_id = ItemId(format_id(&krate.root));
62
+ // Prefer the user-supplied crate name (matches Cargo.toml `[package].name`,
63
+ // hyphens preserved). Fall back to rustdoc's lib target name (underscored)
64
+ // when no hint is set.
65
+ let crate_name = if opts.crate_name_hint.is_empty() {
66
+ krate
67
+ .index
68
+ .get(&krate.root)
69
+ .and_then(|i| i.name.clone())
70
+ .unwrap_or_default()
71
+ } else {
72
+ opts.crate_name_hint.clone()
73
+ };
74
+ let crate_version = krate.crate_version.clone();
75
+
76
+ let crate_spec = CrateSpec {
77
+ name: crate_name,
78
+ version: crate_version,
79
+ root_module_id,
80
+ modules,
81
+ items,
82
+ external_crates: links.external_crates(),
83
+ diagnostics: diagnostics.clone(),
84
+ };
85
+ if diagnostics.iter().any(|d| d.code == codes::INTRA_DOC_LINK_UNRESOLVED) {
86
+ // Carry intra-doc-link diagnostics into the crate-level diagnostics
87
+ // (already in `crate_spec.diagnostics`); we return the same set in the
88
+ // top-level summary for caller-level visibility.
89
+ }
90
+ ExtractResult {
91
+ crate_spec,
92
+ diagnostics,
93
+ }
94
+ }
95
+
96
+ fn should_emit(item: &RdItem, opts: &ExtractOptions) -> bool {
97
+ if !opts.include_hidden {
98
+ for attr in &item.attrs {
99
+ if let rustdoc_types::Attribute::Other(s) = attr {
100
+ if s.contains("doc(hidden)") {
101
+ return false;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ if opts.include_private {
107
+ return true;
108
+ }
109
+ // Without --document-private-items, rustdoc's `index` only contains
110
+ // items it considers documented (public, or implicitly public via a
111
+ // public parent). Most items inside impl blocks, trait assoc items,
112
+ // enum variants, and struct fields carry `Default` visibility but are
113
+ // still public because their parent is. Trust rustdoc's filtering and
114
+ // only drop explicit `Crate`/`Restricted` visibility.
115
+ match &item.visibility {
116
+ RdVisibility::Public | RdVisibility::Default => true,
117
+ RdVisibility::Crate | RdVisibility::Restricted { .. } => false,
118
+ }
119
+ }
120
+
121
+ fn lower_module(id: &Id, item: &RdItem, m: &rustdoc_types::Module, krate: &Crate) -> ModuleSpec {
122
+ let path = path_for_id(id, krate);
123
+ let item_ids: Vec<ItemId> = m.items.iter().map(|i| ItemId(format_id(i))).collect();
124
+ let sub_module_paths: Vec<Vec<String>> = m
125
+ .items
126
+ .iter()
127
+ .filter_map(|i| krate.index.get(i))
128
+ .filter(|child| matches!(child.inner, ItemEnum::Module(_)))
129
+ .map(|child| {
130
+ let mut p = path.clone();
131
+ if let Some(name) = &child.name {
132
+ p.push(name.clone());
133
+ }
134
+ p
135
+ })
136
+ .collect();
137
+ ModuleSpec {
138
+ id: ItemId(format_id(id)),
139
+ path,
140
+ docs_markdown: item.docs.clone(),
141
+ doc_aliases: extract_doc_aliases(item),
142
+ item_ids,
143
+ sub_module_paths,
144
+ source: lower_source(item),
145
+ }
146
+ }
147
+
148
+ fn lower_item(
149
+ id: &Id,
150
+ rd_item: &RdItem,
151
+ krate: &Crate,
152
+ links: &LinkContext<'_>,
153
+ diagnostics: &mut Vec<Diagnostic>,
154
+ ) -> Option<Item> {
155
+ let item_id = ItemId(format_id(id));
156
+ let inner = lower_inner(rd_item, krate, links, diagnostics)?;
157
+ let docs_markdown = rd_item.docs.clone();
158
+ let doctests = docs_markdown
159
+ .as_deref()
160
+ .map(extract_doctests)
161
+ .unwrap_or_default();
162
+ let item = Item {
163
+ id: item_id,
164
+ name: rd_item.name.clone(),
165
+ path: path_for_id(id, krate),
166
+ visibility: lower_visibility(&rd_item.visibility),
167
+ source: lower_source(rd_item),
168
+ docs_markdown,
169
+ doc_aliases: extract_doc_aliases(rd_item),
170
+ deprecation: rd_item.deprecation.as_ref().map(|d| Deprecation {
171
+ since: d.since.clone(),
172
+ note: d.note.clone(),
173
+ }),
174
+ stability: extract_stability(rd_item),
175
+ feature_gates: extract_feature_gates(rd_item),
176
+ attrs_structured: extract_structured_attrs(rd_item),
177
+ links: rd_item
178
+ .links
179
+ .iter()
180
+ .map(|(label, id)| {
181
+ let target = links.resolve_id(id).map(|resolved| {
182
+ if resolved.external {
183
+ LinkTarget::External {
184
+ crate_name: links
185
+ .external_crate_index
186
+ .get(&resolved.crate_id)
187
+ .map(|ec| ec.name.clone())
188
+ .unwrap_or_default(),
189
+ path: resolved.path,
190
+ html_root_url: resolved.html_root_url,
191
+ }
192
+ } else {
193
+ LinkTarget::Internal { id: resolved.id }
194
+ }
195
+ }).unwrap_or(LinkTarget::Internal { id: ItemId(format_id(id)) });
196
+ (label.clone(), target)
197
+ })
198
+ .collect(),
199
+ inner,
200
+ doctests,
201
+ };
202
+ Some(item)
203
+ }
204
+
205
+ fn lower_inner(
206
+ rd_item: &RdItem,
207
+ krate: &Crate,
208
+ links: &LinkContext<'_>,
209
+ diagnostics: &mut Vec<Diagnostic>,
210
+ ) -> Option<ItemInner> {
211
+ let inner = match &rd_item.inner {
212
+ ItemEnum::Function(f) => ItemInner::Function(FunctionItem {
213
+ signature: lower_signature(
214
+ &f.sig,
215
+ rd_item.name.as_deref().unwrap_or(""),
216
+ f.header.is_const,
217
+ f.header.is_async,
218
+ f.header.is_unsafe,
219
+ links,
220
+ ),
221
+ generics: lower_generics(&f.generics),
222
+ is_const: f.header.is_const,
223
+ is_async: f.header.is_async,
224
+ is_unsafe: f.header.is_unsafe,
225
+ has_body: f.has_body,
226
+ }),
227
+ ItemEnum::Struct(s) => ItemInner::Struct(StructItem {
228
+ struct_kind: match &s.kind {
229
+ StructKind::Plain { .. } => StructKindKind::Plain,
230
+ StructKind::Tuple(_) => StructKindKind::Tuple,
231
+ StructKind::Unit => StructKindKind::Unit,
232
+ },
233
+ generics: lower_generics(&s.generics),
234
+ fields: extract_struct_fields(&s.kind),
235
+ has_stripped_fields: matches!(
236
+ &s.kind,
237
+ StructKind::Plain {
238
+ has_stripped_fields: true,
239
+ ..
240
+ }
241
+ ),
242
+ impls: s.impls.iter().map(|i| ItemId(format_id(i))).collect(),
243
+ }),
244
+ ItemEnum::Enum(e) => ItemInner::Enum(EnumItem {
245
+ generics: lower_generics(&e.generics),
246
+ variants: e.variants.iter().map(|i| ItemId(format_id(i))).collect(),
247
+ has_stripped_variants: e.has_stripped_variants,
248
+ impls: e.impls.iter().map(|i| ItemId(format_id(i))).collect(),
249
+ }),
250
+ ItemEnum::Variant(v) => ItemInner::Variant(VariantItem {
251
+ variant_kind: match &v.kind {
252
+ VariantKind::Plain => VariantKindKind::Plain,
253
+ VariantKind::Tuple(_) => VariantKindKind::Tuple,
254
+ VariantKind::Struct { .. } => VariantKindKind::Struct,
255
+ },
256
+ discriminant: v.discriminant.as_ref().map(|d| d.expr.clone()),
257
+ }),
258
+ ItemEnum::Union(u) => ItemInner::Union(UnionItem {
259
+ generics: lower_generics(&u.generics),
260
+ fields: u.fields.iter().map(|i| ItemId(format_id(i))).collect(),
261
+ has_stripped_fields: u.has_stripped_fields,
262
+ impls: u.impls.iter().map(|i| ItemId(format_id(i))).collect(),
263
+ }),
264
+ ItemEnum::Trait(t) => ItemInner::Trait(TraitItem {
265
+ is_auto: t.is_auto,
266
+ is_unsafe: t.is_unsafe,
267
+ is_dyn_compatible: t.is_dyn_compatible,
268
+ generics: lower_generics(&t.generics),
269
+ bounds: t.bounds.iter().map(crate::signature::render_bound).collect(),
270
+ items: t.items.iter().map(|i| ItemId(format_id(i))).collect(),
271
+ implementations: t
272
+ .implementations
273
+ .iter()
274
+ .map(|i| ItemId(format_id(i)))
275
+ .collect(),
276
+ }),
277
+ ItemEnum::TraitAlias(a) => ItemInner::TraitAlias(TraitAliasItem {
278
+ generics: lower_generics(&a.generics),
279
+ bounds: a.params.iter().map(crate::signature::render_bound).collect(),
280
+ }),
281
+ ItemEnum::Impl(i) => ItemInner::Impl(ImplItem {
282
+ generics: lower_generics(&i.generics),
283
+ trait_path: i.trait_.as_ref().map(|p| type_path_from_resolved(p, links)),
284
+ for_type: type_path_for_type(&i.for_, links),
285
+ items: i.items.iter().map(|x| ItemId(format_id(x))).collect(),
286
+ is_negative: i.is_negative,
287
+ is_synthetic: i.is_synthetic,
288
+ is_blanket: i.blanket_impl.is_some(),
289
+ provided_trait_methods: i.provided_trait_methods.clone(),
290
+ }),
291
+ ItemEnum::TypeAlias(a) => ItemInner::TypeAlias(TypeAliasItem {
292
+ aliased_type: type_path_for_type(&a.type_, links),
293
+ generics: lower_generics(&a.generics),
294
+ }),
295
+ ItemEnum::Constant { type_, const_ } => ItemInner::Constant(ConstantItem {
296
+ type_display: render_type(type_),
297
+ expr: const_.expr.clone(),
298
+ value: const_.value.clone(),
299
+ is_literal: const_.is_literal,
300
+ }),
301
+ ItemEnum::Static(s) => ItemInner::Static(StaticItem {
302
+ type_display: render_type(&s.type_),
303
+ expr: s.expr.clone(),
304
+ is_mutable: s.is_mutable,
305
+ is_unsafe: s.is_unsafe,
306
+ }),
307
+ ItemEnum::Macro(source) => ItemInner::Macro(MacroItem {
308
+ source: source.clone(),
309
+ }),
310
+ ItemEnum::ProcMacro(pm) => ItemInner::ProcMacro(ProcMacroItem {
311
+ macro_kind: match pm.kind {
312
+ rustdoc_types::MacroKind::Bang => ProcMacroKindKind::Bang,
313
+ rustdoc_types::MacroKind::Attr => ProcMacroKindKind::Attr,
314
+ rustdoc_types::MacroKind::Derive => ProcMacroKindKind::Derive,
315
+ },
316
+ helpers: pm.helpers.clone(),
317
+ }),
318
+ ItemEnum::AssocType {
319
+ generics,
320
+ bounds,
321
+ type_,
322
+ } => ItemInner::AssocType(AssocTypeItem {
323
+ generics: lower_generics(generics),
324
+ bounds: bounds.iter().map(crate::signature::render_bound).collect(),
325
+ default_display: type_.as_ref().map(render_type),
326
+ }),
327
+ ItemEnum::AssocConst { type_, value } => ItemInner::AssocConst(AssocConstItem {
328
+ type_display: render_type(type_),
329
+ default_display: value.clone(),
330
+ }),
331
+ ItemEnum::Use(u) => ItemInner::Use(UseItem {
332
+ source: u.source.clone(),
333
+ name: u.name.clone(),
334
+ target_id: u.id.as_ref().map(|i| ItemId(format_id(i))),
335
+ is_glob: u.is_glob,
336
+ }),
337
+ ItemEnum::StructField(t) => ItemInner::StructField(StructFieldItem {
338
+ type_display: render_type(t),
339
+ }),
340
+ ItemEnum::Module(_) => ItemInner::Module,
341
+ ItemEnum::Primitive(_) => ItemInner::Primitive,
342
+ ItemEnum::ExternType => ItemInner::ExternType,
343
+ ItemEnum::ExternCrate { .. } => return None,
344
+ };
345
+ let _ = (krate, links, diagnostics);
346
+ Some(inner)
347
+ }
348
+
349
+ fn lower_visibility(v: &RdVisibility) -> Visibility {
350
+ match v {
351
+ RdVisibility::Public => Visibility::Public,
352
+ RdVisibility::Crate => Visibility::Crate,
353
+ RdVisibility::Restricted { path, .. } => Visibility::Restricted { path: path.clone() },
354
+ RdVisibility::Default => Visibility::Default,
355
+ }
356
+ }
357
+
358
+ fn lower_source(item: &RdItem) -> Option<SourceLocation> {
359
+ item.span.as_ref().map(|s| SourceLocation {
360
+ file: s.filename.to_string_lossy().into_owned(),
361
+ line_start: s.begin.0 as u32,
362
+ line_end: s.end.0 as u32,
363
+ })
364
+ }
365
+
366
+ fn extract_doc_aliases(item: &RdItem) -> Vec<String> {
367
+ let mut out: Vec<String> = Vec::new();
368
+ for attr in &item.attrs {
369
+ if let rustdoc_types::Attribute::Other(s) = attr {
370
+ if let Some(rest) = s.strip_prefix("#[doc(alias = \"") {
371
+ if let Some(end) = rest.find("\")]") {
372
+ out.push(rest[..end].to_string());
373
+ }
374
+ } else if let Some(rest) = s.strip_prefix("#[doc(alias(") {
375
+ if let Some(end) = rest.find("))]") {
376
+ for raw in rest[..end].split(',') {
377
+ let trimmed = raw.trim().trim_matches('"');
378
+ if !trimmed.is_empty() {
379
+ out.push(trimmed.to_string());
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ out
387
+ }
388
+
389
+ fn extract_stability(item: &RdItem) -> Option<Stability> {
390
+ for attr in &item.attrs {
391
+ if let rustdoc_types::Attribute::Other(s) = attr {
392
+ if s.starts_with("#[stable(") {
393
+ return Some(Stability {
394
+ level: StabilityLevel::Stable,
395
+ since: extract_attr_string(s, "since"),
396
+ feature: extract_attr_string(s, "feature"),
397
+ issue: None,
398
+ });
399
+ }
400
+ if s.starts_with("#[unstable(") {
401
+ return Some(Stability {
402
+ level: StabilityLevel::Unstable,
403
+ since: None,
404
+ feature: extract_attr_string(s, "feature"),
405
+ issue: extract_attr_string(s, "issue")
406
+ .and_then(|i| i.trim_matches(|c: char| !c.is_ascii_digit()).parse().ok()),
407
+ });
408
+ }
409
+ }
410
+ }
411
+ None
412
+ }
413
+
414
+ fn extract_attr_string(s: &str, key: &str) -> Option<String> {
415
+ let needle = format!("{} = \"", key);
416
+ let start = s.find(&needle)? + needle.len();
417
+ let rest = &s[start..];
418
+ let end = rest.find('"')?;
419
+ Some(rest[..end].to_string())
420
+ }
421
+
422
+ fn extract_feature_gates(_item: &RdItem) -> Vec<String> {
423
+ // Phase 1: feature gates are not directly carried on items in rustdoc JSON
424
+ // until the renderer reconstructs them from cfg attrs. Placeholder so the
425
+ // field exists in the schema and Phase 3 rendering can fill it in.
426
+ Vec::new()
427
+ }
428
+
429
+ fn extract_structured_attrs(item: &RdItem) -> Vec<String> {
430
+ item.attrs
431
+ .iter()
432
+ .map(|attr| match attr {
433
+ rustdoc_types::Attribute::Other(s) => s.clone(),
434
+ other => format!("{:?}", other),
435
+ })
436
+ .collect()
437
+ }
438
+
439
+ fn extract_struct_fields(kind: &StructKind) -> Vec<ItemId> {
440
+ match kind {
441
+ StructKind::Plain { fields, .. } => fields.iter().map(|i| ItemId(format_id(i))).collect(),
442
+ StructKind::Tuple(ids) => ids
443
+ .iter()
444
+ .filter_map(|maybe| maybe.as_ref().map(|i| ItemId(format_id(i))))
445
+ .collect(),
446
+ StructKind::Unit => Vec::new(),
447
+ }
448
+ }
449
+
450
+ fn type_path_from_resolved(p: &rustdoc_types::Path, links: &LinkContext<'_>) -> TypePath {
451
+ let resolved = links.resolve_id(&p.id);
452
+ TypePath {
453
+ crate_id: resolved.as_ref().map(|r| r.crate_id).unwrap_or(0),
454
+ path: resolved.map(|r| r.path).unwrap_or_else(|| vec![p.path.clone()]),
455
+ display: p.path.clone(),
456
+ external: false,
457
+ html_root_url: None,
458
+ }
459
+ }
460
+
461
+ fn type_path_for_type(ty: &rustdoc_types::Type, links: &LinkContext<'_>) -> TypePath {
462
+ if let rustdoc_types::Type::ResolvedPath(p) = ty {
463
+ return type_path_from_resolved(p, links);
464
+ }
465
+ TypePath {
466
+ crate_id: 0,
467
+ path: vec![render_type(ty)],
468
+ display: render_type(ty),
469
+ external: false,
470
+ html_root_url: None,
471
+ }
472
+ }
473
+
474
+ fn path_for_id(id: &Id, krate: &Crate) -> Vec<String> {
475
+ if let Some(summary) = krate.paths.get(id) {
476
+ return summary.path.clone();
477
+ }
478
+ if let Some(item) = krate.index.get(id) {
479
+ if let Some(name) = &item.name {
480
+ return vec![name.clone()];
481
+ }
482
+ }
483
+ Vec::new()
484
+ }
485
+
486
+ #[allow(dead_code)]
487
+ fn unused_diagnostics_marker() -> Diagnostic {
488
+ diagnostics::warning(codes::INTRA_DOC_LINK_UNRESOLVED, "marker")
489
+ }
490
+
491
+ // Keep imports referenced even though the immediate use is in helper modules.
492
+ const _: fn(&HashMap<u32, ()>) = |_| {};
@@ -0,0 +1,58 @@
1
+ //! Convert nightly rustdoc JSON into a stable `RustdocSpec` snapshot.
2
+ //!
3
+ //! `sourcey-rustdoc` powers the `rustdoc()` source adapter in [sourcey], the
4
+ //! static documentation generator. It is published as a standalone crate so
5
+ //! any tool that wants a stable, versioned representation of a Rust crate's
6
+ //! API surface can consume the same schema sourcey uses internally.
7
+ //!
8
+ //! [sourcey]: https://sourcey.com
9
+ //!
10
+ //! # Pipeline
11
+ //!
12
+ //! The high-level flow is:
13
+ //!
14
+ //! 1. Run nightly rustdoc against a Cargo manifest and capture its JSON
15
+ //! output (use the [`rustdoc-json`](https://crates.io/crates/rustdoc-json)
16
+ //! crate for this step).
17
+ //! 2. Deserialize into [`rustdoc_types::Crate`] using a matching
18
+ //! `rustdoc-types` version.
19
+ //! 3. Walk the crate through [`extract::extract_crate`] to produce a
20
+ //! [`RustdocSpec`] tree: modules, items, signature tokens, intra-doc
21
+ //! link table, extracted doctests, and resolved external-crate
22
+ //! references.
23
+ //! 4. Serialize the spec to JSON for downstream consumers (sourcey's
24
+ //! `rustdoc()` adapter, alternative renderers, semver-checkers, etc.).
25
+ //!
26
+ //! The bundled `sourcey-rustdoc` binary performs all four steps for the
27
+ //! common case. Library callers can drive each step directly.
28
+ //!
29
+ //! # Schema stability
30
+ //!
31
+ //! [`SPEC_VERSION`] is the schema's major version. Consumers should
32
+ //! check `RustdocSpec.version` on load and bail with a clear remedy when
33
+ //! it does not match.
34
+ //!
35
+ //! # Examples
36
+ //!
37
+ //! ```no_run
38
+ //! use sourcey_rustdoc::{
39
+ //! extract::{extract_crate, ExtractOptions},
40
+ //! RustdocSpec, SPEC_VERSION,
41
+ //! };
42
+ //!
43
+ //! fn read_snapshot(path: &str) -> serde_json::Result<RustdocSpec> {
44
+ //! let bytes = std::fs::read(path).expect("snapshot");
45
+ //! let spec: RustdocSpec = serde_json::from_slice(&bytes)?;
46
+ //! assert_eq!(spec.version, SPEC_VERSION, "schema version mismatch");
47
+ //! Ok(spec)
48
+ //! }
49
+ //! ```
50
+
51
+ pub mod diagnostics;
52
+ pub mod doctest;
53
+ pub mod extract;
54
+ pub mod links;
55
+ pub mod signature;
56
+ pub mod spec;
57
+
58
+ pub use spec::{RustdocSpec, SOURCEY_RUSTDOC_VERSION, SPEC_VERSION};
@@ -0,0 +1,126 @@
1
+ use rustdoc_types::{Crate, Id, ItemKind};
2
+ use std::collections::HashMap;
3
+
4
+ use crate::spec::{ExternalCrateRef, ItemId};
5
+
6
+ /// Build a lookup table for intra-doc link resolution.
7
+ /// Given an Item.links entry (label -> Id), we want the full path so the
8
+ /// renderer can produce a sourcey URL or fall back to an external one.
9
+ pub struct LinkContext<'a> {
10
+ pub krate: &'a Crate,
11
+ pub external_crate_index: HashMap<u32, ExternalCrateRef>,
12
+ }
13
+
14
+ impl<'a> LinkContext<'a> {
15
+ pub fn new(krate: &'a Crate) -> Self {
16
+ let external_crate_index = krate
17
+ .external_crates
18
+ .iter()
19
+ .map(|(crate_id, ec)| {
20
+ (
21
+ *crate_id,
22
+ ExternalCrateRef {
23
+ crate_id: *crate_id,
24
+ name: ec.name.clone(),
25
+ html_root_url: ec.html_root_url.clone(),
26
+ },
27
+ )
28
+ })
29
+ .collect();
30
+ Self {
31
+ krate,
32
+ external_crate_index,
33
+ }
34
+ }
35
+
36
+ pub fn external_crates(&self) -> Vec<ExternalCrateRef> {
37
+ let mut out: Vec<_> = self.external_crate_index.values().cloned().collect();
38
+ out.sort_by(|a, b| a.crate_id.cmp(&b.crate_id));
39
+ out
40
+ }
41
+
42
+ pub fn resolve_id(&self, id: &Id) -> Option<ResolvedLink> {
43
+ if let Some(summary) = self.krate.paths.get(id) {
44
+ let crate_id = summary.crate_id;
45
+ let path = summary.path.clone();
46
+ let kind = summary.kind.clone();
47
+ let external = crate_id != 0;
48
+ let html_root_url = if external {
49
+ self.external_crate_index
50
+ .get(&crate_id)
51
+ .and_then(|ec| ec.html_root_url.clone())
52
+ } else {
53
+ None
54
+ };
55
+ return Some(ResolvedLink {
56
+ id: ItemId(format_id(id)),
57
+ crate_id,
58
+ path,
59
+ kind,
60
+ external,
61
+ html_root_url,
62
+ });
63
+ }
64
+ if let Some(item) = self.krate.index.get(id) {
65
+ // Sub-items (struct fields, variants, methods) frequently lack a
66
+ // paths entry. Best effort: surface what we know.
67
+ return Some(ResolvedLink {
68
+ id: ItemId(format_id(id)),
69
+ crate_id: item.crate_id,
70
+ path: item.name.clone().map(|n| vec![n]).unwrap_or_default(),
71
+ kind: classify_inner_kind(item),
72
+ external: item.crate_id != 0,
73
+ html_root_url: self
74
+ .external_crate_index
75
+ .get(&item.crate_id)
76
+ .and_then(|ec| ec.html_root_url.clone()),
77
+ });
78
+ }
79
+ None
80
+ }
81
+ }
82
+
83
+ #[derive(Debug, Clone)]
84
+ pub struct ResolvedLink {
85
+ pub id: ItemId,
86
+ pub crate_id: u32,
87
+ pub path: Vec<String>,
88
+ pub kind: ItemKind,
89
+ pub external: bool,
90
+ pub html_root_url: Option<String>,
91
+ }
92
+
93
+ pub fn format_id(id: &Id) -> String {
94
+ format!("{}", id.0)
95
+ }
96
+
97
+ fn classify_inner_kind(item: &rustdoc_types::Item) -> ItemKind {
98
+ use rustdoc_types::ItemEnum;
99
+ match &item.inner {
100
+ ItemEnum::Module(_) => ItemKind::Module,
101
+ ItemEnum::Struct(_) => ItemKind::Struct,
102
+ ItemEnum::StructField(_) => ItemKind::StructField,
103
+ ItemEnum::Union(_) => ItemKind::Union,
104
+ ItemEnum::Enum(_) => ItemKind::Enum,
105
+ ItemEnum::Variant(_) => ItemKind::Variant,
106
+ ItemEnum::Function(_) => ItemKind::Function,
107
+ ItemEnum::Trait(_) => ItemKind::Trait,
108
+ ItemEnum::TraitAlias(_) => ItemKind::TraitAlias,
109
+ ItemEnum::Impl(_) => ItemKind::Impl,
110
+ ItemEnum::TypeAlias(_) => ItemKind::TypeAlias,
111
+ ItemEnum::Constant { .. } => ItemKind::Constant,
112
+ ItemEnum::Static(_) => ItemKind::Static,
113
+ ItemEnum::Macro(_) => ItemKind::Macro,
114
+ ItemEnum::ProcMacro(pm) => match pm.kind {
115
+ rustdoc_types::MacroKind::Bang => ItemKind::Macro,
116
+ rustdoc_types::MacroKind::Attr => ItemKind::ProcAttribute,
117
+ rustdoc_types::MacroKind::Derive => ItemKind::ProcDerive,
118
+ },
119
+ ItemEnum::AssocConst { .. } => ItemKind::AssocConst,
120
+ ItemEnum::AssocType { .. } => ItemKind::AssocType,
121
+ ItemEnum::ExternCrate { .. } => ItemKind::ExternCrate,
122
+ ItemEnum::Use(_) => ItemKind::Use,
123
+ ItemEnum::Primitive(_) => ItemKind::Primitive,
124
+ ItemEnum::ExternType => ItemKind::ExternType,
125
+ }
126
+ }