Simple Footnotes plugin. Add footnotes to your text by enclosing them in [ref] and [/ref].
This commit is contained in:
26
simple_footnotes/README.md
Normal file
26
simple_footnotes/README.md
Normal file
@@ -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 text<sup>1</sup>
|
||||||
|
|
||||||
|
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.
|
||||||
1
simple_footnotes/__init__.py
Normal file
1
simple_footnotes/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .simple_footnotes import *
|
||||||
91
simple_footnotes/simple_footnotes.py
Normal file
91
simple_footnotes/simple_footnotes.py
Normal file
@@ -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]", "<x-simple-footnote>").replace("[/ref]", "</x-simple-footnote>")
|
||||||
|
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(
|
||||||
|
"<x-simple-footnote>", "[ref]").replace("</x-simple-footnote>", "[/ref]").replace(
|
||||||
|
"<body>", "").replace("</body>", "")
|
||||||
|
if False:
|
||||||
|
count = 0
|
||||||
|
endnotes = []
|
||||||
|
for f in footnotes:
|
||||||
|
count += 1
|
||||||
|
fnstr = '<a class="simple-footnote" name="%s-%s-back" href="#%s-%s"><sup>%s</a>' % (
|
||||||
|
article.slug, count, article.slug, count, count)
|
||||||
|
endstr = '<li id="%s-%s">%s <a href="#%s-%s-back">↑</a></li>' % (
|
||||||
|
article.slug, count, f[len("[ref]"):-len("[/ref]")], article.slug, count)
|
||||||
|
content = content.replace(f, fnstr)
|
||||||
|
endnotes.append(endstr)
|
||||||
|
content += '<h4>Footnotes</h4><ol class="simple-footnotes">%s</ul>' % ("\n".join(endnotes),)
|
||||||
|
article._content = content
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.article_generator_finalized.connect(parse_for_footnotes)
|
||||||
|
|
||||||
33
simple_footnotes/test_simple_footnotes.py
Normal file
33
simple_footnotes/test_simple_footnotes.py
Normal file
@@ -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",
|
||||||
|
('words<sup id="sf-article-1-back"><a title="footnote" '
|
||||||
|
'href="#sf-article-1" class="simple-footnote">1</a></sup>end'
|
||||||
|
'<ol class="simple-footnotes">'
|
||||||
|
u'<li id="sf-article-1">footnote <a href="#sf-article-1-back" class="simple-footnote-back">\u21a9</a></li>'
|
||||||
|
'</ol>'))
|
||||||
|
|
||||||
|
def test_no_footnote_inside_code(self):
|
||||||
|
self._expect("words<code>this is code[ref]footnote[/ref] end code </code> end",
|
||||||
|
"words<code>this is code[ref]footnote[/ref] end code </code> end")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user