├── .gitignore ├── MANIFEST.in ├── README.md ├── bin └── sssgen ├── setup.py ├── sssgen ├── __init__.py └── mako_helpers.py └── tutorial ├── css ├── default.css └── reset.css ├── default.html.mako_layout ├── index.html.mako └── textfiles ├── spongebob.txt └── squidward.txt /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | sssgen.egg-info/ 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include bin * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sssgen: Simple static site generator 2 | - 3 | 4 | A static site generator which goes out of its way to make sense. 5 | 6 | First, install: 7 | 8 | ``` 9 | sudo python setup.py install 10 | ``` 11 | 12 | Then check out the tutorial in `tutorial/`: 13 | 14 | ``` 15 | sssgen --input tutorial/ --serve 16 | ``` 17 | 18 | Visit in your browser. Or just visit . 19 | -------------------------------------------------------------------------------- /bin/sssgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | if sys.version_info.major != 3: 5 | raise RuntimeError('sssgen only supports Python 3') 6 | 7 | import argparse 8 | import json 9 | import mako.lookup 10 | import mako.template 11 | import os 12 | import os.path 13 | import queue 14 | import re 15 | import http.server 16 | import socketserver 17 | import shutil 18 | import sys 19 | import tempfile 20 | import time 21 | import traceback 22 | import watchdog.observers 23 | import watchdog.events 24 | 25 | parser = argparse.ArgumentParser(description='generate a static site') 26 | parser.add_argument('--debug', action='store_true') 27 | parser.add_argument('--input', default=os.getcwd()) 28 | parser.add_argument('--output', default=None) 29 | parser.add_argument('--serve', action='store_true') 30 | args = parser.parse_args() 31 | 32 | def peek_line(f): 33 | p = f.tell() 34 | s = f.readline() 35 | f.seek(p) 36 | return s 37 | 38 | def read_and_strip_front_matter(fn): 39 | f = open(fn) 40 | j = {} 41 | if peek_line(f) == '---\n': 42 | f.readline() 43 | s = '' 44 | while True: 45 | line = f.readline() 46 | if line == '---\n': 47 | break 48 | s += line 49 | try: 50 | j = json.loads(s) 51 | except: 52 | raise Exception('%s: the front matter is not valid JSON' % fn) 53 | return j, f.read() 54 | 55 | def generate(): 56 | config = { 57 | 'ignore_regexes': [] 58 | } 59 | try: 60 | config = dict(list(config.items()) + list(json.load(open('_config.json')).items())) 61 | except IOError: 62 | pass 63 | 64 | if args.serve: 65 | assert args.output is None, "--serve doesn't work with --output" 66 | output_root = args.output or tempfile.mkdtemp() 67 | assert not os.listdir(output_root), 'Output directory is not empty!' 68 | if not args.serve: 69 | print('Outputting at "%s".' % output_root) 70 | 71 | templates_dir = tempfile.mkdtemp() 72 | if args.debug: 73 | print('Intermediate templates dir is "%s".' % templates_dir) 74 | 75 | global_tree = {} 76 | mako_templates_to_render = [] 77 | output_path_to_json = {} 78 | 79 | # Paths relative to `args.input`, with the cumulative base JSON. 80 | dirs = [([''], {})] 81 | 82 | while dirs: 83 | direc_list, inherited_json = dirs.pop(0) 84 | direc = os.path.join(*direc_list) 85 | input_dir = os.path.join(args.input, direc) 86 | 87 | # Find `_inherit.json` first and apply it to the base JSON, since it 88 | # applies to everything in and under this directory. 89 | filenames = os.listdir(input_dir) 90 | if '_inherit.json' in filenames: 91 | more_inherited_json = json.load(open(os.path.join(input_dir, '_inherit.json'))) 92 | inherited_json = dict(list(inherited_json.items()) + list(more_inherited_json.items())) 93 | 94 | for filename in filenames: 95 | input_path = os.path.join(input_dir, filename) 96 | output_path = os.path.join(output_root, direc, filename) 97 | 98 | # Ignore files. 99 | if any(re.search(r, filename) for r in config['ignore_regexes']): 100 | if args.debug: 101 | print('Ignoring "%s", is matched by config[\'ignore_regexes\'].' % input_path) 102 | 103 | # Make a directory. 104 | elif os.path.isdir(input_path): 105 | if args.debug: 106 | print('Creating directory "%s".' % output_path) 107 | os.mkdir(output_path) 108 | dirs.append((direc_list + [filename], inherited_json)) 109 | 110 | elif os.path.isfile(input_path): 111 | # Initially, partial_page_json is just the intrinsic JSON. 112 | _, ext = os.path.splitext(filename) 113 | if ext == '.mako': 114 | filename = filename[:-len(ext)] 115 | partial_page_json = {'url': os.path.join('/', direc, filename)} 116 | 117 | if ext in ['.mako', '.mako_layout']: 118 | # Strip the front matter and maybe insert <%inherit/> tag into the source. 119 | front_matter_json, source = read_and_strip_front_matter(input_path) 120 | partial_page_json = dict(list(inherited_json.items()) + list(front_matter_json.items()) + list(partial_page_json.items())) 121 | if 'layout' in partial_page_json: 122 | try: 123 | source = '<%%inherit file="/%s"/>\n%s' % (partial_page_json['layout'], source) 124 | except: 125 | import pdb; pdb.set_trace() 126 | partial_page_json['layout'] = os.path.join(output_root, partial_page_json['layout']) 127 | try: 128 | os.makedirs(os.path.join(templates_dir, direc)) 129 | except OSError: 130 | pass 131 | 132 | # Write out source to templates_dir, with helpers. 133 | f = open(os.path.join(templates_dir, direc, filename), 'w') 134 | f.write('<%namespace name="sssgen" module="sssgen.mako_helpers"/>') 135 | f.write(source) 136 | f.close() 137 | 138 | # If a .mako, defer rendering until later. 139 | if ext == '.mako': 140 | output_path = output_path[:-len(ext)] 141 | mako_templates_to_render.append((os.path.join(direc, filename), output_path)) 142 | 143 | output_path_to_json[output_path] = partial_page_json 144 | else: 145 | # If it's any other kind of file, just copy it over. 146 | if args.debug: 147 | print('Copying "%s" to "%s".' % (input_path, output_path)) 148 | shutil.copyfile(input_path, output_path) 149 | 150 | # Any concrete file gets added to the 'tree', which is a nested dict. 151 | if ext != '.mako_layout': 152 | t = global_tree 153 | for part in [_f for _f in os.path.normpath(output_path[len(output_root):]).split(os.sep) if _f]: 154 | if part not in t: 155 | t[part] = {} 156 | t = t[part] 157 | 158 | # The leaf (file) gets the JSON, 159 | t.clear() 160 | for k, v in list(partial_page_json.items()): 161 | t[k] = v 162 | else: 163 | assert False 164 | 165 | # Template rendering deferred until now so that each template: 166 | # . gets the full file `tree` in the render-data, 167 | # . has been rewritten with <%inherit/> tags, 168 | # . has had its front matter stripped. 169 | errors = 0 170 | for path, output_path in mako_templates_to_render: 171 | 172 | # The final page JSON is the aggregation of each layout JSON, and then 173 | # the {inherited, front matter, and intrinsic} JSON. 174 | page_json = {} 175 | j = output_path_to_json[output_path] 176 | visited_layouts = [output_path] 177 | while True: 178 | page_json = dict(list(j.items()) + list(page_json.items())) 179 | if 'layout' in j: 180 | if j['layout'] in visited_layouts: 181 | raise Exception('detected a loop in layout resolution: %s' % visited_layouts) 182 | visited_layouts.append(j['layout']) 183 | j = output_path_to_json[j['layout']] 184 | else: 185 | break 186 | 187 | if args.debug: 188 | print('Generating "%s" from "%s", page_json=%s\n.' % (output_path, os.path.join(templates_dir, path), page_json)) 189 | 190 | # Also include `args.input` in TemplateLookup so that things like 191 | # <%include> will work. 192 | lookup = mako.lookup.TemplateLookup(directories=[templates_dir, args.input], input_encoding='utf-8', output_encoding='utf-8') 193 | data = { 194 | 'tree': global_tree, 195 | 'page': page_json 196 | } 197 | f = open(output_path, 'w') 198 | try: 199 | f.write(lookup.get_template(path).render_unicode(**data)) 200 | except Exception as e: 201 | errors += 1 202 | print('Error rendering "%s"!' % path) 203 | print(' Exception: %s' % e) 204 | _, _, exc_tb = sys.exc_info() 205 | print(' Traceback:') 206 | for f_s in traceback.extract_tb(exc_tb): 207 | print(' %s L%i:' % (f_s.filename, f_s.lineno)) 208 | print(' %s' % f_s.line) 209 | f.close() 210 | 211 | if errors > 0: 212 | print('There were %i error(s)!' % errors) 213 | return output_root, False 214 | return output_root, True 215 | 216 | if not args.serve: 217 | _, success = generate() 218 | sys.exit(0 if success else 1) 219 | 220 | class DefaultContentTypeIsHTML(http.server.SimpleHTTPRequestHandler): 221 | extensions_map = http.server.SimpleHTTPRequestHandler.extensions_map 222 | extensions_map[''] = 'text/html' 223 | 224 | class TCPServerWithTimeout(socketserver.TCPServer): 225 | timeout = 1 226 | 227 | output_dir = None 228 | httpd = None 229 | 230 | def generate_and_restart_httpd(): 231 | print('Regenerating..') 232 | 233 | # Stop the httpd server. 234 | global httpd 235 | if httpd is not None: 236 | httpd.server_close() 237 | httpd = None 238 | 239 | # Set up a fresh, empty output folder, otherwise generate() will fail. 240 | global output_dir 241 | if output_dir is not None: 242 | shutil.rmtree(output_dir) 243 | 244 | # If generate() succeeds, restart the httpd server. 245 | output_dir, success = generate() 246 | if success: 247 | os.chdir(output_dir) 248 | socketserver.TCPServer.allow_reuse_address = True 249 | httpd = TCPServerWithTimeout(('localhost', 8000), DefaultContentTypeIsHTML) 250 | print('Serving %s at http://localhost:8000.' % output_dir) 251 | else: 252 | print('Not restarting httpd until the errors are fixed.') 253 | 254 | class EventHandler(watchdog.events.FileSystemEventHandler): 255 | 256 | def __init__(self): 257 | self.changes_queue = queue.Queue() 258 | 259 | def on_any_event(self, e): 260 | if args.debug: 261 | print('EventHandler received: %s' % e) 262 | self.changes_queue.put(e) 263 | 264 | if args.serve: 265 | generate_and_restart_httpd() 266 | observer = watchdog.observers.Observer() 267 | event_handler = EventHandler() 268 | observer.schedule(event_handler, args.input, recursive=True) 269 | observer.start() 270 | try: 271 | while True: 272 | if args.debug: 273 | print('Tick.') 274 | 275 | # See if `watchdog` has noticed any changes in the input directory. 276 | have_changes = False 277 | try: 278 | while True: 279 | _ = event_handler.changes_queue.get(block=False) 280 | time.sleep(0.5) 281 | have_changes = True 282 | except queue.Empty: 283 | pass 284 | if have_changes: 285 | generate_and_restart_httpd() 286 | 287 | # Handle a request, or time out in 1s. 288 | httpd.handle_request() 289 | except KeyboardInterrupt: 290 | observer.stop() 291 | observer.join() 292 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='sssgen', 5 | version='0.6', 6 | author='Edmund Huber', 7 | author_email='me@ehuber.info', 8 | description='Simple Static Site GENerator', 9 | url='https://github.com/edmund-huber/sssgen', 10 | license='MIT', 11 | zip_safe=False, 12 | scripts=['bin/sssgen'], 13 | packages=['sssgen'], 14 | install_requires=[ 15 | 'Mako==1.1.3', 16 | 'MarkupSafe==1.1.1', 17 | 'watchdog==0.10.3' 18 | ], 19 | python_requires='>=3.5' 20 | ) 21 | -------------------------------------------------------------------------------- /sssgen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edmund-huber/sssgen/219da1ecf1784c53529d1e16538a4e18b35a015d/sssgen/__init__.py -------------------------------------------------------------------------------- /sssgen/mako_helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import mako.runtime 4 | 5 | @mako.runtime.supports_caller 6 | def collapse_html(context): 7 | html = mako.runtime.capture(context, context['caller'].body) 8 | collapsed_html = re.sub(">\s*<", "><", html) 9 | return collapsed_html 10 | -------------------------------------------------------------------------------- /tutorial/css/default.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | margin: 0; 8 | line-height: 1.6; 9 | font-size: 19px; 10 | } 11 | 12 | #content > * { 13 | margin-top: 1em; 14 | margin-right: auto; 15 | margin-bottom: 1em; 16 | margin-left: auto; 17 | width: 65%; 18 | } 19 | 20 | #content > div + div { 21 | margin-top: 1.5em; 22 | } 23 | 24 | #content > h1, #content > h2, #content > h3 { 25 | margin-left: 10%; 26 | } 27 | 28 | h1, h2, h3, h4, h5, h6 { 29 | margin-bottom: 1.5em; 30 | margin-top: 2em; 31 | } 32 | 33 | table { 34 | border-collapse: collapse; 35 | font-size: inherit; 36 | } 37 | 38 | .frontpage > *{ 39 | margin: 1em 0; 40 | } 41 | 42 | /* blog styles */ 43 | 44 | .blog-post { 45 | padding: 2em 0; 46 | } 47 | 48 | .blog-post> div { 49 | text-indent: 2em; 50 | } 51 | 52 | .blog-post > div * { 53 | text-indent: 0; 54 | } 55 | 56 | blockquote { 57 | border-left: 2em solid #eee; 58 | font-style: italic; 59 | padding-left: 2em; 60 | } 61 | 62 | code { 63 | background-color: #eee; 64 | border: 1px solid #ddd; 65 | font-size: 90%; 66 | padding: 2px; 67 | } 68 | 69 | #content > code { 70 | display: block; 71 | font-family: monospace; 72 | padding-left: 17.5%; 73 | padding-right: 17.5%; 74 | white-space: pre; 75 | width: 100%; 76 | } 77 | 78 | /* specific to rtl blog post */ 79 | 80 | .annotation { 81 | font-family: monospace; 82 | } 83 | 84 | .annotation td { 85 | padding: 0 5px; 86 | } 87 | 88 | .annotation td:nth-child(2n) { 89 | background-color: #eee; 90 | } 91 | -------------------------------------------------------------------------------- /tutorial/css/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /tutorial/default.html.mako_layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ${page['title']} 6 | 7 | 8 | 9 | 10 |
11 | ${next.body()} 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /tutorial/index.html.mako: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction to sssgen 3 | layout: default.html.mako_layout 4 | --- 5 | 6 |

