Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'release_0.9' into stable

Conflicts:
	docs/content/docs/urls.mkd
	docs/templates/base.html
	sample/content/tests/dates.mkd
	sample/content/tests/dates1.mkd
	wok/__init__.py
  • Loading branch information...
commit 05160b5df03c243836a4b1913a0682877753f1c7 2 parents faca0b4 + 594b51f
@mythmon authored
Showing with 734 additions and 192 deletions.
  1. +33 −47 README.mkd
  2. +2 −2 docs/README
  3. +16 −8 docs/content/community.mkd
  4. +8 −14 docs/content/docs.mkd
  5. +168 −0 docs/content/docs/hooks.mkd
  6. +16 −6 docs/content/docs/urls.mkd
  7. +48 −0 docs/content/download.mkd
  8. +16 −17 docs/content/home.mkd
  9. +5 −0 docs/hooks/__hooks__.py
  10. 0  docs/hooks/__init__.py
  11. +20 −0 docs/media/css/base.css
  12. +6 −2 docs/templates/base.html
  13. +24 −10 setup.py
  14. +1 −1  {sample → test_site}/config
  15. +1 −0  {sample → test_site}/content/pagination-bits/a.mkd
  16. +1 −0  {sample → test_site}/content/pagination-bits/b.mkd
  17. +1 −0  {sample → test_site}/content/pagination-bits/c.mkd
  18. +1 −0  {sample → test_site}/content/pagination-bits/d.mkd
  19. +1 −0  {sample → test_site}/content/pagination-bits/e.mkd
  20. +1 −0  {sample → test_site}/content/pagination-bits/f.mkd
  21. +1 −0  {sample → test_site}/content/pagination-bits/g.mkd
  22. +1 −0  {sample → test_site}/content/pagination-bits/h.mkd
  23. +1 −0  {sample → test_site}/content/pagination-bits/i.mkd
  24. +1 −0  {sample → test_site}/content/pagination-bits/j.mkd
  25. +1 −0  {sample → test_site}/content/pagination-bits/k.mkd
  26. 0  {sample → test_site}/content/pagination-test.mkd
  27. +1 −1  {sample → test_site}/content/tests.mkd
  28. +5 −0 test_site/content/tests/dates.mkd
  29. +6 −0 test_site/content/tests/dates1.mkd
  30. 0  {sample → test_site}/content/tests/dates2.mkd
  31. 0  {sample → test_site}/content/tests/dates3.mkd
  32. 0  {sample → test_site}/content/tests/markdown.mkd
  33. 0  {sample → test_site}/content/tests/plain.txt
  34. 0  {sample → test_site}/content/tests/rest_titles.rst
  35. 0  {sample → test_site}/content/tests/restructuredtext.rst
  36. 0  {sample → test_site}/content/tests/unpublished.txt
  37. +26 −0 test_site/hooks/__hooks__.py
  38. 0  {sample → test_site}/media/friendly.css
  39. +1 −1  {sample → test_site}/templates/base.html
  40. +1 −0  {sample → test_site}/templates/default.html
  41. 0  {sample → test_site}/templates/index.html
  42. 0  {sample → test_site}/templates/pagination.html
  43. +1 −1  wok/__init__.py
  44. 0  wok/contrib/__init__.py
  45. +55 −0 wok/contrib/hooks.py
  46. +77 −19 wok/engine.py
  47. +2 −0  wok/exceptions.py
  48. +156 −56 wok/page.py
  49. +14 −6 wok/tests/test_util.py
  50. +15 −1 wok/util.py
