diff --git a/complex_footnotes/README.md b/complex_footnotes/README.md new file mode 100644 index 0000000..d4105cc --- /dev/null +++ b/complex_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/complex_footnotes/__init__.py b/complex_footnotes/__init__.py new file mode 100644 index 0000000..2f958a8 --- /dev/null +++ b/complex_footnotes/__init__.py @@ -0,0 +1 @@ +from .simple_footnotes import * diff --git a/complex_footnotes/simple_footnotes.py b/complex_footnotes/simple_footnotes.py new file mode 100644 index 0000000..5de48b7 --- /dev/null +++ b/complex_footnotes/simple_footnotes.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- # + +from pelican import signals +import html5lib +import six + +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 = [u''] + 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 u''.join(L) + + +def sequence_gen(genlist): + for gen in genlist: + for elem in gen: + yield elem + + +def parse_for_footnotes(article_or_page_generator): + all_content = [ + getattr(article_or_page_generator, attr, None) \ + for attr in [u'articles', u'drafts', u'pages']] + all_content = [x for x in all_content if x is not None] + for article in sequence_gen(all_content): + if u"[ref]" in article._content and u"[/ref]" in article._content: + content = article._content.replace(u"[ref]", u"").replace(u"[/ref]", + u"") + parser = html5lib.HTMLParser(tree=html5lib.getTreeBuilder(u"dom")) + dom = parser.parse(content) + endnotes = [] + count = 0 + for footnote in dom.getElementsByTagName(u"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 = u"sf-%s-%s" % (article.slug, count) + fnbackid = u"%s-back" % (fnid,) + endnotes.append((footnote, fnid, fnbackid)) + number = dom.createElement(u"sup") + number.setAttribute(u"id", fnbackid) + + numbera = dom.createElement(u"a") + numbera.setAttribute(u"href", u"#%s" % fnid) + numbera.setAttribute(u"class", u"simple-footnote") + numbera.appendChild(dom.createTextNode(six.text_type(count))) + txt = getText(footnote, recursive=True).replace(u"\n", u" ") + + footnote_container = dom.createElement(u"span") + footnote_container.setAttribute(u"class", u"simple-footnote-container") + footnote_content = dom.createElement(u"span") + footnote_content.appendChild(footnote.firstChild) + footnote_content.setAttribute(u"class", u"simple-footnote-content") + footnote_container.appendChild(footnote_content) + print(footnote.firstChild) + + numbera.setAttribute(u"title", txt) + number.appendChild(footnote_container) + number.appendChild(numbera) + + footnote.parentNode.insertBefore(number, footnote) + footnote.parentNode.insertBefore(footnote_container, footnote) + if endnotes: + ol = dom.createElement(u"ol") + ol.setAttribute(u"class", u"simple-footnotes") + for e, fnid, fnbackid in endnotes: + # li = dom.createElement(u"li") + # li.setAttribute(u"id", fnid) + # while e.firstChild: + # li.appendChild(e.firstChild) + # backlink = dom.createElement(u"a") + # backlink.setAttribute(u"href", u"#%s" % fnbackid) + # backlink.setAttribute(u"class", u"simple-footnote-back") + # backlink.appendChild(dom.createTextNode(u'\u21a9')) + # li.appendChild(dom.createTextNode(u" ")) + # li.appendChild(backlink) + # ol.appendChild(li) + e.parentNode.removeChild(e) + dom.getElementsByTagName(u"body")[0].appendChild(ol) + s = html5lib.serializer.HTMLSerializer(omit_optional_tags=False, quote_attr_values='legacy') + output_generator = s.serialize( + html5lib.treewalkers.getTreeWalker(u"dom")(dom.getElementsByTagName(u"body")[0])) + article._content = u"".join(list(output_generator)).replace( + u"", u"[ref]").replace(u"", u"[/ref]").replace( + u"", u"").replace(u"", u"") + + +def register(): + signals.article_generator_finalized.connect(parse_for_footnotes) + signals.page_generator_finalized.connect(parse_for_footnotes) diff --git a/complex_footnotes/test_simple_footnotes.py b/complex_footnotes/test_simple_footnotes.py new file mode 100644 index 0000000..04af1dc --- /dev/null +++ b/complex_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