├── docs ├── hooks │ ├── __init__.py │ └── __hooks__.py ├── media │ ├── favicon.ico │ └── css │ │ ├── base.css │ │ └── pygments.css ├── README ├── config ├── templates │ ├── index-links.html │ ├── default.html │ ├── index-links-recurse.html │ └── base.html └── content │ ├── footer.mkd │ ├── docs │ ├── devserver.mkd │ ├── glossary.mkd │ ├── content │ │ ├── tagging.mkd │ │ └── categories.mkd │ ├── config.mkd │ ├── urls.mkd │ ├── templates.mkd │ ├── content.mkd │ ├── pagination.mkd │ ├── renderers.mkd │ └── hooks.mkd │ ├── docs.mkd │ ├── home.mkd │ ├── download.mkd │ ├── community.mkd │ └── tutorial.mkd ├── wok ├── contrib │ ├── __init__.py │ └── hooks.py ├── tests │ ├── __init__.py │ ├── test_page.py │ ├── test_engine.py │ └── test_util.py ├── __init__.py ├── exceptions.py ├── util.py ├── jinja.py ├── rst_pygments.py ├── renderers.py ├── dev_server.py ├── engine.py └── page.py ├── requirements.txt ├── scripts └── wok ├── test_site ├── content │ ├── 404.mkd │ ├── pagination-bits │ │ ├── a.mkd │ │ ├── b.mkd │ │ ├── c.mkd │ │ ├── d.mkd │ │ ├── e.mkd │ │ ├── f.mkd │ │ ├── g.mkd │ │ ├── h.mkd │ │ ├── i.mkd │ │ ├── j.mkd │ │ └── k.mkd │ ├── tests │ │ ├── unpublished.txt │ │ ├── dates.mkd │ │ ├── plain.txt │ │ ├── dates2.mkd │ │ ├── chinese.mkd │ │ ├── dates1.mkd │ │ ├── dates3.mkd │ │ ├── rest_titles.rst │ │ ├── markdown.mkd │ │ ├── restructuredtext.rst │ │ └── html_renderer.html │ ├── tests.mkd │ ├── pagination-test.mkd │ └── tests.mkd_ignore ├── templates │ ├── 404.html │ ├── base.html │ ├── default.html │ ├── index.html │ ├── default.html_ignore │ └── pagination.html ├── config ├── wok_expected_output-test_site ├── renderers │ └── __renderers__.py ├── hooks │ └── __hooks__.py └── media │ └── friendly.scss ├── .gitignore ├── bin ├── python-tests └── site-tests ├── .travis.yml ├── LICENSE ├── setup.py ├── CHANGELOG.mkd └── README.mkd /docs/hooks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wok/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wok/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /wok/__init__.py: -------------------------------------------------------------------------------- 1 | version = u'1.1.1' 2 | -------------------------------------------------------------------------------- /wok/exceptions.py: -------------------------------------------------------------------------------- 1 | class DependencyException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /docs/media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythmon/wok/HEAD/docs/media/favicon.ico -------------------------------------------------------------------------------- /scripts/wok: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from wok.engine import Engine 3 | 4 | Engine() 5 | -------------------------------------------------------------------------------- /test_site/content/404.mkd: -------------------------------------------------------------------------------- 1 | title: 404 2 | type: 404 3 | --- 4 | This means it wasn't found. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | *.pyc 3 | /build 4 | /MANIFEST 5 | */output 6 | /venv/ 7 | /wok.egg-info/ 8 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/a.mkd: -------------------------------------------------------------------------------- 1 | title: A 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page A 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/b.mkd: -------------------------------------------------------------------------------- 1 | title: B 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page B 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/c.mkd: -------------------------------------------------------------------------------- 1 | title: C 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page C 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/d.mkd: -------------------------------------------------------------------------------- 1 | title: D 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page D 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/e.mkd: -------------------------------------------------------------------------------- 1 | title: E 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page E 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/f.mkd: -------------------------------------------------------------------------------- 1 | title: F 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page F 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/g.mkd: -------------------------------------------------------------------------------- 1 | title: G 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page G 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/h.mkd: -------------------------------------------------------------------------------- 1 | title: H 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page H 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/i.mkd: -------------------------------------------------------------------------------- 1 | title: I 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page I 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/j.mkd: -------------------------------------------------------------------------------- 1 | title: J 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page J 6 | -------------------------------------------------------------------------------- /test_site/content/pagination-bits/k.mkd: -------------------------------------------------------------------------------- 1 | title: K 2 | category: pagination 3 | tags: tiny 4 | --- 5 | Page K 6 | -------------------------------------------------------------------------------- /bin/python-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $PYTHON_TESTS == "false" ]]; then 4 | exit 0 5 | fi 6 | 7 | py.test wok 8 | -------------------------------------------------------------------------------- /docs/README: -------------------------------------------------------------------------------- 1 | This is a wok site. To see it as rendered HTML, run wok and look in the 2 | generated 'output' directory. 3 | -------------------------------------------------------------------------------- /docs/config: -------------------------------------------------------------------------------- 1 | site_title: Wok 2 | url_include_index: no 3 | url_pattern: "/{category}/{slug}/{page}/index.html" 4 | slug_from_filename: True 5 | -------------------------------------------------------------------------------- /docs/hooks/__hooks__.py: -------------------------------------------------------------------------------- 1 | from wok.contrib.hooks import HeadingAnchors 2 | 3 | hooks = { 4 | 'page.template.post': [ HeadingAnchors() ], 5 | } 6 | -------------------------------------------------------------------------------- /test_site/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | This is a 404 5 | 6 | {{ page.content }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /test_site/content/tests/unpublished.txt: -------------------------------------------------------------------------------- 1 | title: unpublished 2 | category: tests 3 | published: false 4 | --- 5 | This page should not appear in indexes, or rendered to a file. 6 | -------------------------------------------------------------------------------- /test_site/config: -------------------------------------------------------------------------------- 1 | site_title: Wok Scratch Site 2 | author: Default 3 | url_pattern: "/{category}/{slug}{page}/index.{ext}" 4 | url_include_index: no 5 | ignore_files: [ '*_ignore', '*~' ] 6 | -------------------------------------------------------------------------------- /test_site/content/tests/dates.mkd: -------------------------------------------------------------------------------- 1 | title: Dates 2 | type: index 3 | category: tests 4 | date: 2014-04-29 5 | url: "/{category}/{slug}{page}/{date.year}/{date.month}/{date.day}/index.{ext}" 6 | --- 7 | -------------------------------------------------------------------------------- /test_site/content/tests.mkd: -------------------------------------------------------------------------------- 1 | title: Tests Main 2 | slug: tests 3 | type: index 4 | url: /index{page}.html 5 | --- 6 | These are the tests 7 | 8 | The pagination test is [here](/pagination/index.html). 9 | -------------------------------------------------------------------------------- /test_site/content/tests/plain.txt: -------------------------------------------------------------------------------- 1 | title: Plaintext Test Page 2 | slug: plain 3 | category: tests 4 | tags: [plain, sample] 5 | --- 6 | This should be plain text 7 | With *none* of that "mark up" crap 8 | And look like haiku 9 | -------------------------------------------------------------------------------- /test_site/content/tests/dates2.mkd: -------------------------------------------------------------------------------- 1 | title: Date and time 2 | date: 2011-10-12 3 | time: 12:20:00 4 | category: tests/dates 5 | url: "/{category}/{slug}{page}/{date.year}/{date.month}/{date.day}/index.{ext}" 6 | --- 7 | This a date and time 8 | -------------------------------------------------------------------------------- /test_site/content/tests/chinese.mkd: -------------------------------------------------------------------------------- 1 | title: Chinese Test Page 中華民族 2 | slug: chinese 3 | category: tests 4 | tags: [chinese, sample, unicode] 5 | --- 6 | This is a test page for Chinese characters. 7 | ------------------------------ 8 | 9 | 中華民族 10 | -------------------------------------------------------------------------------- /test_site/content/tests/dates1.mkd: -------------------------------------------------------------------------------- 1 | title: Datetime only 2 | datetime: 2011-10-12 12:20:00 3 | category: tests/dates 4 | url: "/{category}/{slug}{page}/{date.year}-{date.month}-{date.day}-{time.hour}-{datetime.minute}/index.{ext}" 5 | --- 6 | This only has a datetime 7 | -------------------------------------------------------------------------------- /docs/templates/index-links.html: -------------------------------------------------------------------------------- 1 | {%- extends "base.html" %} 2 | {%- block content %} 3 | {{ super() }} 4 | 9 | {%- endblock %} 10 | -------------------------------------------------------------------------------- /test_site/content/tests/dates3.mkd: -------------------------------------------------------------------------------- 1 | title: Time overwriding datetime. 2 | datetime: 2011-10-12 12:20:00 3 | time: 15:00:00 4 | category: tests/dates 5 | url: "/{category}/{slug}{page}/{date.year}/{date.month}/{date.day}/index.{ext}" 6 | --- 7 | In this a datetime is overwritten by at time. 8 | -------------------------------------------------------------------------------- /docs/content/footer.mkd: -------------------------------------------------------------------------------- 1 | title: Footer 2 | make_file: no 3 | --- 4 | ©2011 Mike Cooper, et al. 5 | 6 | This site is powered by [wok][]. 7 | 8 | Fork me on [Github][fork]. 9 | 10 | [wok]: https://github.com/mythmon/wok 11 | [fork]: https://github.com/mythmon/wok/tree/master/docs 12 | -------------------------------------------------------------------------------- /docs/templates/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {%- block content %} 4 |
5 |

{{ page.title }}

6 | {%- if page.subtitle %} 7 |

{{ page.subtitle }}

8 | {%- endif %} 9 |
10 | {{ page.content }} 11 | {%- endblock %} 12 | -------------------------------------------------------------------------------- /test_site/content/pagination-test.mkd: -------------------------------------------------------------------------------- 1 | title: Pagination Test 2 | slug: pagination 3 | type: pagination 4 | pagination: 5 | list: page.subpages 6 | limit: 3 7 | sort_key: slug 8 | sort_reverse: True 9 | --- 10 | Let's test pagination. 11 | 12 | You should see links to the pages A-K, reverse sorted by name, chunked into groups of 3. 13 | -------------------------------------------------------------------------------- /test_site/content/tests.mkd_ignore: -------------------------------------------------------------------------------- 1 | title: Tests Main 2 | slug: tests 3 | type: index 4 | url: /index{page}.html 5 | --- 6 | This is a bogus file should be ignored by Wok if the configuration 7 | directive 8 | ``` 9 | ignore_files = [ '*_ignore' ] 10 | ``` 11 | is set. 12 | 13 | 14 | These are the tests 15 | 16 | The pagination test is [here](/pagination/index.html). 17 | -------------------------------------------------------------------------------- /docs/templates/index-links-recurse.html: -------------------------------------------------------------------------------- 1 | {%- extends "default.html" %} 2 | {%- block content %} 3 | {{ super() }} 4 | 12 | {%- endblock %} 13 | -------------------------------------------------------------------------------- /test_site/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Wok Test 5 | 6 | 7 | 8 | {% block body %} 9 | {% endblock body %} 10 | 11 |

12 | Footer 13 | Site by: {{site.author}} 14 | Last generated: Datetime: {{site.datetime}}, Date: {{site.date}}, Time: {{site.time}} 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test_site/templates/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 |

{{ page.title }}

4 |

by: {% for author in page.authors %} 5 | {{author}}{% if not loop.last %}, {% endif %} 6 | {% endfor %}

7 |

datetime: {{page.datetime}}, date: {{page.date}}, time: {{page.time}}

8 |

Tags:

9 | 14 | Hooked: {{ hooked }} 15 | {{ page.content }} 16 | {% endblock body %} 17 | -------------------------------------------------------------------------------- /test_site/wok_expected_output-test_site: -------------------------------------------------------------------------------- 1 | WARNING:root:Textile not enabled. 2 | WARNING:root:HTML rendering relies on the BeautifulSoup library. 3 | WARNING:root:To use compile_sass hook, you must install libsass-python package. 4 | ERROR:root:It looks like the page "content/tests/dates1.mkd" is an orphan! This will probably cause problems. 5 | ERROR:root:It looks like the page "content/tests/dates2.mkd" is an orphan! This will probably cause problems. 6 | ERROR:root:It looks like the page "content/tests/dates3.mkd" is an orphan! This will probably cause problems. 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: ["2.7"] 3 | sudo: false 4 | 5 | env: 6 | - PYTHON_TESTS=true TEST_SITE=false CMP_OUTPUT=false 7 | - PYTHON_TESTS=false TEST_SITE=docs CMP_OUTPUT=false 8 | - PYTHON_TESTS=false TEST_SITE=test_site CMP_OUTPUT=true 9 | - PYTHON_TESTS=false TEST_SITE=OSULUG/OSULUG-website CMP_OUTPUT=false 10 | 11 | script: 12 | - bin/python-tests 13 | - bin/site-tests 14 | 15 | notifications: 16 | email: false 17 | irc: 18 | channels: 19 | - "chat.freenode.net#wok" 20 | on_success: always 21 | on_failure: always 22 | -------------------------------------------------------------------------------- /test_site/content/tests/rest_titles.rst: -------------------------------------------------------------------------------- 1 | title: ReST Title test 2 | category: tests 3 | --- 4 | ============= 5 | This is an h1 6 | ============= 7 | Text of an h1 8 | 9 | ------------- 10 | This is an h2 11 | ------------- 12 | Text of an h2 13 | 14 | ================== 15 | This is another h1 16 | ================== 17 | another h1 text 18 | 19 | ---------- 20 | Another h2 21 | ---------- 22 | Text in an h2 23 | 24 | ~~~~~~~~~~~~~ 25 | This is an h3 26 | ~~~~~~~~~~~~~ 27 | Text in an h3 28 | 29 | an h4 30 | ===== 31 | text in an h4 32 | 33 | an h5 34 | ----- 35 | text in an h5 36 | 37 | an h6 38 | ~~~~~ 39 | text in an h6 40 | 41 | This is text 42 | -------------------------------------------------------------------------------- /test_site/renderers/__renderers__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | try: 4 | from bs4 import BeautifulSoup 5 | def render(plain, page_meta): 6 | soup = BeautifulSoup(plain) 7 | return soup.body 8 | 9 | except ImportError: 10 | import cgi 11 | logging.warning('HTML rendering relies on the BeautifulSoup library.') 12 | def render(plain, page_meta): 13 | return '

Rendering error

' \ 14 | + '

BeautifulSoup could not be loaded.

' \ 15 | + '

Original plain document follows:

' \ 16 | + '

' + cgi.escape(plain) + '

' 17 | 18 | 19 | renderers = { 20 | 'html': type('',(object,), { 'render': staticmethod(render) }) 21 | } 22 | -------------------------------------------------------------------------------- /wok/tests/test_page.py: -------------------------------------------------------------------------------- 1 | try: 2 | from twisted.trial.unittest import TestCase 3 | except ImportError: 4 | from unittest import TestCase 5 | 6 | from wok.page import Author 7 | 8 | class TestAuthor(TestCase): 9 | 10 | def test_author(self): 11 | a = Author.parse('Bob Smith') 12 | self.assertEqual(a.raw, 'Bob Smith') 13 | self.assertEqual(a.name, 'Bob Smith') 14 | 15 | a = Author.parse('Bob Smith ') 16 | self.assertEqual(a.raw, 'Bob Smith ') 17 | self.assertEqual(a.name, 'Bob Smith') 18 | self.assertEqual(a.email, 'bob@here.com') 19 | 20 | a = Author.parse('') 21 | self.assertEqual(a.raw, '') 22 | self.assertEqual(a.email, 'bob@here.com') 23 | -------------------------------------------------------------------------------- /test_site/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 | 4 |

{{page.title}}

5 | {{ page.content }} 6 | 7 |
8 | 9 | {% for subpage in page.subpages %} 10 |

{{subpage.title}}

11 | {{subpage.content}} 12 | {% endfor %} 13 | 14 |
15 | 16 |

Tags

17 | {% for tag in site.tags %} 18 |

{{tag}}

19 |
    20 | {% for page in site.tags[tag] %} 21 |
  • {{page.title}}
  • 22 | {% endfor %} 23 |
24 | {% endfor %} 25 | 26 |
27 | 28 |
{{ site.pages }}
29 | 30 |
31 | 32 |

Top Level Categories

33 |
    34 | {% for cat in site.categories %} 35 |
  • {{ cat }}
  • 36 | {% endfor %} 37 |
38 | 39 | {% endblock body %} 40 | -------------------------------------------------------------------------------- /test_site/content/tests/markdown.mkd: -------------------------------------------------------------------------------- 1 | title: Markdown Test Page 2 | slug: markdown 3 | category: tests 4 | tags: [markdown, sample] 5 | author: [Me , Myself , I ] 6 | --- 7 | This is the markdown test page 8 | ------------------------------ 9 | This page tests 10 | 11 | - That markdown rendering works 12 | - That it supports footnotes[^1]. 13 | - That code syntax highlighting works 14 | - That definition lists work 15 | 16 | The plugins enabled are 17 | 18 | Definition Lists 19 | : To do this 20 | Footnotes 21 | : Cause they are nice 22 | CodeHilite 23 | : To do syntax highlighting 24 | 25 | [^1]: Because those are nice. 26 | 27 | ### Code Sample 28 | 29 | ::python 30 | for i in range(5): 31 | print("Hello world for the {0}th time!".format(i)) 32 | -------------------------------------------------------------------------------- /test_site/templates/default.html_ignore: -------------------------------------------------------------------------------- 1 | 8 | {% extends "base.html" %} 9 | {% block body %} 10 |

{{ page.title }}

11 |

by: {% for author in page.authors %} 12 | {{author}}{% if not loop.last %}, {% endif %} 13 | {% endfor %}

14 |

datetime: {{page.datetime}}, date: {{page.date}}, time: {{page.time}}

15 |

Tags:

16 |
    17 | {% for tag in page.tags %} 18 |
  • {{ tag }}
  • 19 | {% endfor %} 20 |
