docxide_template_derive/
lib.rs1extern crate proc_macro;
2mod codegen;
3mod docx_extract;
4mod naming;
5mod placeholders;
6
7use docx_rs::read_docx;
8use proc_macro::TokenStream;
9use quote::quote;
10use std::{
11 collections::HashMap,
12 fs,
13 path::PathBuf,
14};
15
16use syn::{parse_str, LitStr};
17
18use codegen::generate_struct;
19use docx_extract::{
20 collect_text_from_document_children, collect_text_from_footer_children,
21 collect_text_from_header_children, extract_text_from_xml_tags, is_valid_docx_file,
22 print_docxide_message,
23};
24use naming::derive_type_name_from_filename;
25use placeholders::generate_struct_content;
26
27#[proc_macro]
49pub fn generate_templates(input: TokenStream) -> TokenStream {
50 let embed = cfg!(feature = "embed");
51
52 let lit: LitStr = syn::parse(input).expect("expected a string literal, e.g. generate_templates!(\"path/to/templates\")");
53 let folder_path = lit.value();
54
55 let paths = fs::read_dir(&folder_path).unwrap_or_else(|e| panic!("Failed to read template directory {:?}: {}", folder_path, e));
56 let mut structs = Vec::new();
57 let mut seen_type_names: HashMap<String, PathBuf> = HashMap::new();
58
59 for path in paths {
60 let path = path.expect("Failed to read path").path();
61
62 if !is_valid_docx_file(&path) {
63 print_docxide_message("Invalid template file, skipping.", &path);
64 continue;
65 }
66
67 let type_name = match derive_type_name_from_filename(&path) {
68 Ok(name) if parse_str::<syn::Ident>(&name).is_ok() => name,
69 other => {
70 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
71 if stem.starts_with(|c: char| c.is_ascii_digit()) {
72 let attempted = other.unwrap_or_default();
73 print_docxide_message(
74 &format!(
75 "Filename starts with a digit, which produces an invalid Rust type name `{}`. Skipping.",
76 if attempted.is_empty() { stem.to_string() } else { attempted }
77 ),
78 &path,
79 );
80 } else {
81 print_docxide_message(
82 "Unable to derive a valid Rust type name from file name. Skipping.",
83 &path,
84 );
85 }
86 continue;
87 }
88 };
89
90 if let Some(existing_path) = seen_type_names.get(&type_name) {
91 panic!(
92 "\n\n[Docxide-template] Type name collision: both {:?} and {:?} produce the struct name `{}`.\n\
93 Rename one of the files to avoid this conflict.\n",
94 existing_path, path, type_name
95 );
96 }
97 seen_type_names.insert(type_name.clone(), path.clone());
98
99 let type_ident = syn::Ident::new(type_name.as_str(), proc_macro::Span::call_site().into());
100
101 let buf = match fs::read(&path) {
102 Ok(buf) => buf,
103 Err(_) => {
104 print_docxide_message("Unable to read file content. Skipping.", &path);
105 continue;
106 }
107 };
108
109 let doc = match read_docx(&buf) {
110 Ok(doc) => doc,
111 Err(_) => {
112 print_docxide_message("Unable to read docx content. Skipping.", &path);
113 continue;
114 }
115 };
116
117 let mut corpus = collect_text_from_document_children(doc.document.children);
118
119 let section = &doc.document.section_property;
120 for (_, header) in section.get_headers() {
121 corpus.extend(collect_text_from_header_children(&header.children));
122 }
123 for (_, footer) in section.get_footers() {
124 corpus.extend(collect_text_from_footer_children(&footer.children));
125 }
126
127 corpus.extend(extract_text_from_xml_tags(&buf, &["<a:t", "<m:t"]));
128
129 let content = generate_struct_content(corpus);
130
131 let abs_path = path.canonicalize().expect("Failed to canonicalize template path");
132 let abs_path_str = abs_path.to_str().expect("Failed to convert path to string");
133
134 let template_struct = generate_struct(
135 type_ident,
136 abs_path_str,
137 &content.fields,
138 &content.replacement_placeholders,
139 &content.replacement_fields,
140 embed,
141 );
142
143 structs.push(template_struct)
144 }
145
146 let combined = quote! {
147 #(#structs)*
148 };
149
150 combined.into()
151}