From 4187ad9858f35b398ca46bd454012ad841bfc361 Mon Sep 17 00:00:00 2001 From: Martin McCallion Date: Tue, 3 Nov 2020 15:07:53 +0000 Subject: [PATCH 1/2] Add the Quotebacks plugin. --- v8/quotebacks/README.md | 45 ++++++ v8/quotebacks/quotebacks_mdx.plugin | 14 ++ v8/quotebacks/quotebacks_mdx.py | 240 ++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100644 v8/quotebacks/README.md create mode 100644 v8/quotebacks/quotebacks_mdx.plugin create mode 100644 v8/quotebacks/quotebacks_mdx.py diff --git a/v8/quotebacks/README.md b/v8/quotebacks/README.md new file mode 100644 index 00000000..6b97bce1 --- /dev/null +++ b/v8/quotebacks/README.md @@ -0,0 +1,45 @@ +# The Quotebacks Markdown Extension Plugin + +## Description + +[Quotebacks](https://quotebacks.net) is a recently-developed JavaScript library for applying a standard styling to blockquotes on web pages. As released, it also includes a browser plugin for Chrome, that helps with capturing the quotes and applying the correct HTML. + +However, if you don't use Chrome, and/or you prefer to write in Markdown, it's not ideal. + +Luckily [Matt Webb](http://interconnected.org/home/2020/06/16/quotebacks) has written an [extension for Python-Markdown](https://github.com/genmon/quotebacks-mdx) which allows the writer to activate the Quotebacks styling using simple Markdown. + +This change incorporates that extension into Nikola. + +To use it you'll have to have the `quotebacks.js` file installed, and have suitable code in your template to ensure that it's referenced in posts. Or just suitable code to import it from the CDN. + +## Usage + +To use it you'll need to make the `quoteback.js` library accessible to your site. You can either download it and keep a local copy, and then include something like the following in your `EXTRA_HEAD_DATA` constant in `conf.py`: + +``` + EXTRA_HEAD_DATA = """ + + """ +``` + +Or you can access it using a CDN, in which case you'll want the following: + +``` + EXTRA_HEAD_DATA = """ + """ + + +class QuotebacksExtension(MarkdownExtension, Extension): + """Python-Markdown extension that wraps QuotebacksProcessor and + makes it available to transform HTML docs. + """ + + def __init__(self, **kwargs): + """ Override the init and set self.config according to: + + https://python-markdown.github.io/extensions/api/ + + Keys set in self.config can be passed in as kwargs to this + extension to override the default values. These are available + from self.getConfigs() (as a dict) later. + """ + # self.config["key"] = [ + # [], # default value + # "Sets ...", # description of parameter + # ] + self.config = {} + + super().__init__(**kwargs) + + def extendMarkdown(self, md): + # Run with a priority of 19. + # Needs to run: + # - AFTER a tags have been created + # - BEFORE smartypants + # Pass in the dictionary of config variables: this tree processor has overridden + # its init to accept these. + md.treeprocessors.register( + QuotebacksProcessor(md, self.getConfigs()), "quotebacks", 19 + ) + + +class QuotebacksProcessor(markdown.treeprocessors.Treeprocessor): + """Python-Markdown processor that changes the blockquotes in the output document to + match the Quotebacks format. + + Rules: + + - there must be more than one child of the blockquote + - the final child must be a p element + - the p element must have the pattern: + +

-- AUTHOR, TITLE

