Skip to content

Commit

Permalink
feat: multiple fs imports, free placement of imported diagrams
Browse files Browse the repository at this point in the history
  • Loading branch information
mersinvald committed Dec 13, 2023
1 parent 4cd047c commit f78120d
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 67 deletions.
20 changes: 6 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,18 @@ To learn more, see the [Theming Section](https://mermaid-js.github.io/mermaid/#/

### Separating diagrams from code

A diagram can be loaded from file to reduce clutter in the documentation comments.
A diagram, or multiple, can be loaded from file to reduce clutter in the documentation comments.

Such diagram will always be placed under the rest of the document section.
Reading diagrams from file can be combined with placing them into the doc-comment, to get multiple diagrams describing a single entity, however only one can be placed inside the file. (FIXME).

```rust
#[cfg_attr(doc, aquamarine::aquamarine, path = "./diagram.mermaid")]
#[cfg_attr(doc, aquamarine::aquamarine)]
/// My diagram #1
/// include_mmd!("diagram1.mmd")
/// My diagram #2
/// include_mmd!("diagram2.mmd")
pub fn example_foad_from_file() {}
```

```bash
# diagram.mermaid
graph LR
s([Source]) --> a[[aquamarine]]
r[[rustdoc]] --> f([Docs w/ Mermaid!])
subgraph rustc[Rust Compiler]
a -. load diagram.mermaid .-> r
end
```

### In the wild

Crates that use `aquamarine` in their documentation
Expand Down
2 changes: 1 addition & 1 deletion demo/diagram.mermaid → demo/diagram_0.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ graph LR
s([Source]) --> a[[aquamarine]]
r[[rustdoc]] --> f([Docs w/ Mermaid!])
subgraph rustc[Rust Compiler]
a -. "load diagram.mermaid" .-> r
a -. "load diagram_0.mermaid" .-> r
end
5 changes: 5 additions & 0 deletions demo/diagram_1.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
graph LR
A[Square Rect] -- Link text --> B((Circle))
A --> C(Round Rect)
B --> D{Rhombus}
C --> D
12 changes: 9 additions & 3 deletions demo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ pub fn example() {}
/// To learn more, see the [Theming Section](https://mermaid-js.github.io/mermaid/#/theming) of the mermaid.js book
pub fn example_with_styling() {}

#[cfg_attr(doc, aquamarine::aquamarine, path = "./diagram.mermaid")]
#[cfg_attr(doc, aquamarine::aquamarine)]
/// A diagram can be loaded from a file as well!
///
/// Reduce clutter in your doc comments, when a diagram is big enough
/// include_mmd!("diagram_0.mmd")
///
/// Reduce clutter in your doc comments, when a diagram is big enough.
///
/// You can include multiple diagrams in a single doc comment, using the macro-like syntax `include_mmd!("/path/to/diagram.mmd")`
///
/// include_mmd!(diagram_1.mmd)
///
/// **Note:** diagrams loaded form file are always placed in the bottom of the doc section
/// **Note:** `indlude_mmd!` syntax is only supported inside doc comments
pub fn example_load_from_file() {}
91 changes: 45 additions & 46 deletions src/attrs.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use include_dir::{include_dir, Dir};
use itertools::Itertools;
use proc_macro2::TokenStream;
use proc_macro_error::{abort, emit_call_site_warning};
use quote::quote;
use std::iter;
use syn::{Attribute, Ident, MetaNameValue};
use std::fs;
use std::path::Path;
use include_dir::{include_dir, Dir};
use std::{iter, path::PathBuf};
use syn::{Attribute, Ident, MetaNameValue};

// embedded JS code being inserted as html script elmenets
static MERMAID_JS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/doc/js/");
Expand Down Expand Up @@ -35,6 +35,8 @@ pub enum Attr {
DiagramEntry(Ident, String),
/// Diagram end token
DiagramEnd(Ident),
/// Include Anchor
DiagramIncludeAnchor(Ident, PathBuf),
}

impl Attr {
Expand All @@ -45,20 +47,21 @@ impl Attr {
Attr::DiagramStart(ident) => Some(ident),
Attr::DiagramEntry(ident, _) => Some(ident),
Attr::DiagramEnd(ident) => Some(ident),
Attr::DiagramIncludeAnchor(ident, _) => Some(ident),
}
}

pub fn is_diagram_end(&self) -> bool {
match self {
Attr::DiagramEnd(_) => true,
_ => false
_ => false,
}
}

pub fn is_diagram_start(&self) -> bool {
match self {
Attr::DiagramStart(_) => true,
_ => false
_ => false,
}
}

Expand All @@ -80,27 +83,9 @@ impl From<Vec<Attribute>> for Attrs {
impl quote::ToTokens for Attrs {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut attrs = self.0.iter();
let mut loaded_from_file = None;
while let Some(attr) = attrs.next() {
match attr {
Attr::Forward(attr) => {
// check if filepath is supplied
if attr.path().is_ident("path") {
if let syn::Meta::List(ref ml) = attr.meta {
for token in ml.tokens.to_token_stream().into_iter() {
if let proc_macro2::TokenTree::Literal(value) = token {
let data =
std::fs::read_to_string(value.to_string().replace("\"", ""))
.expect("Unable to read mermaid file");
loaded_from_file = Some(generate_diagram_rustdoc(
vec![data.as_str()].into_iter(),
));
}
}
}
}
attr.to_tokens(tokens)
}
Attr::Forward(attr) => attr.to_tokens(tokens),
Attr::DocComment(_, comment) => tokens.extend(quote! {
#[doc = #comment]
}),
Expand All @@ -118,18 +103,17 @@ impl quote::ToTokens for Attrs {
emit_call_site_warning!("encountered an unexpected attribute that's going to be ignored, this is a bug! ({})", body);
}
Attr::DiagramEnd(_) => (),
Attr::DiagramIncludeAnchor(_, path) => {
let data = std::fs::read_to_string(path).expect("Unable to read mermaid file");
tokens.extend(generate_diagram_rustdoc(Some(data.as_str()).into_iter()))
}
}
}

if let Some(diagram) = loaded_from_file {
tokens.extend(diagram)
}
}
}

fn place_mermaid_js() -> std::io::Result<()> {
let target_dir = std::env::var("CARGO_TARGET_DIR")
.unwrap_or("./target".to_string());
let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or("./target".to_string());
let docs_dir = Path::new(&target_dir).join("doc");
// extract mermaid module iff rustdoc folder exists already
if docs_dir.exists() {
Expand Down Expand Up @@ -210,16 +194,16 @@ const MERMAID_INIT_SCRIPT: &str = r#"
}
"#;

fn generate_diagram_rustdoc<'a>(parts: impl Iterator<Item=&'a str>) -> TokenStream {
fn generate_diagram_rustdoc<'a>(parts: impl Iterator<Item = &'a str>) -> TokenStream {
let preamble = iter::once(r#"<div class="mermaid">"#);
let postamble = iter::once("</div>");


let mermaid_js_init = format!(r#"<script type="module">{}</script>"#,
MERMAID_INIT_SCRIPT
.replace("{mermaidModuleFile}", MERMAID_JS_LOCAL)
.replace("{fallbackRemoteUrl}", MERMAID_JS_CDN));

let mermaid_js_init = format!(
r#"<script type="module">{}</script>"#,
MERMAID_INIT_SCRIPT
.replace("{mermaidModuleFile}", MERMAID_JS_LOCAL)
.replace("{fallbackRemoteUrl}", MERMAID_JS_CDN)
);

let body = preamble.chain(parts).chain(postamble).join("\n");

Expand All @@ -235,18 +219,20 @@ fn generate_diagram_rustdoc<'a>(parts: impl Iterator<Item=&'a str>) -> TokenStre

impl Attrs {
pub fn push_attrs(&mut self, attrs: Vec<Attribute>) {
use syn::Lit::*;
use syn::Expr;
use syn::ExprLit;
use syn::Lit::*;

let mut current_location = Location::OutsideDiagram;
let mut diagram_start_ident = None;

for attr in attrs {
match attr.meta.require_name_value() {
Ok(MetaNameValue {
value: Expr::Lit(ExprLit {lit: Str(s), .. }), path, ..
}) if path.is_ident("doc") => {
value: Expr::Lit(ExprLit { lit: Str(s), .. }),
path,
..
}) if path.is_ident("doc") => {
let ident = path.get_ident().unwrap();
for attr in split_attr_body(ident, &s.value(), &mut current_location) {
if attr.is_diagram_start() {
Expand Down Expand Up @@ -281,7 +267,7 @@ impl Location {
fn is_inside(self) -> bool {
match self {
Location::InsideDiagram => true,
_ => false
_ => false,
}
}
}
Expand All @@ -306,7 +292,7 @@ fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec<Attr>
buffer: Vec<&'a str>,
}

let mut ctx = Default::default();
let mut ctx: Ctx<'_> = Default::default();

let flush_buffer_as_doc_comment = |ctx: &mut Ctx| {
if !ctx.buffer.is_empty() {
Expand All @@ -326,6 +312,16 @@ fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec<Attr>

while let Some(token) = tokens.next() {
match (*loc, token, tokens.peek()) {
// Detect include anchor
(OutsideDiagram, token, _) if token.starts_with("include_mmd!") => {
// cleanup
let path = token.trim_start_matches("include_mmd!").trim();
let path = path.trim_start_matches('(').trim_end_matches(')');
let path = path.trim_matches('"');
let path = PathBuf::from(path);
ctx.attrs
.push(Attr::DiagramIncludeAnchor(ident.clone(), path));
}
// Flush the buffer, then open the diagram code block
(OutsideDiagram, TICKS, Some(&MERMAID)) => {
tokens.next();
Expand Down Expand Up @@ -354,7 +350,7 @@ fn split_attr_body(ident: &Ident, input: &str, loc: &mut Location) -> Vec<Attr>
ctx.attrs
}

fn tokenize_doc_str(input: &str) -> impl Iterator<Item=&str> {
fn tokenize_doc_str(input: &str) -> impl Iterator<Item = &str> {
const TICKS: &str = "```";
split_inclusive(input, TICKS).flat_map(|token| {
// not str::split_whitespace because we don't wanna filter-out the whitespace tokens
Expand All @@ -363,7 +359,7 @@ fn tokenize_doc_str(input: &str) -> impl Iterator<Item=&str> {
}

// TODO: remove once str::split_inclusive is stable
fn split_inclusive<'a, 'b: 'a>(input: &'a str, delim: &'b str) -> impl Iterator<Item=&'a str> {
fn split_inclusive<'a, 'b: 'a>(input: &'a str, delim: &'b str) -> impl Iterator<Item = &'a str> {
let mut tokens = vec![];
let mut prev = 0;

Expand Down Expand Up @@ -404,6 +400,9 @@ mod tests {
Attr::DiagramStart(..) => f.write_str("Attr::DiagramStart"),
Attr::DiagramEntry(_, body) => write!(f, "Attr::DiagramEntry({:?})", body),
Attr::DiagramEnd(..) => f.write_str("Attr::DiagramEnd"),
Attr::DiagramIncludeAnchor(_, path) => {
write!(f, "Attr::DiagramIncludeAnchor({:?})", path)
}
}
}
}
Expand All @@ -428,15 +427,15 @@ mod tests {
fn temp_split_inclusive() {
let src = "```";
let out: Vec<_> = split_inclusive(src, "```").collect();
assert_eq!(&out, &["```", ]);
assert_eq!(&out, &["```",]);

let src = "```abcd```";
let out: Vec<_> = split_inclusive(src, "```").collect();
assert_eq!(&out, &["```", "abcd", "```"]);

let src = "left```abcd```right";
let out: Vec<_> = split_inclusive(src, "```").collect();
assert_eq!(&out, &["left", "```", "abcd", "```", "right", ]);
assert_eq!(&out, &["left", "```", "abcd", "```", "right",]);
}

mod split_attr_body_tests {
Expand Down
21 changes: 18 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
//!
//! You can even add multiple diagrams!
//!
//! To see it in action, go to the [demo crate](https://docs.rs/aquamarine-demo-crate/0.3.3/aquamarine_demo_crate/fn.example.html) docs.rs page.
//! To see it in action, go to the [demo crate](https://docs.rs/aquamarine-demo-crate/0.4.0/aquamarine_demo_crate/fn.example.html) docs.rs page.
//!
//! ### Dark-mode
//!
Expand Down Expand Up @@ -51,10 +51,25 @@
//! /// ```
//! # fn example() {}
//! ```
//!
//! [Demo on docs.rs](https://docs.rs/aquamarine-demo-crate/0.3.3/aquamarine_demo_crate/fn.example_with_styling.html)
//! [Demo on docs.rs](https://docs.rs/aquamarine-demo-crate/0.4.0/aquamarine_demo_crate/fn.example_with_styling.html)
//!
//! To learn more, see the [Theming Section](https://mermaid-js.github.io/mermaid/#/theming) of the mermaid.js book
//!
//! ### Loading from a file
//!
//! When describing complex logic, a diagram can get quite big.
//!
//! To reduce clutter in your doc comments, you can load a diagram from file using the `include_mmd!` macro-like syntax.
//!
//! ```no_run
//! /// Diagram #1
//! /// include_mmd!("diagram_1.mmd")
//! ///
//! /// Diagram #2
//! /// include_mmd!("diagram_2.mmd")
//! # fn example() {}
//! ```
//! [Demo on docs.rs](https://docs.rs/aquamarine-demo-crate/0.4.0/aquamarine_demo_crate/fn.example_load_from_file.html)
extern crate proc_macro;

Expand Down

0 comments on commit f78120d

Please sign in to comment.