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 .exec()
88 .map_err(|err| {
89 manyhow::error_message!("Unable to execute cargo metadata command. {err}")
90 })?;
91
92 metadata.workspace_root.into_std_path_buf()
93 },
94 ];
95
96 for search_path in search_paths {
97 let full_path = search_path.join(file_path);
98
99 if full_path.exists() {
100 return full_path.canonicalize().map_err(|err| {
101 manyhow::error_message!("Unable to canonicalize file path. {err}").into()
102 });
103 }
104 }
105
106 Err(manyhow::error_message!("File path `{}` not found", file_path.display()).into())
107}
108
109fn read_file(file_path: impl AsRef<Path>) -> manyhow::Result<String> {
110 let full_path = {
111 let file_path = file_path.as_ref();
112 if file_path.is_absolute() {
113 file_path.to_owned()
114 } else {
115 search_file(file_path)?
116 }
117 };
118
119 std::fs::read_to_string(full_path)
120 .map_err(|err| manyhow::error_message!("Unable to read file. {err}").into())
121}
122
123fn process_file<F: FnOnce(String, &str) -> manyhow::Result<String>>(
124 content: String,
125 range: &IncludeRange<'_>,
126 anchor_processor: F,
127) -> manyhow::Result<String> {
128 let content = match range {
129 IncludeRange::Full => content,
131 IncludeRange::Range { from, to } => {
132 let from = from.unwrap_or_default().saturating_sub(1);
134 let mut lines = content.lines().skip(from);
136 if let Some(to) = to {
137 let n = to - from;
140 lines.take(n).join("\n")
141 } else {
142 lines.join("\n")
144 }
145 }
146 IncludeRange::Anchor { name } => anchor_processor(content, name)?,
147 };
148
149 Ok(content.trim().to_owned())
150}