Skip to content

Commit 47798da

Browse files
committed
Upload source
1 parent fffcbfd commit 47798da

File tree

21 files changed

+1504
-0
lines changed

21 files changed

+1504
-0
lines changed

Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "gisture"
3+
version = "0.1.0"
4+
edition = "2018"
5+
6+
[dependencies]
7+
cacache = "9.0.0"
8+
chrono = "0.4.19"
9+
clap = "2.33.3"
10+
colored = "2.0.0"
11+
futures = "0.3.17"
12+
handlebars = "4.1.3"
13+
html-escape = "0.2.9"
14+
html-minifier = "3.0.14"
15+
lol_html = "0.3.0"
16+
once_cell = "1.8.0"
17+
portpicker = "0.1.1"
18+
pulldown-cmark = "0.8.0"
19+
serde = { version = "1.0.130", features = ["derive"] }
20+
serde_json = "1.0.68"
21+
sitemap = "0.4.1"
22+
syntect = "4.6.0"
23+
tokio = { version = "1.12.0", features = ["full"] }
24+
ureq = { version = "*", features = ["json"] }
25+
warp = "0.3.1"
26+
27+
[profile.release]
28+
lto = "thin"
29+
panic = "abort"

README.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<h1 align="center"><code>gisture</code></h1>
2+
<h4 align="center">Utilizing GitHub Gists as a Blogging Platform</h4>
3+
<p align="center">
4+
<img src="https://img.shields.io/badge/version-0.1.0-blue.svg" title="version" alt="version">
5+
<a href="https://github.com/mufeedvh/gisture/blob/master/LICENSE"><img alt="GitHub license" src="https://img.shields.io/github/license/mufeedvh/gisture.svg"></a>
6+
</p>
7+
8+
A minimal and flexible blog generator based on GitHub Gists with SEO, Templating, Syntax Highlighting, and Metadata support out-of-the-box.
9+
10+
## Table of Contents
11+
12+
- [Features](#features)
13+
- [Why Gist?](#why-gist)
14+
- [Installation](#installation)
15+
- [Usage](#usage)
16+
- [Templating](#templating)
17+
- [Modification Guide](#modification-guide)
18+
- [Contribution](#contribution)
19+
- [License](#license)
20+
- [FAQ](#faq)
21+
22+
## Features
23+
24+
- **Single Binary** - Just run the binary and it will generate the starter boilerplate.
25+
- **Simple Configuration** - A simple JSON file will be created upon initiation which has everything you need to setup your blog.
26+
- **Templating** - A set of template variables are prefixed to write your own blog template or port any blog theme easily using everyone's familiar Handlebars. (See [Templating](#templating))
27+
- **SEO Utility** - A `sitemap.xml` and `robots.txt` are automatically generated according to your gist entries.
28+
- **Syntax Highlighting** - Every code snippet in your Gist will be highlighted in the generated HTML and you can add your own syntax spec with [Sublime Text syntax definitions](http://www.sublimetext.com/docs/3/syntax.html#include-syntax). (Thanks to [syntect](https://github.com/trishume/syntect))
29+
- **Helpful Log Messages** - Every error case has been handled with a helpful and verbose error message to provide a breeze CLI experience.
30+
- **Caching** - Since Gists are fetched from the API, building multiple blog entries will take time hence gisture handles a disk cache and only build when a gist is updated.
31+
32+
## Why Gist?
33+
34+
Here are some of the concepts that made me utilize/choose GitHub Gists:
35+
36+
- **Git:** You can use git to edit and manage a gist just like a repository.
37+
- **Hosting:** Your Gists will exist as long as GitHub exists.
38+
- **Integrated Comment Section:** Every gist has a comment section which supports Markdown.
39+
- **Transparent:** As it's based on gists, anyone can check revisions to see the changes made to a blog entry.
40+
- **Starring:** You can bookmark a gist entry into your GitHub account by starring it.
41+
42+
Also GitHub's Markdown editor is pretty cool and gisture uses [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) which supports GitHub flavored elements.
43+
44+
## Installation
45+
46+
$ cargo install --git https://github.com/mufeedvh/gisture
47+
48+
[Install Rust/Cargo](https://rust-lang.org/tools/install)
49+
50+
## Build From Source
51+
52+
**Prerequisites:**
53+
54+
* [Git](https://git-scm.org/downloads)
55+
* [Rust](https://rust-lang.org/tools/install)
56+
* Cargo (Automatically installed when installing Rust)
57+
* A C linker (Only for Linux, generally comes pre-installed)
58+
59+
```
60+
$ git clone https://github.com/mufeedvh/gisture.git
61+
$ cd gisture/
62+
$ cargo build --release
63+
```
64+
65+
The first command clones this repository into your local machine and the last two commands enters the directory and builds the source in release mode.
66+
67+
## Usage
68+
69+
A gisture blog should have `xyz.blog.md` as it's Gist filename where `/xyz` becomes the permalink, description as it's `meta description`, a Markdown title (`# Title`) for it's `title`.
70+
71+
Setup configuration and Generate template boilerplate:
72+
73+
$ gisture
74+
75+
Build blog files with the configuration:
76+
77+
$ gisture build
78+
79+
Open up a preview web server on port `1337`:
80+
81+
$ gisture serve 1337
82+
83+
Just running `serve` will open up the web server on a random free port.
84+
85+
## Templating
86+
87+
gisture uses Handlebars as it's templating engine. All you need to make/port a theme for your blog, are these files and a couple of template variables which are automatically generated upon initiation.
88+
89+
### Template Files
90+
91+
- `index.html` - The homepage.
92+
- `page.html` - A blog/page entry.
93+
- `page_list.html` - The blog listing element.
94+
- `404.html` - Page Not Found template.
95+
96+
### Template Variables
97+
98+
**NOTE:** Just refer to the `templates/` directory to get up and running quickly, it has a starter template that utilizes these variables.
99+
100+
**Blog:**
101+
102+
- `{{ blog_title }}` - The home title of the blog.
103+
- `{{ blog_description }}` - The home description of the blog.
104+
- `{{ blog_url }}` - The URL of the blog.
105+
- `{{ blog_list }}` - The list of all the blog/page entries as an HTML element. (`blog_list.html`)
106+
107+
**Gist:**
108+
109+
- `{{ page_title }}` - A blog/page entry's title.
110+
- `{{ page_description }}` - A blog/page entry's description.
111+
- `{{ page_url }}` - The full URL of a blog/page entry.
112+
- `{{ published_date }}` - The published datetime of a blog/page entry.
113+
- `{{ updated_at }}` - The recent update datetime of a blog/page entry.
114+
- `{{ blog_contents }}` - The content of the blog/page entry.
115+
116+
## Modification Guide
117+
118+
Here are some code pointers if you want to modify gisture to fit your own needs or to add new features. I have tried to make the code verbose and easier to modify. :)
119+
120+
- [Markdown Parser Options](https://github.com/mufeedvh/gisture/blob/master/src/parsers.rs#L115-L123)
121+
- [HTML Rewriting/Handling](https://github.com/mufeedvh/gisture/blob/master/src/parsers.rs#L49-L90)
122+
- [Add New Configuration Options](https://github.com/mufeedvh/gisture/blob/master/src/config.rs#L11-L36)
123+
- [Gist User Data](https://github.com/mufeedvh/gisture/blob/master/src/gist.rs#L14-L22)
124+
- [Gist Entry Data](https://github.com/mufeedvh/gisture/blob/master/src/gist.rs#L149-L156)
125+
- [Disk Caching](https://github.com/mufeedvh/gisture/blob/master/src/cache.rs#L6-L35)
126+
- [SEO Function Utils](https://github.com/mufeedvh/gisture/blob/master/src/metadata.rs#L21-L24)
127+
- [Syntax Highlighting Theme](https://github.com/mufeedvh/gisture/blob/master/src/parsers.rs#L45)
128+
129+
### API Guide
130+
131+
- [User's Gists](https://docs.github.com/en/rest/reference/gists#list-gists-for-a-user)
132+
- [Single Gist](https://docs.github.com/en/rest/reference/gists#get-a-gist)
133+
- [Gist Revision](https://docs.github.com/en/rest/reference/gists#get-a-gist-revision)
134+
- [Gist Comments](https://docs.github.com/en/rest/reference/gists#list-gist-comments)
135+
136+
## Contribution
137+
138+
Ways to contribute:
139+
140+
- Suggest a feature
141+
- Report a bug
142+
- Fix something and open a pull request
143+
- Help me document the code
144+
- Spread the word
145+
- Create a better starter template for gisture because I suck at CSS
146+
147+
## License
148+
Licensed under the MIT License, see <a href="https://github.com/mufeedvh/gisture/blob/master/LICENSE">LICENSE</a> for more information.
149+
150+
## FAQ
151+
152+
1. Why?
153+
154+
> This is the embodiment of the [automation XKCD comic](https://xkcd.com/1319/), all I wanted to do was write a blog (which I didn't) and this is the result. I am not a fan of static site generators because of the markdown metadata section + the disqus comment hosting shenanigans (I don't like disqus) although [utteranc.es](https://utteranc.es/) is pretty cool. So I set out to find another static solution and ended up deciding to utilize Gist as a blogging platform because it comes with my favorite Markdown editor, an excellent comment section, starring for bookmarks and hence the yoink. Hopefully someone other than me who actually wants to write a blog finds it useful so putting it out there.
155+
156+
2. Why are the Handlebars variables unescaped?
157+
158+
> It's Markdown converted to HTML so it needs to be unescaped and one does not simply XSS their own blog.
159+
160+
## Liked the project?
161+
162+
Support the author by buying him a coffee!
163+
164+
<a href="https://www.buymeacoffee.com/mufeedvh" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="45" width="170"></a>
165+
166+
---

gisture.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"github_username": "octocat",
3+
"blog_title": "John Wick's Pencil",
4+
"blog_description": "Rusty, I Guess.",
5+
"blog_url": "https://blog.johnwickspencil.com/",
6+
"pages_title": "{{ blog_title }} | John Wick's Pencil",
7+
"minify_html": false,
8+
"show_comments": true
9+
}

src/.extras/banner

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
_ _
2+
(_) | |
3+
__ _ _ ___| |_ _ _ _ __ ___
4+
/ _` | / __| __| | | | '__/ _ \
5+
| (_| | \__ \ |_| |_| | | | __/
6+
\__, |_|___/\__|\__,_|_| \___|
7+
__/ |
8+
|___/ v0.1.0

src/cache.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use std::path::Path;
2+
3+
use cacache;
4+
5+
#[derive(Debug, Clone)]
6+
pub(crate) struct Cache {
7+
pub permalink_key: String,
8+
pub updated_at: String,
9+
}
10+
11+
// NOTE: cache function calls are blocked (forced sync)
12+
impl Cache {
13+
/// Save a blog entry to disk cache
14+
pub async fn save_cache_entry(&self) -> Result<(), cacache::Error> {
15+
let cache_dir = String::from("./gisture_cache");
16+
17+
cacache::write(&cache_dir, &self.permalink_key, self.updated_at.as_bytes()).await?;
18+
19+
Ok(())
20+
}
21+
22+
/// Check if a blog entry is cached on disk
23+
pub async fn is_cached(&self) -> Result<bool, cacache::Error> {
24+
let cache_dir = String::from("./gisture_cache");
25+
let data = cacache::read(&cache_dir, &self.permalink_key).await?;
26+
27+
let build_file = Path::new(&format!("public/{}", &self.permalink_key)).exists();
28+
29+
if data == self.updated_at.as_bytes() && build_file {
30+
Ok(true)
31+
} else {
32+
Ok(false)
33+
}
34+
}
35+
}

src/cli.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use clap::{App, Arg, ArgMatches};
2+
3+
pub fn interface() -> ArgMatches<'static> {
4+
App::new("gisture")
5+
.version("0.1.0")
6+
.author("Mufeed VH <[email protected]>")
7+
.about("A minimal and light-weight blog generator based on GitHub gists.")
8+
.arg(Arg::with_name("COMMAND")
9+
.help("`build` compiles all your gists to HTML pages.\n`serve` launches a web server to preview the generated pages.")
10+
.value_name("build | serve")
11+
.required(false)
12+
.index(1))
13+
.arg(Arg::with_name("PORT")
14+
.help("PORT to run the preview server on (comes after the `serve` command).")
15+
.value_name("PORT")
16+
.required(false)
17+
.index(2))
18+
.get_matches()
19+
}

src/config.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use std::fs::File;
2+
use std::io::prelude::*;
3+
use std::io::BufReader;
4+
use std::path::Path;
5+
use std::process::exit;
6+
7+
use serde::{Deserialize, Serialize};
8+
9+
use crate::messages::{push_message, Type};
10+
11+
#[derive(Serialize, Deserialize)]
12+
pub(crate) struct Config {
13+
pub github_username: String,
14+
pub blog_title: String,
15+
pub blog_description: String,
16+
pub blog_url: String,
17+
pub pages_title: String,
18+
pub minify_html: bool,
19+
pub show_comments: bool,
20+
}
21+
22+
static CONFIG_FILE: &str = "gisture.json";
23+
24+
impl Config {
25+
pub fn default() -> Self {
26+
// default config file values
27+
Self {
28+
github_username: "octocat".into(),
29+
blog_title: "John Wick's Pencil".into(),
30+
blog_description: "Rusty, I Guess.".into(),
31+
blog_url: "https://blog.johnwickspencil.com/".into(),
32+
pages_title: "{{ blog_title }} | John Wick's Pencil".into(),
33+
minify_html: false,
34+
show_comments: true,
35+
}
36+
}
37+
38+
/// Generate a boilerplate config file
39+
pub fn generate_default() {
40+
if !Path::new(CONFIG_FILE).exists() {
41+
let json_config = match serde_json::to_string_pretty(&Self::default()) {
42+
Ok(json_config) => json_config,
43+
Err(error) => {
44+
let message = format!("Failed to serialize JSON config: \n\t{}", error);
45+
push_message(Type::Error, &message);
46+
exit(1)
47+
}
48+
};
49+
let mut config_file = match File::create(CONFIG_FILE) {
50+
Ok(file) => file,
51+
Err(error) => {
52+
let message = format!("Failed to create config file: \n\t{}", error);
53+
push_message(Type::Error, &message);
54+
exit(1)
55+
}
56+
};
57+
match config_file.write_all(json_config.as_bytes()) {
58+
Ok(()) => (),
59+
Err(error) => {
60+
let message = format!("Failed to write to config file: \n\t{}", error);
61+
push_message(Type::Error, &message);
62+
exit(1)
63+
}
64+
}
65+
}
66+
}
67+
68+
/// Fetch config values from `gisture.json`
69+
pub fn get_config() -> Self {
70+
let file = match File::open(CONFIG_FILE) {
71+
Ok(file) => file,
72+
Err(error) => {
73+
let message = format!("Failed to open config file: \n\t{}", error);
74+
push_message(Type::Error, &message);
75+
exit(1)
76+
}
77+
};
78+
79+
let mut buf_reader = BufReader::new(file);
80+
let mut user_config = String::new();
81+
match buf_reader.read_to_string(&mut user_config) {
82+
Ok(_) => (),
83+
Err(error) => {
84+
let message = format!("Failed while reading config file: \n\t{}", error);
85+
push_message(Type::Error, &message);
86+
exit(1)
87+
}
88+
}
89+
90+
let config: Config = match serde_json::from_str(&user_config) {
91+
Ok(config) => config,
92+
Err(error) => {
93+
let message = format!("Failed while serializing user config: \n\t{}", error);
94+
push_message(Type::Error, &message);
95+
exit(1)
96+
}
97+
};
98+
99+
config
100+
}
101+
}

0 commit comments

Comments
 (0)