Skip to content

Commit

Permalink
initial commit: img, video, include_code tags
Browse files Browse the repository at this point in the history
  • Loading branch information
jakevdp committed May 2, 2013
1 parent a4df915 commit 57d62ce
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 0 deletions.
52 changes: 52 additions & 0 deletions liquid_tags/Readme.md
@@ -0,0 +1,52 @@
# Liquid-style Tags
*Author: Jake Vanderplas <jakevdp@cs.washington.edu>*

This plugin allows liquid-style tags to be inserted into markdown within
Pelican documents. Liquid uses tags bounded by ``{% ... %}``, and is used
to extend markdown in other blogging platforms such as octopress.

This set of extensions does not actually interface with liquid, but allows
users to define their own liquid-style tags which will be inserted into
the markdown preprocessor stream. There are several built-in tags, which
can be added as follows.

First, in your pelicanconf.py file, add the plugins you want to use:

PLUGIN_PATH = '/path/to/pelican-plugins'
PLUGINS = ['liquid_tags.img', 'liquid_tags.video',
'liquid_tags.include_code']

There are several options available

## Image Tag
To insert a sized and labeled image in your document, enable the
``liquid_tags.video`` plugin and use the following:

{% img [class name(s)] path/to/image [width [height]] [title text | "title text" ["alt text"]] %}


## Video Tag
To insert flash/HTML5-friendly video into a post, enable the
``liquid_tags.video`` plugin, and add to your document:

{% video /url/to/video.mp4 [width] [height] [/path/to/poster.png] %}

The width and height are in pixels, and can be optionally specified. If they
are not, then the original video size will be used. The poster is an image
which is used as a preview of the video.

To use a video from file, make sure it's in a static directory and put in
the appropriate url.

## Include Code
To include code from a file in your document with a link to the original
file, enable the ``liquid_tags.include_code`` plugin, and add to your
document:

{% include_code myscript.py [Title text] %}

The script must be in the ``code`` subdirectory of your content folder, and
in order for the resulting hyperlink to work, this directory must be listed
under the STATIC_PATHS setting, e.g.:

STATIC_PATHS = ['images', 'code']
1 change: 1 addition & 0 deletions liquid_tags/__init__.py
@@ -0,0 +1 @@
from .liquid_tags import *
66 changes: 66 additions & 0 deletions liquid_tags/img.py
@@ -0,0 +1,66 @@
"""
Image Tag
---------
This implements a Liquid-style image tag for Pelican,
based on the octopress image tag [1]_
Syntax
------
{% img [class name(s)] [http[s]:/]/path/to/image [width [height]] [title text | "title text" ["alt text"]] %}
Examples
--------
{% img /images/ninja.png Ninja Attack! %}
{% img left half http://site.com/images/ninja.png Ninja Attack! %}
{% img left half http://site.com/images/ninja.png 150 150 "Ninja Attack!" "Ninja in attack posture" %}
Output
------
<img src="/images/ninja.png">
<img class="left half" src="http://site.com/images/ninja.png" title="Ninja Attack!" alt="Ninja Attack!">
<img class="left half" src="http://site.com/images/ninja.png" width="150" height="150" title="Ninja Attack!" alt="Ninja in attack posture">
[1] https://github.com/imathis/octopress/blob/master/plugins/image_tag.rb
"""
import re
from .mdx_liquid_tags import LiquidTags

SYNTAX = '{% img [class name(s)] [http[s]:/]/path/to/image [width [height]] [title text | "title text" ["alt text"]] %}'

# Regular expression to match the entire syntax
ReImg = re.compile("""(?P<class>\S.*\s+)?(?P<src>(?:https?:\/\/|\/|\S+\/)\S+)(?:\s+(?P<width>\d+))?(?:\s+(?P<height>\d+))?(?P<title>\s+.+)?""")

# Regular expression to split the title and alt text
ReTitleAlt = re.compile("""(?:"|')(?P<title>[^"']+)?(?:"|')\s+(?:"|')(?P<alt>[^"']+)?(?:"|')""")


@LiquidTags.register('img')
def img(preprocessor, tag, markup):
markup = markup.strip()
attrs = None

# Parse the markup string
match = ReImg.search(markup)
if match:
attrs = dict([(key, val.strip())
for (key, val) in match.groupdict().iteritems() if val])
else:
raise ValueError('Error processing input. '
'Expected syntax: {0}'.format(SYNTAX))

