Patching RedCloth: unique footnote hyperlinks
Recently, I integrated Textile into this blog via RedCloth. As I started to write articles in my Adventures in RSpec series, I found I wanted to use footnotes. Since Textile supports them, I figured I was in luck; however, I soon found a hyperlink collision when two fragments of Textile markup appear on the same page, each with the same-numbered footnote. RedCloth simply generates a hyperlink to target fn1 for footnote #1, so if there are two footnote #1s on the same page, they both link to target fn1, making it the equivalent of a race condition.
I looked for guidance to the Textpattern folks. A certain Mary told me that Textile has fixed this problem, but likely RedCloth has not. After some investigation, I agreed that this was the case. I looked through the Textile source, to the extent I can understand PHP, and saw their fix was simple: keep a table of footnote numbers to unique IDs, then use the unique ID in place of the number in the hyperlink and target. I figured that I could do that.
It took a couple of hours, and I’ll spare you the details. Here is what I wrote:
require 'digest/sha1'
class UniqueFootnoteIdGeneratingRedCloth < RedCloth
def initialize(markup)
@footnote_ids_by_number = {}
super(markup)
end
def next_footnote_id
# SHA1 isn't significant; I just figured it'd make a good unique ID
Digest::SHA1.hexdigest(rand(2**64).to_s)
end
def to_html(*rules)
self.scan( /\b\[([0-9]+?)\](\s)?/ ) do |match|
@footnote_ids_by_number[$1] = next_footnote_id
end
super
end
private
def footnote_ref(text)
text.gsub!( /\b\[([0-9]+?)\](\s)?/ ) do |match|
"<sup><a href=\"#fn#{@footnote_ids_by_number[$1]}\">#{$1}</a></sup>#{$2}"
end
end
def textile_fn_( tag, num, atts, cite, content )
atts << " id=\"fn#{ @footnote_ids_by_number[num] }\""
content = "<sup>#{ num }</sup> #{ content }"
atts = shelve( atts ) if atts
"\t<p#{ atts }>#{ content }</p>"
end
end
I ended up copy/pasting more code than I wanted to, so I’ll have to submit something to why for his perusal. Perhaps this will make it into a future version of RedCloth.