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}