docstr/lib.rs
1#, "?style=flat-square&logo=rust)](https://crates.io/crates/", env!("CARGO_PKG_NAME"), ")")]
2#, "?style=flat-square&logo=docs.rs)](https://docs.rs/", env!("CARGO_PKG_NAME"), ")")]
3#"]
4#, "-blue?style=flat-square&logo=rust)")]
5//! [](https://github.com/nik-rev/docstr)
6//!
7//! This crate provides a macro [`docstr!`] for ergonomically creating multi-line string literals.
8//!
9//! ```toml
10#![doc = concat!(env!("CARGO_PKG_NAME"), " = ", "\"", env!("CARGO_PKG_VERSION_MAJOR"), ".", env!("CARGO_PKG_VERSION_MINOR"), "\"")]
11//! ```
12//!
13//! Note: `docstr` does not have any dependencies such as `syn` or `quote`, so compile-speeds are very fast.
14//!
15//! # Usage
16//!
17//! [`docstr!`](crate::docstr) takes documentation comments as arguments and converts them into a string
18//!
19//! ```rust
20//! use docstr::docstr;
21//!
22//! let hello_world_in_c: &'static str = docstr!(
23//! /// #include <stdio.h>
24//! ///
25//! /// int main(int argc, char **argv) {
26//! /// printf("hello world\n");
27//! /// return 0;
28//! /// }
29//! );
30//!
31//! assert_eq!(hello_world_in_c, r#"#include <stdio.h>
32//!
33//! int main(int argc, char **argv) {
34//! printf("hello world\n");
35//! return 0;
36//! }"#)
37//! ```
38//!
39//! # Composition
40//!
41//! [`docstr!`](crate::docstr) can pass the generated string to any macro. This example shows the string being forwarded to the [`format!`] macro:
42//!
43//! ```
44//! # use docstr::docstr;
45//! let name = "Bob";
46//! let age = 21;
47//!
48//! let greeting: String = docstr!(format!
49//! /// Hello, my name is {name}.
50//! /// I am {} years old!
51//! age
52//! );
53//!
54//! assert_eq!(greeting, "\
55//! Hello, my name is Bob.
56//! I am 21 years old!");
57//! ```
58//!
59//! This is great because there's just a single macro, `docstr!`, that can do anything. No need for `docstr_format!`, `docstr_println!`, `docstr_write!`, etc.
60//!
61//! ## How composition works
62//!
63//! If the first argument to `docstr!` is a path to a macro, that macro will be called. This invocation:
64//!
65//! ```
66//! # use docstr::docstr;
67//! let greeting: String = docstr!(format!
68//! /// Hello, my name is {name}.
69//! /// I am {} years old!
70//! age
71//! );
72//! ```
73//!
74//! Is equivalent to this:
75//!
76//! ```
77//! let greeting: String = format!("\
78//! Hello, my name is {name}.
79//! I am {} years old!"
80//! age,
81//! );
82//! ```
83//!
84//! You can inject arguments before the format string:
85//!
86//! ```rust
87//! # let mut w = String::new();
88//! # use std::fmt::Write as _;
89//! # use docstr::docstr;
90//! docstr!(write! w
91//! /// Hello, world!
92//! );
93//! ```
94//!
95//! Expands to:
96//!
97//! ```rust
98//! # let mut w = String::new();
99//! # use std::fmt::Write as _;
100//! write!(w, "Hello, world!");
101//! ```
102//!
103//! # Global Import
104//!
105//! This will make `docstr!` globally accessible in your entire crate, without needing to import it:
106//!
107//! ```
108//! #[macro_use(docstr)]
109//! extern crate docstr;
110//! ```
111
112use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
113
114/// Turns documentation comments into string at compile-time.
115///
116/// ```rust
117/// use docstr::docstr;
118///
119/// let hello_world: String = docstr!(format!
120/// /// fn say_hi() {{
121/// /// println!("Hello, my name is {}");
122/// /// }}
123/// "Bob"
124/// );
125///
126/// assert_eq!(hello_world, r#"fn say_hi() {
127/// println!("Hello, my name is Bob");
128/// }"#);
129/// ```
130///
131/// Expands to this:
132///
133/// ```rust
134/// format!(r#"fn say_hi() {{
135/// println!("Hello, my name is {}");
136/// }}"#, "Bob");
137/// ```
138///
139/// See the [crate-level](crate) documentation for more info
140#[proc_macro]
141pub fn docstr(input: TokenStream) -> TokenStream {
142 let mut input = input.into_iter().peekable();
143
144 // If we encounter any errors, we collect them into here
145 // and report them all at once
146 //
147 // compile_error!("you have done horrible things!")
148 let mut compile_errors = TokenStream::new();
149 let mut compile_error = |span: Span, message: &str| {
150 compile_errors.extend(CompileError::new(span, message));
151 };
152
153 // Path to the macro that we send tokens to.
154 //
155 // If this is `None`, we don't forward the path to any macro,
156 // and docstr! produces a string literal of type &'static str
157 //
158 // docstr!(
159 // /// hello world
160 // )
161 // => "hello world"
162 //
163 // docstr!(format!
164 // /// hello {world}
165 // )
166 // => format!("hello {world}")
167 let macro_path = match input.peek() {
168 // No macro path, this will directly produce a string literal
169 //
170 // docstr!(
171 // /// hello world
172 // )
173 Some(TokenTree::Punct(punct)) if *punct == '#' => None,
174 // Macro input is completely empty
175 //
176 // docstr!()
177 None => {
178 return CompileError::new(
179 Span::call_site(),
180 "requires at least a documentation comment argument: `/// ...`",
181 )
182 .into()
183 }
184 // Path to a macro.
185 //
186 // docstr!(format!
187 // /// hello {world}
188 // )
189 Some(_) => {
190 // Contains tokens of the macro, e.g. `std::format!`
191 match extract_macro_path(&mut input) {
192 Ok(macro_path) => macro_path,
193 Err(compile_error) => return compile_error.into(),
194 }
195 }
196 };
197
198 // Tokens BEFORE the doc comments, which are appended
199 // directly to the `macro_path` we just got - before the `doc_comments`
200 let mut tokens_before_doc_comments = TokenStream::new();
201
202 // Contents of the doc comments which we collect
203 //
204 // /// foo
205 // /// bar
206 //
207 // Expands to:
208 //
209 // #[doc = "foo"]
210 // #[doc = "bar"]
211 //
212 // Which we collect to:
213 //
214 // ["foo", "bar"]
215 let mut doc_comments = Vec::new();
216
217 // Tokens AFTER the doc comments, which are appended
218 // directly to the `macro_path` we just got - after the `doc_comments`
219 let mut tokens_after_doc_comments = TokenStream::new();
220
221 /// In the middle of `docstr!(...)` macro's invocation, we will always have doc comments.
222 ///
223 /// ```ignore
224 /// docstr!(
225 /// // DocComments::NotReached
226 /// but we can have tokens here
227 /// // DocComments::Inside
228 /// /// foo
229 /// /// bar
230 /// // DocComments::Finished
231 /// and here too
232 /// )
233 /// ```
234 #[derive(Eq, PartialEq, PartialOrd, Ord)]
235 enum DocCommentProgress {
236 /// doc comments `///` not reached yet
237 NotReached,
238 /// currently we are INSIDE the doc comments
239 Inside,
240 /// We have parsed all the doc comments
241 Finished,
242 }
243
244 // State machine corresponding to our current progress in the macro
245 let mut doc_comment_progress = DocCommentProgress::NotReached;
246
247 // Let's collect all of the doc comments into a Vec<String> where each
248 // String corresponds to the doc comment
249 while let Some(tt) = input.next() {
250 // #[doc = "..."]
251 // ^
252 let doc_comment_start_span = match tt {
253 // this token is passed verbatim to the macro at the end,
254 // after the doc comments
255 tt if doc_comment_progress == DocCommentProgress::Finished => {
256 tokens_after_doc_comments.extend([tt]);
257 continue;
258 }
259 // start of doc comment
260 TokenTree::Punct(punct) if punct == '#' => {
261 match doc_comment_progress {
262 DocCommentProgress::NotReached => {
263 doc_comment_progress = DocCommentProgress::Inside;
264 }
265 DocCommentProgress::Inside => {
266 // ok
267 }
268 DocCommentProgress::Finished => {
269 unreachable!("if it's finished we would `continue` in an earlier arm")
270 }
271 }
272 match input.peek() {
273 Some(TokenTree::Punct(punct)) if *punct == '!' => {
274 compile_error(
275 punct.span(),
276 "Inner doc comments `//! ...` are not supported. Please use `/// ...`",
277 );
278 // eat '!'
279 input.next();
280 }
281 _ => (),
282 }
283 punct.span()
284 }
285 // this token is passed verbatim to the macro at the beginning,
286 // before the doc comments
287 tt if doc_comment_progress == DocCommentProgress::NotReached => {
288 // Comma before '#' is optional
289 //
290 // docstr!(writeln! w,
291 // ^ this comma can be omitted
292 // #[doc = "..."]
293 // ^ next token
294 // )
295 let insert_comma = match input.peek() {
296 Some(TokenTree::Punct(next)) => match &tt {
297 TokenTree::Punct(current) if *current == ',' && *next == '#' => false,
298 _ if *next == '#' => true,
299 _ => false,
300 },
301 _ => false,
302 };
303
304 tokens_before_doc_comments.extend([tt]);
305
306 if insert_comma {
307 tokens_before_doc_comments
308 .extend([TokenTree::Punct(Punct::new(',', Spacing::Joint))]);
309 }
310
311 continue;
312 }
313 _ => {
314 unreachable!("when the next token is not `#` progress is `Finished`")
315 }
316 };
317
318 // #[doc = "..."]
319 // ^^^^^^^^^^^^^
320 let doc_comment_square_brackets = match input.next() {
321 Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => group,
322 Some(tt) => {
323 compile_error(tt.span(), "expected `[...]`");
324 continue;
325 }
326 None => {
327 compile_error(
328 doc_comment_start_span,
329 "expected `#` to be followed by `[...]`",
330 );
331 continue;
332 }
333 };
334
335 // Check if there is a doc comment after this one
336 //
337 // #[doc = "..."] #[doc = "..."]
338 // ^^^^^^^^^^^^^^ current ^ next?
339 match input.peek() {
340 Some(TokenTree::Punct(punct)) if *punct == '#' => {
341 // Yes, there is. Continue doc comment
342 }
343 _ => {
344 // The next token is not `#` so there are no more doc comments
345 doc_comment_progress = DocCommentProgress::Finished;
346 }
347 }
348
349 // #[doc = "..."]
350 // ^^^^^^^^^^^^^
351 let mut doc_comment_attribute_inner = doc_comment_square_brackets.stream().into_iter();
352
353 // #[doc = "..."]
354 // ^^^
355 let kw_doc_span = match doc_comment_attribute_inner.next() {
356 Some(TokenTree::Ident(kw_doc)) if kw_doc.to_string() == "doc" => kw_doc.span(),
357 Some(tt) => {
358 compile_error(tt.span(), "expected `doc`");
359 continue;
360 }
361 None => {
362 compile_error(
363 doc_comment_square_brackets.span_open(),
364 "expected `doc` after `[`",
365 );
366 continue;
367 }
368 };
369
370 // #[doc = "..."]
371 // ^
372 let punct_eq_span = match doc_comment_attribute_inner.next() {
373 Some(TokenTree::Punct(eq)) if eq == '=' => eq.span(),
374 Some(tt) => {
375 compile_error(tt.span(), "expected `=`");
376 continue;
377 }
378 None => {
379 compile_error(kw_doc_span, "expected `=` after `doc`");
380 continue;
381 }
382 };
383
384 // #[doc = "..."]
385 // ^^^^^
386 let next = doc_comment_attribute_inner.next();
387 let Some(tt) = next else {
388 compile_error(punct_eq_span, "expected string literal after `=`");
389 continue;
390 };
391 let span = tt.span();
392
393 // #[doc = "..."]
394 // ^^^
395 let Ok(litrs::Literal::String(literal)) = litrs::Literal::try_from(tt) else {
396 compile_error(
397 span,
398 "only string \"...\" or r\"...\" literals are supported",
399 );
400 continue;
401 };
402
403 let literal = literal.value();
404
405 // Reached contents of the doc comment
406 //
407 // let's remove leading space
408 //
409 // /// foo bar
410 //
411 // this expands to:
412 //
413 // #[doc = " foo bar"]
414 // ^ remove this space from the actual output
415 //
416 // We usually always have a space after the comment token,
417 // since it looks good. And e.g. Rustdoc ignores it as well.
418 let literal = literal.strip_prefix(' ').unwrap_or(literal);
419
420 doc_comments.push(literal.to_string());
421 }
422
423 if doc_comments.is_empty() {
424 compile_error(
425 Span::call_site(),
426 "requires at least a documentation comment argument: `/// ...`",
427 );
428 }
429
430 // The fully constructed string literal that we output
431 //
432 // docstr!(
433 // /// foo
434 // /// bar
435 // )
436 //
437 // becomes this:
438 //
439 // "foo\nbar"
440 let string = doc_comments
441 .into_iter()
442 .reduce(|mut acc, s| {
443 acc.push('\n');
444 acc.push_str(&s);
445 acc
446 })
447 .unwrap_or_default();
448
449 let Some(macro_) = macro_path else {
450 if !tokens_before_doc_comments.is_empty() || !tokens_after_doc_comments.is_empty() {
451 compile_error(
452 Span::call_site(),
453 concat!(
454 "expected macro input to only contain doc comments: `/// ...`, ",
455 "because you haven't supplied a macro path as the 1st argument"
456 ),
457 );
458 }
459
460 if !compile_errors.is_empty() {
461 return compile_errors;
462 }
463
464 // Just a plain string literal
465 return TokenTree::Literal(Literal::string(&string)).into();
466 };
467
468 if !compile_errors.is_empty() {
469 return compile_errors;
470 }
471
472 // The following:
473 //
474 // let a = docstr!(format!
475 // hello
476 // /// foo
477 // /// bar
478 // a,
479 // b
480 // );
481 //
482 // Expands into this:
483 //
484 // let a = format!(hello, "foo\nbar", a, b);
485 TokenStream::from_iter(
486 // format!(hello, "foo\nbar", a, b)
487 // ^^^^^^^
488 macro_.into_iter().chain([TokenTree::Group(Group::new(
489 // format!(hello, "foo\nbar", a, b)
490 // ^ ^
491 Delimiter::Parenthesis,
492 // format!(hello, "foo\nbar", a, b)
493 // ^^^^^^^^^^^^^^^^^^^^^^^
494 TokenStream::from_iter(
495 // format!(hello, "foo\nbar", a, b)
496 // ^^^^^^
497 tokens_before_doc_comments
498 .into_iter()
499 .chain([
500 // format!(hello, "foo\nbar", a, b)
501 // ^^^^^^^^^^
502 TokenTree::Literal(Literal::string(&string)),
503 // format!(hello, "foo\nbar", a, b)
504 // ^
505 TokenTree::Punct(Punct::new(',', Spacing::Joint)),
506 ])
507 // format!(hello, "foo\nbar", a, b)
508 // ^^^^
509 .chain(tokens_after_doc_comments),
510 ),
511 ))]),
512 )
513}
514
515/// Extracts path to macro, if one exists
516///
517/// ```ignore
518/// docstr!(::std::format!
519/// ^^^^^^^^^^^^^^
520/// /// ...
521/// )
522/// ```
523fn extract_macro_path(
524 input: &mut std::iter::Peekable<proc_macro::token_stream::IntoIter>,
525) -> Result<Option<TokenStream>, CompileError> {
526 let mut macro_path = TokenStream::new();
527
528 enum PreviousMacroPathToken {
529 PathSeparator,
530 Ident,
531 }
532
533 // Tracked for better error messages
534 let mut previous_macro_path_token = None;
535
536 macro_rules! invalid_macro_path {
537 () => {
538 CompileError::new(
539 macro_path
540 .into_iter()
541 .next()
542 .map(|tt| tt.span())
543 .unwrap_or_else(Span::call_site),
544 "invalid macro path",
545 )
546 };
547 }
548
549 // on the first compile error we stop trying to process the path because it won't
550 // make any sense after that
551 loop {
552 let tt = input.next();
553 match tt {
554 // Reached end of macro
555 //
556 // std::format!
557 // ^
558 Some(TokenTree::Punct(exclamation)) if exclamation == '!' => {
559 macro_path.extend([TokenTree::Punct(exclamation)]);
560 break;
561 }
562 // std::format!
563 // ^^
564 Some(TokenTree::Punct(colon)) if colon == ':' => {
565 match previous_macro_path_token {
566 Some(PreviousMacroPathToken::Ident) | None => {
567 previous_macro_path_token = Some(PreviousMacroPathToken::PathSeparator);
568 }
569 Some(PreviousMacroPathToken::PathSeparator) => {
570 return Err(invalid_macro_path!());
571 }
572 }
573
574 macro_path.extend([TokenTree::Punct(colon)]);
575
576 match input.next() {
577 // std::format!
578 // ^
579 Some(TokenTree::Punct(colon)) if colon == ':' => {
580 macro_path.extend([TokenTree::Punct(colon)]);
581 }
582 _ => {
583 return Err(invalid_macro_path!());
584 }
585 }
586 }
587 // std::format!
588 // ^^^
589 // ^^^^^^
590 Some(TokenTree::Ident(ident)) => match previous_macro_path_token {
591 Some(PreviousMacroPathToken::PathSeparator) | None => {
592 macro_path.extend([TokenTree::Ident(ident)]);
593 previous_macro_path_token = Some(PreviousMacroPathToken::Ident);
594 }
595 Some(PreviousMacroPathToken::Ident) => {
596 return Err(invalid_macro_path!());
597 }
598 },
599 _ if !macro_path.is_empty() => {
600 let macro_path_display = macro_path.to_string();
601 let last_token = macro_path.into_iter().last().expect("!.is_empty()");
602 return Err(CompileError::new(
603 last_token.span(),
604 format!("macro path must be followed by `!`, try: `{macro_path_display}!`"),
605 ));
606 }
607 _ => {
608 return Err(CompileError::new(
609 tt.map(|tt| tt.span()).unwrap_or_else(Span::call_site),
610 "unexpected token",
611 ));
612 }
613 }
614 }
615
616 Ok(Some(macro_path))
617}
618
619/// `.into_iter()` generates `compile_error!($message)` at `$span`
620struct CompileError {
621 /// Where the compile error is generates
622 pub span: Span,
623 /// Message of the compile error
624 pub message: String,
625}
626
627impl From<CompileError> for TokenStream {
628 fn from(value: CompileError) -> Self {
629 value.into_iter().collect()
630 }
631}
632
633impl CompileError {
634 /// Create a new compile error
635 pub fn new(span: Span, message: impl AsRef<str>) -> Self {
636 Self {
637 span,
638 message: message.as_ref().to_string(),
639 }
640 }
641}
642
643impl IntoIterator for CompileError {
644 type Item = TokenTree;
645 type IntoIter = std::array::IntoIter<Self::Item, 3>;
646
647 fn into_iter(self) -> Self::IntoIter {
648 [
649 TokenTree::Ident(Ident::new("compile_error", self.span)),
650 TokenTree::Punct({
651 let mut punct = Punct::new('!', Spacing::Alone);
652 punct.set_span(self.span);
653 punct
654 }),
655 TokenTree::Group({
656 let mut group = Group::new(Delimiter::Brace, {
657 TokenStream::from_iter(vec![TokenTree::Literal({
658 let mut string = Literal::string(&self.message);
659 string.set_span(self.span);
660 string
661 })])
662 });
663 group.set_span(self.span);
664 group
665 }),
666 ]
667 .into_iter()
668 }
669}