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, Ident, Span, TokenStream, TokenTree};
18use quote::quote;
19use std::{
20 env, fs,
21 io::{self, BufRead},
22 path::PathBuf,
23 sync::atomic::{AtomicU64, Ordering},
24};
25use syn::{
26 parse::{Parse, ParseStream},
27 parse2,
28 spanned::Spanned,
29 LitStr, Meta, Token,
30};
31
32static INCLUDE_COUNTER: AtomicU64 = AtomicU64::new(0);
33
34/// Include code from within a source block in an AsciiDoc file.
35///
36/// All AsciiDoc [source blocks](https://docs.asciidoctor.org/asciidoc/latest/verbatim/source-blocks/)
37/// with delimited [listing blocks](https://docs.asciidoctor.org/asciidoc/latest/verbatim/listing-blocks/) are supported.
38///
39/// # Arguments
40///
41/// * `path` (*Required*) Path relative to the crate root directory.
42/// * `name` (*Required*) Name of the code fence to include.
43/// * `scope` Include the snippet in braces `{ .. }`.
44/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
45///
46/// # Examples
47///
48/// Consider the following source block in a crate `README.adoc` AsciiDoc file:
49///
50/// ```asciidoc
51/// [,rust,id="example"]
52/// ----
53/// let m = example()?;
54/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
55/// ----
56/// ```
57///
58/// We can include this code block in our Rust tests:
59///
60/// ```no_run
61/// struct Model {
62/// name: String,
63/// }
64///
65/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
66/// Ok(Model { name: "example".into() })
67/// }
68///
69/// #[test]
70/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
71/// include_asciidoc!("README.adoc", "example");
72/// Ok(())
73/// }
74/// ```
75#[cfg(feature = "asciidoc")]
76#[proc_macro]
77pub fn include_asciidoc(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
78 asciidoc::include_asciidoc(item.into())
79 .unwrap_or_else(syn::Error::into_compile_error)
80 .into()
81}
82
83/// Include code from within a code fence in a Markdown file.
84///
85/// All CommonMark [code fences](https://spec.commonmark.org/current/#fenced-code-blocks) are supported.
86///
87/// # Arguments
88///
89/// * `path` (*Required*) Path relative to the crate root directory.
90/// * `name` (*Required*) Name of the code fence to include.
91/// * `scope` Include the snippet in braces `{ .. }`.
92/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
93///
94/// # Examples
95///
96/// Consider the following code fence in a crate `README.md` Markdown file:
97///
98/// ````markdown
99/// ```rust example
100/// let m = example()?;
101/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
102/// ```
103/// ````
104///
105/// In Rust documentation comments, we can use `# line` to hide setup code.
106/// That's not possible in Markdown, so we can include only the code we want to demonstrate;
107/// however, we can still compile and even run it in Rust tests:
108///
109/// ```no_run
110/// struct Model {
111/// name: String,
112/// }
113///
114/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
115/// Ok(Model { name: "example".into() })
116/// }
117///
118/// #[test]
119/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
120/// include_markdown!("README.md", "example");
121/// Ok(())
122/// }
123/// ```
124#[proc_macro]
125pub fn include_markdown(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
126 markdown::include_markdown(item.into())
127 .unwrap_or_else(syn::Error::into_compile_error)
128 .into()
129}
130
131/// Include code from within a code block in a Textile file.
132///
133/// All Textile [code blocks](https://textile-lang.com/doc/block-code) are supported.
134///
135/// # Arguments
136///
137/// * `path` (*Required*) Path relative to the crate root directory.
138/// * `name` (*Required*) Name of the code fence to include.
139/// * `scope` Include the snippet in braces `{ .. }`.
140/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
141///
142/// # Examples
143///
144/// Consider the following code block in a crate `README.textile` Textile file:
145///
146/// ```textile
147/// bc(rust#example). let m = example()?;
148/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
149/// ```
150///
151/// In Rust documentation comments, we can use `# line` to hide setup code.
152/// That's not possible in Textile, so we can include only the code we want to demonstrate;
153/// however, we can still compile and even run it in Rust tests:
154///
155/// ```no_run
156/// struct Model {
157/// name: String,
158/// }
159///
160/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
161/// Ok(Model { name: "example".into() })
162/// }
163///
164/// #[test]
165/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
166/// include_textile!("README.textile", "example");
167/// Ok(())
168/// }
169/// ```
170#[cfg(feature = "textile")]
171#[proc_macro]
172pub fn include_textile(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
173 textile::include_textile(item.into())
174 .unwrap_or_else(syn::Error::into_compile_error)
175 .into()
176}
177
178/// Include code from within a source block in an Org file.
179///
180/// All Org [source code blocks](https://orgmode.org/manual/Structure-of-Code-Blocks.html) are supported.
181///
182/// # Arguments
183///
184/// * `path` (*Required*) Path relative to the crate root directory.
185/// * `name` (*Required*) Name of the code fence to include.
186/// * `scope` Include the snippet in braces `{ .. }`.
187/// * `relative` (*Requires rustc 1.88 or newer*) Path is relative to the source file calling the macro.
188///
189/// # Examples
190///
191/// Consider the following source block in a crate `README.org` Org file:
192///
193/// ```org
194/// #+NAME: example
195/// #+BEGIN_SRC rust
196/// let m = example()?;
197/// assert_eq!(format!("{m:?}"), r#"Model { name: "example" }"#);
198/// #+END_SRC
199/// ```
200///
201/// In Rust documentation comments, we can use `# line` to hide setup code.
202/// That's not possible in Org, so we can include only the code we want to demonstrate;
203/// however, we can still compile and even run it in Rust tests:
204///
205/// ```no_run
206/// struct Model {
207/// name: String,
208/// }
209///
210/// fn example() -> Result<Model, Box<dyn std::error::Error>> {
211/// Ok(Model { name: "example".into() })
212/// }
213///
214/// #[test]
215/// fn test_example() -> Result<(), Box<dyn std::error::Error>> {
216/// include_org!("README.org", "example");
217/// Ok(())
218/// }
219/// ```
220#[cfg(feature = "org")]
221#[proc_macro]
222pub fn include_org(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
223 org::include_org(item.into())
224 .unwrap_or_else(syn::Error::into_compile_error)
225 .into()
226}
227
228struct MarkdownArgs {
229 path: LitStr,
230 name: LitStr,
231 scope: Option<Span>,
232 relative: Option<Span>,
233}
234
235impl Parse for MarkdownArgs {
236 fn parse(input: ParseStream) -> syn::Result<Self> {
237 const REQ_PARAMS: &str = r#"missing required string parameters ("path", "name")"#;
238
239 let path = input
240 .parse()
241 .map_err(|err| syn::Error::new(err.span(), REQ_PARAMS))?;
242 input.parse::<Token![,]>()?;
243 let name = input
244 .parse()
245 .map_err(|err| syn::Error::new(err.span(), REQ_PARAMS))?;
246
247 let mut scope = None;
248 let mut relative = None;
249
250 if input.parse::<Token![,]>().is_ok() {
251 let params = input.parse_terminated(Meta::parse, Token![,])?;
252 for param in params {
253 if param.path().is_ident("scope") {
254 scope = Some(param.span());
255 } else if param.path().is_ident("relative") {
256 relative = Some(param.span());
257 } else {
258 return Err(syn::Error::new(param.span(), "unsupported parameter"));
259 }
260 }
261 } else if !input.is_empty() {
262 return Err(syn::Error::new(input.span(), "unexpected token"));
263 }
264
265 Ok(Self {
266 path,
267 name,
268 scope,
269 relative,
270 })
271 }
272}
273
274fn include_file<F>(item: TokenStream, f: F) -> syn::Result<TokenStream>
275where
276 F: FnOnce(&str, io::Lines<io::BufReader<fs::File>>) -> io::Result<(u32, Vec<String>)>,
277{
278 let args: MarkdownArgs = parse2(item)?;
279 let root = match args.relative {
280 #[cfg(span_locations)]
281 Some(span) => span.local_file(),
282 #[cfg(not(span_locations))]
283 Some(span) => return Err(syn::Error::new(span, "requires rustc 1.88 or newer")),
284 None => None,
285 };
286 let (file, display_path) =
287 open(root, &args.path.value()).map_err(|err| syn::Error::new(args.path.span(), err))?;
288 let (start_line, content) = extract(file, &args.name.value(), f)
289 .map_err(|err| syn::Error::new(args.name.span(), err))?;
290
291 let n = INCLUDE_COUNTER.fetch_add(1, Ordering::Relaxed);
292 let guard_type = Ident::new(&format!("__IncludeFileGuard{n}"), Span::call_site());
293 let guard_var = Ident::new(&format!("__include_file_guard{n}"), Span::call_site());
294
295 // Compute the file expression for the guard based on whether `relative` was passed.
296 // Use Location::caller().file() to resolve paths consistently with panic messages.
297 let file_expr: TokenStream = if args.relative.is_some() {
298 // Path is relative to the source file.
299 // Resolve against caller's directory and normalize.
300 let path_str = args.path.value();
301 quote! {
302 {
303 let __caller = ::std::panic::Location::caller().file();
304 let __caller_dir = ::std::path::Path::new(__caller)
305 .parent()
306 .unwrap_or(::std::path::Path::new(""));
307 let __resolved = __caller_dir.join(#path_str);
308 let mut __parts: ::std::vec::Vec<::std::path::Component<'_>> =
309 ::std::vec::Vec::new();
310 for __c in __resolved.components() {
311 match __c {
312 ::std::path::Component::ParentDir => { __parts.pop(); }
313 ::std::path::Component::CurDir => {}
314 _ => __parts.push(__c),
315 }
316 }
317 let __normalized: ::std::path::PathBuf = __parts.iter().collect();
318 __normalized.to_string_lossy().into_owned()
319 }
320 }
321 } else {
322 // Path is relative to CARGO_MANIFEST_DIR.
323 // Find the crate-relative portion of the caller path by testing
324 // progressively shorter suffixes against CARGO_MANIFEST_DIR. The
325 // prefix that remains is whatever the compiler prepended (e.g., a
326 // workspace-relative directory), which we prepend to display_path
327 // so the reported path matches what panic messages use.
328 let path_str = &display_path;
329 quote! {
330 {
331 let __caller = ::std::panic::Location::caller().file();
332 let __manifest = ::std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
333 let __path: &str = #path_str;
334 let __caller_path = ::std::path::Path::new(__caller);
335 let __components: ::std::vec::Vec<::std::path::Component<'_>> =
336 __caller_path.components().collect();
337 let mut __prefix_len = 0usize;
338 for __skip in 0..__components.len() {
339 let __suffix: ::std::path::PathBuf =
340 __components[__skip..].iter().collect();
341 if __manifest.join(&__suffix).is_file() {
342 __prefix_len = __skip;
343 break;
344 }
345 }
346 if __prefix_len == 0 {
347 ::std::string::String::from(__path)
348 } else {
349 let __prefix: ::std::path::PathBuf =
350 __components[..__prefix_len].iter().collect();
351 __prefix.join(__path).to_string_lossy().into_owned()
352 }
353 }
354 }
355 };
356
357 let guard = quote! {
358 struct #guard_type {
359 file: ::std::string::String,
360 line: u32,
361 }
362 impl ::std::ops::Drop for #guard_type {
363 fn drop(&mut self) {
364 if ::std::thread::panicking() {
365 ::std::eprintln!(
366 "note: panicked in code included from {}:{}",
367 self.file,
368 self.line
369 );
370 }
371 }
372 }
373 let #guard_var = #guard_type {
374 file: #file_expr,
375 line: #start_line,
376 };
377 };
378
379 let body: TokenStream = content.parse()?;
380 let mut output = guard;
381 output.extend(body);
382 // Explicitly drop the guard right after the included body so that, when
383 // multiple macros are used in the same scope, prior guards are already gone
384 // before the next snippet starts. Without this, a panic in snippet N would
385 // unwind all N guards and print N "note:" lines instead of one.
386 output.extend(quote! { ::std::mem::drop(#guard_var); });
387
388 if args.scope.is_some() {
389 output = TokenTree::Group(Group::new(Delimiter::Brace, output)).into();
390 }
391
392 Ok(output)
393}
394
395fn open(root: Option<PathBuf>, path: &str) -> io::Result<(fs::File, String)> {
396 let manifest_dir: PathBuf = env::var("CARGO_MANIFEST_DIR")
397 .map_err(|_| io::Error::other("no manifest directory"))?
398 .into();
399 let root_dir = match root {
400 Some(ref src) => src
401 .parent()
402 .map(|dir| manifest_dir.join(dir))
403 .ok_or_else(|| io::Error::other("no source parent directory"))?,
404 None => manifest_dir.clone(),
405 };
406 let full_path = root_dir.join(path);
407 let file = fs::File::open(&full_path)?;
408 let display_path = {
409 // Canonicalize to resolve any `..` components; fall back to the
410 // unresolved paths if canonicalization fails (e.g., a race with
411 // deletion), which is acceptable since we already opened the file.
412 let canonical_full = fs::canonicalize(&full_path).unwrap_or_else(|_| full_path.clone());
413 let canonical_manifest =
414 fs::canonicalize(&manifest_dir).unwrap_or_else(|_| manifest_dir.clone());
415 let rel = canonical_full
416 .strip_prefix(&canonical_manifest)
417 .unwrap_or(std::path::Path::new(path));
418 rel.to_string_lossy().into_owned()
419 };
420 Ok((file, display_path))
421}
422
423fn extract<R, F>(buffer: R, name: &str, f: F) -> io::Result<(u32, String)>
424where
425 R: io::Read,
426 F: FnOnce(&str, io::Lines<io::BufReader<R>>) -> io::Result<(u32, Vec<String>)>,
427{
428 let reader = io::BufReader::new(buffer);
429 let (start_line, lines) = f(name, reader.lines())?;
430 if lines.is_empty() {
431 return Err(io::Error::new(
432 io::ErrorKind::NotFound,
433 format!("code fence '{}' not found", name),
434 ));
435 }
436
437 Ok((start_line, lines.join("\n")))
438}