1use core::fmt::Write;
2use std::collections::{HashMap, HashSet};
3use std::path::PathBuf;
4
5use ecow::{EcoString, EcoVec};
6use indexmap::IndexSet;
7use serde::{Deserialize, Serialize};
8use tinymist_analysis::docs::tidy::remove_list_annotations;
9use tinymist_world::package::PackageSpec;
10use typst::diag::{StrResult, eco_format};
11use typst::syntax::package::PackageManifest;
12use typst::syntax::{FileId, Span};
13
14use crate::LocalContext;
15use crate::docs::{DefDocs, PackageDefInfo, file_id_repr, module_docs};
16use crate::package::{PackageInfo, get_manifest_id};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PackageDoc {
21 meta: PackageMeta,
22 packages: Vec<PackageMeta>,
23 files: Vec<FileMeta>,
24 modules: Vec<(EcoString, crate::docs::DefInfo, ModuleInfo)>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29struct ModuleInfo {
30 prefix: EcoString,
31 name: EcoString,
32 loc: Option<usize>,
33 parent_ident: EcoString,
34 aka: EcoVec<String>,
35}
36
37pub fn package_docs(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<PackageDoc> {
39 log::info!("generate_md_docs {spec:?}");
40
41 let toml_id = get_manifest_id(spec)?;
42 let manifest = ctx.get_manifest(toml_id)?;
43
44 let for_spec = toml_id.package().unwrap();
45 let entry_point = toml_id.join(&manifest.package.entrypoint);
46
47 ctx.preload_package(entry_point);
48
49 let PackageDefInfo { root, module_uses } = module_docs(ctx, entry_point)?;
50
51 crate::log_debug_ct!("module_uses: {module_uses:#?}");
52
53 let manifest = ctx.get_manifest(toml_id)?;
54
55 let meta = PackageMeta {
56 namespace: spec.namespace.clone(),
57 name: spec.name.clone(),
58 version: spec.version.to_string(),
59 manifest: Some(manifest),
60 };
61
62 let mut modules_to_generate = vec![(root.name.clone(), root)];
63 let mut generated_modules = HashSet::new();
64 let mut file_ids: IndexSet<FileId> = IndexSet::new();
65
66 let mut primary_aka_cache = HashMap::<FileId, EcoVec<String>>::new();
69 let mut akas = |fid: FileId| {
70 primary_aka_cache
71 .entry(fid)
72 .or_insert_with(|| {
73 module_uses
74 .get(&file_id_repr(fid))
75 .unwrap_or_else(|| panic!("no module uses for {}", file_id_repr(fid)))
76 .clone()
77 })
78 .clone()
79 };
80
81 let mut modules = vec![];
82
83 while !modules_to_generate.is_empty() {
84 for (parent_ident, mut def) in std::mem::take(&mut modules_to_generate) {
85 let module_val = def.decl.as_ref().unwrap();
88 let fid = module_val.file_id();
89 let aka = fid.map(&mut akas).unwrap_or_default();
90
91 let primary = aka.first().cloned().unwrap_or_default();
94
95 let persist_fid = fid.map(|fid| file_ids.insert_full(fid).0);
96
97 let module_info = ModuleInfo {
98 prefix: primary.as_str().into(),
99 name: def.name.clone(),
100 loc: persist_fid,
101 parent_ident: parent_ident.clone(),
102 aka,
103 };
104
105 for child in def.children.iter_mut() {
106 let span = child.decl.as_ref().map(|decl| decl.span());
107 let fid_range = span.and_then(|v| {
108 v.id().and_then(|fid| {
109 let allocated = file_ids.insert_full(fid).0;
110 let src = ctx.source_by_id(fid).ok()?;
111 let rng = src.range(v)?;
112 Some((allocated, rng.start, rng.end))
113 })
114 });
115 let child_fid = child.decl.as_ref().and_then(|decl| decl.file_id());
116 let child_fid = child_fid.or_else(|| span.and_then(Span::id)).or(fid);
117 let span = fid_range.or_else(|| {
118 let fid = child_fid?;
119 Some((file_ids.insert_full(fid).0, 0, 0))
120 });
121 child.loc = span;
122
123 if child.parsed_docs.is_some() {
124 child.docs = None;
125 }
126
127 let ident = if !primary.is_empty() {
128 eco_format!("symbol-{}-{primary}.{}", child.kind, child.name)
129 } else {
130 eco_format!("symbol-{}-{}", child.kind, child.name)
131 };
132
133 if child.is_external
134 && let Some(fid) = child_fid
135 {
136 let lnk = if fid.package() == Some(for_spec) {
137 let sub_aka = akas(fid);
138 let sub_primary = sub_aka.first().cloned().unwrap_or_default();
139 child.external_link = Some(format!(
140 "#symbol-{}-{sub_primary}.{}",
141 child.kind, child.name
142 ));
143 format!("#{}-{}-in-{sub_primary}", child.kind, child.name).replace(".", "")
144 } else if let Some(spec) = fid.package() {
145 let lnk = format!(
146 "https://typst.app/universe/package/{}/{}",
147 spec.name, spec.version
148 );
149 child.external_link = Some(lnk.clone());
150 lnk
151 } else {
152 let lnk: String = "https://typst.app/docs".into();
153 child.external_link = Some(lnk.clone());
154 lnk
155 };
156 child.symbol_link = Some(lnk);
157 }
158
159 let child_children = std::mem::take(&mut child.children);
160 if !child_children.is_empty() {
161 crate::log_debug_ct!("sub_fid: {child_fid:?}");
162 let lnk = match child_fid {
163 Some(fid) => {
164 let aka = akas(fid);
165 let primary = aka.first().cloned().unwrap_or_default();
166
167 if generated_modules.insert(fid) {
168 let mut child = child.clone();
169 child.children = child_children;
170 modules_to_generate.push((ident.clone(), child));
171 }
172
173 let link = format!("module-{primary}").replace(".", "");
174 format!("#{link}")
175 }
176 None => "builtin".to_owned(),
177 };
178
179 child.module_link = Some(lnk);
180 }
181
182 child.id = ident;
183 }
184
185 modules.push((parent_ident, def, module_info));
186 }
187 }
188
189 let mut packages = IndexSet::new();
190
191 let files = file_ids
192 .into_iter()
193 .map(|fid| {
194 let pkg = fid
195 .package()
196 .map(|spec| packages.insert_full(spec.clone()).0);
197
198 FileMeta {
199 package: pkg,
200 path: fid.vpath().as_rootless_path().to_owned(),
201 }
202 })
203 .collect();
204
205 let packages = packages
206 .into_iter()
207 .map(|spec| PackageMeta {
208 namespace: spec.namespace.clone(),
209 name: spec.name.clone(),
210 version: spec.version.to_string(),
211 manifest: None,
212 })
213 .collect();
214
215 let doc = PackageDoc {
216 meta,
217 packages,
218 files,
219 modules,
220 };
221
222 Ok(doc)
223}
224
225pub fn package_docs_typ(doc: &PackageDoc) -> StrResult<String> {
227 let mut out = String::new();
228
229 let _ = writeln!(out, "{}", include_str!("package-doc.typ"));
230
231 let pi = &doc.meta;
232 let _ = writeln!(
233 out,
234 "#package-doc(bytes(read(\"{}-{}-{}.json\")))",
235 pi.namespace, pi.name, pi.version,
236 );
237
238 Ok(out)
239}
240
241pub fn package_docs_md(doc: &PackageDoc) -> StrResult<String> {
243 let mut out = String::new();
244
245 let title = doc.meta.spec().to_string();
246
247 writeln!(out, "# {title}").unwrap();
248 out.push('\n');
249 writeln!(out, "This documentation is generated locally. Please submit issues to [tinymist](https://github.com/Myriad-Dreamin/tinymist/issues) if you see **incorrect** information in it.").unwrap();
250 out.push('\n');
251 out.push('\n');
252
253 let package_meta = jbase64(&doc.meta);
254 let _ = writeln!(out, "<!-- begin:package {package_meta} -->");
255
256 let mut errors = vec![];
257 for (parent_ident, def, module_info) in &doc.modules {
258 let primary = &module_info.prefix;
260 if !module_info.prefix.is_empty() {
261 let _ = writeln!(out, "---\n## Module: {primary}");
262 }
263
264 crate::log_debug_ct!("module: {primary} -- {parent_ident}");
265 let module_info = jbase64(&module_info);
266 let _ = writeln!(out, "<!-- begin:module {primary} {module_info} -->");
267
268 for child in &def.children {
269 let convert_err = None::<EcoString>;
270
271 let ident = if !primary.is_empty() {
272 eco_format!("symbol-{}-{primary}.{}", child.kind, child.name)
273 } else {
274 eco_format!("symbol-{}-{}", child.kind, child.name)
275 };
276 let _ = writeln!(out, "### {}: {} in {primary}", child.kind, child.name);
277
278 if let Some(lnk) = &child.symbol_link {
279 let _ = writeln!(out, "[Symbol Docs]({lnk})\n");
280 }
281
282 let head = jbase64(&child);
283 let _ = writeln!(out, "<!-- begin:symbol {ident} {head} -->");
284
285 if let Some(DefDocs::Function(sig)) = &child.parsed_docs {
286 let _ = writeln!(out, "<!-- begin:sig -->");
287 let _ = writeln!(out, "```typc");
288 let _ = write!(out, "let {}", child.name);
289 let _ = sig.print(&mut out);
290 let _ = writeln!(out, ";");
291 let _ = writeln!(out, "```");
292 let _ = writeln!(out, "<!-- end:sig -->");
293 }
294
295 let mut printed_docs = false;
296 match (&child.parsed_docs, convert_err) {
297 (_, Some(err)) => {
298 let err = format!("failed to convert docs in {title}: {err}").replace(
299 "-->", "—>", );
301 let _ = writeln!(out, "<!-- convert-error: {err} -->");
302 errors.push(err);
303 }
304 (Some(docs), _) if !child.is_external => {
305 let _ = writeln!(out, "{}", remove_list_annotations(docs.docs()));
306 printed_docs = true;
307 if let DefDocs::Function(docs) = docs {
308 for param in docs
309 .pos
310 .iter()
311 .chain(docs.named.values())
312 .chain(docs.rest.as_ref())
313 {
314 let _ = writeln!(out, "<!-- begin:param {} -->", param.name);
315 let ty = match ¶m.cano_type {
316 Some((short, _, _)) => short,
317 None => "unknown",
318 };
319 let _ = writeln!(
320 out,
321 "#### {} ({ty:?})\n<!-- begin:param-doc {} -->\n{}\n<!-- end:param-doc {} -->",
322 param.name, param.name, param.docs, param.name
323 );
324 let _ = writeln!(out, "<!-- end:param -->");
325 }
326 }
327 }
328 (_, None) => {}
329 }
330
331 if !printed_docs {
332 let plain_docs = child.docs.as_deref();
333 let plain_docs = plain_docs.or(child.oneliner.as_deref());
334
335 if let Some(docs) = plain_docs {
336 let contains_code = docs.contains("```");
337 if contains_code {
338 let _ = writeln!(out, "`````typ");
339 }
340 let _ = writeln!(out, "{docs}");
341 if contains_code {
342 let _ = writeln!(out, "`````");
343 }
344 }
345 }
346
347 if let Some(lnk) = &child.module_link {
348 match lnk.as_str() {
349 "builtin" => {
350 let _ = writeln!(out, "A Builtin Module");
351 }
352 lnk => {
353 let _ = writeln!(out, "[Module Docs]({lnk})\n");
354 }
355 }
356 }
357
358 let _ = writeln!(out, "<!-- end:symbol {ident} -->");
359 }
360
361 let _ = writeln!(out, "<!-- end:module {primary} -->");
362 }
363
364 let res = ConvertResult { errors };
365 let err = jbase64(&res);
366 let _ = writeln!(out, "<!-- begin:errors {err} -->");
367 let _ = writeln!(out, "## Errors");
368 for errs in res.errors {
369 let _ = writeln!(out, "- {errs}");
370 }
371 let _ = writeln!(out, "<!-- end:errors -->");
372
373 let meta = PackageMetaEnd {
374 packages: doc.packages.clone(),
375 files: doc.files.clone(),
376 };
377 let package_meta = jbase64(&meta);
378 let _ = writeln!(out, "<!-- end:package {package_meta} -->");
379
380 Ok(out)
381}
382
383fn jbase64<T: Serialize>(s: &T) -> String {
384 use base64::Engine;
385 let content = serde_json::to_string(s).unwrap();
386 base64::engine::general_purpose::STANDARD.encode(content)
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct PackageMeta {
392 pub namespace: EcoString,
394 pub name: EcoString,
396 pub version: String,
398 pub manifest: Option<PackageManifest>,
400}
401
402impl PackageMeta {
403 pub fn spec(&self) -> PackageSpec {
405 PackageSpec {
406 namespace: self.namespace.clone(),
407 name: self.name.clone(),
408 version: self.version.parse().expect("Invalid version format"),
409 }
410 }
411}
412
413#[derive(Debug, Serialize, Deserialize)]
415pub struct PackageMetaEnd {
416 packages: Vec<PackageMeta>,
417 files: Vec<FileMeta>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct FileMeta {
423 package: Option<usize>,
424 path: PathBuf,
425}
426
427#[derive(Serialize, Deserialize)]
428struct ConvertResult {
429 errors: Vec<String>,
430}
431
432#[cfg(test)]
433mod tests {
434 use tinymist_world::package::{PackageRegistry, PackageSpec};
435
436 use super::{PackageInfo, package_docs, package_docs_md, package_docs_typ};
437 use crate::tests::*;
438
439 fn test(pkg: PackageSpec) {
440 run_with_sources("", |verse: &mut LspUniverse, path| {
441 let pkg_root = verse.registry.resolve(&pkg).unwrap();
442 let pi = PackageInfo {
443 path: pkg_root.as_ref().to_owned(),
444 namespace: pkg.namespace,
445 name: pkg.name,
446 version: pkg.version.to_string(),
447 };
448 run_with_ctx(verse, path, &|a, _p| {
449 let docs = package_docs(a, &pi).unwrap();
450 let dest = format!(
451 "../../target/{}-{}-{}.json",
452 pi.namespace, pi.name, pi.version
453 );
454 std::fs::write(dest, serde_json::to_string_pretty(&docs).unwrap()).unwrap();
455 let typ = package_docs_typ(&docs).unwrap();
456 let dest = format!(
457 "../../target/{}-{}-{}.typ",
458 pi.namespace, pi.name, pi.version
459 );
460 std::fs::write(dest, typ).unwrap();
461 let md = package_docs_md(&docs).unwrap();
462 let dest = format!(
463 "../../target/{}-{}-{}.md",
464 pi.namespace, pi.name, pi.version
465 );
466 std::fs::write(dest, md).unwrap();
467 })
468 })
469 }
470
471 #[test]
472 fn tidy() {
473 test(PackageSpec {
474 namespace: "preview".into(),
475 name: "tidy".into(),
476 version: "0.3.0".parse().unwrap(),
477 });
478 }
479
480 #[test]
481 fn touying() {
482 test(PackageSpec {
483 namespace: "preview".into(),
484 name: "touying".into(),
485 version: "0.6.0".parse().unwrap(),
486 });
487 }
488
489 #[test]
490 fn fletcher() {
491 test(PackageSpec {
492 namespace: "preview".into(),
493 name: "fletcher".into(),
494 version: "0.5.8".parse().unwrap(),
495 });
496 }
497
498 #[test]
499 fn cetz() {
500 test(PackageSpec {
501 namespace: "preview".into(),
502 name: "cetz".into(),
503 version: "0.2.2".parse().unwrap(),
504 });
505 }
506}