Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ Cactus makes it easy to relatively link to pages and static assets inside your p

Just use the URL you would normally use: don't forget the leading slash.

You can add extra context to your pages. Just insert it on the top of the file like this:

name: koen
age: 29
{% extends "base.html" %}

which will be converted to a dict: `{'name': 'koen', 'age': '29'}` and added to the context.
If the first line of your document should contain a colon,
you can use the more explicit style of metadata [as used in Jekyll](http://jekyllrb.com/docs/frontmatter/):

---
Author: Me
Style: Great
---
Todays motto: whatever

Always use `---` (three dashes) to fence the metadata.

### Templates

Expand Down
52 changes: 2 additions & 50 deletions cactus/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,37 +71,21 @@ def data(self):
except:
logger.warning("Template engine could not process page: %s", self.path)

def context(self, data=None, extra=None):
def context(self):
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should refactor this method. data is passed, but we never use it. extra is never passed. How about we remove those altogether?

Cheers,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I had left it in because somebody else might rely on them being there. But that seems overly cautious.

The page context.
"""
if extra is None:
extra = {}

context = {'__CACTUS_CURRENT_PAGE__': self,}

page_context, data = self.parse_context(data or self.data())

context.update(self.site.context())
context.update(extra)
context.update(page_context)

return Context(context)

def render(self):
"""
Takes the template data with context and renders it to the final output file.
"""

data = self.data()
context = self.context(data=data)

# This is not very nice, but we already used the header context in the
# page context, so we don't need it anymore.
page_context, data = self.parse_context(data)

context, data = self.site.plugin_manager.preBuildPage(
self.site, self, context, data)
self.site, self, self.context(), self.data())

return Template(data).render(context)

Expand All @@ -125,37 +109,5 @@ def build(self):

self.site.plugin_manager.postBuildPage(self)

def parse_context(self, data, splitChar=':'):
"""
Values like

name: koen
age: 29

will be converted in a dict: {'name': 'koen', 'age': '29'}
"""

if not self.is_html():
return {}, data

values = {}
lines = data.splitlines()
if not lines:
return {}, ''

for i, line in enumerate(lines):

if not line:
continue

elif splitChar in line:
line = line.split(splitChar)
values[line[0].strip()] = (splitChar.join(line[1:])).strip()

else:
break

return values, '\n'.join(lines[i:])

def __repr__(self):
return '<Page: {0}>'.format(self.source_path)
133 changes: 133 additions & 0 deletions cactus/plugin/builtin/page_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#coding:utf-8
from __future__ import unicode_literals
import logging
import re

logger = logging.getLogger(__name__)


class PageContextPlugin(object):
"""
A plugin to extract context variables from the top of the file

TODO:
- read style of metadata from config
- read splitChar from config
"""
ORDER = 0

def preBuildPage(self, page, context, data):
"""
Update the page context with the context variables from the file
and delete them
"""
if not page.is_html() or not data:
return context, data

lines = data.splitlines()

# Look into the file to decide on the style of metadata
# TODO: make that configurable
if lines[0].strip() == '---':
page_context, lines = self.jekyll_style(lines, page)
else:
page_context, lines = self.simple_style(lines)
context.update(page_context)

return context, '\n'.join(lines)

def jekyll_style(self, lines, page):
"""
metadata as defined in http://jekyllrb.com/docs/frontmatter/

`lines` should not be empty
`page` is required only for the error message
"""
if lines[0].strip() != '---': # no metadata
return {}, lines

context = {}
splitChar = ':'
for i, line in enumerate(lines[1:]):
if line.strip() == '---': # end of metadata
i += 1
break

try:
key, value = line.split(splitChar, 1)
except ValueError:
# splitChar not in line
logger.warning('Page context data in file %s seem to end in line %d',
page.source_path, i)
break

context[key.strip()] = value.strip()

return context, lines[i+1:]

def simple_style(self, lines):
"""
Only lines at the top of the file with a colon are metadata.
No multiline metadata

`lines` should not be empty
"""
context = {}
splitChar = ':'

for i, line in enumerate(lines):

if splitChar not in line:
break

key, value = line.split(splitChar, 1)
context[key.strip()] = value.strip()

return context, lines[i:]

def multimarkdown_style(self, lines):
"""
metadata as defined in http://fletcher.github.io/MultiMarkdown-4/metadata

`lines` should not be empty
"""
context = {}
splitChar = ':'
fenced = False
key = None
whitespace = re.compile(r'\s')

if splitChar not in lines[0] and lines[0].strip() != '---':
return {}, lines # no metadata

for i, line in enumerate(lines):
# fencing is allowed but not required in multimarkdown
if line.strip() == '---':
if i == 0:
fenced = True
continue
elif fenced:
i += 1
break

# a blank line triggers the beginning of the rest of the document
if not line.strip():
# if the file starts with a blank line, nothing should be changed
if i != 0:
i += 1
break

# indented lines are the continuation of the previous value
# but multiline values don't have to be indented
if key and (whitespace.match(line) or not splitChar in line):
context[key] = ' '.join(
(context[key], line.lstrip()))
continue

key, value = line.split(splitChar, 1)

# keys are lower cased and stripped of all whitespace
key = re.sub(r'\s', '', key.lower())
context[key] = value.strip()

return context, lines[i:]
8 changes: 6 additions & 2 deletions cactus/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from cactus.plugin.builtin.cache import CacheDurationPlugin
from cactus.plugin.builtin.context import ContextPlugin
from cactus.plugin.builtin.ignore import IgnorePatternsPlugin
from cactus.plugin.builtin.page_context import PageContextPlugin
from cactus.plugin.loader import CustomPluginsLoader, ObjectsPluginLoader
from cactus.plugin.manager import PluginManager
from cactus.static.external.manager import ExternalManager
Expand Down Expand Up @@ -75,8 +76,11 @@ def __init__(self, path, config_paths=None, ui=None,
[
CustomPluginsLoader(self.plugin_path), # User plugins
ObjectsPluginLoader([ # Builtin plugins
ContextPlugin(), CacheDurationPlugin(),
IgnorePatternsPlugin(), PageContextCompatibilityPlugin(),
ContextPlugin(),
PageContextPlugin(),
CacheDurationPlugin(),
IgnorePatternsPlugin(),
PageContextCompatibilityPlugin(),
])
]
)
Expand Down
66 changes: 53 additions & 13 deletions cactus/tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,65 @@ def test_custom_context(self):

class TestCustomPageContext(SiteTestCase):
"""
Test that custom context in the header of pages is feeded to a page
Test that custom context in the header of pages is fed to a page

Includes the built-in site context ('CACTUS'), and custom context.
"""
def setUp(self):
super(TestCustomPageContext, self).setUp()

page = "a: 1\n\tb: Monkey\n{{ a }}\n{{ b }}"
with open(os.path.join(self.site.page_path, "test.html"), "w") as f:
f.write(page)

self.site.build()

def test_custom_context(self):
"""
Test that custom context is provided
"""
with open(os.path.join(self.site.build_path, "test.html")) as f:
a, b = f.read().split("\n")
test_data = [
("a: 1\n"
"\tb: Monkey\n"
"{{ a }}\n"
"{{ b }}",

self.assertEqual(a, "1")
self.assertEqual(b, "Monkey")
"1\n"
"Monkey"),

("a: 1\n"
"\n" # blank line inserted
"\tb: Monkey\n"
"{{ a }}",

"\n"
"\tb: Monkey\n"
"1"),

("---\n" # fenced
" a: 1\n"
"b: Monkey\n"
"---\n"
"{{ a }}\n"
"{{ b }}",

"1\n"
"Monkey"),

("---\n" # double fenced
" a: 1\n"
"b: Monkey\n"
"---\n"
"---\n"
"c: 1\n"
"---\n"
"{{ a }}\n"
"{{ b }}",

"---\n"
"c: 1\n"
"---\n"
"1\n"
"Monkey"),

]
for page, expected in test_data:
with open(os.path.join(self.site.page_path, "test.html"), "w") as f:
f.write(page)

self.site.build()

with open(os.path.join(self.site.build_path, "test.html")) as f:
self.assertEqual(f.read(), expected)
Loading