diff --git a/liquid_tags/Readme.md b/liquid_tags/Readme.md new file mode 100644 index 0000000..30c4734 --- /dev/null +++ b/liquid_tags/Readme.md @@ -0,0 +1,52 @@ +# Liquid-style Tags +*Author: Jake Vanderplas * + +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'] \ No newline at end of file diff --git a/liquid_tags/__init__.py b/liquid_tags/__init__.py new file mode 100644 index 0000000..eabcd63 --- /dev/null +++ b/liquid_tags/__init__.py @@ -0,0 +1 @@ +from .liquid_tags import * diff --git a/liquid_tags/img.py b/liquid_tags/img.py new file mode 100644 index 0000000..68a3b1a --- /dev/null +++ b/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 +------ + +Ninja Attack! +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\S.*\s+)?(?P(?:https?:\/\/|\/|\S+\/)\S+)(?:\s+(?P\d+))?(?:\s+(?P\d+))?(?P\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 + diff --git a/liquid_tags/include_code.py b/liquid_tags/include_code.py new file mode 100644 index 0000000..302d875 --- /dev/null +++ b/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 diff --git a/liquid_tags/liquid_tags.py b/liquid_tags/liquid_tags.py new file mode 100644 index 0000000..881d8ea --- /dev/null +++ b/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) diff --git a/liquid_tags/mdx_liquid_tags.py b/liquid_tags/mdx_liquid_tags.py new file mode 100644 index 0000000..3e32011 --- /dev/null +++ b/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) diff --git a/liquid_tags/video.py b/liquid_tags/video.py new file mode 100644 index 0000000..5ce441a --- /dev/null +++ b/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