├── .gitignore ├── documentation ├── config.yaml ├── content │ ├── features.yaml │ └── index.yaml ├── output │ ├── features │ │ ├── dynamic-pages.html │ │ └── pages.html │ └── index.html └── templates │ ├── base.html │ └── main.html ├── rabbitfish ├── __init__.py ├── cli.py ├── jug.py └── pages.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /documentation/config.yaml: -------------------------------------------------------------------------------- 1 | name: RabbitFish Documentation 2 | description: Documentation for the RabbitFish static site generator. 3 | 4 | pages: 5 | - !Page 6 | name: index 7 | template: main.html 8 | - !DynamicPage 9 | name: features 10 | template: main.html 11 | url: 'features/{slug}.html' 12 | - !IndexPage 13 | name: index 14 | template: index.html 15 | to_index: features 16 | -------------------------------------------------------------------------------- /documentation/content/features.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pages 3 | slug: pages 4 | content: | 5 |

Pages

6 |

7 | Pages are simple HTML pages that RabbitFish generates using 8 | your Jinja2 templates and YAML configuration files. Three things are 9 | required to define a Page: 10 |

    11 |
  1. 12 | An entry to the pages list in your main config.yaml file. 13 |
  2. 14 |
  3. A YAML file containing the content for the Page.
  4. 15 |
  5. A Jinja2 template to create the HTML for the Page.
  6. 16 |
17 |

18 |

19 | In your config.yaml file, the pages list specifies the pages 20 | that RabbitFish will generate for your site. Each entry specifies the 21 | name, template, and (optionally) url that will be used for the Page. If no 22 | url is specified, it will simply append '.html' to the page name and 23 | place it at the root level of your site. The name will be used to 24 | reference that Page from now on. The config.yaml file for 25 | this documentation looks like this: 26 | 27 |

28 |

29 | As with the configuration, the content for your pages is also stored in 30 | YAML files. The contents of the YAML content file for your Page will be 31 | passed in as the template context, so your template can access your 32 | content by using the names of the YAML attributes you created as Jinja2 33 | variables. So if you defined title: Pages in your YAML 34 | configuration file, you will be able to insert that data into your 35 | template as {{ title }}. 36 |

37 |

38 | Your templates are ordinary Jinja2 templates and can use all the normal 39 | features available in Jinja2. 40 |

41 | --- 42 | title: Dynamic Pages 43 | slug: dynamic-pages 44 | content: | 45 |

Dynamic Pages

46 |

47 | Dynamic Pages operate nearly identically to regular pages, 48 | however they allow you to create multiple Pages with a single 49 | entry in your config.yaml. This is excellent for create Pages 50 | such as blog entries. This features section of the documentation is also 51 | created using a Dynamic Page. To do this, 52 | Dynamic Pages require a small amount of extra configuration 53 | in both your YAML content file and your config.yaml file. 54 |

55 |

56 | To create a Dynamic Page your YAML content file will be a 57 | little different. The single YAML file will contain multiple YAML documents 58 | each of which must specify a slug attribute. A 59 | shortened version of the YAML file that is used to generate this page looks 60 | like this: 61 | 62 |

63 |

64 | Unlike a regular Page, when creating a 65 | Dynamic Page it is necessary to specify a url 66 | attribute in your config.yaml file. That URL must also contain a 67 | placeholder for which the Dynamic Page's slug will be 68 | substituded. If your page definition includes a date attribute 69 | it will also made available to your URL. For this to work properly, the 70 | date attribute must be specified as a timestamp like so: 71 | date: !!timestampe 2010-02-19. This is handled using Python 72 | 3's string formatting, so the url for your 73 | Dynamic Page should look something 74 | like this: 75 | url: 'blog/{date.year}/{date.month}/{date.day}/{slug}.html'. 76 | The config.yaml file that was used to generate this documentation, for 77 | example, looks like this: 78 | 79 |

80 | ... 81 | -------------------------------------------------------------------------------- /documentation/content/index.yaml: -------------------------------------------------------------------------------- 1 | title: RabbitFish Documentation 2 | content: | 3 |

4 | RabbitFish is a static site generator written with Python 3. It uses 5 | YAML for storing configuration information 6 | and content, and 7 | Jinja2 for templating. This 8 | documentation was generated with RabbitFish. 9 |

10 |

Features

11 |

12 |