+ + The source Markdown format for this is: + + > ... + > + > -- AUTHOR, [TITLE](CITE_URL) + """ + + def __init__(self, md, config): + """ Usually a tree processor won't take a config dict, but here + we're overriding init to accept one. This is for future options. + + Stash the markdown instance for later. + """ + self.config = config + self.md = md + + # We record whether there were quotebacks found + self.md.quotebacks_found = False + + super().__init__(md) + + def run(self, root): + # Iterate over all blockquote elements + for bq in root.iter("blockquote"): + # blockquote must have >1 children + if len(bq) <= 1: + LOGGER.info("blockquote must have >1 children") + continue + + # blockquote's final child must be a paragraph + if bq[-1].tag != "p": + LOGGER.info("blockquote's final child must be a p tag") + continue + + # Keep the paragraph element handy. It must look like + # '-- AUTHOR TITLE' + p_elem = bq[-1] + + if not (len(p_elem) == 1 and p_elem[0].tag == "a"): + LOGGER.info("Final p must have one child and it must be an a tag") + continue + + a_elem = p_elem[0] + title = a_elem.text + cite_url = a_elem.get("href", None) + + if not title: + LOGGER.info("Final a tag must have text for the title") + continue + + if not cite_url: + LOGGER.info("Final a tag must have an href for the cite URL") + continue + + # There must be no tail text + if p_elem.tail is not None: + LOGGER.info("p must have no tail text") + continue + + # The start text must follow a strict pattern + if not p_elem.text: + LOGGER.info("p must have start text") + + m = re.match(r"^-- (?P.+),\s+$", p_elem.text) + if not m: + LOGGER.info("p must match the pattern '-- AUTHOR, '") + continue + + author = m.groupdict()["author"] + + # Time to change the document! + # First, remove the original element + bq.remove(p_elem) + + # Build the replacement tag, using the original elements where possible + footer = ET.SubElement(bq, "footer",) + footer.text = p_elem.text + cite = ET.SubElement(footer, "cite") + cite.append(a_elem) + + # Set bq Attributes + attrib = { + "class": "quoteback", + "data-title": title, + "data-author": author, + "cite": cite_url, + } + + [bq.set(k, v) for k, v in attrib.items()] + + LOGGER.info("Successful: blockquote -> quoteback") + self.md.quotebacks_found = True + + +def test(): + # Log everything + logging.basicConfig(stream=sys.stdout, level=logging.INFO) + + # Use UTF-8 instead of the defaut HTML entities + # https://python-markdown.github.io/extensions/smarty/ + smarty_substitutions = { + "left-single-quote": "‘", + "right-single-quote": "’", + "left-double-quote": "“", + "right-double-quote": "”", + "left-angle-quote": "«", + "right-angle-quote": "»", + "ellipsis": "…", + "ndash": "–", + "mdash": "—", + } + + renderer = markdown.Markdown( + output_format="html5", + tab_length=2, + extensions=["smarty", "attr_list", QuotebacksExtension()], + extension_configs={"smarty": {"substitutions": smarty_substitutions}}, + ) + + source_md = """# Test Document + +Hello, World! + +> The text renaissance is an actual _renaissance._ It's a story of history-inspired +> renewal in a very fundamental way: exciting recent developments are due in part to a new +> generation of young product visionaries circling back to the early history of digital +> text, rediscovering old, abandoned ideas, and reimagining the bleeding edge in terms of +> the unexplored adjacent possible of the 80s and 90s. +> +> -- @ribbonfarm, [A Text Renaissance](https://www.ribbonfarm.com/2020/02/24/a-text-renaissance/) +""" + + expected_html = """

Test Document

+

Hello, World!

+
+

The text renaissance is an actual renaissance. It’s a story of history-inspired +renewal in a very fundamental way: exciting recent developments are due in part to a new +generation of young product visionaries circling back to the early history of digital +text, rediscovering old, abandoned ideas, and reimagining the bleeding edge in terms of +the unexplored adjacent possible of the 80s and 90s.

