include_utils_macro/
lib.rs#![allow(missing_docs)]
use std::path::{Path, PathBuf};
use itertools::Itertools;
use manyhow::manyhow;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::LitStr;
use crate::include_location::{IncludeLocation, IncludeRange};
mod include_location;
#[manyhow]
#[proc_macro]
pub fn include_str_part(file: LitStr) -> manyhow::Result<TokenStream2> {
let file = file.value();
let location = IncludeLocation::parse(&file)?;
let file_content = read_file(location.path)?;
let processed_content = process_file(file_content, &location.range, |_content, _name| {
Err(manyhow::error_message!("Anchors is not supported for the plain string").into())
})?;
Ok(quote!(#processed_content))
}
#[manyhow]
#[proc_macro]
pub fn include_md(file: LitStr) -> manyhow::Result<TokenStream2> {
let file = file.value();
let location = IncludeLocation::parse(&file)?;
let file_content = read_file(location.path)?;
let processed_content = process_file(file_content, &location.range, |content, anchor_name| {
let anchor_begin = format!("<!-- ANCHOR: {anchor_name}");
let anchor_end = format!("<!-- ANCHOR_END: {anchor_name}");
let mut has_anchor = false;
let output = content
.lines()
.skip_while(|line| !line.starts_with(&anchor_begin))
.skip(1)
.take_while(|line| {
let is_end = line.starts_with(&anchor_end);
if is_end {
has_anchor = true;
}
!is_end
})
.join("\n");
manyhow::ensure!(
has_anchor,
"Included file doesn't contain anchor with name: {anchor_name}"
);
Ok(output)
})?;
Ok(quote!(#processed_content))
}
fn cargo_manifest_dir() -> manyhow::Result<PathBuf> {
let dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|err| {
manyhow::error_message!("Unable to read `CARGO_MANIFEST_DIR` environment variable. {err}")
})?;
Ok(dir.into())
}
fn search_file(file_path: &Path) -> manyhow::Result<PathBuf> {
let manifest_dir = cargo_manifest_dir()?;
let search_paths = [
manifest_dir.clone(),
#[cfg(feature = "workspace")]
{
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(manifest_dir.join("Cargo.toml"))
.exec()
.map_err(|err| {
manyhow::error_message!("Unable to execute cargo metadata command. {err}")
})?;
metadata.workspace_root.into_std_path_buf()
},
];
for search_path in search_paths {
let full_path = search_path.join(file_path);
if full_path.exists() {
return full_path.canonicalize().map_err(|err| {
manyhow::error_message!("Unable to canonicalize file path. {err}").into()
});
}
}
Err(manyhow::error_message!("File path `{}` not found", file_path.display()).into())
}
fn read_file(file_path: impl AsRef<Path>) -> manyhow::Result<String> {
let full_path = {
let file_path = file_path.as_ref();
if file_path.is_absolute() {
file_path.to_owned()
} else {
search_file(file_path)?
}
};
std::fs::read_to_string(full_path)
.map_err(|err| manyhow::error_message!("Unable to read file. {err}").into())
}
fn process_file<F: FnOnce(String, &str) -> manyhow::Result<String>>(
content: String,
range: &IncludeRange<'_>,
anchor_processor: F,
) -> manyhow::Result<String> {
let content = match range {
IncludeRange::Full => content,
IncludeRange::Range { from, to } => {
let from = from.unwrap_or_default().saturating_sub(1);
let mut lines = content.lines().skip(from);
if let Some(to) = to {
let n = to - from;
lines.take(n).join("\n")
} else {
lines.join("\n")
}
}
IncludeRange::Anchor { name } => anchor_processor(content, name)?,
};
Ok(content.trim().to_owned())
}