16 |

17 |

18 | For reference, you can view the RabbitFish Site used to generate this 19 | documentation on GitHub: RabbitFish Documentation. 20 |

21 |

Usage

22 |

23 | (From within the directory containing your RabbitFish site and config.yaml) 24 |
rabbitfish generatesite 25 |

26 |

Plans for the Future

27 |

28 |

    29 |
  1. 30 | A URL lookup feature for use in templates similar to Django's 31 | {% url %} or Flask's url_for(). 32 |
  2. 33 |
  3. Static file handling.
  4. 34 |
  5. 35 | Live previewing of your in-development site using Flask so you don't 36 | have to re-generate the site to preview changes that you've made. 37 |
  6. 38 |
  7. 39 | Flask-based web admin to allow you to edit the content on your site 40 | live online and regenerate the files automatically when saving. 41 |
  8. 42 |
43 |

44 | -------------------------------------------------------------------------------- /documentation/output/features/dynamic-pages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dynamic Pages 5 | 6 | 7 |

RabbitFish

8 |

a static site generator

9 |

Dynamic Pages

10 |

11 | Dynamic Pages operate nearly identically to regular pages, 12 | however they allow you to create multiple Pages with a single 13 | entry in your config.yaml. This is excellent for create Pages 14 | such as blog entries. This features section of the documentation is also 15 | created using a Dynamic Page. To do this, 16 | Dynamic Pages require a small amount of extra configuration 17 | in both your YAML content file and your config.yaml file. 18 |

19 |

20 | To create a Dynamic Page your YAML content file will be a 21 | little different. The single YAML file will contain multiple YAML documents 22 | each of which must specify a slug attribute. A 23 | shortened version of the YAML file that is used to generate this page looks 24 | like this: 25 | 26 |

27 |

28 | Unlike a regular Page, when creating a 29 | Dynamic Page it is necessary to specify a url 30 | attribute in your config.yaml file. That URL must also contain a 31 | placeholder for which the Dynamic Page's slug will be 32 | substituded. If your page definition includes a date attribute 33 | it will also made available to your URL. For this to work properly, the 34 | date attribute must be specified as a timestamp like so: 35 | date: !!timestampe 2010-02-19. This is handled using Python 36 | 3's string formatting, so the url for your 37 | Dynamic Page should look something 38 | like this: 39 | url: 'blog/{date.year}/{date.month}/{date.day}/{slug}.html'. 40 | The config.yaml file that was used to generate this documentation, for 41 | example, looks like this: 42 | 43 |

44 | 45 | 46 | -------------------------------------------------------------------------------- /documentation/output/features/pages.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pages 5 | 6 | 7 |

RabbitFish

8 |

a static site generator

9 |

Pages

10 |

11 | Pages are simple HTML pages that RabbitFish generates using 12 | your Jinja2 templates and YAML configuration files. Three things are 13 | required to define a Page: 14 |

    15 |
  1. 16 | An entry to the pages list in your main config.yaml file. 17 |
  2. 18 |
  3. A YAML file containing the content for the Page.
  4. 19 |
  5. A Jinja2 template to create the HTML for the Page.
  6. 20 |
21 |

22 |

23 | In your config.yaml file, the pages list specifies the pages 24 | that RabbitFish will generate for your site. Each entry specifies the 25 | name, template, and (optionally) url that will be used for the Page. If no 26 | url is specified, it will simply append '.html' to the page name and 27 | place it at the root level of your site. The name will be used to 28 | reference that Page from now on. The config.yaml file for 29 | this documentation looks like this: 30 | 31 |

32 |

33 | As with the configuration, the content for your pages is also stored in 34 | YAML files. The contents of the YAML content file for your Page will be 35 | passed in as the template context, so your template can access your 36 | content by using the names of the YAML attributes you created as Jinja2 37 | variables. So if you defined title: Pages in your YAML 38 | configuration file, you will be able to insert that data into your 39 | template as {{ title }}. 40 |

41 |

42 | Your templates are ordinary Jinja2 templates and can use all the normal 43 | features available in Jinja2. 44 |

45 | 46 | 47 | -------------------------------------------------------------------------------- /documentation/output/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RabbitFish Documentation 5 | 6 | 7 |

RabbitFish

8 |

a static site generator

9 |

