├── .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 | -
12 | An entry to the
pages
list in your main config.yaml file.
13 |
14 | - A YAML file containing the content for the Page.
15 | - A Jinja2 template to create the HTML for the Page.
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 | -
30 | A URL lookup feature for use in templates similar to Django's
31 | {% url %} or Flask's url_for().
32 |
33 | - Static file handling.
34 | -
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 |
38 | -
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 |
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 | -
16 | An entry to the
pages
list in your main config.yaml file.
17 |
18 | - A YAML file containing the content for the Page.
19 | - A Jinja2 template to create the HTML for the Page.
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 | -
36 | A URL lookup feature for use in templates similar to Django's
37 | {% url %} or Flask's url_for().
38 |
39 | - Static file handling.
40 | -
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 |
44 | -
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 |
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 |
--------------------------------------------------------------------------------