[Initial import of Linko, a link inserter Christian Neukirchen **20050625094602] { addfile ./linko.rb hunk ./linko.rb 1 +# +# Linko is a simple filter to ease linking for common words. +# +# Linko::WORDS holds a Hash that maps words to URLs, feel free +# to +replace+ it of you want, or just add your common links. +# +# Each occurence of myword_ will be translated into a +# footnote-style link. Each occurence of myword__ will +# be translated into a link with the word as text. +# +# Linko uses the standard *Cloth API, that is: +# +# Linko.new(mytext).to_html +# +# Linko tries to be minimal invasive and will not do link replacement +# inside tags like +pre+, +code+ and +script+. +# + +class Linko < String + # Word -> HTML mapping for Linko substitution. + WORDS = { + 'Ruby' => 'http://www.ruby-lang.org/', + 'Anarchaia' => 'http://chneukirchen.org/anarchaia/', + 'chris blogs' => 'http://chneukirchen.org/blog/', + } + + # Substitute Linko tags in the string, return HTML with proper links. + def to_html + result = "" + apply = true + + footnotes = assign_footnotes + + tokenize { |kind, text| + if kind == :tag + result << text + # Don't mess around in scripts and code. + if text =~ %r!<(/?)(?:pre|code|kbd|script|math)[\s>]! + apply = ($1 == "/") # Opening or closing tag? + end + else + apply && WORDS.each { |word, url| + text.gsub!(/#{Regexp.quote word}__/) { + %Q{#{word}} + } + text.gsub!(/#{Regexp.quote word}_/) { + %Q{#{word}#{footnotes[word]}} + } + } + result << text + end + } + result + end + + private + + def assign_footnotes + footnotes = {} + WORDS.keys.find_all { |word| index word }. # What words do appear? + sort_by { |word| index word }. # Where do they appear? + each_with_index { |word, i| + footnotes[word] = i + 1 # Assign a sequential number. + } + footnotes + end + + TAG_SOUP = /\G([^<]*)(<[^>]*>)/ + + # Small, little and probably horrible tag_soup-style parser. + def tokenize(&block) + + tokens = [] + + prev_end = 0 + scan(TAG_SOUP) { + block.call(:text, $1) if $1 != "" + block.call(:tag, $2) + + prev_end = $~.end(0) + } + + if prev_end < size + block.call(:text, self[prev_end..-1]) # Flush rest. + end + + self + end +end addfile ./test_linko.rb hunk ./test_linko.rb 1 +require 'test/unit' +require 'linko' + +Linko::WORDS.replace({ + 'Ruby' => 'http://www.ruby-lang.org/', + 'foo' => 'bar', + 'quux' => 'quuux', + }) + +class TestLinko < Test::Unit::TestCase + def assert_linko(expected, string) + assert_equal expected, Linko.new(string).to_html + end + + def test_pass_thru + assert_linko "foo", "foo" + assert_linko "this is HTML", + "this is HTML" + end + + def test_single_replacement + assert_linko 'this is foo1!', 'this is foo_!' + assert_linko 'Ruby1 ' + + 'makes programming fun.', + 'Ruby_ makes programming fun.' + end + + def test_double_replacement + assert_linko 'this is foo!', 'this is foo__!' + assert_linko 'Ruby ' + + 'makes programming fun.', + 'Ruby__ makes programming fun.' + end + + def test_html_safety + assert_linko 'this is foo1!', + 'this is foo_!' + assert_linko 'this is foo1!', + 'this is foo_!' + assert_linko 'this is bar', + 'this is bar' + + assert_linko 'this is !', + 'this is !' + end + + def test_fn_numbering + assert_linko 'foo1 ' << + 'Ruby2 ' << + 'Ruby2 ' << + 'foo1 ' << + 'foo1 ' << + 'Ruby2 ' << + 'quux3', + "foo_ Ruby_ Ruby_ foo_ foo_ Ruby_ quux_" + end +end }