# Check if alt text is present -- if so, split it from title
if 'title' in attrs:
match = ReTitleAlt.search(attrs['title'])
if match:
attrs.update(match.groupdict())
if not attrs.get('alt'):
attrs['alt'] = attrs['title']

# Return the formatted text
return "<img {0}>".format(' '.join('{0}="{1}"'.format(key, val)
for (key, val) in attrs.iteritems()))

#----------------------------------------------------------------------
# This import allows image tag to be a Pelican plugin
from liquid_tags import register

92 changes: 92 additions & 0 deletions liquid_tags/include_code.py
@@ -0,0 +1,92 @@
"""
Include Code Tag
----------------
This implements a Liquid-style video tag for Pelican,
based on the octopress video tag [1]_
Syntax
------
{% include_code path/to/code [Title text] %}
The "path to code" is relative to the code path in
the content directory (TODO: allow this to be set in configs).
Example
-------
{% include_code myscript.py %}
This will import myscript.py from content/downloads/code/myscript.py
and output the contents in a syntax highlighted code block inside a figure,
with a figcaption listing the file name and download link.
The file link will be valid only if the 'code' directory is listed
in the STATIC_PATHS setting, e.g.:
STATIC_PATHS = ['images', 'code']
[1] https://github.com/imathis/octopress/blob/master/plugins/include_code.rb
"""
import re
import os
from .mdx_liquid_tags import LiquidTags


SYNTAX = "{% include_code /path/to/code.py [lang:python] [title] %}"
FORMAT = re.compile(r"""^(?:\s+)?(?P<src>\S+)(?:\s+)?(?:(?:lang:)(?P<lang>\S+))?(?:\s+)?(?P<title>.+)?$""")


@LiquidTags.register('include_code')
def include_code(preprocessor, tag, markup):
markup = markup.strip()

title = None
lang = None
src = None

match = FORMAT.search(markup)
if match:
argdict = match.groupdict()
title = argdict['title']
lang = argdict['lang']
src = argdict['src']

if not src:
raise ValueError("Error processing input, "
"expected syntax: {0}".format(SYNTAX))

# TODO: make this directory a configurable setting
code_dir = 'code'
code_path = os.path.join('content', code_dir, src)

if not os.path.exists(code_path):
return "File {0} could not be found".format(code_path)

code = open(code_path).read()

if title:
title = "{0} {1}".format(title, os.path.basename(src))
else:
title = os.path.basename(src)

url = '/{0}/{1}/{2}'.format('static', code_dir, src)

open_tag = ("<figure class='code'>\n<figcaption><span>{title}</span> "
"<a href='{url}'>download</a></figcaption>".format(title=title,
url=url))
close_tag = "</figure>"

# store HTML tags in the stash. This prevents them from being
# modified by markdown.
open_tag = preprocessor.configs.htmlStash.store(open_tag, safe=True)
close_tag = preprocessor.configs.htmlStash.store(close_tag, safe=True)

source = (open_tag
+ '\n\n ' + '\n '.join(code.split('\n')) + '\n\n'
+ close_tag + '\n')

return source


#----------------------------------------------------------------------
# This import allows image tag to be a Pelican plugin
from liquid_tags import register
14 changes: 14 additions & 0 deletions liquid_tags/liquid_tags.py
@@ -0,0 +1,14 @@
from pelican import signals
from mdx_liquid_tags import LiquidTags
from pelican.readers import EXTENSIONS

def addLiquidTags(gen):
if not gen.settings.get('MD_EXTENSIONS'):
MDReader = EXTENSIONS['markdown']
gen.settings['MD_EXTENSIONS'] = MDReader.default_extensions

if LiquidTags not in gen.settings['MD_EXTENSIONS']:
gen.settings['MD_EXTENSIONS'].append(LiquidTags())

def register():
signals.initialized.connect(addLiquidTags)
76 changes: 76 additions & 0 deletions liquid_tags/mdx_liquid_tags.py
@@ -0,0 +1,76 @@
"""
Markdown Extension for Liquid-style Tags
----------------------------------------
A markdown extension to allow user-defined tags of the form::
{% tag arg1 arg2 ... argn %}
Where "tag" is associated with some user-defined extension.
These result in a preprocess step within markdown that produces
either markdown or html.
"""
import markdown
import itertools
import re
import os
from functools import wraps

