1#![doc(html_root_url = "https://docs.rs/toad-macros/0.2.0")]
4#![cfg_attr(all(not(test), feature = "no_std"), no_std)]
5#![cfg_attr(not(test), forbid(missing_debug_implementations, unreachable_pub))]
6#![cfg_attr(not(test), deny(unsafe_code, missing_copy_implementations))]
7#![cfg_attr(any(docsrs, feature = "docs"), feature(doc_cfg))]
8#![deny(missing_docs)]
9
10use proc_macro::TokenStream;
11use quote::ToTokens;
12use regex::Regex;
13use syn::parse::Parse;
14use syn::{parse_macro_input, LitStr};
15
16struct DocSection(LitStr);
17
18impl Parse for DocSection {
19 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
20 Ok(Self(input.parse::<LitStr>()?))
21 }
22}
23
24const RFC7252: &str = include_str!("./rfc7252.txt");
25
26#[proc_macro]
46pub fn rfc_7252_doc(input: TokenStream) -> TokenStream {
47 let DocSection(section_literal) = parse_macro_input!(input as DocSection);
48
49 let sec = section_literal.value();
50 let docstring = gen_docstring(sec, RFC7252);
51
52 LitStr::new(&docstring, section_literal.span()).to_token_stream()
53 .into()
54}
55
56fn gen_docstring(sec: String, rfc: &'static str) -> String {
57 let section_rx =
59 Regex::new(format!(r"(?s)\n{}\.\s+(.*?)(\n\d|$)", sec.replace('.', "\\.")).as_str()).unwrap_or_else(|e| {
60 panic!("Section {} invalid: {:?}", sec, e)
61 });
62 let rfc_section = section_rx.captures_iter(rfc)
63 .next()
64 .unwrap_or_else(|| panic!("Section {} not found", sec))
65 .get(1)
66 .unwrap_or_else(|| panic!("Section {} is empty", sec))
67 .as_str();
68
69 let mut lines = trim_leading_ws(rfc_section);
70 let line1 = lines.drain(0..1)
71 .next()
72 .unwrap_or_else(|| panic!("Section {} is empty", sec));
73 let rest = lines.join("\n");
74
75 format!(
76 r"# {title}
77[_generated from RFC7252 section {section}_](https://datatracker.ietf.org/doc/html/rfc7252#section-{section})
78
79{body}",
80 title = line1,
81 section = sec,
82 body = rest
83 )
84}
85
86fn trim_leading_ws(text: &str) -> Vec<String> {
93 #[derive(Clone, Copy)]
94 enum TrimStart {
95 Yes,
96 InCodeFence,
97 }
98
99 let trim_start = Regex::new(r"^ +").unwrap();
100 let trim_indent = Regex::new(r"^ ").unwrap();
101
102 text.split('\n')
103 .fold((Vec::<String>::new(), TrimStart::Yes),
104 |(mut lines, strip), s| {
105 let trimmed = trim_start.replace(s, "").to_string();
106 let dedented = trim_indent.replace(s, "").to_string();
107
108 let is_fence = trimmed.starts_with("```");
109
110 match (is_fence, strip) {
111 | (false, TrimStart::Yes) => {
112 lines.push(trimmed);
113 (lines, strip)
114 },
115 | (false, TrimStart::InCodeFence) => {
116 lines.push(dedented);
117 (lines, strip)
118 },
119 | (true, TrimStart::Yes) => {
120 lines.push(trimmed);
121 (lines, TrimStart::InCodeFence)
122 },
123 | (true, TrimStart::InCodeFence) => {
124 lines.push(trimmed);
125 (lines, TrimStart::Yes)
126 },
127 }
128 })
129 .0
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn rfcdoc_works() {
138 let rfc = r"
139Table of Contents
140
141 1. Foo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
142 1.1. Bingus . . . . . . . . . . . . . . . . . . . . . . . . . . 2
143 1.2. Terminology . . . . . . . . . . . . . . . . . . . . . . . . 3
144 2. Bar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
145
1461. Foo
147 bar baz quux
148
149 ```text
150 dingus bar
151 foo
152 ```
1531.1. Bingus
154 lorem ipsum frisky gypsum
155
1561.2. Terminology
157 Bat: tool used for baseball
158 Code: if (name === 'Jerry') {throw new Error('get out jerry!!1');}
159
1602. Bar
161 bingus
162 o fart
163 o poo";
164 assert_eq!(
166 gen_docstring("1".into(), rfc),
167 r"# Foo
168[_generated from RFC7252 section 1_](https://datatracker.ietf.org/doc/html/rfc7252#section-1)
169
170bar baz quux
171
172```text
173dingus bar
174 foo
175```"
176 );
177
178 assert_eq!(
180 gen_docstring("2".into(), rfc),
181 r"# Bar
182[_generated from RFC7252 section 2_](https://datatracker.ietf.org/doc/html/rfc7252#section-2)
183
184bingus
185o fart
186o poo"
187 );
188 }
189}