Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* Added Hugo-like shortcode implementation
* Registered post-list as a shortcode
* Added shortcode support to markdown compiler
  • Loading branch information
ralsina committed Dec 23, 2015
1 parent c02f64e commit 653fb9f
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 63 deletions.
15 changes: 14 additions & 1 deletion nikola/nikola.py
Expand Up @@ -57,7 +57,7 @@
from blinker import signal

from .post import Post # NOQA
from . import DEBUG, utils
from . import DEBUG, utils, shortcodes
from .plugin_categories import (
Command,
LateTask,
Expand Down Expand Up @@ -334,6 +334,7 @@ def __init__(self, **config):
self.configuration_filename = config.pop('__configuration_filename__', False)
self.configured = bool(config)
self.injected_deps = defaultdict(list)
self.shortcode_registry = {}

self.rst_transforms = []
self.template_hooks = {
Expand Down Expand Up @@ -1292,6 +1293,18 @@ def url_replacer(self, src, dst, lang=None, url_type=None):

return result

def register_shortcode(self, name, f):
"""Register function f to handle shortcode "name"."""
if name in self.shortcode_registry:
utils.LOGGER.warn('Shortcode name conflict: %s', name)
return
self.shortcode_registry[name] = f

def apply_shortcodes(self, data):
"""Apply shortcodes from the registry into data."""

return shortcodes.apply_shortcodes(data, self.shortcode_registry)

def generic_rss_renderer(self, lang, title, link, description, timeline, output_path,
rss_teasers, rss_plain, feed_length=10, feed_url=None,
enclosure=_enclosure, rss_links_append_query=None):
Expand Down
3 changes: 2 additions & 1 deletion nikola/plugins/compile/markdown/__init__.py
Expand Up @@ -41,7 +41,7 @@

from nikola.plugin_categories import PageCompiler
from nikola.utils import makedirs, req_missing, write_metadata

from nikola.shortcodes import apply_shortcodes

class CompileMarkdown(PageCompiler):
"""Compile Markdown into HTML."""
Expand Down Expand Up @@ -75,6 +75,7 @@ def compile_html(self, source, dest, is_two_file=True):
if not is_two_file:
_, data = self.split_metadata(data)
output = markdown(data, self.extensions)
output = apply_shortcodes(output, self.site.shortcode_registry, self.site)
out_file.write(output)

def create_post(self, path, **kw):
Expand Down
134 changes: 73 additions & 61 deletions nikola/plugins/compile/rest/post_list.py
Expand Up @@ -50,6 +50,7 @@ class Plugin(RestExtension):
def set_site(self, site):
"""Set Nikola site."""
self.site = site
self.site.register_shortcode('post-list', _do_post_list)
directives.register_directive('post-list', PostList)
PostList.site = site
return super(Plugin, self).set_site(site)
Expand Down Expand Up @@ -147,66 +148,77 @@ def run(self):
lang = self.options.get('lang', utils.LocaleBorg().current_lang)
template = self.options.get('template', 'post_list_directive.tmpl')
sort = self.options.get('sort')
if self.site.invariant: # for testing purposes
post_list_id = self.options.get('id', 'post_list_' + 'fixedvaluethatisnotauuid')
else:
post_list_id = self.options.get('id', 'post_list_' + uuid.uuid4().hex)

filtered_timeline = []
posts = []
step = -1 if reverse is None else None
if show_all is None:
timeline = [p for p in self.site.timeline]
else:
timeline = [p for p in self.site.timeline if p.use_in_feeds]

if categories:
timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories]

for post in timeline:
if tags:
cont = True
tags_lower = [t.lower() for t in post.tags]
for tag in tags:
if tag in tags_lower:
cont = False

if cont:
continue

filtered_timeline.append(post)

if sort:
filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC)

for post in filtered_timeline[start:stop:step]:
if slugs:
cont = True
for slug in slugs:
if slug == post.meta('slug'):
cont = False

if cont:
continue

bp = post.translated_base_path(lang)
if os.path.exists(bp):
self.state.document.settings.record_dependencies.add(bp)

posts += [post]

if not posts:
return []
self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE")

template_data = {
'lang': lang,
'posts': posts,
# Need to provide str, not TranslatableSetting (Issue #2104)
'date_format': self.site.GLOBAL_CONTEXT.get('date_format')[lang],
'post_list_id': post_list_id,
'messages': self.site.MESSAGES,
}
output = self.site.template_system.render_template(
template, None, template_data)
output = _do_post_list(start, stop, reverse, tags, categories, slugs, show_all,
lang, template, sort, state=self.state, site=self.site)
self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE")
return [nodes.raw('', output, format='html')]


def _do_post_list(start=None, stop=None, reverse = False, tags=None, categories=None,
slugs=None, show_all=False, lang=None, template='post_list_directive.tmpl',
sort=None, id=None, data=None, state=None, site=None):
if lang is None:
lang = utils.LocaleBorg().current_lang
if site.invariant: # for testing purposes
post_list_id = id or 'post_list_' + 'fixedvaluethatisnotauuid'
else:
post_list_id = id or 'post_list_' + uuid.uuid4().hex

