| @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; |
| |
| use flate2::{Compression, write::GzEncoder}; |
| use miette::{IntoDiagnostic, Result}; |
| -use pulldown_cmark::{Options, Parser, html}; |
| +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd, html}; |
| use tera::{Context, Tera}; |
| use tracing::warn; |
| |
| @@ -196,6 +196,24 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { |
| } |
| }; |
| |
| + // Copy any locally-referenced files (e.g. images) from the README's |
| + // directory into the version directory so they resolve correctly from |
| + // the generated index.html. |
| + let readme_dir = readme_path.parent().unwrap_or_else(|| Path::new(".")); |
| + if let Ok(content) = tokio::fs::read_to_string(readme_path).await { |
| + for rel in extract_local_refs(&content) { |
| + let src = readme_dir.join(&rel); |
| + if src.is_file() { |
| + // Preserve any sub-directory structure relative to the README. |
| + let dest = version_dir.join(&rel); |
| + if let Some(parent) = dest.parent() { |
| + tokio::fs::create_dir_all(parent).await.into_diagnostic()?; |
| + } |
| + tokio::fs::copy(&src, &dest).await.into_diagnostic()?; |
| + } |
| + } |
| + } |
| + |
| // ── 7. Changelog section ────────────────────────────────────────────────── |
| let changelog_html = match ChangelogExtractor |
| .section(config.changelog.clone(), &version) |
| @@ -294,6 +312,33 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { |
| |
| // ── Private helpers ─────────────────────────────────────────────────────────── |
| |
| +/// Extract URLs of locally-referenced files from a Markdown document. |
| +/// |
| +/// Returns relative paths that are referenced as images or links and that do |
| +/// not look like remote URLs (no `://` scheme) or bare fragment anchors |
| +/// (starting with `#`). |
| +fn extract_local_refs(md: &str) -> Vec<String> { |
| + let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; |
| + let mut refs = Vec::new(); |
| + for event in Parser::new_ext(md, opts) { |
| + let url: Option<pulldown_cmark::CowStr> = match event { |
| + Event::Start(Tag::Image { dest_url, .. }) => Some(dest_url), |
| + Event::Start(Tag::Link { dest_url, .. }) => Some(dest_url), |
| + // pulldown-cmark emits End events with a TagEnd — ignore those. |
| + Event::End(TagEnd::Image | TagEnd::Link) => None, |
| + _ => None, |
| + }; |
| + if let Some(url) = url { |
| + let s = url.as_ref(); |
| + // Skip remote URLs, data URIs, and fragment-only links. |
| + if !s.contains("://") && !s.starts_with('#') && !s.is_empty() { |
| + refs.push(s.to_owned()); |
| + } |
| + } |
| + } |
| + refs |
| +} |
| + |
| /// Render Markdown to an HTML string. |
| fn render_markdown(md: &str) -> String { |
| let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; |