21 | Hooked: {{ hooked }} 22 | {{ page.content }} 23 | {% endblock body %} 24 | -------------------------------------------------------------------------------- /docs/content/docs/devserver.mkd: -------------------------------------------------------------------------------- 1 | title: Development Server 2 | slug: devserver 3 | category: docs 4 | --- 5 | Wok provides a simple HTTP server that can be used to test your site during 6 | development. You can run it like this 7 | 8 | ::console 9 | $ wok --server 10 | 11 | This will generate your site, and run a simple server on localhost port 8000 12 | 13 | You can also specify the port and address to listen on. 14 | 15 | - `--port N` - Specify the port to listen on. The default is 8000. 16 | - `--address A` - Specify the address to bind to. The default is 0.0.0.0. 17 | 18 | You will have to restart the server any time something changes, even media like 19 | CSS or images. Wok currently won't notice if you change something. 20 | 21 | > **Warning:** Don't even *think* about running this in anything resembling a 22 | > production environment. It's slow, single-threaded, and very probably 23 | > insecure. It should only be used for testing purposes. 24 | -------------------------------------------------------------------------------- /bin/site-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | case "$TEST_SITE" in 4 | "false"|"") 5 | echo 'No $TEST_SITE requested, not testing.' 6 | exit 0 7 | ;; 8 | */*) 9 | dir=$(echo $TEST_SITE | awk -F "/" '{print $2}') 10 | if [[ -d $dir && -d $dir/.git ]]; then 11 | rm -rf $dir 12 | fi 13 | git clone --depth 1 "https://github.com/$TEST_SITE" $dir 14 | if [[ $? -gt 0 ]]; then 15 | exit 1 16 | fi 17 | TEST_SITE=$dir 18 | ;; 19 | esac 20 | 21 | if [[ ! -d $TEST_SITE ]]; then 22 | echo "Error: Site $TEST_SITE not found." 23 | exit 1 24 | fi 25 | 26 | echo "Testing site $TEST_SITE" 27 | 28 | cd $TEST_SITE 29 | EXPECTED_FILE=wok_expected_output-${TEST_SITE} 30 | OUTPUT_FILE=wok_output_travis-ci.${TEST_SITE}.`date +%Y%m%d-%H%M%S`.txt 31 | wok -v > $OUTPUT_FILE 2>&1 32 | rc=$? 33 | 34 | if [ -n "$CMP_OUTPUT" -a "$CMP_OUTPUT" = "true" ]; then 35 | echo "Comparing output $EXPECTED_FILE $OUTPUT_FILE" 36 | diff $EXPECTED_FILE $OUTPUT_FILE 37 | rc=$? 38 | fi 39 | 40 | exit $rc 41 | -------------------------------------------------------------------------------- /docs/content/docs/glossary.mkd: -------------------------------------------------------------------------------- 1 | title: Glossary 2 | category: docs 3 | --- 4 | ### Slug 5 | 6 | A slug is a url-friendly representation of the name of a page. It should be 7 | unique across the site, should be all lower case, and not contain punctuation 8 | (beyond what fits in a url), non-ascii characters, or spaces. For example, here 9 | are some titles and how wok will automatically slugify them. 10 | 11 | - "My trip to the zoo" → `my-trip-to-the-zoo` 12 | - "This is a title. Or is it?" → `this-is-a-title-or-is-it` 13 | - "Books!" → `books` 14 | 15 | You can also manually specify a slug on a content file, with the metadata 16 | attribute `slug`. For example 17 | 18 | ::yaml 19 | title: My awesome trip to the zoo last Thursday. 20 | slug: zoo 21 | 22 | Since slugs are often used in urls, this can make your urls a lot nicer 23 | looking, instead of 100 character monsters. There are also times that you might 24 | reference a slug from a template, and short names are easier to remember and 25 | type. 26 | -------------------------------------------------------------------------------- /test_site/hooks/__hooks__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from wok.contrib.hooks import compile_sass 3 | 4 | hook_count = 0 5 | def make_hook(name): 6 | def logging_hook(*args): 7 | global hook_count 8 | logging.info('logging_hook: {0}: {1}'.format(name, hook_count)) 9 | hook_count += 1 10 | return [logging_hook] 11 | 12 | hooks = { 13 | 'site.start': make_hook('site.start'), 14 | 'site.output.pre': make_hook('site.output.pre'), 15 | 'site.output.post': [compile_sass], 16 | 'site.content.gather.pre': make_hook('site.content.gather.pre'), 17 | 'site.content.gather.post': make_hook('site.content.gather.post'), 18 | 'page.meta.pre': make_hook('page.template.pre'), 19 | 'page.meta.post': make_hook('page.template.post'), 20 | 'page.render.pre': make_hook('page.template.pre'), 21 | 'page.render.post': make_hook('page.template.post'), 22 | 'page.template.pre': make_hook('page.template.pre'), 23 | 'page.template.post': make_hook('page.template.post'), 24 | 'site.stop': make_hook('site.stop'), 25 | } 26 | 27 | logging.info('loaded hooks.') 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Wok is a static website generator. 2 | Copyright (C) 2011 Michael Cooper, et al. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | The software is provided "as is", without warranty of any kind, express or 15 | implied, including but not limited to the warranties of merchantability, 16 | fitness for a particular purpose and noninfringement. In no event shall the 17 | authors or copyright holders be liable for any claim, damages or other 18 | liability, whether in an action of contract, tort or otherwise, arising from, 19 | out of or in connection with the software or the use or other dealings in the 20 | software. 21 | -------------------------------------------------------------------------------- /test_site/content/tests/restructuredtext.rst: -------------------------------------------------------------------------------- 1 | title: reStructuredText Test Page 2 | slug: rst 3 | category: tests 4 | tags: [rest, sample] 5 | --- 6 | This is the reStructuredText test page 7 | ====================================== 8 | This page tests 9 | 10 | * That reStructuredText rendering works. 11 | * That I know how to make a RST document. 12 | * Python code highlighting works. 13 | 14 | .. sourcecode:: python 15 | 16 | class Author(object): 17 | """Smartly manages a author with name and email""" 18 | parse_author_regex = re.compile(r'([^<>]*)( +<(.*@.*)>)$') 19 | 20 | def __init__(self, raw='', name=None, email=None): 21 | self.raw = raw 22 | self.name = name 23 | self.email = email 24 | 25 | @classmethod 26 | def parse(cls, raw): 27 | a = cls(raw) 28 | a.name, _, a.email = cls.parse_author_regex.match(raw).groups() 29 | 30 | def __str__(self): 31 | if not self.name: 32 | return self.raw 33 | if not self.email: 34 | return self.name 35 | 36 | return "{0} <{1}>".format(self.name, self.email) 37 | -------------------------------------------------------------------------------- /test_site/templates/pagination.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block body %} 3 | 4 |

{{page.title}}

5 | {{ page.content }} 6 | 7 |
8 | 9 | {% if pagination %} 10 | 11 | {% if pagination.prev_page %} 12 | Previous Page 13 | {% endif %} 14 | Page {{ pagination.cur_page }} of {{ pagination.num_pages }} 15 | {% if pagination.next_page %} 16 | Next Page 17 | {% endif %} 18 | 19 | {% for subpage in pagination.page_items %} 20 |

{{subpage.title}}

21 | {{subpage.content}} 22 | {% endfor %} 23 | {% endif %} 24 | 25 |
26 | 27 |

Tags

28 | {% for tag in site.tags %} 29 |

{{tag}}

30 |
    31 | {% for page in site.tags[tag] %} 32 |
  • {{page.title}}
  • 33 | {% endfor %} 34 |
35 | {% endfor %} 36 | 37 |
38 | 39 |
{{ site.pages }}
40 | 41 |
42 | 43 |

Top Level Categories

44 |
    45 | {% for cat in site.categories %} 46 |
  • {{ cat }}
  • 47 | {% endfor %} 48 |
49 | 50 | {% endblock body %} 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | from setuptools import setup 4 | 5 | from wok import version 6 | 7 | setup( 8 | name='wok', 9 | version=version.encode("utf8"), 10 | author='Mike Cooper', 11 | author_email='mythmon@gmail.com', 12 | url='http://wok.mythmon.com', 13 | description='Static site generator', 14 | long_description= 15 | "Wok is a static website generator. It turns a pile of templates, " 16 | "content, and resources (like CSS and images) into a neat stack of " 17 | "plain HTML. You run it on your local computer, and it generates a " 18 | "directory of web files that you can upload to your web server, or " 19 | "serve directly.", 20 | download_url="http://wok.mythmon.com/download", 21 | classifiers=[ 22 | "Development Status :: 4 - Beta", 23 | "License :: OSI Approved :: MIT License", 24 | 'Operating System :: POSIX', 25 | 'Programming Language :: Python', 26 | ], 27 | install_requires=[ 28 | 'Jinja2==2.6', 29 | 'Markdown==2.6.5', 30 | 'PyYAML==3.10', 31 | 'Pygments==2.1', 32 | 'docutils>=0.8.1', 33 | 'awesome-slugify==1.4', 34 | 'pytest==2.5.2', 35 | ], 36 | packages=['wok'], 37 | package_data={'wok':['contrib/*']}, 38 | scripts=['scripts/wok'], 39 | ) 40 | -------------------------------------------------------------------------------- /test_site/content/tests/html_renderer.html: -------------------------------------------------------------------------------- 1 | title: HTML Renderer Test Page 2 | slug: html-renderer 3 | category: tests 4 | tags: [renderers, sample] 5 | author: [Me , Myself , I ] 6 | --- 7 | 8 | 9 | This is the HTML Renderer test page 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

This is a sample HTML test page

21 |

22 | This page is rendered by a special renderer which is provided in 23 | the renderers directory, with the help of 24 | the BeautifulSoup python package. 25 |

26 |

27 | The source document contains a long header 28 | with meta and link tags that should be 29 | dropped, leaving only a the content of the body; a 30 | title and these two paragraphs. 31 |

32 | 33 | -------------------------------------------------------------------------------- /docs/content/docs.mkd: -------------------------------------------------------------------------------- 1 | title: Docs 2 | tags: [_nav] 3 | nav_sort: 2 4 | type: index-links-recurse 5 | --- 6 | 7 | Install 8 | ------- 9 | If you just want to use wok, then you should install it from the 10 | [Python Package Index][pypi]. A good way to do that is with `pip`: 11 | 12 | ::console 13 | $ sudo pip install wok 14 | 15 | This will install wok and the required dependencies. It will also install some optional dependencies: the two rendering libraries, [Markdown][mkd], and [reStructuredTest][rst]. It will also install a syntax highlighting library, [Pygments][pgmnts]. 16 | 17 | [pypi]: http://pypi.python.org/pypi 18 | [mkd]: http://daringfireball.net/projects/markdown/ 19 | [rst]: http://docutils.sourceforge.net/rst.html 20 | [pgmnts]: http://pygments.org/ 21 | 22 | Tutorial 23 | -------- 24 | For a quick start guide to wok, check out [the tutorial](/tutorial/), which 25 | goes over some basics about content and templates. Nothing too fancy, but it 26 | should get you started. 27 | 28 | Contribute 29 | ---------- 30 | You can find wok's source code on [Github][gh]. If you find any issues, 31 | please post them to [the issue tracker][gh-issues]. If you want to help 32 | code, feel free. Patches and pull requests are welcome. 33 | 34 | [gh]: https://github.com/mythmon/wok 35 | [gh-issues]: https://github.com/mythmon/wok/issues 36 | 37 | Specific Topics 38 | --------------- 39 | 40 | -------------------------------------------------------------------------------- /docs/content/docs/content/tagging.mkd: -------------------------------------------------------------------------------- 1 | title: Tagging 2 | category: docs/content 3 | --- 4 | Wok offers a [tagging][] system to help categorize your [content][]. You can 5 | tag content by including a YAML list in the `tags` field of the metadata, e.g. 6 | 7 | ::yaml 8 | title: My new eco-friendly pants 9 | tags: [recycling, bottomware] 10 | --- 11 | TIL pants made from recycled tires are *hot*. 12 | 13 | [tagging]: http://en.wikipedia.org/wiki/Tag_(metadata) 14 | [content]: /docs/content/ 15 | 16 | You can use tags like so: 17 | 18 | * In a content file's metadata, you can specify tags as a list, e.g., 19 | `tags: [foo, bar, herp, derp]` 20 | * In a template, you can access the current page's tags with `{{ page.tags }}`. 21 | This is a simple list of strings. 22 | * In a template, you can also access all the tags on the site with `{{ 23 | site.tags }}`, which is a dictionary with the tags as keys, and lists of 24 | pages tagged with the specified tag as values. E.g., `{{ site.tags['foo'] }}` 25 | would return every page tagged with 'foo'. 26 | 27 | Example template 28 | ---------------- 29 | This template fragment will load all content pages on the entire site with the 30 | tag "frontpage" and display links to them in a list. 31 | 32 | ::jinja2 33 | 38 | 39 | -------------------------------------------------------------------------------- /wok/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | from unicodedata import normalize 3 | from datetime import date, time, datetime, timedelta 4 | 5 | def chunk(li, n): 6 | """Yield succesive n-size chunks from l.""" 7 | for i in xrange(0, len(li), n): 8 | yield li[i:i+n] 9 | 10 | def date_and_times(meta): 11 | 12 | date_part = None 13 | time_part = None 14 | 15 | if 'date' in meta: 16 | date_part = meta['date'] 17 | 18 | if 'time' in meta: 19 | time_part = meta['time'] 20 | 21 | if 'datetime' in meta: 22 | if date_part is None: 23 | if isinstance(meta['datetime'], datetime): 24 | date_part = meta['datetime'].date() 25 | elif isinstance(meta['datetime'], date): 26 | date_part = meta['datetime'] 27 | 28 | if time_part is None and isinstance(meta['datetime'], datetime): 29 | time_part = meta['datetime'].time() 30 | 31 | if isinstance(time_part, int): 32 | seconds = time_part % 60 33 | minutes = (time_part / 60) % 60 34 | hours = (time_part / 3600) 35 | 36 | time_part = time(hours, minutes, seconds) 37 | 38 | meta['date'] = date_part 39 | meta['time'] = time_part 40 | 41 | if date_part is not None and time_part is not None: 42 | meta['datetime'] = datetime(date_part.year, date_part.month, 43 | date_part.day, time_part.hour, time_part.minute, 44 | time_part.second, time_part.microsecond, time_part.tzinfo) 45 | elif date_part is not None: 46 | meta['datetime'] = datetime(date_part.year, date_part.month, date_part.day) 47 | else: 48 | meta['datetime'] = None 49 | -------------------------------------------------------------------------------- /docs/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ site.title }} ~ {{ page.title }} 5 | 6 | 7 | 8 | {% block head %}{% endblock %} 9 | 20 | 21 | 22 | 23 |
24 |
25 |

wok

26 | 35 |
36 | 37 |
38 | {%- block content %} 39 | {%- endblock %} 40 |
41 | 42 |
43 | {{ site.slugs.footer.content|safe }} 44 |

Last generated: {{ site.datetime.strftime('%c') }}.

