use crate::core::file_error::{FileOperation, FileResultExt};
use std::path::PathBuf;
use std::sync::Arc;
pub(crate) const NON_TEMPLATED_LITERAL_GUARD_START: &str = "__AGPM_LITERAL_RAW_START__";
pub(crate) const NON_TEMPLATED_LITERAL_GUARD_END: &str = "__AGPM_LITERAL_RAW_END__";
pub(crate) trait ContentExtractor {
fn cache(&self) -> &Arc<crate::cache::Cache>;
fn project_dir(&self) -> &PathBuf;
async fn extract_content(&self, resource: &crate::lockfile::LockedResource) -> Option<String> {
tracing::debug!(
"Attempting to extract content for resource '{}' (type: {:?})",
resource.name,
resource.resource_type
);
let source_path = if let Some(source_name) = &resource.source {
let url = resource.url.as_ref()?;
let is_local_source = resource.resolved_commit.as_deref().is_none_or(str::is_empty);
tracing::debug!(
"Resource '{}': source='{}', url='{}', is_local={}",
resource.name,
source_name,
url,
is_local_source
);
if is_local_source {
let path = std::path::PathBuf::from(url).join(&resource.path);
tracing::debug!("Using local source path: {}", path.display());
path
} else {
let sha = resource.resolved_commit.as_deref()?;
tracing::debug!(
"Resource '{}': Getting worktree for SHA {}...",
resource.name,
&sha[..8.min(sha.len())]
);
let worktree_dir = match self.cache().get_worktree_path(url, sha) {
Ok(path) => {
tracing::debug!("Worktree path: {}", path.display());
path
}
Err(e) => {
tracing::warn!(
"Failed to construct worktree path for resource '{}': {}",
resource.name,
e
);
return None;
}
};
let full_path = worktree_dir.join(&resource.path);
tracing::debug!(
"Full source path for '{}': {} (worktree exists: {})",
resource.name,
full_path.display(),
worktree_dir.exists()
);
full_path
}
} else {
let local_path = std::path::Path::new(&resource.path);
let resolved_path = if local_path.is_absolute() {
local_path.to_path_buf()
} else {
self.project_dir().join(local_path)
};
tracing::debug!(
"Resource '{}': Using local file path: {}",
resource.name,
resolved_path.display()
);
resolved_path
};
let content = match tokio::fs::read_to_string(&source_path).await.with_file_context(
FileOperation::Read,
&source_path,
format!("reading content for resource '{}'", resource.name),
"content_filter",
) {
Ok(c) => c,
Err(e) => {
tracing::warn!(
"Failed to read content for resource '{}' from {}: {}",
resource.name,
source_path.display(),
e
);
return None;
}
};
let processed_content = if resource.path.ends_with(".md") {
match crate::markdown::MarkdownDocument::parse(&content) {
Ok(doc) => {
let templating_enabled = is_markdown_templating_enabled(doc.metadata.as_ref());
let mut stripped_content = doc.content;
if !templating_enabled && content_contains_template_syntax(&stripped_content) {
tracing::debug!(
"Protecting non-templated markdown content for '{}'",
resource.name
);
stripped_content = wrap_content_in_literal_guard(stripped_content);
}
stripped_content
}
Err(e) => {
tracing::warn!(
"Failed to parse markdown for resource '{}': {}. Using raw content.",
resource.name,
e
);
content
}
}
} else if resource.path.ends_with(".json") {
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(mut json) => {
if let Some(obj) = json.as_object_mut() {
obj.remove("dependencies");
}
serde_json::to_string_pretty(&json).unwrap_or(content)
}
Err(e) => {
tracing::warn!(
"Failed to parse JSON for resource '{}': {}. Using raw content.",
resource.name,
e
);
content
}
}
} else {
content
};
Some(processed_content)
}
}
pub(crate) fn is_markdown_templating_enabled(
metadata: Option<&crate::markdown::MarkdownMetadata>,
) -> bool {
metadata
.and_then(|md| md.extra.get("agpm"))
.and_then(|agpm| agpm.as_object())
.and_then(|agpm_obj| agpm_obj.get("templating"))
.and_then(|value| value.as_bool())
.unwrap_or(false)
}
pub(crate) fn content_contains_template_syntax(content: &str) -> bool {
content.contains("{{") || content.contains("{%") || content.contains("{#")
}
pub(crate) fn wrap_content_in_literal_guard(content: String) -> String {
let mut wrapped = String::with_capacity(
content.len()
+ NON_TEMPLATED_LITERAL_GUARD_START.len()
+ NON_TEMPLATED_LITERAL_GUARD_END.len()
+ 2, );
wrapped.push_str(NON_TEMPLATED_LITERAL_GUARD_START);
wrapped.push('\n');
wrapped.push_str(&content);
if !content.ends_with('\n') {
wrapped.push('\n');
}
wrapped.push_str(NON_TEMPLATED_LITERAL_GUARD_END);
wrapped
}