Introduction

7 | 8 |
9 | An sssgen project is a directory tree of four kinds of files: 10 |
    11 |
  • .mako files: these will be rendered using the Mako templating engine.
  • 12 |
  • .mako_layout files: these are made available to .mako files for layout purposes.
  • 13 |
  • Ignored files: files that exist in the tree but won't in the output tree, for example any file beginning with _.
  • 14 |
  • Any other file is copied to the destination directory.
  • 15 |
16 |
17 | 18 |
19 | The interesting work in an sssgen project is done in 20 | .mako files. For example, the source to to this page is 21 | available at tutorial/index.html.mako. 22 |
23 | 24 |
25 | Every .mako template gets two variables: 26 | page and tree. 27 |
28 | 29 |

The page variable

30 | 31 |
32 | Here is the content of page for this file: 33 |
34 | 35 | <%! 36 | import pprint 37 | %> 38 | 39 |
${pprint.PrettyPrinter(indent=4).pformat(page)}
40 | 41 |
42 | If you look at the source for this page, you'll see that the keys 43 | title and layout were taken from the so-called 44 | "front matter" of the file. The front matter is just YAML placed at the beginning of the file, 46 | delimited by ---. Front matter is one of four sources for the 47 | contents of the page variable: 48 |
49 | 50 |
51 | page is defined as inherited YAML + layout YAML + front matter 52 | YAML + intrinsic YAML. Each N+1th source of page keys overrides 53 | the Nth source. 54 |
55 | 56 |
57 | The first source of keys in the page variable is inherited 58 | YAML. Inherited YAML is the collected of YAML as constructed by walking 59 | down the directory to your file, aggregating _inherit.yaml 60 | files along the way. We don't use inherited YAML in this tutorial but it's 61 | very useful. 62 |
63 | 64 |
65 | The next source of page keys is the chain of layouts. Each 66 | .mako_layout file may contain front matter, which is merged into 67 | page. 68 |
69 | 70 |
71 | Then comes front matter YAML. 72 |
73 | 74 |
75 | The final source of keys is intrinsic YAML. The url key is 76 | provided by sssgen itself. It gives you the URL for this file, 77 | relative to the root of the project. 78 |
79 | 80 |

