Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #2193 from getnikola/shortcodes
Shortcodes
  • Loading branch information
ralsina committed Dec 23, 2015
2 parents c02f64e + beb6b87 commit 19cd8dd
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 109 deletions.
8 changes: 8 additions & 0 deletions CHANGES.txt
@@ -1,3 +1,11 @@
New in master
=============

Features
--------

* Hugo-like shortcodes (Issue #1707)

New in v7.7.4
=============

Expand Down
41 changes: 41 additions & 0 deletions docs/extending.txt
Expand Up @@ -549,3 +549,44 @@ arguments. Example use:
endswith=endswith) + t

site.template_hooks['page_header'].append(greeting, True, u'Nikola Tesla', endswith=u'!')

Shortcodes
==========

Some (hopefully all) markup compilers support shortcodes in these forms::

{{% foo %}} # No arguments
{{% foo bar %}} # One argument, containing "bar"
{{% foo bar baz=bat %}} # Two arguments, one containing "bar", one called "baz" containing "bat"

{{% foo %}}Some text{{% /foo %}} # one argument called "data" containing "Some text"

So, if you are creating a plugin that generates markup, it may be a good idea to register it as a shortcode
in addition of a restructured text directive or markdown extension, thus making it available to all markups.

To implement your own shortcodes from a plugin, you can call ``Nikola.register_shortcode(name, f)`` with the following
arguments:

name:
name of the shortcode (foo in the examples above)
f:
A function that will handle the shortcode

The shortcode handler should take *at least* the following named arguments:

site:
An instance of the Nikola class, to access site state

data:
If the shortcut is used as opening/closing tags, it will be the text between them, otherwise None

If the shortcode tag has arguments of the form "foo=bar" they will be passed as named arguments. Everything else
will be passed as positional arguments in the function call.

So, for example::

{{% foo bar baz=bat beep %}}Some text{{% /foo %}}

Will cause a call like this::

foo_handler("bar", "beep", baz="bat", data="Some text", site=whatever)
54 changes: 53 additions & 1 deletion docs/manual.txt
Expand Up @@ -546,7 +546,6 @@ Or you can completely customize the link using the ``READ_MORE_LINK`` option.
# }} A literal } (U+007D RIGHT CURLY BRACKET)
# READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'


Drafts
~~~~~~

Expand Down Expand Up @@ -800,6 +799,59 @@ Using Pandoc for reStructuredText, Markdown and other input formats that have a
standalone Nikola plugin is **not recommended** as it disables plugins and
extensions that are usually provided by Nikola.

Shortcodes
----------

This feature is "inspired" (copied wholesale) from `Hugo <https://gohugo.io/extras/shortcodes/>`__ so I will
steal part of their docs too.

A shortcode is a simple snippet inside a content file that Nikola will render using a predefined template or
custom code from a plugin.

To use them from plugins, please see `Extending Nikola <https://getnikola.com/extending.html#shortcodes>`__

Using a shortcode
~~~~~~~~~~~~~~~~~

In your content files, a shortcode can be called by using the {{% name parameters %}} form. Shortcode parameters are space delimited. Parameters with spaces can be quoted.

The first word is always the name of the shortcode. Parameters follow the name. Depending upon how the shortcode is defined, the parameters may be named, positional or both (although you can’t mixed parameter types in a single call). The format for named parameters models that of HTML with the format name="value".

Some shortcodes use or require closing shortcodes. Like HTML, the opening and closing shortcodes match (name only), the closing being prepended with a slash.

Example of a paired shortcode (note that we don't have a highlight shortcode yet ;-)::

{{% highlight go %}} A bunch of code here {{% /highlight %}}

Build-in shortcodes
~~~~~~~~~~~~~~~~~~~

post-list
Will show a list of posts, see the `Post List directive for details <#post-list>`__


Template-based shortcodes
~~~~~~~~~~~~~~~~~~~~~~~~~

If you put a template in ``shortcodes/`` called ``mycode.tmpl`` then Nikola will create a shortcode
called ``mycode`` you can use. Any options you pass to the shortcode will be available as variables
for that template. If you use the shortcode as paired, then the contents between the paired tags will
be available in the ``data`` variable.

If you want to access the Nikola object, it will be available as ``site``. Use with care :-)

For example, if your ``shortcodes/foo.tmpl`` contains this::

This uses the bar variable: ${bar}

And your post contains this::

{{% foo bar=bla %}}

Then the output file will contain::

This uses the bar variable: bla

Redirections
------------

Expand Down
31 changes: 29 additions & 2 deletions 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 @@ -905,7 +906,7 @@ def init_plugins(self, commands_only=False):
plugin_info.plugin_object

self._activate_plugins_of_category("ConfigPlugin")

self._register_templated_shortcodes()
signal('configured').send(self)

def _set_global_context(self):
Expand Down Expand Up @@ -1292,6 +1293,32 @@ def url_replacer(self, src, dst, lang=None, url_type=None):

return result

def _register_templated_shortcodes(self):
"""Register shortcodes provided by templates in shortcodes/ folder."""
if not os.path.isdir('shortcodes'):
return
for fname in os.listdir('shortcodes'):
name, ext = os.path.splitext(fname)
if ext == '.tmpl':
with open(os.path.join('shortcodes', fname)) as fd:
template_data = fd.read()

def render_shortcode(t_data=template_data, **kw):
return self.template_system.render_template_to_string(t_data, kw)

self.register_shortcode(name, render_shortcode)

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 on 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
2 changes: 2 additions & 0 deletions nikola/plugins/compile/markdown/__init__.py
Expand Up @@ -41,6 +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):
Expand Down Expand Up @@ -75,6 +76,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

0 comments on commit 19cd8dd

Please sign in to comment.