Skip to content

Commit

Permalink
Moved in new stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
jarrett committed Oct 27, 2012
1 parent 721098a commit 9f8f474
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Rakefile
@@ -0,0 +1,5 @@
require 'rake/testtask'

Rake::TestTask.new do |t|
t.test_files = FileList['test/*_test.rb']
end
40 changes: 40 additions & 0 deletions lib/rbbcode.rb
@@ -0,0 +1,40 @@
# Uncomment this when developing:
#$:.unshift './lib'

require 'erb'
require 'rubygems'
require 'treetop'
require 'rbbcode/node_extensions'

class RbbCode
def self.parser_class
if !@grammar_loaded
Treetop.load_from_string(
ERB.new(
File.read(
File.join(
File.dirname(__FILE__),
'rbbcode/rbbcode_grammar.treetop'
)
)
).result
)
@grammar_loaded = true
end
RbbCodeGrammarParser
end

def initialize(options = {})
@options = options
end

def convert(bb_code)
html = self.class.parser_class.new.parse("\n\n" + bb_code + "\n\n").to_html
if @options[:emoticons]
@options[:emoticons].each do |emoticon, url|
html.gsub!(emoticon, '<img src="' + url + '" alt="Emoticon"/>')
end
end
html
end
end
126 changes: 126 additions & 0 deletions lib/rbbcode/node_extensions.rb
@@ -0,0 +1,126 @@
class RbbCode
module RecursiveConversion
def recursively_convert(node, depth = 0)
if node.terminal?
if node.respond_to?(:to_html)
node.to_html
else
node.text_value.match(/^[\r\n\t]+$/) ? '' : node.text_value
end
else
if node.respond_to?(:to_html)
node.to_html
else
node.elements.collect do |sub_node|
recursively_convert(sub_node, depth + 1)
end.join
end
end
end
end

module DocumentNode
def to_html
contents.elements.collect { |p| p.to_html }.join
end
end

module ParagraphNode
include RecursiveConversion

def to_html
html = elements.collect do |node|
recursively_convert(node)
end.join
"\n<p>" + html + "</p>\n"
end
end

module BlockquoteNode
include RecursiveConversion

def to_html
"\n<blockquote>" + recursively_convert(contents) + "</blockquote>\n"
end
end

module ListNode
include RecursiveConversion

def to_html
"\n<ul>" + recursively_convert(contents) + "</ul>\n"
end
end

module ListItemNode
include RecursiveConversion

def to_html
"\n<li>" + recursively_convert(contents) + "</li>\n"
end
end

module URLTagNode
def url_to_html
if respond_to?(:url) and respond_to?(:text)
# A URL tag formatted like [url=http://example.com]Example[/url]
'<a href="' + url.text_value + '">' + text.text_value + '</a>'
else
# A URL tag formatted like [url]http://example.com[/url]
'<a href="' + inner_bbcode + '">' + inner_bbcode + '</a>'
end
end
end

module ImgTagNode
def img_to_html
'<img src="' + inner_bbcode + '" alt="Image"/>'
end
end

module TagNode
include RecursiveConversion

TAG_MAPPINGS = {'b' => 'strong', 'i' => 'em', 'u' => 'u', 'url' => URLTagNode, 'img' => ImgTagNode}

def contents
# The first element is the opening tag, the second is everything inside,
# and the third is the closing tag.
elements[1]
end

def tag_name
elements.first.text_value.slice(1..-2).downcase
end

def inner_bbcode
contents.elements.collect { |e| e.text_value }.join
end

def inner_html
contents.elements.collect do |node|
recursively_convert(node)
end.join
end

def to_html
t = TAG_MAPPINGS[tag_name]
if t.nil?
raise "No tag mapping found for #{tag_name}"
elsif t.is_a?(Module)
extend(t)
send(tag_name + '_to_html')
# Thus, if our tag_name is"url, and TAG_MAPPINGS points us to URLTagNode,
# that module must define url_to_html.
else
"<#{t}>" + inner_html + "</#{t}>"
end
end
end

module SingleBreakNode
def to_html
'<br/>'
end
end
end
113 changes: 113 additions & 0 deletions lib/rbbcode/rbbcode_grammar.treetop
@@ -0,0 +1,113 @@
<%
def def_tag(rule_name, tag_name)
"
rule #{rule_name}
('[#{tag_name.downcase}]'/'[#{tag_name.upcase}]')
(!'[/#{tag_name.downcase}]' !'[/#{tag_name.upcase}]'
(tag <RbbCode::TagNode> / .))+
('[/#{tag_name.downcase}]' / '[/#{tag_name.upcase}]')
end
"
end
%>

grammar RbbCodeGrammar
rule document
# Consume the trailing linebreaks, because the paragraph lookahead
# doesn't consume them.
contents:(blockquote <RbbCode::BlockquoteNode> / list <RbbCode::ListNode> / paragraph <RbbCode::ParagraphNode>)* break_ws* <RbbCode::DocumentNode>
end

rule paragraph
(break_ws 2..)
(
!(break_ws 2..)
paragraph_contents
)+
end