# Define some regular expressions
LIQUID_TAG = re.compile(r'\{%.*?%\}')
EXTRACT_TAG = re.compile(r'(?:\s*)(\S+)(?:\s*)')


class _LiquidTagsPreprocessor(markdown.preprocessors.Preprocessor):
_tags = {}
def __init__(self, configs):
self.configs = configs

def run(self, lines):
page = '\n'.join(lines)
liquid_tags = LIQUID_TAG.findall(page)

for i, markup in enumerate(liquid_tags):
# remove {% %}
markup = markup[2:-2]
tag = EXTRACT_TAG.match(markup).groups()[0]
markup = EXTRACT_TAG.sub('', markup, 1)
if tag in self._tags:
liquid_tags[i] = self._tags[tag](self, tag, markup)

# add an empty string to liquid_tags so that chaining works
liquid_tags.append('')

# reconstruct string
page = ''.join(itertools.chain(*zip(LIQUID_TAG.split(page),
liquid_tags)))

# resplit the lines
return page.split("\n")


class LiquidTags(markdown.Extension):
"""Wrapper for MDPreprocessor"""
@classmethod
def register(cls, tag):
"""Decorator to register a new include tag"""
def dec(func):
if tag in _LiquidTagsPreprocessor._tags:
warnings.warn("Enhanced Markdown: overriding tag '%s'" % tag)
_LiquidTagsPreprocessor._tags[tag] = func
return func
return dec

def extendMarkdown(self, md, md_globals):
self.htmlStash = md.htmlStash
md.registerExtension(self)
# for the include_code preprocessor, we need to re-run the
# fenced code block preprocessor after substituting the code.
# Because the fenced code processor is run before, {% %} tags
# within equations will not be parsed as an include.
md.preprocessors.add('mdincludes',
_LiquidTagsPreprocessor(self), ">html_block")


def makeExtension(configs=None):
"""Wrapper for a MarkDown extension"""
return LiquidTags(configs=configs)
72 changes: 72 additions & 0 deletions liquid_tags/video.py
@@ -0,0 +1,72 @@
"""
Video Tag
---------
This implements a Liquid-style video tag for Pelican,
based on the octopress video tag [1]_
Syntax
------
{% video url/to/video [width height] [url/to/poster] %}
Example
-------
{% video http://site.com/video.mp4 720 480 http://site.com/poster-frame.jpg %}
Output
------
<video width='720' height='480' preload='none' controls poster='http://site.com/poster-frame.jpg'>
<source src='http://site.com/video.mp4' type='video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"'/>
</video>
[1] https://github.com/imathis/octopress/blob/master/plugins/video_tag.rb
"""
import os
import re
from .mdx_liquid_tags import LiquidTags

SYNTAX = "{% video url/to/video [url/to/video] [url/to/video] [width height] [url/to/poster] %}"

VIDEO = re.compile(r'(/\S+|https?:\S+)(\s+(/\S+|https?:\S+))?(\s+(/\S+|https?:\S+))?(\s+(\d+)\s(\d+))?(\s+(/\S+|https?:\S+))?')

VID_TYPEDICT = {'.mp4':"type='video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"'",
'.ogv':"type='video/ogg; codecs=theora, vorbis'",
'.webm':"type='video/webm; codecs=vp8, vorbis'"}


@LiquidTags.register('video')
def video(preprocessor, tag, markup):
markup = markup.strip()

videos = []
width = None
height = None
poster = None

match = VIDEO.search(markup)
if match:
groups = match.groups()
videos = [g for g in groups[0:6:2] if g]
width = groups[6]
height = groups[7]
poster = groups[9]

if any(videos):
video_out = "<video width='{width}' height='{height}' preload='none' controls poster='{poster}'>".format(width=width, height=height, poster=poster)
for vid in videos:
base, ext = os.path.splitext(vid)
if ext not in VID_TYPEDICT:
raise ValueError("Unrecognized video extension: "
"{0}".format(ext))
video_out += ("<source src='{0}' "
"{1}>".format(vid, VID_TYPEDICT[ext]))
video_out += "</video>"
else:
raise ValueError("Error processing input, "
"expected syntax: {0}".format(SYNTAX))

return video_out


#----------------------------------------------------------------------
# This import allows image tag to be a Pelican plugin
from liquid_tags import register

0 comments on commit 57d62ce

Please sign in to comment.