1use core::fmt::Write;
2use std::collections::{HashMap, HashSet};
3use std::ops::Range;
4use std::path::{Path, PathBuf};
5
6use ecow::{EcoString, EcoVec};
7use indexmap::IndexSet;
8use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
9use serde::{Deserialize, Serialize};
10use tinymist_analysis::docs::tidy::remove_list_annotations;
11use tinymist_std::path::unix_slash;
12use tinymist_world::package::PackageSpec;
13use typst::diag::{StrResult, eco_format};
14use typst::syntax::package::PackageManifest;
15use typst::syntax::{FileId, Source, Span, SyntaxNode, VirtualRoot};
16use typst_shim::syntax::{RootedPathExt, source_range};
17
18use crate::LocalContext;
19use crate::docs::{DefDocs, PackageDefInfo, SourceQuery, file_id_repr, module_docs};
20use crate::index::{ScipPublicApi, ScipPublicModule};
21use crate::package::{PackageInfo, get_manifest_id, package_entrypoint_id};
22use crate::prelude::Definition;
23use crate::syntax::DefKind;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PackageDoc {
28 meta: PackageMeta,
29 packages: Vec<PackageMeta>,
30 files: Vec<FileMeta>,
31 modules: Vec<(EcoString, crate::docs::DefInfo, ModuleInfo)>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36struct ModuleInfo {
37 prefix: EcoString,
38 name: EcoString,
39 parent_ident: EcoString,
40 aka: EcoVec<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 path: Option<PathBuf>,
43 #[serde(skip)]
44 source: Option<EcoString>,
45}
46
47#[derive(Debug, Clone)]
49pub struct PackageDocTypFile {
50 pub path: PathBuf,
52 pub content: String,
54}
55
56struct BundleModulePath {
57 module_source: PathBuf,
58 module_source_import: String,
59 module_common_import: String,
60 module_output: String,
61 module_func: String,
62 symbol_paths: Vec<BundleSymbolPath>,
63 source_source: Option<PathBuf>,
64 source_source_import: Option<String>,
65 source_common_import: Option<String>,
66 source_output: Option<String>,
67 source_func: String,
68 source_path: Option<String>,
69 source_text: Option<EcoString>,
70}
71
72struct BundleSymbolPath {
73 section: &'static str,
74 symbol_index: usize,
75 source: PathBuf,
76 source_import: String,
77 common_import: String,
78 output: String,
79 func: String,
80}
81
82#[derive(Debug, Clone, Copy)]
83enum BundleSection {
84 Constants,
85 Functions,
86}
87
88impl BundleSection {
89 const ALL: [Self; 2] = [Self::Constants, Self::Functions];
90
91 fn id(self) -> &'static str {
92 match self {
93 Self::Constants => "constants",
94 Self::Functions => "functions",
95 }
96 }
97
98 fn accepts(self, child: &crate::docs::DefInfo) -> bool {
99 match self {
100 Self::Constants => {
101 matches!(child.kind, DefKind::Constant | DefKind::Variable)
102 && !child.name.as_str().starts_with('_')
103 }
104 Self::Functions => {
105 matches!(child.kind, DefKind::Function) && !child.name.as_str().starts_with('_')
106 }
107 }
108 }
109}
110
111#[derive(Default)]
112struct PackageDocSpanIndex {
113 by_file: HashMap<FileId, SourceSpanIndex>,
114}
115
116struct SourceSpanIndex {
117 source: Source,
118 ranges: HashMap<Span, Range<usize>>,
119}
120
121impl PackageDocSpanIndex {
122 fn preload(ctx: &LocalContext, fids: &[FileId]) -> Self {
123 let shared = ctx.shared().clone();
124 let by_file = fids
125 .par_iter()
126 .filter_map(|fid| {
127 let source = shared.source_by_id(*fid).ok()?;
128 Some((*fid, SourceSpanIndex::new(source)))
129 })
130 .collect();
131 Self { by_file }
132 }
133
134 fn source(&mut self, ctx: &LocalContext, fid: FileId) -> Option<Source> {
135 Some(self.entry(ctx, fid)?.source.clone())
136 }
137
138 fn source_range(&mut self, ctx: &LocalContext, span: Span) -> Option<(Source, Range<usize>)> {
139 let fid = span.id()?;
140 let entry = self.entry(ctx, fid)?;
141 let range = entry
142 .ranges
143 .get(&span)
144 .cloned()
145 .or_else(|| source_range(&entry.source, span))?;
146 Some((entry.source.clone(), range))
147 }
148
149 fn entry(&mut self, ctx: &LocalContext, fid: FileId) -> Option<&SourceSpanIndex> {
150 if let std::collections::hash_map::Entry::Vacant(entry) = self.by_file.entry(fid) {
151 let source = ctx.source_by_id(fid).ok()?;
152 entry.insert(SourceSpanIndex::new(source));
153 }
154
155 self.by_file.get(&fid)
156 }
157}
158
159impl SourceSpanIndex {
160 fn new(source: Source) -> Self {
161 let mut ranges = HashMap::new();
162 collect_source_spans(source.root(), 0, &mut ranges);
163 Self { source, ranges }
164 }
165}
166
167fn collect_source_spans(
168 node: &SyntaxNode,
169 offset: usize,
170 ranges: &mut HashMap<Span, Range<usize>>,
171) {
172 let span = node.span();
173 if span.id().is_some() {
174 ranges.entry(span).or_insert(offset..offset + node.len());
175 }
176
177 let mut child_offset = offset;
178 for child in node.children() {
179 collect_source_spans(child, child_offset, ranges);
180 child_offset += child.len();
181 }
182}
183
184pub fn package_docs(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<PackageDoc> {
186 log::info!("generate_md_docs {spec:?}");
187
188 let toml_id = get_manifest_id(spec)?;
189 let manifest = ctx.get_manifest(toml_id)?;
190
191 let for_spec = toml_id
192 .package_compat()
193 .expect("package manifest must be in a package");
194 let entry_point = package_entrypoint_id(toml_id, &manifest.package.entrypoint);
195
196 let depended: Vec<_> = ctx.depended_source_files().into_iter().collect();
197 ctx.preload_expr_stages(depended.iter().copied());
198 let mut span_index = PackageDocSpanIndex::preload(ctx, &depended);
199
200 let PackageDefInfo { root, module_uses } = module_docs(ctx, entry_point)?;
201
202 crate::log_debug_ct!("module_uses: {module_uses:#?}");
203
204 let manifest = ctx.get_manifest(toml_id)?;
205
206 let meta = PackageMeta {
207 namespace: spec.namespace.clone(),
208 name: spec.name.clone(),
209 version: spec.version.to_string(),
210 manifest: Some(manifest),
211 };
212
213 let mut modules_to_generate = vec![(root.name.clone(), root)];
214 let mut generated_modules = HashSet::new();
215 let mut file_ids: IndexSet<FileId> = IndexSet::new();
216
217 let mut primary_aka_cache = HashMap::<FileId, EcoVec<String>>::new();
220 let mut akas = |fid: FileId| {
221 primary_aka_cache
222 .entry(fid)
223 .or_insert_with(|| {
224 module_uses
225 .get(&file_id_repr(fid))
226 .unwrap_or_else(|| panic!("no module uses for {}", file_id_repr(fid)))
227 .clone()
228 })
229 .clone()
230 };
231
232 let mut modules = vec![];
233
234 while !modules_to_generate.is_empty() {
235 for (parent_ident, mut def) in std::mem::take(&mut modules_to_generate) {
236 set_scip_symbol(ctx, &mut span_index, &mut def);
239 let module_val = def.decl.as_ref().unwrap();
240 let fid = module_val.file_id();
241 let aka = fid.map(&mut akas).unwrap_or_default();
242
243 let primary = aka.first().cloned().unwrap_or_default();
246
247 if let Some(fid) = fid {
248 file_ids.insert_full(fid);
249 }
250
251 let module_info = ModuleInfo {
252 prefix: primary.as_str().into(),
253 name: def.name.clone(),
254 parent_ident: parent_ident.clone(),
255 aka,
256 path: fid.map(|fid| fid.vpath().get_without_slash().to_owned().into()),
257 source: fid
258 .and_then(|fid| span_index.source(ctx, fid).map(|src| src.text().into())),
259 };
260
261 for child in def.children.iter_mut() {
262 set_scip_symbol(ctx, &mut span_index, child);
263 let span = child.decl.as_ref().map(|decl| decl.span());
264 let fid_range = span.and_then(|v| {
265 let fid = v.id()?;
266 let allocated = file_ids.insert_full(fid).0;
267 let (src, rng) = span_index.source_range(ctx, v)?;
268 let start = ctx.to_lsp_range(rng.clone(), &src).start;
269 child.source = Some(SourceQuery {
270 file: allocated,
271 position: start,
272 });
273 Some((allocated, rng.start, rng.end))
274 });
275 let child_fid = child.decl.as_ref().and_then(|decl| decl.file_id());
276 let child_fid = child_fid.or_else(|| span.and_then(Span::id)).or(fid);
277 let span = fid_range.or_else(|| {
278 let fid = child_fid?;
279 Some((file_ids.insert_full(fid).0, 0, 0))
280 });
281 child.loc = span;
282
283 if child.parsed_docs.is_some() {
284 child.docs = None;
285 }
286
287 let ident = if !primary.is_empty() {
288 eco_format!("symbol-{}-{primary}.{}", child.kind, child.name)
289 } else {
290 eco_format!("symbol-{}-{}", child.kind, child.name)
291 };
292
293 if child.is_external
294 && let Some(fid) = child_fid
295 {
296 let lnk = if matches!(fid.root(), VirtualRoot::Package(spec) if spec == for_spec)
297 {
298 let sub_aka = akas(fid);
299 let sub_primary = sub_aka.first().cloned().unwrap_or_default();
300 child.external_link = Some(format!(
301 "#symbol-{}-{sub_primary}.{}",
302 child.kind, child.name
303 ));
304 if matches!(child.kind, DefKind::Module) {
305 module_heading_anchor(&sub_primary)
306 } else {
307 format!("#{}-{}-in-{sub_primary}", child.kind, child.name)
308 .replace(".", "")
309 }
310 } else if let VirtualRoot::Package(spec) = fid.root() {
311 let lnk = format!(
312 "https://typst.app/universe/package/{}/{}",
313 spec.name, spec.version
314 );
315 child.external_link = Some(lnk.clone());
316 lnk
317 } else {
318 let lnk: String = "https://typst.app/docs".into();
319 child.external_link = Some(lnk.clone());
320 lnk
321 };
322 child.symbol_link = Some(lnk);
323 }
324
325 let child_children = std::mem::take(&mut child.children);
326 if !child_children.is_empty() {
327 crate::log_debug_ct!("sub_fid: {child_fid:?}");
328 let lnk = match child_fid {
329 Some(fid) => {
330 let aka = akas(fid);
331 let primary = aka.first().cloned().unwrap_or_default();
332
333 if generated_modules.insert(fid) {
334 let mut child = child.clone();
335 child.children = child_children;
336 modules_to_generate.push((ident.clone(), child));
337 }
338
339 module_heading_anchor(&primary)
340 }
341 None => "builtin".to_owned(),
342 };
343
344 child.module_link = Some(lnk);
345 }
346
347 child.id = ident;
348 }
349
350 modules.push((parent_ident, def, module_info));
351 }
352 }
353
354 let mut bundle_links = HashMap::new();
355 for (idx, (parent_ident, _, info)) in modules.iter().enumerate() {
356 let path = module_output_path(idx, info);
357 bundle_links.insert(parent_ident.clone(), path.clone());
358 for aka in &info.aka {
359 bundle_links.insert(eco_format!("symbol-module-{aka}"), path.clone());
360 }
361 }
362 for (idx, (_, def, info)) in modules.iter_mut().enumerate() {
363 apply_bundle_links(def, &bundle_links);
364 apply_symbol_bundle_links(idx, def, info);
365 }
366
367 let mut packages = IndexSet::new();
368
369 let files = file_ids
370 .into_iter()
371 .map(|fid| {
372 let pkg = fid
373 .package_compat()
374 .map(|spec| packages.insert_full(spec.clone()).0);
375
376 FileMeta {
377 package: pkg,
378 path: fid.vpath().get_without_slash().to_owned().into(),
379 uri: ctx.uri_for_id(fid).ok().map(|uri| uri.to_string()),
380 }
381 })
382 .collect();
383
384 let packages = packages
385 .into_iter()
386 .map(|spec| PackageMeta {
387 namespace: spec.namespace.clone(),
388 name: spec.name.clone(),
389 version: spec.version.to_string(),
390 manifest: None,
391 })
392 .collect();
393
394 let doc = PackageDoc {
395 meta,
396 packages,
397 files,
398 modules,
399 };
400
401 Ok(doc)
402}
403
404impl PackageDoc {
405 pub fn scip_public_api(&self) -> ScipPublicApi {
407 ScipPublicApi {
408 modules: self
409 .modules
410 .iter()
411 .map(|(_, def, info)| ScipPublicModule {
412 file_path: info.path.as_ref().map(|path| unix_slash(path)),
413 module_symbol: def.symbol.clone(),
414 public_symbols: def
415 .children
416 .iter()
417 .filter(|child| is_public_api_symbol(child))
418 .filter_map(|child| child.symbol.clone())
419 .collect(),
420 })
421 .collect(),
422 }
423 }
424}
425
426fn is_public_api_symbol(def: &crate::docs::DefInfo) -> bool {
427 !def.name.as_ref().starts_with('_')
428}
429
430fn set_scip_symbol(
431 ctx: &LocalContext,
432 span_index: &mut PackageDocSpanIndex,
433 def: &mut crate::docs::DefInfo,
434) {
435 let Some(decl) = def.decl.as_ref() else {
436 return;
437 };
438 let definition = Definition::new(decl.clone(), None);
439 let disambiguator = span_index
440 .source_range(ctx, decl.span())
441 .map(|(source, range)| {
442 let start = ctx.to_lsp_range(range, &source).start;
443 format!("L{}_C{}", start.line, start.character)
444 })
445 .unwrap_or_else(|| format!("{:x}", decl.span().into_raw()));
446 def.symbol =
447 crate::index::scip::scip_symbol_with_disambiguator(&definition, disambiguator).ok();
448}
449
450pub fn package_docs_typ(doc: &PackageDoc) -> StrResult<String> {
452 let mut out = String::new();
453
454 let _ = writeln!(out, "{}", include_str!("package-doc.typ"));
455
456 let pi = &doc.meta;
457 let _ = writeln!(
458 out,
459 "#package-doc(bytes(read(\"{}-{}-{}.json\")), scip: read(\"{}-{}-{}.scip\", encoding: none))",
460 pi.namespace, pi.name, pi.version, pi.namespace, pi.name, pi.version,
461 );
462
463 Ok(out)
464}
465
466pub fn package_docs_bundle_typ(doc: &PackageDoc) -> StrResult<Vec<PackageDocTypFile>> {
468 let pi = &doc.meta;
469 let base = format!("{}-{}-{}", pi.namespace, pi.name, pi.version);
470
471 let module_paths = doc
472 .modules
473 .iter()
474 .enumerate()
475 .map(|(idx, (_, def, info))| {
476 let module_source = module_source_path(info);
477 let source_source = info.source.as_ref().map(|_| module_source_page_path(info));
478 let symbol_paths = BundleSection::ALL
479 .into_iter()
480 .flat_map(|section| {
481 def.children
482 .iter()
483 .filter(move |child| section.accepts(child))
484 .enumerate()
485 .map(move |(symbol_index, child)| {
486 let source = module_symbol_source_path(
487 idx,
488 info,
489 section.id(),
490 child.name.as_str(),
491 );
492 BundleSymbolPath {
493 section: section.id(),
494 symbol_index,
495 source_import: unix_slash(&source),
496 common_import: relative_import_to_common(&source),
497 output: module_symbol_output_path(
498 idx,
499 info,
500 section.id(),
501 child.name.as_str(),
502 ),
503 func: format!(
504 "render-module-{idx}-{}-{symbol_index}",
505 section.id()
506 ),
507 source,
508 }
509 })
510 })
511 .collect();
512 BundleModulePath {
513 module_source_import: unix_slash(&module_source),
514 module_common_import: relative_import_to_common(&module_source),
515 module_output: module_output_path(idx, info),
516 module_func: format!("render-module-{idx}"),
517 symbol_paths,
518 source_source_import: source_source.as_ref().map(|path| unix_slash(path)),
519 source_common_import: source_source
520 .as_ref()
521 .map(|path| relative_import_to_common(path)),
522 source_output: info
523 .source
524 .as_ref()
525 .map(|_| module_source_output_path(info)),
526 source_func: format!("render-source-{idx}"),
527 source_path: info.path.as_ref().map(|path| unix_slash(path)),
528 source_text: info.source.clone(),
529 source_source,
530 module_source,
531 }
532 })
533 .collect::<Vec<_>>();
534
535 let mut files = vec![];
536 files.push(PackageDocTypFile {
537 path: PathBuf::from("common.typ"),
538 content: include_str!("package-doc.typ").to_owned(),
539 });
540
541 let mut entry = String::new();
542 let _ = writeln!(
543 entry,
544 "#import \"/typ/packages/tinymist-index/lib.typ\": create_index"
545 );
546 for path in &module_paths {
547 let _ = writeln!(
548 entry,
549 "#import {}: {}",
550 typst_string(&path.module_source_import),
551 path.module_func
552 );
553 for symbol in &path.symbol_paths {
554 let _ = writeln!(
555 entry,
556 "#import {}: {}",
557 typst_string(&symbol.source_import),
558 symbol.func
559 );
560 }
561 if let Some(source_import) = &path.source_source_import {
562 let _ = writeln!(
563 entry,
564 "#import {}: {}",
565 typst_string(source_import),
566 path.source_func
567 );
568 }
569 }
570 let _ = writeln!(
571 entry,
572 "#let package-info = json(bytes(read(\"../{base}.json\")))"
573 );
574 let _ = writeln!(
575 entry,
576 "#let package-index = create_index(read(\"../{base}.scip\", encoding: none))"
577 );
578 for path in &module_paths {
579 let _ = writeln!(entry, "#{}(package-info, package-index)", path.module_func);
580 for symbol in &path.symbol_paths {
581 let _ = writeln!(entry, "#{}(package-info, package-index)", symbol.func);
582 }
583 }
584 for path in &module_paths {
585 if path.source_text.is_some() {
586 let _ = writeln!(entry, "#{}(package-info, package-index)", path.source_func);
587 }
588 }
589 files.push(PackageDocTypFile {
590 path: PathBuf::from("index.typ"),
591 content: entry,
592 });
593
594 for (idx, path) in module_paths.into_iter().enumerate() {
595 let mut content = String::new();
596 let _ = writeln!(
597 content,
598 "#import {}: package-module-document",
599 typst_string(&path.module_common_import)
600 );
601 let _ = writeln!(
602 content,
603 "#let {func}(package-info, package-index) = package-module-document(package-info, package-index, module-index: {idx}, path: {})",
604 typst_string(&path.module_output),
605 func = path.module_func,
606 );
607 files.push(PackageDocTypFile {
608 path: path.module_source,
609 content,
610 });
611
612 for symbol in path.symbol_paths {
613 let mut content = String::new();
614 let _ = writeln!(
615 content,
616 "#import {}: package-module-symbol-document",
617 typst_string(&symbol.common_import)
618 );
619 let _ = writeln!(
620 content,
621 "#let {func}(package-info, package-index) = package-module-symbol-document(package-info, package-index, module-index: {idx}, section: {}, symbol-index: {}, path: {})",
622 typst_string(symbol.section),
623 symbol.symbol_index,
624 typst_string(&symbol.output),
625 func = symbol.func,
626 );
627 files.push(PackageDocTypFile {
628 path: symbol.source,
629 content,
630 });
631 }
632
633 if let (
634 Some(source_source),
635 Some(source_common_import),
636 Some(source_output),
637 Some(source_path),
638 Some(source_text),
639 ) = (
640 path.source_source,
641 path.source_common_import,
642 path.source_output,
643 path.source_path,
644 path.source_text,
645 ) {
646 let mut content = String::new();
647 let _ = writeln!(
648 content,
649 "#import {}: package-source-document",
650 typst_string(&source_common_import)
651 );
652 let _ = writeln!(
653 content,
654 "#let {func}(package-info, package-index) = package-source-document(package-info, package-index, module-index: {idx}, path: {}, source-path: {}, source: {})",
655 typst_string(&source_output),
656 typst_string(&source_path),
657 typst_string(source_text.as_str()),
658 func = path.source_func,
659 );
660 files.push(PackageDocTypFile {
661 path: source_source,
662 content,
663 });
664 }
665 }
666
667 Ok(files)
668}
669
670fn module_source_path(info: &ModuleInfo) -> PathBuf {
671 PathBuf::from("modules").join(module_package_path(info))
672}
673
674fn module_source_page_path(info: &ModuleInfo) -> PathBuf {
675 PathBuf::from("sources").join(module_package_path(info))
676}
677
678fn module_symbol_source_path(
679 idx: usize,
680 info: &ModuleInfo,
681 section: &str,
682 symbol: &str,
683) -> PathBuf {
684 let mut path = PathBuf::from("symbols");
685 path.push(module_symbol_path(idx, info, section, symbol, "typ"));
686 path
687}
688
689fn module_package_path(info: &ModuleInfo) -> PathBuf {
690 info.path
691 .clone()
692 .unwrap_or_else(|| PathBuf::from(module_fallback_file_name(info)))
693}
694
695fn module_fallback_file_name(info: &ModuleInfo) -> String {
696 let raw = if !info.parent_ident.is_empty() {
697 info.parent_ident.as_str()
698 } else if !info.prefix.is_empty() {
699 info.prefix.as_str()
700 } else {
701 info.name.as_str()
702 };
703 let mut stem = String::new();
704 let mut prev_dash = false;
705 for ch in raw.chars().flat_map(char::to_lowercase) {
706 if ch.is_ascii_alphanumeric() {
707 stem.push(ch);
708 prev_dash = false;
709 } else if !prev_dash {
710 stem.push('-');
711 prev_dash = true;
712 }
713 }
714
715 let stem = stem.trim_matches('-');
716 if stem.is_empty() {
717 "module.typ".to_owned()
718 } else {
719 format!("{stem}.typ")
720 }
721}
722
723fn module_output_path(idx: usize, info: &ModuleInfo) -> String {
724 if idx == 0 {
725 "index.html".to_owned()
726 } else {
727 let mut output = module_package_path(info);
728 output.set_extension("html");
729 unix_slash(&output)
730 }
731}
732
733fn module_source_output_path(info: &ModuleInfo) -> String {
734 format!("{}.html", unix_slash(&module_package_path(info)))
735}
736
737fn module_symbol_output_path(idx: usize, info: &ModuleInfo, section: &str, symbol: &str) -> String {
738 unix_slash(&module_symbol_path(idx, info, section, symbol, "html"))
739}
740
741fn module_symbol_path(
742 idx: usize,
743 info: &ModuleInfo,
744 section: &str,
745 symbol: &str,
746 extension: &str,
747) -> PathBuf {
748 let file_name = format!("{}.{}", symbol_file_stem(symbol), extension);
749 if idx == 0 {
750 return PathBuf::from(section).join(file_name);
751 }
752
753 let path = module_package_path(info);
754 let stem = path
755 .file_stem()
756 .map(|stem| stem.to_owned())
757 .unwrap_or_else(|| info.name.as_str().into());
758 let mut output = path.parent().map(Path::to_owned).unwrap_or_default();
759 output.push(stem);
760 output.push(section);
761 output.push(file_name);
762 output
763}
764
765fn symbol_file_stem(raw: &str) -> String {
766 let mut stem = String::new();
767 let mut prev_dash = false;
768 for ch in raw.chars() {
769 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
770 stem.push(ch);
771 prev_dash = false;
772 } else if !prev_dash {
773 stem.push('-');
774 prev_dash = true;
775 }
776 }
777
778 let stem = stem.trim_matches('-');
779 if stem.is_empty() {
780 "symbol".to_owned()
781 } else {
782 stem.to_owned()
783 }
784}
785
786fn relative_import_to_common(source: &Path) -> String {
787 let mut path = PathBuf::new();
788 let depth = source
789 .parent()
790 .map(|parent| parent.components().count())
791 .unwrap_or(0);
792 for _ in 0..depth {
793 path.push("..");
794 }
795 path.push("common.typ");
796 unix_slash(&path)
797}
798
799fn typst_string(value: &str) -> String {
800 serde_json::to_string(value).expect("Typst string serialization must succeed")
801}
802
803fn module_heading_anchor(primary: &str) -> String {
804 let mut anchor = String::from("#");
805 let mut prev_dash = false;
806
807 for ch in format!("Module: {primary}").chars() {
808 if ch.is_whitespace() || matches!(ch, ':' | '-') {
809 if !prev_dash {
810 anchor.push('-');
811 prev_dash = true;
812 }
813 } else if matches!(ch, '.' | '(' | ')') {
814 continue;
815 } else {
816 if ch == 'M' {
817 anchor.push('m');
818 } else {
819 anchor.push(ch);
820 }
821 prev_dash = false;
822 }
823 }
824
825 anchor
826}
827
828fn apply_bundle_links(def: &mut crate::docs::DefInfo, links: &HashMap<EcoString, String>) {
829 for child in &mut def.children {
830 if matches!(child.kind, DefKind::Module)
831 && let Some(link) = links.get(&child.id)
832 {
833 child.bundle_link = Some(link.clone());
834 }
835 }
836}
837
838fn apply_symbol_bundle_links(idx: usize, def: &mut crate::docs::DefInfo, info: &ModuleInfo) {
839 for child in &mut def.children {
840 for section in BundleSection::ALL {
841 if section.accepts(child) {
842 child.bundle_link = Some(module_symbol_output_path(
843 idx,
844 info,
845 section.id(),
846 child.name.as_str(),
847 ));
848 break;
849 }
850 }
851 }
852}
853
854pub fn package_docs_md(doc: &PackageDoc) -> StrResult<String> {
856 let mut out = String::new();
857
858 let title = doc.meta.spec().to_string();
859
860 writeln!(out, "# {title}").unwrap();
861 out.push('\n');
862 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();
863 out.push('\n');
864 out.push('\n');
865
866 let package_meta = jbase64(&doc.meta);
867 let _ = writeln!(out, "<!-- begin:package {package_meta} -->");
868
869 let mut errors = vec![];
870 for (parent_ident, def, module_info) in &doc.modules {
871 let primary = &module_info.prefix;
873 if !module_info.prefix.is_empty() {
874 let _ = writeln!(out, "---\n## Module: {primary}");
875 }
876
877 crate::log_debug_ct!("module: {primary} -- {parent_ident}");
878 let module_info = jbase64(&module_info);
879 let _ = writeln!(out, "<!-- begin:module {primary} {module_info} -->");
880
881 for child in &def.children {
882 let convert_err = None::<EcoString>;
883
884 let ident = if !primary.is_empty() {
885 eco_format!("symbol-{}-{primary}.{}", child.kind, child.name)
886 } else {
887 eco_format!("symbol-{}-{}", child.kind, child.name)
888 };
889 let _ = writeln!(out, "### {}: {} in {primary}", child.kind, child.name);
890
891 if let Some(lnk) = &child.symbol_link {
892 let _ = writeln!(out, "[Symbol Docs]({lnk})\n");
893 }
894
895 let head = jbase64(&child);
896 let _ = writeln!(out, "<!-- begin:symbol {ident} {head} -->");
897
898 if let Some(DefDocs::Function(sig)) = &child.parsed_docs {
899 let _ = writeln!(out, "<!-- begin:sig -->");
900 let _ = writeln!(out, "```typc");
901 let _ = write!(out, "let {}", child.name);
902 let _ = sig.print(&mut out);
903 let _ = writeln!(out, ";");
904 let _ = writeln!(out, "```");
905 let _ = writeln!(out, "<!-- end:sig -->");
906 }
907
908 let mut printed_docs = false;
909 match (&child.parsed_docs, convert_err) {
910 (_, Some(err)) => {
911 let err = format!("failed to convert docs in {title}: {err}").replace(
912 "-->", "—>", );
914 let _ = writeln!(out, "<!-- convert-error: {err} -->");
915 errors.push(err);
916 }
917 (Some(docs), _) if !child.is_external => {
918 let _ = writeln!(out, "{}", remove_list_annotations(docs.docs()));
919 printed_docs = true;
920 if let DefDocs::Function(docs) = docs {
921 for param in docs
922 .pos
923 .iter()
924 .chain(docs.named.values())
925 .chain(docs.rest.as_ref())
926 {
927 let _ = writeln!(out, "<!-- begin:param {} -->", param.name);
928 let ty = match ¶m.cano_type {
929 Some((short, _, _)) => short,
930 None => "unknown",
931 };
932 let _ = writeln!(
933 out,
934 "#### {} ({ty:?})\n<!-- begin:param-doc {} -->\n{}\n<!-- end:param-doc {} -->",
935 param.name, param.name, param.docs, param.name
936 );
937 let _ = writeln!(out, "<!-- end:param -->");
938 }
939 }
940 }
941 (_, None) => {}
942 }
943
944 if !printed_docs {
945 let plain_docs = child.docs.as_deref();
946 let plain_docs = plain_docs.or(child.oneliner.as_deref());
947
948 if let Some(docs) = plain_docs {
949 let contains_code = docs.contains("```");
950 if contains_code {
951 let _ = writeln!(out, "`````typ");
952 }
953 let _ = writeln!(out, "{docs}");
954 if contains_code {
955 let _ = writeln!(out, "`````");
956 }
957 }
958 }
959
960 if let Some(lnk) = &child.module_link {
961 match lnk.as_str() {
962 "builtin" => {
963 let _ = writeln!(out, "A Builtin Module");
964 }
965 lnk => {
966 let _ = writeln!(out, "[Module Docs]({lnk})\n");
967 }
968 }
969 }
970
971 let _ = writeln!(out, "<!-- end:symbol {ident} -->");
972 }
973
974 let _ = writeln!(out, "<!-- end:module {primary} -->");
975 }
976
977 let res = ConvertResult { errors };
978 let err = jbase64(&res);
979 let _ = writeln!(out, "<!-- begin:errors {err} -->");
980 let _ = writeln!(out, "## Errors");
981 for errs in res.errors {
982 let _ = writeln!(out, "- {errs}");
983 }
984 let _ = writeln!(out, "<!-- end:errors -->");
985
986 let meta = PackageMetaEnd {
987 packages: doc.packages.clone(),
988 files: doc.files.clone(),
989 };
990 let package_meta = jbase64(&meta);
991 let _ = writeln!(out, "<!-- end:package {package_meta} -->");
992
993 Ok(out)
994}
995
996fn jbase64<T: Serialize>(s: &T) -> String {
997 use base64::Engine;
998 let content = serde_json::to_string(s).unwrap();
999 base64::engine::general_purpose::STANDARD.encode(content)
1000}
1001
1002#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub struct PackageMeta {
1005 pub namespace: EcoString,
1007 pub name: EcoString,
1009 pub version: String,
1011 pub manifest: Option<PackageManifest>,
1013}
1014
1015impl PackageMeta {
1016 pub fn spec(&self) -> PackageSpec {
1018 PackageSpec {
1019 namespace: self.namespace.clone(),
1020 name: self.name.clone(),
1021 version: self.version.parse().expect("Invalid version format"),
1022 }
1023 }
1024}
1025
1026#[derive(Debug, Serialize, Deserialize)]
1028pub struct PackageMetaEnd {
1029 packages: Vec<PackageMeta>,
1030 files: Vec<FileMeta>,
1031}
1032
1033#[derive(Debug, Clone, Serialize, Deserialize)]
1035pub struct FileMeta {
1036 package: Option<usize>,
1037 path: PathBuf,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1039 uri: Option<String>,
1040}
1041
1042#[derive(Serialize, Deserialize)]
1043struct ConvertResult {
1044 errors: Vec<String>,
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049 use std::sync::{Arc, Mutex};
1050
1051 use tinymist_world::package::{PackageRegistry, PackageSpec, registry::PREVIEW_NS};
1052
1053 use super::{
1054 PackageInfo, package_docs, package_docs_bundle_typ, package_docs_md, package_docs_typ,
1055 };
1056 use crate::analysis::Analysis;
1057 use crate::tests::*;
1058
1059 fn test(pkg: PackageSpec) {
1060 run_with_sources("", |verse: &mut LspUniverse, path| {
1061 let pkg_root = verse.registry.resolve(&pkg).unwrap();
1062 let pi = PackageInfo {
1063 path: pkg_root.as_ref().to_owned(),
1064 namespace: pkg.namespace,
1065 name: pkg.name,
1066 version: pkg.version.to_string(),
1067 };
1068 let public_api = Arc::new(Mutex::new(None));
1069 let public_api_docs = public_api.clone();
1070 run_with_ctx(verse, path, &|a, _p| {
1071 let docs = package_docs(a, &pi).unwrap();
1072 *public_api_docs.lock().unwrap() = Some(docs.scip_public_api());
1073 let dest = format!(
1074 "../../target/{}-{}-{}.json",
1075 pi.namespace, pi.name, pi.version
1076 );
1077 std::fs::write(dest, serde_json::to_string_pretty(&docs).unwrap()).unwrap();
1078 let typ = package_docs_typ(&docs).unwrap();
1079 let dest = format!(
1080 "../../target/{}-{}-{}.typ",
1081 pi.namespace, pi.name, pi.version
1082 );
1083 std::fs::write(dest, typ).unwrap();
1084 let md = package_docs_md(&docs).unwrap();
1085 let dest = format!(
1086 "../../target/{}-{}-{}.md",
1087 pi.namespace, pi.name, pi.version
1088 );
1089 std::fs::write(dest, md).unwrap();
1090 let bundle = package_docs_bundle_typ(&docs).unwrap();
1091 let dest = std::path::PathBuf::from(format!(
1092 "../../target/{}-{}-{}.bundle",
1093 pi.namespace, pi.name, pi.version
1094 ));
1095 let _ = std::fs::remove_dir_all(&dest);
1096 for file in bundle {
1097 let path = dest.join(file.path);
1098 if let Some(parent) = path.parent() {
1099 std::fs::create_dir_all(parent).unwrap();
1100 }
1101 std::fs::write(path, file.content).unwrap();
1102 }
1103 });
1104 let analysis = Arc::new(Analysis::default());
1105 let public_api_index = public_api.clone();
1106 let scip = analysis
1107 .query_snapshot(verse.computation(), None)
1108 .run_within_package(&pi, |a| {
1109 let knowledge = crate::index::knowledge(a).unwrap();
1110 let public_api = public_api_index.lock().unwrap();
1111 let public_api = public_api
1112 .as_ref()
1113 .expect("package docs must build public API before SCIP");
1114 knowledge
1115 .bind(a.shared())
1116 .to_scip_bytes_with_public_api(public_api)
1117 })
1118 .unwrap();
1119 let dest = format!(
1120 "../../target/{}-{}-{}.scip",
1121 pi.namespace, pi.name, pi.version
1122 );
1123 std::fs::write(dest, scip).unwrap();
1124 })
1125 }
1126
1127 #[test]
1128 fn tidy() {
1129 test(PackageSpec {
1130 namespace: PREVIEW_NS.into(),
1131 name: "tidy".into(),
1132 version: "0.3.0".parse().unwrap(),
1133 });
1134 }
1135
1136 #[test]
1137 fn touying() {
1138 test(PackageSpec {
1139 namespace: PREVIEW_NS.into(),
1140 name: "touying".into(),
1141 version: "0.6.0".parse().unwrap(),
1142 });
1143 }
1144
1145 #[test]
1146 fn fletcher() {
1147 test(PackageSpec {
1148 namespace: PREVIEW_NS.into(),
1149 name: "fletcher".into(),
1150 version: "0.5.8".parse().unwrap(),
1151 });
1152 }
1153
1154 #[test]
1155 fn cetz() {
1156 test(PackageSpec {
1157 namespace: PREVIEW_NS.into(),
1158 name: "cetz".into(),
1159 version: "0.2.2".parse().unwrap(),
1160 });
1161 }
1162}