rule paragraph_contents
(tag <RbbCode::TagNode> / single_break_ws / .)
end

rule break_ws
# Allow whitespace around the linebreaks
[ \t]* [\r\n] [ \t]*
end

rule whitespace
# Any whitespace, including linebreaks
[ \t\r\n]
end

rule single_break_ws
# We don't count linebreaks when they're immediately followed by
# certain keywords. This avoids printing an extra <br/> in some cases.
break_ws !break_ws !(break_ws* ('[/quote]' / '[*]' / '[/list]')) <RbbCode::SingleBreakNode>
end

rule blockquote
break_ws*
'[quote]'
contents:(
# Possible linebreaks after opening quote tag
break_ws*

# First paragraph (mandatory)
(blockquote_paragraph <RbbCode::ParagraphNode>)

# Subsequent paragraphs (optional)
(
(break_ws 2..)
(blockquote_paragraph <RbbCode::ParagraphNode>)
)*

# Possible linebreaks before closing quote tag
break_ws*
)
'[/quote]'
end

rule blockquote_paragraph
(!('[/quote]' / (break_ws 2..)) paragraph_contents)+
end

rule list
break_ws*
'[list]'
contents:(
# Possible linebreaks after opening list tag
whitespace*

# At least one list item
(
(
'[*]'
contents:(!'[/list]' !'[*]' paragraph_contents)*
<RbbCode::ListItemNode>
)
)+

# Possible linebreaks before closing list tag
whitespace*
)
'[/list]'
end

rule tag
# Make sure that anytime you call def_tag, you add it to this list:
bold / italic / underline / simple_url / complex_url / img
end

<%= def_tag 'bold', 'b' %>
<%= def_tag 'italic', 'i' %>
<%= def_tag 'underline', 'u' %>
<%= def_tag 'simple_url', 'url' %>
<%= def_tag 'img', 'img' %>

rule complex_url
'[url=' url:[^\]]+ ']'
text:(!'[/url]' .)+
'[/url]'
end
end
35 changes: 35 additions & 0 deletions test/blockquotes_test.rb
@@ -0,0 +1,35 @@
require File.join(File.expand_path(File.dirname(__FILE__)), 'test_helper.rb')

class TestBlockquotes < Test::Unit::TestCase
include RbbCode::HTMLAssertions

def test_blockquotes
0.upto(3) do |pre_breaks|
0.upto(3).each do |post_breaks|
bb_code = '[quote]' + ("\n" * pre_breaks) + 'Quoth the raven' + ("\n" * post_breaks) + '[/quote]'
assert_converts_to(
'<blockquote><p>Quoth the raven</p></blockquote>',
bb_code,
{},
"#{bb_code} did not convert properly"
)
end
end

0.upto(3) do |pre_breaks|
0.upto(3).each do |post_breaks|
bb_code = '[quote]' + ("\n" * pre_breaks) + "Quoth\n\nthe\n\nraven" + ("\n" * post_breaks) + '[/quote]'
assert_converts_to(
'<blockquote>
<p>Quoth</p>
<p>the</p>
<p>raven</p>
</blockquote>',
bb_code,
{},
"#{bb_code} did not convert properly"
)
end
end
end
end
61 changes: 61 additions & 0 deletions test/doc_parsing_test.rb
@@ -0,0 +1,61 @@
require File.join(File.expand_path(File.dirname(__FILE__)), 'test_helper.rb')

class TestDocParsing < Test::Unit::TestCase
include RbbCode::HTMLAssertions

def test_parsing_whole_document
assert_converts_to(
# Expected HTML:
'
<p><strong>This <em>is</em> awesome</strong></p>
<p>Another paragraph.</p>
<p>A paragraph with<br/>a line break.</p>
<p>Here\'s one kind of link: <a href="http://example.org">http://example.org</a></p>
<p>Here\'s another: <a href="http://example.com">Example</a></p>
<p>This is an image: <img src="http://example.com/foo.png" alt="Image"/></p>
<blockquote>
<p>This <em>is</em> a quotation.</p>
<p>Yes it is.</p>
</blockquote>
<ul>
<li>Entry 1</li>
<li>Entry 2</li>
<li>Entry 3</li>
</ul>
<p>Smile: <img src="/happy.png" alt="Emoticon"/> Frown: <img src="/sad.png" alt="Emoticon"/></p>
',
# BBCode:
'
[b]This [i]is[/i] awesome[/b]
Another paragraph.
A paragraph with
a line break.
Here\'s one kind of link: [url]http://example.org[/url]
Here\'s another: [url=http://example.com]Example[/url]
This is an image: [img]http://example.com/foo.png[/img]
[quote]
This [i]is[/i] a quotation.
Yes it is.
[/quote]
[list] [*]Entry 1 [*]Entry 2
[*]Entry 3
[/list]
Smile: :) Frown: :(
',
# RbbCode options:
:emoticons => {':)' => '/happy.png', ':(' => '/sad.png'}
)
end
end

0 comments on commit 9f8f474

Please sign in to comment.