include_utils_macro/
lib.rs1#![allow(missing_docs)]
4
5use std::path::{Path, PathBuf};
6
7use itertools::Itertools;
8use manyhow::manyhow;
9use proc_macro2::TokenStream as TokenStream2;
10use quote::quote;
11use syn::LitStr;
12
13use crate::include_location::{IncludeLocation, IncludeRange};
14
15mod include_location;
16
17#[manyhow]
18#[proc_macro]
19pub fn include_str_part(file: LitStr) -> manyhow::Result<TokenStream2> {
20 let file = file.value();
21 let location = IncludeLocation::parse(&file)?;
22 let file_content = read_file(location.path)?;
23
24 let processed_content = process_file(file_content, &location.range, |_content, _name| {
25 Err(manyhow::error_message!("Anchors is not supported for the plain string").into())
26 })?;
27
28 Ok(quote!(#processed_content))
29}
30
31#[manyhow]
32#[proc_macro]
33pub fn include_md(file: LitStr) -> manyhow::Result<TokenStream2> {
34 let file = file.value();
35 let location = IncludeLocation::parse(&file)?;
36 let file_content = read_file(location.path)?;
37
38 let processed_content = process_file(file_content, &location.range, |content, anchor_name| {
40 let anchor_begin = format!("<!-- ANCHOR: {anchor_name}");
41 let anchor_end = format!("<!-- ANCHOR_END: {anchor_name}");
42
43 let mut has_anchor = false;
44
45 let output = content
46 .lines()
47 .skip_while(|line| !line.starts_with(&anchor_begin))
48 .skip(1)
49 .take_while(|line| {
50 let is_end = line.starts_with(&anchor_end);
51 if is_end {
52 has_anchor = true;
53 }
54 !is_end
55 })
56 .join("\n");
57
58 manyhow::ensure!(
59 has_anchor,
60 "Included file doesn't contain anchor with name: {anchor_name}"
61 );
62 Ok(output)
63 })?;
64
65 Ok(quote!(#processed_content))
66}
67
68fn cargo_manifest_dir() -> manyhow::Result<PathBuf> {
69 let dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|err| {
70 manyhow::error_message!("Unable to read `CARGO_MANIFEST_DIR` environment variable. {err}")
71 })?;
72 Ok(dir.into())
73}
74
75fn search_file(file_path: &Path) -> manyhow::Result<PathBuf> {
78 let manifest_dir = cargo_manifest_dir()?;
79
80 let search_paths = [
81 manifest_dir.clone(),
82 #[cfg(feature = "workspace")]
84 {
85 let metadata = cargo_metadata::MetadataCommand::new()
86 .manifest_path(manifest_dir.join("Cargo.toml"))
87 .current_dir(manifest_dir)
88 .no_deps()
89 .exec()
90 .map_err(|err| {
91 manyhow::error_message!("Unable to execute cargo metadata command. {err}")
92 })?;
93
94 metadata.workspace_root.into_std_path_buf()
95 },
96 ];
97
98 for search_path in search_paths {
99 let full_path = search_path.join(file_path);
100
101 if full_path.exists() {
102 return full_path.canonicalize().map_err(|err| {
103 manyhow::error_message!("Unable to canonicalize file path. {err}").into()
104 });
105 }
106 }
107
108 Err(manyhow::error_message!("File path `{}` not found", file_path.display()).into())
109}
110
111fn read_file(file_path: impl AsRef<Path>) -> manyhow::Result<String> {
112 let full_path = {
113 let file_path = file_path.as_ref();
114 if file_path.is_absolute() {
115 file_path.to_owned()
116 } else {
117 search_file(file_path)?
118 }
119 };
120
121 std::fs::read_to_string(full_path)
122 .map_err(|err| manyhow::error_message!("Unable to read file. {err}").into())
123}
124
125fn process_file<F: FnOnce(String, &str) -> manyhow::Result<String>>(
126 content: String,
127 range: &IncludeRange<'_>,
128 anchor_processor: F,
129) -> manyhow::Result<String> {
130 let content = match range {
131 IncludeRange::Full => content,
133 IncludeRange::Range { from, to } => {
134 let from = from.unwrap_or_default().saturating_sub(1);
136 let mut lines = content.lines().skip(from);
138 if let Some(to) = to {
139 let n = to - from;
142 lines.take(n).join("\n")
143 } else {
144 lines.join("\n")
146 }
147 }
148 IncludeRange::Anchor { name } => anchor_processor(content, name)?,
149 };
150
151 Ok(content.trim().to_owned())
152}