The tree variable

81 | 82 |
83 | Here is the content of tree for this file: 84 |
85 | 86 |
${pprint.PrettyPrinter(indent=4).pformat(tree)}
87 | 88 |
89 | tree reflects the directory tree of the project. The leaves 90 | are the page variable for a given file. tree is 91 | useful, for example, for listing sub-contents. In the following block of 92 | code, we use the tree variable and the intrinsic YAML key 93 | url that we discussed earlier, to list Spongebob characters: 94 |
95 | 96 | <%text filter="h,trim"> 97 |
    98 | Here are my favorite Spongebob Squarepants characters: 99 | % for name_of_file, yaml in tree['textfiles'].items(): 100 |
  • ${name_of_file}
  • 101 | % endfor 102 |
103 |
104 | 105 | 111 | -------------------------------------------------------------------------------- /tutorial/textfiles/spongebob.txt: -------------------------------------------------------------------------------- 1 | .--..--..--..--..--..--. 2 | .' \ (`._ (_) _ \ 3 | .' | '._) (_) | 4 | \ _.')\ .----..---. / 5 | |(_.' | / .-\-. \ | 6 | \ 0| | ( O| O) | o| 7 | | _ | .--.____.'._.-. | 8 | \ (_) | o -` .-` | 9 | | \ |`-._ _ _ _ _\ / 10 | \ | | `. |_||_| | 11 | | o | \_ \ | -. .-. 12 | |.-. \ `--..-' O | `.`-' .' 13 | _.' .' | `-.-' /-.__ ' .-' 14 | .' `-.` '.|='=.='=.='=.='=|._/_ `-'.' 15 | `-._ `. |________/\_____| `-.' 16 | .' ).| '=' '='\/ '=' | 17 | `._.` '---------------' 18 | //___\ //___\ 19 | || || 20 | LGB ||_.-. ||_.-. 21 | (_.--__) (_.--__) 22 | -------------------------------------------------------------------------------- /tutorial/textfiles/squidward.txt: -------------------------------------------------------------------------------- 1 | .--'''''''''--. 2 | .' .---. '. 3 | / .-----------. \ 4 | / .-----. \ 5 | | .-. .-. | 6 | | / \ / \ | 7 | \ | .-. | .-. | / 8 | '-._| | | | | | |_.-' 9 | | '-' | '-' | 10 | \___/ \___/ 11 | _.-' / \ `-._ 12 | .' _.--| |--._ '. 13 | ' _...-| |-..._ ' 14 | | | 15 | '.___.' 16 | | | 17 | _| |_ 18 | /\( )/\ 19 | / ` ' \ 20 | | | | | 21 | '-' '-' 22 | | | | | 23 | | | | | 24 | | |-----| | 25 | .`/ | | |/`. 26 | | | | | 27 | '._.'| .-. |'._.' 28 | \ | / 29 | | | | 30 | | | | 31 | | | | 32 | /| | |\ 33 | .'_| | |_`. 34 | LGB `. | | | .' 35 | . / | \ . 36 | /o`.-' / \ `-.`o\ 37 | /o o\ .' `. /o o\ 38 | `.___.' `.___.' 39 | --------------------------------------------------------------------------------