agpm_cli/templating/
content.rs1use std::path::PathBuf;
7use std::sync::Arc;
8
9pub(crate) const NON_TEMPLATED_LITERAL_GUARD_START: &str = "__AGPM_LITERAL_RAW_START__";
13pub(crate) const NON_TEMPLATED_LITERAL_GUARD_END: &str = "__AGPM_LITERAL_RAW_END__";
14
15pub(crate) trait ContentExtractor {
20 fn cache(&self) -> &Arc<crate::cache::Cache>;
22
23 fn project_dir(&self) -> &PathBuf;
25
26 async fn extract_content(&self, resource: &crate::lockfile::LockedResource) -> Option<String> {
41 tracing::debug!(
42 "Attempting to extract content for resource '{}' (type: {:?})",
43 resource.name,
44 resource.resource_type
45 );
46
47 let source_path = if let Some(source_name) = &resource.source {
49 let url = resource.url.as_ref()?;
50
51 let is_local_source = resource.resolved_commit.as_deref().is_none_or(str::is_empty);
53
54 tracing::debug!(
55 "Resource '{}': source='{}', url='{}', is_local={}",
56 resource.name,
57 source_name,
58 url,
59 is_local_source
60 );
61
62 if is_local_source {
63 let path = std::path::PathBuf::from(url).join(&resource.path);
65 tracing::debug!("Using local source path: {}", path.display());
66 path
67 } else {
68 let sha = resource.resolved_commit.as_deref()?;
70
71 tracing::debug!(
72 "Resource '{}': Getting worktree for SHA {}...",
73 resource.name,
74 &sha[..8.min(sha.len())]
75 );
76
77 let worktree_dir = match self.cache().get_worktree_path(url, sha) {
79 Ok(path) => {
80 tracing::debug!("Worktree path: {}", path.display());
81 path
82 }
83 Err(e) => {
84 tracing::warn!(
85 "Failed to construct worktree path for resource '{}': {}",
86 resource.name,
87 e
88 );
89 return None;
90 }
91 };
92
93 let full_path = worktree_dir.join(&resource.path);
94 tracing::debug!(
95 "Full source path for '{}': {} (worktree exists: {})",
96 resource.name,
97 full_path.display(),
98 worktree_dir.exists()
99 );
100 full_path
101 }
102 } else {
103 let local_path = std::path::Path::new(&resource.path);
105 let resolved_path = if local_path.is_absolute() {
106 local_path.to_path_buf()
107 } else {
108 self.project_dir().join(local_path)
109 };
110
111 tracing::debug!(
112 "Resource '{}': Using local file path: {}",
113 resource.name,
114 resolved_path.display()
115 );
116
117 resolved_path
118 };
119
120 let content = match tokio::fs::read_to_string(&source_path).await {
122 Ok(c) => c,
123 Err(e) => {
124 tracing::warn!(
125 "Failed to read content for resource '{}' from {}: {}",
126 resource.name,
127 source_path.display(),
128 e
129 );
130 return None;
131 }
132 };
133
134 let processed_content = if resource.path.ends_with(".md") {
136 match crate::markdown::MarkdownDocument::parse(&content) {
138 Ok(doc) => {
139 let templating_enabled = is_markdown_templating_enabled(doc.metadata.as_ref());
140 let mut stripped_content = doc.content;
141
142 if !templating_enabled && content_contains_template_syntax(&stripped_content) {
143 tracing::debug!(
144 "Protecting non-templated markdown content for '{}'",
145 resource.name
146 );
147 stripped_content = wrap_content_in_literal_guard(stripped_content);
148 }
149
150 stripped_content
151 }
152 Err(e) => {
153 tracing::warn!(
154 "Failed to parse markdown for resource '{}': {}. Using raw content.",
155 resource.name,
156 e
157 );
158 content
159 }
160 }
161 } else if resource.path.ends_with(".json") {
162 match serde_json::from_str::<serde_json::Value>(&content) {
164 Ok(mut json) => {
165 if let Some(obj) = json.as_object_mut() {
166 obj.remove("dependencies");
168 }
169 serde_json::to_string_pretty(&json).unwrap_or(content)
170 }
171 Err(e) => {
172 tracing::warn!(
173 "Failed to parse JSON for resource '{}': {}. Using raw content.",
174 resource.name,
175 e
176 );
177 content
178 }
179 }
180 } else {
181 content
183 };
184
185 Some(processed_content)
186 }
187}
188
189pub(crate) fn is_markdown_templating_enabled(
191 metadata: Option<&crate::markdown::MarkdownMetadata>,
192) -> bool {
193 metadata
194 .and_then(|md| md.extra.get("agpm"))
195 .and_then(|agpm| agpm.as_object())
196 .and_then(|agpm_obj| agpm_obj.get("templating"))
197 .and_then(|value| value.as_bool())
198 .unwrap_or(false)
199}
200
201pub(crate) fn content_contains_template_syntax(content: &str) -> bool {
203 content.contains("{{") || content.contains("{%") || content.contains("{#")
204}
205
206pub(crate) fn wrap_content_in_literal_guard(content: String) -> String {
208 let mut wrapped = String::with_capacity(
209 content.len()
210 + NON_TEMPLATED_LITERAL_GUARD_START.len()
211 + NON_TEMPLATED_LITERAL_GUARD_END.len()
212 + 2, );
214
215 wrapped.push_str(NON_TEMPLATED_LITERAL_GUARD_START);
216 wrapped.push('\n');
217 wrapped.push_str(&content);
218 if !content.ends_with('\n') {
219 wrapped.push('\n');
220 }
221 wrapped.push_str(NON_TEMPLATED_LITERAL_GUARD_END);
222
223 wrapped
224}