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}