45 |
46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/content/home.mkd: -------------------------------------------------------------------------------- 1 | title: Home 2 | url: "/index.html" 3 | tags: [_nav] 4 | nav_sort: 1 5 | --- 6 | Wok is a static website generator. It turns a pile of templates, 7 | content, and resources (like CSS and images) into a neat stack of plain 8 | HTML. You run it on your local computer, and it generates a directory of 9 | web files that you can upload to your web server, or serve directly. 10 | 11 | The idea is that you don't need a big server-side engine like PHP, [Drupal][], 12 | [Django][], etc. to generate every page, every visit, for every visitor. 13 | Caching helps, but it can only do so much. Instead, you can generate the entire 14 | site once ahead of time, and only regenerate things when something has changed, 15 | e.g, with a post-commit hook on a Git repository containing your content or 16 | layout. 17 | 18 | Wok is similar in concept to other static site generators, like 19 | [Jekyll][], [Hyde][], and [nanoc][], but different in a few ways, like 20 | a page/subpage category system, and a restricted focus of features. For 21 | example, instead of providing tools to generate RSS feeds directly, the 22 | tools that are needed to build a generic XML document are provided. 23 | 24 | [drupal]: http://drupal.org 25 | [django]: http://djangoproject.com 26 | [jekyll]: https://github.com/mojombo/jekyll 27 | [hyde]: https://github.com/lakshmivyas/hyde 28 | [nanoc]: http://nanoc.stoneship.org/ 29 | 30 | Features 31 | -------- 32 | 33 | - Content and presentation are separated 34 | - [Markdown][mkd], [reStructuredText][rst], and plain text renders 35 | - [Jinja2][]-based templating system 36 | - Optional syntax highlighting via [Pygments][] 37 | - Tagging and a hierarchical category system 38 | - Simple development server 39 | - Pagination support 40 | - Custom Python hooks that run during site generation 41 | 42 | [jinja2]: http://jinja.pocoo.org 43 | [mkd]: http://daringfireball.net/projects/markdown/ 44 | [rst]: http://docutils.sourceforge.net/rst.html 45 | [pygments]: http://pygments.org/ 46 | -------------------------------------------------------------------------------- /docs/content/download.mkd: -------------------------------------------------------------------------------- 1 | title: Download 2 | tags: [_nav] 3 | nav_sort: 4 4 | --- 5 | 6 | The recommended way to get wok is from the [Python Package Index][pypi]. You 7 | can do that with `pip`: 8 | 9 | ::console 10 | $ sudo pip install wok 11 | 12 | This will install wok and the required dependencies. It will also install some optional dependencies: the two rendering libraries, [Markdown][mkd], and [reStructuredTest][rst]. It will also install a syntax highlighting library, [Pygments][]. 13 | 14 | [pypi]: http://pypi.python.org/pypi 15 | [mkd]: http://daringfireball.net/projects/markdown/ 16 | [rst]: http://docutils.sourceforge.net/rst.html 17 | [pygments]: http://pygments.org/ 18 | 19 | Other Methods 20 | ============= 21 | 22 | You can also checkout out the code [from Github][gh], or download a tarball: 23 | 24 | Latest Stable 25 | ------------- 26 | 27 | - [Version 0.9](https://github.com/mythmon/wok/tarball/v0.9) 28 | 29 | Older Versions 30 | -------------- 31 | 32 | - [Version 0.8.2](https://github.com/mythmon/wok/tarball/v0.8.2) 33 | - [Version 0.8.1](https://github.com/mythmon/wok/tarball/v0.8.1) 34 | - [Version 0.8](https://github.com/mythmon/wok/tarball/v0.8) 35 | - [Version 0.7](https://github.com/mythmon/wok/tarball/v0.7) 36 | - [Version 0.6.3](https://github.com/mythmon/wok/tarball/v0.6.3) 37 | - [Version 0.6.2](https://github.com/mythmon/wok/tarball/v0.6.2) 38 | - [Version 0.6.1](https://github.com/mythmon/wok/tarball/v0.6.1) 39 | - [Version 0.6](https://github.com/mythmon/wok/tarball/v0.6) 40 | - [Version 0.5.1](https://github.com/mythmon/wok/tarball/v0.5.1) 41 | - [Version 0.5](https://github.com/mythmon/wok/tarball/v0.5) 42 | - [Version 0.4](https://github.com/mythmon/wok/tarball/v0.4) 43 | - [Version 0.3](https://github.com/mythmon/wok/tarball/v0.3) 44 | - [Version 0.2.1](https://github.com/mythmon/wok/tarball/v0.2.1) 45 | - [Version 0.2](https://github.com/mythmon/wok/tarball/v0.2) 46 | - [Version 0.1](https://github.com/mythmon/wok/tarball/v0.1) 47 | 48 | [gh]: https://github.com/mythmon/wok 49 | -------------------------------------------------------------------------------- /docs/content/community.mkd: -------------------------------------------------------------------------------- 1 | title: Community 2 | tags: [_nav] 3 | nav_sort: 3 4 | --- 5 | In the wild 6 | ----------- 7 | Wok powers an ever-growing list of sites. If you want to see the power 8 | of what wok can do, or how to do something, these websites are a good 9 | resource: 10 | 11 | - This site ([source](https://github.com/mythmon/wok/tree/master/docs)) 12 | - [Oregon State University LUG](http://lug.oregonstate.edu) 13 | ([source](https://github.com/OSULUG/OSULUG-Website)) 14 | - [Bravo Server](http://bravoserver.org) 15 | ([source](https://github.com/MostAwesomeDude/bravo/tree/master/website)) - 16 | A custom Minecraft server written in Python. 17 | - [robmd.net](http://robmd.net) 18 | ([source](https://github.com/robatron/robmd.net)) - Personal web site of 19 | Rob McGuire-Dale. 20 | - [uberj.com](http://uberj.com) ([source](https://github.com/uberj/wbsite)) - 21 | Personal web site of Jacques Uber. 22 | - [ngokevin.com](http://ngokevin.com) 23 | ([source](https://github.com/ngokevin/ngokevin)) - Personal web site of 24 | Kevin Ngo. 25 | - [corbinsimpson.com](http://corbinsimpson.com) 26 | ([source](https://github.com/MostAwesomeDude/website)) - Personal web site 27 | of Corbin Simpson. 28 | - [philipbjorge.com](http://www.philipbjorge.com) 29 | ([source](https://github.com/philipbjorge/philipbjorge.com)) - Personal 30 | website of Philip Bjorge 31 | - [dmbaughman.com](http://dmbaughman.com) - Personal website of David 32 | Baughman 33 | 34 | Contributors 35 | ------------ 36 | Wok is written by Mike Cooper. 37 | 38 | ### Thanks To 39 | Without these early adopters, wok would have lacked direction, definition, and 40 | a purpose. Each of them helped me work through figuring out what wok could do, 41 | needed do to, where it was broken, and what it should become. 42 | 43 | - Rob McGuire-Dale (robatron) 44 | - Kevin Ngo (ngoke) 45 | - Corbin Simpson (MostAwesomeDude) 46 | - Jacque Uber (uberj) 47 | 48 | Support 49 | ------- 50 | If there are bugs or feature requests for wok, please send them to [the issue 51 | tracker][gh-issues]. 52 | 53 | [gh-issues]: https://github.com/mythmon/wok/issues 54 | -------------------------------------------------------------------------------- /docs/content/docs/config.mkd: -------------------------------------------------------------------------------- 1 | title: Configuration 2 | slug: config 3 | category: docs 4 | --- 5 | Settings can be changed in the file `config` in the site's root directory. 6 | The file is a YAML file. Possible configuration options (and their defaults) 7 | are: 8 | 9 | - `output_dir` ('output') - The directory in which to place the generated 10 | files, e.g., `output_dir: output`. 11 | - `content_dir` ('content') - The directory where content files are stored, 12 | e.g., `content_dir: content`. 13 | - `templates_dir` ('templates') - The directory where templates are stored, 14 | e.g., `templates_dir: templates`. 15 | - `media_dir` ('media') - Where the media files are copied from, e.g., 16 | `media_dir: media`. 17 | - `site_title` ('Some Random wok Site') - Context variable for the title of the 18 | site. Available to templates as `{{ site.title }}`. 19 | - `author` (No default) - Context variable for the main author of the site. 20 | Always available to the templates as `{{ site.author }}`, and provides a 21 | default for the `{{ page.author }}` variable if it is not defined in the 22 | [page's metadata][content]. 23 | - `url_pattern` (`/{category}/{slug}.html`) - The pattern used to name and 24 | place the output files. The default produces URLs 25 | like`/category/subcategory/foo.html`. To get "wordpress style" urls, you 26 | could use `/{category}/{slug}/index.html`. For more information, please see 27 | the [URL management page][URLs]. 28 | - `url_include_index` (true) - If this option is turned off, then `index.*` on 29 | the end of urls will be removed in templates. This will turn the url 30 | `/docs/config/index.html` into `/docs/config/`. 31 | - `relative_urls` (false) - If this option is turned on, then any urls 32 | generated will not include a leading '/'. If this is false, all urls 33 | generated will include a leading '/'. 34 | - `rst_doctitle` (false) - Disable rst/docutils' promotion of a lone top-level 35 | section title to document title (was enabled up to wok 1.1.1 - by mistake). 36 | Might be optionally enabled here (or on per page basis) again - for backwards 37 | compatibility. 38 | 39 | [content]: /docs/content/ 40 | [URLs]: /docs/urls/ 41 | -------------------------------------------------------------------------------- /wok/jinja.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import fnmatch 4 | 5 | from jinja2.loaders import FileSystemLoader, TemplateNotFound 6 | from jinja2.loaders import split_template_path 7 | 8 | class AmbiguousTemplate(Exception): 9 | pass 10 | 11 | class GlobFileLoader(FileSystemLoader): 12 | """ 13 | As ``jinja2.loaders.FileSystemLoader`` except allow support for globbing. 14 | 15 | The loader takes the path to the templates as string, or if multiple 16 | locations are wanted a list of them which is then looked up in the 17 | given order: 18 | 19 | >>> loader = GlobFileLoader('/path/to/templates') 20 | >>> loader = GlobFileLoader(['/path/to/templates', '/other/path']) 21 | 22 | Per default the template encoding is ``'utf-8'`` which can be changed 23 | by setting the `encoding` parameter to something else. 24 | """ 25 | 26 | def __init__(self, ignores=[], *args, **kwargs): 27 | super(GlobFileLoader, self).__init__(*args, **kwargs) 28 | self.ignores = ignores 29 | 30 | def get_source(self, environment, template): 31 | pieces = split_template_path(template) 32 | for searchpath in self.searchpath: 33 | globbed_filename = os.path.join(searchpath, *pieces) 34 | filenames = glob.glob(globbed_filename) 35 | 36 | # Filter out files if they match any of the ignore patterns 37 | for ig in self.ignores: 38 | filenames = [ f for f in filenames 39 | if not fnmatch.fnmatch(os.path.basename(f), ig) ] 40 | 41 | if len(filenames) > 1: 42 | raise AmbiguousTemplate(template) 43 | elif len(filenames) < 1: 44 | continue 45 | filename = filenames[0] 46 | 47 | with open(filename) as f: 48 | contents = f.read().decode(self.encoding) 49 | 50 | mtime = os.path.getmtime(filename) 51 | def uptodate(): 52 | try: 53 | return os.path.getmtime(filename) == mtime 54 | except OSError: 55 | return False 56 | return contents, filename, uptodate 57 | else: 58 | raise TemplateNotFound(template) 59 | -------------------------------------------------------------------------------- /wok/tests/test_engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import tempfile 5 | 6 | try: 7 | from twisted.trial.unittest import TestCase 8 | except ImportError: 9 | from unittest import TestCase 10 | 11 | from wok import renderers 12 | from wok.engine import Engine 13 | 14 | 15 | DefaultRenderers = {} 16 | 17 | def setUpModule(): 18 | for renderer in renderers.all: 19 | DefaultRenderers.update((ext, renderer) for ext in renderer.extensions) 20 | 21 | 22 | class TestEngine(TestCase): 23 | 24 | def setUp(self): 25 | self.tmp_path = tempfile.mkdtemp() 26 | os.chdir(self.tmp_path) 27 | 28 | def tearDown(self): 29 | os.chdir('..') 30 | if self.tmp_path is not None: 31 | shutil.rmtree(self.tmp_path) 32 | self.tmp_path = None 33 | if '__hooks__' in sys.modules: 34 | del sys.modules['__hooks__'] 35 | if '__renderers__' in sys.modules: 36 | del sys.modules['__renderers__'] 37 | 38 | def test_load_hooks_no_hooks(self): 39 | e = Engine.__new__(Engine) 40 | e.load_hooks() 41 | 42 | self.assertFalse(hasattr(e, 'hooks')) 43 | 44 | def test_load_hooks_empty_directory(self): 45 | os.mkdir('hooks') 46 | 47 | e = Engine.__new__(Engine) 48 | e.load_hooks() 49 | 50 | self.assertFalse(hasattr(e, 'hooks')) 51 | 52 | def test_load_hooks(self): 53 | os.mkdir('hooks') 54 | with open(os.path.join('hooks', '__hooks__.py'), 'a') as f: 55 | f.write('hooks = { "name": "action" }\n') 56 | 57 | e = Engine.__new__(Engine) 58 | e.load_hooks() 59 | 60 | self.assertIsInstance(e.hooks, dict) 61 | self.assertIn('name', e.hooks) 62 | self.assertEqual(e.hooks['name'], 'action') 63 | 64 | def test_load_renderers_no_ext_renderers(self): 65 | e = Engine.__new__(Engine) 66 | e.load_renderers() 67 | 68 | self.assertIsInstance(e.renderers, dict) 69 | self.assertDictEqual(e.renderers, DefaultRenderers) 70 | 71 | def test_load_renderers_empty_directory(self): 72 | os.mkdir('renderers') 73 | 74 | e = Engine.__new__(Engine) 75 | e.load_renderers() 76 | 77 | self.assertIsInstance(e.renderers, dict) 78 | self.assertDictEqual(e.renderers, DefaultRenderers) 79 | 80 | def test_load_renderers(self): 81 | os.mkdir('renderers') 82 | with open(os.path.join('renderers', '__renderers__.py'), 'a') as f: 83 | f.write('renderers = { "html": "class" }\n') 84 | 85 | e = Engine.__new__(Engine) 86 | e.load_renderers() 87 | 88 | self.assertIsInstance(e.renderers, dict) 89 | self.assertDictContainsSubset(DefaultRenderers, e.renderers) 90 | self.assertIn('html', e.renderers) 91 | self.assertEqual(e.renderers['html'], 'class') 92 | -------------------------------------------------------------------------------- /wok/rst_pygments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The Pygments reStructuredText directive 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | This fragment is a Docutils_ 0.5 directive that renders source code 7 | (to HTML only, currently) via Pygments. 8 | 9 | To use it, adjust the options below and copy the code into a module 10 | that you import on initialization. The code then automatically 11 | registers a ``sourcecode`` directive that you can use instead of 12 | normal code blocks like this:: 13 | 14 | .. sourcecode:: python 15 | 16 | My code goes here. 17 | 18 | If you want to have different code styles, e.g. one with line numbers 19 | and one without, add formatters with their names in the VARIANTS dict 20 | below. You can invoke them instead of the DEFAULT one by using a 21 | directive option:: 22 | 23 | .. sourcecode:: python 24 | :linenos: 25 | 26 | My code goes here. 27 | 28 | Look at the `directive documentation`_ to get all the gory details. 29 | 30 | .. _Docutils: http://docutils.sf.net/ 31 | .. _directive documentation: 32 | http://docutils.sourceforge.net/docs/howto/rst-directives.html 33 | 34 | :copyright: Copyright 2006-2010 by the Pygments team, see AUTHORS. 35 | :license: BSD, see LICENSE for details. 36 | """ 37 | 38 | # Options 39 | # ~~~~~~~ 40 | 41 | # Set to True if you want inline CSS styles instead of classes 42 | INLINESTYLES = False 43 | 44 | from pygments.formatters import HtmlFormatter 45 | 46 | # The default formatter 47 | DEFAULT = HtmlFormatter(noclasses=INLINESTYLES) 48 | 49 | # Add name -> formatter pairs for every variant you want to use 50 | VARIANTS = { 51 | 'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True), 52 | } 53 | 54 | 55 | from docutils import nodes 56 | from docutils.parsers.rst import directives, Directive 57 | 58 | from pygments import highlight 59 | from pygments.lexers import get_lexer_by_name, TextLexer 60 | 61 | class Pygments(Directive): 62 | """ Source code syntax hightlighting. 63 | """ 64 | required_arguments = 1 65 | optional_arguments = 0 66 | final_argument_whitespace = True 67 | option_spec = dict([(key, directives.flag) for key in VARIANTS]) 68 | has_content = True 69 | 70 | def run(self): 71 | self.assert_has_content() 72 | try: 73 | lexer = get_lexer_by_name(self.arguments[0]) 74 | except ValueError: 75 | # no lexer found - use the text one instead of an exception 76 | lexer = TextLexer() 77 | # take an arbitrary option if more than one is given 78 | formatter = self.options and VARIANTS[self.options.keys()[0]] or DEFAULT 79 | parsed = highlight(u'\n'.join(self.content), lexer, formatter) 80 | return [nodes.raw('', parsed, format='html')] 81 | 82 | directives.register_directive('sourcecode', Pygments) 83 | 84 | -------------------------------------------------------------------------------- /docs/content/docs/content/categories.mkd: -------------------------------------------------------------------------------- 1 | title: Categories 2 | category: docs/content 3 | --- 4 | The category system of wok is unique. Instead of having categories that 5 | contain page and other (sub)categories, there are only pages. The site 6 | can be modeled as a [tree][], where every node is a page. The root is 7 | the template variable `site.categories`. The children of this node are 8 | any pages with no category. 9 | 10 | This site tree is built from the `page.slug` and `page.category` fields. 11 | The page that has a slug of `blog` is the top of the blog category, and 12 | its children are any pages that define `category: blog` 13 | 14 | [tree]: http://en.wikipedia.org/wiki/Tree_(data_structure) 15 | 16 | An example 17 | ========== 18 | Let's say I want to make a simple website with wok. I want to have a 19 | blog section, a section for my projects, and some static pages, like 20 | about and a homepage. 21 | 22 | About and home are easy - Simply make about.mkd and home.mkd, and give 23 | them appropriate titles and content. Since these won't be a major part 24 | of the site tree, we don't have to worry about them too much, except 25 | that they should not have any category defined (since they are at the 26 | top of the tree). 27 | 28 | The blog section is more interesting. First I need to make the blog 29 | category. To do this, I make a page, blog.mkd, that has a slug of `blog` 30 | (either defined with `slug: blog` or because the title of the page 31 | sluggifies to "blog"). This will likely use a template that knows to 32 | display the subpages in a blog-like way. 33 | 34 | To make an individual blog post, I'll make a new file, say 35 | `coolnews.mkd`. Give it some content, a title, a date, whatever. 36 | The important part is that it has `category: blog`. This tells wok 37 | that coolnews is a child of blog. As such, it will show up in blogs 38 | `page.subpages` and in the site tree in the right place. 39 | 40 | Next comes the projects. I have two projects: Foo and Bar. Foo is 41 | simple, it only needs one page. So I make a projects page/category and a 42 | projects/foo page, just like blog and blog/coolnews. 43 | 44 | Bar is more complicated. As well as have a general landing page for it, 45 | I want to have a few sub pages: source code and documentation. I'll make 46 | projects/bar just as projects/foo. The slug is `foo` and the category is 47 | `projects`. Easy. 48 | 49 | The subpages of of foo are no more complicated. For documentation I make 50 | `docs.mkd`, and give it a category of `projects/foo`. This says it is a 51 | subpage of foo, which is itself a subpage of projects. The source code 52 | page is the same. So now I have `projects/foo`, `projects/foo/docs` and 53 | `projects/foo/src`. 54 | 55 | Here is the entire site represented as the tree we know it to be. 56 | 57 | |-- Main 58 | |-- About 59 | |-- Blog 60 | | |-- Cool News 61 | | `-- Neat happenings 62 | | 63 | `-- Projects 64 | |-- Foo 65 | `-- Bar 66 | |-- Source 67 | `-- Documentation 68 | 69 | So we have 10 content files, and 10 generated pages for the site. 70 | -------------------------------------------------------------------------------- /docs/media/css/base.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Carter+One); 2 | 3 | /* Layout */ 4 | #wrap { 5 | width: 800px; 6 | margin: 10px auto 20px; 7 | } 8 | 9 | /* Header */ 10 | header { 11 | border-bottom: 1px solid #A60000; 12 | padding: 10px; 13 | overflow: hidden; 14 | position: relative; 15 | height: 40px; 16 | } 17 | header h1 { 18 | display: inline-block; 19 | margin: 0px; 20 | position: absolute; 21 | bottom: -10px; 22 | right: 10px; 23 | font-size: 3em; 24 | font-family: 'Carter One', sans-serif; 25 | } 26 | header h1 a { 27 | text-decoration: none; 28 | color: #A60000; 29 | } 30 | header h1 a:hover { 31 | text-decoration: none; 32 | } 33 | header nav { 34 | display: inline-block; 35 | position: absolute; 36 | left: 0px; 37 | bottom: 0px; 38 | font-family: 'Carter One', sans-serif; 39 | } 40 | header nav a { 41 | text-decoration: none; 42 | font-size: 1.2em; 43 | padding: 0 10px; 44 | margin-right: 5px; 45 | color: #A60000; 46 | } 47 | header nav a:hover { 48 | text-decoration: none; 49 | } 50 | header nav a.active { 51 | border-top: 1px solid #A60000; 52 | } 53 | /* Body */ 54 | #content { 55 | position: relative; 56 | padding: 10px; 57 | color: #4E3434; 58 | line-height: 1.5; 59 | font-size: 16px; 60 | font-family: serif; 61 | } 62 | 63 | #content span.version_note { 64 | position: absolute; 65 | right: 0px; 66 | top: 5px; 67 | font-size: 10px; 68 | } 69 | 70 | 71 | a { 72 | color: #BF3030; 73 | font-weight: bold; 74 | text-decoration: none; 75 | } 76 | a:hover { 77 | color: #EF4030; 78 | text-decoration: underline; 79 | } 80 | #content h1, #content h2, #content h3, #content h4, #content h5, #content h6 { 81 | font-family: 'Carter One', sans-serif; 82 | font-weight: 100; 83 | color: #322; 84 | margin: 5px 0 10px; 85 | } 86 | h1 a.heading_anchor, h2 a.heading_anchor, h3 a.heading_anchor 87 | h4 a.heading_anchor, h5 a.heading_anchor, h6 a.heading_anchor { 88 | display: none; 89 | color: #A60000; 90 | } 91 | h1:hover a.heading_anchor, h2:hover a.heading_anchor, 92 | h3:hover a.heading_anchor, h4:hover a.heading_anchor, 93 | h5:hover a.heading_anchor, h6:hover a.heading_anchor { 94 | padding: 2px; 95 | border: 3px; 96 | display: inline; 97 | font-weight: normal; 98 | } 99 | h1 a.heading_anchor:hover, h2 a.heading_anchor:hover, 100 | h3 a.heading_anchor:hover, h4 a.heading_anchor:hover, 101 | h5 a.heading_anchor:hover, h6 a.heading_anchor:hover { 102 | color: #FFF; 103 | background-color: #A60000; 104 | } 105 | 106 | p { 107 | margin: 0 0 20px 0; 108 | } 109 | pre { 110 | padding: 5px 0 5px 10px; 111 | border-left: 3px solid #DDD; 112 | font-family: Monospace; 113 | font-size: 14px; 114 | color: #555; 115 | background-color: #F0F0F0; 116 | } 117 | code { 118 | background-color: #F0F0F0; 119 | padding: 1px 2px; 120 | font-weight: bold; 121 | font-size: 14px; 122 | } 123 | 124 | /* Footer */ 125 | footer { 126 | border-top: 1px solid #A60000; 127 | padding: 10px; 128 | font-size: 0.7em; 129 | color: #999; 130 | } 131 | footer p { 132 | margin: 0 10px 0 0; 133 | display: inline; 134 | } 135 | -------------------------------------------------------------------------------- /docs/media/css/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #408080; font-style: italic } /* Comment */ 3 | .err { border: 1px solid #FF0000 } /* Error */ 4 | .k { color: #008000; font-weight: bold } /* Keyword */ 5 | .o { color: #666666 } /* Operator */ 6 | .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 7 | .cp { color: #BC7A00 } /* Comment.Preproc */ 8 | .c1 { color: #408080; font-style: italic } /* Comment.Single */ 9 | .cs { color: #408080; font-style: italic } /* Comment.Special */ 10 | .gd { color: #A00000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #FF0000 } /* Generic.Error */ 13 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .gi { color: #00A000 } /* Generic.Inserted */ 15 | .go { color: #808080 } /* Generic.Output */ 16 | .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 21 | .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 22 | .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 23 | .kp { color: #008000 } /* Keyword.Pseudo */ 24 | .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #B00040 } /* Keyword.Type */ 26 | .m { color: #666666 } /* Literal.Number */ 27 | .s { color: #BA2121 } /* Literal.String */ 28 | .na { color: #7D9029 } /* Name.Attribute */ 29 | .nb { color: #008000 } /* Name.Builtin */ 30 | .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 31 | .no { color: #880000 } /* Name.Constant */ 32 | .nd { color: #AA22FF } /* Name.Decorator */ 33 | .ni { color: #999999; font-weight: bold } /* Name.Entity */ 34 | .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 35 | .nf { color: #0000FF } /* Name.Function */ 36 | .nl { color: #A0A000 } /* Name.Label */ 37 | .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 38 | .nt { color: #008000; font-weight: bold } /* Name.Tag */ 39 | .nv { color: #19177C } /* Name.Variable */ 40 | .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #666666 } /* Literal.Number.Float */ 43 | .mh { color: #666666 } /* Literal.Number.Hex */ 44 | .mi { color: #666666 } /* Literal.Number.Integer */ 45 | .mo { color: #666666 } /* Literal.Number.Oct */ 46 | .sb { color: #BA2121 } /* Literal.String.Backtick */ 47 | .sc { color: #BA2121 } /* Literal.String.Char */ 48 | .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 49 | .s2 { color: #BA2121 } /* Literal.String.Double */ 50 | .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 51 | .sh { color: #BA2121 } /* Literal.String.Heredoc */ 52 | .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 53 | .sx { color: #008000 } /* Literal.String.Other */ 54 | .sr { color: #BB6688 } /* Literal.String.Regex */ 55 | .s1 { color: #BA2121 } /* Literal.String.Single */ 56 | .ss { color: #19177C } /* Literal.String.Symbol */ 57 | .bp { color: #008000 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #19177C } /* Name.Variable.Class */ 59 | .vg { color: #19177C } /* Name.Variable.Global */ 60 | .vi { color: #19177C } /* Name.Variable.Instance */ 61 | .il { color: #666666 } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /docs/content/docs/urls.mkd: -------------------------------------------------------------------------------- 1 | title: Managing URLs 2 | slug: urls 3 | category: docs 4 | --- 5 | Wok offers a flexible and easy-to-use mechanism for managing URLs. There are 6 | two components: The global `url_pattern` setting in the main configuration 7 | file, and the `url` setting in a [page's content metadata][content]. 8 | 9 | [content]: /docs/content/ 10 | 11 | Global URL settings 12 | ------------------- 13 | 14 | In the main configuration file, you can define a global URL pattern by defining 15 | the `url_pattern` setting. There are a few available variables, which you can 16 | arrange to your liking: 17 | 18 | - `{category}` - The category of the site, separated by forward slashes ('/'). 19 | - `{slug}` - The slug of the page. 20 | - `{page}` - The current page number, when [pagination][] is used. On the first 21 | page, this will always be an empty string. 22 | - `{ext}` - The file extension of the template that was used to generate this 23 | file. 24 | - `{date}`, `{datetime}`, `{time}` - The date/time that were specified in the 25 | metadata of a page. These are Python date, datetime, and time objects, so 26 | they have year, month, day, hour, minute, etc fields. If you don't specify 27 | these, they will default to `None`. 28 | 29 | If you don't include `{page}` in your `url_pattern`, then pagination won't 30 | work. Instead it will overwrite each page with its sequel, resulting in only 31 | the last page remaining. If you aren't using any pagination, then you don't 32 | need the variable. 33 | 34 | Any time two or more forward slashes are generated, they will be replaced by a 35 | single forward slash. This lets you do things like 36 | `/{category}/{page}/{slug}.html` without worrying about an empty page or empty 37 | category causing bad paths. 38 | 39 | If you set the option `url_include_index` to `false` in the config file, then 40 | any time the url ends with `index.*`, that will also be removed from the url 41 | patterns. The files will still be named `index.*`, however. So for example if 42 | your url pattern is `/{category}/{slug}/index.html`, the a blog post about 43 | balloons would create a file named `/blog/balloons/index.html`, but anytime you 44 | reference it in a template its `url` field will be `/blog/balloons/`. This 45 | makes URLs look a lot cleaner. This option off set to `true` by default (ie: 46 | keep index in urls). 47 | 48 | ### Examples: 49 | 50 | #### The default pattern. 51 | This will produce boring, old school URLs, like `/docs/urls.html` for this page, or `/news/cool5.html` for the fifth page of a cool news article. 52 | 53 | ::yaml 54 | url_pattern: /{category}/{slug}{page}.{ext} 55 | 56 | #### The "Wordpress" pattern. 57 | These are "clean" urls popularized by blogging tools like wordpress. This 58 | pattern would create the url `/docs/urls/` for this page, or 59 | `/news/cool/5/index.html` for the fifth page of a cool news article. 60 | 61 | ::yaml 62 | url_pattern: /{category}/{slug}/{page}/index.html 63 | 64 | Page-specific URL settings 65 | -------------------------- 66 | In addition to the global URL pattern, every page can have its own custom URL, which is defined in the content's metadata with the `url` setting. For example: 67 | 68 | ::yaml 69 | title: Foo page 70 | url: /foo/bar.html 71 | --- 72 | Foo page content. 73 | 74 | If you want to support pagination for this page, you must include a {page} variable in the url definition here, like 75 | 76 | ::yaml 77 | url: /foo/bar{page}.html 78 | -------------------------------------------------------------------------------- /docs/content/docs/templates.mkd: -------------------------------------------------------------------------------- 1 | title: Templates 2 | category: docs 3 | --- 4 | Content is then rendered using [Jinja2][] templates. Templates can have any 5 | extension, and that extension will carry over to the generated files. So if you 6 | want to generate an html file, then your default template would be named 7 | `default.html`, and be in the `templates` directory. 8 | 9 | To choose what template a content file should use, specify the `type` metadata 10 | field. For more information, see the section on [content](/docs/content). 11 | 12 | Various variables relating to the site and the current page are provided to the 13 | template system. For example: 14 | 15 | ::jinja2 16 | {% extends "base.html" %} 17 | 18 | {% block content %} 19 |

{{ page.title }}

20 | {{ page.content }} 21 | {% endblock %} 22 | 23 | There are two main objects available to the templater: `site`, and `page`. 24 | These variables, and the following attributes, are guaranteed to exist, and be 25 | decently well formed, unless otherwise noted. 26 | 27 | [Jinja2]:http://jinja.pocoo.org/ 28 | 29 | ## site 30 | - `site.title` - The title of the site, defined in the config file by the 31 | `site_title` option. 32 | - `site.datetime` - The last time the site was generated, as a full date and time. 33 | - `site.date` - The date of the last site generation. 34 | - `site.time` - The time of day of the last site generation. 35 | - `site.pages` - All the pages on the site, in a flat list. 36 | - `site.slugs` - A dictionary where the key is the slug of the page, and the 37 | value is the page itself. 38 | - `site.tags` - A dictionary. The keys are tag names, and each value is a 39 | list of pages that have that tag.. `{tag: [list of pages]}`. 40 | - `site.categories` - The top level categories of the site as a dictionary. 41 | They keys are the category names, and the values are the main page of that 42 | category. {category: main page}. 43 | - `site.author` - Only defined if it is specified with the author field in 44 | the config file. If multiple authors are specified, this is the first one. 45 | - `site.authors` - Like `site.author`, but as a list of all authorers 46 | specified. 47 | 48 | ## page 49 | Each of these may be set or overwritten in the YAML header on each content 50 | file, unless otherwise stated. 51 | 52 | - `page.title` - The title of the page. 53 | - `page.slug` - Used for making urls. Usually a lower case, no punctuation, 54 | no whitespace version of the title. Will be auto generated from title if 55 | not specified. 56 | - `page.author` - The first author of the page, as an `Author` object. If 57 | unspecified in the YAML header, the site default will be used, if that is 58 | unspecified, the object will still exist, but will have `None` for its 59 | attributes. If multiple authors are specified, this will be the first one. 60 | - `page.authors` - Like `page.author`, except as a list of all specifed 61 | authors. 62 | - `page.published` - Whether the page is to be generated and put in the 63 | variables of other pages. If this is false, the page won't be processed, or 64 | available to any templates. 65 | - `page.datetime` - The datetime content's metadata. Usually the date the 66 | content was written or last updated. 67 | - `page.date` - Like above, but as a date object (no time information). 68 | - `page.time` - Like above, but only the time of day. 69 | - `page.url` - The root-relative url that the page will be at. Generated by 70 | the `url_pattern`, but can by manually overwritten. 71 | - `page.subpages` - All the direct children of this page as a list. 72 | -------------------------------------------------------------------------------- /docs/content/docs/content.mkd: -------------------------------------------------------------------------------- 1 | title: Content 2 | category: docs 3 | --- 4 | Content can be written in various markup languages (currently [Markdown][mkd], 5 | [reStructuredText][rst], and [Textile][]), with a [YAML][yaml] header, 6 | separated by 3 hyphens alone on a line. For example: 7 | 8 | ::text 9 | title: Sample Post 10 | author: Mike Cooper 11 | --- 12 | The content of the page. 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec 15 | pellentesque, est non hendrerit mattis, arcu nibh venenatis sapien, quis 16 | porta sem libero placerat magna. Suspendisse condimentum turpis fringilla 17 | ligula porta vestibulum et ut sem. Cras hendrerit pulvinar metus at 18 | imperdiet. 19 | 20 | For more details on how to work with renderers, see [the docs about 21 | renderers](/docs/renderers/). 22 | 23 | [mkd]: http://daringfireball.net/projects/markdown/ 24 | [rst]: http://docutils.sourceforge.net/rst.html 25 | [textile]: http://textile.sitemonks.com/ 26 | [yaml]: http://www.yaml.org/ 27 | 28 | 29 | Metadata 30 | -------- 31 | These are the variables that affect the rending, layout, and categorization of 32 | pages in the YAML metadata. 33 | 34 | - `title` - The title of the page. If not specified, the filename (minus 35 | `.mkd`) will be used. 36 | - `type` - Used to determine the template to use. If unspecified, the 37 | template `default` is used. 38 | - `author` - In `My Name ` format. 39 | - `slug` - Name for URLs and filenames. If not specified, it will be 40 | generated from the title. 41 | - `date`, `time`, or `datetime` - Published date in [ISO8601][8601] format 42 | (ie: `2011-02-17 14:31:00`). 43 | 44 | > Note that due to the way the YAML standard works, seconds are required 45 | > in date times and times, other wise they will not be parsed correctly. 46 | 47 | - `category` - Including sub categories as a slash-separated list. 48 | `projects/wok/docs` means the page is in the category `docs` which is a 49 | subcategory of `wok` which is a subcategory of 50 | `projects`. 51 | - `tags` - A comma-separated list of tags in square brackets, e.g., 52 | `[foo, bar, herp, derp]`. ([More about tagging][tagging]) 53 | - `published` - To exclude some pages from the site, but not remove them 54 | entirely. 55 | - `url` - To manually specify the path for the generated page. If none is 56 | specified, the global pattern defined in `url_pattern` in the main 57 | configuration file will be used. ([More about managing URLs][URLs]) 58 | - `pagination` - An object who's presence will trigger paginating this page. 59 | See [pagination][] for more details. 60 | - `rst_doctitle` - Re-enable rst/docutils' promotion of a lone top-level 61 | section title to document title. 62 | (Only for backwards compatibility - was enabled up to wok 1.1.1 - a bug?) 63 | 64 | [8601]: http://en.wikipedia.org/wiki/ISO_8601 65 | [URLs]: /docs/urls/ 66 | [tagging]: /docs/content/tagging/ 67 | [pagination]: /docs/pagination/ 68 | 69 | Categories 70 | ---------- 71 | Categories in wok can be treated as a tree, where every node is a page. There 72 | are no seperate categories and pages, there are just pages, and sub-pages, and 73 | sub-sub-pages, etc. For more infomation, see the [category 74 | docs](/docs/content/categories/). 75 | 76 | Syntax Highlighting 77 | ------------------- 78 | wok can use [Pygments][pyg] to do syntax highlighting. It will be automatically 79 | enabled if you mark a block of text as code, and assign it a language. Marking 80 | a block of text is different in each mark up language. 81 | 82 | For more details, see how to do it in each renderer 83 | [here](/docs/renderers/#heading-syntax-highlighting). 84 | 85 | [pyg]: http://pygments.org 86 | -------------------------------------------------------------------------------- /test_site/media/friendly.scss: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight .c { color: #60a0b0; font-style: italic } /* Comment */ 3 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 4 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 5 | .highlight .o { color: #666666 } /* Operator */ 6 | .highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ 10 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 13 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 15 | .highlight .go { color: #808080 } /* Generic.Output */ 16 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 18 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 21 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 22 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 23 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 25 | .highlight .kt { color: #902000 } /* Keyword.Type */ 26 | .highlight .m { color: #40a070 } /* Literal.Number */ 27 | .highlight .s { color: #4070a0 } /* Literal.String */ 28 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 29 | .highlight .nb { color: #007020 } /* Name.Builtin */ 30 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 31 | .highlight .no { color: #60add5 } /* Name.Constant */ 32 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 33 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 34 | .highlight .ne { color: #007020 } /* Name.Exception */ 35 | .highlight .nf { color: #06287e } /* Name.Function */ 36 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 37 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 39 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 40 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #40a070 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #40a070 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #40a070 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #40a070 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 48 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 50 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 51 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 54 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 56 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /docs/content/docs/pagination.mkd: -------------------------------------------------------------------------------- 1 | title: Pagination 2 | category: docs 3 | --- 4 | Pagination is useful for when you have a list of thing with unknown length, but 5 | you don't want the page to get too big. For example, a blog's index page 6 | shouldn't show every post since the site was started, but only the last few. 7 | Pagination can do that. 8 | 9 | Pagination requires team work from both the template and the content. 10 | 11 | Content 12 | ------- 13 | Add a pagination field to "turn on" the pagionator. This field is a YAML object 14 | with a few sub elements: 15 | 16 | - `pagination.list` - The list of things to paginate over. This must be a name 17 | of a list, as accessible from a template. Commonly this is `page.subpages`. 18 | - `pagination.limit` - The number of items from the list to show on each page. 19 | - `pagination.sort_key` - Optional. What key to sort the list by before 20 | dividing it into page size chunks. Could be something like `title` or 21 | `datetime`. 22 | - `pagination.sort_reverse` - Optional. Boolean value that does what is says on 23 | the tin. True will make the list sort in reverse order. 24 | 25 | Here is an example metadata fragment for a blog index page. 26 | 27 | ::yaml 28 | title: Blog Index 29 | type: blog-index 30 | url: /blog/{page}/index.html 31 | pagination: 32 | list: page.subpages 33 | limit: 10 34 | sort_key: datetime 35 | sort_reverse: True 36 | 37 | Template 38 | -------- 39 | Now that the renderer knows that pagination should happen, it will provide some 40 | new data to the template. Note: the original list will not be modified. Here 41 | are the new variables accessible to the template: 42 | 43 | - `pagination.page_items` - The section of the list that should be shown on 44 | this page. 45 | - `pagination.cur_page` - The current page in the pagination sequence. 1 46 | indexed. 47 | - `pagination.num_pages` - The number of pages in the series. 48 | - `pagination.next_page` - The next page in the series. This is a page 49 | dictionary, just like `page` or `page.subpages[0]`, so it has the usual 50 | `title`, `url`, `content`, etc. variables. Won't exist if this is the last 51 | page. 52 | - `pagination.prev_page` - Just like `next_page`. Won't exist if this is the 53 | first page. 54 | 55 | The `next_page` and `prev_page` are real page objects, so you can do things like get their `author`, or `date` (these will be the same as the current page though), and most usefully, their `url` field, so you can make a link to them. 56 | 57 | Here is how you might use pagination in your templates. 58 | 59 | ::jinja2 60 | {% if pagination %} 61 | 62 | {% if pagination.prev_page %} 63 | Previous Page 64 | {% endif %} 65 | Page {{ pagination.cur_page }} of {{ pagination.num_pages }} 66 | {% if pagination.next_page %} 67 | Next Page 68 | {% endif %} 69 | 70 | {% for subpage in pagination.page_items %} 71 |

{{subpage.title}}

72 | {{subpage.content}} 73 | {% endfor %} 74 | {% endif %} 75 | 76 | This will show pagination controls, and then a list of links to subpages. 77 | 78 | URLs 79 | ---- 80 | To get this to work, each page is its own file, which means each file has to 81 | have a new name. To provide for this, there is a variable available to url 82 | generation, `page`, which contains the the current page number, except on the 83 | first page. On page 1, the `page` variable is an empty string. This way you 84 | don't have to think about pagination when linking to the first page of a 85 | series. 86 | 87 | If you define a `url` in your content page (like we did with our blog example 88 | earlier) the page number will still be substituted in just like in a normal 89 | `url_pattern`. You *must* include the page variable, or else every page will 90 | simply overwrite the previous one. 91 | 92 | For more information, see the page about [URL Management](/docs/urls/). 93 | -------------------------------------------------------------------------------- /wok/tests/test_util.py: -------------------------------------------------------------------------------- 1 | try: 2 | from twisted.trial.unittest import TestCase 3 | except ImportError: 4 | from unittest import TestCase 5 | 6 | from datetime import date, time, datetime, tzinfo 7 | 8 | from wok import util 9 | 10 | class TestDatetimes(TestCase): 11 | 12 | def setUp(self): 13 | """ 14 | The date used is February 3rd, 2011 at 00:23 in the morning. 15 | 16 | The datetime is the first commit of wok. 17 | The date is the day this test was first written. 18 | The time is pi second. 19 | """ 20 | self.datetime = datetime(2011, 2, 3, 0, 23, 0, 0) 21 | self.date = date(2011, 10, 12) 22 | self.time = time(3, 14, 15, 0) 23 | 24 | def test_blanks(self): 25 | inp = {} 26 | out = { 27 | 'datetime': None, 28 | 'date': None, 29 | 'time': None, 30 | } 31 | 32 | util.date_and_times(inp) 33 | self.assertEquals(inp, out) 34 | 35 | def test_just_date(self): 36 | inp = {'date': self.date} 37 | out = { 38 | 'datetime': datetime(2011, 10, 12, 0, 0, 0, 0), 39 | 'date': self.date, 40 | 'time': None, 41 | } 42 | 43 | util.date_and_times(inp) 44 | self.assertEquals(inp, out) 45 | 46 | def test_just_time(self): 47 | t = self.time # otherwise the datetime line gets awful 48 | inp = {'time': t} 49 | out = { 50 | 'datetime': None, 51 | 'date': None, 52 | 'time': t, 53 | } 54 | 55 | util.date_and_times(inp) 56 | self.assertEquals(inp, out) 57 | 58 | def test_date_and_times(self): 59 | inp = {'date': self.date, 'time': self.time} 60 | out = { 61 | 'datetime': datetime(2011, 10, 12, 3, 14, 15, 0), 62 | 'date': self.date, 63 | 'time': self.time, 64 | } 65 | 66 | util.date_and_times(inp) 67 | self.assertEquals(inp, out) 68 | 69 | def test_just_datetime(self): 70 | inp = {'datetime': self.datetime} 71 | out = { 72 | 'datetime': self.datetime, 73 | 'date': self.datetime.date(), 74 | 'time': self.datetime.time(), 75 | } 76 | 77 | util.date_and_times(inp) 78 | self.assertEquals(inp, out) 79 | 80 | def test_datetime_and_date(self): 81 | inp = {'datetime': self.datetime, 'date': self.date} 82 | out = { 83 | 'datetime': datetime(2011, 10, 12, 0, 23, 0, 0), 84 | 'date': self.date, 85 | 'time': self.datetime.time(), 86 | } 87 | 88 | util.date_and_times(inp) 89 | self.assertEquals(inp, out) 90 | 91 | def test_datetime_and_time(self): 92 | inp = {'datetime': self.datetime, 'time': self.time} 93 | out = { 94 | 'datetime': datetime(2011, 2, 3, 3, 14, 15, 0), 95 | 'date': self.datetime.date(), 96 | 'time': self.time, 97 | } 98 | 99 | util.date_and_times(inp) 100 | self.assertEquals(inp, out) 101 | 102 | def test_all(self): 103 | inp = {'datetime': self.datetime, 'date': self.date, 'time': self.time} 104 | out = { 105 | 'datetime': datetime(2011, 10, 12, 3, 14, 15, 0), 106 | 'date': self.date, 107 | 'time': self.time, 108 | } 109 | 110 | util.date_and_times(inp) 111 | self.assertEquals(inp, out) 112 | 113 | def test_types(self): 114 | """ 115 | YAML doesn't always give us the types we want. Handle that correctly. 116 | """ 117 | # Yaml will only make something a datetime if it also includes a time. 118 | inp = {'datetime': date(2011, 12, 25)} 119 | out = { 120 | 'datetime': datetime(2011, 12, 25), 121 | 'date': date(2011, 12, 25), 122 | 'time': None, 123 | } 124 | 125 | util.date_and_times(inp) 126 | self.assertEquals(inp, out) 127 | 128 | # Yaml likes to give times as the number of seconds. 129 | inp = {'date': self.date, 'time': 43200} 130 | out = { 131 | 'datetime': datetime(2011, 10, 12, 12, 0, 0), 132 | 'date': self.date, 133 | 'time': time(12, 0, 0), 134 | } 135 | 136 | util.date_and_times(inp) 137 | self.assertEquals(inp, out) 138 | -------------------------------------------------------------------------------- /wok/contrib/hooks.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8 : 2 | """Some hooks that might be useful.""" 3 | 4 | import os 5 | import glob 6 | import subprocess 7 | from StringIO import StringIO 8 | import logging 9 | 10 | from slugify import slugify 11 | 12 | from wok.exceptions import DependencyException 13 | 14 | try: 15 | from lxml import etree 16 | except ImportError: 17 | etree = None 18 | 19 | try: 20 | import sass 21 | except ImportError: 22 | sass = None 23 | 24 | class HeadingAnchors(object): 25 | """ 26 | Put some paragraph heading anchors. 27 | 28 | Serves as a 'page.template.post' wok hook. 29 | """ 30 | 31 | def __init__(self, max_heading=3): 32 | if not etree: 33 | logging.warning('To use the HeadingAnchors hook, you must install ' 34 | 'the library lxml.') 35 | return 36 | self.max_heading = max_heading 37 | logging.info('Loaded hook HeadingAnchors') 38 | 39 | def __call__(self, config, page): 40 | if not etree: 41 | return 42 | logging.debug('Called hook HeadingAnchors on {0}'.format(page)) 43 | parser = etree.HTMLParser() 44 | sio_source = StringIO(page.rendered) 45 | tree = etree.parse(sio_source, parser) 46 | 47 | for lvl in range(1, self.max_heading+1): 48 | headings = tree.iterfind('//h{0}'.format(lvl)) 49 | for heading in headings: 50 | if not heading.text: 51 | continue 52 | logging.debug('[HeadingAnchors] {0} {1}' 53 | .format(heading, heading.text)) 54 | 55 | name = 'heading-{0}'.format(slugify(heading.text)) 56 | anchor = etree.Element('a') 57 | anchor.set('class', 'heading_anchor') 58 | anchor.set('href', '#' + name) 59 | anchor.set('title', 'Permalink to this section.') 60 | anchor.text = u'¶' 61 | heading.append(anchor) 62 | 63 | heading.set('id', name) 64 | 65 | sio_destination = StringIO() 66 | 67 | # Use the extension of the template to determine the type of document 68 | if page.template.filename.endswith(".html") or page.filename.endswith(".htm"): 69 | logging.debug('[HeadingAnchors] outputting {0} as HTML'.format(page)) 70 | tree.write(sio_destination, method='html') 71 | else: 72 | logging.debug('[HeadingAnchors] outputting {0} as XML'.format(page)) 73 | tree.write(sio_destination) 74 | page.rendered = sio_destination.getvalue() 75 | 76 | 77 | def compile_sass(config, output_dir): 78 | ''' 79 | Compile Sass files -> CSS in the output directory. 80 | 81 | Any .scss or .sass files found in the output directory will be compiled 82 | to CSS using Sass. The compiled version of the file will be created in the 83 | same directory as the Sass file with the same name and an extension of 84 | .css. For example, foo.scss -> foo.css. 85 | 86 | Serves as a 'site.output.post' wok hook, e.g., your __hooks__.py file might 87 | look like this: 88 | 89 | from wok.contrib.hooks import compile_sass 90 | 91 | hooks = { 92 | 'site.output.post': [compile_sass] 93 | } 94 | 95 | Dependencies: 96 | 97 | - libsass 98 | ''' 99 | logging.info('Running hook compile_sass on {0}.'.format(output_dir)) 100 | for root, dirs, files in os.walk(output_dir): 101 | for f in files: 102 | fname, fext = os.path.splitext(f) 103 | # Sass partials should not be compiled 104 | if not fname.startswith('_') and fext == '.scss' or fext == '.sass': 105 | abspath = os.path.abspath(root) 106 | sass_src = '{0}/{1}'.format(abspath, f) 107 | sass_dest = '{0}/{1}.css'.format(abspath, fname) 108 | 109 | if sass is None: 110 | logging.warning('To use compile_sass hook, you must install ' 111 | 'libsass-python package.') 112 | return 113 | 114 | compiled_str = sass.compile(filename=sass_src, output_style='compressed') 115 | with open(sass_dest, 'w') as f: 116 | f.write(compiled_str) 117 | 118 | # TODO: Get rid of extra housekeeping by compiling Sass files in 119 | # "site.output.pre" hook 120 | abspath = os.path.abspath(output_dir) 121 | for f in glob.glob(os.path.join(abspath, '**', '*.s[a,c]ss')): 122 | os.remove(f) 123 | -------------------------------------------------------------------------------- /wok/renderers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from wok import util 3 | 4 | # Check for pygments 5 | try: 6 | import pygments 7 | have_pygments = True 8 | except ImportError: 9 | logging.warn('Pygments not enabled.') 10 | have_pygments = False 11 | 12 | # List of available renderers 13 | all = [] 14 | 15 | class Renderer(object): 16 | extensions = [] 17 | 18 | @classmethod 19 | def render(cls, plain, page_meta): # the page_meta might contain renderer options... 20 | return plain 21 | all.append(Renderer) 22 | 23 | class Plain(Renderer): 24 | """Plain text renderer. Replaces new lines with html
s""" 25 | extensions = ['txt'] 26 | 27 | @classmethod 28 | def render(cls, plain, page_meta): 29 | return plain.replace('\n', '
') 30 | all.append(Plain) 31 | 32 | # Include markdown, if it is available. 33 | try: 34 | from markdown import markdown 35 | 36 | class Markdown(Renderer): 37 | """Markdown renderer.""" 38 | extensions = ['markdown', 'mkd', 'md'] 39 | 40 | plugins = [ 41 | 'markdown.extensions.def_list', 42 | 'markdown.extensions.headerid', 43 | 'markdown.extensions.tables', 44 | 'markdown.extensions.toc', 45 | 'markdown.extensions.footnotes' 46 | ] 47 | if have_pygments: 48 | plugins.extend(['codehilite(css_class=codehilite)', 'fenced_code']) 49 | 50 | @classmethod 51 | def render(cls, plain, page_meta): 52 | return markdown(plain, extensions=cls.plugins) 53 | 54 | all.append(Markdown) 55 | 56 | except ImportError: 57 | logging.warn("markdown isn't available, trying markdown2") 58 | markdown = None 59 | 60 | # Try Markdown2 61 | if markdown is None: 62 | try: 63 | import markdown2 64 | class Markdown2(Renderer): 65 | """Markdown2 renderer.""" 66 | extensions = ['markdown', 'mkd', 'md'] 67 | 68 | extras = ['def_list', 'footnotes'] 69 | if have_pygments: 70 | extras.append('fenced-code-blocks') 71 | 72 | @classmethod 73 | def render(cls, plain, page_meta): 74 | return markdown2.markdown(plain, extras=cls.extras) 75 | 76 | all.append(Markdown2) 77 | except ImportError: 78 | logging.warn('Markdown not enabled.') 79 | 80 | 81 | # Include ReStructuredText Parser, if we have docutils 82 | try: 83 | import docutils.core 84 | from docutils.writers.html4css1 import Writer as rst_html_writer 85 | from docutils.parsers.rst import directives 86 | 87 | if have_pygments: 88 | from wok.rst_pygments import Pygments as RST_Pygments 89 | directives.register_directive('Pygments', RST_Pygments) 90 | 91 | class ReStructuredText(Renderer): 92 | """reStructuredText renderer.""" 93 | extensions = ['rst'] 94 | options = {} 95 | 96 | @classmethod 97 | def render(cls, plain, page_meta): 98 | w = rst_html_writer() 99 | #return docutils.core.publish_parts(plain, writer=w)['body'] 100 | # Problem: missing heading and/or title if it's a lone heading 101 | # 102 | # Solution: 103 | # Disable the promotion of a lone top-level section title to document title 104 | # (and subsequent section title to document subtitle promotion) 105 | # 106 | # http://docutils.sourceforge.net/docs/api/publisher.html#id3 107 | # http://docutils.sourceforge.net/docs/user/config.html#doctitle-xform 108 | # 109 | overrides = { 'doctitle_xform': page_meta.get('rst_doctitle', cls.options['doctitle']), } 110 | return docutils.core.publish_parts(plain, writer=w, settings_overrides=overrides, source_path=page_meta['source_path'])['body'] 111 | 112 | all.append(ReStructuredText) 113 | except ImportError: 114 | logging.warn('reStructuredText not enabled.') 115 | 116 | 117 | # Try Textile 118 | try: 119 | import textile 120 | class Textile(Renderer): 121 | """Textile renderer.""" 122 | extensions = ['textile'] 123 | 124 | @classmethod 125 | def render(cls, plain, page_meta): 126 | return textile.textile(plain) 127 | 128 | all.append(Textile) 129 | except ImportError: 130 | logging.warn('Textile not enabled.') 131 | 132 | 133 | if len(all) <= 2: 134 | logging.error("You probably want to install either a Markdown library (one of " 135 | "'Markdown', or 'markdown2'), 'docutils' (for reStructuredText), or " 136 | "'textile'. Otherwise only plain text input will be supported. You " 137 | "can install any of these with 'sudo pip install PACKAGE'.") 138 | -------------------------------------------------------------------------------- /wok/dev_server.py: -------------------------------------------------------------------------------- 1 | ''' Really simple HTTP *development* server 2 | 3 | Do *NOT* attempt to use this as anything resembling a production server. It is 4 | meant to be used as a development test server only. 5 | 6 | You might ask, "Why do I need a development server for static pages?" One 7 | hyphenated modifier: "root-relative." Since wok dumps all of the media files 8 | in the root output directory, pages that reside inside subdirectories still 9 | need to access these media files in a unified way. 10 | 11 | E.g., if you include `base.css` in your `base.html` template, `base.css` should 12 | be accessable to any page that uses `base.html`, even if it's a categorized 13 | page, and thus, goes into a subdirectory. This way, your CSS include tag could 14 | read `` (note the '/' in the `href` 15 | property) and `base.css` can be accessed from anywhere. 16 | ''' 17 | 18 | import sys 19 | import os 20 | from BaseHTTPServer import HTTPServer 21 | from SimpleHTTPServer import SimpleHTTPRequestHandler 22 | 23 | class dev_server: 24 | 25 | def __init__(self, serv_dir=None, host='', port=8000, dir_mon=False, 26 | watch_dirs=[], change_handler=None): 27 | ''' 28 | Initialize a new development server on `host`:`port`, and serve the 29 | files in `serv_dir`. If `serv_dir` is not provided, it will use the 30 | current working directory. 31 | 32 | If `dir_mon` is set, the server will check for changes before handling 33 | every request. If a change is detected, then wok will regenerate the 34 | site. 35 | ''' 36 | self.serv_dir = os.path.abspath(serv_dir) 37 | self.host = host 38 | self.port = port 39 | self.dir_mon = dir_mon 40 | self.watch_dirs = [os.path.abspath(d) for d in watch_dirs] 41 | self.change_handler = change_handler 42 | 43 | def run(self): 44 | if self.serv_dir: 45 | os.chdir(self.serv_dir) 46 | 47 | if self.dir_mon: 48 | wrap = RebuildHandlerWrapper(self.change_handler, self.watch_dirs) 49 | req_handler = wrap.request_handler 50 | else: 51 | req_handler = SimpleHTTPRequestHandler 52 | 53 | httpd = HTTPServer((self.host, self.port), req_handler) 54 | socket_info = httpd.socket.getsockname() 55 | 56 | print("Starting dev server on http://%s:%s... (Ctrl-C to stop)" 57 | %(socket_info[0], socket_info[1])) 58 | print "Serving files from", self.serv_dir 59 | 60 | if self.dir_mon: 61 | print "Monitoring the following directories for changes: " 62 | for d in self.watch_dirs: 63 | print "\t", d 64 | else: 65 | print "Directory monitoring is OFF" 66 | 67 | try: 68 | httpd.serve_forever() 69 | except KeyboardInterrupt: 70 | print "\nStopping development server..." 71 | 72 | 73 | class RebuildHandlerWrapper(object): 74 | 75 | def __init__(wrap_self, rebuild, watch_dirs): 76 | """ 77 | We can't pass arugments to HTTPRequestHandlers, because HTTPServer 78 | calls __init__. So make a closure. 79 | """ 80 | wrap_self.rebuild = rebuild 81 | wrap_self.watch_dirs = watch_dirs 82 | 83 | wrap_self._modtime_sum = None 84 | wrap_self.changed() 85 | 86 | class RebuildHandler(SimpleHTTPRequestHandler): 87 | """Rebuild if something has changed.""" 88 | 89 | def handle(self): 90 | """ 91 | Handle a request and, if anything has changed, rebuild the 92 | site before responding. 93 | """ 94 | if wrap_self.changed(): 95 | wrap_self.rebuild() 96 | 97 | SimpleHTTPRequestHandler.handle(self) 98 | 99 | wrap_self.request_handler = RebuildHandler 100 | 101 | def changed(self): 102 | """ 103 | Returns if the contents of the monitored directories have changed since 104 | the last call. It will return always return false on first run. 105 | """ 106 | last_modtime_sum = self._modtime_sum 107 | 108 | # calculate simple sum of file modification times 109 | self._modtime_sum = 0 110 | for d in self.watch_dirs: 111 | for root, dirs, files in os.walk(d): 112 | for f in files: 113 | abspath = os.path.join(root, f) 114 | self._modtime_sum += os.stat(abspath).st_mtime 115 | 116 | if last_modtime_sum is None: 117 | # always return false on first run 118 | return False 119 | else: 120 | # otherwise return if file modification sums changed since last run 121 | return (last_modtime_sum != self._modtime_sum) 122 | -------------------------------------------------------------------------------- /docs/content/docs/renderers.mkd: -------------------------------------------------------------------------------- 1 | title: Renderers 2 | category: docs 3 | --- 4 | Wok supports several different _renderers_, which take the content of the site 5 | and transform it into HTML that is ready to be processed by the rest of wok. 6 | The currently supported renderers are 7 | 8 | - Markdown 9 | - reStructuredText 10 | - Textile 11 | - Plain 12 | - Raw 13 | 14 | When wok initializes, it builds a list of renderers based on what systems you 15 | have installed on your system; if Markdown is available, but Textile is not, 16 | then Textile will not be loaded, but Markdown will be. 17 | 18 | When processing a content, wok examines the _file extension_ to determine what 19 | renderer to use.For example, the markdown renderer will render files names with 20 | `.markdown`, `.mkd`, and `.md` file extensions. 21 | 22 | Details 23 | ------- 24 | ### Markdown 25 | Extensions: `.markdown`, `.mkd`, `.md` 26 | 27 | The ability to render markdown is actually provided by two independent 28 | libraries. Wok will use either the [`Markdown`][mkd], or the 29 | [`markdown2`][mkd2] libraries to render files. If Markdown is found, it will be 30 | used, and if it is not found, wok will search for Markdown2. For the most part, 31 | they should handle content equivalently, but they way they handle syntax 32 | highlighting differs. See below for more details 33 | 34 | > Note: `Markdown` and `markdown2` are not related, except that they both 35 | > render markdown content. `markdown2` is not the new version of `Markdown` 36 | 37 | [mkd]: http://pypi.python.org/pypi/Markdown 38 | [mkd2]: http://pypi.python.org/pypi/markdown2 39 | 40 | ### reStructuredText 41 | Extensions: `.rst` 42 | 43 | reStructuredText documents are rendered by the [`docutils`][] library. For 44 | information about writing content in reStructuredText, check out the [Quick 45 | _re_Structured_Text_][quickrst] guide. 46 | 47 | [docutils]: http://pypi.python.org/pypi/docutils 48 | [quickrst]: http://docutils.sourceforge.net/docs/user/rst/quickref.html 49 | 50 | ### Textile 51 | Extensions: `.textile` 52 | 53 | Textile documents are processed by the [`textile`][] library. For a reference 54 | on how to write Textile documents, you can read the [Hobix Textile 55 | Reference][txtguide] 56 | 57 | [textile]: http://pypi.python.org/pypi/textile 58 | [txtguide]: http://redcloth.org/hobix.com/textile/ 59 | 60 | ### Plain 61 | Extensions: `.txt` 62 | 63 | The plain renderer handles text documents. It will include text files without 64 | any processing, except to convert all new lines to HTML `
` elements. This 65 | way the text in the HTML document has the same line breaks as the file in a 66 | text editor. This renderer does not depend on any libraries, so is always 67 | enabled. 68 | 69 | ### Raw 70 | The raw renderer uses the content of files without any processing at all. It is 71 | also the default fall back render: if no other renderer has a matching 72 | extension, it will be used. This means that you can use any content that 73 | already has stylign applied, or that you do not want to style, such as directly 74 | writing HTML or including CSV files. 75 | 76 | Syntax Highlighting 77 | ------------------- 78 | Wok can use [Pygments][pyg] to do syntax highlighting. It will be automatically 79 | enabled if you mark a block of text as code, and assign it a language. Marking 80 | a block of text is different with each renderer. 81 | 82 | [pyg]: http://pygments.org 83 | 84 | ### Renders 85 | 86 | #### In reStructuredText 87 | 88 | ::rst 89 | ..sourcecode:: python 90 | 91 | class Foo(object): 92 | pass 93 | 94 | That is, define a new block with the directive `sourcecode`. Give it a 95 | parameter of the language name. Then the code must be separated from the 96 | directive by a blank line, and be indented. 97 | 98 | #### In Markdown (with Markdown) 99 | 100 | ::markdown 101 | ::python 102 | class Foo(object): 103 | pass 104 | 105 | That is, make a code block whose first line is `::`. This will 106 | hide that first line, and highlight the code. 107 | 108 | #### In Markdown (with markdown2) 109 | 110 | ::markdown 111 | ```python 112 | class Foo(object): 113 | pass 114 | ``` 115 | 116 | The markdown2 library uses fenced code blocks instead of the `::` method. Surround the code blocks with lines containing only \`\`\` 118 | (which don't need to be indented like normal code blocks, by the way). To force 119 | syntax highlighting for a language, add the language name to the end of the 120 | first line, like in the example. 121 | 122 | ### CSS 123 | Doing this will only wrap the code in CSS classes, it doesn't actually apply 124 | any styles. To do that you need to include a Pygments style CSS sheet. To get 125 | one of these style sheets, run this command 126 | 127 | ::console 128 | $ pygmentize -S default -f html 129 | 130 | This will dump a style definition onto stdout. This command gives the default 131 | style, but there are others available. Drop the outputted file into your media 132 | directory, and include the css in one of your templates and you are good to go! 133 | 134 | Alternatively, you could download a pre-genered pygments CSS file from [this 135 | repository][pygcss]. 136 | 137 | [pygcss]: https://github.com/Anomareh/pygments-styles-dump 138 | 139 | Overriding renderers 140 | -------------------- 141 | 142 | Since version 1.2, wok provides a way to configure the renderers used 143 | by the site generation pipeline. This can be useful in the following cases. 144 | 145 | - The renderer attached to some extension is not the one that should 146 | be called. 147 | - Some files need specific treatment that can't be handled by the 148 | renderers proposed by default. 149 | 150 | The renderers interface is built like the one used to setup 151 | [hooks](/docs/hooks). Make a new directory in your site root named 152 | `renderers`. In this directory, create the file `__renderers__.py`. 153 | For example: 154 | 155 | ::text 156 | site-root/ 157 | |-- config 158 | |-- content/ 159 | |-- hooks/ 160 | |-- renderers/ 161 | | |-- __renderers__.py 162 | | |-- vcard_renderer.py 163 | | `-- html_filter.py 164 | | 165 | |-- media/ 166 | |-- output/ 167 | `-- templates/ 168 | 169 | From the `__renderers__.py` file, wok will import the variable 170 | `renderers` which should be a dictionary; the keys are filename 171 | extensions, and the values are the renderer to use. If a key is 172 | already defined internally (like `md` or `txt` respectively for 173 | Markdown or plain text rendering), the renderer defined here will 174 | override the default one. 175 | 176 | A renderer can be any object, class or module as long as it provides a 177 | `render` function which takes the input document as argument, and 178 | returns the formatted result. For example: 179 | 180 | ::python 181 | ''' __renderers__.py 182 | 183 | Defines wok renderers. 184 | ''' 185 | from bs4 import BeautifulSoup 186 | 187 | class HtmlRenderer(object): 188 | @classmethod 189 | def render(cls, plain): 190 | '''Parse the given HTML document and return only its body.''' 191 | soup = BeautifulSoup(plain) 192 | return soup.body 193 | 194 | renderers = { 195 | 'html': HtmlRenderer 196 | } 197 | 198 | The test site that can be found in [sources][gh] provides the same 199 | feature with a different syntax. 200 | 201 | [gh]: https://github.com/mythmon/wok 202 | 203 | -------------------------------------------------------------------------------- /CHANGELOG.mkd: -------------------------------------------------------------------------------- 1 | Version 1.1.1 2 | ------------- 3 | In which Travis rocks and OSULUG's website gets fixed. 4 | 5 | - Switch to awesome-slugify. 6 | - Fix the tests. 7 | - Improve the CI infrastructure. 8 | - Add important sites to CI infrastructure. 9 | 10 | Version 1.1 11 | ----------- 12 | In which we all get on the same page. 13 | 14 | - Added site config as a parameter to all hooks. [Jonathan Goodson] 15 | - More robust slugification. [Joe Turner] 16 | - Don't remove dotfiles from output directories. [Joe Turner] 17 | - Better relative URL support in the form of `url_subdir` config. [stachern] 18 | - Experimental support to add extra configuration to Markdown. [stachern] 19 | - Universal newline support. [Catherine Devlin] 20 | - Fixed pagination on global site attributes. [abbgrade] 21 | 22 | 23 | Version 1.0.0b 24 | -------------- 25 | In which "The 'b' stands for 'bastard'." 26 | 27 | - The built in development server now auto reloads when files changes. 28 | [robatron]. 29 | - A new contributed hook, `wok.contrib.hooks.compile_sass`, that does exactly 30 | what it says on the tin [robatron]. 31 | - Add support for Jinja2 extensions. 32 | - Better unicode support. 33 | - Really use None for missing date/time values. [remram44] 34 | - Fix HeadingAnchors to write saner HTML [Hein-Pieter van Bramm] 35 | - Experimental support for relative links. 36 | - Improvements to pip-installability. 37 | - The hook `site.output.post` is now only run once, not once per file. 38 | 39 | Version 0.9 40 | ----------- 41 | In which things get interesting. 42 | 43 | ### Features 44 | - Site hooks, for arbitrary inserting python code into the rendering process. 45 | - Add a documentation wok site. 46 | - New template variable, `site.slugs`, to access pages in a url-agnostic way. 47 | - Date, datetime, and time url variables. 48 | 49 | Version 0.8.2 50 | ------------- 51 | In which we leave 1970. 52 | 53 | ### Fixes 54 | - Fix Github issue 62, in which the next page object was not set on the first 55 | page of a two page group. 56 | - Dates, datetimes, and times now have a default value of `None` instead of 57 | the epoch time. 58 | 59 | Version 0.8.1 60 | ------------- 61 | In which I document things. 62 | 63 | ### Features 64 | - New template variable, `site.slugs`, which is a dictionary from slugs to 65 | pages. 66 | - New variables for url generation: date, time, and datetime, which can be 67 | used to make urls like "/blog/2012/01/26/new-wok-version" 68 | - There is now a doc site, which is a simple wok site that explains how to 69 | use wok, and serves as a simple example. 70 | 71 | Version 0.8 72 | ----------- 73 | In which URLs get cleaner. 74 | 75 | ### Features 76 | - Pagination now has saner sorting defaults. 77 | - Raw author objects will now print out nicely in templates. 78 | - Added `make_file` metadata option. 79 | - Previews can be specified, both as part of content, or metadata headers. 80 | - Add option to not put "index.html" on the end of URLs, for prettification. 81 | 82 | ### Fixes 83 | - The `published` metadata option will now prevent pages from showing up in 84 | any page's metadata, as intended. 85 | 86 | ### Deprecated 87 | - URL patterns should use {ext} instead of {type} to specify the extension 88 | that was used on the template, because type is confusing, being the same 89 | name as the metadata option that specifies the template to choose. 90 | 91 | Version 0.7 92 | ----------- 93 | In which I deprecate my first featured. 94 | 95 | ### Features 96 | - Define tags and authors as YAML lists, instead of CSV. 97 | - File format independence. Non-html files can now be generated. 98 | - The options menu is a little more user friendly now. [robatron] 99 | 100 | ### Fixes 101 | - Pages without a YAML header won't break things now. 102 | - Fixed date/time handling. 103 | 104 | ### Deprecated 105 | - The old way of doing comma separated tags and authors will continue to 106 | work, but will be removed in 0.8. 107 | 108 | Version 0.6.3 109 | ------------- 110 | In which git bisect is invaluable. 111 | 112 | ### Fixes 113 | - Somehow date handling got broken. Make it better. 114 | 115 | Version 0.6.2 116 | ------------- 117 | In which wok stops stomping around. 118 | 119 | ### Features 120 | - Authors can now be a list of users. 121 | - If the current directory doesn't appear to be a wok site, wok will refuse 122 | to run. 123 | - Change some logging message. 124 | 125 | ### Fixes 126 | - A pagination page that only has one page won't crash anymore. 127 | 128 | Version 0.6.1 129 | ------------- 130 | In which wok gets a watch. 131 | ### Features 132 | - Added `page.date` and `page.time` template variables. 133 | 134 | ### Fixes 135 | - Make custom variables in pages not propagate to later pages. 136 | - Make orphan pages only print an error, not throw an exception. 137 | 138 | Version 0.6 139 | ----------- 140 | In which I tear out my hair. 141 | 142 | ### Features 143 | - Pagination 144 | 145 | ### Other 146 | - Now using Python's built in logging module. 147 | 148 | Version 0.5.1 149 | ------------- 150 | In which pygments isn't required anymore. 151 | 152 | ### Fixes 153 | - Fix a bug related to optionally depending on pygments. 154 | - Fix some output formatting. 155 | 156 | Version 0.5 157 | ----------- 158 | In which the MIT license is applied. 159 | 160 | ### Features 161 | - The structure of the generated files is now user configurable. 162 | - New configuration option: `author`. See docs for more info. 163 | - Markdown, Pygments, and Docutils dependencies are now optional. 164 | - Output from wok will now wrap to terminal windows. 165 | 166 | ### Other 167 | - The README now references some sample sites. 168 | - Added a license. Wok is now officially open source! 169 | - Tweaked output formats. 170 | - Page metadata is now stored in a more consistent way. 171 | 172 | Version 0.4 173 | ----------- 174 | In which things get more flexible. 175 | 176 | ### Features 177 | - Added a built in testing server, to easily test absolute links on the site. 178 | - Add `--version` option. 179 | - Added `site.pages`, a flat list of the pages of the site. 180 | - Added `site.categories`, a dictionary: 181 | `{top level categories : immediate children of that category}`. 182 | - Added page.url field, settable in a page's header and accessible from 183 | templates. 184 | - Started adding unit tests. 185 | 186 | ### Fixes 187 | - Fix `Page.author` to actually parse author strings right. 188 | 189 | 190 | Version 0.3 191 | ----------- 192 | In which things get brighter. 193 | 194 | - Add optional syntax highlighting using [Pygments][pyg]. 195 | - Add tags for pages. 196 | - Add `--verbose` (`-v`) and `--quiet` (`-q`) options to the script. 197 | 198 | [pyg]: http://pygments.org 199 | 200 | Version 0.2.2 201 | ------------- 202 | In which I try to teach better. 203 | 204 | - Improve the sample site. 205 | - Enable syntax highlighting for Markdown. 206 | - Fix requirements for PyPI/pip 207 | 208 | Version 0.2.1 209 | ------------- 210 | In which I release what should have been v0.2. 211 | 212 | - Add a sample site. 213 | - Actually enable reStructuredText support. 214 | - Don't try to parse hidden files. 215 | - Fix datetime handling. 216 | - Media directory is now optional. 217 | - Make output files in a tree structure based on category. 218 | 219 | Version 0.2 220 | ----------- 221 | In which MostAwesomeDude convinces me to add reStructured Text. 222 | 223 | - Initial support for reStructuredText. 224 | - Installable package. 225 | - Bug fixes based on user feedback. 226 | 227 | Version 0.1 228 | ----------- 229 | In which things start. 230 | 231 | - First release. 232 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | Wok 2 | === 3 | 4 | [![Build Status](https://travis-ci.org/mythmon/wok.svg?branch=master)](https://travis-ci.org/mythmon/wok) 5 | 6 | Wok is a static website generator. It turns a pile of templates, 7 | content, and resources (like CSS and images) into a neat stack of plain 8 | HTML. Wok is distributed under the MIT license (see the 9 | [`LICENSE` file](LICENSE) for more details). 10 | 11 | The idea is that you don't need a big server-side engine like PHP to 12 | generate every page every visit: you can generate them all ahead of 13 | time, and only regenerate things when something has changed. A good way 14 | this could be done would be with a post-commit hook on a git repository 15 | containing your content or layout. 16 | 17 | I made wok because projects like [Jekyll][jekyll], [Hyde][hyde], and 18 | [Static][static] were intriguing, but in the end didn't quite match what 19 | I wanted to do with my website. So I am writing my own. Funnily, the 20 | mythical website that inspired wok still hasn't been written. 21 | 22 | [jekyll]: https://github.com/mojombo/jekyll 23 | [hyde]: https://github.com/lakshmivyas/hyde 24 | [static]: http://static.newqdev.com/ 25 | 26 | Sample Sites 27 | ------------ 28 | A bare bones site is included in the wok git repo, in the `test` directory. 29 | It is really just a playground for devs to test new features, and not a good 30 | learning tool. 31 | 32 | The documentation site available in the `doc` directory should be a fairly 33 | complete example of a small site that is good to learn from. 34 | 35 | For some real world examples check out these sites. 36 | 37 | - [Oregon State University Linux Users Group](http://lug.oregonstate.edu) 38 | ([source](https://github.com/OSULUG/OSULUG-Website)) 39 | - [Source Mage GNU/Linux newsblog](http://sourcemage.org/News/) 40 | ([source](https://bitbucket.org/sourcemage/website/src)) 41 | - [Bravo Server](http://bravoserver.org) 42 | ([source](https://github.com/MostAwesomeDude/bravo/tree/master/website)) - 43 | A custom Minecraft server written in Python. 44 | - [robmd.net](http://robmd.net) 45 | ([source](https://github.com/robatron/robmd.net)) - Personal website of 46 | Rob McGuire-Dale. 47 | - [uberj.com](http://www.uberj.com) 48 | ([source](https://github.com/uberj/wbsite)) - Personal website of Jacques 49 | Uber 50 | - [ngokevin.com](http://ngokevin.com) 51 | ([source](https://github.com/ngokevin/ngokevin)) - Personal website of 52 | Kevin Ngo 53 | - [corbinsimpson.com](http://corbinsimpson.com) 54 | ([source](https://github.com/mostawesomedude/website)) - Personal website 55 | of Corbin Simpson 56 | - [philipbjorge.com](http://www.philipbjorge.com) 57 | ([source](https://github.com/philipbjorge/philipbjorge.com)) - Personal 58 | website of Philip Bjorge 59 | - [dmbaughman.com](http://dmbaughman.com) - Personal website of 60 | David Baughman 61 | - Your site here! If you are using wok to generate sites, and don't mind 62 | serving as an example, let me know and I will add a link to your site 63 | here. 64 | 65 | For some more documentation, checkout [the doc site][docs]. To learn and share 66 | with other users, you can check out [the wiki][wiki]. 67 | 68 | [docs]: http://wok.mythmon.com 69 | [wiki]: https://github.com/mythmon/wok/wiki 70 | 71 | Installation 72 | ------------ 73 | The recommended way to install wok is from the [Python Package Index][pypi] 74 | with this command. 75 | 76 | sudo pip install wok 77 | 78 | Alternatively, if you want to hack on wok or just need the latest code, 79 | you can run from git head, and if you want to you can install to your 80 | system directories with this command. Note that you will need to install 81 | dependencies by hand in this case. 82 | 83 | sudo python2 setup.py install 84 | 85 | [pypi]: http://pypi.python.org/pypi 86 | 87 | ###Dependencies 88 | All dependencies are available from pip. Although optional, you really should 89 | install either markdown or docutils, and if you install from pip, they will be 90 | installed for you. Pygments is used for syntax highlighting, and will also be 91 | installed from pip. 92 | 93 | ####Required 94 | 95 | - `pyyaml` 96 | - `jinja2` 97 | 98 | ####Optional 99 | 100 | - `Markdown` - for rendering markdown documents. 101 | - `docutils` - for rendering reStructuredText documents. 102 | - `Pygments` - for syntax highlighting. 103 | 104 | Usage 105 | ----- 106 | > You might be interested in [the tutorial](http://wok.mythmon.com/tutorial/) 107 | 108 | To use wok, go to the directory where your site files are located, and run the 109 | command `wok`. No output will be given unless something goes wrong. If it 110 | returns without error, you should have a shiny new output folder containing 111 | some HTML, and your media that represents your shiny new site. 112 | 113 | To aid in testing links on the site, wok includes a development server. 114 | You can run it with the command `wok --server`, which will generate the 115 | site as normal, and then run a webserver on port 8080. The comments on 116 | that particular file say: 117 | 118 | > Do *NOT* attempt to use this as anything resembling a production 119 | > server. It is meant to be used as a development test server only. 120 | 121 | This test server is slow, and likely insecure, but for local testing of 122 | the site during development, it is really convenient. 123 | 124 | Wok pulls the pieces of your site from three places. For each of these 125 | places, you can modify the path wok looks for them in the configuration 126 | file. 127 | 128 | ### Content ### 129 | Pulled from a directory named `content` by default. Content is written 130 | in lightweight markup or in plain text, with an optional YAML header 131 | section. The directory structure of the file mean nothing to wok. It 132 | builds the structure of the site based on the titles and the category 133 | metadata. 134 | 135 | Since wok uses lightweight mark up languages like [Markdown][mkd] and 136 | [reStructuredText][rst], it is easy to do nice formatting in the pages 137 | without using a GUI editor or a cumbersome language like HTML. Built in 138 | syntax highlighting and media copying make things even easier. 139 | 140 | [mkd]: http://daringfireball.net/projects/markdown/ 141 | [rst]: http://docutils.sourceforge.net/rst.html 142 | 143 | [More info](http://wok.mythmon.com/docs/content/) 144 | 145 | ### Templates ### 146 | Pulled from `templates` by default. Wok uses [Jinja2][jinja] templates, 147 | with various variables exposed to build pages. This is a very flexible 148 | templating environment with control flow, filters, and other ways to 149 | slice and dice the data that wok gives you. 150 | 151 | [jinja]: http://jinja.pocoo.org/ 152 | 153 | [More info](http://wok.mythmon.com/docs/templates/) 154 | 155 | Configuration 156 | ------------- 157 | Settings can be changed in the file `config` in the current directory. 158 | 159 | Possible configuration options (and their defaults) are 160 | 161 | - `output_dir` ('output') - Where the outputted files are put. 162 | - `content_dir` ('content') - Where to find the content. 163 | - `templates_dir` ('templates') - Where the templates are. 164 | - `media_dir` ('media') - Where the media files are copied from. 165 | - `site_title` ('Some Random wok Site') - Available to templates as 166 | `site.title`. 167 | - `author` (No default) - Fills `site.author`, and provides a default to 168 | `page.author`. 169 | - `url_pattern` (`/{category}/{slug}.html`) - The pattern used to name and 170 | place the output files. The default produces URLs like 171 | `/category/subcategory/foo.html`. To get "wordpress style" urls, you could 172 | use `/{category}/{slug}/index.html`. 173 | 174 | Available variables: 175 | 176 | - `{category}` - The category of the site, slash seperated. 177 | - `{slug}` - The slug of the page. 178 | - `{page}` - The current page. 179 | - `{ext}` - The extension that the page should used. 180 | - `{date}`, `{datetime}`, and `{time}` - The date/time from the metadata 181 | of the page 182 | 183 | - `url_include_index` (Yes) - If true, keep `index.*` in urls. 184 | 185 | More info: 186 | [config](http://wok.mythmon.com/docs/config/), 187 | [urls](http://wok.mythmon.com/docs/urls/). 188 | -------------------------------------------------------------------------------- /docs/content/docs/hooks.mkd: -------------------------------------------------------------------------------- 1 | title: Hooks 2 | category: docs 3 | --- 4 | Since version 0.9, wok provides a hook interface to add custom python code to 5 | the site generation pipeline. 6 | 7 | Before starting to programm, check the [wok_hooks project][wok_hooks] for already available plugins. 8 | 9 | To use hooks, make a new directory in your site root named `hooks`. In this 10 | directory, make the file `__hooks__.py`. For example: 11 | 12 | ::text 13 | site-root/ 14 | |-- config 15 | |-- content/ 16 | |-- hooks/ 17 | | |-- __hooks__.py 18 | | |-- hook_foo.py 19 | | `-- hook_bar.py 20 | | 21 | |-- media/ 22 | |-- output/ 23 | `-- templates/ 24 | 25 | 26 | From the `__hooks__.py` file, wok will import the variable `hooks`. 27 | This should be a dictionary; The keys are the hook names below, and the values 28 | are lists of functions (or other callables) to run for the hooks. Simply give 29 | the names of the functions, but do not invoke them. For example: 30 | 31 | ::python 32 | ''' __hooks__.py 33 | 34 | Attach Python functions to wok hooks. 35 | ''' 36 | 37 | import hook_foo 38 | import hook_bar 39 | 40 | # The `hooks` dictionary that wok will import 41 | hooks = { 42 | 'site.start': [hook_foo.download_images], 43 | 'site.content.gather.post': [hook_bar.process_pages] 44 | } 45 | 46 | If the hooks below have parameters, the listed functions should accept 47 | those parameters. Hooks will be run in the order they appear below. 48 | 49 | Available hooks 50 | --------------- 51 | Below are the available hooks, when they will be run, and the arguments they 52 | will pass to the hooked functions (if any.) 53 | 54 | ###List of hooks 55 | 56 | - [site.start](#site.start) 57 | - [site.output.pre](#site.output.pre) 58 | - [site.output.post](#site.output.post) 59 | - [site.content.gather.pre](#site.content.gather.pre) 60 | - [site.content.gather.post](#site.content.gather.post) 61 | - [page.render.pre](#page.render.pre) 62 | - [page.render.post](#page.render.post) 63 | - [page.meta.pre](#page.meta.pre) 64 | - [page.meta.post](#page.meta.post) 65 | - [page.template.pre](#page.template.pre) 66 | - [page.template.post](#page.template.post) 67 | - [site.done](#site.done) 68 | 69 | ### Details 70 | 71 | 72 | 73 | `site.start(config)` 74 | : `config` 75 | : Dictionary containing site configuration 76 | :

Called before anything else has started, except for the loading of hooks. 77 | This would be a good time to modify the content, templates, or the files in 78 | the media directory.

79 | 80 | `site.output.pre(config, output_path)` 81 | : `config` 82 | : Dictionary containing site configuration 83 | : `output_path` 84 | : The path to the output directory. 85 | : This path will run before the output directory is populated by the media, 86 | and after any existing output files have been deleted. You can add files 87 | that may be overwritten by the media files or the site content. 88 | 89 | `site.output.post(config, output_path)` 90 | : `config` 91 | : Dictionary containing site configuration 92 | : `output_path` 93 | : The path to the output directory. 94 | : This hook will run after the output directory is populated by the media, 95 | and before the content pages have started to be processed. You can use this 96 | to modify, overwrite, or otherwise fiddle with the media directory after it 97 | has been copied to the output directory. 98 | 99 | `site.content.gather.pre(config)` 100 | : `config` 101 | : Dictionary containing site configuration 102 | : Return value 103 | : List of `Page` objects to add to the list of pages. 104 | : This hook will run before wok gathers content pages, and can be used to add 105 | pages that don't exist on disk. 106 | 107 | `site.content.gather.post(config, pages)` 108 | : `config` 109 | : Dictionary containing site configuration 110 | : `pages` 111 | : The list of pages that wok has gathered from the content directory, and 112 | any other hooks that have run. Also includes the duplicated versions of 113 | paginated pages. 114 | : Return value 115 | : List of `Page` objects to add to the list of pages. 116 | : This hook will run after wok gathers content pages, and can be used to add 117 | pages that don't exist on disk, or to remove pages added by other means. If 118 | you modify the list of pages received, those changes will take effect in 119 | wok. You may also return pages to add. 120 | 121 | `page.render.pre(config, page)` 122 | : `config` 123 | : Dictionary containing site configuration 124 | : `page` 125 | : The current page object that is being processed. 126 | : This hook will be called for each page before the page is rendered by 127 | Markdown, reStructuredText, etc. The unrendered text will be in the 128 | variable `page.original`, if there is an original text. Keep in mind that 129 | some pages won't be run through this hook because they come from other 130 | sources, such as hooks, or pagination. 131 | 132 | `page.render.post(config, page)` 133 | : `config` 134 | : Dictionary containing site configuration 135 | : `page` 136 | : The current page object that is being processed. 137 | : This hook will be called for each page right after the page is rendered by 138 | Markdown, reStructuredText, etc. The unrendered text will be in the 139 | variable `page.original`, and the rendered text will be in the meta 140 | variable `page.meta['content']. Keep in mind that some pages won't be run 141 | through this hook because they come from other sources, such as hooks, or 142 | pagination. 143 | 144 | `page.meta.pre(config, page)` 145 | : `config` 146 | : Dictionary containing site configuration 147 | : `page` 148 | : The current page object that is being processed. 149 | : This hook will be called for each page before the page has its metadata 150 | filled in. Some metadata will exist, but it will be in an unnormalized 151 | state. 152 | 153 | `page.meta.post(config, page)` 154 | : `config` 155 | : Dictionary containing site configuration 156 | : `page` 157 | : The current page object that is being processed. 158 | : This hook will be called for each page right after the page has its 159 | metadata filled in and normalized. 160 | 161 | `page.template.pre(config, page, templ_vars)` 162 | : `config` 163 | : Dictionary containing site configuration 164 | : `page` 165 | : The current page object that is being processed. 166 | : `templ_vars` 167 | : A dictionary of the variables that will be sent to the template engine. 168 | See the documentation on [templates][] for its normal contents. 169 | : This hook will be called for each page before the page is sent to 170 | the template engine. At this point, the content has been transformed 171 | from markup input (such as markdown) to html output, if applicable. 172 | The transformed version is in `templ_vars['page']['content']`. If you 173 | modify the `templ_vars` parameter, those changes will be visible to 174 | the template engine. The given functions should take in these two 175 | variables. 176 | 177 | [templates]: /docs/templates/ 178 | 179 | `page.template.post(config, page)` 180 | : `config` 181 | : Dictionary containing site configuration 182 | : `page` 183 | : The current page being processed. 184 | : This hook will be called after the page has been processed by the template 185 | engine. The next step will be write the file to disk, if applicable, so any 186 | last minute changes should happen here. 187 | 188 | `site.done(config)` 189 | : `config` 190 | : Dictionary containing site configuration 191 | : Called after wok has finished everything. This would be a good time to make 192 | any general modifications to the output, or to do something like upload the 193 | site for distribution. If the `--server` option has been specified, this 194 | will happen before the server is run. 195 | 196 | [wok_hooks]: https://github.com/abbgrade/wok_hooks 197 | -------------------------------------------------------------------------------- /docs/content/tutorial.mkd: -------------------------------------------------------------------------------- 1 | title: Tutorial 2 | --- 3 | Wok is simple. To demonstrate that, we are going to make a simple wok site from 4 | scratch. 5 | 6 | > Note: All paths in this document will be relative to the web root, unless 7 | > otherwise stated. 8 | 9 | # 0. Installation 10 | 11 | If you don't already have wok, install it with 12 | 13 | sudo pip install wok 14 | 15 | `markdown` and `docutils` are optional, but will be installed as dependencies. 16 | They allow the use of the Markdown and reStructuredText markup languages, so you 17 | want at least one of them. 18 | 19 | # 1. File Layout 20 | 21 | First, make a directory to hold all your site's code. Wok expects to find at least `content` and `templates` directories. It will also use the directories `media` and `hooks`, as well as the file `config`. 22 | 23 | --MySite/ 24 | |-- content/ 25 | |-- templates/ 26 | |-- media/ 27 | |-- hooks/ 28 | \-- config 29 | 30 | The `content` directory is where we will store the text of the web site. The 31 | `templates` directory will store the templates that give the site form and 32 | function. The `media` directory is optional, and will contain all of your CSS, 33 | JavaScript, images, and various other support files that simply need to be 34 | copied to the web site's root. The `hooks` directory is for adding Python code 35 | to the rendering process of wok, which we won't be covering in this tutorial. 36 | Finally the config file, which is also optional, may contain some setting that 37 | wok will use to change the way it renders the site. 38 | 39 | Go ahead and make this directories, and create an empty file named config, 40 | because we will be using them all in this tutorial, except for `hooks`. 41 | 42 | # 2. Content 43 | 44 | What is a site without content? Wok defines content as the user generated text 45 | that goes on a page. For example, if you were to look at the [source of this 46 | site][wokdocsrc] (which is highly recommend by the way), you would fine the 47 | file that describes this page is a mostly plain text document, with some 48 | metadata and Markdown formatted syntax. Content is the unique part of every 49 | page, and can't be automatically generated by wok -- you have to write it 50 | yourself. 51 | 52 | For now our simple site will have 3 pages, home, about, and contact. We will be 53 | using the Markdown markup languages, one of the several that wok supports. Wok 54 | also support reStructuredText, Textile, plain text, and raw text. 55 | 56 | > What's the difference between plain and raw text? Raw text will preserve the 57 | > file exactly as it is when it goes into the generated site. Plain text will 58 | > add HTML `
` tags at newlines, to preserve the visual structure of the 59 | > file. 60 | 61 | [wokdocsrc]: https://github.com/mythmon/wok/tree/master/docs 62 | 63 | ## Home 64 | 65 | The home page will be the main landing page for our website. As such its URL 66 | should be `/index.html`. Sounds pretty simple, and it is. Make a file named 67 | `home.mkd` in your content directory, and give it these contents. 68 | 69 | `home.mkd` 70 | 71 | title: Home 72 | url: /index.html 73 | --- 74 | This is the home page. It is kind of bare right now. 75 | 76 | That is all it takes to tell wok what it needs to know. The part above the 77 | `---` is the metadata for the page. It is technically optional, but every page 78 | should have at least the `title` attribute, or else wok will complain. The 79 | `url` field is optional, and isn't normally specified. It is usually generated 80 | based on the url-pattern rules. In this case however, since we want home to 81 | always be at `/index.html` no matter what, we can define that here. Below the 82 | `---` is simply the content of the page. Since we have given the file the 83 | extension of `.mkd`, wok will use Markdown to render the content. 84 | 85 | That is all we need to make a simple page. We will come back to this later and 86 | add some features, but for now, we are done. 87 | 88 | ## Contact 89 | 90 | Contact is even simpler than home, because it doesn't need a particular URL. We 91 | will put it in `contact.mkd`; here are the contents of that file. 92 | 93 | title: Contact 94 | --- 95 | You can call me at 555-555-5555, or by email at `noone@nowhere.foo`. 96 | 97 | Since we didn't define a URL field, wok will auto generate one, based on the 98 | URL rule. Since we haven't specified one of those (it would go in the `config` 99 | file), wok will use the default, which is `/{category}/{slug}.{ext}`. The slug 100 | for this site is `contact`, and we don't have any category defined, and the 101 | extension on the template is (going to be) 'html'. So this page's URL will be 102 | `/contact.html`. 103 | 104 | > What's a slug? A slug is a string that refers to the page that will good for 105 | > URLs. Wok will generate them based on the title, by converting it to lower 106 | > case ASCII, with no punctuation or spaces. You could also define your own by 107 | > saying `slug: something` in the metadata. 108 | 109 | ## About 110 | 111 | About will be a little more complicated then contact, but not by much. 112 | 113 | `about.mkd` 114 | 115 | title: About This Site 116 | slug: about 117 | --- 118 | This is a sample wok site, use to demonstrate that yes, it is easy to make 119 | a wok site. 120 | 121 | Notice that we defined a slug here. We didn't have to, but if we didn't wok 122 | would have generated a slug of `about-this-site`. 123 | `www.example.com/about-this-site.html` doesn't look very good, so we defined 124 | our own slug, shortening it to simply `about`. 125 | 126 | ## Organization 127 | 128 | We put these page's content in files that matched their slugs. But wok doesn't 129 | really look at the file name, except to determine that they are Markdown files. 130 | Every file in the `content` directory is treated the same, regardless of file 131 | name, or directory structure. That means we could have called the about page 132 | page `foo/bar/bob.mkd`, and it wouldn't have changed anything, in wok's eyes. 133 | Feel free to organize the content files into any structure you want: completely 134 | flat, with no directories at all, one directory per month of writing, or a 135 | directory per category. It doesn't matter to wok. 136 | 137 | # Templates 138 | 139 | If you tried to run `wok` right now, it would give an error that the template 140 | `default.*` can not be found. That is because if a template wasn't specified 141 | in a content file -- which we didn't do -- `default.*` is used. 142 | 143 | > Why `default.*`, instead of `default.html`? Wok has the ability to generate 144 | > more than just HTML. If you wanted to render some LaTeX files, you could make 145 | > your default template `default.tex`, and wok will generate `index.tex` 146 | > instead of `index.html` (if your url-pattern allowed for this, which the 147 | > default does). This is also useful for generating things like RSS feeds. 148 | 149 | For now we will just make a single template file, and all of our pages will be 150 | the same. Templates are made using the Jinja template engine, which is very 151 | similar to Django's templates. Here is the content of `templates/default.html` 152 | 153 | 154 | 155 | 156 | 157 | Wok Quickstart 158 | 159 | 160 | 161 | 162 |
163 |

{{ page.title }}

164 | 169 |

170 | 171 |
172 | {{ page.content }} 173 |
174 | 175 | 176 | 177 | 178 | 179 | > `header`? `nav`? `article`? What are all these strange tags you keep using? 180 | > 181 | > This template was made using the HTML5 semantic elements. Adding additional 182 | > semantics to your code is a Good Thing (tm). For more information, check out 183 | > [Dive into HTML5's page about semantics][dive5]. 184 | 185 | [dive5]: http://diveintohtml5.org/semantics.html 186 | 187 | This is an extremely basic HTML5 template, that without some snazzy CSS is 188 | going to look awfully boring, but it will serve for our purposes. 189 | 190 | Notice the sections in wrapped in `{{ }}`. Those are Jinja variables. Wok 191 | provides several objects for the template engine, such as page, which contains 192 | the title of the page, the actual text content, and the author of the page (if 193 | you specified one). 194 | 195 | Each content file will be generated with a template, and since we didn't 196 | specify otherwise, they will all use the default template: this one. The 197 | resulting output will be saved in the output directory, with a path built from 198 | either the URL specified on the page, or the url-patterns of the site. 199 | 200 | # Rendering 201 | 202 | Now we are ready to turn our pile of code into a tasty web site. If you ran wok 203 | by itself, it would create an output directory, copy the contents of our (empty 204 | or nonexistent) media directory to it, and then render everything to that 205 | directory. This is good for copying serving your website from a real web server, 206 | like Apache, or uploading to your web server, but isn't very nice to preview 207 | during development, since root-relative links (which you should be using!) 208 | won't work when we view these files from a web browser. 209 | 210 | To fix this problem, wok provides the `--server` option, that will make wok run 211 | a simple, naive web server from the output directory. This isn't a production 212 | ready server by any means, but is nice to see your changes without uploading 213 | everything. Run this command from the base of your wok directory. 214 | 215 | wok --server 216 | 217 | It will say it is running a server, and then wait. Open the link it printed out 218 | (http://localhost:8000), and check out the site in your browser. 219 | 220 | Additionally, the development server will re-render the site on page load if 221 | any files have changed. This way you can edit a content, template, or media 222 | file, and then reload your browser to see the changes, instead of having to 223 | restart the server. 224 | 225 | # Next steps 226 | 227 | Congratulations, you have made a simple wok site. This should be enough to make 228 | a basic site, and combined with some HTML and CSS know-how, can lead to a good 229 | looking site that is quick and easy to add new content to, such as a blog. 230 | 231 | But wok has more power than this. For more advanced features, check out the 232 | rest of the documentation on this site. Additionally, users are encouraged to 233 | share and trade tips and cool things that can be done with wok on [the Github 234 | wiki][wiki] and share extensions on [the wok_hooks project][wok_hooks]. 235 | 236 | [wiki]: https://github.com/mythmon/wok/wiki 237 | [wok_hooks]: https://github.com/abbgrade/wok_hooks -------------------------------------------------------------------------------- /wok/engine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | import os 3 | import sys 4 | import shutil 5 | import fnmatch 6 | from datetime import datetime 7 | from optparse import OptionParser, OptionGroup 8 | import logging 9 | 10 | import yaml 11 | 12 | import wok 13 | from wok.page import Page, Author 14 | from wok import renderers 15 | from wok import util 16 | from wok.dev_server import dev_server 17 | 18 | import locale 19 | 20 | class Engine(object): 21 | """ 22 | The main engine of wok. Upon initialization, it generates a site from the 23 | source files. 24 | """ 25 | default_options = { 26 | 'content_dir': 'content', 27 | 'template_dir': 'templates', 28 | 'output_dir': 'output', 29 | 'media_dir': 'media', 30 | 'site_title': 'Some random Wok site', 31 | 'url_pattern': '/{category}/{slug}{page}.{ext}', 32 | 'url_include_index': True, 33 | 'slug_from_filename': False, 34 | 'relative_urls': False, 35 | 'locale': None, 36 | 'markdown_extra_plugins': [], 37 | 'ignore_files': [], 38 | 'rst_doctitle': False, 39 | } 40 | SITE_ROOT = os.getcwd() 41 | 42 | def __init__(self, output_lvl=1): 43 | """ 44 | Set up CLI options, logging levels, and start everything off. 45 | Afterwards, run a dev server if asked to. 46 | """ 47 | 48 | # CLI options 49 | # ----------- 50 | parser = OptionParser(version='%prog v{0}'.format(wok.version)) 51 | 52 | # Add option to initialize an new project 53 | init_grp = OptionGroup(parser, "Initialize project", 54 | "Creates a config file and the required directories. ") 55 | init_grp.add_option('--init', action='store_true', 56 | dest='initproject', 57 | help="create a confg file before generating the site") 58 | init_grp.add_option('--site_title', 59 | dest='site_title', 60 | help="configures the site title to the given value") 61 | parser.add_option_group(init_grp) 62 | 63 | # Add option to to run the development server after generating pages 64 | devserver_grp = OptionGroup(parser, "Development server", 65 | "Runs a small development server after site generation. " 66 | "--address and --port will be ignored if --server is absent.") 67 | devserver_grp.add_option('--server', action='store_true', 68 | dest='runserver', 69 | help="run a development server after generating the site") 70 | devserver_grp.add_option('--address', action='store', dest='address', 71 | help="specify ADDRESS on which to run development server") 72 | devserver_grp.add_option('--port', action='store', dest='port', 73 | type='int', 74 | help="specify PORT on which to run development server") 75 | parser.add_option_group(devserver_grp) 76 | 77 | # Options for noisiness level and logging 78 | logging_grp = OptionGroup(parser, "Logging", 79 | "By default, log messages will be sent to standard out, " 80 | "and report only errors and warnings.") 81 | parser.set_defaults(loglevel=logging.WARNING) 82 | logging_grp.add_option('-q', '--quiet', action='store_const', 83 | const=logging.ERROR, dest='loglevel', 84 | help="be completely quiet, log nothing") 85 | logging_grp.add_option('--warnings', action='store_const', 86 | const=logging.WARNING, dest='loglevel', 87 | help="log warnings in addition to errors") 88 | logging_grp.add_option('-v', '--verbose', action='store_const', 89 | const=logging.INFO, dest='loglevel', 90 | help="log ALL the things!") 91 | logging_grp.add_option('--debug', action='store_const', 92 | const=logging.DEBUG, dest='loglevel', 93 | help="log debugging info in addition to warnings and errors") 94 | logging_grp.add_option('--log', '-l', dest='logfile', 95 | help="log to the specified LOGFILE instead of standard out") 96 | parser.add_option_group(logging_grp) 97 | 98 | cli_options, args = parser.parse_args() 99 | 100 | # Set up logging 101 | # -------------- 102 | logging_options = { 103 | 'format': '%(levelname)s: %(message)s', 104 | 'level': cli_options.loglevel, 105 | } 106 | if cli_options.logfile: 107 | logging_options['filename'] = cli_options.logfile 108 | else: 109 | logging_options['stream'] = sys.stdout 110 | 111 | logging.basicConfig(**logging_options) 112 | 113 | # Init project 114 | # ------------ 115 | 116 | if cli_options.initproject: 117 | ''' Create the config file and the required directories if the user said to. 118 | ''' 119 | orig_dir = os.getcwd() 120 | os.chdir(self.SITE_ROOT) 121 | 122 | # create config 123 | 124 | options = Engine.default_options.copy() 125 | 126 | # read old config if present 127 | if os.path.isfile('config'): 128 | with open('config') as f: 129 | yaml_config = yaml.load(f) 130 | 131 | if yaml_config: 132 | options.update(yaml_config) 133 | 134 | if cli_options.site_title: 135 | options['site_title'] = cli_options.site_title 136 | 137 | # save new config 138 | with open('config', 'w') as f: 139 | yaml.dump(options, f) 140 | 141 | # create required dirs 142 | 143 | required_dirs = [options['content_dir'], options['template_dir']] 144 | for required_dir in required_dirs: 145 | if not os.path.isdir(required_dir): 146 | os.mkdir(required_dir) 147 | 148 | os.chdir(orig_dir) 149 | 150 | # Action! 151 | # ------- 152 | self.generate_site() 153 | 154 | # Dev server 155 | # ---------- 156 | if cli_options.runserver: 157 | ''' Run the dev server if the user said to, and watch the specified 158 | directories for changes. The server will regenerate the entire wok 159 | site if changes are found after every request. 160 | ''' 161 | output_dir = os.path.join(self.options['server_root']) 162 | host = '' if cli_options.address is None else cli_options.address 163 | port = 8000 if cli_options.port is None else cli_options.port 164 | server = dev_server(serv_dir=output_dir, host=host, port=port, 165 | dir_mon=True, 166 | watch_dirs=[ 167 | self.options['media_dir'], 168 | self.options['template_dir'], 169 | self.options['content_dir'] 170 | ], 171 | change_handler=self.generate_site) 172 | server.run() 173 | 174 | def generate_site(self): 175 | ''' Generate the wok site ''' 176 | orig_dir = os.getcwd() 177 | os.chdir(self.SITE_ROOT) 178 | 179 | self.all_pages = [] 180 | 181 | self.read_options() 182 | self.sanity_check() 183 | self.load_hooks() 184 | self.load_renderers() 185 | self.renderer_options() 186 | 187 | self.run_hook('site.start') 188 | 189 | self.prepare_output() 190 | self.load_pages() 191 | self.make_tree() 192 | self.render_site() 193 | 194 | self.run_hook('site.done') 195 | 196 | os.chdir(orig_dir) 197 | 198 | def read_options(self): 199 | """Load options from the config file.""" 200 | self.options = Engine.default_options.copy() 201 | 202 | if os.path.isfile('config'): 203 | with open('config') as f: 204 | yaml_config = yaml.load(f) 205 | 206 | if yaml_config: 207 | self.options.update(yaml_config) 208 | 209 | # Make authors a list, even only a single author was specified. 210 | authors = self.options.get('authors', self.options.get('author', None)) 211 | if isinstance(authors, list): 212 | self.options['authors'] = [Author.parse(a) for a in authors] 213 | elif isinstance(authors, str): 214 | csv = authors.split(',') 215 | self.options['authors'] = [Author.parse(a) for a in csv] 216 | if len(self.options['authors']) > 1: 217 | logging.warn('Deprecation Warning: Use YAML lists instead of ' 218 | 'CSV for multiple authors. i.e. ["John Doe", "Jane ' 219 | 'Smith"] instead of "John Doe, Jane Smith". In config ' 220 | 'file.') 221 | 222 | if '{type}' in self.options['url_pattern']: 223 | logging.warn('Deprecation Warning: You should use {ext} instead ' 224 | 'of {type} in the url pattern specified in the config ' 225 | 'file.') 226 | 227 | # Set locale if needed 228 | wanted_locale = self.options.get('locale') 229 | if wanted_locale is not None: 230 | try: 231 | locale.setlocale(locale.LC_TIME, wanted_locale) 232 | except locale.Error as err: 233 | logging.warn('Unable to set locale to `%s`: %s', 234 | wanted_locale, err 235 | ) 236 | 237 | # add a subdir prefix to the output_dir, if present in the config 238 | self.options['server_root'] = self.options['output_dir'] 239 | self.options['output_dir'] = os.path.join(self.options['output_dir'], self.options.get('url_subdir', '')) 240 | 241 | def renderer_options(self): 242 | """Monkeypatches renderer options as in `config` file.""" 243 | # Markdown extra plugins 244 | markdown_extra_plugins = \ 245 | self.options.get('markdown_extra_plugins', []) 246 | if hasattr(renderers, 'Markdown'): 247 | renderers.Markdown.plugins.extend(markdown_extra_plugins) 248 | if hasattr(renderers, 'Markdown2'): 249 | renderers.Markdown2.extras.extend(markdown_extra_plugins) 250 | 251 | # reStructuredText options 252 | if hasattr(renderers, 'ReStructuredText'): 253 | renderers.ReStructuredText.options.update( \ 254 | {'doctitle' : self.options.get('rst_doctitle', False), \ 255 | }) 256 | 257 | def sanity_check(self): 258 | """Basic sanity checks.""" 259 | # Make sure that this is (probabably) a wok source directory. 260 | if not (os.path.isdir('templates') or os.path.isdir('content')): 261 | logging.critical("This doesn't look like a wok site. Aborting.") 262 | sys.exit(1) 263 | 264 | def load_hooks(self): 265 | try: 266 | sys.path.append('hooks') 267 | import __hooks__ 268 | self.hooks = __hooks__.hooks 269 | logging.info('Loaded {0} hooks: {0}'.format(self.hooks)) 270 | except ImportError as e: 271 | if "__hooks__" in str(e): 272 | logging.info('No hooks module found.') 273 | else: 274 | # don't catch import errors raised within a hook 275 | logging.info('Import error within hooks.') 276 | raise 277 | 278 | def load_renderers(self): 279 | self.renderers = {} 280 | for renderer in renderers.all: 281 | self.renderers.update((ext, renderer) for ext in renderer.extensions) 282 | 283 | try: 284 | sys.path.append('renderers') 285 | import __renderers__ 286 | self.renderers.update(__renderers__.renderers) 287 | logging.info('Loaded {0} renderers'.format(len(__renderers__.renderers))) 288 | except ImportError as e: 289 | if "__renderers__" in str(e): 290 | logging.info('No renderers module found.') 291 | else: 292 | # don't catch import errors raised within a renderer 293 | logging.info('Import error within renderers.') 294 | raise 295 | 296 | def run_hook(self, hook_name, *args): 297 | """ Run specified hooks if they exist """ 298 | logging.debug('Running hook {0}'.format(hook_name)) 299 | returns = [] 300 | try: 301 | for hook in self.hooks.get(hook_name, []): 302 | returns.append(hook(self.options, *args)) 303 | except AttributeError: 304 | logging.info('Hook {0} not defined'.format(hook_name)) 305 | return returns 306 | 307 | def prepare_output(self): 308 | """ 309 | Prepare the output directory. Remove any contents there already, and 310 | then copy over the media files, if they exist. 311 | """ 312 | if os.path.isdir(self.options['output_dir']): 313 | for name in os.listdir(self.options['output_dir']): 314 | # Don't remove dotfiles 315 | if name[0] == ".": 316 | continue 317 | path = os.path.join(self.options['output_dir'], name) 318 | if os.path.isfile(path): 319 | os.unlink(path) 320 | else: 321 | shutil.rmtree(path) 322 | else: 323 | os.makedirs(self.options['output_dir']) 324 | 325 | self.run_hook('site.output.pre', self.options['output_dir']) 326 | 327 | # Copy the media directory to the output folder 328 | if os.path.isdir(self.options['media_dir']): 329 | try: 330 | for name in os.listdir(self.options['media_dir']): 331 | path = os.path.join(self.options['media_dir'], name) 332 | if os.path.isdir(path): 333 | shutil.copytree( 334 | path, 335 | os.path.join(self.options['output_dir'], name), 336 | symlinks=True 337 | ) 338 | else: 339 | shutil.copy(path, self.options['output_dir']) 340 | 341 | 342 | # Do nothing if the media directory doesn't exist 343 | except OSError: 344 | logging.warning('There was a problem copying the media files ' 345 | 'to the output directory.') 346 | 347 | self.run_hook('site.output.post', self.options['output_dir']) 348 | 349 | def load_pages(self): 350 | """Load all the content files.""" 351 | # Load pages from hooks (pre) 352 | for pages in self.run_hook('site.content.gather.pre'): 353 | if pages: 354 | self.all_pages.extend(pages) 355 | 356 | # Load files 357 | for root, dirs, files in os.walk(self.options['content_dir']): 358 | # Filter out files if they match any of the ignore patterns 359 | for ig in self.options['ignore_files']: 360 | files = [ f for f in files 361 | if not fnmatch.fnmatch(os.path.basename(f), ig) ] 362 | # Grab all the parsable files 363 | for f in files: 364 | # Don't parse hidden files. 365 | if f.startswith('.'): 366 | continue 367 | 368 | ext = f.split('.')[-1] 369 | renderer = self.renderers.get(ext) 370 | 371 | if renderer is None: 372 | logging.warning('No parser found ' 373 | 'for {0}. Using default renderer.'.format(f)) 374 | renderer = renderers.Renderer 375 | 376 | p = Page.from_file(os.path.join(root, f), self.options, self, renderer) 377 | if p and p.meta['published']: 378 | self.all_pages.append(p) 379 | 380 | # Load pages from hooks (post) 381 | for pages in self.run_hook('site.content.gather.post', self.all_pages): 382 | if pages: 383 | self.all_pages.extend(pages) 384 | 385 | def make_tree(self): 386 | """ 387 | Make the category pseudo-tree. 388 | 389 | In this structure, each node is a page. Pages with sub pages are 390 | interior nodes, and leaf nodes have no sub pages. It is not truly a 391 | tree, because the root node doesn't exist. 392 | """ 393 | self.categories = {} 394 | site_tree = [] 395 | # We want to parse these in a approximately breadth first order 396 | self.all_pages.sort(key=lambda p: len(p.meta['category'])) 397 | 398 | # For every page 399 | for p in self.all_pages: 400 | # If it has a category (ie: is not at top level) 401 | if len(p.meta['category']) > 0: 402 | top_cat = p.meta['category'][0] 403 | if not top_cat in self.categories: 404 | self.categories[top_cat] = [] 405 | 406 | self.categories[top_cat].append(p.meta) 407 | 408 | try: 409 | # Put this page's meta in the right place in site_tree. 410 | siblings = site_tree 411 | for cat in p.meta['category']: 412 | # This line will fail if the page is an orphan 413 | parent = [subpage for subpage in siblings 414 | if subpage['slug'] == cat][0] 415 | siblings = parent['subpages'] 416 | siblings.append(p.meta) 417 | except IndexError: 418 | logging.error('It looks like the page "{0}" is an orphan! ' 419 | 'This will probably cause problems.'.format(p.path)) 420 | 421 | def render_site(self): 422 | """Render every page and write the output files.""" 423 | # Gather tags 424 | tag_set = set() 425 | for p in self.all_pages: 426 | tag_set = tag_set.union(p.meta['tags']) 427 | tag_dict = dict() 428 | for tag in tag_set: 429 | # Add all pages with the current tag to the tag dict 430 | tag_dict[tag] = [p.meta for p in self.all_pages 431 | if tag in p.meta['tags']] 432 | 433 | # Gather slugs 434 | slug_dict = dict((p.meta['slug'], p.meta) for p in self.all_pages) 435 | 436 | for p in self.all_pages: 437 | # Construct this every time, to avoid sharing one instance 438 | # between page objects. 439 | templ_vars = { 440 | 'site': { 441 | 'title': self.options.get('site_title', 'Untitled'), 442 | 'datetime': datetime.now(), 443 | 'date': datetime.now().date(), 444 | 'time': datetime.now().time(), 445 | 'tags': tag_dict, 446 | 'pages': self.all_pages[:], 447 | 'categories': self.categories, 448 | 'slugs': slug_dict, 449 | }, 450 | } 451 | 452 | for k, v in self.options.iteritems(): 453 | if k not in ('site_title', 'output_dir', 'content_dir', 454 | 'templates_dir', 'media_dir', 'url_pattern'): 455 | 456 | templ_vars['site'][k] = v 457 | 458 | if 'author' in self.options: 459 | templ_vars['site']['author'] = self.options['author'] 460 | 461 | # Rendering the page might give us back more pages to render. 462 | new_pages = p.render(templ_vars) 463 | 464 | if p.meta['make_file']: 465 | p.write() 466 | 467 | if new_pages: 468 | logging.debug('found new_pages') 469 | self.all_pages += new_pages 470 | 471 | if __name__ == '__main__': 472 | Engine() 473 | exit(0) 474 | -------------------------------------------------------------------------------- /wok/page.py: -------------------------------------------------------------------------------- 1 | # System 2 | import os 3 | import sys 4 | from collections import namedtuple 5 | from datetime import datetime, date, time 6 | import logging 7 | import copy 8 | 9 | # Libraries 10 | import jinja2 11 | import yaml 12 | import re 13 | from slugify import slugify 14 | 15 | # Wok 16 | from wok import util 17 | from wok import renderers 18 | from wok.jinja import GlobFileLoader, AmbiguousTemplate 19 | 20 | class Page(object): 21 | """ 22 | A single page on the website in all its form (raw, rendered, templated), 23 | as well as its associated metadata. 24 | """ 25 | 26 | tmpl_env = None 27 | 28 | @classmethod 29 | def create_tmpl_env(cls, options): 30 | cls.tmpl_env = jinja2.Environment( 31 | loader=GlobFileLoader( 32 | searchpath=options.get('template_dir', 'templates'), 33 | ignores=options.get('ignore_files', [])), 34 | extensions=options.get('jinja2_extensions', [])) 35 | 36 | def __init__(self, options, engine): 37 | self.options = options 38 | self.filename = None 39 | self.meta = {} 40 | self.engine = engine 41 | 42 | @classmethod 43 | def from_meta(cls, meta, options, engine, renderer=renderers.Plain): 44 | """ 45 | Build a page object from a meta dictionary. 46 | 47 | Note that you still need to call `render` and `write` to do anything 48 | interesting. 49 | """ 50 | page = cls(options, engine) 51 | page.meta = meta 52 | page.options = options 53 | page.renderer = renderer 54 | 55 | if 'pagination' in meta: 56 | logging.debug('from_meta: current page %d' % 57 | meta['pagination']['cur_page']) 58 | 59 | # Make a template environment. Hopefully no one expects this to ever 60 | # change after it is instantiated. 61 | if cls.tmpl_env is None: 62 | cls.create_tmpl_env(page.options) 63 | 64 | page.build_meta() 65 | return page 66 | 67 | @classmethod 68 | def from_file(cls, path, options, engine, renderer=renderers.Plain): 69 | """ 70 | Load a file from disk, and parse the metadata from it. 71 | 72 | Note that you still need to call `render` and `write` to do anything 73 | interesting. 74 | """ 75 | page = cls(options, engine) 76 | page.original = None 77 | page.options = options 78 | page.renderer = renderer 79 | 80 | logging.info('Loading {0}'.format(os.path.basename(path))) 81 | 82 | if cls.tmpl_env is None: 83 | cls.create_tmpl_env(page.options) 84 | 85 | page.path = path 86 | page.filename = os.path.basename(path) 87 | 88 | with open(path, 'rU') as f: 89 | page.original = f.read().decode('utf-8') 90 | splits = page.original.split('\n---\n') 91 | 92 | if len(splits) > 3: 93 | logging.warning('Found more --- delimited sections in {0} ' 94 | 'than expected. Squashing the extra together.' 95 | .format(page.path)) 96 | 97 | # Handle the case where no metadata was provided. 98 | if len(splits) == 1: 99 | page.original = splits[0] 100 | page.meta = {} 101 | page.original_preview = '' 102 | 103 | elif len(splits) == 2: 104 | header = splits[0] 105 | page.meta = yaml.load(header) 106 | page.original = splits[1] 107 | page.original_preview = page.meta.get('preview', '') 108 | 109 | elif len(splits) >= 3: 110 | header = splits[0] 111 | page.meta = {} 112 | page.original = '\n'.join(splits[1:]) 113 | page.original_preview = splits[1] 114 | page.meta.update(yaml.load(header)) 115 | logging.debug('Got preview') 116 | 117 | page.build_meta() 118 | 119 | page.engine.run_hook('page.render.pre', page) 120 | page.meta['content'] = page.renderer.render(page.original, page.meta) # the page.meta might contain renderer options... 121 | page.meta['preview'] = page.renderer.render(page.original_preview, page.meta) 122 | page.engine.run_hook('page.render.post', page) 123 | 124 | return page 125 | 126 | def build_meta(self): 127 | """ 128 | Ensures the guarantees about metadata for documents are valid. 129 | 130 | `page.title` - Will be a string. 131 | `page.slug` - Will be a string. 132 | `page.author` - Will have fields `name` and `email`. 133 | `page.authors` - Will be a list of Authors. 134 | `page.category` - Will be a list. 135 | `page.published` - Will exist. 136 | `page.datetime` - Will be a datetime, or None. 137 | `page.date` - Will be a date, or None. 138 | `page.time` - Will be a time, or None. 139 | `page.tags` - Will be a list. 140 | `page.url` - Will be the url of the page, relative to the web root. 141 | `page.subpages` - Will be a list containing every sub page of this page 142 | """ 143 | 144 | self.engine.run_hook('page.meta.pre', self) 145 | 146 | if not self.meta: 147 | self.meta = {} 148 | 149 | # source_path 150 | if not 'source_path' in self.meta: 151 | self.meta['source_path'] = None 152 | if self.filename: 153 | self.meta['source_path'] = self.filename 154 | 155 | # title 156 | if not 'title' in self.meta: 157 | if self.filename: 158 | # Take off the last file extension. 159 | self.meta['title'] = '.'.join(self.filename.split('.')[:-1]) 160 | if (self.meta['title'] == ''): 161 | self.meta['title'] = self.filename 162 | 163 | logging.warning("You didn't specify a title in {0}. Using the " 164 | "file name as a title.".format(self.path)) 165 | elif 'slug' in self.meta: 166 | self.meta['title'] = self.meta['slug'] 167 | logging.warning("You didn't specify a title in {0}, which was " 168 | "not generated from a file. Using the slug as a title." 169 | .format(self.meta['slug'])) 170 | else: 171 | logging.error("A page was generated that is not from a file, " 172 | "has no title, and no slug. I don't know what to do. " 173 | "Not using this page.") 174 | logging.info("Bad Meta's keys: {0}".format(self.meta.keys())) 175 | logging.debug("Bad Meta: {0}".format(self.meta)) 176 | raise BadMetaException() 177 | 178 | # slug 179 | if not 'slug' in self.meta: 180 | if self.filename and self.options['slug_from_filename']: 181 | filename_no_ext = '.'.join(self.filename.split('.')[:-1]) 182 | if filename_no_ext == '': 183 | filename_no_ext = self.filename 184 | self.meta['slug'] = slugify(filename_no_ext) 185 | logging.info("You didn't specify a slug, generating it from the " 186 | "filename.") 187 | else: 188 | self.meta['slug'] = slugify(unicode(self.meta['title'])) 189 | logging.info("You didn't specify a slug, and no filename " 190 | "exists. Generating the slug from the title.") 191 | 192 | elif self.meta['slug'] != slugify(self.meta['slug']): 193 | logging.warning('Your slug should probably be all lower case, and ' 194 | 'match "[a-z0-9-]*"') 195 | 196 | # authors and author 197 | authors = self.meta.get('authors', self.meta.get('author', None)) 198 | if isinstance(authors, list): 199 | self.meta['authors'] = [Author.parse(a) for a in authors] 200 | elif isinstance(authors, str): 201 | self.meta['authors'] = [Author.parse(a) for a in authors.split(',')] 202 | if len(self.meta['authors']) > 1: 203 | logging.warn('Deprecation Warning: Use YAML lists instead of ' 204 | 'CSV for multiple authors. i.e. ["John Doe", "Jane ' 205 | 'Smith"] instead of "John Doe, Jane Smith". In ' 206 | '{0}.'.format(self.path)) 207 | 208 | elif authors is None: 209 | self.meta['authors'] = self.options.get('authors', []) 210 | else: 211 | # wait, what? Authors is of wrong type. 212 | self.meta['authors'] = [] 213 | logging.error(('Authors in {0} is an unknown type. Valid types ' 214 | 'are string or list. Instead it is a {1}') 215 | .format(self.meta['slug']), authors.type) 216 | 217 | if self.meta['authors']: 218 | self.meta['author'] = self.meta['authors'][0] 219 | else: 220 | self.meta['author'] = Author() 221 | 222 | # category 223 | if 'category' in self.meta: 224 | if isinstance(self.meta['category'], str): 225 | self.meta['category'] = self.meta['category'].split('/') 226 | elif isinstance(self.meta['category'], list): 227 | pass 228 | else: 229 | # category is of wrong type. 230 | logging.error('Category in {0} is an unknown type. Valid ' 231 | 'types are string or list. Instead it is a {1}' 232 | .format(self.meta['slug'], type(self.meta['category']))) 233 | self.meta['category'] = [] 234 | else: 235 | self.meta['category'] = [] 236 | if self.meta['category'] == None: 237 | self.meta = [] 238 | 239 | # published 240 | if not 'published' in self.meta: 241 | self.meta['published'] = True 242 | 243 | # make_file 244 | if not 'make_file' in self.meta: 245 | self.meta['make_file'] = True 246 | 247 | # datetime, date, time 248 | util.date_and_times(self.meta) 249 | 250 | # tags 251 | if 'tags' in self.meta: 252 | if isinstance(self.meta['tags'], list): 253 | # good 254 | pass 255 | elif isinstance(self.meta['tags'], str): 256 | self.meta['tags'] = [t.strip() for t in 257 | self.meta['tags'].split(',')] 258 | if len(self.meta['tags']) > 1: 259 | logging.warn('Deprecation Warning: Use YAML lists instead ' 260 | 'of CSV for multiple tags. i.e. tags: [guide, ' 261 | 'howto] instead of tags: guide, howto. In {0}.' 262 | .format(self.path)) 263 | else: 264 | self.meta['tags'] = [] 265 | 266 | logging.debug('Tags for {0}: {1}'. 267 | format(self.meta['slug'], self.meta['tags'])) 268 | 269 | # pagination 270 | if 'pagination' not in self.meta: 271 | self.meta['pagination'] = {} 272 | 273 | if 'cur_page' not in self.meta['pagination']: 274 | self.meta['pagination']['cur_page'] = 1 275 | if 'num_pages' not in self.meta['pagination']: 276 | self.meta['pagination']['num_pages'] = 1 277 | 278 | # template 279 | try: 280 | template_type = str(self.meta.get('type', 'default')) 281 | self.template = self.tmpl_env.get_template(template_type + '.*') 282 | except jinja2.loaders.TemplateNotFound: 283 | logging.error('No template "{0}.*" found in template directory. Aborting.' 284 | .format(template_type)) 285 | sys.exit() 286 | except AmbiguousTemplate: 287 | logging.error(('Ambiguous template found. There are two files that ' 288 | 'match "{0}.*". Aborting.').format(template_type)) 289 | sys.exit() 290 | 291 | # url 292 | parts = { 293 | 'slug': self.meta['slug'], 294 | 'category': '/'.join(self.meta['category']), 295 | 'page': self.meta['pagination']['cur_page'], 296 | 'date': self.meta['date'], 297 | 'datetime': self.meta['datetime'], 298 | 'time': self.meta['time'], 299 | } 300 | logging.debug('current page: ' + repr(parts['page'])) 301 | 302 | # Pull extensions from the template's real file name. 303 | parts['ext'] = os.path.splitext(self.template.filename)[1] 304 | if parts['ext']: 305 | parts['ext'] = parts['ext'][1:] # remove leading dot 306 | # Deprecated 307 | parts['type'] = parts['ext'] 308 | self.meta['ext'] = parts['ext'] 309 | 310 | if parts['page'] == 1: 311 | parts['page'] = '' 312 | 313 | if 'url' in self.meta: 314 | logging.debug('Using page url pattern') 315 | self.url_pattern = self.meta['url'] 316 | else: 317 | logging.debug('Using global url pattern') 318 | self.url_pattern = self.options['url_pattern'] 319 | 320 | self.meta['url'] = self.url_pattern.format(**parts) 321 | 322 | logging.info('URL pattern is: {0}'.format(self.url_pattern)) 323 | logging.info('URL parts are: {0}'.format(parts)) 324 | 325 | # Get rid of extra slashes 326 | self.meta['url'] = re.sub(r'//+', '/', self.meta['url']) 327 | 328 | # If we have been asked to, rip out any plain "index.html"s 329 | if not self.options['url_include_index']: 330 | self.meta['url'] = re.sub(r'/index\.html$', '/', self.meta['url']) 331 | 332 | # To be used for writing page content 333 | self.meta['path'] = self.meta['url'] 334 | # If site is going to be in a subdirectory 335 | if self.options.get('url_subdir'): 336 | self.meta['url'] = self.options['url_subdir'] + self.meta['url'] 337 | 338 | # Some urls should start with /, some should not. 339 | if self.options['relative_urls'] and self.meta['url'][0] == '/': 340 | self.meta['url'] = self.meta['url'][1:] 341 | if not self.options['relative_urls'] and self.meta['url'][0] != '/': 342 | self.meta['url'] = '/' + self.meta['url'] 343 | 344 | logging.debug('url is: ' + self.meta['url']) 345 | 346 | # subpages 347 | self.meta['subpages'] = [] 348 | 349 | self.engine.run_hook('page.meta.post', self) 350 | 351 | def render(self, templ_vars=None): 352 | """ 353 | Renders the page with the template engine. 354 | """ 355 | logging.debug('Rendering ' + self.meta['slug']) 356 | if not templ_vars: 357 | templ_vars = {} 358 | 359 | # Handle pagination if we needed. 360 | if 'pagination' in self.meta and 'list' in self.meta['pagination']: 361 | extra_pages = self.paginate(templ_vars) 362 | else: 363 | extra_pages = [] 364 | 365 | # Don't clobber possible values in the template variables. 366 | if 'page' in templ_vars: 367 | logging.debug('Found defaulted page data.') 368 | templ_vars['page'].update(self.meta) 369 | else: 370 | templ_vars['page'] = self.meta 371 | 372 | # Don't clobber pagination either. 373 | if 'pagination' in templ_vars: 374 | templ_vars['pagination'].update(self.meta['pagination']) 375 | else: 376 | templ_vars['pagination'] = self.meta['pagination'] 377 | 378 | # ... and actions! (and logging, and hooking) 379 | self.engine.run_hook('page.template.pre', self, templ_vars) 380 | logging.debug('templ_vars.keys(): ' + repr(templ_vars.keys())) 381 | self.rendered = self.template.render(templ_vars) 382 | logging.debug('extra pages is: ' + repr(extra_pages)) 383 | self.engine.run_hook('page.template.post', self) 384 | 385 | return extra_pages 386 | 387 | def paginate(self, templ_vars): 388 | extra_pages = [] 389 | logging.debug('called pagination for {0}'.format(self.meta['slug'])) 390 | if 'page_items' not in self.meta['pagination']: 391 | logging.debug('doing pagination for {0}'.format(self.meta['slug'])) 392 | # This is the first page of a set of pages. Set up the rest. Other 393 | # wise don't do anything. 394 | 395 | source_spec = self.meta['pagination']['list'].split('.') 396 | logging.debug('pagination source is: ' + repr(source_spec)) 397 | 398 | if source_spec[0] == 'page': 399 | source = self.meta 400 | source_spec.pop(0) 401 | elif source_spec[0] == 'site': 402 | source = templ_vars['site'] 403 | source_spec.pop(0) 404 | else: 405 | logging.error('Unknown pagination source! Not paginating') 406 | return 407 | 408 | for k in source_spec: 409 | source = source[k] 410 | 411 | sort_key = self.meta['pagination'].get('sort_key', None) 412 | sort_reverse = self.meta['pagination'].get('sort_reverse', False) 413 | 414 | logging.debug('sort_key: {0}, sort_reverse: {1}'.format( 415 | sort_key, sort_reverse)) 416 | 417 | if not source: 418 | return extra_pages 419 | if isinstance(source[0], Page): 420 | source = [p.meta for p in source] 421 | 422 | if sort_key is not None: 423 | if isinstance(source[0], dict): 424 | source.sort(key=lambda x: x[sort_key], 425 | reverse=sort_reverse) 426 | else: 427 | source.sort(key=lambda x: x.__getattribute__(sort_key), 428 | reverse=sort_reverse) 429 | 430 | chunks = list(util.chunk(source, self.meta['pagination']['limit'])) 431 | if not chunks: 432 | return extra_pages 433 | 434 | # Make a page for each chunk 435 | for idx, chunk in enumerate(chunks[1:], 2): 436 | new_meta = copy.deepcopy(self.meta) 437 | new_meta.update({ 438 | 'url': self.url_pattern, 439 | 'pagination': { 440 | 'page_items': chunk, 441 | 'num_pages': len(chunks), 442 | 'cur_page': idx, 443 | } 444 | }) 445 | new_page = self.from_meta(new_meta, self.options, self.engine, 446 | renderer=self.renderer) 447 | logging.debug('page {0} is {1}'.format(idx, new_page)) 448 | if new_page: 449 | extra_pages.append(new_page) 450 | 451 | # Set up the next/previous page links 452 | for idx, page in enumerate(extra_pages): 453 | if idx == 0: 454 | page.meta['pagination']['prev_page'] = self.meta 455 | else: 456 | page.meta['pagination']['prev_page'] = extra_pages[idx-1].meta 457 | 458 | if idx < len(extra_pages) - 1: 459 | page.meta['pagination']['next_page'] = extra_pages[idx+1].meta 460 | else: 461 | page.meta['pagination']['next_page'] = None 462 | 463 | # Pagination date for this page 464 | self.meta['pagination'].update({ 465 | 'page_items': chunks[0], 466 | 'num_pages': len(chunks), 467 | 'cur_page': 1, 468 | }) 469 | # Extra pages doesn't include the first page, so if there is at 470 | # least one, then make a link to the next page. 471 | if len(extra_pages) > 0: 472 | self.meta['pagination']['next_page'] = extra_pages[0].meta 473 | 474 | return extra_pages 475 | 476 | def write(self): 477 | """Write the page to a rendered file on disk.""" 478 | 479 | # Use what we are passed, or the default given, or the current dir 480 | base_path = self.options.get('output_dir', '.') 481 | path = self.meta['path'] 482 | if path and path[0] == '/': 483 | path = path[1:] 484 | base_path = os.path.join(base_path, path) 485 | if base_path.endswith('/'): 486 | base_path += 'index.' + self.meta['ext'] 487 | 488 | try: 489 | os.makedirs(os.path.dirname(base_path)) 490 | except OSError as e: 491 | logging.debug('makedirs failed for {0}'.format( 492 | os.path.basename(base_path))) 493 | # Probably that the dir already exists, so thats ok. 494 | # TODO: double check this. Permission errors are something to worry 495 | # about 496 | logging.info('writing to {0}'.format(base_path)) 497 | 498 | logging.debug('Writing {0} to {1}'.format(self.meta['slug'], base_path)) 499 | f = open(base_path, 'w') 500 | f.write(self.rendered.encode('utf-8')) 501 | f.close() 502 | 503 | def __repr__(self): 504 | return "<wok.page.Page '{0}'>".format(self.meta['slug']) 505 | 506 | 507 | class Author(object): 508 | """Smartly manages a author with name and email""" 509 | parse_author_regex = re.compile(r'^([^<>]*) *(<(.*@.*)>)?$') 510 | 511 | def __init__(self, raw='', name=None, email=None): 512 | self.raw = raw.strip() 513 | self.name = name 514 | self.email = email 515 | 516 | @classmethod 517 | def parse(cls, raw): 518 | if isinstance(raw, cls): 519 | return raw 520 | 521 | a = cls(raw) 522 | a.name, _, a.email = cls.parse_author_regex.match(raw).groups() 523 | if a.name: 524 | a.name = a.name.strip() 525 | if a.email: 526 | a.email = a.email.strip() 527 | return a 528 | 529 | def __str__(self): 530 | if not self.name: 531 | return self.raw 532 | if not self.email: 533 | return self.name 534 | 535 | return "{0} <{1}>".format(self.name, self.email) 536 | 537 | def __repr__(self): 538 | return '">'.format(self.name, self.email) 539 | 540 | def __unicode__(self): 541 | s = self.__str__() 542 | return s.replace('<', '<').replace('>', '>') 543 | 544 | class BadMetaException(Exception): 545 | pass 546 | --------------------------------------------------------------------------------