From c85ea081c076f63fad432267bb00d9ec038ab587 Mon Sep 17 00:00:00 2001 From: sil Date: Sat, 15 Feb 2014 01:21:06 +0000 Subject: [PATCH] Simple Footnotes plugin. Add footnotes to your text by enclosing them in [ref] and [/ref]. --- simple_footnotes/README.md | 26 +++++++ simple_footnotes/__init__.py | 1 + simple_footnotes/simple_footnotes.py | 91 +++++++++++++++++++++++ simple_footnotes/test_simple_footnotes.py | 33 ++++++++ 4 files changed, 151 insertions(+) create mode 100644 simple_footnotes/README.md create mode 100644 simple_footnotes/__init__.py create mode 100644 simple_footnotes/simple_footnotes.py create mode 100644 simple_footnotes/test_simple_footnotes.py diff --git a/simple_footnotes/README.md b/simple_footnotes/README.md new file mode 100644 index 0000000..d4105cc --- /dev/null +++ b/simple_footnotes/README.md @@ -0,0 +1,26 @@ +Simple Footnotes +================ + +A Pelican plugin to add footnotes to blog posts. + +When writing a post or page, add a footnote like this: + + Here's my written text[ref]and here is a footnote[/ref]. + +This will appear as, roughly: + +Here's my written text1 + + 1. and here is a footnote ↩ + +Inspired by Andrew Nacin's [Simple Footnotes WordPress plugin](http://wordpress.org/plugins/simple-footnotes/). + +Requirements +============ + +Needs html5lib, so you'll want to `pip install html5lib` before running. + +Should work with any content format (ReST, Markdown, whatever), because +it looks for the `[ref]` and `[/ref]` once the conversion to HTML has happened. + +Stuart Langridge, http://www.kryogenix.org/, February 2014. diff --git a/simple_footnotes/__init__.py b/simple_footnotes/__init__.py new file mode 100644 index 0000000..2f958a8 --- /dev/null +++ b/simple_footnotes/__init__.py @@ -0,0 +1 @@ +from .simple_footnotes import * diff --git a/simple_footnotes/simple_footnotes.py b/simple_footnotes/simple_footnotes.py new file mode 100644 index 0000000..ea2e480 --- /dev/null +++ b/simple_footnotes/simple_footnotes.py @@ -0,0 +1,91 @@ +from pelican import signals +import re +import html5lib + +RAW_FOOTNOTE_CONTAINERS = ["code"] + +def getText(node, recursive = False): + """Get all the text associated with this node. + With recursive == True, all text from child nodes is retrieved.""" + L = [''] + for n in node.childNodes: + if n.nodeType in (node.TEXT_NODE, node.CDATA_SECTION_NODE): + L.append(n.data) + else: + if not recursive: + return None + L.append(getText(n) ) + return ''.join(L) + +def parse_for_footnotes(article_generator): + for article in article_generator.articles: + if "[ref]" in article._content and "[/ref]" in article._content: + content = article._content.replace("[ref]", "").replace("[/ref]", "") + parser = html5lib.HTMLParser(tree=html5lib.getTreeBuilder("dom")) + dom = parser.parse(content) + endnotes = [] + count = 0 + for footnote in dom.getElementsByTagName("x-simple-footnote"): + pn = footnote + leavealone = False + while pn: + if pn.nodeName in RAW_FOOTNOTE_CONTAINERS: + leavealone = True + break + pn = pn.parentNode + if leavealone: + continue + count += 1 + fnid = "sf-%s-%s" % (article.slug, count) + fnbackid = "%s-back" % (fnid,) + endnotes.append((footnote, fnid, fnbackid)) + number = dom.createElement("sup") + number.setAttribute("id", fnbackid) + numbera = dom.createElement("a") + numbera.setAttribute("href", "#%s" % fnid) + numbera.setAttribute("class", "simple-footnote") + numbera.appendChild(dom.createTextNode(str(count))) + txt = getText(footnote, recursive=True).replace("\n", " ") + numbera.setAttribute("title", txt) + number.appendChild(numbera) + footnote.parentNode.insertBefore(number, footnote) + if endnotes: + ol = dom.createElement("ol") + ol.setAttribute("class", "simple-footnotes") + for e, fnid, fnbackid in endnotes: + li = dom.createElement("li") + li.setAttribute("id", fnid) + while e.firstChild: + li.appendChild(e.firstChild) + backlink = dom.createElement("a") + backlink.setAttribute("href", "#%s" % fnbackid) + backlink.setAttribute("class", "simple-footnote-back") + backlink.appendChild(dom.createTextNode(u'\u21a9')) + li.appendChild(dom.createTextNode(" ")) + li.appendChild(backlink) + ol.appendChild(li) + e.parentNode.removeChild(e) + dom.getElementsByTagName("body")[0].appendChild(ol) + s = html5lib.serializer.htmlserializer.HTMLSerializer(omit_optional_tags=False, quote_attr_values=True) + output_generator = s.serialize(html5lib.treewalkers.getTreeWalker("dom")(dom.getElementsByTagName("body")[0])) + article._content = "".join(list(output_generator)).replace( + "", "[ref]").replace("", "[/ref]").replace( + "", "").replace("", "") + if False: + count = 0 + endnotes = [] + for f in footnotes: + count += 1 + fnstr = '%s' % ( + article.slug, count, article.slug, count, count) + endstr = '
  • %s
  • ' % ( + article.slug, count, f[len("[ref]"):-len("[/ref]")], article.slug, count) + content = content.replace(f, fnstr) + endnotes.append(endstr) + content += '

    Footnotes

      %s' % ("\n".join(endnotes),) + article._content = content + + +def register(): + signals.article_generator_finalized.connect(parse_for_footnotes) + diff --git a/simple_footnotes/test_simple_footnotes.py b/simple_footnotes/test_simple_footnotes.py new file mode 100644 index 0000000..04af1dc --- /dev/null +++ b/simple_footnotes/test_simple_footnotes.py @@ -0,0 +1,33 @@ +import unittest +from simple_footnotes import parse_for_footnotes + +class PseudoArticleGenerator(object): + articles = [] +class PseudoArticle(object): + _content = "" + slug = "article" + +class TestFootnotes(unittest.TestCase): + + def _expect(self, input, expected_output): + ag = PseudoArticleGenerator() + art = PseudoArticle() + art._content = input + ag.articles = [art] + parse_for_footnotes(ag) + self.assertEqual(art._content, expected_output) + + def test_simple(self): + self._expect("words[ref]footnote[/ref]end", + ('words1end' + '
        ' + u'
      1. footnote \u21a9
      2. ' + '
      ')) + + def test_no_footnote_inside_code(self): + self._expect("wordsthis is code[ref]footnote[/ref] end code end", + "wordsthis is code[ref]footnote[/ref] end code end") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file