diff --git a/liquid_tags/Readme.md b/liquid_tags/Readme.md index 6616c91b1..bf27b5e2a 100644 --- a/liquid_tags/Readme.md +++ b/liquid_tags/Readme.md @@ -258,8 +258,10 @@ comment line `# ` will be expanded on load but can be collapsed by tapping on their header. Cells without collapsed comments are rendered as standard code input cells. -## Configuration settings in custom tags +## Advanced features + +### Configuration settings in custom tags Tags do not have access to the full Pelicans settings, and instead arrange for the variables to be passed to the tag. For tag authors who plan to add their tag as in-tree tags, they can just add the variables they need to an array in @@ -269,6 +271,15 @@ user's `pelicanconf.py` settings: LIQUID_CONFIGS = (('PATH', '.', "The default path"), ('SITENAME', 'Default Sitename', 'The name of the site')) +### Custom delimiters +If you are using Liquid Tags in conjunction with some other plugin that also +uses the `{%` and `%}` delimiters to mark its blocks, then chaos will occur. +You can avoid the chaos by defining an alternative set of delimiters for +Liquid Tags. Just add them as a tuple in your `pelicanconf.py` settings: + + LT_DELIMITERS = ('<+', '+>') + + ## Testing To test the plugin in multiple environments we use [tox](http://tox.readthedocs.org/en/latest/). To run the entire test suite: diff --git a/liquid_tags/img.py b/liquid_tags/img.py index 22d83e436..f38e2b052 100644 --- a/liquid_tags/img.py +++ b/liquid_tags/img.py @@ -22,6 +22,7 @@ [1] https://github.com/imathis/octopress/blob/master/plugins/image_tag.rb """ +import os import re from .mdx_liquid_tags import LiquidTags import six @@ -29,11 +30,13 @@ 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+.+)?""") +ReImg = re.compile(r'(?P<class>[-\w\s]+\s+)?(?P<src>(?P<scheme>[a-zA-Z]+://)?(?P<path>\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>[^"']+)?(?:"|')""") +# Attributes to keep in the emmitted img tag +IMG_ATTRS = ['class', 'src', 'width', 'height', 'title',] @LiquidTags.register('img') def img(preprocessor, tag, markup): @@ -42,8 +45,7 @@ def img(preprocessor, tag, markup): # Parse the markup string match = ReImg.search(markup) if match: - attrs = dict([(key, val.strip()) - for (key, val) in six.iteritems(match.groupdict()) if val]) + attrs = {k: v.strip() for k, v in match.groupdict().items() if v} else: raise ValueError('Error processing input. ' 'Expected syntax: {0}'.format(SYNTAX)) @@ -56,9 +58,16 @@ def img(preprocessor, tag, markup): 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 six.iteritems(attrs))) + # prepend site url to absolute paths + if 'scheme' not in attrs and os.path.isabs(attrs['src']): + siteurl = preprocessor.configs.getConfig('SITEURL') + attrs['src'] = siteurl + attrs['path'] + + # create tag + img_attrs = ['{0!s}={1!r}'.format(k, attrs[k]) + for k in IMG_ATTRS if attrs.get(k)] + s = "<img {0}>".format(' '.join(img_attrs)) + return s #---------------------------------------------------------------------- # This import allows image tag to be a Pelican plugin diff --git a/liquid_tags/include_code.py b/liquid_tags/include_code.py index 185a62ee0..a70917b8f 100644 --- a/liquid_tags/include_code.py +++ b/liquid_tags/include_code.py @@ -39,9 +39,11 @@ """ import re import os -import sys +import six from .mdx_liquid_tags import LiquidTags +if six.PY2: + from io import open SYNTAX = "{% include_code /path/to/code.py [lang:python] [lines:X-Y] "\ "[:hidefilename:] [:hidelink:] [:hideall:] [title] %}" @@ -77,7 +79,7 @@ def include_code(preprocessor, tag, markup): argdict = match.groupdict() title = argdict['title'] or "" lang = argdict['lang'] - codec = argdict['codec'] or "utf8" + codec = argdict['codec'] or "utf-8" lines = argdict['lines'] hide_filename = bool(argdict['hidefilename']) hide_link = bool(argdict['hidelink']) @@ -86,6 +88,9 @@ def include_code(preprocessor, tag, markup): first_line, last_line = map(int, lines.split("-")) src = argdict['src'] + if not codec: + codec = 'utf-8' + if not src: raise ValueError("Error processing input, " "expected syntax: {0}".format(SYNTAX)) @@ -96,9 +101,6 @@ def include_code(preprocessor, tag, markup): if not os.path.exists(code_path): raise ValueError("File {0} could not be found".format(code_path)) - if not codec: - codec = 'utf-8' - with open(code_path, encoding=codec) as fh: if lines: code = fh.readlines()[first_line - 1: last_line] @@ -145,18 +147,11 @@ def include_code(preprocessor, tag, markup): else: lang_include = '' - if sys.version_info[0] < 3: - source = (open_tag - + '\n\n ' - + lang_include - + '\n '.join(code.decode(codec).split('\n')) + '\n\n' - + close_tag + '\n') - else: - source = (open_tag - + '\n\n ' - + lang_include - + '\n '.join(code.split('\n')) + '\n\n' - + close_tag + '\n') + source = (open_tag + + '\n\n ' + + lang_include + + '\n '.join(code.split('\n')) + '\n\n' + + close_tag + '\n') return source diff --git a/liquid_tags/mdx_liquid_tags.py b/liquid_tags/mdx_liquid_tags.py index 60edf5e21..a7e97e97e 100644 --- a/liquid_tags/mdx_liquid_tags.py +++ b/liquid_tags/mdx_liquid_tags.py @@ -16,46 +16,45 @@ import os from functools import wraps -# Define some regular expressions -LIQUID_TAG = re.compile(r'\{%.*?%\}', re.MULTILINE | re.DOTALL) -EXTRACT_TAG = re.compile(r'(?:\s*)(\S+)(?:\s*)') LT_CONFIG = { 'CODE_DIR': 'code', 'NOTEBOOK_DIR': 'notebooks', 'FLICKR_API_KEY': 'flickr', - 'GIPHY_API_KEY': 'giphy' + 'GIPHY_API_KEY': 'giphy', + 'LT_DELIMITERS': ('{%', '%}'), + 'SITEURL': '', } LT_HELP = { 'CODE_DIR' : 'Code directory for include_code subplugin', 'NOTEBOOK_DIR' : 'Notebook directory for notebook subplugin', 'FLICKR_API_KEY': 'Flickr key for accessing the API', - 'GIPHY_API_KEY': 'Giphy key for accessing the API' + 'GIPHY_API_KEY': 'Giphy key for accessing the API', + 'LT_DELIMITERS': 'Alternative set of Liquid Tags block delimiters', + 'SITEURL': 'Base URL of your web site. ' + 'Inserted before absolute media paths.', } class _LiquidTagsPreprocessor(markdown.preprocessors.Preprocessor): + LT_FMT = r'{0}(?:\s*)(?P<tag>\S+)(?:\s*)(?P<markup>.*?)(?:\s*){1}' + LT_RE_FLAGS = re.MULTILINE | re.DOTALL | re.UNICODE _tags = {} + def __init__(self, configs): + cls = self.__class__ + liquid_tag_re = cls.LT_FMT.format( + *map(re.escape, configs.getConfig('LT_DELIMITERS'))) + self.liquid_tag = re.compile(liquid_tag_re, cls.LT_RE_FLAGS) self.configs = configs + def expand_tag(self, match): + tag, markup = match.groups() + if tag in self.__class__._tags: + return self.__class__._tags[tag](self, tag, markup) + else: + return match[0] + 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.strip()) - - # 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") + page = self.liquid_tag.sub(self.expand_tag, page) + return page.splitlines() class LiquidTags(markdown.Extension): @@ -82,15 +81,19 @@ def dec(func): return func return dec - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md): 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") + if 'include_code' in _LiquidTagsPreprocessor._tags: + # 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. + # + # The now deprecated add() function, with ">htmlblock" argument + # resulted in a priority of 15. Which is what we use here. + md.preprocessors.register(_LiquidTagsPreprocessor(self), + 'mdincludes', 15) def makeExtension(configs=None): diff --git a/liquid_tags/notebook.py b/liquid_tags/notebook.py index c310e6d5e..597755e63 100644 --- a/liquid_tags/notebook.py +++ b/liquid_tags/notebook.py @@ -283,8 +283,9 @@ def notebook(preprocessor, tag, markup): language_applied_highlighter = partial(custom_highlighter, language=language) + content_root = preprocessor.configs.getConfig('PATH', default='content') nb_dir = preprocessor.configs.getConfig('NOTEBOOK_DIR') - nb_path = os.path.join(settings.get('PATH', 'content'), nb_dir, src) + nb_path = os.path.join(content_root, nb_dir, src) if not os.path.exists(nb_path): raise ValueError("File {0} could not be found".format(nb_path)) diff --git a/liquid_tags/requirements.txt b/liquid_tags/requirements.txt old mode 100755 new mode 100644 diff --git a/liquid_tags/test_data/content/test-generic-config-alt-delim.md b/liquid_tags/test_data/content/test-generic-config-alt-delim.md new file mode 100644 index 000000000..bc457e95b --- /dev/null +++ b/liquid_tags/test_data/content/test-generic-config-alt-delim.md @@ -0,0 +1,7 @@ +Title: test alternate tag delimiters +Date: 2020-06-13 +Authors: M. Stamatogiannakis + +Regular tag is not expanded: {% generic config author %} is stupid + +Alternate tag is expanded: <+ generic config author +> is smart diff --git a/liquid_tags/test_generation.py b/liquid_tags/test_generation.py index 169034ad2..42b81f2d1 100644 --- a/liquid_tags/test_generation.py +++ b/liquid_tags/test_generation.py @@ -2,6 +2,7 @@ from __future__ import print_function import filecmp +import logging import os import unittest from shutil import rmtree @@ -32,65 +33,46 @@ def tearDown(self): rmtree(self.temp_cache) os.chdir(PLUGIN_DIR) - @unittest.skipIf(IPYTHON_VERSION != 3, - reason="output must be created with ipython version 3") - def test_generate_with_ipython3(self): + @unittest.skipIf(IPYTHON_VERSION not in (2, 3), + reason="iPython v%d is not supported" % IPYTHON_VERSION) + def test_generate(self): '''Test generation of site with the plugin.''' - base_path = os.path.dirname(os.path.abspath(__file__)) - base_path = os.path.join(base_path, 'test_data') - content_path = os.path.join(base_path, 'content') - output_path = os.path.join(base_path, 'output') - settings_path = os.path.join(base_path, 'pelicanconf.py') - settings = read_settings(path=settings_path, - override={'PATH': content_path, - 'OUTPUT_PATH': self.temp_path, - 'CACHE_PATH': self.temp_cache, - } - ) - + # set paths + _pj = os.path.join + base_path = _pj(os.path.dirname(os.path.abspath(__file__)), 'test_data') + content_path = _pj(base_path, 'content') + output_path = _pj(base_path, 'output') + settings_path = _pj(base_path, 'pelicanconf.py') + + # read settings + override = { + 'PATH': content_path, + 'OUTPUT_PATH': self.temp_path, + 'CACHE_PATH': self.temp_cache, + } + settings = read_settings(path=settings_path, override=override) + + # run and test created files pelican = Pelican(settings) pelican.run() # test existence - assert os.path.exists(os.path.join(self.temp_path, - 'test-ipython-notebook-nb-format-3.html')) - assert os.path.exists(os.path.join(self.temp_path, - 'test-ipython-notebook-nb-format-4.html')) + assert os.path.exists(_pj(self.temp_path, + 'test-ipython-notebook-nb-format-3.html')) + assert os.path.exists(_pj(self.temp_path, + 'test-ipython-notebook-nb-format-4.html')) # test differences - #assert filecmp.cmp(os.path.join(output_path, - # 'test-ipython-notebook-v2.html'), - # os.path.join(self.temp_path, - # 'test-ipython-notebook.html')) - - @unittest.skipIf(IPYTHON_VERSION != 2, - reason="output must be created with ipython version 2") - def test_generate_with_ipython2(self): - '''Test generation of site with the plugin.''' - - base_path = os.path.dirname(os.path.abspath(__file__)) - base_path = os.path.join(base_path, 'test_data') - content_path = os.path.join(base_path, 'content') - settings_path = os.path.join(base_path, 'pelicanconf.py') - settings = read_settings(path=settings_path, - override={'PATH': content_path, - 'OUTPUT_PATH': self.temp_path, - 'CACHE_PATH': self.temp_cache, - } - ) - - pelican = Pelican(settings) - pelican.run() + if IPYTHON_VERSION == 3: + f1 = _pj(output_path, 'test-ipython-notebook-v2.html') + f2 = _pj(self.temp_path, 'test-ipython-notebook.html') + #assert filecmp.cmp(f1, f2) + elif IPYTHON_VERSION == 2: + f1 = _pj(output_path, 'test-ipython-notebook-v3.html') + f2 = _pj(self.temp_path, 'test-ipython-notebook.html') + #assert filecmp.cmp(f1, f2) + else: + logging.error('Unexpected IPYTHON_VERSION: %s', IPYTHON_VERSION) + assert False - # test existence - assert os.path.exists(os.path.join(self.temp_path, - 'test-ipython-notebook-nb-format-3.html')) - assert os.path.exists(os.path.join(self.temp_path, - 'test-ipython-notebook-nb-format-4.html')) - - # test differences - #assert filecmp.cmp(os.path.join(output_path, - # 'test-ipython-notebook-v3.html'), - # os.path.join(self.temp_path, - # 'test-ipython-notebook.html')) diff --git a/liquid_tags/test_generic.py b/liquid_tags/test_generic.py index 4960219d7..0f3784754 100644 --- a/liquid_tags/test_generic.py +++ b/liquid_tags/test_generic.py @@ -34,28 +34,54 @@ def tearDown(self): def test_generic_tag_with_config(self): '''Test generation of site with a generic tag that reads in a config file.''' - base_path = os.path.dirname(os.path.abspath(__file__)) - base_path = os.path.join(base_path, 'test_data') - content_path = os.path.join(base_path, 'content') - output_path = os.path.join(base_path, 'output') - settings_path = os.path.join(base_path, 'pelicanconf.py') - settings = read_settings(path=settings_path, - override={'PATH': content_path, - 'OUTPUT_PATH': self.temp_path, - 'CACHE_PATH': self.temp_cache, - } - ) + _pj = os.path.join + base_path = _pj(os.path.dirname(os.path.abspath(__file__)), 'test_data') + content_path = _pj(base_path, 'content') + output_path = _pj(base_path, 'output') + settings_path = _pj(base_path, 'pelicanconf.py') + override = { + 'PATH': content_path, + 'OUTPUT_PATH': self.temp_path, + 'CACHE_PATH': self.temp_cache, + } + settings = read_settings(path=settings_path, override=override) pelican = Pelican(settings) pelican.run() - assert os.path.exists(os.path.join(self.temp_path, - 'test-generic-config-tag.html')) + # test normal tags + f = _pj(self.temp_path, 'test-generic-config-tag.html') + assert os.path.exists(f) + assert "Tester" in open(f).read() - assert "Tester" in open(os.path.join(self.temp_path, - 'test-generic-config-tag.html')).read() # test differences - #assert filecmp.cmp(os.path.join(output_path, - # 'test-ipython-notebook-v3.html'), - # os.path.join(self.temp_path, - # 'test-ipython-notebook.html')) + f1 = _pj(output_path, 'test-ipython-notebook-v3.html') + f2 = _pj(self.temp_path, 'test-ipython-notebook.html') + #assert filecmp.cmp(f1, f2) + + def test_generic_alt_delimiters(self): + '''Test generation of site with alternatively delimited tags.''' + + _pj = os.path.join + base_path = _pj(os.path.dirname(os.path.abspath(__file__)), 'test_data') + content_path = _pj(base_path, 'content') + output_path = _pj(base_path, 'output') + settings_path = _pj(base_path, 'pelicanconf.py') + override = { + 'PATH': content_path, + 'OUTPUT_PATH': self.temp_path, + 'CACHE_PATH': self.temp_cache, + 'LT_DELIMITERS': ('<+', '+>'), + } + settings = read_settings(path=settings_path, override=override) + + pelican = Pelican(settings) + pelican.run() + + # test alternate delimiters + f = _pj(self.temp_path, 'test-alternate-tag-delimiters.html') + fc = open(f).read() + assert '{% generic config author %} is stupid' in fc + assert 'The Tester is smart' in fc + assert 'The Tester is stupid' not in fc + diff --git a/liquid_tags/test_include_code.py b/liquid_tags/test_include_code.py index a5321c67f..b25190784 100644 --- a/liquid_tags/test_include_code.py +++ b/liquid_tags/test_include_code.py @@ -1,5 +1,7 @@ +# -*- coding: utf-8 -*- import re import sys +import six import unittest import pytest @@ -10,7 +12,7 @@ raise unittest.SkipTest('Those tests are pytest-compatible only') @pytest.mark.parametrize( - 'input,expected', [ + 'input, expected', [ ( 'test_data/main.c', ('test_data/main.c', None, None, None, None, None, None, None) @@ -62,8 +64,10 @@ class Object: class preprocessor: @classmethod - def func(*x, safe=False): - return ''.join([str(s) for s in x]) + def func(cls, *x, **kwargs): + safe = kwargs.get('safe', False) + cls_s = "<class '%s.%s'>" % (cls.__module__, cls.__name__) + return '%s%s' % (cls_s, ''.join([str(s) for s in x])) def __init__(self): self.configs = Object() @@ -77,10 +81,11 @@ def __init__(self): 'test_data/main.c', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' - '<span>main.c</span> ' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-filename">main.c</span>' '<a href=\'/test_data/main.c\'>download</a>' - '\n' + '</figcaption>\n' '\n' ' #include <stdio.h>\n' ' \n' @@ -99,7 +104,7 @@ def __init__(self): 'test_data/main.c :hideall:', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' + '<figure class=\'code\'>\n' '\n' '\n' ' #include <stdio.h>\n' @@ -119,9 +124,11 @@ def __init__(self): 'test_data/main.c :hidefilename: C application', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-title">C application</span>' '<a href=\'/test_data/main.c\'>download</a>' - '\n' + '</figcaption>\n' '\n' ' #include <stdio.h>\n' ' \n' @@ -140,9 +147,10 @@ def __init__(self): 'test_data/main.c :hidelink:', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' - '<span>main.c</span> ' - '\n' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-filename">main.c</span>' + '</figcaption>\n' '\n' ' #include <stdio.h>\n' ' \n' @@ -161,10 +169,11 @@ def __init__(self): 'test_data/main.c lang:c', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' - '<span>main.c</span> ' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-filename">main.c</span>' '<a href=\'/test_data/main.c\'>download</a>' - '\n' + '</figcaption>\n' '\n' ' :::c\n' ' #include <stdio.h>\n' @@ -184,10 +193,12 @@ def __init__(self): 'test_data/main.c lines:4-6', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' - '<span>main.c [Lines 4-6]</span> ' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-filename">main.c</span>' + '<span class="liquid-tags-code-lines">[Lines 4-6]</span>' '<a href=\'/test_data/main.c\'>download</a>' - '\n' + '</figcaption>\n' '\n' ' printf("Hello world!");\n' ' \n' @@ -201,10 +212,11 @@ def __init__(self): 'test_data/main_cz.c codec:iso-8859-1', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' - '<span>main_cz.c</span> ' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-filename">main_cz.c</span>' '<a href=\'/test_data/main_cz.c\'>download</a>' - '\n' + '</figcaption>\n' '\n' ' #include <stdio.h>\n' ' \n' @@ -223,10 +235,12 @@ def __init__(self): 'test_data/main.c C Application', ( '<class \'liquid_tags.test_include_code.preprocessor\'>' - '<figure class=\'code\'>\n<figcaption>' - '<span>C Application main.c</span> ' + '<figure class=\'code\'>\n' + '<figcaption>' + '<span class="liquid-tags-code-title">C Application</span>' + '<span class="liquid-tags-code-filename">main.c</span>' '<a href=\'/test_data/main.c\'>download</a>' - '\n' + '</figcaption>\n' '\n' ' #include <stdio.h>\n' ' \n' @@ -243,5 +257,13 @@ def __init__(self): ), ] ) + def test_create_html(input, expected): - assert include_code.include_code(preprocessor(), 'include_code', input) == expected + # output is returned as utf-8 + output = include_code.include_code(preprocessor(), 'include_code', input) + + # expected needs to be interpreted as utf-8 in python2 + if six.PY2: + expected = expected.decode('utf-8') + + assert output == expected diff --git a/liquid_tags/tox.ini b/liquid_tags/tox.ini index e2e22bdc3..3f093ab4d 100644 --- a/liquid_tags/tox.ini +++ b/liquid_tags/tox.ini @@ -2,8 +2,8 @@ skipsdist = True minversion = 1.8 envlist = - py{27,34}-ipython2, - py{27,34}-ipython3, + py{27,34,36,37}-ipython2, + py{27,34,36,37}-ipython3, [testenv] commands = py.test