diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb36640 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + name: Lint, Test, Build + runs-on: ubuntu-latest + + env: + CARGO_TERM_COLOR: always + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache cargo registry + build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Cargo fmt (check) + run: cargo fmt --all -- --check + + - name: Cargo clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Cargo test + run: cargo test --workspace --all-features --no-fail-fast + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache node_modules (bun) + uses: actions/cache@v4 + with: + path: | + node_modules + ~/.cache/bun + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install JS dependencies + run: bun install --frozen-lockfile + + - name: Build CSS (Tailwind) + run: bun run build + + - name: Cargo build (release) + run: cargo build --release + + - name: Generate site (SSG) + run: ./target/release/typstify-ssg + + - name: Upload site artifact + uses: actions/upload-artifact@v4 + with: + name: site + path: | + site/** + style/output.css diff --git a/typstify-ssg/build.rs b/typstify-ssg/build.rs index 6102dc0..0a209f3 100644 --- a/typstify-ssg/build.rs +++ b/typstify-ssg/build.rs @@ -1,27 +1,23 @@ -use std::process::Command; -use std::path::Path; +use std::{path::Path, process::Command}; fn main() { println!("cargo:rerun-if-changed=../style/input.css"); println!("cargo:rerun-if-changed=../tailwind.config.js"); println!("cargo:rerun-if-changed=../package.json"); - + // Check if bun is available - let has_bun = Command::new("bun") - .arg("--version") - .output() - .is_ok(); - + let has_bun = Command::new("bun").arg("--version").output().is_ok(); + if !has_bun { println!("cargo:warning=bun is not available, CSS will not be built automatically"); return; } - + // Check if output.css exists, if not, build it let output_css = Path::new("../style/output.css"); if !output_css.exists() { println!("cargo:warning=Building CSS with Tailwind..."); - + let output = Command::new("bun") .arg("x") .arg("tailwindcss") @@ -32,13 +28,16 @@ fn main() { .arg("--minify") .current_dir("..") .output(); - + match output { Ok(output) if output.status.success() => { println!("cargo:warning=CSS built successfully"); } Ok(output) => { - println!("cargo:warning=Failed to build CSS: {}", String::from_utf8_lossy(&output.stderr)); + println!( + "cargo:warning=Failed to build CSS: {}", + String::from_utf8_lossy(&output.stderr) + ); } Err(e) => { println!("cargo:warning=Failed to run tailwindcss command: {}", e); diff --git a/typstify-ssg/src/feed.rs b/typstify-ssg/src/feed.rs index 6ccf41d..4485b8e 100644 --- a/typstify-ssg/src/feed.rs +++ b/typstify-ssg/src/feed.rs @@ -1,15 +1,15 @@ -use atom_syndication::{Feed, Entry, Link, Person, Text}; -use crate::config::AppConfig; -use crate::content::Content; +use atom_syndication::{Entry, Feed, Link, Person, Text}; use chrono::{DateTime, Utc}; +use crate::{config::AppConfig, content::Content}; + pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { let mut feed = Feed::default(); - + // Set feed metadata feed.set_title(config.site.title.clone()); feed.set_subtitle(Text::plain(config.site.description.clone())); - + // Set feed link let feed_link = Link { href: format!("{}/{}", config.site.base_url, config.feed.filename), @@ -20,10 +20,10 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { length: None, }; feed.set_links(vec![feed_link]); - + // Set feed ID (usually the website URL) feed.set_id(config.site.base_url.clone()); - + // Set updated time to the most recent content let now = Utc::now(); if let Some(latest_content) = content.first() { @@ -42,23 +42,27 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { } else { feed.set_updated(now); } - + // Create entries from content let mut entries = Vec::new(); - + for content_item in content.iter().take(config.feed.max_items) { // Skip draft content if content_item.metadata.is_draft() { continue; } - + let mut entry = Entry::default(); - + // Set entry title entry.set_title(content_item.metadata.get_title()); - + // Set entry ID and link - let content_url = format!("{}/{}.html", config.site.base_url.trim_end_matches('/'), content_item.slug()); + let content_url = format!( + "{}/{}.html", + config.site.base_url.trim_end_matches('/'), + content_item.slug() + ); entry.set_id(content_url.clone()); entry.set_links(vec![Link { href: content_url, @@ -68,13 +72,13 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { title: None, length: None, }]); - + // Set entry content let summary = content_item.metadata.get_description(); if !summary.is_empty() { entry.set_summary(Some(Text::html(summary))); } - + // Set entry author if let Some(author) = content_item.metadata.get_author() { entry.set_authors(vec![Person { @@ -90,14 +94,18 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { uri: None, }]); } - + // Set published date if available if let Some(date_str) = content_item.metadata.get_date() { // Try to parse as RFC3339 first, then as simple date if let Ok(fixed_date) = DateTime::parse_from_rfc3339(date_str) { entry.set_published(Some(fixed_date)); } else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { - let fixed_date = naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc().fixed_offset(); + let fixed_date = naive_date + .and_hms_opt(0, 0, 0) + .unwrap() + .and_utc() + .fixed_offset(); entry.set_published(Some(fixed_date)); } else { // Fallback to current time if date parsing fails @@ -107,9 +115,12 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { // Use current time as fallback entry.set_published(Some(now.fixed_offset())); } - + // Set entry categories/tags - let categories: Vec<_> = content_item.metadata.tags.iter() + let categories: Vec<_> = content_item + .metadata + .tags + .iter() .map(|tag| atom_syndication::Category { term: tag.clone(), scheme: None, @@ -117,10 +128,10 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed { }) .collect(); entry.set_categories(categories); - + entries.push(entry); } - + feed.set_entries(entries); feed } diff --git a/typstify-ssg/src/lib.rs b/typstify-ssg/src/lib.rs index b1c4473..200bc1e 100644 --- a/typstify-ssg/src/lib.rs +++ b/typstify-ssg/src/lib.rs @@ -245,38 +245,36 @@ impl Site { match (a.metadata.get_date(), b.metadata.get_date()) { (Some(date_a), Some(date_b)) => { // Try to parse as RFC3339 first, then as simple date - let parsed_a = chrono::DateTime::parse_from_rfc3339(date_a) - .or_else(|_| { - chrono::NaiveDate::parse_from_str(date_a, "%Y-%m-%d") - .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into()) - }); - let parsed_b = chrono::DateTime::parse_from_rfc3339(date_b) - .or_else(|_| { - chrono::NaiveDate::parse_from_str(date_b, "%Y-%m-%d") - .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into()) - }); - + let parsed_a = chrono::DateTime::parse_from_rfc3339(date_a).or_else(|_| { + chrono::NaiveDate::parse_from_str(date_a, "%Y-%m-%d") + .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into()) + }); + let parsed_b = chrono::DateTime::parse_from_rfc3339(date_b).or_else(|_| { + chrono::NaiveDate::parse_from_str(date_b, "%Y-%m-%d") + .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into()) + }); + match (parsed_a, parsed_b) { - (Ok(a), Ok(b)) => b.cmp(&a), // Most recent first - (Ok(_), Err(_)) => std::cmp::Ordering::Less, // Valid date comes first + (Ok(a), Ok(b)) => b.cmp(&a), // Most recent first + (Ok(_), Err(_)) => std::cmp::Ordering::Less, // Valid date comes first (Err(_), Ok(_)) => std::cmp::Ordering::Greater, // Valid date comes first (Err(_), Err(_)) => date_b.cmp(date_a), // Fallback to string comparison } } - (Some(_), None) => std::cmp::Ordering::Less, // Items with dates come first + (Some(_), None) => std::cmp::Ordering::Less, // Items with dates come first (None, Some(_)) => std::cmp::Ordering::Greater, // Items with dates come first - (None, None) => std::cmp::Ordering::Equal, // No preference + (None, None) => std::cmp::Ordering::Equal, // No preference } }); // Generate feed let feed = crate::feed::create_feed(&self.config, &sorted_content); - + // Write feed to file let feed_path = self.output_dir.join(&self.config.feed.filename); let feed_xml = feed.to_string(); std::fs::write(&feed_path, feed_xml)?; - + info!("Generated feed: {}", feed_path.display()); Ok(()) } diff --git a/typstify-ssg/src/renderers.rs b/typstify-ssg/src/renderers.rs index f114d70..2cd82c1 100644 --- a/typstify-ssg/src/renderers.rs +++ b/typstify-ssg/src/renderers.rs @@ -109,20 +109,23 @@ impl TypstRenderer { // Pre-process Typst-specific elements let mut processed_content = content.to_string(); - + // Replace #line() with HTML hr processed_content = regex::Regex::new(r"#line\([^)]*\)") .unwrap() .replace_all(&processed_content, "