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