(config_file: P) -> Tavern
+ where P: AsRef
+{
+ // Read the previously written TOML file
+ let toml_data =
+ std::fs::read_to_string(&config_file).expect("Failed to read TOML file");
+
+ // Deserialize it
+ toml::from_str(&toml_data).expect("Failed to parse TOML")
+}
+
+
+fn create_database() -> Result<(), Box>
+{
+ // This part would be the entry point of your CI/CD script
+ // It would load your data and then save it to the database
+
+ // Create a Tavern object
+ let tavern = read_from_file("Tavern.toml");
+ //let tavern = generate_tavern();
+
+ // Open the database and save the Tavern content
+ let db = Database::open(Path::new("tavern.db"))?;
+
+ db.insert_tavern(&tavern.title, &tavern.description)?;
+ println!("Saved site settings: Title='{}', Description='{}'", tavern.title, tavern.description);
+
+ for author in &tavern.authors
+ {
+ db.insert_adventurer(author)?;
+ println!("Saved adventurer: {}", author.name);
+ }
+
+ for tale in &tavern.tales
+ {
+ db.insert_tale(tale)?;
+ println!("Saved tale: {}", tale.title);
+ }
+
+ Ok(())
+}
+
+pub fn main()
+{
+ match std::env::set_current_dir("/home/myrddin/cybermages/blog/")
+ {
+ Ok(_) =>
+ {
+ println!("Successfully changed working directory.");
+ }
+ Err(e) =>
+ {
+ eprintln!("Failed to change directory: {}", e);
+ }
+ }
+
+ match create_database()
+ {
+ Ok(_) => {}
+ Err(e) => { eprintln!("Error: {}", e); }
+ }
+}
diff --git a/src/adventurer.rs b/src/adventurer.rs
index 3f2058d..6136d66 100644
--- a/src/adventurer.rs
+++ b/src/adventurer.rs
@@ -12,13 +12,16 @@ pub struct Adventurer
/// The full name of the adventurer.
pub name: String,
- /// A unique handle or username for the adventurer (e.g., used in URLs or mentions).
+ /// A unique handle or username for the adventurer (e.g., used in URLs or
+ /// mentions).
pub handle: String,
- /// A link to the adventurer's profile (e.g., personal website, GitHub, etc.).
+ /// A link to the adventurer's profile (e.g., personal website, GitHub,
+ /// etc.).
pub profile: String,
- /// A URL or path to an image representing the adventurer (e.g., avatar or portrait).
+ /// A URL or path to an image representing the adventurer (e.g., avatar or
+ /// portrait).
pub image: String,
/// A short descriptive text or tagline about the adventurer.
@@ -27,6 +30,4 @@ pub struct Adventurer
-impl Adventurer
-{
-}
+impl Adventurer {}
diff --git a/src/converter.rs b/src/converter.rs
new file mode 100644
index 0000000..1f3433b
--- /dev/null
+++ b/src/converter.rs
@@ -0,0 +1,83 @@
+//use std::io::Write;
+
+use pulldown_cmark::{html, Parser};
+
+
+pub enum Converter {}
+
+
+
+impl Converter
+{
+ /*
+ /// Public function to handle file-to-file conversion
+ pub fn md_to_html(markdown_path: &std::path::Path,
+ html_path: &std::path::Path)
+ -> std::io::Result<()>
+ {
+ let markdown_content = std::fs::read_to_string(markdown_path)?;
+ let html_output = Self::markdown_to_html(&markdown_content);
+ let mut html_file = std::fs::File::create(html_path)?;
+
+ html_file.write_all(html_output.as_bytes())?;
+ Ok(())
+ }
+ */
+
+ /// Private function to handle the core conversion logic
+ pub fn markdown_to_html(markdown_content: &str) -> String
+ {
+ let parser = Parser::new(markdown_content);
+ let mut html_output = String::new();
+ html::push_html(&mut html_output, parser);
+ html_output
+ }
+}
+
+// tests/integration_test.rs
+// This file would be in your project's `tests` directory.
+// It tests the public functions of your main program.
+// You will also need to add `use crate::*` to access the functions.
+
+#[cfg(test)]
+mod tests
+{
+ use std::fs;
+ use std::io::Write;
+ use std::path::Path;
+
+ use super::*; // Import the parent module's items
+
+ #[test]
+ fn test_markdown_conversion_with_files()
+ {
+ let test_dir = Path::new("temp_test_dir");
+ let temp_md_path = test_dir.join("test.md");
+ let temp_html_path = test_dir.join("test.html");
+
+ // Ensure the test directory is clean
+ if test_dir.exists()
+ {
+ fs::remove_dir_all(&test_dir).unwrap();
+ }
+ fs::create_dir(&test_dir).unwrap();
+
+ // Create a temporary Markdown file for the test
+ let markdown_content = "# Hello, World!\n\nThis is a **test**.";
+ let expected_html =
+ "
Hello, World!
\n
This is a test.
\n";
+
+ let mut temp_md_file = fs::File::create(&temp_md_path).unwrap();
+ temp_md_file.write_all(markdown_content.as_bytes()).unwrap();
+
+ // Use the new Converter API
+ Converter::md_to_html(&temp_md_path, &temp_html_path).unwrap();
+ let converted_html = fs::read_to_string(&temp_html_path).unwrap();
+
+ // Assert that the output matches the expected HTML
+ assert_eq!(converted_html.trim(), expected_html.trim());
+
+ // Clean up the temporary directory after the test
+ fs::remove_dir_all(&test_dir).unwrap();
+ }
+}
diff --git a/src/database.rs b/src/database.rs
new file mode 100644
index 0000000..cb1f046
--- /dev/null
+++ b/src/database.rs
@@ -0,0 +1,309 @@
+// The connection strings are:
+// PostgreSQL - "postgres://root:root@localhost:5432"
+// SQLite - "sqlite::memory:"
+// const DATABASE_URL: &str = "sqlite::memory:";
+// const DB_NAME: &str = "cm_tavern_db";
+use std::path::Path;
+
+use rusqlite::{params, Connection, Result};
+#[cfg(not(feature = "publisher"))]
+use rusqlite::Statement;
+
+use crate::adventurer::Adventurer;
+#[cfg(feature = "publisher")]
+use crate::converter::Converter;
+#[cfg(not(feature = "publisher"))]
+use crate::tale::FrontMatter;
+use crate::tale::Tale;
+
+
+
+pub struct Database
+{
+ connection: Connection
+}
+
+
+
+impl Database
+{
+ /// Opens a connection to the SQLite database and creates the necessary
+ /// tables.
+ pub fn open(db_path: &Path) -> Result
+ {
+ let connection = Connection::open(db_path)?;
+
+ #[cfg(feature = "publisher")]
+ Self::create_tables(&connection)?;
+
+ Ok(Database { connection })
+ }
+
+ /// Creates the 'tales' and 'adventurers' tables if they don't exist.
+ #[cfg(feature = "publisher")]
+ fn create_tables(conn: &Connection) -> Result<()>
+ {
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS adventurers (
+ handle TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ profile TEXT,
+ image TEXT,
+ blurb TEXT
+ )",
+ []
+ )?;
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS tales (
+ slug TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ author TEXT NOT NULL,
+ summary TEXT,
+ publish_date TEXT NOT NULL,
+ content TEXT
+ )",
+ []
+ )?;
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL UNIQUE
+ )",
+ []
+ )?;
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS tale_tags (
+ tale_slug TEXT,
+ tag_id INTEGER,
+ FOREIGN KEY(tale_slug) REFERENCES tales(slug),
+ FOREIGN KEY(tag_id) REFERENCES tags(id),
+ UNIQUE(tale_slug, tag_id)
+ )",
+ []
+ )?;
+ conn.execute(
+ "CREATE TABLE IF NOT EXISTS tavern (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ )",
+ [],
+ )?;
+ Ok(())
+ }
+
+ /// Inserts a single tale into the database.
+ #[cfg(feature = "publisher")]
+ pub fn insert_tale(&self, tale: &Tale) -> Result<()>
+ {
+ // Read the markdown content from the file
+ let markdown_content = std::fs::read_to_string(&tale.content)
+ .map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?;
+
+ // Convert the markdown to HTML
+ let html_content = Converter::markdown_to_html(&markdown_content);
+
+ self.connection.execute(
+ "INSERT OR REPLACE INTO tales (
+ slug, title, author, summary, publish_date, content
+ ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
+ params![
+ tale.front_matter.slug,
+ tale.front_matter.title,
+ tale.front_matter.author,
+ tale.front_matter.summary,
+ tale.front_matter.publish_date.to_string(),
+ html_content
+ ]
+ )?;
+
+ // Insert tags and link them to the tale
+ for tag_name in &tale.front_matter.tags
+ {
+ let tag_id = self.insert_and_get_tag_id(tag_name)?;
+ self.connection.execute("INSERT OR IGNORE INTO tale_tags \
+ (tale_slug, tag_id) VALUES (?1, ?2)",
+ params![tale.front_matter.slug, tag_id])?;
+ }
+
+ Ok(())
+ }
+
+ /// Inserts a tag and returns its ID. If the tag already exists, it returns
+ /// the existing ID.
+ #[cfg(feature = "publisher")]
+ fn insert_and_get_tag_id(&self, tag_name: &str) -> Result
+ {
+ self.connection
+ .execute("INSERT OR IGNORE INTO tags (name) VALUES (?1)",
+ params![tag_name])?;
+ let id: i64 = self.connection
+ .query_row("SELECT id FROM tags WHERE name = ?1",
+ params![tag_name],
+ |row| row.get(0))?;
+ Ok(id)
+ }
+
+ /// Inserts a single adventurer into the database.
+ #[cfg(feature = "publisher")]
+ pub fn insert_adventurer(&self, adventurer: &Adventurer) -> Result<()>
+ {
+ self.connection.execute(
+ "INSERT OR REPLACE INTO adventurers (
+ handle, name, profile, image, blurb
+ ) VALUES (?1, ?2, ?3, ?4, ?5)",
+ params![
+ adventurer.handle,
+ adventurer.name,
+ adventurer.profile,
+ adventurer.image,
+ adventurer.blurb
+ ]
+ )?;
+ Ok(())
+ }
+
+ /// Inserts the site-wide settings like title and description.
+ #[cfg(feature = "publisher")]
+ pub fn insert_tavern(&self, title: &str, description: &str) -> Result<()> {
+ self.connection.execute_batch(&format!(
+ "BEGIN;
+ INSERT OR REPLACE INTO tavern (key, value) VALUES ('title', '{}');
+ INSERT OR REPLACE INTO tavern (key, value) VALUES ('description', '{}');
+ COMMIT;",
+ title, description
+ ))?;
+ Ok(())
+ }
+
+ /// Fetches a vector of lightweight FrontMatter objects from the database.
+ #[cfg(not(feature = "publisher"))]
+ pub fn get_tales_summary(&self, categories: &[String]) -> Result> {
+ let mut query = String::from(
+ "SELECT
+ t.title,
+ t.slug,
+ t.summary,
+ t.author,
+ t.publish_date,
+ GROUP_CONCAT(tg.name, ',') AS tags
+ FROM tales AS t
+ JOIN tale_tags AS tt ON t.slug = tt.tale_slug
+ JOIN tags AS tg ON tt.tag_id = tg.id"
+ );
+
+ // If categories are provided, filter the results
+ if !categories.is_empty() {
+ query.push_str(" WHERE tg.name IN (");
+ // Add a placeholder for each category
+ let placeholders: Vec<_> = (0..categories.len()).map(|_| "?").collect();
+ query.push_str(&placeholders.join(", "));
+ query.push_str(")");
+ }
+
+ // Group by tale to get all tags for each tale
+ query.push_str(" GROUP BY t.slug");
+
+ query.push_str(" ORDER BY t.publish_date DESC");
+
+ let mut stmt: Statement = self.connection.prepare(&query)?;
+
+ let tales_iter = stmt.query_map(rusqlite::params_from_iter(categories), |row| {
+ let date_str: String = row.get(4)?;
+ let tags_str: String = row.get(5)?;
+ let tags: Vec = tags_str.split(',').map(|s| s.to_string()).collect();
+ Ok(FrontMatter {
+ title: row.get(0)?,
+ slug: row.get(1)?,
+ summary: row.get(2)?,
+ author: row.get(3)?,
+ publish_date: chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S").map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?,
+ tags,
+ })
+ })?;
+
+ let summaries: Result, _> = tales_iter.collect();
+ summaries
+ }
+
+ /// Fetches a single tale from the database by its slug.
+ /// This function is only available in the 'reader' build.
+ #[cfg(not(feature = "publisher"))]
+ pub fn get_tale_by_slug(&self, slug: &str) -> Result