1use include_dir::{include_dir, Dir};
2use itertools::Itertools;
3use proc_macro::Span;
4use proc_macro2::TokenStream;
5use proc_macro_error::{abort, emit_call_site_warning, emit_error};
6use quote::quote;
7use std::fs;
8use std::path::Path;
9use std::{iter, path::PathBuf};
10use syn::{Attribute, Ident, MetaNameValue};
11
12static MERMAID_JS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/doc/js/");
14
15const MERMAID_JS_LOCAL: &str = "static.files.mermaid/mermaid.esm.min.mjs";
18const MERMAID_JS_LOCAL_DIR: &str = "static.files.mermaid";
19const MERMAID_JS_CDN: &str = "https://unpkg.com/mermaid@10/dist/mermaid.esm.min.mjs";
20
21const UNEXPECTED_ATTR_ERROR: &str =
22 "unexpected attribute inside a diagram definition: only #[doc] is allowed";
23
24#[derive(Clone, Default)]
25pub struct Attrs(Vec<Attr>);
26
27#[derive(Clone)]
28pub enum Attr {
29 Forward(Attribute),
31 DocComment(Ident, String),
33 DiagramStart(Ident),
35 DiagramEntry(Ident, String),
37 DiagramEnd(Ident),
39 DiagramIncludeAnchor(Ident, PathBuf),
41}
42
43impl Attr {
44 pub fn as_ident(&self) -> Option<&Ident> {
45 match self {
46 Attr::Forward(attr) => attr.path().get_ident(),
47 Attr::DocComment(ident, _) => Some(ident),
48 Attr::DiagramStart(ident) => Some(ident),
49 Attr::DiagramEntry(ident, _) => Some(ident),
50 Attr::DiagramEnd(ident) => Some(ident),
51 Attr::DiagramIncludeAnchor(ident, _) => Some(ident),
52 }
53 }
54
55 pub fn is_diagram_end(&self) -> bool {
56 match self {
57 Attr::DiagramEnd(_) => true,
58 _ => false,
59 }
60 }
61
62 pub fn is_diagram_start(&self) -> bool {
63 match self {
64 Attr::DiagramStart(_) => true,
65 _ => false,
66 }
67 }
68
69 pub fn expect_diagram_entry_text(&self) -> &str {
70 match self {
71 Attr::DiagramEntry(_, body) => body.as_str(),
72 _ => abort!(self.as_ident(), UNEXPECTED_ATTR_ERROR),
73 }
74 }
75}
76impl From<Vec<Attribute>> for Attrs {
77 fn from(attrs: Vec<Attribute>) -> Self {
78 let mut out = Attrs::default();
79 out.push_attrs(attrs);
80 out
81 }
82}
83
84impl quote::ToTokens for Attrs {
85 fn to_tokens(&self, tokens: &mut TokenStream) {
86 let mut attrs = self.0.iter();
87 while let Some(attr) = attrs.next() {
88 match attr {
89 Attr::Forward(attr) => attr.to_tokens(tokens),
90 Attr::DocComment(_, comment) => tokens.extend(quote! {
91 #[doc = #comment]
92 }),
93 Attr::DiagramStart(_) => {
94 let diagram = attrs
95 .by_ref()
96 .take_while(|x| !x.is_diagram_end())
97 .map(Attr::expect_diagram_entry_text);
98
99 tokens.extend(generate_diagram_rustdoc(diagram));
100 }
101 Attr::DiagramEntry(_, body) => {
104 emit_call_site_warning!("encountered an unexpected attribute that's going to be ignored, this is a bug! ({})", body);
105 }
106 Attr::DiagramEnd(_) => (),
107 Attr::DiagramIncludeAnchor(_, path) => {
108 let manifest_dir =
110 std::env::var("CARGO_MANIFEST_DIR").unwrap_or(".".to_string());
111
112 let path = &PathBuf::new().join(manifest_dir).join(path);
114
115 let data = match std::fs::read_to_string(path) {
116 Ok(data) => data,
117 Err(e) => {
118 emit_error!(
119 Span::call_site(),
120 "failed to read mermaid file from path {:?}: {}",
121 path,
122 e,
123 );
124 continue;
125 }
126 };
127 tokens.extend(generate_diagram_rustdoc(Some(data.as_str()).into_iter()))
128 }
129 }
130 }
131 }
132}
133
134fn place_mermaid_js() -> std::io::Result<()> {
135 let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or("./target".to_string());
136 let docs_dir = Path::new(&target_dir).join("doc");
137 if docs_dir.exists() {
139 let static_files_mermaid_dir = docs_dir.join(MERMAID_JS_LOCAL_DIR);
140 if static_files_mermaid_dir.exists() {
141 Ok(())
142 } else {
143 fs::create_dir_all(&static_files_mermaid_dir).unwrap();
144 MERMAID_JS_DIR.extract(static_files_mermaid_dir)?;
145 Ok(())
146 }
147 } else {
148 Ok(())
150 }
151}
152
153const MERMAID_INIT_SCRIPT: &str = r#"
154 const mermaidModuleFile = "{mermaidModuleFile}";
155 const fallbackRemoteUrl = "{fallbackRemoteUrl}";
156 const rustdocVarsId= "rustdoc-vars";
157 const dataRootPathAttr = "data-root-path";
158
159
160 function initializeMermaid(mermaid) {
161 var amrn_mermaid_theme =
162 window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
163 ? 'dark'
164 : 'default';
165
166 mermaid.initialize({
167 'startOnLoad':'true',
168 'theme': amrn_mermaid_theme,
169 'logLevel': 3 });
170 mermaid.run();
171 }
172
173 function failedToLoadWarnings() {
174 for(var elem of document.getElementsByClassName("mermaid")) {
175 elem.innerHTML =
176 `<div> <mark>
177 ⚠ Cannot render diagram! Failed to import module from local
178 file and remote location also!
179 Either access the rustdocs via HTTP/S using a
180 <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/set_up_a_local_testing_server">
181 local web server
182 </a>, for example:
183 python3 -m http.server --directory target/doc/, <br> or enable local file access in your
184 Safari/Firefox/Chrome browser, for example
185 starting Chrome with flag '--allow-file-access-from-files'.
186 </mark></div> `;
187 }
188 }
189
190
191 // If rustdoc is read from file directly, the import of mermaid module
192 // from file will fail. In this case falling back to remote location.
193 // If neither succeeds, the mermaid markdown is replaced by notice to
194 // enable file acecss in browser.
195 try {
196 var rootPath = document
197 .getElementById(rustdocVarsId)
198 .attributes[dataRootPathAttr]
199 .value;
200 const {
201 default: mermaid,
202 } = await import(rootPath + mermaidModuleFile);
203 initializeMermaid(mermaid);
204 } catch (e) {
205 try {
206 const {
207 default: mermaid,
208 } = await import(fallbackRemoteUrl);
209 initializeMermaid(mermaid);
210 } catch (e) {
211 failedToLoadWarnings();
212 }
213 }
214"#;
215
216fn generate_diagram_rustdoc<'a>(parts: impl Iterator<Item = &'a str>) -> TokenStream {
217 let preamble = iter::once(r#"<div class="mermaid">"#);
218 let postamble = iter::once("</div>");
219
220 let mermaid_js_init = format!(
221 r#"<script type="module">{}</script>"#,
222 MERMAID_INIT_SCRIPT
223 .replace("{mermaidModuleFile}", MERMAID_JS_LOCAL)
224 .replace("{fallbackRemoteUrl}", MERMAID_JS_CDN)
225 );
226
227 let body = preamble.chain(parts).chain(postamble).join("\n");
228
229 place_mermaid_js().unwrap_or_else(|e| {
230 eprintln!("failed to place mermaid.js on the filesystem: {}", e);
231 });
232
233 quote! {
234 #[doc = #mermaid_js_init]
235 #[doc = #body]
236 }
237}
238
239impl Attrs {
240 pub fn push_attrs(&mut self, attrs: Vec<Attribute>) {
241 use syn::Expr;
242 use syn::ExprLit;
243 use syn::Lit::*;
244
245 let mut current_location = Location::OutsideDiagram;
246 let mut diagram_start_ident = None;
247
248 for attr in attrs {
249 match attr.meta.require_name_value() {
250 Ok(MetaNameValue {
251 value: Expr::Lit(ExprLit { lit: Str(s), .. }),
252 path,
253 ..
254 }) if path.is_ident("doc") => {
255 let ident = path.get_ident().unwrap();
256 for attr in split_attr_body(ident, &s.value(), &mut current_location) {
257 if attr.is_diagram_start() {
258 diagram_start_ident.replace(ident.clone());
259 }
260 self.0.push(attr);
261 }
262 }
263 _ => {
264 if let Location::InsideDiagram = current_location {
265 abort!(attr, UNEXPECTED_ATTR_ERROR)
266 } else {
267 self.0.push(Attr::Forward(attr))
268 }
269 }
270 }
271 }
272
273 if current_location.is_inside() {
274 abort!(diagram_start_ident, "diagram code block is not terminated");
275 }
276 }
277}
278
279#[derive(Debug, Copy, Clone, Eq, PartialEq)]
280enum Location {
281 OutsideDiagram,
282 InsideDiagram,
283}
284
285impl Location {
286 fn is_inside(self) -> bool {
287 match self {
288 Location::InsideDiagram => true,
289 _ => false,
290 }
291 }
292}
293
294fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec<Attr> {
295 use self::Location::*;
296
297 const TICKS: &str = "```";
298 const MERMAID: &str = "mermaid";
299
300 let mut tokens = tokenize_doc_str(input).peekable();
301
302 if tokens.peek().is_none() && !loc.is_inside() {
304 return vec![Attr::DocComment(ident.clone(), String::new())];
305 };
306
307 #[derive(Default)]
309 struct Ctx<'a> {
310 attrs: Vec<Attr>,
311 buffer: Vec<&'a str>,
312 }
313
314 let mut ctx: Ctx<'_> = Default::default();
315
316 let flush_buffer_as_doc_comment = |ctx: &mut Ctx| {
317 if !ctx.buffer.is_empty() {
318 ctx.attrs.push(Attr::DocComment(
319 ident.clone(),
320 ctx.buffer.drain(..).join(" "),
321 ));
322 }
323 };
324
325 let flush_buffer_as_diagram_entry = |ctx: &mut Ctx| {
326 let s = ctx.buffer.drain(..).join(" ");
327 if !s.trim().is_empty() {
328 ctx.attrs.push(Attr::DiagramEntry(ident.clone(), s));
329 }
330 };
331
332 while let Some(token) = tokens.next() {
333 match (*loc, token, tokens.peek()) {
334 (OutsideDiagram, token, _) if token.starts_with("include_mmd!") => {
336 let path = token.trim_start_matches("include_mmd!").trim();
338 let path = path.trim_start_matches('(').trim_end_matches(')');
339 let path = path.trim_matches('"');
340 let path = PathBuf::from(path);
341 ctx.attrs
342 .push(Attr::DiagramIncludeAnchor(ident.clone(), path));
343 }
344 (OutsideDiagram, TICKS, Some(&MERMAID)) => {
346 tokens.next();
347 *loc = InsideDiagram;
348 flush_buffer_as_doc_comment(&mut ctx);
349 ctx.attrs.push(Attr::DiagramStart(ident.clone()));
350 }
351 (InsideDiagram, TICKS, _) => {
353 *loc = OutsideDiagram;
354 flush_buffer_as_diagram_entry(&mut ctx);
355 ctx.attrs.push(Attr::DiagramEnd(ident.clone()))
356 }
357 _ => ctx.buffer.push(token),
358 }
359 }
360
361 if !ctx.buffer.is_empty() {
362 if loc.is_inside() {
363 flush_buffer_as_diagram_entry(&mut ctx);
364 } else {
365 flush_buffer_as_doc_comment(&mut ctx);
366 };
367 }
368
369 ctx.attrs
370}
371
372fn tokenize_doc_str(input: &str) -> impl Iterator<Item = &str> {
373 const TICKS: &str = "```";
374 split_inclusive(input, TICKS).flat_map(|token| {
375 token.split(' ')
377 })
378}
379
380fn split_inclusive<'a, 'b: 'a>(input: &'a str, delim: &'b str) -> impl Iterator<Item = &'a str> {
382 let mut tokens = vec![];
383 let mut prev = 0;
384
385 for (idx, matches) in input.match_indices(delim) {
386 tokens.extend(nonempty(&input[prev..idx]));
387
388 prev = idx + matches.len();
389
390 tokens.push(matches);
391 }
392
393 if prev < input.len() {
394 tokens.push(&input[prev..]);
395 }
396
397 tokens.into_iter()
398}
399
400fn nonempty(s: &str) -> Option<&str> {
401 if s.is_empty() {
402 None
403 } else {
404 Some(s)
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::{split_inclusive, Attr};
411 use std::fmt;
412
413 #[cfg(test)]
414 impl fmt::Debug for Attr {
415 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 match self {
417 Attr::Forward(..) => f.write_str("Attr::Forward"),
418 Attr::DocComment(_, body) => write!(f, "Attr::DocComment({:?})", body),
419 Attr::DiagramStart(..) => f.write_str("Attr::DiagramStart"),
420 Attr::DiagramEntry(_, body) => write!(f, "Attr::DiagramEntry({:?})", body),
421 Attr::DiagramEnd(..) => f.write_str("Attr::DiagramEnd"),
422 Attr::DiagramIncludeAnchor(_, path) => {
423 write!(f, "Attr::DiagramIncludeAnchor({:?})", path)
424 }
425 }
426 }
427 }
428
429 #[cfg(test)]
430 impl Eq for Attr {}
431
432 #[cfg(test)]
433 impl PartialEq for Attr {
434 fn eq(&self, other: &Self) -> bool {
435 use std::mem::discriminant;
436 use Attr::*;
437 match (self, other) {
438 (DocComment(_, a), DocComment(_, b)) => a == b,
439 (DiagramEntry(_, a), DiagramEntry(_, b)) => a == b,
440 (a, b) => discriminant(a) == discriminant(b),
441 }
442 }
443 }
444
445 #[test]
446 fn temp_split_inclusive() {
447 let src = "```";
448 let out: Vec<_> = split_inclusive(src, "```").collect();
449 assert_eq!(&out, &["```",]);
450
451 let src = "```abcd```";
452 let out: Vec<_> = split_inclusive(src, "```").collect();
453 assert_eq!(&out, &["```", "abcd", "```"]);
454
455 let src = "left```abcd```right";
456 let out: Vec<_> = split_inclusive(src, "```").collect();
457 assert_eq!(&out, &["left", "```", "abcd", "```", "right",]);
458 }
459
460 mod split_attr_body_tests {
461 use super::super::*;
462
463 use proc_macro2::Ident;
464 use proc_macro2::Span;
465
466 use pretty_assertions::assert_eq;
467
468 fn i() -> Ident {
469 Ident::new("fake", Span::call_site())
470 }
471
472 struct TestCase<'a> {
473 ident: Ident,
474 location: Location,
475 input: &'a str,
476 expect_location: Location,
477 expect_attrs: Vec<Attr>,
478 }
479
480 fn check(case: TestCase) {
481 let mut loc = case.location;
482 let attrs = split_attr_body(&case.ident, case.input, &mut loc);
483 assert_eq!(loc, case.expect_location);
484 assert_eq!(attrs, case.expect_attrs);
485 }
486
487 #[test]
488 fn one_line_one_diagram() {
489 let case = TestCase {
490 ident: i(),
491 location: Location::OutsideDiagram,
492 input: "```mermaid abcd```",
493 expect_location: Location::OutsideDiagram,
494 expect_attrs: vec![
495 Attr::DiagramStart(i()),
496 Attr::DiagramEntry(i(), "abcd".into()),
497 Attr::DiagramEnd(i()),
498 ],
499 };
500
501 check(case)
502 }
503
504 #[test]
505 fn one_line_multiple_diagrams() {
506 let case = TestCase {
507 ident: i(),
508 location: Location::OutsideDiagram,
509 input: "```mermaid abcd``` ```mermaid efgh``` ```mermaid ijkl```",
510 expect_location: Location::OutsideDiagram,
511 expect_attrs: vec![
512 Attr::DiagramStart(i()),
513 Attr::DiagramEntry(i(), "abcd".into()),
514 Attr::DiagramEnd(i()),
515 Attr::DocComment(i(), " ".into()),
516 Attr::DiagramStart(i()),
517 Attr::DiagramEntry(i(), "efgh".into()),
518 Attr::DiagramEnd(i()),
519 Attr::DocComment(i(), " ".into()),
520 Attr::DiagramStart(i()),
521 Attr::DiagramEntry(i(), "ijkl".into()),
522 Attr::DiagramEnd(i()),
523 ],
524 };
525
526 check(case)
527 }
528
529 #[test]
530 fn other_snippet() {
531 let case = TestCase {
532 ident: i(),
533 location: Location::OutsideDiagram,
534 input: "```rust panic!()```",
535 expect_location: Location::OutsideDiagram,
536 expect_attrs: vec![Attr::DocComment(i(), "``` rust panic!() ```".into())],
537 };
538
539 check(case)
540 }
541
542 #[test]
543 fn carry_over() {
544 let case = TestCase {
545 ident: i(),
546 location: Location::OutsideDiagram,
547 input: "left```mermaid abcd```right",
548 expect_location: Location::OutsideDiagram,
549 expect_attrs: vec![
550 Attr::DocComment(i(), "left".into()),
551 Attr::DiagramStart(i()),
552 Attr::DiagramEntry(i(), "abcd".into()),
553 Attr::DiagramEnd(i()),
554 Attr::DocComment(i(), "right".into()),
555 ],
556 };
557
558 check(case)
559 }
560
561 #[test]
562 fn multiline_termination() {
563 let case = TestCase {
564 ident: i(),
565 location: Location::InsideDiagram,
566 input: "abcd```",
567 expect_location: Location::OutsideDiagram,
568 expect_attrs: vec![
569 Attr::DiagramEntry(i(), "abcd".into()),
570 Attr::DiagramEnd(i()),
571 ],
572 };
573
574 check(case)
575 }
576
577 #[test]
578 fn multiline_termination_single_token() {
579 let case = TestCase {
580 ident: i(),
581 location: Location::InsideDiagram,
582 input: "```",
583 expect_location: Location::OutsideDiagram,
584 expect_attrs: vec![Attr::DiagramEnd(i())],
585 };
586
587 check(case)
588 }
589
590 #[test]
591 fn multiline_termination_carry() {
592 let case = TestCase {
593 ident: i(),
594 location: Location::InsideDiagram,
595 input: "abcd```right",
596 expect_location: Location::OutsideDiagram,
597 expect_attrs: vec![
598 Attr::DiagramEntry(i(), "abcd".into()),
599 Attr::DiagramEnd(i()),
600 Attr::DocComment(i(), "right".into()),
601 ],
602 };
603
604 check(case)
605 }
606 }
607}