-
18 | {%- for entry in entries %}
19 |
- {{ entry.title | e }} ({{ entry.content | wordcount // 300 + 1 }} min read) – {{ entry.pretty_date }} 20 | {%- endfor %} 21 |
├── .gitignore
├── LICENSE
├── README.md
├── chisel.py
├── requirements.txt
└── templates
├── 58b.css.jinja2
├── archive.html
├── archive_by_year_month.html
├── base.html
├── detail.html
├── feed.xml
├── footer.html
├── header.html
├── home.html
├── nav.html
└── site_settings.jinja2
/.gitignore:
--------------------------------------------------------------------------------
1 | export
2 | blog
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 David Zhou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chisel
2 |
3 | Chisel is a simple python static blog generation utility by [David Zhou][dz].
4 |
5 | ## Usage
6 |
7 | ```bash
8 | $ python3 chisel.py
9 | ```
10 |
11 | ## Sample entry
12 |
13 | `sample.markdown`:
14 |
15 | ```markdown
16 | Title
17 | 3/2/2009
18 |
19 | This is now the body of the post. By default, the body is evaluated and parsed with markdown.
20 |
21 | Another line.
22 | ```
23 |
24 | Entry format is described as follows:
25 |
26 | - Line 1: Enter a title
27 | - Line 2: Enter date in m/d/Y
28 | - Line 3: Blank line
29 | - Line 4: Content in Markdown here onward
30 |
31 | ## Adding Steps
32 |
33 | Use the `@step` decorator. The main loop passes in the master file list and [jinja2][j2] environment.
34 |
35 | ## Settings
36 |
37 | Change these settings:
38 |
39 | - `SOURCE`: Location of source files for entries (must end with a `/`), e.g., `SOURCE = "./blog/"`
40 | - `DESTINATION`: Location to place generated files (must end with a `/`), e.g., `DESTINATION = "./explort/"`
41 | - `HOME_SHOW`: Number of entries to show on homepage, e.g., `HOME_SHOW = 15`
42 | - `TEMPLATE_PATH`: Path to folder where tempaltes live (must end with a `/`), e.g., `TEMPLATE_PATH = "./templates/"`
43 | - `TEMPLATE_OPTIONS`: Dictionary of options to give jinja2, e.g., `TEMPLATE_OPTIONS = {}`
44 | - `TEMPLATES`: Dictionary of templates (required keys: 'home', 'detail', 'archive'), e.g.,
45 | ```python
46 | TEMPLATES = {
47 | 'home': "home.html",
48 | 'detail': "detail.html",
49 | 'archive': "archive.html",
50 | }
51 | ```
52 | - `TIME_FORMAT`: Format of human readable time stamp. Default: `"%B %d, %Y - %I:%M %p"`
53 | - `ENTRY_TIME_FORMAT`: Format of date declaration in second line of posts. Default: `"%m/%d/%Y"`
54 | - `FORMAT`: Callable that takes in text and returns formatted text. Default: `FORMAT = lambda text: markdown.markdown(text, extensions=['markdown.extensions.footnotes'])`
55 |
56 | [dz]: https://github.com/dz/chisel
57 | [j2]: https://jinja.palletsprojects.com/en/
58 |
--------------------------------------------------------------------------------
/chisel.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | # Chisel
4 | # David Zhou
5 | #
6 | # Requires:
7 | # jinja2
8 |
9 | import sys, re, time, os
10 | import jinja2, markdown
11 | from functools import cmp_to_key
12 |
13 | #Settings
14 | SOURCE = "./blog/" #end with slash
15 | DESTINATION = "./export/" #end with slash
16 | HOME_SHOW = 15 #numer of entries to show on homepage
17 | TEMPLATE_PATH = "./templates/"
18 | TEMPLATE_OPTIONS = {}
19 | TEMPLATES = {
20 | 'home': "home.html",
21 | 'detail': "detail.html",
22 | 'archive': "archive.html",
23 | 'rss': "feed.xml",
24 | }
25 | TIME_FORMAT = "%B %d, %Y"
26 | ENTRY_TIME_FORMAT = "%m/%d/%Y"
27 | #FORMAT should be a callable that takes in text
28 | #and returns formatted text
29 | FORMAT = lambda text: markdown.markdown(text, extensions=['markdown.extensions.footnotes'])
30 | #########
31 |
32 | STEPS = []
33 |
34 | def step(func):
35 | def wrapper(*args, **kwargs):
36 | print("\t\tGenerating %s..." %func.__name__, end="");
37 | func(*args, **kwargs)
38 | print("done.")
39 | STEPS.append(wrapper)
40 | return wrapper
41 |
42 | def get_tree(source):
43 | files = []
44 | for root, ds, fs in os.walk(source):
45 | for name in fs:
46 | if name[0] == ".": continue
47 | path = os.path.join(root, name)
48 | f = open(path, "r")
49 | title = f.readline().strip('\n\t')
50 | date = time.strptime(f.readline().strip(), ENTRY_TIME_FORMAT)
51 | year, month, day = date[:3]
52 | files.append({
53 | 'title': title,
54 | 'epoch': time.mktime(date),
55 | 'content': FORMAT(''.join(f.readlines()[1:])),
56 | 'url': '/'.join([str(year), "%.2d" % month, "%.2d" % day, os.path.splitext(name)[0] + ".html"]),
57 | 'pretty_date': time.strftime(TIME_FORMAT, date),
58 | 'rssdate': time.strftime("%a, %d %b %Y %H:%M:%S %z", date),
59 | 'date': date,
60 | 'year': year,
61 | 'month': month,
62 | 'day': day,
63 | 'filename': name,
64 | })
65 | f.close()
66 | return files
67 |
68 | def compare_entries(x, y):
69 | result = (y['epoch'] > x['epoch']) - (y['epoch'] < x['epoch'])
70 | if result == 0:
71 | return (y['filename'] > x['filename']) - (y['filename'] < x['filename'])
72 | return result
73 |
74 | def write_file(url, data):
75 | path = DESTINATION + url
76 | dirs = os.path.dirname(path)
77 | if not os.path.isdir(dirs):
78 | os.makedirs(dirs)
79 | file = open(path, "w")
80 | file.write(data)
81 | file.close()
82 |
83 | @step
84 | def generate_homepage(f, e):
85 | """Generate homepage"""
86 | template = e.get_template(TEMPLATES['home'])
87 | write_file("index.html", template.render(entries=f[:HOME_SHOW]))
88 |
89 | @step
90 | def generate_rss(f, e):
91 | """Generate rss feed"""
92 | template = e.get_template(TEMPLATES['rss'])
93 | write_file("rss.xml", template.render(entries=f[:HOME_SHOW]))
94 |
95 | @step
96 | def master_archive(f, e):
97 | """Generate master archive list of all entries"""
98 | template = e.get_template(TEMPLATES['archive'])
99 | write_file("archives.html", template.render(entries=f))
100 |
101 | @step
102 | def detail_pages(f, e):
103 | """Generate detail pages of individual posts"""
104 | template = e.get_template(TEMPLATES['detail'])
105 | for file in f:
106 | write_file(file['url'], template.render(entry=file, entries=f))
107 |
108 | def main():
109 | print("Chiseling...");
110 | print("\tReading files...", end="");
111 | files = sorted(get_tree(SOURCE), key=cmp_to_key(compare_entries))
112 | env = jinja2.Environment(loader=jinja2.FileSystemLoader(TEMPLATE_PATH), **TEMPLATE_OPTIONS)
113 | print("done.")
114 | print("\tRunning steps...");
115 | for step in STEPS:
116 | step(files, env)
117 | print("\tdone.")
118 | print("done.")
119 |
120 | if __name__ == "__main__":
121 | sys.exit(main())
122 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | jinja2
2 | markdown
3 |
--------------------------------------------------------------------------------
/templates/58b.css.jinja2:
--------------------------------------------------------------------------------
1 | /*
2 | Inline styling inspired by 58 bytes of css to look great
3 | nearly everywhere: https://jrl.ninja/etc/1/
4 | */
5 | body {
6 | font-family: Liberation Sans, Arial, sans-serif;
7 | background-color: #fff;
8 | line-height: 1.5;
9 | }
10 | main {
11 | max-width: 70ch;
12 | padding: 2ch;
13 | margin: auto;
14 | }
15 | header, footer {
16 | display: flex;
17 | justify-content: space-between;
18 | margin: 2.5ch auto;
19 | padding-left: 0;
20 | padding-right: 0;
21 | color: #888;
22 | }
23 | time {
24 | color: #888;
25 | }
26 | img {
27 | width: 100%;
28 | min-width: 100%;
29 | max-width: 70ch;
30 | }
31 | a {
32 | color: #009;
33 | text-decoration: none;
34 | outline: 0;
35 | }
36 | a:hover {
37 | text-decoration: underline;
38 | }
39 | a:visited {
40 | color: #4B0082;
41 | }
42 | blockquote p {
43 | margin-left: 3ch;
44 | margin-right: 3ch;
45 | color: deeppink;
46 | }
47 | ul.year_block > li, ul.month_block > li {
48 | list-style: none;
49 | }
50 | hr {
51 | border:none;
52 | text-align: center;
53 | }
54 | hr:after {
55 | content: "* * *";
56 | position: relative;
57 | top:0.25rem;
58 | }
59 | .footnote hr {
60 | margin:1.5rem auto 0.75rem 0;
61 | width:36%;
62 | border-color:#aaa;
63 | border-style:solid;
64 | border-width:1px 0 0;
65 | }
66 | .footnote hr:after {
67 | content: none!important;
68 | }
69 | .footnote li {
70 | font-size: 87%;
71 | }
72 |
--------------------------------------------------------------------------------
/templates/archive.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block header %}
4 |
18 | {%- for entry in entries %}
19 |
22 | {{year}}
4 |
5 | {% for month, month_list in year_list|groupby('month')|reverse %}
6 |
18 | {% endfor %}
19 | {{month}}
8 |
9 | {% for day, day_list in month_list|groupby('day')|reverse %}
10 | {% for entry in day_list %}
11 |
15 |