include_file/lib.rs
1// Copyright 2025 Heath Stewart.
2// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3
4#![doc = include_str!("../README.md")]
5
6mod asciidoc;
7mod markdown;
8mod org;
9#[cfg(test)]
10mod tests;
11mod textile;
12
13use proc_macro2::{Span, TokenStream};
14use std::{
15 fmt, fs,
16 io::{self, BufRead},
17 path::PathBuf,
18};
19use syn::{
20 parse::{Parse, ParseStream},
21 parse2, LitStr, Token,
22};
23
24/// Include code from within a source block in an AsciiDoc file.
25///
26/// Two arguments are required: a file path relative to the current source file,
27/// and an id defined within the source block attributes as shown below.
28///
29/// All AsciiDoc [source blocks](https://docs.asciidoctor.org/asciidoc/latest/verbatim/source-blocks/)
30/// with delimited [listing blocks](https://docs.asciidoctor.org/asciidoc/latest/verbatim/listing-blocks/) are supported.
31///
32/// # Examples
33///
34/// Consider the following source block in a crate `README.adoc` AsciiDoc file:
35///
36/// ```asciidoc
37/// [,rust,id="example"]
38/// ----
39/// let m = example()?;
40/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
41/// ----
42/// ```
43///
44/// We can include this code block in our Rust tests:
45///
46/// ```no_run
47/// struct Model {
48/// name: String,
49/// }
50///
51/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
52/// Ok(Model { name: "example".into() })
53/// }
54///
55/// #[test]
56/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
57/// include_asciidoc!("../README.adoc", "example");
58/// Ok(())
59/// }
60/// ```
61#[proc_macro]
62pub fn include_asciidoc(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
63 asciidoc::include_asciidoc(item.into())
64 .unwrap_or_else(syn::Error::into_compile_error)
65 .into()
66}
67
68/// Include code from within a code fence in a Markdown file.
69///
70/// Two arguments are required: a file path relative to the current source file,
71/// and a name defined within the code fence as shown below.
72///
73/// All CommonMark [code fences](https://spec.commonmark.org/current/#fenced-code-blocks) are supported.
74///
75/// # Examples
76///
77/// Consider the following code fence in a crate `README.md` Markdown file:
78///
79/// ````markdown
80/// ```rust example
81/// let m = example()?;
82/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
83/// ```
84/// ````
85///
86/// In Rust documentation comments, we can use `# line` to hide setup code.
87/// That's not possible in Markdown, so we can include only the code we want to demonstrate;
88/// however, we can still compile and even run it in Rust tests:
89///
90/// ```no_run
91/// struct Model {
92/// name: String,
93/// }
94///
95/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
96/// Ok(Model { name: "example".into() })
97/// }
98///
99/// #[test]
100/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
101/// include_markdown!("../README.md", "example");
102/// Ok(())
103/// }
104/// ```
105#[proc_macro]
106pub fn include_markdown(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
107 markdown::include_markdown(item.into())
108 .unwrap_or_else(syn::Error::into_compile_error)
109 .into()
110}
111
112/// Include code from within a code block in a Textile file.
113///
114/// Two arguments are required: a file path relative to the current source file,
115/// and an id defined within the code block as shown below.
116///
117/// All Textile [code blocks](https://textile-lang.com/doc/block-code) are supported.
118///
119/// # Examples
120///
121/// Consider the following code block in a crate `README.textile` Textile file:
122///
123/// ```textile
124/// bc(rust#example). let m = example()?;
125/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
126/// ```
127///
128/// In Rust documentation comments, we can use `# line` to hide setup code.
129/// That's not possible in Textile, so we can include only the code we want to demonstrate;
130/// however, we can still compile and even run it in Rust tests:
131///
132/// ```no_run
133/// struct Model {
134/// name: String,
135/// }
136///
137/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
138/// Ok(Model { name: "example".into() })
139/// }
140///
141/// #[test]
142/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
143/// include_textile!("../README.textile", "example");
144/// Ok(())
145/// }
146/// ```
147#[proc_macro]
148pub fn include_textile(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
149 textile::include_textile(item.into())
150 .unwrap_or_else(syn::Error::into_compile_error)
151 .into()
152}
153
154/// Include code from within a source block in an Org file.
155///
156/// Two arguments are required: a file path relative to the current source file,
157/// and a name defined with `#+NAME:` immediately before the source block as shown below.
158///
159/// All Org [source code blocks](https://orgmode.org/manual/Structure-of-Code-Blocks.html) are supported.
160///
161/// # Examples
162///
163/// Consider the following source block in a crate `README.org` Org file:
164///
165/// ```org
166/// #+NAME: example
167/// #+BEGIN_SRC rust
168/// let m = example()?;
169/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
170/// #+END_SRC
171/// ```
172///
173/// In Rust documentation comments, we can use `# line` to hide setup code.
174/// That's not possible in Org, so we can include only the code we want to demonstrate;
175/// however, we can still compile and even run it in Rust tests:
176///
177/// ```no_run
178/// struct Model {
179/// name: String,
180/// }
181///
182/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
183/// Ok(Model { name: "example".into() })
184/// }
185///
186/// #[test]
187/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
188/// include_org!("../README.org", "example");
189/// Ok(())
190/// }
191/// ```
192#[proc_macro]
193pub fn include_org(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
194 org::include_org(item.into())
195 .unwrap_or_else(syn::Error::into_compile_error)
196 .into()
197}
198
199struct MarkdownArgs {
200 path: LitStr,
201 name: LitStr,
202}
203
204impl fmt::Debug for MarkdownArgs {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.debug_struct("MarkdownArgs")
207 .field("path", &self.path.value())
208 .field("name", &self.name.value())
209 .finish()
210 }
211}
212
213impl Parse for MarkdownArgs {
214 fn parse(input: ParseStream) -> syn::Result<Self> {
215 let path = input.parse()?;
216 input.parse::<Token![,]>()?;
217 let name = input.parse()?;
218 Ok(Self { path, name })
219 }
220}
221
222fn include_file<F>(item: TokenStream, f: F) -> syn::Result<TokenStream>
223where
224 F: FnOnce(&str, io::Lines<io::BufReader<fs::File>>) -> io::Result<Vec<String>>,
225{
226 let args: MarkdownArgs = parse2(item).map_err(|_| {
227 syn::Error::new(
228 Span::call_site(),
229 "expected (path, name) literal string arguments",
230 )
231 })?;
232 let file = open(&args.path.value()).map_err(|err| syn::Error::new(args.path.span(), err))?;
233 let content = extract(file, &args.name.value(), f)
234 .map_err(|err| syn::Error::new(args.name.span(), err))?;
235
236 Ok(content.parse()?)
237}
238
239fn open(path: &str) -> io::Result<fs::File> {
240 let manifest_dir: PathBuf = option_env!("CARGO_MANIFEST_DIR")
241 .ok_or_else(|| io::Error::other("no manifest directory"))?
242 .into();
243 let path = manifest_dir.join(path);
244 fs::File::open(path)
245}
246
247fn extract<R, F>(buffer: R, name: &str, f: F) -> io::Result<String>
248where
249 R: io::Read,
250 F: FnOnce(&str, io::Lines<io::BufReader<R>>) -> io::Result<Vec<String>>,
251{
252 let reader = io::BufReader::new(buffer);
253 let lines = f(name, reader.lines())?;
254 if lines.is_empty() {
255 return Err(io::Error::new(
256 io::ErrorKind::NotFound,
257 format!("code fence '{}' not found", name),
258 ));
259 }
260
261 Ok(lines.join("\n"))
262}