10 | RabbitFish is a static site generator written with Python 3. It uses 11 | YAML for storing configuration information 12 | and content, and 13 | Jinja2 for templating. This 14 | documentation was generated with RabbitFish. 15 |

16 |

Features

17 |

18 |

22 |

23 |

24 | For reference, you can view the RabbitFish Site used to generate this 25 | documentation on GitHub: RabbitFish Documentation. 26 |

27 |

Usage

28 |

29 | (From within the directory containing your RabbitFish site and config.yaml) 30 |
rabbitfish generatesite 31 |

32 |

Plans for the Future

33 |

34 |

    35 |
  1. 36 | A URL lookup feature for use in templates similar to Django's 37 | {% url %} or Flask's url_for(). 38 |
  2. 39 |
  3. Static file handling.
  4. 40 |
  5. 41 | Live previewing of your in-development site using Flask so you don't 42 | have to re-generate the site to preview changes that you've made. 43 |
  6. 44 |
  7. 45 | Flask-based web admin to allow you to edit the content on your site 46 | live online and regenerate the files automatically when saving. 47 |
  8. 48 |
49 |

50 | 51 | 52 | -------------------------------------------------------------------------------- /documentation/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 |

RabbitFish

8 |

a static site generator

9 | {% block content %}{% endblock %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /documentation/templates/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %}{{ content }}{% endblock %} 4 | -------------------------------------------------------------------------------- /rabbitfish/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshourisman/rabbitfish/0f2b45556b42e95035c73cee8f2ce4c6997ba587/rabbitfish/__init__.py -------------------------------------------------------------------------------- /rabbitfish/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from optparse import OptionParser 5 | 6 | from jug import generatesite 7 | 8 | VERSION = "0.1a" 9 | 10 | 11 | def main(): 12 | print("One dead, unjugged rabbitfish later...", file=sys.stderr) 13 | usage = "usage: %prog ACTION" 14 | version = "%prog {}".format(VERSION) 15 | 16 | parser = OptionParser(prog='rabbitfish', 17 | usage=usage, 18 | version=version) 19 | 20 | options, args = parser.parse_args() 21 | 22 | ACTIONS = { 23 | 'generatesite': generatesite, 24 | } 25 | 26 | try: 27 | action = args[0] 28 | except IndexError: 29 | parser.print_usage() 30 | return 31 | 32 | if action not in ACTIONS: 33 | parser.print_usage() 34 | print('{} is not a supported action.'.format(action), file=sys.stderr) 35 | print('Supported actions include: {}'.format(' ,'.join(ACTIONS)), 36 | file=sys.stderr) 37 | return 38 | 39 | ACTIONS[action]() 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /rabbitfish/jug.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | from pages import Page, DynamicPage, ListPage 4 | 5 | 6 | class Site(object): 7 | def __init__(self, config_file=None): 8 | self.config = None 9 | self.pages = [] 10 | self.dynamic_pages = {} 11 | 12 | if config_file is not None: 13 | self.load_configuration(config_file) 14 | 15 | def load_configuration(self, config_file): 16 | self.config = yaml.load(open(config_file, 'r')) 17 | self.pages = self.config['pages'] 18 | for page in self.pages: 19 | if type(page) is DynamicPage: 20 | self.dynamic_pages[page.name] = page 21 | 22 | def generate(self): 23 | for page in self.pages: 24 | if type(page) is ListPage: 25 | page.render_to_output(self.dynamic_pages) 26 | else: 27 | page.render_to_output() 28 | 29 | 30 | def generatesite(): 31 | site = Site("config.yaml") 32 | site.generate() 33 | -------------------------------------------------------------------------------- /rabbitfish/pages.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import yaml 4 | 5 | from jinja2 import Environment, FileSystemLoader 6 | 7 | env = Environment(loader=FileSystemLoader('templates')) 8 | 9 | 10 | def date(value, format="%X"): 11 | return value.strftime(format) 12 | env.filters['date'] = date 13 | 14 | 15 | class Page(yaml.YAMLObject): 16 | yaml_tag = '!Page' 17 | 18 | def __setstate__(self, state): 19 | if 'url' not in state: 20 | state['url'] = "{}.html".format(state['name']) 21 | state['output'] = "output/{}".format(state['url']) 22 | state['directory'] = os.path.dirname(state['output']) 23 | self.__dict__.update(state) 24 | 25 | def get_context(self, **kwargs): 26 | context = kwargs 27 | context['now'] = datetime.datetime.now() 28 | if 'url' not in context: 29 | context['url'] = self.url 30 | 31 | return context 32 | 33 | def render_to_string(self): 34 | print("Rendering page {0} with {1}.".format(self.name, self.template)) 35 | template = env.get_template(self.template) 36 | content = yaml.load(open("content/{}.yaml".format(self.name))) 37 | return template.render(self.get_context(object=content)) 38 | 39 | def render_to_output(self): 40 | if not os.path.exists(self.directory): 41 | os.makedirs(self.directory) 42 | open(self.output, 'w').write(self.render_to_string()) 43 | 44 | 45 | class DynamicPage(Page): 46 | yaml_tag = '!DynamicPage' 47 | 48 | def __setstate__(self, state): 49 | if 'url' not in state: 50 | state['url'] = "{}.html" 51 | self.__dict__.update(state) 52 | 53 | def get_context(self, **kwargs): 54 | context = super(DynamicPage, self).get_context(**kwargs) 55 | if 'tags' in context['object']: 56 | raw_tags = context['object']['tags'] 57 | if ',' in raw_tags: 58 | split_tags = raw_tags.split(',') 59 | else: 60 | split_tags = raw_tags.split(' ') 61 | context['tags'] = split_tags 62 | 63 | return context 64 | 65 | def render_to_string(self): 66 | print("Rendering dynamic page {0} with {1}".format( 67 | self.name, self.template)) 68 | template = env.get_template(self.template) 69 | content_list = yaml.load_all(open("content/{}.yaml".format(self.name))) 70 | 71 | pages = [] 72 | for content in content_list: 73 | url_context = { 74 | 'slug': content['slug'], 75 | } 76 | if 'date' in content and type(content['date']) in \ 77 | [datetime.date, datetime.datetime]: 78 | url_context['date'] = content['date'] 79 | url = self.url.format(**url_context) 80 | html = template.render(self.get_context( 81 | object=content, 82 | url=url)) 83 | pages.append((url, html)) 84 | 85 | return pages 86 | 87 | def render_to_output(self): 88 | pages = self.render_to_string() 89 | print(" - Rendered {} instances of DynamicPage {}".format( 90 | len(pages), self.name)) 91 | for page in pages: 92 | url = page[0] 93 | html = page[1] 94 | 95 | if url.endswith('/'): 96 | url = url + 'index.html' 97 | output = "output/{}".format(url) 98 | directory = os.path.dirname(output) 99 | if not os.path.exists(directory): 100 | os.makedirs(directory) 101 | open(output, 'w').write(html) 102 | 103 | 104 | class ListPage(Page): 105 | yaml_tag = '!ListPage' 106 | 107 | def __setstate__(self, state): 108 | super(ListPage, self).__setstate__(state) 109 | if 'num_to_index' not in state: 110 | state['index_all'] = True 111 | else: 112 | state['index_all'] = False 113 | self.__dict__.update(state) 114 | 115 | def get_page_url(self, page): 116 | page_type = self.dynamic_pages[self.to_index] 117 | url_format = page_type.url 118 | url_context = {'slug': page['slug']} 119 | if 'date' in page and type(page['date']) in \ 120 | [datetime.date, datetime.datetime]: 121 | url_context['date'] = page['date'] 122 | 123 | url = url_format.format(**url_context) 124 | return url 125 | 126 | def render_to_string(self): 127 | print("Rendering list page {0} with {1}".format( 128 | self.name, self.template)) 129 | template = env.get_template(self.template) 130 | content_list = yaml.load_all( 131 | open("content/{}.yaml".format(self.to_index))) 132 | 133 | index = [] 134 | count = 0 135 | for page in content_list: 136 | if not self.index_all and count >= self.num_to_index: 137 | continue 138 | else: 139 | count += 1 140 | 141 | page['url'] = self.get_page_url(page) 142 | index.append(page) 143 | print(" - Rendered ListPage {} with {} {}".format( 144 | self.name, count, self.to_index)) 145 | return template.render(self.get_context(object_list=index)) 146 | 147 | def render_to_output(self, dynamic_pages): 148 | self.dynamic_pages = dynamic_pages 149 | super(ListPage, self).render_to_output() 150 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | pyyaml 3 | --------------------------------------------------------------------------------