├── .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 |
5 | {{ site_title }}: Archive by {{ site_author }} 6 | {%- if entries | length > 1 -%} 7 | {{ entries | length }} entries 8 | {%- else -%} 9 | {{ entries | length }} entry 10 | {%- endif -%} 11 |
12 | {% endblock %} 13 | 14 | {% block main %} 15 |
16 | {# {% include 'archive_by_year_month.html' %} #} 17 |
    18 | {%- for entry in entries %} 19 |
  1. {{ entry.title | e }} ({{ entry.content | wordcount // 300 + 1 }} min read) – {{ entry.pretty_date }}
  2. 20 | {%- endfor %} 21 |
22 |
23 | {% endblock %} 24 | 25 | {% block footer %} 26 | {% include 'footer.html' %} 27 | {% endblock %} -------------------------------------------------------------------------------- /templates/archive_by_year_month.html: -------------------------------------------------------------------------------- 1 |
2 | {% for year, year_list in entries|groupby('year')|reverse %} 3 |

{{year}}

4 | 18 | {% endfor %} 19 |
-------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% from 'site_settings.jinja2' import site_title, site_author, site_author_github -%} 5 | 6 | {{ site_title }}{% block title %}{% endblock %} 7 | 8 | 9 | 12 | 13 | 14 |
15 | {% block header %}{% endblock %} 16 | {%- block main %}{% endblock -%} 17 | {% block footer %}{% endblock %} 18 |
19 | 20 | -------------------------------------------------------------------------------- /templates/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block header %} 4 | {% include 'header.html' %} 5 | {% endblock -%} 6 | 7 | {%- block main %} 8 | {% set curr = entry.url %} 9 | {% set total = entries | count %} 10 |
11 | 12 |

{{ entry.title | e }}

13 | {{ entry.content }} 14 |
15 | {% include 'nav.html' %} 16 | {%- endblock %} 17 | 18 | {%- block footer %} 19 | {% include 'footer.html' %} 20 | {% endblock -%} -------------------------------------------------------------------------------- /templates/feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% from 'site_settings.jinja2' import site_title, site_author, site_author_email, site_url -%} 4 | 5 | {{ site_title }} 6 | {{ site_title }} by {{ site_author }} 7 | 8 | {{ site_url }}/ 9 | en-us 10 | {% for entry in entries %} 11 | 12 | {{ entry.title | e }} 13 | {{ site_author_email }} 14 | {{ site_url }}/{{ entry.url }} 15 | {{ site_url }}/{{ entry.url }} 16 | {{ entry.rssdate }}GMT 17 | 18 | {{ entry.content|e }} 19 | 20 | 21 | {% endfor %} 22 | 23 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 |
2 | {{ site_title }} by {{ site_author }} 3 | archive 4 |
-------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block header %} 4 | {% include 'header.html' %} 5 | {% endblock -%} 6 | 7 | {% block main %} 8 |
9 | {%- for entry in entries %} 10 |

{{ entry.title }}

11 | {{ entry.content }} 12 | 13 | {% endfor -%} 14 |
15 | {% endblock %} 16 | 17 | {%- block footer %} 18 | {% include 'footer.html' %} 19 | {% endblock -%} 20 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | {# Footer navigation begins here #} 2 | {%- for entry in entries %} 3 | {%- if entry.url == curr %} 4 | {%- if loop.index0 > 0 %} 5 | {%- set next = entries[loop.index0-1].url %} 6 | {%- set nexttitle = entries[loop.index0-1].title %} 7 | {%- else %} 8 | {%- set next = None %} 9 | {%- set nexttitle = None %} 10 | {%- endif %} 11 | {%- if loop.index0+1 < total %} 12 | {%- set prev = entries[loop.index0+1].url %} 13 | {%- set prevtitle = entries[loop.index0+1].title %} 14 | {%- else %} 15 | {%- set prev = None %} 16 | {%- set prevtitle = None %} 17 | {%- endif %} 18 | 30 | {%- endif %} 31 | {%- endfor %} 32 | {# Footer navigation ends here #} -------------------------------------------------------------------------------- /templates/site_settings.jinja2: -------------------------------------------------------------------------------- 1 | # Site settings 2 | 3 | # Site name 4 | {%- set site_title = "nodnod.net" -%} 5 | 6 | # Site url (end without /) 7 | {%- set site_url = "http://nodnod.net" -%} 8 | 9 | # Author 10 | {%- set site_author = "David Zhou" -%} 11 | 12 | # Author email 13 | {%- set site_author_email = "username@siteurl.com (Site Author)" -%} 14 | 15 | # Author's GitHub profile 16 | {%- set site_author_github = "https://github.com/dz" -%} --------------------------------------------------------------------------------