+ +
""" + + html = renderer.convert(source_md) + + assert renderer.quotebacks_found is True + + print(html) + + if html != expected_html: + LOGGER.error("Generated HTML does not match expected HTML") + else: + LOGGER.info("-- SUCCESS --") + + +if __name__ == "__main__": + # Runs tests only if run as a script + test() From 5882d73893a8630c45d258aef4b737e1a194b0c0 Mon Sep 17 00:00:00 2001 From: Martin McCallion Date: Fri, 13 Aug 2021 15:12:15 +0100 Subject: [PATCH 2/2] Address suggestions on pull request from last November. --- v8/quotebacks/LICENSE | 21 +++++++ v8/quotebacks/README.md | 11 ++-- v8/quotebacks/quotebacks_mdx.py | 106 +++++++------------------------- 3 files changed, 50 insertions(+), 88 deletions(-) create mode 100644 v8/quotebacks/LICENSE diff --git a/v8/quotebacks/LICENSE b/v8/quotebacks/LICENSE new file mode 100644 index 00000000..ba373b21 --- /dev/null +++ b/v8/quotebacks/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Matt Webb + +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. diff --git a/v8/quotebacks/README.md b/v8/quotebacks/README.md index 6b97bce1..9d8d9da2 100644 --- a/v8/quotebacks/README.md +++ b/v8/quotebacks/README.md @@ -8,9 +8,9 @@ However, if you don't use Chrome, and/or you prefer to write in Markdown, it's n Luckily [Matt Webb](http://interconnected.org/home/2020/06/16/quotebacks) has written an [extension for Python-Markdown](https://github.com/genmon/quotebacks-mdx) which allows the writer to activate the Quotebacks styling using simple Markdown. -This change incorporates that extension into Nikola. +This plugin incorporates that extension into Nikola. The original code is MIT-licensed, which allows us to use it freely. -To use it you'll have to have the `quotebacks.js` file installed, and have suitable code in your template to ensure that it's referenced in posts. Or just suitable code to import it from the CDN. +You'll have to have the `quotebacks.js` file installed, and have code in your `config.py` to import it from the CDN or reference it directly. See below. ## Usage @@ -22,7 +22,7 @@ To use it you'll need to make the `quoteback.js` library accessible to your site """ ``` -Or you can access it using a CDN, in which case you'll want the following: +Or you can access it using the CDN, in which case you'll want the following: ``` EXTRA_HEAD_DATA = """ @@ -40,6 +40,9 @@ What is Nikola? The answer is here: > > -- Roberto Alsina and the Nikola contributors, [The Nikola Handbook](https://getnikola.com/handbook.html) +``` +You need to format the footer of the blockquote as shown above: -``` +* a blank, quoted line; +* followed by a quoted line with two hyphens, a space, the name of the author or authors, a comma and a space, then a Markdown-formatted link to the source. diff --git a/v8/quotebacks/quotebacks_mdx.py b/v8/quotebacks/quotebacks_mdx.py index 4cfd07b7..3af34f55 100644 --- a/v8/quotebacks/quotebacks_mdx.py +++ b/v8/quotebacks/quotebacks_mdx.py @@ -1,6 +1,27 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- +# MIT License +# +# Copyright (c) 2020 Matt Webb +# +# 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. + """ # Quotebacks extension for Python-Markdown @@ -39,23 +60,6 @@ class QuotebacksExtension(MarkdownExtension, Extension): makes it available to transform HTML docs. """ - def __init__(self, **kwargs): - """ Override the init and set self.config according to: - - https://python-markdown.github.io/extensions/api/ - - Keys set in self.config can be passed in as kwargs to this - extension to override the default values. These are available - from self.getConfigs() (as a dict) later. - """ - # self.config["key"] = [ - # [], # default value - # "Sets ...", # description of parameter - # ] - self.config = {} - - super().__init__(**kwargs) - def extendMarkdown(self, md): # Run with a priority of 19. # Needs to run: @@ -172,69 +176,3 @@ def run(self, root): LOGGER.info("Successful: blockquote -> quoteback") self.md.quotebacks_found = True - - -def test(): - # Log everything - logging.basicConfig(stream=sys.stdout, level=logging.INFO) - - # Use UTF-8 instead of the defaut HTML entities - # https://python-markdown.github.io/extensions/smarty/ - smarty_substitutions = { - "left-single-quote": "‘", - "right-single-quote": "’", - "left-double-quote": "“", - "right-double-quote": "”", - "left-angle-quote": "«", - "right-angle-quote": "»", - "ellipsis": "…", - "ndash": "–", - "mdash": "—", - } - - renderer = markdown.Markdown( - output_format="html5", - tab_length=2, - extensions=["smarty", "attr_list", QuotebacksExtension()], - extension_configs={"smarty": {"substitutions": smarty_substitutions}}, - ) - - source_md = """# Test Document - -Hello, World! - -> The text renaissance is an actual _renaissance._ It's a story of history-inspired -> renewal in a very fundamental way: exciting recent developments are due in part to a new -> generation of young product visionaries circling back to the early history of digital -> text, rediscovering old, abandoned ideas, and reimagining the bleeding edge in terms of -> the unexplored adjacent possible of the 80s and 90s. -> -> -- @ribbonfarm, [A Text Renaissance](https://www.ribbonfarm.com/2020/02/24/a-text-renaissance/) -""" - - expected_html = """

Test Document

-

Hello, World!

-
-

The text renaissance is an actual renaissance. It’s a story of history-inspired -renewal in a very fundamental way: exciting recent developments are due in part to a new -generation of young product visionaries circling back to the early history of digital -text, rediscovering old, abandoned ideas, and reimagining the bleeding edge in terms of -the unexplored adjacent possible of the 80s and 90s.

- -
""" - - html = renderer.convert(source_md) - - assert renderer.quotebacks_found is True - - print(html) - - if html != expected_html: - LOGGER.error("Generated HTML does not match expected HTML") - else: - LOGGER.info("-- SUCCESS --") - - -if __name__ == "__main__": - # Runs tests only if run as a script - test()