filtered_timeline = []
posts = []
step = -1 if reverse is None else None
if show_all is None:
timeline = [p for p in site.timeline]
else:
timeline = [p for p in site.timeline if p.use_in_feeds]

if categories:
timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories]

for post in timeline:
if tags:
cont = True
tags_lower = [t.lower() for t in post.tags]
for tag in tags:
if tag in tags_lower:
cont = False

if cont:
continue

filtered_timeline.append(post)

if sort:
filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC)

for post in filtered_timeline[start:stop:step]:
if slugs:
cont = True
for slug in slugs:
if slug == post.meta('slug'):
cont = False

if cont:
continue

bp = post.translated_base_path(lang)
if os.path.exists(bp) and state:
state.document.settings.record_dependencies.add(bp)

posts += [post]

if not posts:
return []

template_data = {
'lang': lang,
'posts': posts,
# Need to provide str, not TranslatableSetting (Issue #2104)
'date_format': site.GLOBAL_CONTEXT.get('date_format')[lang],
'post_list_id': post_list_id,
'messages': site.MESSAGES,
}
output = site.template_system.render_template(
template, None, template_data)
return output
1 change: 1 addition & 0 deletions nikola/post.py
Expand Up @@ -483,6 +483,7 @@ def wrap_encrypt(path, password):
self.translated_source_path(lang),
dest,
self.is_two_file),

if self.meta('password'):
# TODO: get rid of this feature one day (v8?; warning added in v7.3.0.)
LOGGER.warn("The post {0} is using the `password` attribute, which may stop working in the future.")
Expand Down
123 changes: 123 additions & 0 deletions nikola/shortcodes.py
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-

# Copyright © 2012-2015 Roberto Alsina and others.

# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice
# shall be included in all copies or substantial portions of
# the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Support for hugo-style shortcodes."""

try:
from html.parser import HTMLParser
except ImportError:
from HTMLParser import HTMLParser


def apply_shortcodes(data, registry, site=None):
"""Support for hugo-style shortcodes.
{{% name parameters %}} will end up calling the registered "name" function with the given parameters.
{{% name parameters %}} something {{% /name %}} will call name with the parameters and
one extra "data" parameter containing " something ".
The site parameter is passed with the same name to the shortcodes so they can access Nikola state.
>>> apply_shortcodes('==> {{% foo bar=baz %}} <==', {'foo': lambda *a, **k: k['bar']})
'==> baz <=='
>>> apply_shortcodes('==> {{% foo bar=baz %}}some data{{% /foo %}} <==', {'foo': lambda *a, **k: k['bar']+k['data']})
'==> bazsome data <=='
"""

shortcodes = list(_find_shortcodes(data))
# Calculate shortcode results
for sc in shortcodes:
name, args, start, end = sc
a, kw = args
kw['site'] = site
result = registry[name](*a, **kw)
sc.append(result)

# Replace all shortcodes with their output
for sc in shortcodes[::-1]:
_, _, start, end, result = sc
data = data[:start] + result + data[end:]
return data

def _find_shortcodes(data):
""" Find starta and end of shortcode markers.
>>> list(_find_shortcodes('{{% foo %}}{{% bar %}}'))
[['foo', ([], {}), 0, 11], ['bar', ([], {}), 11, 22]]
>>> list(_find_shortcodes('{{% foo bar baz=bat fee=fi fo fum %}}'))
[['foo', (['bar', 'fo', 'fum'], {'fee': 'fi', 'baz': 'bat'}), 0, 37]]
>>> list(_find_shortcodes('{{% foo bar bat=baz%}}some data{{% /foo %}}'))
[['foo', (['bar'], {'bat': 'baz', 'data': 'some data'}), 0, 43]]
"""

# FIXME: this is really space-intolerant

parser = SCParser()
pos = 0
while True:
start = data.find('{{%', pos)
if start == -1:
break
# Get the whole shortcode tag
end = data.find('%}}', start + 1)
name, args = parser.parse_sc('<{}>'.format(data[start + 3:end].strip()))
# Check if this start has a matching close
close_tag = '{{% /{} %}}'.format(name)
close = data.find(close_tag, end + 3)
if close == -1: # No closer
end = end + 3
else: # Tag with closer
args[1]['data'] = data[end + 3:close-1]
end = close + len(close_tag) + 1
pos = end
yield [name, args, start, end]


class SCParser(HTMLParser):
"""Because shortcode attributes are HTML-like abusing the HTML parser parser."""

def parse_sc(self, data):
#print('====> ', data)
self.name = None
self.attrs = {}
self.feed(data)
args = []
kwargs = {}
for a, b in self.attrs:
if b is None:
args.append(a)
else:
kwargs[a] = b
return self.name, (args, kwargs)

def handle_starttag(self, tag, attrs):
self.name = tag
self.attrs = attrs


0 comments on commit 653fb9f

Please sign in to comment.