gorbie_commonmark_macros/
lib.rs

1//! Compile time evaluation of markdown that generates egui widgets
2//!
3//! It is recommended to use this crate through the parent crate
4//! [gorbie_commonmark](https://docs.rs/gorbie-commonmark).
5//! If you for some reason don't want to use it you must also import
6//! [gorbie_commonmark_backend](https://docs.rs/gorbie-commonmark-backend)
7//! directly from your crate to get access to `CommonMarkCache` and internals that
8//! the macros require for the final generated code.
9//!
10//! ## API
11//! ### Embedding markdown text directly
12//!
13//! The macro has the following format:
14//!
15//! commonmark!(ui, cache, text);
16//!
17//! #### Example
18//!
19//! ```
20//! # // If used through gorbie_commonmark the backend crate does not need to be relied upon
21//! # use gorbie_commonmark_backend::CommonMarkCache;
22//! # use gorbie_commonmark_macros::commonmark;
23//! # egui::__run_test_ui(|ui| {
24//! let mut cache = CommonMarkCache::default();
25//! let _response = commonmark!(ui, &mut cache, "# ATX Heading Level 1");
26//! # });
27//! ```
28//!
29//! As you can see it also returns a response like most other egui widgets.
30//!
31//! ### Embedding markdown file
32//!
33//! The macro has the exact same format as the `commonmark!` macro:
34//!
35//! commonmark_str!(ui, cache, file_path);
36//!
37//! #### Example
38//!
39// Unfortunately can't depend on an actual file in the doc test so it must be
40// disabled
41//! ```rust,ignore
42//! # use gorbie_commonmark_backend::CommonMarkCache;
43//! # use gorbie_commonmark_macros::commonmark_str;
44//! # egui::__run_test_ui(|ui| {
45//! let mut cache = CommonMarkCache::default();
46//! commonmark_str!(ui, &mut cache, "foo.md");
47//! # });
48//! ```
49//!
50//! One drawback is that the file cannot be tracked by rust on stable so the
51//! program won't recompile if you only change the content of the file. To
52//! work around this you can use a nightly compiler and enable the
53//! `nightly` feature when iterating on your markdown files.
54//!
55//! ## Limitations
56//!
57//! Compared to it's runtime counterpart gorbie_commonmark it currently does not
58//! offer customization. This is something that will be addressed eventually once
59//! a good API has been chosen.
60//!
61//! ## What this crate is not
62//!
63//! This crate does not have as a goal to make widgets that can be interacted with
64//! through code.
65//!
66//! ```rust,ignore
67//! let ... = commonmark!(ui, &mut cache, "- [ ] Task List");
68//! task_list.set_checked(true); // No !!
69//! ```
70//!
71//! For that you should fall back to normal egui widgets
72#![cfg_attr(feature = "document-features", doc = "# Features")]
73#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
74#![cfg_attr(feature = "nightly", feature(track_path))]
75
76mod generator;
77use generator::*;
78
79use quote::quote_spanned;
80use syn::parse::{Parse, ParseStream, Result};
81use syn::{Expr, LitStr, Token, parse_macro_input};
82
83struct Parameters {
84    ui: Expr,
85    cache: Expr,
86    markdown: LitStr,
87}
88
89impl Parse for Parameters {
90    fn parse(input: ParseStream) -> Result<Self> {
91        let ui: Expr = input.parse()?;
92        input.parse::<Token![,]>()?;
93        let cache: Expr = input.parse()?;
94        input.parse::<Token![,]>()?;
95        let markdown: LitStr = input.parse()?;
96
97        Ok(Parameters {
98            ui,
99            cache,
100            markdown,
101        })
102    }
103}
104
105fn commonmark_impl(ui: Expr, cache: Expr, text: String) -> proc_macro2::TokenStream {
106    let stream = CommonMarkViewerInternal::new().show(ui, cache, &text);
107
108    #[cfg(feature = "dump-macro")]
109    {
110        // Wrap within a function to allow rustfmt to format it
111        println!("fn main() {{");
112        println!("{}", stream.to_string());
113        println!("}}");
114    }
115
116    // false positive due to feature gate
117    #[allow(clippy::let_and_return)]
118    stream
119}
120
121#[proc_macro]
122pub fn commonmark(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
123    let Parameters {
124        ui,
125        cache,
126        markdown,
127    } = parse_macro_input!(input as Parameters);
128
129    commonmark_impl(ui, cache, markdown.value()).into()
130}
131
132#[proc_macro]
133pub fn commonmark_str(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
134    let Parameters {
135        ui,
136        cache,
137        markdown,
138    } = parse_macro_input!(input as Parameters);
139
140    let path = markdown.value();
141    #[cfg(feature = "nightly")]
142    {
143        // Tell rust to track the file so that the macro will regenerate when the
144        // file changes
145        proc_macro::tracked_path::path(&path);
146    }
147
148    let Ok(md) = std::fs::read_to_string(path) else {
149        return quote_spanned!(markdown.span()=>
150            compile_error!("Could not find markdown file");
151        )
152        .into();
153    };
154
155    commonmark_impl(ui, cache, md).into()
156}
157
158fn resolve_backend_crate_import() -> proc_macro2::TokenStream {
159    // The purpose of this is to ensure that when used through gorbie_commonmark
160    // the generated code can always find gorbie_commonmark_backend without the
161    // user having to import themselves.
162    //
163    // There are other ways to do this that does not depend on an external crate
164    // such as exposing a feature flag in this crate that gorbie_commonmark can set.
165    // This works for users, however it is a pain to use in this workspace as
166    // the macro tests won't work when run from the workspace directory. So instead
167    // they must be run from this crate's workspace. I don't want to rely on that mess
168    // so this is the solution. I have also tried some other solutions with no success
169    // or they had drawbacks that I did not like.
170    //
171    // With all that said the resolution is the following:
172    //
173    // Try gorbie_commonmark_backend first. This ensures that the tests will run from
174    // the main workspace despite gorbie_commonmark being present. However if only
175    // gorbie_commonmark is present then a `use gorbie_commonmark::gorbie_commonmark_backend;`
176    // will be inserted into the generated code.
177    //
178    // If none of that work's then the user is missing some crates
179
180    let backend_crate = proc_macro_crate::crate_name("gorbie_commonmark_backend");
181    let main_crate = proc_macro_crate::crate_name("gorbie_commonmark");
182
183    if backend_crate.is_ok() {
184        proc_macro2::TokenStream::new()
185    } else if let Ok(found_crate) = main_crate {
186        let crate_name = match found_crate {
187            proc_macro_crate::FoundCrate::Itself => return proc_macro2::TokenStream::new(),
188            proc_macro_crate::FoundCrate::Name(name) => name,
189        };
190
191        let crate_name_lit = proc_macro2::Ident::new(&crate_name, proc_macro2::Span::call_site());
192        quote::quote!(
193            use #crate_name_lit::gorbie_commonmark_backend;
194        )
195    } else {
196        proc_macro2::TokenStream::new()
197    }
198}
199
200#[test]
201fn tests() {
202    let t = trybuild::TestCases::new();
203    t.pass("tests/pass/*.rs");
204    t.compile_fail("tests/fail/*.rs");
205}