soothfast_docs/
reference.rs1use std::collections::BTreeMap;
8
9use serde_json::Value;
10
11use crate::surface::{ItemGroup, Surface};
12
13pub fn resolve_reference_links(text: &str) -> String {
19 let mut defs = Vec::new();
20 let mut body = String::with_capacity(text.len());
21 for line in text.split_inclusive('\n') {
22 match parse_ref_def(line.trim_end_matches('\n').trim()) {
23 Some(def) => defs.push(def),
24 None => body.push_str(line),
25 }
26 }
27 if defs.is_empty() {
28 return text.to_string();
29 }
30 for (label, target) in &defs {
31 let shorthand = format!("[{label}]");
32 let inline = format!("[{label}]({target})");
33 body = body.replace(&shorthand, &inline);
34 }
35 body
36}
37
38fn parse_ref_def(line: &str) -> Option<(String, String)> {
39 let after_open = line.strip_prefix('[')?;
40 let close = after_open.find(']')?;
41 let label = &after_open[..close];
42 let target = after_open[close + 1..].strip_prefix(':')?.trim();
43 (!label.is_empty() && !target.is_empty()).then(|| (label.to_string(), target.to_string()))
44}
45
46pub fn render(
50 crate_name: &str,
51 surface: &Surface,
52 docs_text: &BTreeMap<String, String>,
53 baseline: &Value,
54) -> String {
55 let groups = surface.grouped();
58 let mut by_module: BTreeMap<&str, Vec<&ItemGroup<'_>>> = BTreeMap::new();
59 for g in &groups {
60 let path = g.representative;
61 let module = path.rsplit_once("::").map(|(m, _)| m).unwrap_or(path);
62 by_module.entry(module).or_default().push(g);
63 }
64
65 let mut out = format!(
66 "# `{crate_name}` API reference\n\n> Generated by `cargo soothfast docs reference`\n> Measurements and verified claims come from the latest baseline; CI gates them.\n"
67 );
68 for (module, groups) in by_module {
69 out.push_str(&format!("\n## `{module}`\n"));
70 for g in groups {
71 let path = g.representative;
72 let info = &surface.items[path];
73 if info.kind == "module" {
74 continue;
75 }
76 let name = path.rsplit("::").next().unwrap_or(path);
77 out.push_str(&format!("\n### `{name}` <sup>{}</sup>\n\n", info.kind));
78 if !info.signature.is_empty() {
79 out.push_str(&format!("```rust ignore\n{}\n```\n", info.signature));
81 }
82 if let Some(docs) = docs_text.get(path)
83 && !docs.trim().is_empty()
84 {
85 out.push_str(&format!("\n{}\n", docs.trim()));
86 }
87 out.push_str(&item_evidence(&g.aliases, baseline));
88 }
89 }
90 out
91}
92
93fn item_evidence(aliases: &[&String], baseline: &Value) -> String {
103 let mut verified = String::new();
104 let mut measured = String::new();
105
106 let Some(items) = baseline["items"].as_object() else {
107 return String::new();
108 };
109 for (id, m) in items {
110 let target = m["covers"].as_str().filter(|c| !c.is_empty()).unwrap_or(id);
111 if !aliases
112 .iter()
113 .any(|p| target == p.as_str() || id == p.as_str())
114 {
115 continue;
116 }
117
118 for a in baseline["assertions"].as_array().into_iter().flatten() {
119 if a["id"].as_str() != Some(id.as_str()) {
120 continue;
121 }
122 let (state, mark) = if a["ok"].as_bool() == Some(true) {
123 ("pass", "✓")
124 } else {
125 ("fail", "✗")
126 };
127 verified.push_str(&format!(
128 "<div class=\"ev-row ev-row-{state}\"><span class=\"ev-mark\">{mark}</span>\
129<span class=\"ev-item\">{}</span><span class=\"ev-detail\">{}</span></div>",
130 escape(a["kind"].as_str().unwrap_or("?")),
131 escape(a["detail"].as_str().unwrap_or("")),
132 ));
133 }
134
135 let mut rows = String::new();
136 for (label, keys) in [
137 ("instructions/iter", ["perfcnt", "instructions"]),
138 ("callgrind Ir/iter", ["callgrind", "ir"]),
139 ("median walltime (ns)", ["walltime", "median_ns"]),
140 ("p99 walltime (ns)", ["walltime", "p99_ns"]),
141 ("allocations/iter", ["alloc", "allocs"]),
142 ("allocated bytes/iter", ["alloc", "bytes"]),
143 ("polls/iter", ["asyncexec", "polls"]),
144 ("wakes/iter", ["asyncexec", "wakes"]),
145 ] {
146 if let Some(v) = m[keys[0]][keys[1]].as_f64() {
147 rows.push_str(&format!(
148 "<div class=\"ev-row\"><span class=\"ev-metric\">{}</span>\
149<span class=\"ev-actual\">{v:.0}</span></div>",
150 escape(label),
151 ));
152 }
153 }
154 if !rows.is_empty() {
155 measured.push_str(&format!(
156 "<div class=\"ev-ledger\"><div class=\"ev-ledger-head\">measured as `{}`</div>{rows}</div>",
157 escape(id),
158 ));
159 }
160 }
161
162 if verified.is_empty() && measured.is_empty() {
163 return String::new();
164 }
165 let mut inner = String::new();
166 if !verified.is_empty() {
167 inner.push_str(&format!(
168 "<div class=\"ev-ledger\"><div class=\"ev-ledger-head\">verified claims</div>{verified}</div>"
169 ));
170 }
171 inner.push_str(&measured);
172 format!("\n<div class=\"evidence-panel\">{inner}</div>\n")
173}
174
175fn escape(s: &str) -> String {
179 let mut out = String::with_capacity(s.len());
180 for c in s.chars() {
181 match c {
182 '&' => out.push_str("&"),
183 '<' => out.push_str("<"),
184 '>' => out.push_str(">"),
185 '"' => out.push_str("""),
186 '\'' => out.push_str("'"),
187 c => out.push(c),
188 }
189 }
190 out
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn inlines_shorthand_reference_links() {
199 let text = "See [`Region::utc_offset_secs`] for details.\n\n[`Region::utc_offset_secs`]: crate::constants::Region::utc_offset_secs\n";
200 let resolved = resolve_reference_links(text);
201 assert_eq!(
202 resolved,
203 "See [`Region::utc_offset_secs`](crate::constants::Region::utc_offset_secs) for details.\n\n"
204 );
205 }
206
207 #[test]
208 fn leaves_text_without_definitions_untouched() {
209 let text = "Plain prose with [an inline link](https://example.com).\n";
210 assert_eq!(resolve_reference_links(text), text);
211 }
212}