diff --git a/.gitignore b/.gitignore index 9f97022..8647666 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +build/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 938d9ae..946f93a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,9 +180,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.138" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "log" @@ -208,13 +208,15 @@ dependencies = [ "lazy_static", "serde", "serde_json", + "toml", + "walkdir", ] [[package]] name = "pest" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" +checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" dependencies = [ "thiserror", "ucd-trie", @@ -222,9 +224,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344" +checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" dependencies = [ "pest", "pest_generator", @@ -232,9 +234,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c" +checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" dependencies = [ "pest", "pest_meta", @@ -245,9 +247,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20" +checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" dependencies = [ "once_cell", "pest", @@ -287,6 +289,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "0.9.0" @@ -381,6 +392,15 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +dependencies = [ + "serde", +] + [[package]] name = "typenum" version = "1.16.0" @@ -404,3 +424,45 @@ name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index d296d53..809ecb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,22 +3,18 @@ name = "org-static" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - - [lib] crate-type = ["cdylib"] [dependencies] - lazy_static = "1.4" emacs = "0.18.0" + serde = { version = "1.0.151", features = ["derive"] } handlebars = "4.3.6" serde_json = "1.0.91" - -[dev-dependencies] -#emacs-rs-module = { version = "0.13.0" } +walkdir = "2.3.2" +toml = "0.5.10" diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..46418cd --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,8 @@ +use org_static::publish; + +fn main() { + match publish("examples/basic".into()) { + Ok(()) => println!("Complete"), + Err(e) => println!("Error: {}", e), + } +} diff --git a/examples/basic/articles/a-test-pos.org b/examples/basic/articles/a-test-pos.org new file mode 100644 index 0000000..527a4c7 --- /dev/null +++ b/examples/basic/articles/a-test-pos.org @@ -0,0 +1,13 @@ +#+title: A test Post +#+filetags: :test: + + +* Article Heading + +Heading + + +#+begin_src emacs-lisp +(org-static-publish-current-project) +(org-static-publish "/media/joe/extradrive1/Projects/fivesigma/org-static/examples/basic/articles/") +#+end_src diff --git a/examples/basic/orgstatic.toml b/examples/basic/orgstatic.toml new file mode 100644 index 0000000..dd6587f --- /dev/null +++ b/examples/basic/orgstatic.toml @@ -0,0 +1,2 @@ +[site] +title = "Basic Example Site" \ No newline at end of file diff --git a/examples/basic/theme/articles.html b/examples/basic/theme/articles.html new file mode 100644 index 0000000..016fcc9 --- /dev/null +++ b/examples/basic/theme/articles.html @@ -0,0 +1,5 @@ + +
title: {{ article.title }}
+
+ {{{content}}} +
diff --git a/readme.org b/readme.org new file mode 100644 index 0000000..e20682c --- /dev/null +++ b/readme.org @@ -0,0 +1,10 @@ + + +#+begin_src emacs-lisp +(progn + (add-to-list 'load-path "/media/joe/extradrive1/Projects/fivesigma/org-static/target/debug") + (require 'org-static)) +#+end_src + + +[[file:examples/basic/articles/a-test-pos.org]] diff --git a/src/article.rs b/src/article.rs index 83d438c..9c1dab9 100644 --- a/src/article.rs +++ b/src/article.rs @@ -1,16 +1,26 @@ -use crate::{error::Result, site::Site, template::Template}; use serde::Serialize; -use serde_json::json; -use std::collections::{HashMap, HashSet}; +use std::{collections::HashSet, path::PathBuf}; -#[derive(Serialize, Default)] +use crate::config::Config; + +#[derive(Serialize, Default, Clone)] pub struct Article { - title: String, - filename: String, - date: String, - author: String, - properties: HashMap, - tags: HashSet, + pub title: String, + pub date: String, + pub author: String, + pub slug: String, + pub file_path: PathBuf, + pub tags: HashSet, +} + +impl Article { + pub fn get_build_path(&self, config: &Config) -> PathBuf { + config + .get_build_path() + .join("articles") + .join(&self.slug) + .join("index.html") + } } #[derive(Serialize, Default)] @@ -20,91 +30,7 @@ pub struct ArticleContext { } impl ArticleContext { - pub fn load(article: Article) -> Result { - Ok(Self { - article, - content: String::new(), - }) - } - - #[cfg(test)] - pub fn load_string(article: Article, content: &str) -> Result { - Ok(Self { - article, - content: content.to_string(), - }) - } -} - -pub fn generate_article( - site: &Site, - article: &ArticleContext, - template: &Template, -) -> Result { - let ctx = json!({ - "site": site, - "data": article, - }); - let res = template.generate(&ctx)?; - Ok(res) -} - -pub fn generate_article_index( - site: &Site, - articles: Vec
, - template: &Template, -) -> Result { - let ctx = json!({ - "site": site, - "data": articles, - }); - - let res = template.generate(&ctx)?; - Ok(res) -} - -#[cfg(test)] -mod tests { - use crate::{site::tests::fixture_site, template::Template}; - - use super::*; - - pub fn fixture_article() -> Article { - Article { - title: "Test Article #1".to_string(), - filename: "test.org".to_string(), - date: "".to_string(), - author: "Bob Bobberson".to_string(), - properties: HashMap::new(), - tags: vec!["article", "new", "cool"] - .into_iter() - .map(String::from) - .collect(), - } - } - - #[test] - fn test_generate_article() { - let article_ctx = - ArticleContext::load_string(fixture_article(), "ten animals i slam in a net") - .expect("couldnt build article"); - let template = - Template::new("content: {{{data.content}}}, site: {{site.title}}".to_string()); - let site = fixture_site(); - let res = - generate_article(&site, &article_ctx, &template).expect("couldnt gnenerate template"); - - assert_eq!(res, "content: ten animals i slam in a net, site: Test Site"); - } - - #[test] - fn test_generate_article_index() { - let articles = vec![fixture_article(), fixture_article()]; - let site = fixture_site(); - let template = Template::new("list: {{#each data}}{{title}} {{/each}}".to_string()); - let res = - generate_article_index(&site, articles, &template).expect("couldnt generate template"); - - assert_eq!(res, "list: Test Article #1 Test Article #1 "); + pub fn new(article: Article, content: String) -> Self { + Self { article, content } } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..48e3a3e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,112 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::{error::Error, site::Site}; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(default)] +pub struct Config { + pub project_root: PathBuf, + pub build_path: PathBuf, + pub articles_path: PathBuf, + pub theme_path: PathBuf, + pub static_path: PathBuf, + pub partials_path: PathBuf, + pub site: Site, +} + +impl Default for Config { + fn default() -> Self { + Config { + site: Site::default(), + project_root: ".".into(), + build_path: "build".into(), + articles_path: "articles".into(), + theme_path: "theme".into(), + static_path: "static".into(), + partials_path: "partials".into(), + } + } +} + +impl Config { + pub fn find(path: PathBuf) -> crate::error::Result { + if let Some(p) = find_config(path) { + let content = std::fs::read_to_string(&p)?; + let mut cfg: Config = toml::from_str(&content)?; + cfg.project_root = p + .canonicalize()? + .parent() + .ok_or_else(|| Error::new("Couldnt find project root"))? + .to_path_buf(); + Ok(cfg) + } else { + Err(Error::new("Config file not found")) + } + } + + pub fn get_build_path(&self) -> PathBuf { + if self.build_path.is_relative() { + PathBuf::from(&self.project_root).join(&self.build_path) + } else { + self.build_path.clone() + } + } + + pub fn get_articles_path(&self) -> PathBuf { + if self.articles_path.is_relative() { + PathBuf::from(&self.project_root).join(&self.articles_path) + } else { + self.articles_path.clone() + } + } + + pub fn get_theme_path(&self) -> PathBuf { + if self.theme_path.is_relative() { + PathBuf::from(&self.project_root).join(&self.theme_path) + } else { + self.theme_path.clone() + } + } + + pub fn get_static_path(&self) -> PathBuf { + if self.static_path.is_relative() { + PathBuf::from(&self.project_root).join(&self.static_path) + } else { + self.static_path.clone() + } + } + + pub fn get_partials_path(&self) -> PathBuf { + if self.partials_path.is_relative() { + PathBuf::from(&self.project_root).join(&self.partials_path) + } else { + self.partials_path.clone() + } + } +} + +fn find_config(mut path: PathBuf) -> Option { + let config_path = path.join("orgstatic.toml"); + if config_path.is_file() { + Some(config_path) + } else if path.pop() { + find_config(path) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_config() { + let path = PathBuf::from("examples/basic/articles/") + .canonicalize() + .unwrap(); + Config::find(path).unwrap(); + } +} diff --git a/src/elisp/org-static.el b/src/elisp/org-static.el new file mode 100644 index 0000000..97c1399 --- /dev/null +++ b/src/elisp/org-static.el @@ -0,0 +1,20 @@ +(require 'liborg_static) + +(defun org-static-file-info (filename) + (vector (nth 0 (cdr (nth 0 (org-collect-keywords '("title" ""))))) + (nth 0 (cdr (nth 0 (org-collect-keywords '("author" ""))))) + (nth 0 (cdr (nth 0 (org-collect-keywords '("date" ""))))) + (nth 0 (cdr (nth 0 (org-collect-keywords '("slug" ""))))) + (nth 0 (cdr (nth 0 (org-collect-keywords '("filetags" ""))))))) + + +(defun org-static-file-contents (filename) + (org-export-string-as (f-read-text filename 'utf-8) 'html t)) + + +(defun org-static-publish-current-project () + (org-static-publish (file-name-directory (buffer-file-name)))) + +(provide 'org-static) + + diff --git a/src/error.rs b/src/error.rs index 8359cfa..7347158 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,8 +11,6 @@ impl std::fmt::Display for Error { } } -impl std::error::Error for Error {} - impl Error { pub fn new(message: &str) -> Self { Error { @@ -21,18 +19,16 @@ impl Error { } } -impl From for Error { - fn from(source: handlebars::TemplateError) -> Self { +impl From for Error { + fn from(source: T) -> Self { Self { message: source.to_string(), } } } -impl From for Error { - fn from(source: handlebars::RenderError) -> Self { - Self { - message: source.to_string(), - } +impl From for emacs::Error { + fn from(source: Error) -> Self { + emacs::Error::msg(source) } } diff --git a/src/lib.rs b/src/lib.rs index 922f924..f5a1a4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,117 @@ #![allow(non_snake_case)] mod article; +mod config; mod error; mod site; mod template; -use emacs::{defun, Env, Result, Value}; +use article::{Article, ArticleContext}; +use std::path::PathBuf; +use template::TemplateEngine; +use walkdir::WalkDir; + +use emacs::{defun, Env, IntoLisp, Result, Vector}; emacs::plugin_is_GPL_compatible!(); -#[emacs::module(name = "org-static")] -fn init(_: &Env) -> Result<()> { +#[emacs::module(name = "liborg_static", defun_prefix = "org-static")] +fn init(e: &Env) -> Result<()> { + e.message("orgstatic loaded")?; + Ok(()) +} + +fn find_articles(env: &Env, config: &config::Config) -> Result> { + let mut articles: Vec
= vec![]; + + for entry in WalkDir::new(&config.get_articles_path()) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map(|ex| ex == "org").unwrap_or(false)) + { + let file_path = entry.path().canonicalize()?; + + let article_data = Vector::from_value_unchecked( + env.call( + "org-static-file-info", + [file_path + .to_str() + .unwrap_or_default() + .to_string() + .into_lisp(env)?], + )?, + 3, + ); + + let title = article_data.get::(0)?; + let author = article_data.get::(1)?; + let date = article_data.get::(2)?; + let slug = { + let s = article_data.get::(3)?; + if s.is_empty() { + file_path + .file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default() + .to_string() + } else { + s + } + }; + let tags = article_data + .get::(4)? + .split(':') + .filter(|i| !i.is_empty()) + .map(|i| i.to_string()) + .collect(); + + articles.push(Article { + title, + slug, + date, + author, + file_path, + tags, + }); + } + Ok(articles) +} + +fn publish_articles(env: &Env, config: &config::Config, articles: &[Article]) -> Result<()> { + let eng = TemplateEngine::new(config.get_partials_path())?; + for article in articles { + std::fs::create_dir_all(article.get_build_path(config))?; + + let content = env + .call( + "org-static-file-contents", + [article + .file_path + .to_str() + .unwrap_or_default() + .to_string() + .into_lisp(env)?], + )? + .into_rust::()?; + + let ctx = ArticleContext::new(article.clone(), content); + + eng.render_file( + config.get_theme_path().join("articles.html"), + article.get_build_path(config).join("index.html"), + &ctx, + )?; + } Ok(()) } #[defun] -fn say_hello(env: &Env, _name: String) -> Result> { - let fname = env - .call("buffer-file-name", []) - .unwrap() - .into_rust::() - .unwrap(); - env.message(fname) +pub fn publish(env: &Env, starting_path: String) -> Result<()> { + let cfg = config::Config::find(PathBuf::from(starting_path))?; + println!("{:?}", cfg); + let articles = find_articles(env, &cfg)?; + env.message(format!("{} articles found", articles.len()))?; + publish_articles(env, &cfg, &articles)?; + Ok(()) } diff --git a/src/site.rs b/src/site.rs index 24198c0..ac4bf03 100644 --- a/src/site.rs +++ b/src/site.rs @@ -1,12 +1,13 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] pub struct Site { title: String, } #[cfg(test)] pub mod tests { + use super::*; #[allow(dead_code)] @@ -15,7 +16,4 @@ pub mod tests { title: "Test Site".to_string(), } } - - #[test] - fn test_name() {} } diff --git a/src/template.rs b/src/template.rs index ce2a8e8..2ce578f 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,22 +1,61 @@ +use std::{collections::HashMap, path::PathBuf}; + use handlebars::Handlebars; use serde::Serialize; -use crate::{error::Result, site::Site}; +use crate::error::Result; -pub struct Template { - pub template_string: String, +#[derive(Debug, Default)] +pub struct TemplateEngine { + partials: HashMap, } -impl Template { - pub fn new(template_string: String) -> Self { - Self { template_string } +impl TemplateEngine { + pub fn new>(partial_path: T) -> Result { + let pp: PathBuf = partial_path.into(); + + let partials = if pp.exists() { + pp.read_dir()? + .filter_map(|i| i.ok()) + .fold(HashMap::new(), |mut acc, i| { + if i.path().is_file() { + if let Some(file_name) = i.path().with_extension("").file_name() { + acc.insert( + file_name.to_str().unwrap_or_default().to_string(), + std::fs::read_to_string(i.path()).unwrap_or_default(), + ); + } + } + acc + }) + } else { + HashMap::new() + }; + + Ok(Self { partials }) } - pub fn generate(&self, data: &T) -> Result { - let reg = Handlebars::new(); + pub fn render_string(&self, template_string: &str, data: D) -> Result { + let mut reg = Handlebars::new(); + for (k, v) in self.partials.iter() { + reg.register_partial(k, v)?; + } - let output = reg.render_template(&self.template_string, data)?; - Ok(output) + let res = reg.render_template(template_string, &data)?; + + Ok(res) + } + + pub fn render_file( + &self, + template_file: PathBuf, + output_path: PathBuf, + data: D, + ) -> Result<()> { + let template_content = std::fs::read_to_string(template_file)?; + let res = self.render_string(&template_content, data)?; + std::fs::write(output_path, res)?; + Ok(()) } } @@ -45,12 +84,11 @@ mod tests { #[test] fn test_template() { - let tmpl = Template::new( - "Hello, my name is {{ name }}, I am {{ age }} years old and i like {{#each tags}}{{this}} {{/each}}" - .to_string(), - ); - let res = tmpl - .generate(&TestData::default()) + let eng = TemplateEngine::default(); + + let template_string = "Hello, my name is {{ name }}, I am {{ age }} years old and i like {{#each tags}}{{this}} {{/each}}"; + let res = eng + .render_string(template_string, &TestData::default()) .expect("Error parsing template"); assert_eq!( res, @@ -65,12 +103,13 @@ mod tests { ..Default::default() }; - let tmpl = Template::new( - "Hello, my name is {{{ name }}}, I am {{ age }} years old and i like {{#each tags}}{{this}} {{/each}}" - .to_string(), - ); + let template_string = "Hello, my name is {{{ name }}}, I am {{ age }} years old and i like {{#each tags}}{{this}} {{/each}}"; - let res = tmpl.generate(&data).expect("Error parsing template"); + let eng = TemplateEngine::default(); + + let res = eng + .render_string(template_string, &data) + .expect("Error parsing template"); assert_eq!( res, "Hello, my name is Alice Allison, I am 21 years old and i like cats dogs rust " diff --git a/test.html b/test.html new file mode 100644 index 0000000..0167482 --- /dev/null +++ b/test.html @@ -0,0 +1,257 @@ + + + + + + + + + + + + + +
+
+
(progn 
+  (add-to-list 'load-path "/media/joe/extradrive1/Projects/fivesigma/org-static/target/debug")
+  (require 'org-static)
+  (org-static-say-hello "joe"))
+
+(buffer-file-name)
+
+(org-export-to-file 'html "test.html")
+
+
+
+
+

Author: Joe Bellus

+

Created: 2022-12-21 Wed 01:30

+
+ +