View
80 README.mkd
@@ -16,12 +16,12 @@ I wanted to do with my website. So I am writing my own. Funnily, the
mythical website that inspired wok still hasn't been written.
[jekyll]: https://github.com/mojombo/jekyll
-[hyde]: https://github.com/lakshmivyas/hyde
-[static]: http://static.newqdev.com/
+[hyde]: https://github.com/lakshmivyas/hyde
+[static]: http://static.newqdev.com/
Sample Sites
------------
-A bare bones site is included in the wok git repo, in the `sample` directory.
+A bare bones site is included in the wok git repo, in the `test` directory.
It is really just a playground for devs to test new features, and not a good
learning tool.
@@ -38,12 +38,20 @@ For some real world examples check out these sites.
- [uberj.com](http://www.uberj.com)
([source](https://github.com/uberj/wbsite)) - Personal website of Jacques
Uber
+- [ngokevin.com](http://ngokevin.com)
+ ([source](https://github.com/ngokevin/ngokevin)) - Personal website of
+ Kevin Ngo
+- [corbinsimpson.com](http://corbinsimpson.com)
+ ([source](https://github.com/mostawesomedude/website)) - Personal website
+ of Corbin Simpson
- Your site here! If you are using wok to generate sites, and don't mind
serving as an example, let me know and I will add a link to your site
here.
-For some tutorials, the [wok github wiki][wiki] has some good pointers.
+For some more documentation, checkout [the doc site][docs]. To learn and share
+with other users, you can check out [the wiki][wiki].
+[docs]: http://wok.mythmon.com
[wiki]: https://github.com/mythmon/wok/wiki
Installation
@@ -66,7 +74,9 @@ dependencies by hand in this case.
###Dependencies
All dependencies are available from pip. Although optional, you really should
-install either markdown or docutils.
+install either markdown or docutils, and if you install from pip, they will be
+installed for you. Pygments is used for syntax highlighting, and will also be
+installed from pip.
####Required
@@ -75,17 +85,16 @@ install either markdown or docutils.
####Optional
-- `markdown` - for rendering markdown documents.
+- `Markdown` - for rendering markdown documents.
- `docutils` - for rendering reStructuredText documents.
-- `pygments` - for syntax highlighting.
+- `Pygments` - for syntax highlighting.
Usage
-----
-To use wok, go to the directory where your site files are located, and
-run the command `wok`. For now, no output will be given unless something
-goes wrong. If it returns without error, you should have a shiny new
-output folder containing some HTML, and your media that represents your
-shiny new site.
+To use wok, go to the directory where your site files are located, and run the
+command `wok`. No output will be given unless something goes wrong. If it
+returns without error, you should have a shiny new output folder containing
+some HTML, and your media that represents your shiny new site.
To aid in testing links on the site, wok includes a development server.
You can run it with the command `wok --server`, which will generate the
@@ -117,9 +126,7 @@ syntax highlighting and media copying make things even easier.
[mkd]: http://daringfireball.net/projects/markdown/
[rst]: http://docutils.sourceforge.net/rst.html
-[More info][more_content]
-
-[more_content]: https://github.com/mythmon/wok/wiki/Content
+[More info](http://wok.mythmon.com/docs/content/)
### Templates ###
Pulled from `templates` by default. Wok uses [Jinja2][jinja] templates,
@@ -127,39 +134,9 @@ with various variables exposed to build pages. This is a very flexible
templating environment with control flow, filters, and other ways to
slice and dice the data that wok gives you.
-[More info][more_templates]
-
-[more_templates]: https://github.com/mythmon/wok/wiki/Templates
[jinja]: http://jinja.pocoo.org/
-### Pagination ###
-Pagination requires teamwork from both the templates and the content of a page.
-In the content, place a new item, `pagination`, with sub-items `limit` and
-`list`. Optionally you can also include `sort_key` and `sort_reverse`.
-
-Example
-
- title: Pagination Test
- pagination:
- list: page.subpages
- limit: 3
- sort_key: slug
- sort_reverse: True
- ---
- Let's test pagination.
-
-Then the template for this page will get a new variable `pagination` with
-useful things like `pagination.page_items` and `pagination.next_page`.
-
-[More info][more_pagination]
-
-[more_pagination]: https://github.com/mythmon/wok/wiki/Pagination
-
-### Media ###
-Media is pretty simple. It is intended to be used for things like site wide
-images, style sheets and JavaScript. Wok will copy everything from the media
-directory straight to the output directory before generating anything. This
-means that generated content can overwrite media.
+[More info](http://wok.mythmon.com/docs/templates/)
Configuration
-------------
@@ -179,9 +156,18 @@ Possible configuration options (and their defaults) are
place the output files. The default produces URLs like
`/category/subcategory/foo.html`. To get "wordpress style" urls, you could
use `/{category}/{slug}/index.html`.
-- `url_use_index` (Yes) - If true, keep `index.*` in urls.
Available variables:
- `{category}` - The category of the site, slash seperated.
- `{slug}` - The slug of the page.
+ - `{page}` - The current page.
+ - `{ext}` - The extension that the page should used.
+ - `{date}`, `{datetime}`, and `{time}` - The date/time from the metadata
+ of the page
+
+- `url_use_index` (Yes) - If true, keep `index.*` in urls.
+
+More info:
+[config](http://wok.mythmon.com/docs/config/),
+[urls](http://wok.mythmon.com/docs/urls/).
View
4 docs/README
@@ -1,2 +1,2 @@
-This is a wok site. To see it as rendered HTML, run wok and look in the output
-directory.
+This is a wok site. To see it as rendered HTML, run wok and look in the
+generated 'output' directory.
View
24 docs/content/community.mkd
@@ -4,19 +4,27 @@ nav_sort: 3
---
In the wild
-----------
-Wok powers an ever growing list of sites.
+Wok powers an ever-growing list of sites. If you want to see the power
+of what wok can do, or how to do something, these websites are a good
+resource:
-- This site ([source](https://github.com/mythmon/tree/master/docs))
+- This site ([source](https://github.com/mythmon/wok/tree/master/docs))
- [Oregon State University LUG](http://lug.oregonstate.edu)
([source](https://github.com/OSULUG/OSULUG-Website))
-- [robmd.net](http://robmd.net)
- ([source](https://github.com/robatron/robmd.net)) - Personal site of Rob
- McGuire-Dale.
- [Bravo Server](http://bravoserver.org)
([source](https://github.com/MostAwesomeDude/bravo/tree/master/website)) -
A custom Minecraft server written in Python.
+- [robmd.net](http://robmd.net)
+ ([source](https://github.com/robatron/robmd.net)) - Personal web site of
+ Rob McGuire-Dale.
- [uberj.com](http://uberj.com) ([source](https://github.com/uberj/wbsite)) -
- Personal website of Jacques Uber.
+ Personal web site of Jacques Uber.
+- [ngokevin.com](http://ngokevin.com)
+ ([source](https://github.com/ngokevin/ngokevin)) - Personal web site of
+ Kevin Ngo.
+- [corbinsimpson.com](http://corbinsimpson.com)
+ ([source](https://github.com/MostAwesomeDude/website)) - Personal web site
+ of Corbin Simpson.
Contributors
------------
@@ -27,7 +35,7 @@ Without these early adopters, wok would have lacked direction, definition, and
a purpose. Each of them helped me work through figuring out what wok could do,
needed do to, where it was broken, and what it should become.
-- Rob Mcguire-Dale (robatron)
+- Rob McGuire-Dale (robatron)
- Kevin Ngo (ngoke)
- Corbin Simpson (MostAwesomeDude)
- Jacque Uber (uberj)
@@ -37,7 +45,7 @@ Support
If there are bugs or feature requests for wok, please send them to [the issue
tracker][gh-issues].
-For real time support, you can check out the irc channel `#wok` on
+For real-time support, you can check out the irc channel `#wok` on
irc.freenode.net.
[gh-issues]: https://github.com/mythmon/wok/issues
View
22 docs/content/docs.mkd
@@ -6,30 +6,24 @@ type: index-links-recurse
Install
-------
-If you just want to use wok, then you should install it from the [Python Package
-Index][pypi]. A good way to do that is with `pip`:
+If you just want to use wok, then you should install it from the
+[Python Package Index][pypi]. A good way to do that is with `pip`:
::console
- sudo pip install wok
+ $ sudo pip install wok
-Which will install wok and the required dependencies. You should also install at least one of Markdown, or reStructuredText, for content markup.
-
- ::console
- sudo pip install Markdown
- sudo pip install docutils
-
-If you want Pygments syntax highlighting, you also need Pygments.
-
- ::console
- sudo pip install Pygments
+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][pymnts].
[pypi]: http://pypi.python.org/pypi
+[mkd]: http://daringfireball.net/projects/markdown/
+[rst]: http://docutils.sourceforge.net/rst.html
+[pgmnts]: http://pygments.org/
Contribute
----------
You can find wok's source code on [Github][gh]. If you find any issues,
please post them to [the issue tracker][gh-issues]. If you want to help
-code, feel free, patches and pull requests are welcome.
+code, feel free. Patches and pull requests are welcome.
[gh]: https://github.com/mythmon/wok
[gh-issues]: https://github.com/mythmon/wok/issues
View
168 docs/content/docs/hooks.mkd
@@ -0,0 +1,168 @@
+title: Hooks
+category: docs
+---
+Since version 0.9, wok provides a hook interface to add custom python code to
+the site generation pipeline.
+
+To use hooks, make a new directory in your site root named `hooks`. In this
+directory, make the file `__hooks__.py`. For example:
+
+ ::text
+ site-root/
+ |-- config
+ |-- content/
+ |-- hooks/
+ | |-- __hooks__.py
+ | |-- hook_foo.py
+ | `-- hook_bar.py
+ |
+ |-- media/
+ |-- output/
+ `-- templates/
+
+
+From the `__hooks__.py` file, wok will import the variable `hooks`.
+This should be a dictionary; The keys are the hook names below, and the values
+are lists of functions (or other callables) to run for the hooks. Simply give
+the names of the functions and (but do not invoke them.) For example:
+
+ ::python
+ ''' __hooks__.py
+
+ Attach Python functions to wok hooks.
+ '''
+
+ import hook_foo
+ import hook_bar
+
+ # The `hooks` dictionary that wok will import
+ hooks = {
+ 'site.start': [hook_foo.download_images],
+ 'site.content.gather.post': [hook_bar.process_pages]
+ }
+
+If the hooks below have parameters, the listed functions should accept
+those parameters. Hooks will be run in the order they appear below.
+
+Available hooks
+---------------
+Below are the available hooks, when they will be run, and the arguments they
+will pass to the hooked functions (if any.)
+
+###List of hooks
+
+- [site.start](#site.start)
+- [site.output.pre](#site.output.pre)
+- [site.output.post](#site.output.post)
+- [site.content.gather.pre](#site.content.gather.pre)
+- [site.content.gather.post](#site.content.gather.post)
+- [page.render.pre](#page.render.pre)
+- [page.render.post](#page.render.post)
+- [page.meta.pre](#page.meta.pre)
+- [page.meta.post](#page.meta.post)
+- [page.template.pre](#page.template.pre)
+- [page.template.post](#page.template.post)
+- [site.done](#site.done)
+
+### Details
+
+<!-- This first one needs to be wrapped in a p tag, because it doesn't happen automatically for some reason...? -->
+
+`site.start()` <a name="site.start"> </a>
+: <p>Called before anything else has started, except for the loading of hooks.
+ This would be a good time to modify the content, templates, or the files in
+ the media directory.</p>
+
+`site.output.pre(output_path)` <a name="site.output.pre"> </a>
+: `output_path`
+ : The path to the output directory.
+: This path will run before the output directory is populated by the media,
+ and after any existing output files have been deleted. You can add files
+ that may be overwritten by the media files or the site content.
+
+`site.output.post(output_path)` <a name="site.output.post"> </a>
+: `output_path`
+ : The path to the output directory.
+: This hook will run after the output directory is populated by the media,
+ and before the content pages have started to be processed. You can use this
+ to modify, overwrite, or otherwise fiddle with the media directory after it
+ has been copied to the output directory.
+
+`site.content.gather.pre()` <a name="site.content.gather.pre"> </a>
+: Return value
+ : List of `Page` objects to add to the list of pages.
+: This hook will run before wok gathers content pages, and can be used to add
+ pages that don't exist on disk.
+
+`site.content.gather.post(pages)` <a name="site.content.gather.post"> </a>
+: `pages`
+ : The list of pages that wok has gathered from the content directory, and
+ any other hooks that have run. Also includes the duplicated versions of
+ paginated pages.
+: Return value
+ : List of `Page` objects to add to the list of pages.
+: This hook will run after wok gathers content pages, and can be used to add
+ pages that don't exist on disk, or to remove pages added by other means. If
+ you modify the list of pages received, those changes will take effect in
+ wok. You may also return pages to add.
+
+`page.render.pre(page)` <a name="page.render.pre"> </a>
+: `page`
+ : The current page object that is being processed.
+: This hook will be called for each page before the page is rendered by
+ Markdown, reStructuredText, etc. The unrendered text will be in the
+ variable `page.original`, if there is an original text. Keep in mind that
+ some pages won't be run through this hook because they come from other
+ sources, such as hooks, or pagination.
+
+`page.render.post(page)` <a name="page.render.post"> </a>
+: `page`
+ : The current page object that is being processed.
+: This hook will be called for each page right after the page is rendered by
+ Markdown, reStructuredText, etc. The unrendered text will be in the
+ variable `page.original`, and the rendered text will be in the meta
+ variable `page.meta['content']. Keep in mind that some pages won't be run
+ through this hook because they come from other sources, such as hooks, or
+ pagination.
+
+`page.meta.pre(page)` <a name="page.render.pre"> </a>
+: `page`
+ : The current page object that is being processed.
+: This hook will be called for each page before the page has it's meta data
+ filled in. Some metadata will exist, but it will be in an unnormalized
+ state.
+
+`page.meta.post(page)` <a name="page.render.post"> </a>
+: `page`
+ : The current page object that is being processed.
+: This hook will be called for each page right after the page has it's meta
+ data filled in and normalized.
+
+`page.template.pre(page, templ_vars)` <a name="page.template.pre"> </a>
+: `page`
+ : The current page object that is being processed.
+: `templ_vars`
+ : A dictionary of the variables that will be sent to the template engine.
+ See the documentation on [templates][] for its normal contents.
+: This hook will be called for each page before the page is sent to
+ the template engine. At this point, the content has been transformed
+ from markup input (such as markdown) to html output, if applicable.
+ The transformed version is in `templ_vars['content']`. If you
+ modify the `templ_vars` parameter, those changes will be visible to
+ the template engine. The given functions should take in these two
+ variables.
+
+[templates]: /docs/templates/
+
+`page.template.post(page)` <a name="page.template.postpage"> </a>
+: `page`
+ : The current page being processed.
+: This hook will be called after the page has been processed by the template
+ engine. The next step will be write the file to disk, if applicable, so any
+ last minute changes should happen here.
+
+`site.done()` <a name="site.done"> </a>
+: Called after wok has finished everything. This would be a good time to make
+ any general modifications to the output, or to do something like upload the
+ site for distribution. If the `--server` option has been specified, this
+ will happen before the server is run.
View
22 docs/content/docs/urls.mkd
@@ -23,17 +23,27 @@ arrange to your liking:
file.
- `{date}`, `{datetime}`, `{time}` - The date/time that were specified in the
metadata of a page. These are Python date, datetime, and time objects, so
- they have year, month, day, hour, minute, etc fields. If you don't specify a
- date in the file, the default is midnight on January 1st, 1970.
+ they have year, month, day, hour, minute, etc fields. If you don't specify
+ these, they will default to `None`.
If you don't include `{page}` in your `url_pattern`, then pagination won't
work. Instead it will overwrite each page with it's sequel, resulting in only
-the last page remaining.
+the last page remaining. If you aren't using any pagination, then you don't
+need the variable.
Any time two or more forward slashes are generated, they will be replaced by a
-single forward slash. If you set the option `url_include_index` to `false`,
-then any time the url ends with `index.*`, that will also be removed from the
-url patterns. The files will still be named `index.*`, however.
+single forward slash. This lets you do things like
+`/{category}/{page}/{slug}.html` without worrying about an empty page or empty
+category causing bad paths.
+
+If you set the option `url_include_index` to `false` in the config file, then
+any time the url ends with `index.*`, that will also be removed from the url
+patterns. The files will still be named `index.*`, however. So for example if
+your url pattern is `/{category}/{slug}/index.html`, the a blog post about
+balloons would create a file named `/blog/balloons/index.html`, but anytime you
+reference it in a template it's `url` field will be `/blog/balloons/`. This
+makes URLs look a lot cleaner. This option off set to `true` by default (ie:
+keep index in urls).
### Examples:
View
48 docs/content/download.mkd
@@ -0,0 +1,48 @@
+title: Download
+tags: [_nav]
+nav_sort: 4
+---
+
+The recommended way to get wok is from the [Python Package Index][pypi]. You
+can do that with `pip`:
+
+ ::console
+ $ sudo pip install wok
+
+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][].
+
+[pypi]: http://pypi.python.org/pypi
+[mkd]: http://daringfireball.net/projects/markdown/
+[rst]: http://docutils.sourceforge.net/rst.html
+[pygments]: http://pygments.org/
+
+Other Methods
+=============
+
+You can also checkout out the code [from Github][gh], or download a tarball:
+
+Latest Stable
+-------------
+
+- [Version 0.9](https://github.com/mythmon/wok/tarball/v0.9)
+
+Older Versions
+--------------
+
+- [Version 0.8.2](https://github.com/mythmon/wok/tarball/v0.8.2)
+- [Version 0.8.1](https://github.com/mythmon/wok/tarball/v0.8.1)
+- [Version 0.8](https://github.com/mythmon/wok/tarball/v0.8)
+- [Version 0.7](https://github.com/mythmon/wok/tarball/v0.7)
+- [Version 0.6.3](https://github.com/mythmon/wok/tarball/v0.6.3)
+- [Version 0.6.2](https://github.com/mythmon/wok/tarball/v0.6.2)
+- [Version 0.6.1](https://github.com/mythmon/wok/tarball/v0.6.1)
+- [Version 0.6](https://github.com/mythmon/wok/tarball/v0.6)
+- [Version 0.5.1](https://github.com/mythmon/wok/tarball/v0.5.1)
+- [Version 0.5](https://github.com/mythmon/wok/tarball/v0.5)
+- [Version 0.4](https://github.com/mythmon/wok/tarball/v0.4)
+- [Version 0.3](https://github.com/mythmon/wok/tarball/v0.3)
+- [Version 0.2.1](https://github.com/mythmon/wok/tarball/v0.2.1)
+- [Version 0.2](https://github.com/mythmon/wok/tarball/v0.2)
+- [Version 0.1](https://github.com/mythmon/wok/tarball/v0.1)
+
+[gh]: https://github.com/mythmon/wok
View
33 docs/content/home.mkd
@@ -8,12 +8,12 @@ content, and resources (like CSS and images) into a neat stack of plain
HTML. You run it on your local computer, and it generates a directory of
web files that you can upload to your web server, or serve directly.
-The idea is that you don't need a big server-side engine like Drupal or
-Django to generate every page, every visit, for every visitor. Caching
-helps, but it can only do so much. Instead, you can generate the entire
-site once ahead of time, and only regenerate things when something has
-changed. A good way this could be done would be with a post-commit hook
-on a git repository containing your content or layout.
+The idea is that you don't need a big server-side engine like PHP, [Drupal][],
+[Django][], etc. to generate every page, every visit, for every visitor.
+Caching helps, but it can only do so much. Instead, you can generate the entire
+site once ahead of time, and only regenerate things when something has changed,
+e.g, with a post-commit hook on a Git repository containing your content or
+layout.
Wok is similar in concept to other static site generators, like
[Jekyll][], [Hyde][], and [nanoc][], but different in a few ways, like
@@ -21,6 +21,8 @@ a page/subpage category system, and a restricted focus of features. For
example, instead of providing tools to generate RSS feeds directly, the
tools that are needed to build a generic XML document are provided.
+[drupal]: http://drupal.org
+[django]: http://djangoproject.com
[jekyll]: https://github.com/mojombo/jekyll
[hyde]: https://github.com/lakshmivyas/hyde
[nanoc]: http://nanoc.stoneship.org/
@@ -28,17 +30,14 @@ tools that are needed to build a generic XML document are provided.
Features
--------
-- Content and presentation are separated.
-- [Markdown][mkd], [reStructuredText][rst], and plain text renders.
-- [Jinja2][] based templating system.
-- Optional: Syntax highlighting via [Pygments][]
-- Tagging and a hierarchical category system.
-- Simple development server, for testing.
-- Support for pagination.
-
-And more to come. Some features that are cooking in the devs' heads are
-custom python plugins, once per page hooks, and support for more markup
-languages.
+- Content and presentation are separated
+- [Markdown][mkd], [reStructuredText][rst], and plain text renders
+- [Jinja2][]-based templating system
+- Optional syntax highlighting via [Pygments][]
+- Tagging and a hierarchical category system
+- Simple development server
+- Pagination support
+- Custom Python hooks that run during site generation
[jinja2]: http://jinja.pocoo.org
[mkd]: http://daringfireball.net/projects/markdown/
View
5 docs/hooks/__hooks__.py
@@ -0,0 +1,5 @@
+from wok.contrib.hooks import HeadingAnchors
+
+hooks = {
+ 'page.template.post': [ HeadingAnchors() ],
+}
View
0  docs/hooks/__init__.py
No changes.
View
20 docs/media/css/base.css
@@ -83,6 +83,26 @@ a:hover {
color: #322;
margin: 5px 0 10px;
}
+h1 a.heading_anchor, h2 a.heading_anchor, h3 a.heading_anchor
+h4 a.heading_anchor, h5 a.heading_anchor, h6 a.heading_anchor {
+ display: none;
+ color: #A60000;
+}
+h1:hover a.heading_anchor, h2:hover a.heading_anchor,
+h3:hover a.heading_anchor, h4:hover a.heading_anchor,
+h5:hover a.heading_anchor, h6:hover a.heading_anchor {
+ padding: 2px;
+ border: 3px;
+ display: inline;
+ font-weight: normal;
+}
+h1 a.heading_anchor:hover, h2 a.heading_anchor:hover,
+h3 a.heading_anchor:hover, h4 a.heading_anchor:hover,
+h5 a.heading_anchor:hover, h6 a.heading_anchor:hover {
+ color: #FFF;
+ background-color: #A60000;
+}
+
p {
margin: 0 0 20px 0;
}
View
8 docs/templates/base.html
@@ -36,7 +36,7 @@
<div id="content">
<span class="version_note">
- This is the 0.8 version of the docs. You could also see the
+ This is the 0.9 version of the docs. You could also see the
<a href="http://dev.wok.mythmon.com{{ page.url }}">
dev version of this page.
</a>
@@ -49,7 +49,11 @@
<footer>
{{ site.slugs.footer.content|safe }}
<p>Last generated: {{ site.datetime.strftime('%c') }}.</p>
- </footer> </div>
+ </footer>
+ </div>
+
+ <script type="text/javascript" src="//www.hellobar.com/hellobar.js"></script>
+ <script type="text/javascript">new HelloBar(1,47972);</script>
</body>
</html>
View
34 setup.py
@@ -4,13 +4,27 @@
from wok import version
-setup(name='wok',
- version=version.encode("utf8"),
- description='Static site generator',
- install_requires=['pyyaml', 'jinja2'],
- author='Mike Cooper',
- author_email='mythmon@gmail.com',
- url='https://www.github.com/mythmon/wok',
- packages=['wok'],
- scripts=['scripts/wok'],
- )
+setup(
+ name='wok',
+ version=version.encode("utf8"),
+ author='Mike Cooper',
+ author_email='mythmon@gmail.com',
+ url='http://wok.mythmon.com',
+ description='Static site generator',
+ long_description=
+ "Wok is a static website generator. It turns a pile of templates, "
+ "content, and resources (like CSS and images) into a neat stack of "
+ "plain HTML. You run it on your local computer, and it generates a "
+ "directory of web files that you can upload to your web server, or "
+ "serve directly.",
+ download_url="http://wok.mythmon.com/download",
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "License :: OSI Approved :: MIT License",
+ 'Operating System :: POSIX',
+ 'Programming Language :: Python',
+ ],
+ requires=['pyyaml', 'jinja2', 'Markdown', 'docutils', 'Pygments'],
+ packages=['wok'],
+ scripts=['scripts/wok'],
+)
View
2  sample/config → test_site/config
@@ -1,4 +1,4 @@
site_title: Wok Scratch Site
author: Default
-url_pattern: "/{category}/{slug}{page}/index.{type}"
+url_pattern: "/{category}/{slug}{page}/index.{ext}"
url_include_index: no
View
1  sample/content/pagination-bits/a.mkd → test_site/content/pagination-bits/a.mkd
@@ -1,3 +1,4 @@
+title: A
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/b.mkd → test_site/content/pagination-bits/b.mkd
@@ -1,3 +1,4 @@
+title: B
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/c.mkd → test_site/content/pagination-bits/c.mkd
@@ -1,3 +1,4 @@
+title: C
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/d.mkd → test_site/content/pagination-bits/d.mkd
@@ -1,3 +1,4 @@
+title: D
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/e.mkd → test_site/content/pagination-bits/e.mkd
@@ -1,3 +1,4 @@
+title: E
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/f.mkd → test_site/content/pagination-bits/f.mkd
@@ -1,3 +1,4 @@
+title: F
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/g.mkd → test_site/content/pagination-bits/g.mkd
@@ -1,3 +1,4 @@
+title: G
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/h.mkd → test_site/content/pagination-bits/h.mkd
@@ -1,3 +1,4 @@
+title: H
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/i.mkd → test_site/content/pagination-bits/i.mkd
@@ -1,3 +1,4 @@
+title: I
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/j.mkd → test_site/content/pagination-bits/j.mkd
@@ -1,3 +1,4 @@
+title: J
category: pagination
tags: tiny
---
View
1  sample/content/pagination-bits/k.mkd → test_site/content/pagination-bits/k.mkd
@@ -1,3 +1,4 @@
+title: K
category: pagination
tags: tiny
---
View
0  sample/content/pagination-test.mkd → test_site/content/pagination-test.mkd
File renamed without changes
View
2  sample/content/tests.mkd → test_site/content/tests.mkd
@@ -5,4 +5,4 @@ url: /index{page}.html
---
These are the tests
-The pagination test is [here](/pagination.html).
+The pagination test is [here](/pagination/index.html).
View
5 test_site/content/tests/dates.mkd
@@ -0,0 +1,5 @@
+title: Dates
+type: index
+category: tests
+url: "/{category}/{slug}{page}/{date.year}/{date.month}/{date.day}/index.{ext}"
+---
View
6 test_site/content/tests/dates1.mkd
@@ -0,0 +1,6 @@
+title: Datetime only
+datetime: 2011-10-12 12:20:00
+category: tests/dates
+url: "/{category}/{slug}{page}/{date.year}-{date.month}-{date.day}-{time.hour}-{datetime.minute}/index.{ext}"
+---
+This only has a datetime
View
0  sample/content/tests/dates2.mkd → test_site/content/tests/dates2.mkd
File renamed without changes
View
0  sample/content/tests/dates3.mkd → test_site/content/tests/dates3.mkd
File renamed without changes
View
0  sample/content/tests/markdown.mkd → test_site/content/tests/markdown.mkd
File renamed without changes
View
0  sample/content/tests/plain.txt → test_site/content/tests/plain.txt
File renamed without changes
View
0  sample/content/tests/rest_titles.rst → test_site/content/tests/rest_titles.rst
File renamed without changes
View
0  sample/content/tests/restructuredtext.rst → test_site/content/tests/restructuredtext.rst
File renamed without changes
View
0  sample/content/tests/unpublished.txt → test_site/content/tests/unpublished.txt
File renamed without changes
View
26 test_site/hooks/__hooks__.py
@@ -0,0 +1,26 @@
+import logging
+
+hook_count = 0
+def make_hook(name):
+ def logging_hook(*args):
+ global hook_count
+ logging.info('logging_hook: {0}: {1}'.format(name, hook_count))
+ hook_count += 1
+ return [logging_hook]
+
+hooks = {
+ 'site.start': make_hook('site.start'),
+ 'site.output.pre': make_hook('site.output.pre'),
+ 'site.output.post': make_hook('site.output.post'),
+ 'site.content.gather.pre': make_hook('site.content.gather.pre'),
+ 'site.content.gather.post': make_hook('site.content.gather.post'),
+ 'page.meta.pre': make_hook('page.template.pre'),
+ 'page.meta.post': make_hook('page.template.post'),
+ 'page.render.pre': make_hook('page.template.pre'),
+ 'page.render.post': make_hook('page.template.post'),
+ 'page.template.pre': make_hook('page.template.pre'),
+ 'page.template.post': make_hook('page.template.post'),
+ 'site.stop': make_hook('site.stop'),
+}
+
+logging.info('loaded hooks.')
View
0  sample/media/friendly.css → test_site/media/friendly.css
File renamed without changes
View
2  sample/templates/base.html → test_site/templates/base.html
@@ -11,7 +11,7 @@
<hr><hr>
Footer
Site by: {{site.author}}
- Last generated: {{site.datetime}}
+ Last generated: Datetime: {{site.datetime}}, Date: {{site.date}}, Time: {{site.time}}
</body>
</html>
View
1  sample/templates/default.html → test_site/templates/default.html
@@ -11,5 +11,6 @@
<li>{{ tag }}</li>
{% endfor %}
</ul>
+ Hooked: {{ hooked }}
{{ page.content }}
{% endblock body %}
View
0  sample/templates/index.html → test_site/templates/index.html
File renamed without changes
View
0  sample/templates/pagination.html → test_site/templates/pagination.html
File renamed without changes
View
2  wok/__init__.py
@@ -1 +1 @@
-version = u'0.8.2'
+version = u'0.9.0'
View
0  wok/contrib/__init__.py
No changes.
View
55 wok/contrib/hooks.py
@@ -0,0 +1,55 @@
+# vim: set fileencoding=utf8 :
+"""Some hooks that might be useful."""
+
+from StringIO import StringIO
+import logging
+
+from wok.exceptions import DependencyException
+from wok.util import slugify
+
+try:
+ from lxml import etree
+except ImportError:
+ etree = None
+
+
+class HeadingAnchors(object):
+ """
+ Put some paragraph heading anchors.
+
+ Serves as a 'page.template.post' wok hook.
+ """
+
+ def __init__(self, max_heading=3):
+ if not etree:
+ raise DependencyException('To use the HeadingAnchors hook, you must '
+ 'install the library lxml.')
+ self.max_heading = max_heading
+ logging.debug('Loaded hook HeadingAnchors')
+
+ def __call__(self, page):
+ logging.debug('Called hook HeadingAnchors on {0}'.format(page))
+ parser = etree.HTMLParser()
+ sio_source = StringIO(page.rendered)
+ tree = etree.parse(sio_source, parser)
+
+ for lvl in range(1, self.max_heading+1):
+ headings = tree.iterfind('//h{0}'.format(lvl))
+ for heading in headings:
+ if not heading.text:
+ continue
+ logging.debug('[HeadingAnchors] {0} {1}'.format(heading, heading.text))
+
+ name = 'heading-{0}'.format(slugify(heading.text))
+ anchor = etree.Element('a')
+ anchor.set('class', 'heading_anchor')
+ anchor.set('href', '#' + name)
+ anchor.set('title', 'Permalink to this section.')
+ anchor.text = u''
+ heading.append(anchor)
+
+ heading.set('id', name)
+
+ sio_destination = StringIO()
+ tree.write(sio_destination)
+ page.rendered = sio_destination.getvalue()
View
96 wok/engine.py
@@ -9,11 +9,12 @@
import yaml
import wok
-from wok import page
+from wok.page import Page, Author
from wok import renderers
from wok import util
from wok import devserver
+
class Engine(object):
"""
The main engine of wok. Upon initialization, it generates a site from the
@@ -29,7 +30,11 @@ class Engine(object):
'url_include_index': True,
}
- def __init__(self, output_lvl = 1):
+ def __init__(self, output_lvl=1):
+ """
+ Set up CLI options, logging levels, and start everything off.
+ Afterwards, run a dev server if asked to.
+ """
# CLI options
# -----------
@@ -37,22 +42,22 @@ def __init__(self, output_lvl = 1):
# Add option to to run the development server after generating pages
devserver_grp = OptionGroup(parser, "Development server",
- "Runs a small development server after site generation. \
- --address and --port will be ignored if --server is absent.")
+ "Runs a small development server after site generation. "
+ "--address and --port will be ignored if --server is absent.")
devserver_grp.add_option('--server', action='store_true',
dest='runserver',
help="run a development server after generating the site")
devserver_grp.add_option('--address', action='store', dest='address',
help="specify ADDRESS on which to run development server")
devserver_grp.add_option('--port', action='store', dest='port',
- type='int',
+ type='int',
help="specify PORT on which to run development server")
parser.add_option_group(devserver_grp)
# Options for noisiness level and logging
logging_grp = OptionGroup(parser, "Logging",
- "By default, log messages will be sent to standard out, \
- and report only errors and warnings.")
+ "By default, log messages will be sent to standard out, "
+ "and report only errors and warnings.")
parser.set_defaults(loglevel=logging.WARNING)
logging_grp.add_option('-q', '--quiet', action='store_const',
const=logging.ERROR, dest='loglevel',
@@ -91,11 +96,17 @@ def __init__(self, output_lvl = 1):
self.read_options()
self.sanity_check()
+ self.load_hooks()
+
+ self.run_hook('site.start')
+
self.prepare_output()
self.load_pages()
self.make_tree()
self.render_site()
+ self.run_hook('site.done')
+
# Dev server
# ----------
# Run the dev server after generating pages if the user said to
@@ -117,9 +128,10 @@ def read_options(self):
# Make authors a list, even only a single author was specified.
authors = self.options.get('authors', self.options.get('author', None))
if isinstance(authors, list):
- self.options['authors'] = [page.Author.parse(a) for a in authors]
+ self.options['authors'] = [Author.parse(a) for a in authors]
elif isinstance(authors, str):
- self.options['authors'] = [page.Author.parse(a) for a in authors.split(',')]
+ csv = authors.split(',')
+ self.options['authors'] = [Author.parse(a) for a in csv]
if len(self.options['authors']) > 1:
logging.warn('Deprecation Warning: Use YAML lists instead of '
'CSV for multiple authors. i.e. ["John Doe", "Jane '
@@ -138,6 +150,26 @@ def sanity_check(self):
logging.critical("This doesn't look like a wok site. Aborting.")
sys.exit(1)
+ def load_hooks(self):
+ try:
+ sys.path.append('hooks')
+ import __hooks__
+ self.hooks = __hooks__.hooks
+ logging.info('Loaded {0} hooks: {0}'.format(self.hooks))
+ except ImportError:
+ logging.info('No hooks module found.')
+
+ def run_hook(self, hook_name, *args):
+ """ Run specified hooks if they exist """
+ logging.debug('Running hook {0}'.format(hook_name))
+ returns = []
+ try:
+ for hook in self.hooks.get(hook_name, []):
+ returns.append(hook(*args))
+ except AttributeError:
+ logging.info('Hook {0} not defined'.format(hook_name))
+ return returns
+
def prepare_output(self):
"""
Prepare the output directory. Remove any contents there already, and
@@ -153,6 +185,8 @@ def prepare_output(self):
else:
os.mkdir(self.options['output_dir'])
+ self.run_hook('site.output.pre', self.options['output_dir'])
+
# Copy the media directory to the output folder
try:
for name in os.listdir(self.options['media_dir']):
@@ -160,19 +194,28 @@ def prepare_output(self):
if os.path.isdir(path):
shutil.copytree(
path,
- os.path.join(self.options['output_dir'], name),
+ os.path.join(self.options['output_dir'], name),
symlinks=True
)
else:
shutil.copy(path, self.options['output_dir'])
+ self.run_hook('site.output.post', self.options['output_dir'])
+
# Do nothing if the media directory doesn't exist
except OSError:
# XXX: We should verify that the problem was the media dir
- pass
+ logging.info('There was a problem copying the media files to the '
+ 'output directory.')
def load_pages(self):
"""Load all the content files."""
+ # Load pages from hooks (pre)
+ for pages in self.run_hook('site.content.gather.pre'):
+ if pages:
+ self.all_pages.extend(pages)
+
+ # Load files
for root, dirs, files in os.walk(self.options['content_dir']):
# Grab all the parsable files
for f in files:
@@ -192,16 +235,21 @@ def load_pages(self):
'for {0}. Using default renderer.'.format(f))
renderer = renderers.Renderer
- p = page.Page(os.path.join(root, f), self.options, renderer)
- if p.meta['published']:
+ p = Page.from_file(os.path.join(root, f), self.options, self, renderer)
+ if p and p.meta['published']:
self.all_pages.append(p)
+ # Load pages from hooks (post)
+ for pages in self.run_hook('site.content.gather.post', self.all_pages):
+ if pages:
+ self.all_pages.extend(pages)
+
def make_tree(self):
"""
- Make the category pseduo-tree.
+ Make the category pseudo-tree.
In this structure, each node is a page. Pages with sub pages are
- interior nodes, and leaf nodes have no sub pages. It is not truely a
+ interior nodes, and leaf nodes have no sub pages. It is not truly a
tree, because the root node doesn't exist.
"""
self.categories = {}
@@ -209,7 +257,9 @@ def make_tree(self):
# We want to parse these in a approximately breadth first order
self.all_pages.sort(key=lambda p: len(p.meta['category']))
- for p in [p for p in self.all_pages]:
+ # For every page
+ for p in self.all_pages:
+ # If it has a category (ie: is not at top level)
if len(p.meta['category']) > 0:
top_cat = p.meta['category'][0]
if not top_cat in self.categories:
@@ -218,11 +268,12 @@ def make_tree(self):
self.categories[top_cat].append(p.meta)
try:
+ # Put this page's meta in the right place in site_tree.
siblings = site_tree
for cat in p.meta['category']:
# This line will fail if the page is an orphan
parent = [subpage for subpage in siblings
- if subpage['slug']== cat][0]
+ if subpage['slug'] == cat][0]
siblings = parent['subpages']
siblings.append(p.meta)
except IndexError:
@@ -237,8 +288,12 @@ def render_site(self):
tag_set = tag_set.union(p.meta['tags'])
tag_dict = dict()
for tag in tag_set:
- tag_dict[tag] = [p.meta for p in self.all_pages if tag in p.meta['tags']]
+ # Add all pages with the current tag to the tag dict
+ tag_dict[tag] = [p.meta for p in self.all_pages
+ if tag in p.meta['tags']]
+ # Gather slugs
+ slug_dict = dict((p.meta['slug'], p.meta) for p in self.all_pages)
for p in self.all_pages:
# Construct this every time, to avoid sharing one instance
@@ -247,10 +302,12 @@ def render_site(self):
'site': {
'title': self.options.get('site_title', 'Untitled'),
'datetime': datetime.now(),
+ 'date': datetime.now().date(),
+ 'time': datetime.now().time(),
'tags': tag_dict,
'pages': self.all_pages[:],
'categories': self.categories,
- 'slugs': dict((p.meta['slug'], p.meta) for p in self.all_pages),
+ 'slugs': slug_dict,
},
}
@@ -265,6 +322,7 @@ def render_site(self):
# Rendering the page might give us back more pages to render.
new_pages = p.render(templ_vars)
+
if p.meta['make_file']:
p.write()
View
2  wok/exceptions.py
@@ -0,0 +1,2 @@
+class DependencyException(Exception):
+ pass
View
212 wok/page.py
@@ -4,6 +4,7 @@
from collections import namedtuple
from datetime import datetime, date, time
import logging
+import copy
# Libraries
import jinja2
@@ -23,61 +24,94 @@ class Page(object):
tmpl_env = None
- def __init__(self, path, options, renderer=None, extra_meta=None):
+ def __init__(self, options, engine):
+ self.options = options
+ self.filename = None
+ self.meta = {}
+ self.engine = engine
+
+ @classmethod
+ def from_meta(cls, meta, options, engine, renderer=renderers.Plain):
+ """
+ Build a page object from a meta dictionary.
+
+ Note that you still need to call `render` and `write` to do anything
+ interesting.
+ """
+ page = cls(options, engine)
+ page.meta = meta
+ page.options = options
+ page.renderer = renderer
+
+ if 'pagination' in meta:
+ logging.debug('from_meta: current page %d' %
+ meta['pagination']['cur_page'])
+
+ # Make a template environment. Hopefully no one expects this to ever
+ # change after it is instantiated.
+ if Page.tmpl_env is None:
+ Page.tmpl_env = jinja2.Environment(loader=GlobFileLoader(
+ page.options.get('template_dir', 'templates')))
+
+ page.build_meta()
+ return page
+
+ @classmethod
+ def from_file(cls, path, options, engine, renderer=renderers.Plain):
"""
Load a file from disk, and parse the metadata from it.
Note that you still need to call `render` and `write` to do anything
interesting.
"""
- self.header = None
- self.original = None
- self.parsed = None
- self.options = options
- self.renderer = renderer if renderer else renderers.Plain
+ page = cls(options, engine)
+ page.original = None
+ page.options = options
+ page.renderer = renderer
logging.info('Loading {0}'.format(os.path.basename(path)))
if Page.tmpl_env is None:
Page.tmpl_env = jinja2.Environment(loader=GlobFileLoader(
- self.options.get('template_dir', 'templates')))
+ page.options.get('template_dir', 'templates')))
- self.path = path
- _, self.filename = os.path.split(path)
+ page.path = path
+ page.filename = os.path.basename(path)
with open(path) as f:
- self.original = f.read()
- splits = self.original.split('\n---\n')
+ page.original = f.read()
+ splits = page.original.split('\n---\n')
if len(splits) > 3:
logging.warning('Found more --- delimited sections in {0} '
'than expected. Squashing the extra together.'
- .format(self.path))
+ .format(page.path))
# Handle the case where no meta data was provided
if len(splits) == 1:
- self.original = splits[0]
- self.meta = {}
+ page.original = splits[0]
+ page.meta = {}
elif len(splits) == 2:
header = splits[0]
- self.meta = yaml.load(header)
- self.original = splits[1]
+ page.meta = yaml.load(header)
+ page.original = splits[1]
- elif len(splits) == 3:
+ elif len(splits) >= 3:
header = splits[0]
- self.meta = {}
- self.original = '\n'.join(splits[1:])
- self.meta['preview'] = splits[1]
- self.meta.update(yaml.load(header))
+ page.meta = {}
+ page.original = '\n'.join(splits[1:])
+ page.meta['preview'] = splits[1]
+ page.meta.update(yaml.load(header))
logging.debug('Got preview')
- if extra_meta:
- logging.debug('Got extra_meta')
- self.meta.update(extra_meta)
+ page.build_meta()
+
+ page.engine.run_hook('page.render.pre', page)
+ page.meta['content'] = page.renderer.render(page.original)
+ page.engine.run_hook('page.render.post', page)
- self.build_meta()
- self.meta['content'] = self.renderer.render(self.original)
+ return page
def build_meta(self):
"""
@@ -97,25 +131,49 @@ def build_meta(self):
`page.subpages` - Will be a list containing every sub page of this page
"""
+ self.engine.run_hook('page.meta.pre', self)
+
if not self.meta:
self.meta = {}
# title
if not 'title' in self.meta:
- self.meta['title'] = '.'.join(self.filename.split('.')[:-1])
- if (self.meta['title'] == ''):
- self.meta['title'] = self.filename
-
- logging.info("You didn't specify a title in {0}. "
- "Using the file name as a title.".format(self.filename))
+ if self.filename:
+ # Take off the last file extension.
+ self.meta['title'] = '.'.join(self.filename.split('.')[:-1])
+ if (self.meta['title'] == ''):
+ self.meta['title'] = self.filename
+
+ logging.warning("You didn't specify a title in {0}. Using the "
+ "file name as a title.".format(self.path))
+ elif 'slug' in self.meta:
+ self.meta['title'] = self.meta['slug']
+ logging.warning("You didn't specify a title in {0}, which was "
+ "not generated from a file. Using the slug as a title."
+ .format(self.meta['slug']))
+ else:
+ logging.error("A page was generated that is not from a file, "
+ "has no title, and no slug. I don't know what to do. "
+ "Not using this page.")
+ logging.info("Bad Meta's keys: {0}".format(self.meta.keys()))
+ logging.debug("Bad Meta: {0}".format(self.meta))
+ raise BadMetaException()
# slug
if not 'slug' in self.meta:
- self.meta['slug'] = util.slugify(self.meta['title'])
- logging.debug("You didn't specify a slug, generating it from the title.")
+ if self.filename:
+ filename_no_ext = '.'.join(self.filename.split('.')[:-1])
+ self.meta['slug'] = util.slugify(filename_no_ext)
+ logging.info("You didn't specify a slug, generating it from the "
+ "filename.")
+ else:
+ self.meta['slug'] = util.slugify(self.meta['title'])
+ logging.info("You didn't specify a slug, and no filename "
+ "exists. Generating the slug from the title.")
+
elif self.meta['slug'] != util.slugify(self.meta['slug']):
logging.warning('Your slug should probably be all lower case, and '
- 'match "[a-z0-9-]*"')
+ 'match "[a-z0-9-]*"')
# authors and author
authors = self.meta.get('authors', self.meta.get('author', None))
@@ -130,24 +188,31 @@ def build_meta(self):
'{0}.'.format(self.path))
elif authors is None:
- if 'authors' in self.options:
- self.meta['authors'] = self.options['authors']
- else:
- self.meta['authors'] = []
+ self.meta['authors'] = self.options.get('authors', [])
else:
- # wait, what?
+ # wait, what? Authors is of wrong type.
self.meta['authors'] = []
logging.error(('Authors in {0} is an unknown type. Valid types '
- 'are string or list.').format(self.path))
+ 'are string or list. Instead it is a {1}')
+ .format(self.meta['slug']), authors.type)
if self.meta['authors']:
- self.meta['author'] = self.meta['authors']
+ self.meta['author'] = self.meta['authors'][0]
else:
self.meta['author'] = Author()
# category
if 'category' in self.meta:
- self.meta['category'] = self.meta['category'].split('/')
+ if isinstance(self.meta['category'], str):
+ self.meta['category'] = self.meta['category'].split('/')
+ elif isinstance(self.meta['category'], list):
+ pass
+ else:
+ # category is of wrong type.
+ logging.error('Category in {0} is an unknown type. Valid '
+ 'types are string or list. Instead it is a {1}'
+ .format(self.meta['slug'], type(self.meta['category'])))
+ self.meta['category'] = []
else:
self.meta['category'] = []
if self.meta['category'] == None:
@@ -229,52 +294,74 @@ def build_meta(self):
if parts['page'] == 1:
parts['page'] = ''
- if not 'url' in self.meta:
- self.meta['url'] = self.options['url_pattern'].format(**parts);
+ if 'url' in self.meta:
+ logging.debug('Using page url pattern')
+ self.url_pattern = self.meta['url']
else:
- self.meta['url'] = self.meta['url'].format(**parts);
+ logging.debug('Using global url pattern')
+ self.url_pattern = self.options['url_pattern']
+
+ self.meta['url'] = self.url_pattern.format(**parts)
+
+ logging.info('URL pattern is: {0}'.format(self.url_pattern))
+ logging.info('URL parts are: {0}'.format(parts))
+
# Get rid of extra slashes
self.meta['url'] = re.sub(r'//+', '/', self.meta['url'])
- logging.debug(self.meta['url'])
+ logging.debug('{0} will be written to {1}'
+ .format(self.meta['slug'], self.meta['url']))
+
# If we have been asked to, rip out any plain "index.html"s
if not self.options['url_include_index']:
self.meta['url'] = re.sub(r'/index\.html$', '/', self.meta['url'])
+ logging.debug('url is: ' + self.meta['url'])
# subpages
self.meta['subpages'] = []
+ self.engine.run_hook('page.meta.post', self)
+
def render(self, templ_vars=None):
"""
Renders the page with the template engine.
"""
+ logging.debug('Rendering ' + self.meta['slug'])
if not templ_vars:
templ_vars = {}
+ # Handle pagination if we needed.
if 'pagination' in self.meta and 'list' in self.meta['pagination']:
extra_pages = self.paginate()
else:
extra_pages = []
+ # Don't clobber possible values in the template variables.
if 'page' in templ_vars:
logging.debug('Found defaulted page data.')
templ_vars['page'].update(self.meta)
else:
templ_vars['page'] = self.meta
+ # Don't clobber pagination either.
if 'pagination' in templ_vars:
templ_vars['pagination'].update(self.meta['pagination'])
else:
templ_vars['pagination'] = self.meta['pagination']
+ # ... and actions! (and logging, and hooking)
+ self.engine.run_hook('page.template.pre', self, templ_vars)
logging.debug('templ_vars.keys(): ' + repr(templ_vars.keys()))
self.rendered = self.template.render(templ_vars)
-
logging.debug('extra pages is: ' + repr(extra_pages))
+ self.engine.run_hook('page.template.post', self)
+
return extra_pages
def paginate(self):
extra_pages = []
+ logging.debug('called pagination for {0}'.format(self.meta['slug']))
if 'page_items' not in self.meta['pagination']:
+ logging.debug('doing pagination for {0}'.format(self.meta['slug']))
# This is the first page of a set of pages. Set up the rest. Other
# wise don't do anything.
@@ -292,7 +379,6 @@ def paginate(self):
return
for k in source_spec:
- logging.debug(k)
source = source[k]
sort_key = self.meta['pagination'].get('sort_key', None)
@@ -301,6 +387,8 @@ def paginate(self):
logging.debug('sort_key: {0}, sort_reverse: {1}'.format(
sort_key, sort_reverse))
+ if not source:
+ return extra_pages
if isinstance(source[0], Page):
source = [p.meta for p in source]
@@ -313,19 +401,25 @@ def paginate(self):
reverse=sort_reverse)
chunks = list(util.chunk(source, self.meta['pagination']['limit']))
+ if not chunks:
+ return extra_pages
# Make a page for each chunk
- for idx, chunk in enumerate(chunks[1:]):
- extra_meta = {
+ for idx, chunk in enumerate(chunks[1:], 2):
+ new_meta = copy.deepcopy(self.meta)
+ new_meta.update({
+ 'url': self.url_pattern,
'pagination': {
'page_items': chunk,
'num_pages': len(chunks),
- 'cur_page': idx + 2,
+ 'cur_page': idx,
}
- }
- new_page = Page(self.path, self.options,
- renderer=self.renderer, extra_meta=extra_meta)
- extra_pages.append(new_page)
+ })
+ new_page = Page.from_meta(new_meta, self.options, self.engine,
+ renderer=self.renderer)
+ logging.debug('page {0} is {1}'.format(idx, new_page))
+ if new_page:
+ extra_pages.append(new_page)
# Set up the next/previous page links
for idx, page in enumerate(extra_pages):
@@ -390,6 +484,9 @@ def __init__(self, raw='', name=None, email=None):
@classmethod
def parse(cls, raw):
+ if isinstance(raw, cls):
+ return raw
+
a = cls(raw)
a.name, _, a.email = cls.parse_author_regex.match(raw).groups()
if a.name:
@@ -412,3 +509,6 @@ def __repr__(self):
def __unicode__(self):
s = self.__str__()
return s.replace('<', '&lt;').replace('>', '&gt;')
+
+class BadMetaException(Exception):
+ pass
View
20 wok/tests/test_util.py
@@ -43,7 +43,7 @@ def test_apostrophes(self):
slug = u'dont-use-bobs-stuff'
self.assertEqual(slug, util.slugify(orig))
- test_apostrophes.todo = "Apostrophes are treated like normal words right now"
+ test_apostrophes.todo = "Apostrophes are treated like normal words."
class TestDatetimes(TestCase):
@@ -53,7 +53,7 @@ def setUp(self):
The datetime is the first commit of wok.
The date is the day this test was first written.
- The time is pi second, in GMT-8.
+ The time is pi second.
"""
self.datetime = datetime(2011, 2, 3, 0, 23, 0, 0)
self.date = date(2011, 10, 12)
@@ -61,7 +61,11 @@ def setUp(self):
def test_blanks(self):
inp = {}
- out = {'datetime': None, 'date': None, 'time': None, }
+ out = {
+ 'datetime': datetime(1970, 1, 1),
+ 'date': date(1970, 1, 1),
+ 'time': time(),
+ }
util.date_and_times(inp)
self.assertEquals(inp, out)
@@ -71,7 +75,7 @@ def test_just_date(self):
out = {
'datetime': datetime(2011, 10, 12, 0, 0, 0, 0),
'date': self.date,
- 'time': None,
+ 'time': time(),
}
util.date_and_times(inp)
@@ -79,7 +83,11 @@ def test_just_date(self):
def test_just_time(self):
inp = {'time': self.time}
- out = {'datetime': None, 'date': None, 'time': self.time, }
+ out = {
+ 'datetime': datetime(1970, 1, 1),
+ 'date': date(1970, 1, 1),
+ 'time': self.time,
+ }
util.date_and_times(inp)
self.assertEquals(inp, out)
@@ -148,7 +156,7 @@ def test_types(self):
out = {
'datetime': datetime(2011, 12, 25),
'date': date(2011, 12, 25),
- 'time': None,
+ 'time': time(),
}
util.date_and_times(inp)
View
16 wok/util.py
@@ -14,7 +14,14 @@ def slugify(text, delim=u'-'):
word = normalize('NFKD', unicode(word)).encode('ascii', 'ignore')
if word:
result.append(word)
- return unicode(delim.join(result))
+
+ result = delim.join(result)
+ if result[0] == '-':
+ result = result[1:]
+ if result[-1] == '-':
+ result = result[:-1]
+
+ return unicode(result)
def chunk(li, n):
@@ -61,3 +68,10 @@ def date_and_times(meta):
meta['datetime'] = datetime(date_part.year, date_part.month, date_part.day)
else:
meta['datetime'] = None
+
+ if meta['date'] is None:
+ meta['date'] = date(1970, 1, 1)
+ if meta['time'] is None:
+ meta['time'] = time()
+ if meta['datetime'] is None:
+ meta['datetime'] = datetime(1970, 1, 1)
Please sign in to comment.
Something went wrong with that request. Please try again.