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::{Delimiter, Group, Span, TokenStream, TokenTree};
14use std::{
15    env, fs,
16    io::{self, BufRead},
17    path::PathBuf,
18};
19use syn::{
20    parse::{Parse, ParseStream},
21    parse2,
22    spanned::Spanned,
23    LitStr, Meta, Token,
24};
25
26/// Include code from within a source block in an AsciiDoc file.
27///
28/// All AsciiDoc [source blocks](https://docs.asciidoctor.org/asciidoc/latest/verbatim/source-blocks/)
29/// with delimited [listing blocks](https://docs.asciidoctor.org/asciidoc/latest/verbatim/listing-blocks/) are supported.
30///
31/// # Arguments
32///
33/// * `path` (*Required*) Path relative to the crate root directory.
34/// * `name` (*Required*) Name of the code fence to include.
35/// * `scope` Include the snippet in braces `{ .. }`.
36/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
37///
38/// # Examples
39///
40/// Consider the following source block in a crate `README.adoc` AsciiDoc file:
41///
42/// ```asciidoc
43/// [,rust,id="example"]
44/// ----
45/// let m = example()?;
46/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
47/// ----
48/// ```
49///
50/// We can include this code block in our Rust tests:
51///
52/// ```no_run
53/// struct Model {
54///     name: String,
55/// }
56///
57/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
58///     Ok(Model { name: "example".into() })
59/// }
60///
61/// #[test]
62/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
63///     include_asciidoc!("README.adoc", "example");
64///     Ok(())
65/// }
66/// ```
67#[proc_macro]
68pub fn include_asciidoc(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
69    asciidoc::include_asciidoc(item.into())
70        .unwrap_or_else(syn::Error::into_compile_error)
71        .into()
72}
73
74/// Include code from within a code fence in a Markdown file.
75///
76/// All CommonMark [code fences](https://spec.commonmark.org/current/#fenced-code-blocks) are supported.
77///
78/// # Arguments
79///
80/// * `path` (*Required*) Path relative to the crate root directory.
81/// * `name` (*Required*) Name of the code fence to include.
82/// * `scope` Include the snippet in braces `{ .. }`.
83/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
84///
85/// # Examples
86///
87/// Consider the following code fence in a crate `README.md` Markdown file:
88///
89/// ````markdown
90/// ```rust example
91/// let m = example()?;
92/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
93/// ```
94/// ````
95///
96/// In Rust documentation comments, we can use `# line` to hide setup code.
97/// That's not possible in Markdown, so we can include only the code we want to demonstrate;
98/// however, we can still compile and even run it in Rust tests:
99///
100/// ```no_run
101/// struct Model {
102///     name: String,
103/// }
104///
105/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
106///     Ok(Model { name: "example".into() })
107/// }
108///
109/// #[test]
110/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
111///     include_markdown!("README.md", "example");
112///     Ok(())
113/// }
114/// ```
115#[proc_macro]
116pub fn include_markdown(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
117    markdown::include_markdown(item.into())
118        .unwrap_or_else(syn::Error::into_compile_error)
119        .into()
120}
121
122/// Include code from within a code block in a Textile file.
123///
124/// All Textile [code blocks](https://textile-lang.com/doc/block-code) are supported.
125///
126/// # Arguments
127///
128/// * `path` (*Required*) Path relative to the crate root directory.
129/// * `name` (*Required*) Name of the code fence to include.
130/// * `scope` Include the snippet in braces `{ .. }`.
131/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
132///
133/// # Examples
134///
135/// Consider the following code block in a crate `README.textile` Textile file:
136///
137/// ```textile
138/// bc(rust#example). let m = example()?;
139/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
140/// ```
141///
142/// In Rust documentation comments, we can use `# line` to hide setup code.
143/// That's not possible in Textile, so we can include only the code we want to demonstrate;
144/// however, we can still compile and even run it in Rust tests:
145///
146/// ```no_run
147/// struct Model {
148///     name: String,
149/// }
150///
151/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
152///     Ok(Model { name: "example".into() })
153/// }
154///
155/// #[test]
156/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
157///     include_textile!("README.textile", "example");
158///     Ok(())
159/// }
160/// ```
161#[proc_macro]
162pub fn include_textile(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
163    textile::include_textile(item.into())
164        .unwrap_or_else(syn::Error::into_compile_error)
165        .into()
166}
167
168/// Include code from within a source block in an Org file.
169///
170/// All Org [source code blocks](https://orgmode.org/manual/Structure-of-Code-Blocks.html) are supported.
171///
172/// # Arguments
173///
174/// * `path` (*Required*) Path relative to the crate root directory.
175/// * `name` (*Required*) Name of the code fence to include.
176/// * `scope` Include the snippet in braces `{ .. }`.
177/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
178///
179/// # Examples
180///
181/// Consider the following source block in a crate `README.org` Org file:
182///
183/// ```org
184/// #+NAME: example
185/// #+BEGIN_SRC rust
186/// let m = example()?;
187/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
188/// #+END_SRC
189/// ```
190///
191/// In Rust documentation comments, we can use `# line` to hide setup code.
192/// That's not possible in Org, so we can include only the code we want to demonstrate;
193/// however, we can still compile and even run it in Rust tests:
194///
195/// ```no_run
196/// struct Model {
197///     name: String,
198/// }
199///
200/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
201///     Ok(Model { name: "example".into() })
202/// }
203///
204/// #[test]
205/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
206///     include_org!("README.org", "example");
207///     Ok(())
208/// }
209/// ```
210#[proc_macro]
211pub fn include_org(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
212    org::include_org(item.into())
213        .unwrap_or_else(syn::Error::into_compile_error)
214        .into()
215}
216
217struct MarkdownArgs {
218    path: LitStr,
219    name: LitStr,
220    scope: Option<Span>,
221    relative: Option<Span>,
222}
223
224impl Parse for MarkdownArgs {
225    fn parse(input: ParseStream) -> syn::Result<Self> {
226        const REQ_PARAMS: &str = r#"missing required string parameters ("path", "name")"#;
227
228        let path = input
229            .parse()
230            .map_err(|err| syn::Error::new(err.span(), REQ_PARAMS))?;
231        input.parse::<Token![,]>()?;
232        let name = input
233            .parse()
234            .map_err(|err| syn::Error::new(err.span(), REQ_PARAMS))?;
235
236        let mut scope = None;
237        let mut relative = None;
238
239        if input.parse::<Token![,]>().is_ok() {
240            let params = input.parse_terminated(Meta::parse, Token![,])?;
241            for param in params {
242                if param.path().is_ident("scope") {
243                    scope = Some(param.span());
244                } else if param.path().is_ident("relative") {
245                    relative = Some(param.span());
246                } else {
247                    return Err(syn::Error::new(param.span(), "unsupported parameter"));
248                }
249            }
250        } else if !input.is_empty() {
251            return Err(syn::Error::new(input.span(), "unexpected token"));
252        }
253
254        Ok(Self {
255            path,
256            name,
257            scope,
258            relative,
259        })
260    }
261}
262
263fn include_file<F>(item: TokenStream, f: F) -> syn::Result<TokenStream>
264where
265    F: FnOnce(&str, io::Lines<io::BufReader<fs::File>>) -> io::Result<Vec<String>>,
266{
267    let args: MarkdownArgs = parse2(item)?;
268    let root = match args.relative {
269        #[cfg(span_locations)]
270        Some(span) => span.local_file(),
271        #[cfg(not(span_locations))]
272        Some(span) => return Err(syn::Error::new(span, "requires rustc 1.88 or newer")),
273        None => None,
274    };
275    let file =
276        open(root, &args.path.value()).map_err(|err| syn::Error::new(args.path.span(), err))?;
277    let content = extract(file, &args.name.value(), f)
278        .map_err(|err| syn::Error::new(args.name.span(), err))?;
279
280    let mut content = content.parse()?;
281    if args.scope.is_some() {
282        content = TokenTree::Group(Group::new(Delimiter::Brace, content)).into();
283    }
284
285    Ok(content)
286}
287
288fn open(root: Option<PathBuf>, path: &str) -> io::Result<fs::File> {
289    let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR")
290        .map_err(|_| io::Error::other("no manifest directory"))?
291        .into();
292    let root = match root {
293        Some(path) => path
294            .parent()
295            .map(|dir| manifest_dir.join(dir))
296            .ok_or_else(|| io::Error::other("no source parent directory"))?,
297        None => manifest_dir,
298    };
299    let path = root.join(path);
300    fs::File::open(path)
301}
302
303fn extract<R, F>(buffer: R, name: &str, f: F) -> io::Result<String>
304where
305    R: io::Read,
306    F: FnOnce(&str, io::Lines<io::BufReader<R>>) -> io::Result<Vec<String>>,
307{
308    let reader = io::BufReader::new(buffer);
309    let lines = f(name, reader.lines())?;
310    if lines.is_empty() {
311        return Err(io::Error::new(
312            io::ErrorKind::NotFound,
313            format!("code fence '{}' not found", name),
314        ));
315    }
316
317    Ok(lines.join("\n"))
318}